A-Star ( A* )

2025. 11. 10. 01:03·📊Algorithm/탐색

A-Star 알고리즘 ( A* )

A-Star 알고리즘은 게임 개발이나 인공지능에서 가장 자주 쓰이는 경로 탐색 알고리즘이다.

유니티의 NavMesh 같은 시스템도 기본적으로 A* 의 원리를 기반으로 만들어졌다.

이름 A-Star ( A* ) 알고리즘
목적 출발지에서 목적지까지 최단 경로를 찾기
특징 현재까지의 거리 + 앞으로의 예측 거리를 동시에 고려
사용 예시 미로 찾기 , 길 찾기 AI , NPC 이동 , RTS 유닛 이동 등
자료구조 우선순위 큐 ( Priority Queue ) 또는 Open / Closed List 사용

 

A* 는 각 노드의 점수를 계산하면서 최적의 경로를 탐색한다.

 

 

1. 점수 계산식

각 노드마다 세 가지 값을 계산한다.

f(n) = g(n) + h(n)
g(n) 시작점에서 현재 노드까지의 실제 비용 ( 실제 이동 거리 )
h(n) 현재 노드에서 목표까지의 예상 비용 ( 휴리스틱 , Heuristic )
f(n) 총 비용 ( A* 는 f 가 가장 작은 노드부터 탐색한다 )

 

 

2. Open / Closed 리스트 구조

Open List 아직 탐색하지 않은 노드 목록
Closed List 이미 탐색을 마친 노드 목록

 

▼ 탐색 과정

  1. 시작 노드를 Open List 에 추가한다
  2. Open List 중 f(n) 이 가장 작은 노드를 선택한다
  3. 그 노드를 확장해서 이웃 노드들의 g , h , f 를 계산한다
  4. 이미 더 좋은 경로가 있으면 갱신하지 않는다
  5. 목표 노드에 도달하면 탐색 종료한다

 

휴리스틱 함수 ( Heuristic )

휴리스틱은 "얼마나 남았을까" 를 예측하는 함수이다.

실제 게임에서는 보통 거리 계산으로 사용한다.

