hrxdev
한국어

#3 마우스로 교도소를 짓다, 그리고 스스로 찾아지는 방

· Nick

두 번째 마일스톤이 끝났고, 처음으로 볼 수 있는 것이 생겼습니다. Godot 클라이언트가 이제 시뮬레이션 코어 위에 올라가 있고, 마우스로 1층짜리 교도소를 지을 수 있습니다. 바닥을 깔고, 벽을 긋고, 문을 놓고, 방을 지정하고, 허물 수 있습니다. 모든 것이 단순한 큐브와 단색이고 안에는 아직 사람이 없습니다. 하지만 그곳은 하나의 장소이고, 카메라를 돌려 가며 둘러볼 수 있는 장소입니다.

지금 할 수 있는 것

새 게임을 시작하고 지도 크기(한 변 64, 128, 256 타일)와 선택 사항인 시드를 고르면 빈 격자가 나옵니다. 카메라는 기울기가 고정된 직교 투영이며, 정확히 90° 단위로 돌고, 커서 방향으로 확대하고, 키보드나 마우스 가운데 버튼으로 이동하고, 건설용 탑다운 모드도 있습니다. 자유롭게 도는 카메라에서는 벽이 방을 가려 버리기 때문에 자유롭게 돌지 않습니다.

바닥과 철거는 사각형으로 지정합니다. 벽은 윤곽을 따라 그립니다(Shift를 누르고 있으면 직선). 문은 클릭 한 번이며, 이웃한 벽에서 방향을 정하고, 키로 돌릴 수 있습니다. 드래그하는 동안 커서 아래 영역이 고스트로 표시됩니다. 초록색 타일은 지어지고 빨간색 타일은 건너뛰어지며, 라벨이 몇 개이고 이유가 무엇인지 알려 줍니다. 손을 놓기 전에는 월드에 아무것도 보내지 않으며, 코어가 타일이 하나도 바뀌지 않는다고 하면 아무것도 보내지 않습니다. 오른쪽 클릭이나 Escape로 취소합니다.

월드 모델에는 여러 층이 있으므로 층 선택기가 있고, 현재 층보다 위에 있는 층은 숨겨집니다. 방에는 전용 도구가 있으며 아래에서 설명합니다. 디버그 오버레이는 청크 경계, 이동 가능한 타일, 커서 아래의 타일, 시간 측정 패널을 보여 줍니다. 인터페이스의 모든 단어는 영어입니다.

미리보기가 곧 실제입니다

결과와 다른 고스트는 없는 것보다 나쁩니다. 그래서 클라이언트는 건설 명령이 무엇을 할지 추측하지 않습니다. 코어는 같은 영역에 대해 “이것을 실행하면 어떻게 되는가”에 타일 하나하나, 거부할 타일에는 그 이유까지 붙여 답할 수 있습니다. 미리보기를 채우는 검사는 명령이 실제로 실행될 때 쓰는 규칙과 같아서, 둘이 어긋날 수가 없습니다. 클라이언트는 답을 그릴 뿐입니다. 답에 영향을 주는 것, 즉 도구, 드래그한 영역, 층, 지도 자체가 바뀌었을 때만 코어에 다시 묻습니다.

같은 원칙이 클라이언트의 나머지 부분도 규정합니다. 클라이언트는 월드의 상태를 읽고 명령을 보낼 뿐, 자체적인 시뮬레이션 로직이 없습니다. 지금은 코어가 메인 스레드에서 틱하며, 클릭으로 만든 명령은 다음 프레임의 시작, 월드가 틱하기 전에 적용됩니다. 월드가 사람으로 가득 찬 뒤 틱 시간이 튀기 시작하면, 코어를 별도 스레드로 옮기는 것이 계획이며, 언제 옮길지는 측정한 임계값으로 정합니다.

스스로 찾아지는 방

방을 만드는 방법을 두 가지 살펴보았습니다. Prison Architect에서는 구역을 그리면 그것이 자기만의 타일을 가진 독립된 오브젝트가 됩니다. 이해하기는 쉽지만 교도소에서는 잘못됩니다. 수감동 전체가 하나의 구역이 되고, 벽을 다시 지으면 다시 칠해야 합니다. 다른 방법은 닫힌 영역을 자동으로 찾는 것인데, 그러면 주변에 벽이 없는 운동장은 지정할 수 없습니다.

제가 고른 것은 두 방식의 절충입니다. 저장하는 것은 각 타일의 방 종류뿐입니다. 감방, 식당, 목욕탕, 의무실, 면회실, 운동장 등입니다. 나머지는 모두 벽에서 계산해 냅니다. 공간이란 같은 층에서 변으로 이어진 열린 타일들의 모음이며, 지도 가장자리나 허공에 닿지 않으면 닫혀 있습니다. 방은 하나의 공간 안에 있는 같은 종류의 모든 타일입니다. 벽과 문은 경계이고, 벽 아래에 칠한 종류는 보존되지만 무시되므로 벽을 없애면 되살아납니다.

