h662

메이플스토리 월드 강의 / 무한 점프맵 만들기 / 10화

무한 점프맵 만들기 10화, 최고 기록 저장

2026년 10월 14일

무한 점프맵 만들기 10화, 최고 기록 저장 완성 화면

내가 오른 가장 높은 층을 최고 기록으로 남기고, 게임을 껐다 켜도 그대로 남게 만듭니다. 기록만 맡는 TowerRecord 를 만들어 화면에 최고 몇 층인지 띄우고, 데이터 스토리지에 SetAsync 로 저장한 뒤 들어올 때 GetAndWait 로 불러옵니다. 처음 들어온 사람은 오류 없이 빈 값이 온다는 함정도 직접 틀려 보고 고칩니다.

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

껐다가 다시 들어와도 그대로
껐다가 다시 들어와도 그대로

데이터 스토리지

게임을 꺼도 남아 있으려면 어딘가에 적어 둬야 합니다. 메이플스토리월드에는 이럴 때 쓰는 저장소가 있고, 이름은 데이터 스토리지입니다. 여기에 적은 값은 게임을 껐다 켜도 남아 있습니다.

데이터 스토리지는 서버만 쓸 수 있습니다. 클라이언트가 직접 적을 수 있다면 누구나 기록을 마음대로 고칠 수 있기 때문입니다. 그래서 기록을 적는 일도, 읽어 오는 일도 서버가 합니다.

그리고 기록은 사람마다 따로 있어야 합니다. 내 기록과 친구의 기록이 섞이면 안 됩니다. 그래서 사람마다 따로 주어지는 유저 데이터 스토리지를 씁니다. 저장소는 게임 서버 밖에 있고, 서버가 그 사람의 칸을 읽어 그 사람에게 보내 줍니다. 클라이언트가 저장소에서 직접 받아 오지는 않습니다.

서버가 그 사람 칸을 읽어서, 그 사람에게
서버가 그 사람 칸을 읽어서, 그 사람에게

순서는 이렇습니다.

  1. 최고 기록을 기억하는 스크립트를 만듭니다.
  2. 그 기록을 화면에 띄웁니다.
  3. 기록을 저장소에 저장합니다.
  4. 다시 들어왔을 때 저장소에서 불러옵니다.

TowerRecord, 최고 기록 기억하기

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

하는 일이 다르면, 스크립트를 나눈다
하는 일이 다르면, 스크립트를 나눈다
  1. MyDesk 에서 새 컴포넌트를 만들고 이름을 TowerRecord 로 정합니다.
  2. Property 를 하나 더합니다. 이름은 bestFloor, 타입은 integer, 처음 값은 0 입니다. Sync 는 그대로 둡니다. 다음 단계에서 화면에 띄울 값이라 서버의 값이 클라이언트로 가야 합니다.
  3. New 로 함수 Reached 를 만들고 integer 매개변수 reachedFloor 를 더합니다. 기록을 정하는 것도 서버가 하니 실행 공간은 ServerOnly 입니다.

Reached 안에서는 닿은 층이 지금 기록보다 높지 않으면 아무것도 하지 않고 끝내고, 높으면 bestFloor 를 그 층으로 바꿉니다. 6화의 ClimbTo 와 같은 모양으로, 더 높을 때만 값을 바꿉니다.

  1. Workspace 에서 DefaultPlayer 를 고르고 Add Component 로 TowerRecord 를 붙입니다.
  2. 위층에 올라가는 순간은 TowerClimber 의 ClimbTo 가 알고 있습니다. ClimbTo 에서 floor 를 바꾼 바로 뒤에 GetComponent 로 TowerRecord 를 꺼내 Reached 를 부르고, floor 에 1 을 더해 넘깁니다. floor 는 0 부터 세니 1 을 더해야 화면의 층 수와 같아집니다.
RootDesk/MyDesk/TowerClimber.mlua
@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화에서 만든 층 글자 바로 아래에 한 줄을 더 놓습니다.

  1. UI 편집 화면에서 DefaultGroup 의 FloorText 를 고르고, 오른쪽 클릭해서 Duplicate 를 누릅니다. 위치와 앵커, 외곽선 같은 설정이 그대로 따라옵니다. 이름은 BestText 로 바꿉니다.
  2. PosX 는 0 으로 층 글자와 같은 가운데에 맞추고, PosY 는 -150 으로 바꿉니다.
  3. 글자 크기는 32, 색은 노란색으로 합니다. 색상 코드는 FFD84A 입니다. 글자 칸에는 최고 0층 을 넣어 둡니다.
  4. TowerHud 에 Property 를 하나 더 만듭니다. 타입은 Entity, 이름은 bestText, Sync 는 None 이고, Reference 창에서 BestText 를 넣습니다.
  5. OnUpdate 에서 플레이어의 TowerRecord 를 GetComponent 로 꺼내 bestFloor 를 최고 몇 층이라는 글자로 바꿔 넣습니다. bestFloor 는 Sync 라서 서버가 바꾼 값이 내 화면에 오니, 클라이언트의 OnUpdate 에서 바로 읽을 수 있습니다.

