이제 탑을 오르고, 떨어지고, 다시 오를 수 있습니다. 그런데 게임을 끄면 내가 몇 층까지 올랐는지가 전부 사라지고, 다시 들어오면 아무것도 남아 있지 않습니다. 완성본처럼 층 글자 아래에 지금까지 오른 가장 높은 층을 띄우고, 게임을 껐다 켜도 그 숫자가 남아 있게 만들겠습니다.

데이터 스토리지
게임을 꺼도 남아 있으려면 어딘가에 적어 둬야 합니다. 메이플스토리월드에는 이럴 때 쓰는 저장소가 있고, 이름은 데이터 스토리지입니다. 여기에 적은 값은 게임을 껐다 켜도 남아 있습니다.
데이터 스토리지는 서버만 쓸 수 있습니다. 클라이언트가 직접 적을 수 있다면 누구나 기록을 마음대로 고칠 수 있기 때문입니다. 그래서 기록을 적는 일도, 읽어 오는 일도 서버가 합니다.
그리고 기록은 사람마다 따로 있어야 합니다. 내 기록과 친구의 기록이 섞이면 안 됩니다. 그래서 사람마다 따로 주어지는 유저 데이터 스토리지를 씁니다. 저장소는 게임 서버 밖에 있고, 서버가 그 사람의 칸을 읽어 그 사람에게 보내 줍니다. 클라이언트가 저장소에서 직접 받아 오지는 않습니다.

순서는 이렇습니다.
- 최고 기록을 기억하는 스크립트를 만듭니다.
- 그 기록을 화면에 띄웁니다.
- 기록을 저장소에 저장합니다.
- 다시 들어왔을 때 저장소에서 불러옵니다.
TowerRecord, 최고 기록 기억하기
최고 기록도 사람마다 따로 있어야 하니, 6화의 TowerClimber 처럼 플레이어에 붙이는 컴포넌트로 만듭니다. TowerClimber 에 적지 않는 이유는 하는 일이 다르기 때문입니다. TowerClimber 는 탑을 오르고 떨어지는 일을 맡고, 기록을 남기고 저장하고 불러오는 것은 다른 일입니다. 그래서 기록만 맡는 스크립트를 따로 만듭니다.

- MyDesk 에서 새 컴포넌트를 만들고 이름을 TowerRecord 로 정합니다.
- Property 를 하나 더합니다. 이름은 bestFloor, 타입은 integer, 처음 값은 0 입니다. Sync 는 그대로 둡니다. 다음 단계에서 화면에 띄울 값이라 서버의 값이 클라이언트로 가야 합니다.
- New 로 함수 Reached 를 만들고 integer 매개변수 reachedFloor 를 더합니다. 기록을 정하는 것도 서버가 하니 실행 공간은 ServerOnly 입니다.
Reached 안에서는 닿은 층이 지금 기록보다 높지 않으면 아무것도 하지 않고 끝내고, 높으면 bestFloor 를 그 층으로 바꿉니다. 6화의 ClimbTo 와 같은 모양으로, 더 높을 때만 값을 바꿉니다.
- Workspace 에서 DefaultPlayer 를 고르고 Add Component 로 TowerRecord 를 붙입니다.
- 위층에 올라가는 순간은 TowerClimber 의 ClimbTo 가 알고 있습니다. ClimbTo 에서 floor 를 바꾼 바로 뒤에 GetComponent 로 TowerRecord 를 꺼내 Reached 를 부르고, floor 에 1 을 더해 넘깁니다. floor 는 0 부터 세니 1 을 더해야 화면의 층 수와 같아집니다.
@Component
script TowerClimber extends Component
@Sync
property integer floor = 0
property boolean isTeleporting = false
property Vector3 teleportTarget = Vector3(0, 0, 0)
property number teleportSeconds = 0
@ExecSpace("ServerOnly")
method void ClimbTo(integer nextFloor)
if self.isTeleporting or nextFloor <= self.floor then
return
end
self.floor = nextFloor
---@type TowerRecord
local record = self.Entity:GetComponent("script.TowerRecord")
record:Reached(nextFloor + 1)
log("floor " .. self.floor)
self:TeleportTo(Vector3(0, nextFloor * 4 + 0.5, 0))
end
@ExecSpace("ClientOnly")
method void OnSyncProperty(string name, any value)
if name == "floor" then
log("floor " .. value)
end
end
@ExecSpace("ServerOnly")
method void OnUpdate(number delta)
if self.isTeleporting then
self.teleportSeconds += delta
local distance = self.Entity.TransformComponent.WorldPosition:Distance(self.teleportTarget)
if distance < 1 or self.teleportSeconds > 1 then
self.isTeleporting = false
end
return
end
local y = self.Entity.TransformComponent.WorldPosition.y
if y < self.floor * 4 - 1 then
self:GameOver()
end
end
@ExecSpace("ServerOnly")
method void GameOver()
self:ShowGameOver(self.floor + 1, self.Entity.PlayerComponent.UserId)
self.floor = 0
log("게임오버")
self:TeleportTo(Vector3(0, 0.5, 0))
end
@ExecSpace("ServerOnly")
method void TeleportTo(Vector3 target)
self.isTeleporting = true
self.teleportTarget = target
self.teleportSeconds = 0
_TeleportService:TeleportToMapPosition(self.Entity, target, "map01")
end
@ExecSpace("Client")
method void ShowGameOver(integer reachedFloor)
_TowerHud:ShowGameOver(reachedFloor)
end
end실행해서 2층에 오르면 최고 기록 2, 3층에 오르면 최고 기록 3 이 찍힙니다. 떨어져서 다시 2층에 올라도 이번에는 찍히지 않습니다. 2층은 지금 기록인 3층보다 높지 않기 때문입니다. 그런데 실행을 멈췄다가 다시 실행하면 2층에 오르자마자 최고 기록 2 가 다시 찍힙니다. 지금 기록은 실행하는 동안만 기억하고 있기 때문입니다.