종류 수식 특징
맨해튼 거리 ( Manhattan ) ` x1 - x2
유클리드 거리 ( Euclidean ) √((x1 - x2)² + (y1 - y2)²) 대각선 이동이 가능할 때
체비쇼프 거리 ( Chebyshev ) max( x1 - x2

 

 

▼격자형 맵 기준 예시 코드

using System.Collections.Generic;
using UnityEngine;

public class AStar
{
    public class Node
    {
        public Vector2Int position;
        public float g; // 시작점 → 현재까지의 실제 비용
        public float h; // 현재 → 목표까지의 예상 비용
        public float f => g + h; // 총 점수
        public Node parent;

        public Node(Vector2Int pos) { position = pos; }
    }

    public List<Vector2Int> FindPath(Vector2Int start, Vector2Int goal, bool[,] map)
    {
        List<Node> open = new List<Node>();
        HashSet<Vector2Int> closed = new HashSet<Vector2Int>();

        Node startNode = new Node(start);
        open.Add(startNode);

        while (open.Count > 0)
        {
            // f 값이 가장 작은 노드 선택
            open.Sort((a, b) => a.f.CompareTo(b.f));
            Node current = open[0];
            open.RemoveAt(0);

            if (current.position == goal)
                return ReconstructPath(current); // 경로 완성

            closed.Add(current.position);

            // 4방향 탐색 (상하좌우)
            Vector2Int[] dirs = {
                Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right
            };

            foreach (var dir in dirs)
            {
                Vector2Int nextPos = current.position + dir;
                if (!IsInside(map, nextPos) || !map[nextPos.x, nextPos.y]) continue;
                if (closed.Contains(nextPos)) continue;

                float g = current.g + 1;
                float h = Mathf.Abs(nextPos.x - goal.x) + Mathf.Abs(nextPos.y - goal.y);
                Node neighbor = new Node(nextPos) { g = g, h = h, parent = current };

                // OpenList에 더 짧은 경로가 있으면 무시
                Node existing = open.Find(n => n.position == nextPos);
                if (existing == null || g < existing.g)
                {
                    if (existing != null) open.Remove(existing);
                    open.Add(neighbor);
                }
            }
        }

        return null; // 경로 없음
    }

    private List<Vector2Int> ReconstructPath(Node node)
    {
        List<Vector2Int> path = new List<Vector2Int>();
        while (node != null)
        {
            path.Add(node.position);
            node = node.parent;
        }
        path.Reverse();
        return path;
    }

    private bool IsInside(bool[,] map, Vector2Int pos)
    {
        return pos.x >= 0 && pos.y >= 0 && pos.x < map.GetLength(0) && pos.y < map.GetLength(1);
    }
}

 

▼ 예시 코드

using System.Collections.Generic;
using UnityEngine;

namespace PathFind
{
    public class Node
    {
        public bool IsWall { get; set; } = false;   // 진행 불가 판정
        public bool IsClosed { get; set; } = false; // 닫힌(연산이 종료된)노드인지 판단
        public Node Prev { get; set; }              // 이전 노드
        public int X { get; set; }                  // X축 좌표
        public int Y { get; set; }                  // Y축 좌표
        public int G { get; set; }                  // 현재까지 이동 거리
        public int H { get; set; }                  // 목적지까지의 직선 거리
        public int F { get => G + H; }
    }

    [System.Serializable] public class Astar
    {
        // 탐색을 진행할 맵
        [field: SerializeField] public Map _map { get; set; }

        // 대각선 탐색 여부
        [SerializeField] private bool _diagonalPath;

        // 대각선 탐색시 충돌되는 벽 우회 여부
        [SerializeField] private bool _disableDiagonalObstacle;

        // 전체 노드를 관리하는 2차원 배열
        private Node[,] NodeArray;

        // 탐색 대상/비대상 노드 리스트
        private List<Node> OpenNode = new List<Node>();

        // 최종적으로 경로로 선정된 노드 리스트
        public List<Node> FinalNodeList { get; private set; } = new List<Node>();

        // 시작, 대상, 이전 노드 정보
        private Node StartNode;
        private Node TargetNode;
        private Node CurNode;

        private void Init(Vector2Int StartPos, Vector2Int TargetPos)
        {
            // 노드 배열 크기 지정
            int sizeX = _map.EndArea.x - _map.StartArea.x + 1;
            int sizeY = _map.EndArea.y - _map.StartArea.y + 1;
            NodeArray = new Node[sizeX, sizeY];

            // 노드 배열만큼 반복
            for (int x = 0; x < sizeX; x++)
            {
                for (int y = 0; y < sizeY; y++)
                {
                    NodeArray[x, y] = new Node();

                    // 좌표 영역의 감지된 노드 인식
                    foreach (Collider2D col in Physics2D.OverlapCircleAll(new Vector2(x + _map.StartArea.x, y + _map.StartArea.y), 0.4f))
                    {
                        // 감지된 노드의 레이어가 "Wall"이라면
                        if (col.gameObject.layer == LayerMask.NameToLayer("Wall")) 
                        {
                            // 벽 여부를 true로
                            NodeArray[x, y].IsWall = true;
                        }
                    }
                    
                    // 노드에 좌표 주입
                    NodeArray[x, y].X = x + _map.StartArea.x;
                    NodeArray[x, y].Y = y + _map.StartArea.y;
                }
            }
            
            // 출발지점과 도착지점 노드를 각 Start, Target Node로 지정
            StartNode = NodeArray[StartPos.x - _map.StartArea.x, StartPos.y - _map.StartArea.y];
            TargetNode = NodeArray[TargetPos.x - _map.StartArea.x, TargetPos.y - _map.StartArea.y];

            // 시작점을 열린 노드로 미리 추가
            OpenNode.Add(StartNode);
        }

        public void PathFinding(Vector2Int StartPos, Vector2Int TargetPos)
        {
// 0. 초기화 -----
            Init(StartPos, TargetPos);

            while (true)
            {
                // 열린 노드가 하나도 없으면 종료(경로를 못 찾았을 때)
                if(OpenNode.Count <= 0)
                {
                    break;
                }

                // 오픈 노드 리스트의 첫 번째 노드를 현재 노드로 지정
                CurNode = OpenNode[0];

// 1. 노드 교체(이동과정) ----- 
                // 열린 노드 리스트 수만큼 반복
                for (int i = 1; i < OpenNode.Count; i++)
                {
                    // 대상 원소와 현재 노드를 비교해서
                    // 대상 원소의 F가 현재 노드의 F이하이고 목적지까지 직선 거리가 더 가깝다면
                    if (OpenNode[i].F <= CurNode.F && OpenNode[i].H < CurNode.H) 
                    {
                        // 현재 노드는 비교했던 노드로 교체
                        CurNode = OpenNode[i];
                    }
                }

                // 현재 노드의 닫힘 여부를 true로 바꾸고 열린 노드 리스트에서 삭제한다.
                CurNode.IsClosed = true;
                OpenNode.Remove(CurNode);

// Final. 알고리즘 종료 단계 ----- 
                if (CurNode == TargetNode)
                {
                    int TotalLengh = CurNode.G;
                    Node TargetCurNode = TargetNode;

                    // 각 노드들에 등록된 이전 노드들을 최종 경로에 추가한다.
                    // 현재 도착지 노드가 시작점 노드라면 반복 종료
                    while (TargetCurNode != StartNode)
                    {
                        FinalNodeList.Add(TargetCurNode);
                        TargetCurNode = TargetCurNode.Prev;
                    }

                    // 시작점 노드를 최종 경로에 추가하고 순서를 역순으로 바꾼다.
                    FinalNodeList.Add(StartNode);
                    FinalNodeList.Reverse();
                    Debug.Log(TotalLengh);
                    return;
                }

// 2. 주변의 노드를 열린 노드에 추가 ----- 
                // 대각방향 탐색 여부가 true 라면
                if (_diagonalPath)
                {
                    // 대각선 노드들 포함
                    AddOpen(Xdir.Right, Ydir.Up);
                    AddOpen(Xdir.Left, Ydir.Up);
                    AddOpen(Xdir.Left, Ydir.Down);
                    AddOpen(Xdir.Right, Ydir.Down);
                }
                // 상하좌우 노드 추가
                AddOpen(Xdir.Center, Ydir.Up);
                AddOpen(Xdir.Right, Ydir.Center);
                AddOpen(Xdir.Center, Ydir.Down);
                AddOpen(Xdir.Left, Ydir.Center);
            }
        }

        public enum Xdir { Left = -1, Center, Right }
        public enum Ydir { Down = -1, Center, Up }

        void AddOpen(Xdir x, Ydir y)
        {
            int X = CurNode.X + (int)x;
            int Y = CurNode.Y + (int)y;

            // 추가적인 노드가 맵의 범위를 벗어나지 않을 때
            if (X >= _map.StartArea.x && 
                X < _map.EndArea.x + 1 && 
                Y >= _map.StartArea.y && 
                Y < _map.EndArea.y + 1
                )
            {
                int CurX = CurNode.X - _map.StartArea.x;
                int CurY = CurNode.Y- _map.StartArea.x;
                X = CurNode.X + (int)x - _map.StartArea.x;
                Y = CurNode.Y + (int)y - _map.StartArea.x;
                
                // 해당 좌표의 노드가 벽이 아니면서 && 닫혀 있지 않다면 
                if(!NodeArray[X, Y].IsWall && 
                    !NodeArray[X, Y].IsClosed)
                {
                    // 대각선 허용시
                    if (_diagonalPath) 
                    {
                        // 진행 대각선 방향에 인접한 두 노드가 모두 벽일때
                        if (NodeArray[CurX, Y].IsWall && 
                            NodeArray[X, CurY].IsWall) 
                        {
                            // □  □  □  □  □ 
                            // □  □  □  □  □ 
                            // □  □  ■  □  □ 
                            // □  □  ↗  ■  □ 
                            // □  □  □  □  □
                            return; // 함수 중단(추가하지 않는다)
                        }
                    }
                    
                    // 대각선 이동시 모서리를 통과하지 않을거면
                    if (_disableDiagonalObstacle) 
                    {
                        if (NodeArray[CurX, Y].IsWall || 
                            NodeArray[X, CurY].IsWall) 
                        {
                            // 진행 대각선 방향에 인접한 상하좌우 방향의 노드 중에서 벽이 있으면
                            // □  □  □  □  □  예시
                            // □  □  □  □  □ 
                            // □  ↗  ■  □  □ 
                            // □  □  □  □  □ 
                            // □  □  □  □  □
                            return;
                        }
                    }
                    
                    // 이웃(주변)노드로 두고
                    Node NeighborNode = NodeArray[X, Y];
                    
                    // 노드당 이동 거리는 직선 10, 대각선 14
                    int MoveCost;
                    if(CurX == X || CurY == Y)
                    {
                        MoveCost = CurNode.G + 10;
                    }
                    else
                    {
                        MoveCost = CurNode.G + 14;
                    }

                    // 이동비용이 주변 노드G보다 작거나 || 
                    // 오픈 리스트에 주변 노드가 포함되어 있지 않다면
                    if (MoveCost < NeighborNode.G || !OpenNode.Contains(NeighborNode))
                    {
                        // 주변 노드에 G, H 주입하고 이전 노드 변수에 현재 노드를 주입하고
                        NeighborNode.G = MoveCost;
                        NeighborNode.H = (Mathf.Abs(NeighborNode.X - TargetNode.X) + Mathf.Abs(NeighborNode.Y - TargetNode.Y)) * 10;
                        NeighborNode.Prev = CurNode;

                        // 열린 리스트에 추가
                        OpenNode.Add(NeighborNode);
                    }
                }
            }
        }
    }
}

 

▼ 예시 코드

using NUnit.Framework;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

class AStarNode
{
    public int Index; // 노드 인덱스 , 구별값 ABCD 0123
    public Vector2 pos; // 노드 위치
    public float G; // 시작점에서 지금 위치까지의 실제 비용
    public float H; // 휴리스틱 , 예측 비용
    public float F => G + H; // 총 점수 , 가중치
    public AStarNode Parent; // 부모 노드 , 경로 복원 용도

    // 생성자
    public AStarNode(int index, Vector2 pos, float g, float h, AStarNode parent = null)
    {
        Index = index;
        this.pos = pos;
        G = g;
        H = h;
        Parent = parent;
    }

    // F값 기준으로 정렬을 위한 비교 함수
    public int CompareTo(AStarNode other)
    {
        // F값으로 우선 비교
        int fCompare = F.CompareTo(other.F);
        if (fCompare != 0)
        {
            return fCompare;
        }
        // F값이 같을 경우 인덱스 기준 비교
        return Index.CompareTo(other.Index);
    }
}

public class AStarExample : MonoBehaviour
{
    // 두 좌표 사이의 거리를 휴리스틱으로 사용하기로 함
    static float Heuristic(Vector2 a, Vector2 b) => Vector2.Distance(a, b);

    private void Start()
    {
        int n = 4; // 노드 4개 사용
        int startIndex = 0; // 시작 위치는 0번 노드 A
        int goalIndex = 3; // 종료 위치는 3번 노드 D

        // 각각의 노드에 좌표 설정해주기
        Vector2[] positions = new Vector2[4]
        {
            new Vector2(0, 0),  //A
            new Vector2(2, 0),  //B
            new Vector2(0, 1),  //C
            new Vector2(1, 1)   //D
        };

        // 그래프 구성
        List<(int to, float cost)>[] graph = new List<(int to, float cost)>[n];
        for (int i = 0; i < n; i++)
        {
            graph[i] = new List<(int to, float cost)>();
        }

        // 비용 정보 추가
        graph[0].Add((1, 5)); // A -> B (1)
        graph[1].Add((2, 2)); // B -> C (2)
        graph[1].Add((3, 3)); // B -> D (3)
        graph[2].Add((3, 5)); // C -> D (5)

        // G 점수 배열 초기화
        float[] gScore = new float[n];
        for (int i = 0; i < n; i++)
        {
            gScore[i] = float.MaxValue;
        }

        // 시작 위치
        gScore[startIndex] = 0;

        // 방문 여부
        bool[] closed = new bool[n];

        // 오픈 리스트 , 탐색 대상들
        var openList = new List<AStarNode>();

        // 시작 지점 추가
        openList.Add(new AStarNode(startIndex, positions[startIndex], 0,
            Heuristic(positions[startIndex], positions[goalIndex])));

        // 탐색 루프
        while(openList.Count > 0)
        {
            // F값 기준 정렬
            openList.Sort((a, b) => a.CompareTo(b));

            // 가장 F가 낮은 노드 찾아오기
            var current = openList[0];

            // 방문한 노드는 제거
            openList.RemoveAt(0);

            // 이미 방문한 노드면 패스
            if (closed[current.Index])
            {
                continue;
            }

            // 방문 처리
            closed[current.Index] = true;

            // 목표에 도달했으면 경로 복원
            if(current.Index == goalIndex)
            {
                // 경로 저장용 리스트
                var path = new List<int>();
                var temp = current;
                
                // 부모 따라가면서 역추적
                while (temp != null)
                {
                    path.Add(temp.Index);
                    temp = temp.Parent;
                }

                // D - C - B - A

                // 역순으로 들어갔으니까 뒤집기
                path.Reverse();

                // A - B - C - D

                Debug.Log("A* Path " + string.Join(" -> ", path));
                Debug.Log("Distance " + current.G);

                break;
            }

            // 인접 노드 탐색
            foreach(var neighbor in graph[current.Index])
            {
                // 이미 처리한 노드는 패스
                if (closed[neighbor.to])
                {
                    continue;
                }

                // 현재의 G + 이동 비용 계산
                float t = current.G + neighbor.cost;

                // 더 짧은 경로 발견시 갱신
                if(t < gScore[neighbor.to])
                {
                    gScore[neighbor.to] = t;

                    // H값 게산
                    float h = Heuristic(positions[neighbor.to], positions[goalIndex]);

                    // 새로운 노드 추가
                    openList.Add(new AStarNode(neighbor.to, positions[neighbor.to], t, h, current));
                }
            }
        }
    }
}
장점 단점
항상 최적의 경로를 찾는다 ( 휴리스틱이 올바르다면 ) 맵이 크면 연산량이 많아진다
휴리스틱 조정으로 현실적인 경로 표현이 가능하다 실시간 게임에서는 속도 조절이 필요하다
다목적 ( 2D , 3D , 격자형 , 그래프형 등 가능 ) 복잡한 지형에서는 구현 난이도가 증가한다

 

 

▼ 게임에서의 활용

RTS 병사 이동 유닛이 장애물을 피해 가장 짧은 길로 이동한다
좀비 추격 AI 플레이어의 위치까지 최단 경로를 계산한다
타워 디펜스 적이 최적 경로로 길을 따라 전진한다
맵 네비게이션 NevMesh 내부적으로 A* 변형 사용

 

 

 

참고 자료

https://qiao.github.io/PathFinding.js/visual/

 

'📊Algorithm > 탐색' 카테고리의 다른 글

Dijkstra & A-Star  (0) 2025.11.20
Dijkstra 다익스트라  (0) 2025.11.20
너비 우선 탐색 ( BFS )  (0) 2025.11.14
깊이 우선 탐색 ( DFS )  (0) 2025.11.14
탐색 알고리즘 ( Search Algorithms )  (0) 2025.11.10
'📊Algorithm/탐색' 카테고리의 다른 글
  • Dijkstra 다익스트라
  • 너비 우선 탐색 ( BFS )
  • 깊이 우선 탐색 ( DFS )
  • 탐색 알고리즘 ( Search Algorithms )
DevHoChan
DevHoChan
맨땅에서 시작하는 코딩 도전
  • DevHoChan
    Debugging Life
    DevHoChan
  • 전체
    오늘
    어제
    • 분류 전체보기 (374)
      • 🕹️Game Life (1)
      • 🖥️Computer Science (5)
      • 📖TIL (141)
        • 🔥Projects (16)
        • 💡DevTips (5)
        • 🤔발생한 문제와 해결 (5)
        • 🔮Unity Graphics (5)
        • 🎤Interview (3)
        • ✅CodingTest (9)
      • 🚀Game Release (4)
      • 🧊Unity Basic (58)
        • 📌용어 사전 (1)
        • 에디터&인터페이스 (3)
        • 디버그 (1)
        • 라이프사이클 (4)
        • 게임오브젝트 (4)
        • 프리팹 (1)
        • 오브젝트풀링 (4)
        • 애트리뷰트 (2)
        • 트랜스폼 (4)
        • 물리&충돌 (1)
        • 프레임&델타타임 (4)
        • 코루틴&이벤트 (7)
        • 수학&보정함수 (3)
        • 디자인패턴 (9)
        • UGUI (3)
        • 벡터 ( Vector ) (3)
        • 씬 ( Scene ) (2)
        • 데이터 관리 (2)
      • ⭐C Sharp (99)
        • 📌용어 사전 (1)
        • 📌문법 사전 (6)
        • 메모리 관리 (3)
        • 00. 문법 (17)
        • 01. 변수 (3)
        • 02. 자료형 (2)
        • 03. 연산자 (6)
        • 04. 조건문 (2)
        • 05. 반복문 (2)
        • 06. 배열 (3)
        • 07. 메서드(함수) (7)
        • 08. 열거형 (3)
        • 09. 구조체 (2)
        • 10. 참조 (2)
        • 11. 객체 지향 (11)
        • 12. 델리게이트 (3)
        • 13. 디자인 패턴 (7)
        • 14. LINQ (1)
        • 📂▼자료구조 (2)
        • 15-1. 제네릭 (3)
        • 15-2. 배열 (4)
        • 15-3. 리스트 (2)
        • 15-4. 스택과 큐 (2)
        • 15-5. 딕셔너리 해시테이블 (2)
        • 15-6. 트리와 그래프 (3)
      • 📊Algorithm (16)
        • BigO (2)
        • 정렬 (4)
        • 셔플 (2)
        • 탐색 (6)
        • 최적화 (1)
      • 📝Game Design (16)
      • 🤖​AI Tools (12)
        • AI 리뷰 분석 (6)
        • Player2 (0)
        • 3D 모델링 (1)
        • 2D 스프라이트 (0)
        • 이미지 (2)
        • 사운드 (1)
        • 동영상 (1)
        • 문서 (1)
      • 🌍Network (6)
      • 🌱Github (11)
        • 기본 개념 (7)
        • 명령어 (1)
        • 도구 활용 (1)
      • ⚙️Visual Studio (5)
        • 🔧설치 및 환경설정 (2)
        • ⌨️HotKey (1)
        • 🚨디버깅 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    gamedesign
    부트캠프
    자료구조
    csharp
    GitHub
    유니티
    CodingTest
    OOP
    메모리관리
    객체지향
    디자인패턴
    문법
    algorithm
    게임기획
    기획
    자료형
    unity
    c#
    til
    게임디자인
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
DevHoChan
A-Star ( A* )
상단으로

티스토리툴바