마을(타운) 씬 구축기 - 카메라 삽질과 KayKit 컬러 배리에이션 활용법
배틀필드 직행은 그만, 마을에서 시작하자
myDGC 프로젝트는 아이소메트릭 뷰의 전투 게임입니다. 그동안은 프리로드가 끝나면 바로 배틀씬으로 직행하는 구조였습니다. 하지만 게임다운 게임을 만들려면 마을이 있어야 하지 않겠습니까? 플레이어가 마을을 돌아다니다가 포탈을 통해 전투에 진입하는, 그 클래식한 흐름 말이죠.
그래서 TownScene을 새로 만들기로 했습니다. 다행히 이전에 만들어둔 town.json 맵 데이터가 있었고, KayKit의 던전 모델도 전부 준비되어 있었습니다. 쉬울 줄 알았습니다. 정말로요.
첫 번째 관문: 씬을 만들면 꼭 카메라를 못 잡더라
게임 개발을 하면서 느끼는 건데, 새 씬을 만들 때마다 카메라가 말을 안 듣습니다. 이번에도 어김없이 그랬습니다.
TownScene의 기본 구조는 간단했습니다. PreloadScene에서 에셋을 로드하고, TownScene에서 마을 맵을 띄우고, 포탈 근처에 가면 BattleScene으로 전환하는 플로우죠.
TownScene → 마을 맵에 나이트 캐릭터 스폰, 클릭으로 이동. 포탈(파란 빛) 가까이 가면 → BattleScene 자동 진입
카메라는 snapToTarget(playerSpawnPosition)으로 스폰 위치를 바라보게 잡았습니다. 여기까지는 좋았는데, 문제는 카메라 조작이었습니다.
BattleScene에서는 드래그로 카메라 팬, 탭으로 캐릭터 이동, 핀치로 줌이 자연스럽게 동작합니다. 그런데 TownScene에서는 드래그해도 카메라가 미동도 하지 않았습니다.
원인: 매 프레임 카메라를 캐릭터로 끌어당기고 있었다
원인을 추적해보니 update() 메서드에서 캐릭터가 이동 중일 때 camera.setTarget()을 매 프레임 호출하고 있었습니다. 사용자가 드래그로 카메라를 팬 해봤자, 다음 프레임에서 바로 캐릭터 위치로 돌아가버리니 카메라가 움직이지 않는 것처럼 보였던 겁니다.
문제를 찾았습니다.
update()에서 캐릭터가 이동 중일 때camera.setTarget()이 매 프레임 호출되어 드래그로 팬한 카메라를 다시 캐릭터 위치로 끌어당깁니다. BattleScene에서는 카메라가 자동 추적하지 않습니다.
BattleScene을 참고하면 답은 명확했습니다. 카메라 자동 추적을 제거하고, 유저가 직접 드래그로 조작하게 하는 것이죠.
// 문제의 코드 - update() 안에서 매 프레임 실행
if (this.playerMoving) {
this.camera.setTarget(this.playerPosition); // 드래그 팬을 덮어씀!
}
// 해결: 자동 추적 제거
// 카메라는 BattleScene처럼 유저가 직접 드래그로 조작
그런데 이것만으로는 부족했습니다. Babylon.js의 onPointerObservable로 입력을 처리하고 있었는데, 씬 전환 시 attachControl 타이밍 문제로 이벤트가 제대로 바인딩되지 않는 경우도 있었습니다. 결국 BattleScene과 동일하게 캔버스에 직접 이벤트를 거는 방식으로 통일하고, dispose 시 touchInput 정리까지 추가해서야 안정적으로 동작했습니다.
// BattleScene과 동일한 조작 체계
// 드래그 → 카메라 팬
// 탭 → 캐릭터 이동
// 핀치 → 줌
교훈 하나 얻었습니다. 새 씬을 만들 때는 기존 씬의 입력 처리 코드를 그대로 가져오되, 카메라 자동 추적 같은 "편의 기능"이 입력을 덮어쓰지 않는지 반드시 확인하자.
두 번째 관문: 8가지 컬러를 왜 하나만 쓰고 있었나
KayKit 포레스트 팩에는 훌륭한 비밀이 있습니다. 모든 에셋이 8가지 컬러 배리에이션을 제공한다는 점입니다. Tree_1_A_Color1부터 Tree_1_A_Color8까지, 나무 하나에 8가지 색상이 있는 거죠. 지형 타일(Top, Cliff)도 마찬가지입니다.
그런데 프로젝트 전체를 살펴보니 Color1만 하드코딩되어 있었습니다.
전부
_Color1하드코딩입니다. 지형 + 데코 모두요.
모델 파일 자체도 Color1만 프로젝트에 복사되어 있었습니다. 먼저 Color2~8 에셋을 전부 복사하는 것부터 시작했습니다.
컬러 테마 시스템 설계
처음에는 거창하게 생각했습니다. BlockAssetManager에 colorTheme 속성을 추가하고, 맵 데이터에 colorTheme: number를 저장하고... 하지만 한 발 물러서 생각해보니, 이건 에디터 UI 편의 기능이지 데이터 구조 변경이 아니었습니다.
JSON에는 Tree_1_A_Color3처럼 정확한 모델명이 들어가면 됩니다. 에디터에서 테마를 선택하면 드롭다운이 해당 컬러 변형을 기본으로 보여주는 방식이면 충분합니다.
핵심 코드는 타일 에디터의 939번 줄이었습니다.
// 변경 전: 하드코딩
const modelName = `${group}_${variant}_Color1`;
// 변경 후: 선택된 테마 반영
const modelName = `${group}_${variant}_Color${selectedColorTheme}`;
테마 변경 시 전체 재빌드
Color Theme 드롭다운을 팔레트 최상단에 배치하고, 테마를 변경하면 Top, Cliff, Cliff Tall, Deco 전체 섹션이 해당 컬러로 재빌드되도록 구현했습니다.
여기서 하나 주의할 점이 있었습니다. buildPalette 함수 안에 선언된 변수를 updatePreview와 placePiece에서도 접근해야 했기 때문에, 모듈 스코프로 끌어올려야 했습니다.
buildPalette안에 있네요.updatePreview와placePiece에서 접근 가능하려면 모듈 스코프로 올려야 합니다.
최종 결과물은 이렇습니다:
- Color Theme 드롭다운 (Color 1~8)이 팔레트 최상단에 위치
- 테마를 바꾸면 Top/Cliff/CliffTall/Deco 버튼이 전부 해당 컬러로 갱신
- 배치된 데코는 JSON에 정확한 모델명으로 저장
- 섞어 쓰기 가능 — 바닥은 Color2, 나무는 Color5 같이 테마를 중간에 바꿔가며 배치 가능
이 섞어 쓰기가 핵심입니다. 전체 맵을 하나의 컬러로 통일할 수도 있고, 구역별로 다른 분위기를 줄 수도 있습니다.
보너스 삽질: 던전 모델 스케일 문제
마을에 던전 팩의 가구 모델(테이블, 의자, 벤치)도 배치하게 되면서 스케일 문제가 발생했습니다. 던전 모델과 포레스트 모델의 기본 크기가 달라서, 일률적으로 적용하면 안 됐습니다.
// 모델별 차별화된 스케일
// 바닥(floor_): 0.75
// 계단(stairs): 0.55
// 가구/소품(나머지 던전 모델): 1.0
또한 맵 로드 시 deco 복원 코드에서 loadBlockModel(m)이 basePath 없이 호출되어 던전 모델을 /models/forest/ 경로에서 찾으려다 실패하는 버그도 있었습니다. 모델 경로를 올바르게 분기하도록 수정해야 했죠.
731번 줄!
loadBlockModel(m)— basePath 없이 호출해서/models/forest/로 로드 시도. 던전 모델이면 실패합니다.
교훈 정리
1. 새 씬의 카메라는 기존 씬에서 복사하되, 자동 추적을 경계하라
카메라 자동 추적(setTarget 매 프레임 호출)은 편리하지만, 사용자 입력(드래그 팬)을 완전히 먹어버립니다. 카메라 제어 주도권이 어디에 있는지 명확히 설계해야 합니다.
2. 에셋의 잠재력을 100% 활용하라
KayKit처럼 컬러 배리에이션을 제공하는 에셋 팩은 많습니다. _Color1 하드코딩으로 8분의 1만 쓰고 있었다는 건, 에셋 문서를 한 번만 더 읽었으면 피할 수 있는 낭비였습니다.
3. 에디터 편의 기능과 데이터 구조를 분리하라
컬러 테마는 에디터 UI의 편의 기능일 뿐, 맵 데이터에 colorTheme 필드를 추가할 필요가 없었습니다. JSON에는 최종 모델명만 저장하면 됩니다. 데이터는 단순하게, 편의는 UI에서.
4. 다른 팩의 모델을 섞어 쓸 때는 경로와 스케일을 확인하라
포레스트 팩과 던전 팩의 기본 경로와 스케일이 다릅니다. 혼용할 때는 로드 경로와 스케일 값을 모델 카테고리별로 분기해야 합니다.
결과적으로 PreloadScene → TownScene → BattleScene의 깔끔한 게임 플로우가 완성됐고, 타일 에디터에서 8가지 컬러를 자유롭게 섞어 쓸 수 있게 됐습니다. 카메라 삽질에 시간을 좀 쓰긴 했지만, 매번 새 씬을 만들 때마다 겪는 통과의례라고 생각하면 마음이 편합니다. 아마 다음 씬에서도 또 카메라로 삽질하겠죠.