이 가이드는 PlayWave 플러그인 없이 직접 설치하는 방법입니다. 플러그인을 사용하는 권장 방법은 빠른 시작 가이드를 참고하세요.
프로젝트 구조
ServerScriptService
Script
PlaywaveService (ModuleScript)
StarterPlayer
StarterPlayerScripts
Client (LocalScript)
RedeemUI (ModuleScript)
| 스크립트 | ClassName | 역할 |
|---|---|---|
| Script | Script | 메인 서버 로직, PlaywaveService 초기화 |
| PlaywaveService | ModuleScript | API 통신, 세션 관리, |
| Client | LocalScript | 서버에 클라이언트 준비 신호 전송 |
| RedeemUI | ModuleScript | PC방 상태 표시 UI 선택 |
PlaywaveService 모듈
핵심 모듈입니다. API 통신, OTT 검증, 하트비트 루프, 세션 종료를 처리합니다.API_URL과 API_KEY를 발급받은 값으로 변경하세요. API Key는 서버 사이드에서만 사용해야 합니다.API 서버 주소
| 환경 | URL | 상태 |
|---|---|---|
| Dev | https://wave-api.playwave.dev/v1 | 사용 가능 |
| QA | https://wave-api-qa.playwave.dev/v1 | 준비 중 |
| Live | https://wave-api.playwave.io/v1 | 준비 중 |
-- PlaywaveService (ModuleScript)
-- ServerScriptService/Script 하위에 배치
local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local API_URL = "https://wave-api.playwave.dev/v1"
local API_KEY = "your-api-key-here"
local HEARTBEAT_INTERVAL = 120 -- 2분
local activeSessions = {}
local PlaywaveService = {}
local function apiRequest(method, path, body)
local requestInfo = {
Url = API_URL .. path,
Method = method,
Headers = {
["X-Api-Key"] = API_KEY,
["Content-Type"] = "application/json",
},
}
if body then
requestInfo.Body = HttpService:JSONEncode(body)
end
local success, response = pcall(function()
return HttpService:RequestAsync(requestInfo)
end)
if not success then
warn("[Playwave] HTTP failed:", response)
return nil, "NETWORK_ERROR"
end
if response.StatusCode >= 400 then
local errorBody = nil
pcall(function() errorBody = HttpService:JSONDecode(response.Body) end)
return errorBody, "HTTP_" .. tostring(response.StatusCode)
end
local ok, decoded = pcall(function() return HttpService:JSONDecode(response.Body) end)
if not ok then
warn("[Playwave] JSON decode failed")
return nil, "DECODE_ERROR"
end
return decoded, nil
end
local function startHeartbeatLoop(userId, gameSessionId, providerUserId)
return task.spawn(function()
while activeSessions[userId] do
task.wait(HEARTBEAT_INTERVAL)
if not activeSessions[userId] then break end
local result, err = apiRequest("PATCH", "/game/session/heartbeat", {
game_session_id = gameSessionId,
provider_user_id = providerUserId,
})
if err then
if err == "HTTP_404" or err == "HTTP_410" or string.sub(err, 1, 7) == "HTTP_5" then
warn("[Playwave] Session lost for UserId", userId, ":", err)
activeSessions[userId] = nil
local player = Players:GetPlayerByUserId(userId)
if player then player:Kick("PlayWave session expired") end
break
end
warn("[Playwave] Heartbeat error:", err)
continue
end
if result and result.data and result.data.result == "CHARGE_EXHAUSTED" then
local player = Players:GetPlayerByUserId(userId)
if player then warn("[Playwave] Charge exhausted for", player.Name) end
end
end
end)
end
local function endPlayerSession(userId)
local session = activeSessions[userId]
if not session then return end
activeSessions[userId] = nil
if session.heartbeatThread then task.cancel(session.heartbeatThread) end
local clientPlayDuration = math.floor(os.clock() - session.startTime)
local _, err = apiRequest("DELETE", "/game/session/end", {
game_session_id = session.gameSessionId,
play_duration_sec = clientPlayDuration,
})
if err and err ~= "HTTP_404" and err ~= "HTTP_409" then
warn("[Playwave] End session failed for UserId", userId, ":", err)
end
end
function PlaywaveService.init(clientReadyEvent, onPcCafe)
clientReadyEvent.OnServerEvent:Connect(function(player)
local joinData = player:GetJoinData()
local ott = joinData.LaunchData
if not ott or ott == "" then
return
end
local result, err = apiRequest("POST", "/game/session/verify", {
ott = ott,
provider_user_id = tostring(player.UserId),
})
if err then
warn("[Playwave] Verify error:", err)
return
end
if result and result.success and result.data and result.data.is_valid then
local data = result.data
activeSessions[player.UserId] = {
gameSessionId = data.game_session_id,
startTime = os.clock(),
heartbeatThread = nil,
}
activeSessions[player.UserId].heartbeatThread = startHeartbeatLoop(
player.UserId,
data.game_session_id,
tostring(player.UserId)
)
if data.is_pc_cafe and onPcCafe then
onPcCafe(player)
end
else
if result and result.data then
warn("[Playwave] Verify failed:", player.Name, result.data.reason)
end
end
end)
Players.PlayerRemoving:Connect(function(player)
endPlayerSession(player.UserId)
end)
game:BindToClose(function()
for userId in pairs(activeSessions) do
endPlayerSession(userId)
end
end)
end
return PlaywaveService
서버 스크립트 (Script)
PlaywaveService를 초기화하고 게임 로직과 연결합니다.-- Script (ServerScriptService)
local PlaywaveService = require(script.PlaywaveService)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- RemoteEvents 생성
local launchDataEvent = Instance.new("RemoteEvent")
launchDataEvent.Name = "LaunchDataEvent"
launchDataEvent.Parent = ReplicatedStorage
local clientReadyEvent = Instance.new("RemoteEvent")
clientReadyEvent.Name = "ClientReadyEvent"
clientReadyEvent.Parent = ReplicatedStorage
local respawnEvent = Instance.new("RemoteEvent")
respawnEvent.Name = "RespawnEvent"
respawnEvent.Parent = ReplicatedStorage
respawnEvent.OnServerEvent:Connect(function(player)
player:LoadCharacterAsync()
end)
-- PlayWave 초기화
PlaywaveService.init(clientReadyEvent, function(player)
launchDataEvent:FireClient(player, "PC_CAFE")
end)
클라이언트 스크립트 (LocalScript)
-- Client (StarterPlayerScripts/LocalScript)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RedeemUI = require(script.RedeemUI)
local redeem = RedeemUI.new()
redeem:start()
-- 서버에 준비 완료 신호 전송
local clientReadyEvent = ReplicatedStorage:WaitForChild("ClientReadyEvent")
clientReadyEvent:FireServer()
RedeemUI 모듈 (선택)
PC방 인증 상태를 화면 오른쪽 상단에 표시하는 UI 모듈입니다.RedeemUI 전체 코드
RedeemUI 전체 코드
-- RedeemUI (ModuleScript)
-- StarterPlayerScripts/Client 하위에 배치
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local RedeemUI = {}
RedeemUI.__index = RedeemUI
function RedeemUI.new()
local self = setmetatable({}, RedeemUI)
self._player = Players.LocalPlayer
return self
end
function RedeemUI:_build()
local playerGui = self._player.PlayerGui
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "RedeemUI"
screenGui.ResetOnSpawn = false
screenGui.IgnoreGuiInset = true
screenGui.Parent = playerGui
local frame = Instance.new("Frame")
frame.Size = UDim2.fromOffset(220, 80)
frame.Position = UDim2.new(1, 20, 0, 16)
frame.AnchorPoint = Vector2.new(1, 0)
frame.BackgroundColor3 = Color3.fromRGB(15, 10, 30)
frame.BackgroundTransparency = 0.5
frame.BorderSizePixel = 0
frame.Parent = screenGui
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = frame
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, -16, 0.45, 0)
label.Position = UDim2.fromOffset(10, 8)
label.BackgroundTransparency = 1
label.Text = "REDEEM CODE"
label.TextColor3 = Color3.fromRGB(255, 255, 255)
label.TextXAlignment = Enum.TextXAlignment.Left
label.Font = Enum.Font.GothamBold
label.TextSize = 13
label.Parent = frame
local codeLabel = Instance.new("TextLabel")
codeLabel.Name = "CodeLabel"
codeLabel.Size = UDim2.new(1, -16, 0.3, 0)
codeLabel.Position = UDim2.new(0, 10, 0.45, 0)
codeLabel.BackgroundTransparency = 1
codeLabel.Text = "INACTIVE"
codeLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
codeLabel.TextXAlignment = Enum.TextXAlignment.Left
codeLabel.Font = Enum.Font.GothamBold
codeLabel.TextSize = 16
codeLabel.Parent = frame
local userLabel = Instance.new("TextLabel")
userLabel.Size = UDim2.new(1, -16, 0.25, 0)
userLabel.Position = UDim2.new(0, 10, 0.72, 0)
userLabel.BackgroundTransparency = 1
userLabel.Text = "ID: " .. tostring(self._player.UserId)
userLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
userLabel.TextXAlignment = Enum.TextXAlignment.Left
userLabel.Font = Enum.Font.Gotham
userLabel.TextSize = 13
userLabel.Parent = frame
TweenService:Create(frame, TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
Position = UDim2.new(1, -16, 0, 16),
}):Play()
return frame, label, codeLabel
end
function RedeemUI:start()
local frame, label, codeLabel = self:_build()
local event = ReplicatedStorage:WaitForChild("LaunchDataEvent", 30)
if not event then return end
event.OnClientEvent:Connect(function(code)
label.TextColor3 = Color3.fromRGB(0, 255, 200)
codeLabel.Text = code
codeLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
end)
end
return RedeemUI