홈 › 읽을거리 › 개발노트

챔피언 방어전 개발 노트: 3D 번들 한계를 우회한 기록

개발노트 · 2026-10-08 · GamePool 편집팀

챔피언 방어전은 3D 디오라마 위에서 건물을 짓고 챔피언을 조종해 웨이브를 막는 게임입니다. 3D 부분은 three.js를 쓰는데, 사이트 방침이 외부 의존성 없는 정적 파일이라서 라이브러리 전체가 아니라 필요한 클래스만 골라 한 파일로 묶은 번들을 쓰고 있습니다. 이 번들이 챔피언 방어전의 연출 작업에서 몇 번이나 발목을 잡았습니다. 건설 연출, 건물 공격 애니메이션, 피격 임팩트, 체력바 잔상, 공중 몬스터를 한 번에 추가한 작업(파일에 412줄 추가, 62줄 삭제)에서 부딪힌 일과 우회 방법을 적습니다.

번들에 무엇이 들어 있는가

번들은 three.js r186에서 쓰는 클래스만 골라 esbuild로 압축한 파일로, 크기는 약 568KB입니다. 같은 파일을 블록 월드, 사냥꾼 고깃집, 군단 점령전, 솔저 러쉬, 그리고 챔피언 방어전, 다섯 게임이 함께 씁니다. 내보내기 목록은 저장소의 README에 적혀 있는데, 챔피언 방어전의 새 연출에 필요한 Box3, Float32BufferAttribute, AdditiveBlending이 목록에 없었습니다. Box3는 번들 안에 코드가 들어 있기는 하지만(다른 클래스가 내부적으로 사용합니다) 바깥으로 내보내지 않아 import할 수 없는 상태였습니다.

번들을 다시 만드는 방법도 README에 적혀 있습니다. three와 esbuild를 설치하고 내보내기 목록에 클래스를 추가해 다시 묶으면 됩니다. 그런데 다섯 게임이 같은 파일을 쓰기 때문에, 한 게임의 연출 하나 때문에 번들을 바꾸면 다섯 게임을 모두 다시 확인해야 합니다. 필요한 기능이 모두 몇 줄짜리 대체 코드로 해결되었기 때문에 이번에는 번들을 건드리지 않고 게임 쪽에서 우회하는 쪽을 골랐습니다. 이 판단이 맞는지는 그 우회가 얼마나 지저분해지는지로 따질 수 있는데, 아래 세 가지는 모두 열 줄 안팎에서 끝났습니다.

첫 번째 우회: Box3 없이 건물 크기 재기

건설 연출의 비계(공사장 나무 뼈대)는 건물 크기에 맞아야 합니다. 건물은 목재, 석재, 철, 황금, 암흑 테마로 레벨마다 모양이 달라서 크기를 코드에 박아 둘 수 없고, 지은 뒤에 측정해야 합니다. 보통은 Box3로 물체 전체의 경계 상자를 한 줄에 구하지만 Box3가 없었습니다. 그래서 직접 계산하는 함수를 만들었습니다. 물체 안의 모든 메시를 돌며 지오메트리의 경계 상자를 얻고, 그 상자의 꼭짓점 8개를 월드 행렬로 변환해 최솟값과 최댓값을 모읍니다. 꼭짓점 8개를 모두 변환하는 이유는, 회전한 물체의 경계 상자를 모서리 두 점만으로 구하면 틀리기 때문입니다.

계산에서 빼야 할 메시가 하나 있었습니다. 건물 바닥에는 땅에 붙어 보이게 하는 접지 그림자 판이 깔려 있는데, 이 판은 건물보다 넓어서 포함하면 비계가 건물보다 크게 만들어집니다. 접지 그림자는 렌더 순서가 1인 메시여서, 이 순서의 메시를 계산에서 제외했습니다. 측정 전에 월드 행렬을 한 번 갱신하는 호출도 필요합니다. 갱신하지 않으면 방금 위치를 옮긴 건물의 행렬이 이전 값 그대로라 크기와 중심이 어긋납니다. 측정한 가로, 세로, 깊이, 중심, 높이는 건물 객체에 저장해서 비계, 건설 먼지, 완성 효과, 피격 때 번쩍이는 붉은 상자, 체력바 높이에 두루 씁니다. 가로와 깊이의 최솟값은 1.4, 높이는 1로 막아서 작은 건물에서도 연출이 찌그러지지 않게 했습니다.

두 번째 우회: BufferAttribute로 직접 지오메트리 만들기

