공부/유니티 디펜스 게임 만들기
A* 알고리즘 with Unity 02
원클릭쓰리버그
2022. 1. 23. 18:35
728x90
결과 그림이다. Inspector 창에 Seeker (경로를 찾는 자), Target(경로 도착지)를 설정해준다.
using System.Collections.Generic;
using UnityEngine;
public class Grid : MonoBehaviour
{
public LayerMask unwalkableMask;
Node[,] grid;
Vector2 gridSize;
float nodeRadius; //노드 반지름
float nodeDiameter; //노드 지름
int gridXCount, gridYCount; //Grid X축 Node 갯수, Grid Y축 Node 갯수
private void Awake()
{
gridSize = new Vector2(40, 40);
nodeRadius = 0.5f;
nodeDiameter = nodeRadius * 2f;
gridXCount = Mathf.RoundToInt(gridSize.x / nodeDiameter);
gridYCount = Mathf.RoundToInt(gridSize.y / nodeDiameter);
CreateGrid();
}
public List<Node> GetNearByNode(Node currentNode)
{
List<Node> NearByNodes = new List<Node>();
// 해당되는 노드의 붙어있는 노드를 체크하여 List에 담는다.
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
// 본인 자신이면 넘어간다.
if (x == 0 && y == 0)
continue;
int check_X = currentNode.gridX + x;
int check_Y = currentNode.gridY + y;
// 범위내 주변노드가 맞다면 List에 저장한다.
if ((0 <= check_X && check_X < gridXCount) &&
(0 <= check_Y && check_Y < gridYCount))
NearByNodes.Add(grid[check_X, check_Y]);
}
}
return NearByNodes;
}
private void CreateGrid()
{
grid = new Node[gridXCount, gridYCount]; // [40,40]
Vector3 worldBottomLeft = transform.position - new Vector3((gridSize.x * 0.5f), 0, (gridSize.y * 0.5f));
for (int x = 0; x < gridXCount; x++)
{
for (int y = 0; y < gridYCount; y++)
{
Vector3 worldPosition = worldBottomLeft + new Vector3((x * nodeDiameter + nodeRadius), 0, (y * nodeDiameter + nodeRadius));
bool walkable = !(Physics.CheckSphere(worldPosition, nodeRadius, unwalkableMask));
grid[x, y] = new Node(walkable, worldPosition, x, y);
}
}
}
public Node GetNodeFromWorldPosition(Vector3 WP)
{
float percen_X = (WP.x + gridSize.x * 0.5f) / gridSize.x;
float percen_Y = (WP.z + gridSize.y * 0.5f) / gridSize.y;
percen_X = Mathf.Clamp01(percen_X);
percen_Y = Mathf.Clamp01(percen_Y);
int x = Mathf.RoundToInt((gridXCount - 1) * percen_X);
int y = Mathf.RoundToInt((gridYCount - 1) * percen_Y);
return grid[x, y];
}
}
using UnityEngine;
public class Node
{
// 이전 Node를 기억해준다.
public Node parent;
public bool IsWalkable;
public Vector3 worldPosition;
public int gridX, gridY;
// 노드의 현재까지 움직인 비용.
public int gCost;
// 노드가 도착점까지 예상되는 비용.
public int hCost;
//비용의 총합
public int Fcost { get => gCost + hCost; }
public Node(bool walkable, Vector3 pos, int x, int y)
{
this.IsWalkable = walkable;
worldPosition = pos;
gridX = x;
gridY = y;
}
}
using System.Collections.Generic;
using UnityEngine;
public class PathManager : MonoBehaviour
{
public Transform seeker, target;
[SerializeField] private Grid grid;
List<Node> path;
private void Awake()
{
grid = GetComponent<Grid>();
}
private void Update() // 실시간
{
FindPath(seeker.position, target.position);
}
private void FindPath(Vector3 seeker, Vector3 target)
{
// openSet은 비용적으로 효율적인 Node를 모아 놓는 List이다.
List<Node> openSet = new List<Node>();
// closeSet은 여지껏 왔던 Node 중 왔던 길,혹은 비용적으로 비효율적인 Node를 모아 놓은 HashSet이다.
HashSet<Node> closeSet = new HashSet<Node>();
Node startNode = grid.GetNodeFromWorldPosition(seeker); // 출발 Node[x,y]
Node targetNode = grid.GetNodeFromWorldPosition(target); // 도착 Node[x,y]
openSet.Add(startNode); // 효율적인 Node면 저장해 놓는다.
while (openSet.Count > 0)
{
//openSet List에서 가장 적은 비용의 Node를 찾는다.
Node currentNode = openSet[0];
for (int i = 0; i < openSet.Count; i++)
{
// 임시로 담겨있는 Node currentNode에서 비용이 작은 Node를 뽑아 그것으로 바꿔준다.
// 만약 Fcost 비용이 같은 경우 앞으로 갈 비용 중 가장 적은 Node를 우선순위로 둔다.
if (openSet[i].Fcost < currentNode.Fcost ||
(openSet[i].Fcost == currentNode.Fcost && openSet[i].hCost < currentNode.hCost))
currentNode = openSet[i];
}
// 뽑은 Node는 openSet에 없애주고, CloseSet에 담겨준다.
openSet.Remove(currentNode);
closeSet.Add(currentNode);
//뽑은 노드와 타켓 노드가 같으면 도착했으므로 탐색을 그만둔다.
if (currentNode == targetNode)
{
GetPath(startNode, targetNode);
break;
}
//뽑은 노드의 주변 노드를 List로 받아 확인한다.
foreach (Node nearByNode in grid.GetNearByNode(currentNode))
{
if (closeSet.Contains(nearByNode) || !nearByNode.IsWalkable)
continue;
// 비용 계산
int newGCost = currentNode.gCost + GetDistance(currentNode, nearByNode);
//기존 openSet에 있는 노드라면
if (openSet.Contains(nearByNode))
{
if (newGCost < nearByNode.gCost) // 계산한 gcost가 낮다면
{
nearByNode.gCost = newGCost; // 갱신시켜준다.
nearByNode.parent = currentNode; // 부모 노드를 저장시킨다.
}
}
else // 처음 접하는 Node일 경우
{
nearByNode.gCost = newGCost;
nearByNode.hCost = GetDistance(nearByNode, targetNode);
nearByNode.parent = currentNode;
openSet.Add(nearByNode);
}
}
}
}
int GetDistance(Node Node_A, Node Node_B)
{
int dist_X = Mathf.Abs(Node_A.gridX - Node_B.gridX);
int dist_Y = Mathf.Abs(Node_A.gridY - Node_B.gridY);
//길이가 짧은 곳을 먼저 대각선으로 간 후,
//길이가 긴쪽에서 짧은 쪽을 뺀 만큼을 더 직직 이동 시킨다.
if (dist_X > dist_Y)
return 14 * dist_Y + 10 * (dist_X - dist_Y);
else
return 14 * dist_X + 10 * (dist_Y - dist_X);
}
void GetPath(Node startNode, Node targetNode)
{
path= new List<Node>();
Node currentNode = targetNode;
while(true)
{
path.Add(currentNode);
if (currentNode == startNode)
break;
currentNode = currentNode.parent;
}
}
private void OnDrawGizmos()
{
if(path !=null)
foreach (var node in path)
{
Gizmos.color = Color.black;
Gizmos.DrawCube(node.worldPosition, Vector3.one);
}
}
}