- 8비트 탑다운 Zelda풍 게임에서 몬스터 추적을 구현하려면 단순 직선 이동만으로는 부족해, Dijkstra와 A*를 비교하며 게임용 경로 탐색의 절충점을 찾음
- 직선 이동은 벽에 막히면 멈추지만 wall-sliding을 붙이면 벽을 따라 움직일 수 있어 조작감은 좋아지고, 몬스터를 지형에 가두는 전략 요소도 만들 수 있음
- Dijkstra 알고리듬은 최단 경로를 보장하지만 시작 노드 주변을 폭넓게 탐색하므로, 목적지가 매 프레임 바뀌는 게임에서는 필요한 다음 방향보다 많은 계산을 하게 됨
- A*는 목적지까지의 거리로 탐색 우선순위를 정해 목적지 방향을 먼저 살피고, 벽을 만나면 주변 노드를 조사하면서 이미 본 노드는 재방문하지 않아 우회 경로를 찾을 수 있음
- 게임 맵에서는 인접 리스트를 미리 만들지 않는 암시적 그래프와 타일 단위 탐색, 반복 깊이 제한 같은 기하 기반 휴리스틱으로 속도와 구현 난도를 조정할 수 있음
게임 맥락과 기본 요구
- PPU466 기반의 8비트 탑다운 Zelda풍 게임에서 몬스터가 플레이어를 추적해야 했음
- PPU466은 PICO-8 같은 판타지 콘솔과 비슷하게 8비트 그래픽, 타일당 4색, 고정 배경, 적은 수의 스프라이트 제약을 가짐
- 목표는 몬스터가 플레이어를 따라오되, 단순히 벽에 막혀 멈추거나 원치 않는 방식으로 갇히지 않게 하는 것임
직선 이동과 wall-sliding
- 가장 단순한 방식은 몬스터와 플레이어 사이에 직선을 그어 그 방향으로 움직이는 것임
- 이 방식만 쓰면 몬스터가 벽에 닿는 순간 멈춤
- wall-sliding을 적용하면 벽에 부딪혔을 때 멈추지 않고 벽을 따라 이동함
- 플레이어 이동에서는 벽과 모서리 근처 조작을 더 반응성 있게 만드는 기법으로, 거의 모든 게임에서 쓰임
- Pac-Man 이후 사용되어 왔고, Pac-Man Championship Edition DX+는 플레이어가 wall-slide할 때 스파크 효과를 추가함
- 직선 이동에 wall-sliding을 붙이면 몬스터를 특정 지형에 가둘 수 있음
- 일부 게임은 이를 전략 요소로 사용하며, Runescape의 safespotting이 예시임
- 이 게임에서는 원하는 동작이 아니어서 실제 경로 탐색 알고리듬을 검토함
Dijkstra 알고리듬의 한계
- Dijkstra 알고리듬은 구현이 직관적이고 최단 경로를 보장함
- 문제는 필요한 것보다 너무 많은 일을 한다는 점임
- 시작 노드에서 그래프의 모든 다른 노드까지 최단 경로를 찾음
- 목적지 노드를 찾으면 멈출 수는 있지만, 특정 목적지 방향으로 탐색을 유도할 방법은 없음
- 비디오게임에서는 플레이어가 계속 움직이므로 몬스터의 목적지가 매 프레임 변경됨
- 몬스터에게 필요한 것은 전체 경로보다 지금 어느 방향으로 움직일지에 가까움
- 맵의 모든 픽셀이나 타일에 대한 최단 경로를 미리 계산할 수는 있지만, 메모리를 많이 사용함
- 레거시 플랫폼이나 자원이 제한된 플랫폼에서는 Dijkstra가 적합하지 않음
A*가 게임 경로 탐색에 맞는 이유
- A* Search Algorithm은 시작 노드에서 목적지까지의 거리 정보를 사용해 탐색 우선순위를 정함
- 첫 단계에서는 목적지까지 직선으로 가려는 방향을 우선 시도함
- Dijkstra와 달리 필요하지 않으면 반대 방향 탐색에 많은 시간을 쓰지 않음
- 벽이 경로를 막으면 주변 노드를 조사해 벽을 우회하려고 함
- Dijkstra처럼 이미 본 노드를 재방문하지 않기 때문에, 되돌아가기가 많이 필요해도 결국 우회 경로를 찾을 수 있음
- 예시에서 A*를 쓰는 몬스터는 벽 뒤에 갇히지 않음
암시적 그래프 자료구조
- 교과서식 그래프는 노드 목록과 인접 행렬 또는 인접 리스트로 표현되지만, 게임에서는 인접 노드를 더 유연하게 만들 수 있음
- 예를 들어 256×240 픽셀 화면에서 각 픽셀 좌표를 하나의 노드로 볼 수 있음
- 인접 픽셀은 위, 아래, 왼쪽, 오른쪽, 대각선 4개를 포함한 8방향임
- 상하좌우 이동 가중치는 1, 대각선 이동 가중치는 √2, 즉 약 1.4임
- 거대한 인접 리스트를 미리 만들지 않고, 실제 방문할 노드에 대해서만 즉석에서 생성할 수 있음
- 벽 위에 있거나 다른 스프라이트가 차지한 픽셀은 유효한 몬스터 위치가 아니므로 동적으로 인접 리스트에서 제외함
- 이 방식이면 맵 에디터에서 인접 불가 노드를 수동으로 제외하지 않아도 됨
맵 기하를 반영한 휴리스틱
- A*의 일부 요소는 맵의 기하 구조에 맞춰 직접 조정할 수 있음
-
스텝 크기
- 픽셀을 노드로 쓰는 대신, 2D 타일 기반 게임에서는 타일을 노드로 사용할 수 있음
- 타일 단위 탐색은 플레이어까지 경로를 찾는 반복 횟수를 크게 줄여 검색을 빠르게 함
- 이 경우 경로는 정확한 한 프레임 단위 이동 목록이 아니라 몬스터가 가야 할 방향들의 시퀀스에 가까움
- 몬스터는 보통 프레임당 1타일 속도로 움직이지 않으므로, 타일 기반 경로라도 실제로 필요한 정보는 플레이어에게 도달할 수 있는 방향임
- 픽셀 기반 경로도 같은 성격을 가지며, 몬스터가 프레임당 1픽셀이나 정수 픽셀 단위로 움직이지 않을 수도 있음
-
반복 깊이
- A*에서는 노드가 우선순위 큐에서 나올 때, 그 노드는 지금까지 본 최선 경로의 마지막 단계임
- 고정된 반복 횟수에서 알고리듬을 멈추면 목적지까지의 최단 경로에 대한 현재까지의 최선 추정 경로를 얻을 수 있음
- 알고리듬을 끝까지 실행하지 않아도 합리적인 진행 방향을 얻을 수 있음
- 최대 반복 깊이는 레벨의 기하 구조에 맞게 조정해야 함
- 깊이가 너무 작으면 몬스터가 여전히 벽 뒤에 갇힐 수 있음
- 예시에서는 고정 깊이 30타일에서 플레이어 위치에 따라 몬스터가 갇혀 진행하지 못함
- A*를 매 프레임 다시 계산하기 때문에 루프가 생길 수 있음
- 벽에 도달한 첫 프레임에는 아래로 가야 한다고 계산함
- 다음 프레임에는 위로 가야 한다고 계산함
- 이 반복으로 몬스터가 루프에 갇힘
- 플레이어가 몬스터의 탐색 범위 안으로 들어오면 올바른 경로를 찾을 수 있음
- 고정 깊이
1에서는 이 현상이 더 극단적으로 나타나며, 몬스터는 플레이어와의 유클리드 거리가 가장 짧은 픽셀로 계속 돌아감
사전 계산의 절충안
- 더 정교하게 만들려면 맵의 어떤 위치에서든 A*가 경로를 찾는 데 필요한 최대 깊이를 미리 계산할 수 있음
- Dijkstra식 전체 경로 사전 계산과 달리 저장해야 하는 것은 그 최대값 하나임
- 해당 최대 깊이가 주어지면 A*는 실시간으로 유효한 경로를 찾을 수 있음