h662

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

무한 점프맵 만들기 7화, 게임오버

2026년 10월 11일

무한 점프맵 만들기 7화, 게임오버 완성 화면

발을 헛디뎌 떨어지면 탑 1층부터 다시 시작하게 만듭니다. 층 바닥 아래에 보이지 않는 선을 두고 서버의 OnUpdate 에서 매번 확인하며, 순간이동한 위치가 서버에 늦게 도착할 때 생기는 문제까지 미리 막습니다.

지난 글에서는 플레이어마다 몇 층에 있는지 기억하게 만들었습니다. 그런데 지금은 2층에서 발을 헛디디면 1층 발판에 그대로 떨어지고, 1층 출구에 닿아도 올라가지 않습니다. 떨어져도 floor 는 그대로 1, 즉 2층에 있다고 기억하고 있고, 1층 출구가 보내려는 층도 1 이라 더 높지 않으니 막히는 것입니다.

완성본은 떨어지면 몇 층에 있었든 탑 1층으로 돌아가 처음부터 다시 오릅니다. 이번 글에서는 이 규칙을 만듭니다.

떨어졌다는 건 어떻게 알까

방법은 간단합니다. 지금 층의 바닥보다 조금 아래에 보이지 않는 선을 긋고, 캐릭터가 이 선보다 아래로 내려가면 떨어진 것으로 봅니다. 층이 올라가면 선도 같이 올라갑니다. 2층에 있으면 2층 바닥 아래, 3층에 있으면 3층 바닥 아래입니다. 그래서 위층에서 떨어지면 아래층 발판에 닿기 전에 선을 지나갑니다.

2층에 있으면 선은 2층 바닥 아래에 있다
2층에 있으면 선은 2층 바닥 아래에 있다

이 판정은 어디서 할까요? 층을 바꾸는 것이 서버였으니, 처음으로 되돌리는 것도 서버가 합니다. 그런데 캐릭터를 움직이는 것은 키를 누르는 내 컴퓨터, 클라이언트입니다. 클라이언트는 움직인 위치를 계속 서버로 보내므로 서버에도 캐릭터의 위치가 있습니다. 서버는 이 위치를 선과 비교하면 됩니다.

클라이언트가 보낸 위치를 서버가 선과 비교한다
클라이언트가 보낸 위치를 서버가 선과 비교한다

바닥선 (OnUpdate)

캐릭터는 언제 떨어질지 모르니 한 번만 봐서는 안 되고 계속 확인해야 합니다. 이럴 때 쓰는 것이 OnUpdate 입니다. OnUpdate 는 화면이 한 번 그려질 때마다 불리는 함수로, 1초에도 수십 번 불립니다. 이 글을 쓴 컴퓨터에서 재 보니 1초에 약 50번이었고, 컴퓨터와 상황에 따라 달라집니다.

OnUpdate 는 화면이 그려질 때마다 한 번씩 불린다
OnUpdate 는 화면이 그려질 때마다 한 번씩 불린다
  1. TowerClimber 스크립트를 열고 + 버튼의 Event Method 에서 OnUpdate 를 고릅니다. 괄호 안의 delta 는 지난번에 불린 뒤로 흐른 시간인데, 지금은 쓰지 않습니다.
  2. 위에 server only 라고 붙어 있습니다. 판정은 서버가 하기로 했으니 그대로 둡니다.
  3. self.Entity 의 TransformComponent 에서 WorldPosition 의 y, 즉 캐릭터의 높이를 꺼냅니다.

선의 높이는 floor 곱하기 4 빼기 1 입니다. 한 층의 높이가 4 이니 floor 곱하기 4 가 지금 층의 바닥이고, 거기서 1 을 빼면 바닥보다 한 칸 아래입니다.

층마다 선의 높이, 1층은 -1, 2층은 3, 3층은 7
층마다 선의 높이, 1층은 -1, 2층은 3, 3층은 7

캐릭터의 높이가 선보다 낮으면 우선 log 로 떨어짐을 찍어 확인합니다. 2층에서 헛디뎌 보면 1층 발판에 닿기 전에 떨어짐이 찍히기 시작합니다. 2층의 선이 1층 발판보다 위에 있기 때문입니다. 1층 발판에 내려선 뒤에도 찍힘은 멈추지 않습니다. 선 아래에 있는 동안 화면이 그려질 때마다 확인하기 때문입니다(Console 은 같은 줄을 하나로 묶고 오른쪽 숫자만 올립니다).

1층에서 떨어지면 숫자가 조금 올라가다 캐릭터가 시작 위치로 돌아오며 멈춥니다. 맵 아래 경계를 벗어나면 시작 위치로 되살아나기 때문입니다.

GameOver

