캐릭터 전환 후 이동 불가 버그 — commandQueue[0]만 바라본 죄
들어가며
"Bad North" 스타일의 실시간 전술 배틀 게임을 만들고 있습니다. Babylon.js 기반 3D 로그라이크인데, 유닛을 선택하고 타일을 클릭하면 이동하는 — 전술 게임의 가장 기본적인 조작이 어느 날 갑자기 먹통이 되었습니다.
정확히는, 첫 번째 유닛은 잘 움직이는데 두 번째 유닛을 선택해서 이동 명령을 내리면 꿈쩍도 안 하는 버그였습니다. 캐릭터 전환만 하면 이동이 씹히는 거죠. Swordsman 하나로만 플레이하면 멀쩡한데, Rogue로 바꾸는 순간 Rogue가 땅에 뿌리를 내립니다.
3월 24일, 이 버그와의 전쟁이 시작되었습니다.
문제 추적: 이벤트부터 큐까지
처음에는 입력 시스템을 의심했습니다. 포인터 이벤트가 제대로 전달되는지, 유닛 선택 로직에 문제가 있는지. BattleScene의 유닛 선택/이동 명령 흐름을 처음부터 끝까지 추적했습니다.
BattleScene.ts의 issueMoveCommand → CommandSystem.moveUnit → queueCommand → 큐에 추가. 여기까진 정상이었습니다. 명령은 분명히 큐에 들어가고 있었습니다.
그런데 들어간 명령이 실행이 안 되는 겁니다.
범인은 commandQueue[0]
CommandSystem.processCommandQueue의 코드를 열어본 순간, 바로 보였습니다.
원인을 찾았습니다.
CommandSystem.processCommandQueue에서 항상commandQueue[0]만 처리하고 있어서, 첫 번째 유닛의 이동 명령이 아직 'executing' 상태면 두 번째 유닛의 'pending' 명령이 영원히 실행되지 않습니다.
문제의 핵심은 이것이었습니다:
// 수정 전 — commandQueue[0]만 바라보는 코드
processCommandQueue() {
if (this.commandQueue.length === 0) return;
const command = this.commandQueue[0]; // 항상 첫 번째만!
if (command.status === 'pending') {
this.executeCommand(command);
}
// executing 상태면? 그냥 리턴. 뒤에 뭐가 있든 모름.
}
유닛 A에게 이동 명령을 내리면 commandQueue[0]에 들어가고, 상태가 executing으로 바뀝니다. 이 상태에서 유닛 B에게 이동 명령을 내리면 commandQueue[1]에 pending 상태로 들어갑니다. 하지만 processCommandQueue는 매 프레임마다 commandQueue[0]만 확인하니까, [0]이 아직 executing이면 "아, 지금 바쁘구나" 하고 그냥 돌아갑니다.
유닛 B의 명령은 영원히 pending 상태로 큐에 갇힙니다.
전술 게임에서 여러 유닛에 동시에 명령을 내리는 건 가장 기본적인 조작인데, 이 기본이 안 되고 있었던 겁니다.
수정: 큐 전체를 순회하도록
수정은 놀라울 정도로 간단했습니다.
// 수정 후 — 큐 전체를 순회
processCommandQueue() {
for (const command of this.commandQueue) {
if (command.status === 'pending') {
this.executeCommand(command);
}
}
}
commandQueue[0]만 보던 눈을 큐 전체로 돌린 것뿐입니다. 유닛 A가 이동 중이어도 유닛 B의 pending 명령을 찾아서 실행합니다. 각 유닛은 독립적으로 움직이니까 서로 블로킹할 이유가 없었거든요.
수정: 큐의 모든 pending 명령을 순회하며 각각 실행하도록 변경했습니다. 이제 유닛 A가 이동 중이어도 유닛 B에 이동 명령을 내릴 수 있습니다.
컴파일 에러 없이 깔끔하게 수정 완료. 하지만 이 날의 버그 사냥은 여기서 끝이 아니었습니다.
연쇄 버그: 점프 중 공중 정지
커맨드 큐를 고치고 나니, 숨어 있던 다른 버그가 모습을 드러냈습니다. 유닛이 높이가 다른 타일로 점프하는데, 점프 도중 다음 타일에 적이 있으면 공중에서 얼어버리는 현상이었습니다.
MovementSystem.updateMovement에서 이미 이동 중(progress > 0)인데 다음 타일에 적이 있으면return해서 유닛이 공중에 멈춰버립니다. 점프 애니메이션 중간에 얼어버리는 거죠.
점프해서 허공에 떠 있는데 갑자기 멈추는 유닛의 모습을 상상해보세요. 마치 와일 E. 코요테가 절벽 끝에서 아래를 내려다보는 장면 같았습니다.
// 수정 전 — 이동 중이든 아니든 무조건 체크
if (isOccupiedByEnemy(nextTile)) {
return; // 점프 중이어도 멈춤!
}
// 수정 후 — 아직 출발 전일 때만 블로킹
if (progress === 0 && isOccupiedByEnemy(nextTile)) {
return; // 출발 전에만 막음
}
// 이미 점프 중이면 도착지까지 이동 완료 → 전투 시스템이 처리
세 번째 버그: 맞기만 하는 유닛
이동 문제를 해결하니 이번엔 전투에서 이상한 현상이 보였습니다. 플레이어 유닛이 적에게 둘러싸이면 맞기만 하고 반격을 안 하는 것처럼 보이는 겁니다.
여러 적이 연속 공격 →
hit끝나기 전에 또hit요청 → 계속 피격 모션. 플레이어 차례가 와서attack애니메이션 재생 시도 →isPlayingOneShot이 true라 차단됨
데미지 계산은 정상이었지만, 연속 피격 애니메이션이 공격 애니메이션을 먹어버리고 있었습니다. 시각적으로 "맞기만 하는 샌드백"이 되는 거죠.
공격 우선 + 피격 쿨다운 조합으로 해결했습니다. attack 애니메이션이 hit를 강제 중단할 수 있게 하고, 피격 모션에 쿨다운을 추가해서 연속 스태거를 방지했습니다.
그리고 이어진 지형 렌더링 삽질
전투 시스템을 고치고 나서는 지형 렌더링 개선에 들어갔는데, 여기서 진짜 삽질이 시작되었습니다.
베이스 블록의 Z-fighting(겹친 면이 깜빡이는 현상)을 해결하려고 renderBase를 제거했더니, 절벽(cliff) 피스 사이에 검정 틈이 드러났습니다. 베이스 블록이 사실 절벽의 빈틈을 메꾸고 있었던 거죠.
그리고 절벽 피스의 방향이 뒤바뀌어 있다는 걸 발견했습니다:
N↔S 방향이 뒤바뀌어 있음 — AUTO가
sideN을 놓는 곳에 USER는sideS. 코너도 전부 N↔S 반전.
원인은 BlockAssetManager의 모델 매핑에서 북쪽과 남쪽 모델 파일이 서로 교차되어 있었던 것입니다. sideN이 남쪽 모델(H)을 사용하고, sideS가 북쪽 모델(B)을 사용하는 식으로요.
// 수정: N↔S 모델 파일 교환
sideN: 'Hill_Cliff_B_Side' // ← 이전에는 H를 사용
sideS: 'Hill_Cliff_H_Side' // ← 이전에는 B를 사용
// outerCorner, innerCorner도 동일하게 교환
거기에 타일당 피스 중복 배치(side + corner 겹침), 중간 레벨 top 피스 누락, height-0 바닥 렌더링 누락까지 줄줄이 수정해야 했습니다. 하나를 고치면 다른 문제가 보이고, 그걸 고치면 또 다른 문제가 나오는 전형적인 연쇄 디버깅이었습니다.
교훈 정리
1. 큐는 전체를 순회하라
queue[0]만 보는 커맨드 시스템은 사실상 "한 번에 하나의 유닛만 조작 가능"이라는 치명적 제약을 만듭니다. 독립적인 엔티티의 명령은 독립적으로 처리되어야 합니다. 커맨드 패턴을 쓸 때, 각 커맨드가 정말 순차적이어야 하는지 병렬이어야 하는지 설계 단계에서 결정해야 합니다.
2. 진행 중인 동작은 완료시켜라
물리적으로 이동 중인 오브젝트를 중간에 멈추면 비현실적인 결과(공중 정지)가 나옵니다. 이미 시작된 동작은 최소 단위까지 완료시키고, 그 이후에 다음 로직(충돌, 전투 등)을 처리하는 것이 자연스럽습니다.
3. 애니메이션 우선순위를 명확히
"공격과 피격 중 뭐가 우선?"이라는 질문에 답이 없으면, 연속 피격 시 공격 모션이 씹히는 문제가 생깁니다. 애니메이션 시스템에는 반드시 우선순위 체계가 필요합니다.
4. 한 버그를 고치면 다른 버그가 나온다
커맨드 큐 → 점프 공중정지 → 피격 애니메이션 → 지형 렌더링까지, 하나를 고치면 다음 레이어의 문제가 드러나는 건 게임 개발에서 너무나 흔한 패턴입니다. 중요한 건 각각을 독립적으로 해결하고, 해결할 때마다 커밋하는 습관입니다.
commandQueue[0] 한 줄이 만든 나비효과. 간단한 인덱스 참조 하나가 전체 전투 시스템의 사용성을 무너뜨릴 수 있다는 걸, 이번 디버깅으로 다시 한번 체감했습니다.