2026.03.25삽질 일지
리팩토링게임개발애니메이션이동 동기화A* 패스파인딩씬 통합

워킹 애니메이션 속도 - 체감의 함정

"80% 빠르게 해주세요"

게임 개발을 하다 보면, 눈으로 보이는 것과 실제 동작하는 것 사이의 괴리에 빠질 때가 있습니다. 이번 이야기는 단순한 "걷기 애니메이션 좀 빠르게" 요청에서 시작해서, 결국 씬 시스템 전체를 뜯어고치게 된 삽질기입니다.

myDGC 프로젝트에서 캐릭터가 던전을 걸어다니는데, 뭔가 이상했습니다. 보폭과 실제 이동 거리가 안 맞는 거죠. 캐릭터가 바닥 위를 미끄러지듯 이동하는 느낌. "애니메이션 속도를 80% 빠르게" 해달라고 요청했습니다.

2.5배로 올려봤습니다. 체감이 안 됩니다.

5배? 여전히 미끄러지는 느낌.

이쯤 되면 깨닫습니다. 애니메이션 속도가 문제가 아니라, 애니메이션과 이동이 아예 연동되어 있지 않다는 것을.

파고들기 시작하면 끝이 없다

이동 시스템을 열어보니, 문제는 속도가 아니라 구조에 있었습니다. 그리고 구조를 보니, 더 큰 문제들이 줄줄이 나오기 시작했습니다.

첫 번째 발견은 피스 배치 순서 버그였습니다.

원인을 찾았습니다. placePiece 함수에서 회전을 먼저 설정한 후 applyDungeonModelTransform을 호출하는 순서 문제입니다. 프리뷰 코드에서는 올바르게 applyDungeonModelTransformrotation 순서인데, placePiece에서는 rotationapplyDungeonModelTransform 순서로 되어 있습니다.

센터링 로직이 월드 좌표 바운딩박스로 오프셋을 계산한 뒤 로컬 좌표에 적용하기 때문에, 회전이 이미 적용된 상태에서 계산하면 오프셋이 틀어집니다. 프리뷰에서는 멀쩡하게 보이는데 실제 배치하면 위치가 어긋나는 — 이런 류의 버그가 제일 찾기 어렵습니다.

던전 타일맵에서 계단과 캐릭터가 배치된 모습

하나 고치면 열 개가 보인다

순서 버그를 고치고 나니, 두 개의 씬이 눈에 들어왔습니다. BattleScene과 TownScene. 둘 다 같은 맵 위에서 동작하는데, 이동 시스템이 완전히 따로 구현되어 있었습니다.

TownScene: CharacterMovement → 계단 등록, A* 경로에 계단 경유, 높이 보간 모두 구현됨 BattleScene: MovementSystem → 계단 개념 자체가 없음

계단이 화면에는 보이는데 밟으면 아무 일도 안 일어나는 상황. BattleScene에서는 캐릭터가 계단을 그냥 뚫고 지나갔습니다.

그런데 잠깐, TownScene도 문제가 있었습니다.

잠깐, TownScene도 deco 카테고리만 필터링하고 있네요. 계단이 실제로 어떤 카테고리로 저장되는지 확인하겠습니다.

다행히 모든 던전 모델이 category: 'deco'로 저장되고 있어서 렌더링 자체는 됐지만, 이 조사 과정에서 근본적인 구조 문제가 드러났습니다:

  1. 피스 렌더링 + 계단 등록 + 충돌 등록 코드가 두 씬에 중복
  2. 두 씬 모두 동일한 센터링→회전 순서 버그 보유
  3. 이동 시스템이 두 벌 (MovementSystem + CharacterMovement)

A* 패스파인딩과 계단의 전쟁

공용 모듈로 추출하고 씬을 통합하기 시작했습니다. 그런데 계단 이동에서 또 막혔습니다.

typescript
// TileMap.ts
const MAX_CLIMB = 0.5;

높이 차이가 1인 타일 사이를 이동하려면 계단이 필요한데, A* 알고리즘의 getNeighbors()MAX_CLIMB: 0.5 제한에 걸려서 경로 자체를 생성하지 못했습니다. 계단이 있으면 높이 제한을 무시해야 하는데, 패스파인더는 계단의 존재를 모르는 상태였습니다.

TileMap에 stairLinks 개념을 추가했습니다. 계단이 연결하는 타일 쌍을 등록하고, getNeighbors()에서 해당 쌍은 높이 제한을 무시하도록.

그런데 computeStairLink의 방향 계산이 반대였습니다.

문제를 찾았습니다! computeStairLink의 방향이 반대입니다. 실제 데이터를 보면 계단 (4,8) rot=90: tile(4,8) h=0 → tile(3,8) h=1. 현재 코드: b = (4+1, 8) = (5,8) h=0 → 같은 높이를 연결 (무의미!)

