지난 글에서는 실행할 때마다 다른 모양의 층이 나오게 만들었습니다. 이번에는 그 층을 위로 세 개 쌓고, 층 끝의 출구에 닿으면 위층으로 올라가게 해 보겠습니다. 다 만들면 위 그림처럼 세 층짜리 탑이 됩니다.
발판 늘리기와 맵 경계
층 셋을 만들려면 발판이 층마다 일곱 개씩, 모두 스물한 개가 필요합니다.
Hierarchy 에서 Platform1 부터 Platform7 까지 일곱 개를 고르고, 복사해서 붙여 넣기를 두 번 합니다. 붙여 넣은 발판은 이름이 Platform1_1 처럼 붙는데, 코드가 발판을 이름으로 찾기 때문에 Platform8 부터 Platform21 까지 차례로 이름을 바꿔 줍니다. 지난 글에서 켠 IsDynamicFoothold 는 함께 복사되니 따로 켜지 않아도 됩니다.

맵 경계도 위로 넓혀야 합니다. 지금 경계의 위쪽 끝이 6 이라, 위층을 올리면 발판이 경계 밖으로 나갑니다. map01 의 MapComponent 에서 RightTop 의 Y 를 12 로 바꿉니다. 층 하나의 높이를 4 로 잡을 것이라 세 번째 층은 높이 8 에서 시작하고, 그 위로 여유를 둔 값이 12 입니다.
층 셋 쌓기 (리팩터링)
지금 OnBeginPlay 안에는 층 하나를 만드는 코드가 통째로 들어 있습니다. 이걸 세 번 복사하면 코드가 세 배로 길어지고, 고칠 때도 세 군데를 고쳐야 합니다. 그래서 층 하나를 만드는 코드를 함수 하나로 묶습니다. 하는 일은 그대로 두고 코드의 모양만 더 쓰기 좋게 바꾸는 것을 리팩터링이라고 합니다.
함수는 여러 줄의 코드에 이름을 붙여, 필요할 때 이름만 불러서 실행하는 묶음입니다. 만드는 순서는 이렇습니다.
- Method 옆의 + 버튼에서 New 를 고르고, 이름을 BuildFloor 로 바꿉니다.
- 점 세 개 버튼의 Add Parameter 로 정수 floor 를 받게 합니다. 함수가 받는 값을 매개변수라고 합니다.
- Execution Space Setting 에서 ServerOnly 를 고릅니다.
우리가 만든 함수는 서버와 클라이언트가 서로 부를 수 있어서, 실행 위치 선택지가 여섯 개로 늘어납니다. Server 는 클라이언트가 부른 함수를 서버에서, Client 는 그 반대로, Multicast 는 서버가 부른 함수를 접속한 모든 클라이언트에서 실행합니다. 지금은 이런 것이 있다는 것만 알아 두고, BuildFloor 는 서버 안에서만 쓰니 ServerOnly 로 둡니다.

OnBeginPlay 에 있던 코드를 BuildFloor 안으로 옮기고 두 군데만 바꿉니다. 발판 번호는 floor 곱하기 7 에 i 를 더하고, 높이는 floor 곱하기 4 만큼 올립니다.

