-
A* 알고리즘을 사용한 길찾기 구현Unity/제로부터 구현 2023. 12. 7. 16:15728x90
오늘의 구현
A* 알고리즘을 사용한 길 찾기 구현
A* 알고리즘이란 최단 경로 탐색 알고리즘으로 시작점과 도착점 사이의 최단 경로를 찾는 데 사용됩니다.
BFS 알고리즘을 알고 있다면 이해하기가 더 쉬울꺼라 생각합니다.
두 알고리즘의 차이점이라면 BFS 는 너비우선 탐색이고 A* 알고리즘은 각 노드의 비용을 계산해 최단 경로를 찾습니다. 경로 찾는데 효과적이지만 탐색 속도가 느릴 수 있습니다.
A* 알고리즘의 코스트 계산은 F = G+H 로
F는 예상비용 , G는 경로 비용,
H는 경로 평가 함수로, 일반적으로 노드 사이의 거리, 장애물 유무 등을 고려해 계산함으로, 이 경로 평가 함수 설정에 따라 알고리즘의 성능이 결정됩니다. 여기서는 단순하게 출발 지점과 도착지점 사이의 직선거리로 계산할 것입니다.(맨헤튼 휴리스틱)
1. 탐색 방향을 먼저 정해봅니다.
탐색방향은 상하좌우 4방향 또는 대각선을 포함한 8 방향을 선택합니다. 저는 8 방향으로 구현했으며 밑변 높이 1인 직각삼각형의 빗변은 루트 2의 길이를 가지므로 대각선을 이동할 땐 코스트를 더 주도록 합니다.
int[] moveX = new int[] {-1, 0 , 1 , 0 , 1 , 1 , -1 , -1 }; int[] moveY = new int[] { 0,-1 , 0 , 1 , 1 , -1 , -1 , 1 }; int[] cost = new int[] { 10, 10, 10, 10, 14, 14 , 14 , 14 };
2.while문
bfs랑 비슷하게 시작합니다.
큐에서 경로를 꺼내올 것이고, 꺼내면 방문처리를 해줍니다. (닫힌목록에 넣는다라고도 합니다)
큐에서 꺼낸 요소의 위치값이 도착지점과 같다면 반복을 멈춥니다.
while(qDot.Count > 0) { Dot node = qDot.Dequeue(); close[node.x, node.y] = true; if (node.x == endX && node.y == endY) { break; } }
3. 구현
열린 목록을 만들어 줍니다.
포문으로 8방향을 확인하면서, 보드판의 범위를 넘지 않고 방문하지 않은 dot만 열린 목록에 넣어줄 것입니다.
IsValidPosition() 함수로 보드판의 유효범위를 확인해 줍니다.
- 대각선으로 이동할때 장애물을 대각선으로 지나쳐 지나가지 않게 로직을 추가해 줬습니다.
List<Dot> openList = new List<Dot>(); for (int i = 0; i< moveX.Length; i++) { int nextX = node.x + moveX[i]; int nextY = node.y + moveY[i]; if( IsValidPosition(nextX,nextY) && !close[nextX,nextY]) { } }
4. 예상 코스트 구하기, parent 설정
현재노드에 이동코스트를 더해줍니다.
맨헤튼 휴리스틱으로 h 값을 구해줍니다.
이동했을때 f 값이 현재 노드의 f값 보다 크다면 현재 노드가 도착지점으로부터 더 가깝기때문에 이동했을때의 dot을 열린 목록에 넣을 필요가 없습니다.
해당 dot의 g,h값이 제대로 반영됬는지 확인하기위해 dot의 g,h를 설정해줬습니다.
위 알고리즘에서 가장 중요하다고 생각하는 부분으로 Parent를 설정하는 것 입니다.
현재 위치에서 이동할수 있는 8방향을 탐색하면서 이동했을때 어느 방향에서 왔는지를 설정해 주는 것 입니다. 최종적으로 도착지점에 도착했을때 도착지점으로 부터 역순으로 parent를 탐색하면서 최단경로를 뽑아낼 것입니다.
int g = node.g + cost[i]; int h = 10 * (Mathf.Abs(nextX - endX) + Mathf.Abs(nextY - endY)); if (open[nextX,nextY] > g + h) { open[nextX, nextY] = g + h; dotArray[nextX, nextY].GetComponent<Dot>().SetMoveCost(g, h); dotArray[nextX, nextY].GetComponent<Dot>().SetParent(node.x, node.y); openList.Add(dotArray[nextX, nextY].GetComponent<Dot>()); }
포문으로 8방향을 다 탐색했으면 오픈목록에서 f 값이 가장 작은 노드부터 큐에 넣어 위 과정을 반복해줍니다.
var list = openList.OrderBy(x => x.GetComponent<Dot>().f).ToArray(); foreach(var item in list) { qDot.Enqueue(item); }
ReverseParent(dotArray[endX, endY].GetComponent<Dot>());
while문이 끝나면 도착지점으로 부터 역순으로 parent를 탐색해 parent라는 리스트에 넣어 줍니다.
제대로 최단경로를 찾았는지 확인을 해봅니다.
void ReverseParent(Dot endDot) { Dot current = endDot; while(true) { if(current.x == startX && current.y == startY) { break; } parent.Add(current); current = dotArray[current.parentX, current.parentY].GetComponent<Dot>(); } parent.Reverse(); foreach (Dot dot in parent) { Debug.Log("Path: " + dot.x + ", " + dot.y); } }
parent의 dot을 하나씩 꺼내 시작지점의 Dot을 최단경로로 이동시켜봅니다.
IEnumerator GoCo() { foreach(Dot dot in parent) { dotArray[startX, startY].GetComponent<Dot>().transform.position = new Vector3(dot.x, dot.y); yield return new WaitForSeconds(.1f); } }
728x90'Unity > 제로부터 구현' 카테고리의 다른 글
플레이어를 기준으로 n크기의 반지름을 가진 원의 접선 그리기. (0) 2023.12.13 Electric Attack 구현하기 ( +Overlap) (0) 2023.12.11 Editor 구현하기 (1) 2023.12.05 같은 맵 계속 반복 시키기(3D). (0) 2023.12.01 1인칭 시점 구현하기 + 이동 (0) 2023.11.30