떨어졌을 때 할 일은 두 가지입니다. 층 번호를 0 으로 되돌리고, 1층 첫 발판으로 순간이동합니다. 이 둘을 GameOver 라는 함수 하나로 묶습니다.

  1. + 버튼에서 New 를 고르고 이름을 GameOver 로 바꿉니다.
  2. 새 함수에는 실행 공간이 붙어 있지 않습니다. 점 세 개 메뉴의 Execution Space Setting 에서 ServerOnly 를 고릅니다.
  3. floor 를 0 으로 바꾸고 log 로 게임오버를 찍습니다. floor 는 Sync 라서 서버가 바꾸면 클라이언트에도 0 이 전해집니다.
  4. 1층 첫 발판 위로 순간이동합니다. 1층은 층 번호가 0 이니 높이는 0.5 입니다.

그리고 OnUpdate 에서 떨어짐을 찍던 줄을 self:GameOver() 로 바꿉니다. 이제 2층에서 헛디디면 1층 발판에 닿기 전에 1층 첫 발판으로 돌아오고, Console 에는 게임오버 한 줄과 클라이언트의 floor 0 이 찍힙니다. floor 가 0 으로 돌아왔으니 1층 출구에 닿으면 다시 2층으로 올라갑니다. 처음에 본 문제가 풀렸습니다.

순간이동이 늦게 도착하면 (TeleportTo, delta)

잘 돌아가지만, 눈에 잘 보이지 않는 문제가 하나 숨어 있습니다. 서버가 순간이동을 시키면 먼저 내 화면에 옮기라고 알리고, 내 화면에서 캐릭터가 옮겨진 뒤, 옮긴 위치를 다시 서버로 보냅니다. 이 위치가 도착하기 전까지 서버가 아는 캐릭터는 아직 옛 자리에 있습니다.

서버가 아는 탑과 내 화면이 잠깐 어긋난다
서버가 아는 탑과 내 화면이 잠깐 어긋난다

그 잠깐 동안에도 OnUpdate 는 계속 선을 확인합니다. 위층으로 올라갈 때 ClimbTo 는 floor 를 먼저 올리니 선도 바로 위층 바닥 아래로 올라가는데, 서버가 아는 캐릭터는 아직 아래층 출구에 있습니다. 서버가 보기엔 캐릭터가 새 선보다 아래에 있는 것이라 막 올라간 순간 게임오버가 됩니다. 게임오버 때도 비슷하게 GameOver 가 한 번 더 불릴 수 있습니다.

Maker 에서 혼자 할 때는 이 시간이 0.02초, 화면 한 장 정도라 잘 드러나지 않습니다. 하지만 완성본을 공개했을 때 실제로 이 문제가 생겼습니다. 그래서 처음부터 막아 둡니다. 방법은 세 단계입니다.

순간이동하면 이동 중 표시, 이동 중에는 판정을 쉬고, 가까워지거나 1초가 지나면 끈다
순간이동하면 이동 중 표시, 이동 중에는 판정을 쉬고, 가까워지거나 1초가 지나면 끈다
  1. Property 를 셋 만듭니다. 이동 중인지 담는 boolean isTeleporting, 목적지를 담는 Vector3 teleportTarget, 이동을 시작한 뒤 흐른 시간을 담는 number teleportSeconds 입니다. 셋 다 서버만 쓰니 Sync 를 None 으로 바꿉니다.
  2. 순간이동을 맡을 함수 TeleportTo 를 만들고 Vector3 target 을 받게 합니다. ServerOnly 로 두고, 이동 중을 켜고 목적지를 기억하고 시간을 0 으로 되돌린 뒤 순간이동합니다.
  3. ClimbTo 와 GameOver 의 순간이동 줄을 TeleportTo 한 줄로 바꿉니다. 같은 일을 하는 코드가 두 곳에 있으면 한 곳으로 모으는 것이 좋습니다. 5화에서 해 본 리팩터링입니다.

ClimbTo 에는 이동 중이면 출구에 닿아도 그냥 끝내는 조건을 더합니다. 마지막으로 OnUpdate 에서 아까 미뤄 둔 delta 를 씁니다. 이동 중이면 delta 를 teleportSeconds 에 계속 더해 흐른 시간을 만들고, 목적지까지 거리가 1 보다 가깝거나 1초가 지났으면 이동 중을 끕니다. 그리고 return 으로 아래의 선 판정을 건너뜁니다.

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
        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.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

end

Maker 에서는 전과 똑같이 보이지만, 이제는 순간이동이 늦게 도착해도 그사이에 엉뚱한 판정이 일어나지 않습니다.

정리

  • OnUpdate 로 화면이 그려질 때마다 선 아래로 떨어졌는지 확인했습니다.
  • 떨어지면 GameOver 가 floor 를 0 으로 되돌리고 1층 첫 발판으로 보냅니다.
  • 순간이동은 TeleportTo 하나로 모으고, 도착할 때까지 판정을 쉬게 했습니다.

다음 글에서는 지금 몇 층인지 화면 위쪽에 보여 줍니다. 지금은 Console 을 봐야만 몇 층인지 알 수 있으니까요.

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