한 가지 더 막아야 할 것이 있습니다. 한 층 안에서 계속 올라가기만 하면, 여섯 번 연속으로 올라갔을 때 높이가 3 이 됩니다. 점프하면 약 1.2 를 더 올라가니 높이 4 에서 시작하는 위층 발판에 출구를 거치지 않고 바로 올라탈 수 있습니다. 그래서 한 층의 높이를 1.5 까지로 막습니다. 아래로 내려가는 걸 막은 if 에 elseif 를 붙여, 1.5 보다 위로 가면 step 을 -1 로 바꿉니다.
이제 OnBeginPlay 는 floor 를 0 부터 2 까지 바꿔 가며 BuildFloor 를 세 번 부르기만 하면 됩니다.
@Component
script FloorBuilder extends Component
@ExecSpace("ServerOnly")
method void OnBeginPlay()
for floor = 0, 2 do
self:BuildFloor(floor)
end
end
@ExecSpace("ServerOnly")
method void BuildFloor(integer floor)
local maxGap = { [-1] = 0.8, [0] = 0.6, [1] = 0.5 }
local layout = { { x = 0, y = 0 } }
for i = 2, 7 do
local before = layout[i - 1]
local step = _UtilLogic:RandomIntegerRange(-1, 1)
if before.y + step * 0.5 < 0 then
step = 1
elseif before.y + step * 0.5 > 1.5 then
step = -1
end
local gap = 0.3 + _UtilLogic:RandomDouble() * (maxGap[step] - 0.3)
layout[i] = { x = before.x + 2.4 + gap, y = before.y + step * 0.5 }
end
for i, spot in ipairs(layout) do
local platform = self.Entity:GetChildByName("Platform" .. (floor * 7 + i))
platform.TransformComponent.Position = Vector3(spot.x, floor * 4 + spot.y, 0)
end
if floor < 2 then
local last = layout[7]
local exit = self.Entity:GetChildByName("Exit" .. (floor + 1))
exit.TransformComponent.Position = Vector3(last.x, floor * 4 + last.y, 0)
end
end
end출구 만들기 (모델, TriggerComponent)
이제 층 끝에 출구를 놓습니다. Preset List 의 포탈 탭에서 portal-1 을 맵에 끌어다 놓고, 이름을 Exit 로 바꿉니다.
이 포탈에는 위 방향키를 눌러야 이동하는 PortalComponent 가 붙어 있습니다. 우리 출구는 닿기만 하면 올라가게 할 것이라 이 컴포넌트는 빼고 빛기둥 그림만 씁니다. 그런데 바로 지우려 하면 경고가 뜹니다. 메이플이 기본으로 주는 포탈 모델에 들어 있는 컴포넌트라, 지우면 그 모델로 놓은 모든 포탈에서 함께 사라진다는 뜻입니다. 원본은 건드리지 않고 아니오를 누릅니다.
대신 이 포탈을 우리만의 모델로 만듭니다. 모델은 엔티티를 도장처럼 저장해 둔 것입니다. 도장을 고치면 그 도장으로 찍은 것이 모두 같이 바뀝니다.
- Hierarchy 에서 Exit 를 오른쪽 클릭하고 Create Model From Entity 를 고릅니다. MyDesk 에 Model_Exit 가 생깁니다.
- Model_Exit 를 골라 PortalComponent 를 지웁니다. 이제 우리 모델이라 경고 없이 지워집니다.
다음은 닿았는지 알아내는 장치입니다. TriggerComponent 는 엔티티 둘레에 보이지 않는 영역을 만들고, 다른 엔티티가 그 영역에 들어오거나 나가면 알려 줍니다.
- Model_Exit 에 Add Component 로 TriggerComponent 를 붙입니다. 처음에는 영역 크기가 0 이라 아무것도 닿지 않습니다.
- Model_Exit 를 더블클릭해 모델 편집 화면을 열고, Collider Auto Fit 의 Apply 를 누릅니다. 영역이 빛기둥 크기(가로 약 0.9, 세로 약 2.6)에 맞춰집니다.
- Save 를 누르고, 모델에 반영할지 묻는 창에서 예를 누릅니다.

출구는 두 개가 필요합니다. 1층 끝과 2층 끝이고, 3층은 마지막 층이라 출구가 없습니다. 맵에 있던 Exit 는 지우고 Model_Exit 를 Scene 으로 두 번 끌어다 놓은 뒤 이름을 Exit1, Exit2 로 바꿉니다. 같은 도장으로 찍었으니 나중에 출구를 고칠 때도 모델 한 곳만 고치면 됩니다.
마지막으로 BuildFloor 맨 아래에서 출구를 층 끝으로 옮깁니다. floor 가 2 보다 작을 때만, layout 의 일곱 번째 칸(그 층의 마지막 발판) 자리에 Exit1, Exit2 를 놓습니다. 위 FloorBuilder 코드의 마지막 if 가 그 부분입니다.