forward가 계단의 아래쪽(바닥)을 가리키므로, 높은 쪽 타일은 -forward 방향이었습니다. 이걸 실제 맵 데이터 4개의 계단으로 전부 검증했습니다:

code
| 계단 | 위치   | rot | link          | 높이     |
|------|--------|-----|---------------|----------|
| 1    | (4,8)  | 90  | (4,8)↔(3,8)  | h0↔h1 ✓ |
| 2    | (7,3)  | 270 | (7,3)↔(8,3)  | h0↔h1 ✓ |
| 3    | (2,2)  | 90  | (2,2)↔(1,2)  | h1↔h2 ✓ |
| 4    | (9,9)  | 270 | (9,9)↔(10,9) | h1↔h2 ✓ |

4개 모두 올바른 높이 쌍 연결. 이제 되겠지?

타일 진입 타이밍의 함정

안 됩니다. 캐릭터가 계단 타일로 이동할 때, 초반에는 Y가 안 올라가다가 중간쯤 되어서야 올라가기 시작합니다.

isOnStair(x, z)가 월드 좌표로 계단 존 안에 있는지 체크하는데, 타일 이동 초반에는 아직 계단 존 범위 밖이라 Y가 안 올라갑니다.

월드 좌표 기반의 기하학적 계단 존 체크가 문제였습니다. 캐릭터가 타일 A에서 타일 B로 이동을 시작하는 순간, 물리적으로는 아직 타일 A 위에 있습니다. 그래서 isOnStair(현재위치)가 false를 반환하고, Y 보간이 시작되지 않는 거죠.

해결: isOnStair(x, z) 대신 hasStairLink(fromTile, toTile)로 변경. 타일 전환이 시작되는 순간(progress=0)부터 두 타일이 stairLink로 연결되어 있으면 바로 Y 보간을 시작합니다.

코드 에디터에서 디버깅하는 모습

3-Phase 경로를 버리다

마지막으로, 레거시 buildRoutedPath의 3-phase 경로 로직이 문제였습니다. 이전에는 계단이 A*에 통합되지 않았기 때문에, "바닥 경로 → 계단 경유 → 윗층 경로" 식으로 3단계로 나눠서 경로를 구성했습니다.

3-phase 로직이 불필요하게 복잡합니다. getNeighbors()가 stairLink를 이미 처리하니까 단순 A*로 충분합니다.

stairLink가 getNeighbors()에 등록되어 있으니, A* 알고리즘이 알아서 계단 타일을 경유하는 최적 경로를 찾습니다. 복잡한 3-phase 로직을 걷어내고 단순 A* 하나로 통일했습니다.

최종 결과: 대수술

"애니메이션 속도 좀 올려주세요"에서 시작한 작업의 최종 결과입니다:

code
| 항목         | Before                                    | After                      |
|-------------|-------------------------------------------|----------------------------|
| 씬 파일      | BattleScene.ts (1941줄) + TownScene.ts (409줄) | GameScene.ts (508줄)       |
| 이동 시스템   | MovementSystem + CharacterMovement (2개)   | UnifiedMovementSystem (1개) |
| 전투 시스템   | BattleScene에 내장                          | BattleManager.ts (분리)     |
| 계단 로직    | 3곳에 중복                                  | TileMap 하나에 집중          |
| 경로 탐색    | 3-phase + 별도 계단 처리                     | 단순 A* (stairLink 통합)    |

교훈

1. 체감이 안 되면 속도가 아니라 연동을 의심하라

애니메이션 속도를 아무리 올려도 체감이 안 된다면, 그건 속도 문제가 아닙니다. 애니메이션과 실제 이동이 동기화되어 있는지 먼저 확인해야 합니다. 2.5배를 올려도 안 되는 건 방향이 틀린 겁니다.

2. 하나를 고치면 구조가 보인다

센터링 순서 버그 하나를 고치다가 이중 구현, 누락된 계단 로직, 불필요한 3-phase 경로까지 발견했습니다. 버그 하나를 제대로 파고들면 시스템의 약점이 전부 드러납니다.

3. 좌표계 판정은 타이밍을 생각하라

isOnStair(worldPos)처럼 현재 위치 기반 판정은, 이동 시작 시점에 아직 목표 영역에 도달하지 않았을 때 실패합니다. 이동이라는 맥락에서는 "어디에 있는가"보다 "어디에서 어디로 가는가"가 더 정확한 판정 기준입니다.

4. 패스파인더에 정보를 넣어라, 경로를 쪼개지 말라

계단, 텔레포터, 엘리베이터 같은 특수 이동 수단은 A*의 getNeighbors()에 통합하는 게 정답입니다. 경로를 단계별로 쪼개서 이어붙이면 복잡도만 올라가고 버그의 온상이 됩니다.