공중 몬스터의 날개는 기본 도형으로는 만들 수 없는 모양이어서 점 7개짜리 다각형을 직접 정의했습니다. 꼭짓점 배열에 중심점과 가장자리 두 점씩을 이어 삼각형 부채꼴로 채우고, 이 배열로 지오메트리를 만듭니다. 그런데 이 단계에서도 Float32BufferAttribute가 없었습니다. 이 클래스는 사실 BufferAttribute에 Float32Array를 넘기는 얇은 껍데기라서, 번들에 들어 있는 BufferAttribute에 Float32Array를 직접 만들어 넘겨 대신했습니다. 월드 지형을 만드는 기존 코드도 이미 같은 방식이었으므로 어색할 것은 없었습니다.

지오메트리를 직접 만들면 따라오는 일이 두 가지 있습니다. 법선 계산을 빼면 조명이 올바르게 먹지 않으므로 날개 지오메트리에도 법선 계산 호출을 넣었습니다. 그리고 이 프로젝트에는 지오메트리를 정리하는 함수가 있는데, 공유하는 지오메트리에는 공유 표시를 달아 두어야 몬스터 한 마리가 정리될 때 다른 몬스터가 쓰는 자원까지 해제되지 않습니다. 날개 지오메트리는 모든 박쥐와 와이번이 같이 쓰므로 공유 표시를 달았고, 날개 재질도 색별로 한 번만 만들어 두고 정리 대상에서 빼는 표시를 달았습니다.

세 번째 우회: 가산 혼합을 숫자 2로

번쩍임, 타격선, 칼 궤적, 건설 완성 빛은 모두 가산 혼합(겹치는 부분이 더 밝아지는 혼합)으로 그립니다. 번들에는 이 상수가 내보내져 있지 않았지만, three.js에서 가산 혼합의 정체는 그냥 정수 2입니다(혼합 없음 0, 일반 1, 가산 2). 재질의 혼합 옵션에 숫자 2를 직접 넣었고, 같은 줄에 “이 번들엔 상수가 없어 값으로 씀”이라는 주석을 남겼습니다. 나중에 번들을 갱신해서 상수가 생기면 그 자리를 바꾸라는 표시입니다. 이 연출용 재질은 깊이 쓰기를 끄고 렌더 순서를 2에서 5 사이로 두어 겹쳐 그리는 순서를 정했습니다.

건설 연출을 만들며 부딪힌 문제

건물을 지으면 비계가 서고 먼지가 일며 바닥에 금빛 원이 차오르다가, 다 지어지면 비계가 흩어지고 빛과 파편이 터지는 연출입니다. 첫 건설에는 1.5초, 레벨을 올릴 때에는 1.05초가 걸립니다. 먼지는 0.06초마다, 망치 소리는 0.19초마다 한 번씩 나오도록 박자를 따로 두었고, 망치 소리는 두 가지 효과음을 무작위로 섞고 음높이도 조금씩 흔들었습니다. 완성 순간에는 파편 20번 튀기기, 연기, 빛 번쩍임, 퍼지는 고리, 화면 흔들림 0.35를 한꺼번에 터뜨립니다.

두 번째 문제는 시작할 때 이미 지어져 있는 건물이었습니다. 스테이지를 시작하면 기본 건물이 미리 배치되는데, 이들에도 건설 연출이 나오면 “조용히” 옵션을 넣어 연출을 건너뛰게 했습니다. 병영은 건설이 끝난 뒤 병사를 채워야 해서, 연출이 끝나는 시점에 채우도록 표시를 달아 두었습니다. 레벨업으로 모양이 바뀌는 건물은 이전 모델을 치우고 새 모델을 만드는데, 본성은 체력도 새 최대 체력에 맞춰 이어져야 해서 계산식이 길어졌고, 지금도 코드에서 읽기 어려운 줄 중 하나입니다.

공중 몬스터를 만들며 부딪힌 문제

박쥐(스테이지 4부터)와 와이번(스테이지 22부터)은 길을 따라가지 않고 하늘길로 본성을 향해 날아옵니다. 가장 먼저 부딪힌 것은 목표 선정이었습니다. 망루나 마법사 탑은 길 위에서 가장 앞서 있는 적, 즉 길 진행도가 가장 큰 적을 노립니다. 그런데 공중 몬스터는 길 위에 있지 않아 진행도라는 값이 없습니다. 그래서 출발점에서 본성까지의 거리 대비 남은 거리를 길 전체 길이에 곱해 진행도를 가짜로 만들어 넣었습니다. 본성에 가까워질수록 진행도가 커지니 위협적인 적부터 노리는 규칙이 그대로 통합니다.