닿으면 위층으로 (Property, 이벤트 핸들러)
닿았을 때 할 일은 출구마다 같으니, 스크립트를 Model_Exit 에 붙입니다. 도장에 붙이면 Exit1 과 Exit2 에 함께 붙습니다. MyDesk 에서 새 컴포넌트 스크립트를 만들고 이름을 FloorExit 로 바꿉니다.
먼저 이 출구가 몇 층으로 보내는지 알아야 합니다. Property 옆의 + 버튼으로 nextFloor 를 만들고, 타입을 number 에서 integer 로 바꿉니다. number 는 0.5 같은 소수까지 담고 integer 는 정수만 담는데, 1.5층 같은 층은 없으니까요. Sync 는 None 으로 둡니다. 이 값은 서버에서만 씁니다(Sync 는 다음 글에서 자세히 다룹니다).
Property 는 Maker 의 Property 창에서 바로 고칠 수 있는 값이라, 도장은 같아도 엔티티마다 다른 값을 줄 수 있습니다.
다음은 닿았을 때 할 일입니다. TriggerComponent 는 무언가 들어오면 TriggerEnterEvent 라는 이벤트를 보냅니다. 이벤트는 무슨 일이 일어났다는 알림이고, 그 알림을 받아 할 일을 적은 함수를 핸들러라고 합니다.
- TriggerEnterEvent 영역에 들어오는 순간
- TriggerStayEvent 영역 안에 있는 동안 계속
- TriggerLeaveEvent 영역에서 나가는 순간
닿는 순간 올라가면 되니 TriggerEnterEvent 를 고르고, 핸들러 안에 세 줄을 적습니다.
@Component
script FloorExit extends Component
property integer nextFloor = 0
@ExecSpace("ServerOnly")
@EventSender("Self")
handler HandleTriggerEnterEvent(TriggerEnterEvent event)
local player = event.TriggerBodyEntity
local start = Vector3(0, self.nextFloor * 4 + 0.5, 0)
_TeleportService:TeleportToMapPosition(player, start, "map01")
end
end- event 의 TriggerBodyEntity 가 출구에 닿은 플레이어입니다.
- 도착 지점은 위층 첫 발판 위입니다. 층마다 첫 발판은 x 0 에 있고, 높이는 nextFloor 곱하기 4 에 0.5 를 더해 발판 조금 위에 내려놓습니다.
- TeleportService 의 TeleportToMapPosition 으로 플레이어를 그 자리로 옮깁니다.
이 핸들러는 ServerOnly 로 둡니다. 플레이어를 어디로 보낼지는 서버가 정해야 접속한 모든 사람에게 같은 자리로 보입니다.
이제 Model_Exit 에 FloorExit 를 붙이고, Exit1 의 nextFloor 는 1, Exit2 의 nextFloor 는 2 로 바꿉니다. 실행하면 1층 끝 출구에 닿을 때 2층 첫 발판으로, 2층 끝 출구에 닿을 때 3층으로 올라갑니다.
그런데 위층에서 발을 헛디디면 아래층으로 떨어지고, 운이 나쁘면 1층 시작점까지 내려갑니다. 떨어졌을 때의 규칙은 뒤에서 게임오버를 만들 때 정하겠습니다.
정리
- 층 하나를 만드는 코드를 BuildFloor 함수로 묶고, 층 번호만 바꿔 세 번 불렀습니다.
- 메이플의 포탈을 가져와 우리 출구 모델로 고치고, TriggerComponent 로 닿았는지 알게 했습니다.
- TriggerEnterEvent 핸들러에서 서버가 플레이어를 위층으로 옮기게 했습니다.
다음 글에서는 플레이어가 지금 몇 층에 있는지 서버가 기억하게 하고, 그 값을 내 화면에 보여 주기 위해 서버의 값을 클라이언트로 보내는 Sync 를 다룹니다.