화면에 최고 기록 띄우기
8화에서 만든 층 글자 바로 아래에 한 줄을 더 놓습니다.
- UI 편집 화면에서 DefaultGroup 의 FloorText 를 고르고, 오른쪽 클릭해서 Duplicate 를 누릅니다. 위치와 앵커, 외곽선 같은 설정이 그대로 따라옵니다. 이름은 BestText 로 바꿉니다.
- PosX 는 0 으로 층 글자와 같은 가운데에 맞추고, PosY 는 -150 으로 바꿉니다.
- 글자 크기는 32, 색은 노란색으로 합니다. 색상 코드는 FFD84A 입니다. 글자 칸에는 최고 0층 을 넣어 둡니다.
- TowerHud 에 Property 를 하나 더 만듭니다. 타입은 Entity, 이름은 bestText, Sync 는 None 이고, Reference 창에서 BestText 를 넣습니다.
- OnUpdate 에서 플레이어의 TowerRecord 를 GetComponent 로 꺼내 bestFloor 를 최고 몇 층이라는 글자로 바꿔 넣습니다. bestFloor 는 Sync 라서 서버가 바꾼 값이 내 화면에 오니, 클라이언트의 OnUpdate 에서 바로 읽을 수 있습니다.
그런데 처음 들어왔을 때 최고 0층 이라고 나옵니다. 0층이라는 층은 없으니 어색합니다. 그래서 bestFloor 가 0 이면 최고 기록 없음 이라고 넣고, 아니면 최고 몇 층을 넣습니다.
@Logic
script TowerHud extends Logic
property Entity floorText = "5e1d9b4c-7048-4b56-ae66-2485c7b9fe1e"
property Entity gameOverPanel = "02f1a3e8-16f5-41e7-89c6-47d80bb613ce"
property Entity reachedText = "714de251-b3d8-427b-80a2-b54a0ccf6e1f"
property Entity bestText = "72615eeb-1f3d-4098-8497-0fdc9aae7be4"
@ExecSpace("ClientOnly")
method void OnUpdate(number delta)
local player = _UserService.LocalPlayer
---@type TowerClimber
local climber = player:GetComponent("script.TowerClimber")
self.floorText.TextComponent.Text = "탑 " .. (climber.floor + 1) .. "층"
---@type TowerRecord
local record = player:GetComponent("script.TowerRecord")
if record.bestFloor == 0 then
self.bestText.TextComponent.Text = "최고 기록 없음"
else
self.bestText.TextComponent.Text = "최고 " .. record.bestFloor .. "층"
end
end
@ExecSpace("ClientOnly")
method void OnBeginPlay()
self.gameOverPanel.Enable = false
end
@ExecSpace("ClientOnly")
method void ShowGameOver(integer reachedFloor)
self.reachedText.TextComponent.Text = "탑 " .. reachedFloor .. "층까지 올랐습니다"
self.gameOverPanel.Enable = true
_TimerService:SetTimerOnce(function()
self.gameOverPanel.Enable = false
end, 2.5)
end
end저장하기, SetAsync
저장은 기록이 바뀌는 순간에만 하면 됩니다. Reached 에서 bestFloor 를 바꾼 바로 뒤입니다.
매 프레임 저장하면 안 됩니다. 저장소는 쓸 때마다 크레딧이라는 몫을 씁니다. 크레딧은 1분마다 채워지는 사용 한도이고, 한 월드가 함께 쓰며, 다 쓰면 저장과 불러오기가 실패합니다. Reached 의 첫 줄 덕분에 더 높은 층에 올랐을 때만 여기까지 오니, 꼭 필요한 만큼만 저장하게 됩니다.

