서론: 게임 AI에서의 장애물 회피 중요성

게임에서의 캐릭터 이동은 그 자체로 중요한 요소입니다. 특히, 실시간으로 변하는 환경에서 캐릭터가 장애물을 피해 이동할 수 있도록 만드는 것은 더욱 중요합니다. 예를 들어, 전투 게임에서는 적이 공격을 회피하거나, 롤플레잉 게임에서는 NPC들이 장애물을 피하며 자연스럽게 환경을 탐험할 수 있어야 합니다. 이를 가능하게 해주는 기술 중 하나가 바로 *A 알고리즘입니다. *A 알고리즘은 목표 지점까지의 최적 경로를 탐색할 때 장애물을 피할 수 있는 유연한 방법을 제공합니다.

이번 글에서는 유니티 엔진을 활용하여 *A 알고리즘**을 사용한 동적 장애물 회피 경로 탐색을 구현하는 방법에 대해 자세히 다뤄보겠습니다. 실제 게임에서 어떻게 활용될 수 있는지 다양한 예시를 들어 설명하고, 이를 위한 고급 코드 구현법도 소개할 예정입니다.


1. A 알고리즘 개요*

A* 알고리즘은 시작 지점에서 목표 지점까지 가는 최단 경로를 찾는 알고리즘으로, 비용을 계산하여 가장 효율적인 경로를 탐색합니다. 이 알고리즘은 그래프 탐색을 사용하며, 각 지점에 대한 휴리스틱 함수를 통해 탐색 효율을 높입니다.

A* 알고리즘의 핵심은 **g(n)**와 h(n) 값입니다:

  • g(n): 현재 지점까지의 이동 비용 (start부터 n까지)
  • h(n): 목표 지점까지의 예상 비용 (휴리스틱 함수로 계산)

A* 알고리즘은 **f(n) = g(n) + h(n)**을 최소화하는 경로를 선택하여 탐색합니다.


2. 유니티에서 A* 알고리즘 구현하기

이제 A* 알고리즘을 유니티에서 어떻게 구현할 수 있는지에 대해 설명하겠습니다. 기본적으로 A* 알고리즘은 그리드 기반 경로 탐색에서 많이 사용되며, 각 그리드 칸은 장애물 유무에 따라 이동 가능 여부가 결정됩니다. 장애물이 있는 칸은 통과 불가로 설정하고, 비어있는 칸은 이동 가능한 경로로 설정합니다.

예시 코드: A* 알고리즘 구현 (기본 경로 탐색)

using UnityEngine;
using System.Collections.Generic;

public class AStarPathfinding : MonoBehaviour
{
    public Transform startPoint;
    public Transform endPoint;
    public LayerMask obstacles; // 장애물 레이어

    private Vector3[] path;
    private Grid grid;

    void Start()
    {
        grid = new Grid(10, 10, 1f); // 10x10 그리드
        FindPath(startPoint.position, endPoint.position);
    }

    void FindPath(Vector3 start, Vector3 end)
    {
        Node startNode = grid.NodeFromWorldPoint(start);
        Node endNode = grid.NodeFromWorldPoint(end);

        HashSet<Node> openSet = new HashSet<Node>(); // A* open list
        HashSet<Node> closedSet = new HashSet<Node>(); // A* closed list
        openSet.Add(startNode);

        while (openSet.Count > 0)
        {
            Node currentNode = GetLowestCostNode(openSet);
            if (currentNode == endNode)
            {
                RetracePath(startNode, endNode);
                return;
            }

            openSet.Remove(currentNode);
            closedSet.Add(currentNode);

            foreach (Node neighbor in grid.GetNeighbors(currentNode))
            {
                if (closedSet.Contains(neighbor) || !neighbor.walkable)
                    continue;

                float newCostToNeighbor = currentNode.gCost + GetDistance(currentNode, neighbor);
                if (newCostToNeighbor < neighbor.gCost || !openSet.Contains(neighbor))
                {
                    neighbor.gCost = newCostToNeighbor;
                    neighbor.hCost = GetDistance(neighbor, endNode);
                    neighbor.parent = currentNode;

                    if (!openSet.Contains(neighbor))
                        openSet.Add(neighbor);
                }
            }
        }
    }