그런데 처음 들어왔을 때 최고 0층 이라고 나옵니다. 0층이라는 층은 없으니 어색합니다. 그래서 bestFloor 가 0 이면 최고 기록 없음 이라고 넣고, 아니면 최고 몇 층을 넣습니다.

RootDesk/MyDesk/TowerHud.mlua
@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 의 첫 줄 덕분에 더 높은 층에 올랐을 때만 여기까지 오니, 꼭 필요한 만큼만 저장하게 됩니다.

기록이 바뀔 때만 저장한다
기록이 바뀔 때만 저장한다
  1. _DataStorageService 의 GetUserDataStorage 에 플레이어의 ProfileCode 를 넘겨 이 사람의 저장소를 꺼냅니다. ProfileCode 는 사람마다 다른 번호라서 사람마다 자기 칸을 받습니다.
  2. SetAsync 로 저장합니다. 첫 번째는 이름표인 키 TowerBest, 두 번째는 저장할 값입니다. 저장소에는 글자만 넣을 수 있어서 tostring 으로 숫자를 글자로 바꿉니다. 세 번째는 저장이 끝나면 불리는 함수입니다.
  3. Async 는 기다리지 않고 바로 다음 줄로 넘어간다는 뜻입니다. 저장이 끝나면 세 번째 함수가 결과를 알려 주고, errorCode 가 0 이면 잘 저장된 것입니다.

실행해서 2층에 오르면 최고 기록 2 다음에 저장 0 이 찍힙니다. 하지만 다시 실행하면 여전히 최고 기록 없음 으로 시작합니다. 저장은 했지만 아직 불러오는 코드가 없기 때문입니다.

불러오기, GetAndWait

불러오는 건 플레이어가 들어오는 순간 한 번이면 됩니다. TowerRecord 에 Event Method 의 OnBeginPlay 를 더하고, 저장소는 서버만 쓰니 실행 공간은 server only 로 둡니다.

  1. 저장할 때처럼 먼저 이 사람의 저장소를 꺼냅니다.
  2. GetAndWait 로 키 TowerBest 를 읽습니다. AndWait 는 값이 올 때까지 기다렸다가 다음 줄로 간다는 뜻이고, 결과는 errorCode 와 저장해 둔 값 두 개가 옵니다.
  3. errorCode 가 0 이 아니면 그냥 돌아가고, 아니면 tonumber 로 글자를 숫자로 바꿔 bestFloor 에 넣습니다.

그런데 저장소를 비워 둔 채, 처음 들어온 사람처럼 실행하면 Console 에 빨간 오류가 계속 찍힙니다. bestFloor 가 nil 이라고 하고, 화면 글자도 최고 0층 에서 멈춥니다.

처음 들어온 사람은 저장해 둔 값이 없습니다. 이때 GetAndWait 는 오류를 주지 않습니다. errorCode 는 0 이고 값만 비어 있습니다. 비어 있는 값을 tonumber 하면 그대로 nil 이 되고, bestFloor 가 nil 이 되어 쓰는 곳마다 오류가 납니다.

빈 값이 tonumber 를 지나 bestFloor 도 nil 이 된다
빈 값이 tonumber 를 지나 bestFloor 도 nil 이 된다

고치는 건 한 줄입니다. 7화에서 본 or 를 써서, errorCode 가 0 이 아니거나 값이 비어 있으면 돌아갑니다. 처음 들어온 사람은 bestFloor 가 그대로 0 이라서 최고 기록 없음 이 보입니다.

RootDesk/MyDesk/TowerRecord.mlua
@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 을 꼭 확인합니다.

다음 글에서는 탑에 난이도를 넣습니다. 높이 올라갈수록 짧은 발판이 늘어나고 틈은 넓어집니다.

막힌 곳이 있으면 디스코드에 몇 화의 어느 부분인지 같이 적어 주세요.