지난 글에서는 플레이어마다 몇 층에 있는지 기억하게 만들었습니다. 그런데 지금은 2층에서 발을 헛디디면 1층 발판에 그대로 떨어지고, 1층 출구에 닿아도 올라가지 않습니다. 떨어져도 floor 는 그대로 1, 즉 2층에 있다고 기억하고 있고, 1층 출구가 보내려는 층도 1 이라 더 높지 않으니 막히는 것입니다.
완성본은 떨어지면 몇 층에 있었든 탑 1층으로 돌아가 처음부터 다시 오릅니다. 이번 글에서는 이 규칙을 만듭니다.
떨어졌다는 건 어떻게 알까
방법은 간단합니다. 지금 층의 바닥보다 조금 아래에 보이지 않는 선을 긋고, 캐릭터가 이 선보다 아래로 내려가면 떨어진 것으로 봅니다. 층이 올라가면 선도 같이 올라갑니다. 2층에 있으면 2층 바닥 아래, 3층에 있으면 3층 바닥 아래입니다. 그래서 위층에서 떨어지면 아래층 발판에 닿기 전에 선을 지나갑니다.

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

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

- TowerClimber 스크립트를 열고 + 버튼의 Event Method 에서 OnUpdate 를 고릅니다. 괄호 안의 delta 는 지난번에 불린 뒤로 흐른 시간인데, 지금은 쓰지 않습니다.
- 위에 server only 라고 붙어 있습니다. 판정은 서버가 하기로 했으니 그대로 둡니다.
- self.Entity 의 TransformComponent 에서 WorldPosition 의 y, 즉 캐릭터의 높이를 꺼냅니다.
선의 높이는 floor 곱하기 4 빼기 1 입니다. 한 층의 높이가 4 이니 floor 곱하기 4 가 지금 층의 바닥이고, 거기서 1 을 빼면 바닥보다 한 칸 아래입니다.

캐릭터의 높이가 선보다 낮으면 우선 log 로 떨어짐을 찍어 확인합니다. 2층에서 헛디뎌 보면 1층 발판에 닿기 전에 떨어짐이 찍히기 시작합니다. 2층의 선이 1층 발판보다 위에 있기 때문입니다. 1층 발판에 내려선 뒤에도 찍힘은 멈추지 않습니다. 선 아래에 있는 동안 화면이 그려질 때마다 확인하기 때문입니다(Console 은 같은 줄을 하나로 묶고 오른쪽 숫자만 올립니다).
1층에서 떨어지면 숫자가 조금 올라가다 캐릭터가 시작 위치로 돌아오며 멈춥니다. 맵 아래 경계를 벗어나면 시작 위치로 되살아나기 때문입니다.
GameOver
떨어졌을 때 할 일은 두 가지입니다. 층 번호를 0 으로 되돌리고, 1층 첫 발판으로 순간이동합니다. 이 둘을 GameOver 라는 함수 하나로 묶습니다.
- + 버튼에서 New 를 고르고 이름을 GameOver 로 바꿉니다.
- 새 함수에는 실행 공간이 붙어 있지 않습니다. 점 세 개 메뉴의 Execution Space Setting 에서 ServerOnly 를 고릅니다.
- floor 를 0 으로 바꾸고 log 로 게임오버를 찍습니다. floor 는 Sync 라서 서버가 바꾸면 클라이언트에도 0 이 전해집니다.
- 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초, 화면 한 장 정도라 잘 드러나지 않습니다. 하지만 완성본을 공개했을 때 실제로 이 문제가 생겼습니다. 그래서 처음부터 막아 둡니다. 방법은 세 단계입니다.

- Property 를 셋 만듭니다. 이동 중인지 담는 boolean isTeleporting, 목적지를 담는 Vector3 teleportTarget, 이동을 시작한 뒤 흐른 시간을 담는 number teleportSeconds 입니다. 셋 다 서버만 쓰니 Sync 를 None 으로 바꿉니다.
- 순간이동을 맡을 함수 TeleportTo 를 만들고 Vector3 target 을 받게 합니다. ServerOnly 로 두고, 이동 중을 켜고 목적지를 기억하고 시간을 0 으로 되돌린 뒤 순간이동합니다.
- ClimbTo 와 GameOver 의 순간이동 줄을 TeleportTo 한 줄로 바꿉니다. 같은 일을 하는 코드가 두 곳에 있으면 한 곳으로 모으는 것이 좋습니다. 5화에서 해 본 리팩터링입니다.
ClimbTo 에는 이동 중이면 출구에 닿아도 그냥 끝내는 조건을 더합니다. 마지막으로 OnUpdate 에서 아까 미뤄 둔 delta 를 씁니다. 이동 중이면 delta 를 teleportSeconds 에 계속 더해 흐른 시간을 만들고, 목적지까지 거리가 1 보다 가깝거나 1초가 지났으면 이동 중을 끕니다. 그리고 return 으로 아래의 선 판정을 건너뜁니다.
@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
endMaker 에서는 전과 똑같이 보이지만, 이제는 순간이동이 늦게 도착해도 그사이에 엉뚱한 판정이 일어나지 않습니다.
정리
- OnUpdate 로 화면이 그려질 때마다 선 아래로 떨어졌는지 확인했습니다.
- 떨어지면 GameOver 가 floor 를 0 으로 되돌리고 1층 첫 발판으로 보냅니다.
- 순간이동은 TeleportTo 하나로 모으고, 도착할 때까지 판정을 쉬게 했습니다.
다음 글에서는 지금 몇 층인지 화면 위쪽에 보여 줍니다. 지금은 Console 을 봐야만 몇 층인지 알 수 있으니까요.
