지난 글에서는 화면 위쪽에 지금 몇 층인지 글자로 띄웠습니다. 그런데 떨어지면 아무 말 없이 1층으로 돌아오고, 위쪽 글자만 탑 1층으로 바뀝니다. 방금 몇 층까지 올라갔는지는 금방 잊어버립니다. 완성본처럼 떨어지면 화면 가운데에 게임오버 글자와 몇 층까지 올랐는지를 띄우고, 잠시 뒤에 저절로 사라지게 만들겠습니다.
누구의 화면에 띄울까
같은 탑에는 여러 사람이 함께 오릅니다. 떨어진 사람은 그중 한 사람이고, 다른 사람은 아직 잘 오르고 있습니다. 그 사람들 화면에 게임오버가 뜨면 안 되니, 알림은 떨어진 그 사람 화면에만 떠야 합니다.

떨어진 걸 아는 쪽은 서버입니다. 7화에서 바닥선 판정을 서버에서 했기 때문입니다. 그런데 화면에 글자를 띄우는 건 클라이언트입니다. 8화에서 본 것처럼 UI 는 클라이언트에만 있습니다. 그래서 서버가 그 사람의 클라이언트에게 알려 줘야 합니다.
Sync 로는 안 되는 이유
6화에서 배운 Sync 는 값을 클라이언트로 보내 줍니다. 그런데 게임오버가 되면 floor 는 0 이 됩니다. 1층에서 떨어지면 floor 는 처음부터 0 이라 값이 바뀌지 않고, 값이 안 바뀌면 Sync 로 아무것도 오지 않습니다.

그래서 이번에는 값이 아니라 일이 일어났다는 것을 보내야 합니다. 이럴 때는 서버가 클라이언트의 함수를 직접 부릅니다. 함수의 실행 공간을 Client 로 정하면, 서버에서 불러도 그 함수는 클라이언트에서 실행됩니다.
순서는 이렇습니다.
- Maker 에서 게임오버 알림 UI 를 만듭니다.
- 그 UI 를 띄우고 감추는 함수를 만듭니다.
- 서버가 떨어진 사람에게만 그 함수를 부르게 합니다.
알림 UI 만들기
알림은 바탕 판 하나와 글자 두 줄로 만듭니다.
- 위쪽 도구 막대에서 UI 버튼을 눌러 UI 편집 화면을 엽니다.
- 왼쪽 도구 줄에서 이미지 도구를 누르면 캔버스 가운데에 하얀 네모가 생깁니다. 이름을 GameOverPanel 로 바꾸고, 크기를 가로 640, 세로 180 으로 넣습니다. 앵커는 화면 한가운데 그대로 둡니다.
- 판의 색은 검정, 투명도 A 는 50 으로 낮춰서 뒤의 게임이 살짝 비치게 합니다.
- 텍스트 도구로 글자를 올리고 이름을 GameOverText 로 바꿉니다. 글자 칸에 게임오버, 글자 색 빨간색, 크기 60, PosY 는 35 입니다.
이때 글자가 한 글자씩 세로로 늘어설 수 있습니다. 글자 상자의 크기가 처음 만들 때 그대로 100 × 100 이라, 크기 60 인 글자가 한 줄에 한 글자밖에 안 들어가기 때문입니다. RectSize 를 가로 600, 세로 80 으로 넓히면 한 줄로 보입니다.
Hierarchy 에서 글자를 판 위로 끌어다 놓으면 글자가 판의 자식이 됩니다. 두 번째 줄 ReachedText 는 GameOverText 를 고른 채 만들면 처음부터 판 안에 생깁니다. RectSize 가로 600, 세로 60, 글자 크기 36, 흰색, PosY 는 -45 로 두고, 글자 칸에는 모양을 보려고 탑 3층까지 올랐습니다 를 넣어 둡니다.
마지막으로 GameOverPanel 의 PosY 를 120 으로 올려 캐릭터가 서 있는 자리를 가리지 않게 합니다. 판을 옮기면 자식인 글자 두 줄도 함께 따라 올라갑니다.
여기서 하나 주의할 점이 있습니다. 처음에 ToastGroup 에 만들었다면 실행해도 알림이 보이지 않습니다. ToastGroup 은 Maker 의 토스트 기능이 쓰는 그룹이라 실행하면 꺼진 채로 시작합니다. 우리가 직접 켜고 끌 알림은 늘 켜져 있는 DefaultGroup 으로 옮깁니다. Hierarchy 에서 판을 DefaultGroup 위로 끌어다 놓으면 글자 두 줄도 함께 옮겨집니다.

