북쪽을 보고 오른쪽이 East인데 왜 West라고 써있지? — 좌표계 방향 혼동 버그 해결기
나침반이 거짓말을 하고 있었다
myDGC 프로젝트에서 타일 에디터를 열심히 만들고 있던 어느 날, 맵을 생성하고 3D 뷰어에서 확인하는데 뭔가 이상합니다. 분명 북쪽(N)이 위에 있는데, 오른쪽에 **W(West)**가 표시되어 있습니다.
잠깐, 북쪽을 보고 오른쪽이면 동쪽(East) 아닌가요?
초등학교 때 배운 "동서남북"이 흔들리는 순간이었습니다. 처음엔 제 상식을 의심했습니다. "혹시 게임 좌표계에서는 다른 건가?" 하지만 아무리 생각해도 N이 위일 때 오른쪽은 E가 맞습니다. 이 단순한 레이블 오류가 사실은 맵 생성 전체에 영향을 미치는 근본적인 문제의 실마리였습니다.
문제의 시작: corridor 맵이 깨진다
사실 이 방향 버그를 발견한 건 다른 문제를 추적하다가였습니다. corridor 템플릿으로 12x12 맵을 생성하면 렌더링이 깨지는 현상이 있었거든요. Claude Code와 함께 디버깅을 시작했습니다.
먼저 생성 과정을 단계별로 트레이싱했습니다.
기본값 12x14로
MapGenerator.corridor()를 호출합니다. 이제 단계별로 트레이싱하겠습니다.
그런데 첫 번째 삽질이 시작됩니다. 열심히 12x14로 분석했는데...
타일 에디터 기본값은 12x12 (line 817-818:
|| 12)입니다. 제가 12x14로 돌렸네요. 12x12로 다시 확인하겠습니다.
기본값부터 틀렸습니다. 이런 사소한 실수가 디버깅 시간을 얼마나 잡아먹는지 모릅니다.
근본 원인을 찾다: Z경로와 입구의 1칸 벽
12x12로 다시 돌려보니 핵심 문제가 드러났습니다. corridor 맵 생성의 코드 흐름은 이렇습니다:
1. generateCorridor() // ← 입구 위치 모름, Z경로만 배치
2. applySymmetry()
3. carveEntrances() // ← 여기서 비로소 입구를 뚫음
4. smoothHeightTransitions()
generateCorridor가 입구 위치를 모르는 상태에서 Z경로를 배치합니다. 이후 carveEntrances가 입구를 뚫으면 두 개의 h0(바닥 높이) 복도가 딱 1칸 간격으로 나란히 붙어버립니다.
Step 3 (입구 파기 직후) — 핵심 문제 발생:
Y\X 0 1 2 3 [4] 5 6 [7] 8 9 10 11
2: 2 2 0 0 [2] 0 0 2 2 2 2 2 ← x=4: 1칸 벽!
3: 2 2 0 0 [2] 0 0 2 2 2 2 2
x=4 위치에 h2 벽이 딱 1칸 끼어있습니다. 스무딩을 30회나 돌려도 이 1칸 벽은 해결되지 않습니다. 결과는 처참했습니다:
현재 코드 12x12 corridor 결과:
- 스무딩: 30회 (최대치, 미수렴)
- 최종 위반: 9개 (R1×8, R4×1)
generateCorridor가 입구 위치를 모르니까 Z경로 팔을 아무 데나 놓고, 이후 입구가 뚫리면 두 h0 복도가 1칸 간격으로 나란히 붙어버립니다. 스무딩이 아무리 돌아도 1칸 벽은 고칠 수 없습니다.
이 진단이 핵심이었습니다.
그런데 나침반이 왜 뒤집혀 있지?
맵 데이터를 스크린샷과 대조하던 중, 나침반 표기가 이상하다는 걸 발견합니다.
맞습니다. N이 위에 있으면 오른쪽은 East여야 하는데 W로 표시되어 있네요. 타일 에디터의 방향 표기가 뒤집혀 있습니다.
원인은 Babylon.js의 왼손 좌표계였습니다.
// 카메라: alpha = -Math.PI / 4 (남서쪽에서 바라봄)
// 이 각도에서 낮은 x(-X)가 화면 오른쪽에 표시됨
// 나침반 매핑 (잘못됨):
W → x = 0 (낮은 x) → 화면에서 오른쪽으로 보임 // ← 뒤집힘!
E → x = max (높은 x) → 화면에서 왼쪽으로 보임 // ← 뒤집힘!
Babylon.js의 왼손 좌표계에서는 카메라 alpha가 -π/4일 때 -X 방향이 화면 오른쪽입니다. 코드에서는 W를 낮은 x에, E를 높은 x에 배치했으니, 화면에서 보면 W가 오른쪽에 나타나는 겁니다. 단순하지만 치명적인 실수였습니다.
영향 범위도 확인했습니다:
- BattleScene.ts:1295-1299 — 게임 나침반 레이블
- tileeditor/main.ts:251-255 — 타일 에디터 나침반 레이블
- MapGenerator.ts:463-488 — 입구 방향 (EAST=x=max, WEST=x=0)
다행히 입구 배치 로직은 좌표값 기반이라 게임플레이에는 영향이 없었지만, 개발자가 맵을 디버깅할 때 동서를 반대로 읽고 있었다는 건 꽤 심각한 문제입니다.
해결: Z경로가 입구를 알게 하다
방향 표기는 E/W 레이블을 교체하는 것으로 간단히 해결했습니다. 본질적인 문제는 corridor 맵 생성이었습니다.
핵심 아이디어: Z경로가 입구 위치를 통과하도록 설계를 변경합니다.
현재 (벽 1칸 문제): 개선안 (입구 통과):
Z팔 벽 입구 입구=Z팔
x:2-3 4 5-6 x:5-6 에서 시작
중간 커넥터로 연결
Z경로의 상단 팔을 입구 x 위치에서 시작하고, 수평으로 연결한 뒤 좌측으로 내려가도록 변경했습니다. 이러면 1칸 벽이 원천적으로 생기지 않습니다.
결과 비교:
| 현재 | 수정안 | |
|---|---|---|
| 스무딩 반복 | 30회 (미수렴) | 30회 |
| 최종 위반 | 9개 | 0개 |
하지만 바로 끝나지 않았습니다. 첫 수정 후에도 미세한 문제가 남았거든요.
(4,0) h=1: N=OOB(낮음), S=(4,1)=h0(낮음), E=(5,0)=h0(낮음). 세 면이 낮은데 9-slice는 outerNE 하나만 렌더 → 남쪽 클리프 누락.
맵 가장자리에서 입구와 Z경로 턴이 만나는 지점의 h1 타일이 깨지는 문제였습니다. 상단 턴을 y=0부터 시작시켜서 해당 영역 전체를 h0으로 만드는 것으로 해결했습니다.
최종 검증:
검증 결과: 규칙 위반 0개, 렌더링 문제 0개.
이 과정에서 얻은 중요한 교훈이 있습니다:
코드 로직을 맹신하지 말고, 결과물을 데이터로 직접 검증할 것.
"코드가 맞으니까 결과도 맞겠지"라는 가정은 위험합니다. 실제로 맵 데이터를 숫자 배열로 출력하고 하나하나 확인하는 과정이 없었다면 가장자리 케이스를 놓쳤을 겁니다.
보너스: Citadel 템플릿 탄생
corridor 문제를 해결하고 나니 자신감이 붙어서 새 템플릿도 만들었습니다.
Citadel — "+"자 형태의 h0 통로가 맵을 4개 구역으로 나누고, 4방향 입구에서 적이 몰려오는 방어전 맵.
12x12, 14x14 모두 위반 0개, 스무딩 2회 수렴. corridor의 교훈을 반영해서 처음부터 입구 위치를 고려한 설계로 깔끔하게 완성했습니다.
교훈 정리
1. 좌표계는 반드시 초기에 검증하라
왼손 좌표계, 오른손 좌표계, Y-up, Z-up... 3D 엔진마다 다릅니다. 나침반 하나 잘못 붙여놓으면 이후 모든 디버깅에서 동서를 반대로 읽게 됩니다. 프로젝트 초기에 좌표계 규약을 명확히 문서화하세요.
2. 생성 단계 간 정보 공유가 핵심
generateCorridor가 입구 위치를 몰랐던 게 근본 원인이었습니다. 파이프라인의 앞 단계가 뒷 단계의 결과를 필요로 한다면, 설계를 재고해야 합니다. 독립적인 단계들이 나중에 합쳐졌을 때 충돌하는 건 맵 생성뿐 아니라 모든 파이프라인 아키텍처에서 흔한 문제입니다.
3. 스무딩은 만능이 아니다
"일단 대충 생성하고 스무딩으로 때우자"는 전략은 한계가 있습니다. 30회를 돌려도 수렴하지 않는 구조적 문제는 스무딩이 해결할 수 없습니다. 생성 단계에서 올바른 구조를 보장하는 게 훨씬 낫습니다.
4. 결과를 데이터로 직접 검증하라
코드를 읽고 머릿속으로 시뮬레이션하는 것보다, 실제 출력을 숫자 배열로 찍어보는 게 백 배 정확합니다. 수동 트레이싱에서 실수가 난 것도, 결국 실제 실행 결과로 검증해서 잡았습니다.