    void RetracePath(Node startNode, Node endNode)
    {
        List<Vector3> waypoints = new List<Vector3>();
        Node currentNode = endNode;

        while (currentNode != startNode)
        {
            waypoints.Add(currentNode.worldPosition);
            currentNode = currentNode.parent;
        }

        waypoints.Reverse();
        path = waypoints.ToArray();
    }

    void OnDrawGizmos()
    {
        if (path != null)
        {
            foreach (Vector3 waypoint in path)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawCube(waypoint, Vector3.one);
            }
        }
    }

    Node GetLowestCostNode(HashSet<Node> openSet)
    {
        Node lowestCostNode = null;
        foreach (Node node in openSet)
        {
            if (lowestCostNode == null || node.fCost < lowestCostNode.fCost)
                lowestCostNode = node;
        }
        return lowestCostNode;
    }

    float GetDistance(Node nodeA, Node nodeB)
    {
        float distX = Mathf.Abs(nodeA.gridX - nodeB.gridX);
        float distY = Mathf.Abs(nodeA.gridY - nodeB.gridY);
        return distX + distY;
    }
}

코드 설명

  • Grid: 게임 공간을 그리드로 나누고, 각 그리드의 상태(이동 가능 여부, 장애물 여부)를 관리하는 클래스입니다.
  • Node: 각 그리드 칸을 나타내며, 각 노드는 gCost, hCost, fCost 및 부모 노드 정보를 가지고 있습니다.
  • FindPath(): A* 알고리즘을 구현한 핵심 함수입니다. 시작 지점에서 목표 지점까지 경로를 계산하고, 장애물을 피해 최적의 경로를 찾습니다.
  • OnDrawGizmos(): 유니티 에디터에서 경로를 시각적으로 확인할 수 있도록 경로를 그립니다.

3. 동적 장애물 회피: 실시간 경로 변경

게임에서는 장애물이 실시간으로 변할 수 있기 때문에, 경로를 찾아내는 동시에 장애물을 피할 수 있도록 경로를 동적으로 수정해야 합니다. 이를 위해서는 경로 추적 중 실시간으로 장애물이 등장할 경우 경로를 다시 탐색하는 기능을 추가해야 합니다.

예시 코드: 동적 장애물 회피

void Update()
{
    // 장애물이 발생하면 경로 재탐색
    if (ObstacleDetected())
    {
        FindPath(startPoint.position, endPoint.position);
    }
}

bool ObstacleDetected()
{
    // 예시로 간단히 Raycast를 통해 장애물 감지
    RaycastHit hit;
    if (Physics.Raycast(transform.position, transform.forward, out hit, 5f, obstacles))
    {
        return true;
    }
    return false;
}

코드 설명

  • ObstacleDetected()는 현재 캐릭터 앞에 장애물이 있을 경우 true를 반환하고, 이 때 FindPath()를 호출하여 경로를 재탐색합니다. 장애물이 이동 경로에 영향을 미치는 상황에서 실시간으로 경로를 조정할 수 있습니다.

4. 게임에서의 활용 사례

이 알고리즘은 다양한 게임에서 활용될 수 있습니다. 예를 들어:

  • 전략 게임에서 유닛들이 장애물을 피하며 적군에게 접근하는 경로를 계산할 때.
  • RPG에서 NPC가 장애물을 피하며 자연스러운 경로로 마을을 돌아다닐 때.
  • 전투 게임에서 적들이 플레이어를 추적하며 장애물을 피할 때.

이렇게 A* 알고리즘을 활용하면 게임 내에서 자연스럽고 지능적인 이동을 구현할 수 있습니다.


마무리

유니티에서 *A 알고리즘을 사용한 장애물 회피 경로 탐색**은 게임 AI의 중요한 부분으로, 게임의 몰입감을 높이는 핵심 기술입니다. 이 글에서는 기본적인 경로 탐색부터 동적 장애물 회피까지 구현하는 방법을 소개했습니다. 이와 같은 기술을 사용하면 게임 내의 캐릭터와 유닛들이 더 자연스럽게 환경과 상호작용하며, 플레이어에게 더 나은 경험을 제공할 수 있습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다