- _DataStorageService 의 GetUserDataStorage 에 플레이어의 ProfileCode 를 넘겨 이 사람의 저장소를 꺼냅니다. ProfileCode 는 사람마다 다른 번호라서 사람마다 자기 칸을 받습니다.
- SetAsync 로 저장합니다. 첫 번째는 이름표인 키 TowerBest, 두 번째는 저장할 값입니다. 저장소에는 글자만 넣을 수 있어서 tostring 으로 숫자를 글자로 바꿉니다. 세 번째는 저장이 끝나면 불리는 함수입니다.
- Async 는 기다리지 않고 바로 다음 줄로 넘어간다는 뜻입니다. 저장이 끝나면 세 번째 함수가 결과를 알려 주고, errorCode 가 0 이면 잘 저장된 것입니다.
실행해서 2층에 오르면 최고 기록 2 다음에 저장 0 이 찍힙니다. 하지만 다시 실행하면 여전히 최고 기록 없음 으로 시작합니다. 저장은 했지만 아직 불러오는 코드가 없기 때문입니다.
불러오기, GetAndWait
불러오는 건 플레이어가 들어오는 순간 한 번이면 됩니다. TowerRecord 에 Event Method 의 OnBeginPlay 를 더하고, 저장소는 서버만 쓰니 실행 공간은 server only 로 둡니다.
- 저장할 때처럼 먼저 이 사람의 저장소를 꺼냅니다.
- GetAndWait 로 키 TowerBest 를 읽습니다. AndWait 는 값이 올 때까지 기다렸다가 다음 줄로 간다는 뜻이고, 결과는 errorCode 와 저장해 둔 값 두 개가 옵니다.
- errorCode 가 0 이 아니면 그냥 돌아가고, 아니면 tonumber 로 글자를 숫자로 바꿔 bestFloor 에 넣습니다.
그런데 저장소를 비워 둔 채, 처음 들어온 사람처럼 실행하면 Console 에 빨간 오류가 계속 찍힙니다. bestFloor 가 nil 이라고 하고, 화면 글자도 최고 0층 에서 멈춥니다.
처음 들어온 사람은 저장해 둔 값이 없습니다. 이때 GetAndWait 는 오류를 주지 않습니다. errorCode 는 0 이고 값만 비어 있습니다. 비어 있는 값을 tonumber 하면 그대로 nil 이 되고, bestFloor 가 nil 이 되어 쓰는 곳마다 오류가 납니다.

고치는 건 한 줄입니다. 7화에서 본 or 를 써서, errorCode 가 0 이 아니거나 값이 비어 있으면 돌아갑니다. 처음 들어온 사람은 bestFloor 가 그대로 0 이라서 최고 기록 없음 이 보입니다.
@Component
script TowerRecord extends Component
@Sync
property integer bestFloor = 0
@ExecSpace("ServerOnly")
method void Reached(integer reachedFloor)
if reachedFloor <= self.bestFloor then
return
end
self.bestFloor = reachedFloor
log("최고 기록 " .. self.bestFloor)
local storage = _DataStorageService:GetUserDataStorage(self.Entity.PlayerComponent.ProfileCode)
storage:SetAsync("TowerBest", tostring(self.bestFloor), function(errorCode, key)
log("저장 " .. errorCode)
end)
end
@ExecSpace("ServerOnly")
method void OnBeginPlay()
local storage = _DataStorageService:GetUserDataStorage(self.Entity.PlayerComponent.ProfileCode)
local errorCode, saved = storage:GetAndWait("TowerBest")
if errorCode ~= 0 or saved == nil then
return
end
self.bestFloor = tonumber(saved)
log("불러옴 " .. self.bestFloor)
end
end다시 실행하면 오류 없이 최고 기록 없음 으로 시작합니다. 3층까지 올라 기록을 저장해 두고, 실행을 멈췄다가 다시 실행하면 들어오자마자 불러옴 3 이 찍히고 화면에도 최고 3층 이 그대로 남아 있습니다. 게임을 껐다 켜도 기록이 남은 것입니다.
정리
- 하는 일이 다르면 스크립트를 따로 만듭니다.
- 데이터 스토리지는 서버만 쓰고, 사람마다 자기 칸이 있습니다.
- 저장은 SetAsync 로, 기록이 바뀔 때만 합니다.
- 불러오기는 GetAndWait 로, 들어올 때 한 번 합니다.
- 처음 들어온 사람은 값이 비어 있으니 nil 을 꼭 확인합니다.
다음 글에서는 탑에 난이도를 넣습니다. 높이 올라갈수록 짧은 발판이 늘어나고 틈은 넓어집니다.