이렇게 하면 따로 알려 주지 않아도 자연스럽게 느껴지는 동작이 나옵니다. 방 한가운데 벽을 세우면 같은 종류의 방 두 개가 됩니다. 벽을 없애면 합쳐집니다. 벽이 있던 자리에 문을 놓으면 아무것도 바뀌지 않습니다. 유효하지 않은 방은 삭제되지 않습니다. 그것은 “고통 없이”의 정반대이기 때문입니다. 대신 이유와 함께 빨간색으로 표시됩니다. 닫혀 있지 않음, 다른 종류와 공유됨, 너무 작음, 문이 없음 중 하나입니다. 사각형을 칠하거나, 닫힌 공간 전체를 한 번의 클릭으로 채우거나, 지울 수 있습니다.

방 목록은 파생 데이터이므로 저장되지 않고 월드 해시에도 들어가지 않으며, 각 타일의 종류만 들어갑니다. 명령 뒤와 시스템 뒤에 다시 만들어지고, 바뀌지 않은 월드를 검사하는 데는 비용이 들지 않습니다. 이번 마일스톤에서는 바뀐 층을 통째로 다시 계산하는데, 256×256 지도에서 약 0.5밀리초가 걸립니다. 청크 단위로 하는 것은 경로 영역이 같은 장치를 필요로 할 때까지 미룰 수 있습니다. 제가 이에 대해 지키는 규칙은, 증분 재구성이 전체 재구성과 같은 결과를 내야 한다는 것입니다.

바뀐 것만 다시 만들고, 층 단면은 공짜로

지도는 벽마다 하나의 오브젝트가 아니라, 층마다 청크마다 하나의 메시로 그려집니다. 코어의 모든 청크에는 버전 카운터가 있고, 렌더러는 마지막으로 그린 버전을 기억합니다. 월드가 바뀌면 더 새로운 버전의 청크만 큐에 들어가며, 큐는 프레임당 시간 예산 안에서 처리되므로 큰 변경이 한 프레임을 멈추게 하는 대신 몇 프레임에 걸쳐 나뉩니다. 방 종류를 칠하는 것은 지도 버전을 올리지 않으므로 메시 빌더를 아예 깨우지 않습니다.

층 단면 보기도 덜 하자는 같은 생각을 따릅니다. 현재 층보다 위의 층은 그냥 숨깁니다. 현재 층의 벽은 셰이더로 1미터 높이까지 눌립니다. 공유 머티리얼 하나와 높이 값 하나로 하므로, 층을 바꾸거나 전체 높이 벽을 토글해도 아무것도 다시 만들지 않습니다. 네 개 층을 오가도 메시 재구성은 0번 일어납니다.

10센티미터의 틈

마지막 점검에서 첫 1층짜리 교도소를 시험해 볼 때, 위층에 바닥이 있었는데 옆에서 보면 벽의 윗면과 그 위의 슬래브 사이에 얇고 밝은 선이 보였습니다. 모서리에서는 바닥이 허공에 떠 있는 것이 보였습니다. 층 단면 보기를 의심했고, 다음엔 z-파이팅을, 그다음엔 스무딩을 의심했습니다.

어느 것도 아니었습니다. 바닥은 두께가 없는 납작한 사각형 하나였고, 그 층의 높이에 10센티미터를 더한 위치에 그려져 있었습니다. 아래층의 벽은 정확히 3미터까지 올라갔습니다. 다음 층의 바닥은 3미터 10센티미터에서 시작했습니다. 틈은 10센티미터 폭이었고, 그 사이로 벽의 불이 비친 윗면이 보였습니다. 수평으로는 모든 것이 완벽하게 맞아서, 알아차리는 데 시간이 걸렸습니다.

해결책은 바닥을 실제 두께를 가진 슬래브로 만들어, 그 층의 시작 지점까지 내려오게 하는 것입니다. 윗면의 높이는 그대로이므로 마우스로 타일을 고르는 것과 모든 오버레이는 바뀌지 않습니다. 같은 청크 안에서 바닥이나 벽이 맞닿는 곳은 옆면을 생략하고 청크 경계에서는 그리므로, 한 청크의 메시가 이웃에 의존하지 않습니다. 그 결과 슬래브 끝에 어두운 10센티미터 가장자리가 남습니다. 전후의 스크린샷을 살펴보고 두기로 했습니다. 바닥의 가장자리처럼 보이고, 실제로 그것이 맞으니까요.

버텨 내는가

마일스톤의 마지막 작업은 마우스로 1층짜리 교도소를 지어 보고 불편한지 확인하는 것이었습니다. 대체로 불편하지 않았습니다. 위의 틈 말고 이번 시험에서 발견된 것은 작지만 귀찮은 것들이었습니다. 마우스로 다른 도구를 고르면 도구 버튼의 강조 표시가 풀리지 않았고, Escape로 ‘새 게임’ 대화상자에서 빠져나올 수 없었습니다. 둘 다 고쳤습니다. 일시정지 메뉴는 나중에 생각해 볼 아이디어 목록에 있습니다.

다음

다음은 M2, 이동과 일과입니다. 월드에 영역과 도달 가능성, 층을 넘나드는 경로 탐색, 군중을 위한 플로우 필드가 생깁니다. 코어는 마침내 매 틱마다 할 진짜 일이 생기고, 벤치마크 게이트도 더는 잠들어 있지 않게 됩니다. 로드맵을 참고하세요.

← #2 · 모든 개발 일지