두 번째는 무엇이 무엇을 맞힐 수 있는가였습니다. 대포와 투석기는 하늘을 못 맞히게 했고, 망루와 마법사 탑, 궁수나 마법사 챔피언은 맞힙니다. 병영 병사는 높이 1.4 이상의 몬스터를 건드리지 못하고, 근접 챔피언은 1.9 이상이면 닿지 않습니다. 박쥐는 접근, 급강하, 물어뜯기, 상승 이탈을 반복하는데, 급강하 때 높이가 0.9까지 내려오기 때문에 이때만 병사와 기사가 닿습니다. 와이번은 체력 120에 사거리 6.5로 높이 4.3 근처를 선회하며 2.2초마다 불덩이를 던지고, 불덩이는 0.75초 만에 목표에 닿습니다. 이 구분을 플레이어가 모르면 대포가 박쥐를 못 맞히는 것이 버그처럼 보입니다. 그래서 공중 몬스터가 처음 나올 때 한 번 “하늘을 나는 적! 대포·투석기·병사는 못 맞혀요”라는 안내를 6초간 띄웁니다.

죽는 모습도 지상 몬스터와 다르게 해야 했습니다. 지상 몬스터는 쓰러져 줄어들며 0.5초 뒤에 사라지지만, 공중 몬스터는 초당 11의 속도로 고도가 내려가며 몸이 빙글 돌고, 바닥에 닿는 순간 먼지와 흔들림이 나온 뒤 0.85초 후에 사라집니다. 박쥐 체력은 16, 와이번은 120으로 낮게 잡았습니다. 대포와 투석기가 닿지 않는 적이라는 제약이 있어서, 맞힐 수 있는 건물이 잡기만 하면 금방 쓰러지게 하려는 판단이었습니다.

정리하며

번들에 없는 기능이 필요할 때의 우회는 대부분 작았지만, 우회가 조용히 틀리는 지점(접지 그림자가 경계 상자에 섞이는 것, 법선을 계산하지 않는 것, 공유 재질이 정리되는 것)이 진짜 비용이었습니다. 번들을 다시 만드는 편이 더 깔끔한 날이 올 수 있습니다. 다른 3D 게임에서도 같은 우회를 반복하게 된다면, 그때는 내보내기 목록에 Box3와 가산 혼합 상수를 넣고 다섯 게임을 함께 확인하는 편이 낫다고 봅니다. 지금은 우회 코드마다 이유를 주석으로 남겨 두어서, 번들이 바뀌는 날 어디를 고쳐야 하는지 검색 한 번으로 찾을 수 있습니다.

관련 게임 플레이하기

다른 읽을거리

개발노트게임 소개 문구와 소스가 어긋난 열세 곳 정정 기록게임 소개 문구를 소스 코드와 하나씩 맞춰 보며 바로잡은 열세 가지 사례를 변경 전후 문장, 근거가 된 소스 값, 어긋난 이유, 점검 방식과 함께 적었습니다.개발노트솔저 러쉬 개발 노트: 넓은 도로와 난이도 조정도로를 9에서 15로 넓히자 게이트 글씨가 늘어났고, 쉬워졌다는 의견에 체력 곡선과 무리 크기를 다시 맞춘 과정을 변경 전후 수치로 적었습니다.개발노트레오니스 개발 노트: 요새 위기 연출과 퍽 누적 재설계위기 시 등장 속도 2배 요청을 잘못 해석해 구현했다가 긴급 호위병 7체로 바로잡은 과정, 퍽 영구 누적과 몬스터 40% 강화의 수치 근거를 정리했습니다.개발노트소리를 직접 합성하며 키운 값들: 긴장 연출과 발사음녹음 파일 없이 코드로 만든 효과음·배경음악 구조와, 레오니스의 긴장 연출·챔피언 울음, 군단 점령전 발사음을 키울 때 바꾼 숫자를 이력으로 정리했습니다.개발노트레오니스 몬스터를 사진 스프라이트로 바꾼 기록레오니스의 몬스터를 도형 그림에서 사진 스프라이트로 바꾼 작업을 SPRITES·SPRITE_CLIPS·SPRITE_H 값과 커밋 시각으로 정리한 개발 노트입니다.가이드디펜스 기본기: 병력 표와 업그레이드 봇 실험왕국 수비대의 병사 표·스테이지 곡선을 소스에서 계산하고 봇으로 골드 사용처를 비교했습니다. 레오니스 조정 이력과 왕관 수호자의 예금 규칙도 정리했습니다.