Unity 2D에서 네비게이션(길찾기) 구현하기(Path Finding, A* Algorithm)
전체 소스코드/ 사용방법 : https://github.com//simple-Astar-algorithm
알고리즘 작성 배경
유니티3D에는 Mesh Navigation을 사용하여 손 쉽게 NPC 등의 길찾기를 구현 할 수 있습니다. 하지만 유니티2D에는 Mesh가 없어서 장애물을 피해가야 하는 등의 길찾기는 쉽게 구현할 수 없습니다. 3D모드로 구현 후 90도 회전하여 2D인 척 하는 등의 방법도 있지만, 오히려 그게 더 어려워 보였기 때문에 A*알고리즘을 작성하게 되었습니다.
알고리즘 기초 개념
1. A* 알고리즘에는 기본적으로 3개의 변수가 있습니다.
g-cost : 시점으로부터 경유지 까지 경로의 가중치
h-cost : 경유지점부터 종점까지 경로의 가중치
f -cost : g-cost 와 h-cost의 합
2. 위 세 변수로 경로들을 탐색합니다.
- 매 루프당 g, h -cost는 최신화 됩니다.
- 따라서 f-cost도 최신화 됩니다. 가장 낮은 f-cost를 가지는 노드로 이동 합니다.
- 만약 최소 f-cost가 목적지라면, 탐색을 종료 합니다.
알고리즘 세부 개념
A* 알고리즘엔 OpenSet, ClosedSet 두개의 집합이 있습니다.
현재 노드의 8방향 전부의 f,g,h - cost를 계산 합니다. 만약 OpenSet에 8방향 노드들이 존재하지 않는다면 각각 OpenSet에 넣어 줍니다.
OpenSet에 있는 원소들의 f-cost 값이 현재 플레이어가 도달한 노드의 f-cost 값 보다 작으면 다음 도달해야 할 값으로 그 OpenSet의 노드를 지정하고, 지정된 노드를 ClosedSet으로 넣습니다. 물론 OpenSet의 해당 값은 삭제 합니다.
OpenSet이 0이 될 때 까지 위 과정을 반복 합니다. 중간에 목적지에 도달하면 탈출 합니다.
알고리즘 제반 스크립트
2D에 사각 노드들을 가상으로 지정해 주어야 합니다. 그래야 장애물(unWalkable) 인지 인식을 할 수 있기 때문입니다.
따라서 Grid 클래스를 지정 해주어야 합니다. 이 클래스는 이동 여부를 결정지을 격자의 크기를 조정할 수 있습니다.
Grid 소스 코드 :
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Grid : MonoBehaviour { public bool displayGridGizmos; public Transform player; public LayerMask OBSTACLE; public Vector2 gridWorldSize; public float nodeRadius; Node[,] grid; float nodeDiameter; int gridSizeX, gridSizeY; private void Awake() { nodeDiameter = nodeRadius * 2; gridSizeX = Mathf.RoundToInt(gridWorldSize.x / nodeDiameter); gridSizeY = Mathf.RoundToInt(gridWorldSize.y / nodeDiameter); CreateGrid(); } [SerializeField] public Listpath; void CreateGrid() { grid = new Node[gridSizeX, gridSizeY]; // 지정된 gridWorldSize의 좌측 최하단의 좌표를 구한다. Vector2 worldBottomLeft = (Vector2)transform.position - Vector2.right * gridWorldSize.x / 2 - Vector2.up * gridWorldSize.y / 2; for (int x = 0; x GetNeighbours(Node node) { List neighbours = new List (); for(int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { if(x == 0 && y == 0) { continue; } int checkX = node.gridX + x; int checkY = node.gridY + y; if(checkX >= 0 && checkX < gridSizeX && checkY >= 0 && checkY < gridSizeY) { neighbours.Add(grid[checkX, checkY]); } } } return neighbours; } // 노드의 위치를 월드좌표로 바꾼다. public Node NodeFromWorldPoint(Vector2 worldPosition) { float percentX = (worldPosition.x + gridWorldSize.x / 2) / gridWorldSize.x; float percentY = (worldPosition.y + gridWorldSize.y / 2) / gridWorldSize.y; percentX = Mathf.Clamp01(percentX); percentY = Mathf.Clamp01(percentY); int x = Mathf.RoundToInt((gridSizeX - 1) * percentX); int y = Mathf.RoundToInt((gridSizeY - 1) * percentY); return grid[x, y]; } // Scene View에 기즈모로 격자 그려주는 코드 // 플레이어 : 청록색, 장애물 : 빨강색, 경로 : 검정색 private void OnDrawGizmos() { Gizmos.DrawWireCube(transform.position, new Vector2(gridWorldSize.x, gridWorldSize.y)); if(grid != null ) { Node playerNode = NodeFromWorldPoint(player.position); foreach (Node n in grid) { Gizmos.color = (n.walkable) ? Color.white : Color.red; if(path != null) { if (path.Contains(n)) Gizmos.color = Color.black; } if (playerNode == n) Gizmos.color = Color.cyan; Gizmos.DrawCube(n.worldPosition, Vector2.one * (nodeDiameter - 0.1f)); } } } }
A* 알고리즘 소스코드
<
/div>
IEnumerator FindPath(Vector2 startPos, Vector2 targetPos) { Node startNode = grid.NodeFromWorldPoint(startPos); Node targetNode = grid.NodeFromWorldPoint(targetPos); bool pathSuccess = false; if (startNode.walkable && targetNode.walkable) { ListopenSet = new List (); HashSet closedSet = new HashSet (); openSet.Add(startNode); while (openSet.Count > 0) { Node currentNode = openSet[0]; for (int i = 1; i < openSet.Count; i++) { if (openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost) { currentNode = openSet[i]; } } openSet.Remove(currentNode); closedSet.Add(currentNode); if (currentNode == targetNode) { if(pathSuccess == false) { PushWay( RetracePath(startNode, targetNode) ) ; } pathSuccess = true; break; } foreach (Node neighbour in grid.GetNeighbours(currentNode)) { if (!neighbour.walkable || closedSet.Contains(neighbour)) continue; int newMovementCostToNeighbour = currentNode.gCost + GetDistance(currentNode, neighbour); if (newMovementCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour)) { neighbour.gCost = newMovementCostToNeighbour; neighbour.hCost = GetDistance(neighbour, targetNode); neighbour.parent = currentNode; if (!openSet.Contains(neighbour)) { openSet.Add(neighbour); } } } } } } Vector2[] RetracePath(Node startNode, Node endNode) { List path = new List (); Node currentNode = endNode; while(currentNode != startNode) { path.Add(currentNode); currentNode = currentNode.parent; } path.Reverse(); grid.path = path; Vector2[] wayPoints = SimplifyPath(path); return wayPoints; } Vector2[] SimplifyPath(List path) { List wayPoints = new List (); for(int i = 0; i < path.Count; i++) { wayPoints.Add(path[i].worldPosition); } return wayPoints.ToArray(); }
길찾기 알고리즘을 찾아보던 중에 2D는 어떻게 하나 싶었는데. 비슷한 고민을 하신 분이 계셨군요 ㅎㅎ 글 잘 읽었습니다!
답글삭제