띄우고 감추기
코드는 8화에서 만든 TowerHud 에 넣습니다. 내 화면의 글자를 맡는 Logic 이니 알림도 같은 곳에 두면 됩니다.
- Property 를 둘 만듭니다. 타입은 Entity, 이름은 gameOverPanel 과 reachedText, Sync 는 None 이고, 동그라미 버튼으로 Reference 창을 열어 GameOverPanel 과 ReachedText 를 넣습니다.
- Event Method 에서 OnBeginPlay 를 만들고 실행 공간을 ClientOnly 로 바꿉니다. 안에는 gameOverPanel 의 Enable 을 false 로 하는 한 줄만 씁니다. Enable 은 물체를 켜고 끄는 스위치이고, 판을 끄면 자식인 글자도 함께 꺼집니다.
- New 로 함수 ShowGameOver 를 만들고 integer 매개변수 reachedFloor 를 더합니다. 직접 만든 함수는 실행 공간이 비어 있으니 ClientOnly 로 정합니다.
ShowGameOver 안에서는 먼저 글자를 바꾸고, 판을 켭니다. 그대로 두면 알림이 계속 떠 있으니, _TimerService 의 SetTimerOnce 로 2.5초 뒤에 다시 끕니다. 괄호 안 첫 번째는 그때 할 일입니다. function 과 end 사이에 판을 끄는 한 줄을 적는데, 이름 없이 그 자리에서 만드는 함수라 이름 없는 함수라고 부릅니다. 두 번째는 기다릴 시간 2.5 입니다.
@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"
@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) .. "층"
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서버가 그 사람에게만 부르기
TowerHud 의 ShowGameOver 는 ClientOnly 라서 서버에서 바로 부르면 아무 일도 일어나지 않습니다. 그래서 TowerClimber 에 클라이언트로 건너가는 함수를 하나 만듭니다.
- TowerClimber 에 New 로 함수 ShowGameOver 를 만들고 integer 매개변수 reachedFloor 를 더합니다. TowerHud 의 함수와 이름이 같지만 다른 스크립트라 괜찮습니다.
- Execution Space Setting 에서 이번에는 ClientOnly 가 아니라 Client 를 고릅니다.
- 안에는 _TowerHud 의 ShowGameOver 를 부르고 받은 층 수를 그대로 넘기는 한 줄을 씁니다. Logic 은 월드에 하나뿐이라 이름 앞에 밑줄을 붙여 어디서든 부를 수 있습니다.

이제 GameOver 에서 이 함수를 부릅니다. 처음에는 GameOver 의 맨 끝에 floor 에 1 을 더해 넘기는 줄을 넣었는데, 3층에서 떨어져도 언제나 탑 1층까지 올랐습니다 가 떴습니다. GameOver 의 위쪽에서 floor 를 먼저 0 으로 바꾸기 때문에, 알림을 부를 때는 floor 가 이미 0 이었던 것입니다. 부르는 줄을 GameOver 의 맨 첫 줄로 올리면 탑 3층까지 올랐습니다 가 제대로 뜹니다.
마지막으로, Client 함수를 그냥 부르면 모든 클라이언트에서 실행됩니다. 떨어진 사람 화면에만 띄우려면 부를 때 맨 뒤에 그 사람의 UserId 를 하나 더 넘깁니다. TowerClimber 는 플레이어에 붙어 있으니 self.Entity 가 바로 떨어진 그 플레이어이고, UserId 는 그 플레이어의 PlayerComponent 에 들어 있습니다. 함수를 만들 때 이 칸을 따로 만들 필요는 없습니다. Client 함수에는 보낼 사람을 정하는 칸이 맨 뒤에 저절로 붙어 있습니다.

@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: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실행해서 3층까지 오른 뒤 떨어지면 화면 가운데에 탑 3층까지 올랐습니다 가 뜨고, 2.5초 뒤에 저절로 사라집니다. 혼자 할 때는 보이는 것이 같지만, 여러 사람이 함께 오를 때는 떨어진 그 사람의 화면에만 알림이 뜹니다.

정리
- 바탕 판 위에 글자를 자식으로 두면, 판 하나로 함께 옮기고 끌 수 있습니다.
- Enable 로 물체를 켜고 끕니다.
- SetTimerOnce 와 이름 없는 함수로, 정한 시간 뒤에 할 일을 맡깁니다.
- 실행 공간이 Client 인 함수는 서버가 불러도 클라이언트에서 실행됩니다.
- 마지막에 UserId 를 넘기면 그 사람의 클라이언트에서만 실행됩니다.
다음 글에서는 내가 오른 가장 높은 층을 기록으로 남기고, 게임을 껐다 켜도 그대로 남게 만듭니다.
