지난 글에서는 표에 적은 자리로 발판을 옮겨 계단을 만들었습니다. 이번에는 그 표를 손으로 적는 대신 규칙으로 만들고, 실행할 때마다 다른 모양의 층이 나오게 합니다. 위 그림처럼 실행할 때마다 발판 배치가 달라집니다.
점프 거리 재기
규칙을 만들려면 먼저 캐릭터가 점프로 얼마나 높이, 얼마나 멀리 가는지 알아야 합니다. 이걸 모르고 발판을 아무 데나 놓으면 아무리 잘 뛰어도 건널 수 없는 층이 생길 수 있습니다.
먼저 제자리에서 뛰면 약 1.2 만큼 올라갑니다. Workspace 에서 DefaultPlayer 를 고르면 Property 창에 RigidbodyComponent 가 보입니다. 캐릭터가 중력을 받아 떨어지고 발판 위를 걷고 뛰게 해 주는 컴포넌트로, 그 안의 WalkJump 1.23 이 점프 높이를 정하는 값입니다.
이번에는 달리다가 발판 끝에서 뛰어 봅니다.
- 같은 높이로 내려올 때까지 가로로 약 1.2 를 날아갑니다.
- 한 칸(0.5) 위 발판으로는 약 1 까지 건널 수 있습니다.
- 한 칸 아래 발판으로는 약 1.3 까지 건널 수 있습니다.
하지만 이건 발판 끝에서 딱 맞춰 뛰었을 때의 이야기입니다. 실제로는 조금 일찍, 조금 늦게 뛰기도 하니 넉넉하게 절반 정도로 잡습니다. 올라갈 때 0.5, 같은 높이 0.6, 내려갈 때 0.8 까지만 발판 사이를 벌립니다.

무작위 (RandomIntegerRange)
이제 발판 자리를 무작위로 정합니다. 주사위를 던지듯 매번 다른 값을 가져와야 합니다. 메이플스토리 월드에는 무작위 수를 뽑는 함수 _UtilLogic:RandomIntegerRange 가 있습니다. 괄호 안에 가장 작은 수와 가장 큰 수를 적으면 그 사이의 정수 하나를 골라 주고, 양 끝의 수도 나올 수 있습니다.

시험 삼아 OnBeginPlay 맨 위에 1 부터 6 까지 하나를 뽑아 log 로 찍는 줄을 넣고 실행해 봅니다. 실행할 때마다 콘솔에 다른 숫자가 찍힙니다. 이것이 무작위입니다.
이걸 발판에 씁니다. 다음 발판은 지금보다 한 칸 내려가거나, 같은 높이이거나, 한 칸 올라갑니다. 이 셋을 -1, 0, 1 로 정하면 RandomIntegerRange(-1, 1) 로 셋 중 하나를 뽑을 수 있고, 여기에 한 칸 높이인 0.5 를 곱하면 다음 발판이 얼마나 오르내릴지가 정해집니다.

테이블을 코드로 만들기
이제 손으로 적은 표 대신 코드로 표를 만듭니다. 시험으로 넣은 주사위 줄과 손으로 적은 일곱 줄은 지우고, 첫 발판만 늘 같은 자리인 0, 0 에 둡니다. 캐릭터가 처음 서는 곳이니까요.
발판 사이 규칙은 maxGap 이라는 테이블에 담습니다. 이번에는 대괄호 안에 -1, 0, 1 을 적어 그 수를 이름표처럼 씁니다. 그러면 maxGap[1] 은 올라갈 때의 0.5, maxGap[-1] 은 내려갈 때의 0.8 이 됩니다.
나머지 여섯 발판은 반복문으로 만듭니다.
- for i = 2, 7 은 i 를 2 부터 7 까지 하나씩 늘려 가며 반복하라는 뜻입니다.
- before 는 바로 앞 발판, 표의 i - 1 번째 줄입니다.
- step 은 RandomIntegerRange 로 -1, 0, 1 중 하나를 뽑습니다. 첫 발판보다 아래로 내려가게 되면 step 을 1 로 바꿔 올라가게 합니다.
- 틈 gap 은 0.3 부터 그 높이의 최대값 사이에서 고릅니다. RandomDouble 은 0 과 1 사이의 소수를 뽑아 주니, 여기에 범위를 곱하고 0.3 을 더하면 됩니다.
- 새 발판 자리를 표의 i 번째 줄에 넣습니다. X 는 앞 발판에서 발판 길이 2.4 와 틈을 더한 곳, Y 는 앞 발판 높이에 step 곱하기 0.5 를 더한 곳입니다.
가장 작은 틈을 0.3 으로 둔 이유는, 그보다 가까우면 두 발판이 한 덩어리처럼 붙어 보이기 때문입니다.


완성된 코드는 이렇습니다.
@Component
script FloorBuilder extends Component
@ExecSpace("ServerOnly")
method void OnBeginPlay()
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
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" .. i)
platform.TransformComponent.Position = Vector3(spot.x, spot.y, 0)
end
end
end여러 번 실행해 보면 실행할 때마다 층의 모양이 달라집니다.

ServerOnly 를 빼면
지난 글에서 OnBeginPlay 에 ServerOnly 를 붙였습니다. 이걸 빼면 무슨 일이 생기는지 보면, 서버와 클라이언트가 각각 하는 일에 감을 잡을 수 있습니다.
- OnBeginPlay 의 Execution Space Setting 에서 사용하지 않음을 고릅니다.
- 발판을 옮기는 반복문 안에, 몇 번째 발판을 어디에 놓았는지 찍는 log 를 넣습니다.
실행하면 콘솔에 일곱 줄이 두 번 찍힙니다. 앞의 일곱 줄은 서버, 뒤의 일곱 줄은 클라이언트입니다. 숫자를 비교해 보면 두 번째 발판부터 자리가 서로 다릅니다. 실행 공간을 정하지 않으면 이 코드는 서버에서도 클라이언트에서도 한 번씩 돌아서, 각자 주사위를 따로 던진 것입니다.

그런데 화면의 발판은 서버가 정한 자리에 놓여 있습니다. 발판 자리는 서버가 정한 값이 클라이언트로 전해져 클라이언트의 값을 덮어쓰기 때문입니다. 결국 클라이언트는 쓰이지도 않을 층을 한 번 더 만든 셈이고, 클라이언트가 가진 layout 표는 서버의 것과 다릅니다. 나중에 이 표로 출구 자리 같은 것을 정하면 서버와 클라이언트가 서로 다른 곳을 가리키게 됩니다.
그래서 층은 서버 한 곳에서만 만들고 그 결과를 모두가 함께 봅니다. 이것이 ServerOnly 를 붙인 이유입니다. log 를 지우고 실행 공간을 다시 ServerOnly 로 돌려 놓습니다.
정리
- 점프 거리를 재서 발판 사이 규칙을 정했습니다. 올라갈 때 0.5, 같은 높이 0.6, 내려갈 때 0.8 까지입니다.
- RandomIntegerRange 와 RandomDouble 로 실행할 때마다 다른 모양의 층을 만들었습니다.
- 무작위처럼 결과가 달라지는 일은 서버 한 곳에서 정해야 한다는 것을 ServerOnly 를 빼 보며 확인했습니다.
다음 글에서는 이 층을 위로 쌓습니다. 층 끝에 출구를 만들고, 출구에 닿으면 위층으로 올라가게 합니다.
