[Sebastian Lague] A* Pathfinding (E02: node grid) Part 2
카테고리: Techniques
이 포스팅은 Sebastian Lague의 A* Pathfinding 영상을 기반으로 진행됩니다.
깃허브 주소: Sebistian Lague’s Github
🧐 구현
여기서부턴 기존에 있던 Grid 클래스를 짜서 구현해야 한다.
📜 구조 및 변수 선언
// 유니티
public class Grid : MonoBehaviour {
public LayerMask unwalkableMask;
public Vector2 gridWorldSize;
public float nodeRadius;
Node[,] grid;
float nodeDiameter;
int gridSizeX, gridSizeY;
// ...
}
public class Node {
public bool walkable;
public Vector3 worldPosition;
public Node(bool _walkable, Vector3 _worldPos) {
walkable = _walkable;
worldPosition = _worldPos;
}
}
// 언리얼
USTRUCT(Atomic)
struct FNode {
GENERATED_USTRUCT_BODY()
public:
bool bWalkable;
FVector worldPosition;
FNode() {
bWalkable = true;
worldPosition = FVector(0, 0, 0);
}
FNode(bool _walkable, FVector _worldPos) {
bWalkable = _walkable;
worldPosition = _worldPos;
}
bool operator==(const FNode& other) const {
return worldPosition == other.worldPosition;
}
};
USTRUCT()
struct FGridRow {
GENERATED_USTRUCT_BODY()
TArray<FNode> Nodes;
};
private:
UPROPERTY(EditAnywhere)
float nodeRadius = 100.0f;
UPROPERTY()
TArray<FGridRow> grid;
UPROPERTY()
int gridSizeX = 10;
UPROPERTY()
int gridSizeY = 10;
UPROPERTY()
float nodeDiameter;
UPROPERTY()
float gridWorldSizeX;
UPROPERTY()
float gridWorldSizeY;
두 코드에서 다른 점이라고 하면, Grid를 선언해줄 때다.
언리얼엔진의 TArray 안에 TArray를 넣어줄 수 없기에, TGridRow 구조체를 선언하여 문제를 해결하였다.
Start() / BeginPlay() 파트
// 유니티
void Start() {
nodeDiameter = nodeRadius*2;
gridSizeX = Mathf.RoundToInt(gridWorldSize.x/nodeDiameter);
gridSizeY = Mathf.RoundToInt(gridWorldSize.y/nodeDiameter);
CreateGrid();
}
// 언리얼
void AGrid::BeginPlay()
{
Super::BeginPlay();
nodeDiameter = nodeRadius * 2;
FVector origin;
FVector boxExtent;
GetActorBounds(false, origin, boxExtent);
gridWorldSizeX = boxExtent.X * 2;
gridWorldSizeY = boxExtent.Y * 2;
gridSizeX = FMath::RoundToInt(gridWorldSizeX / nodeDiameter);
gridSizeY = FMath::RoundToInt(gridWorldSizeY / nodeDiameter);
CreateGrid();
}
👉🏻 차이점
- 반올림 함수
- 유니티: ‘Mathf.RoundToInt’ 함수 사용
- 언리얼: ‘FMath::RoundToInt’ 함수 사용
- 바운드 계산
- 유니티: ‘gridWorldSize’를 외부에서 직접 지정해줌
- 언리얼: 해당 액터의 콜리전과 연동하여 사용할수 있도록 해두었음.
유니티에서는 gridWorldSize를 정해줌으로써 크기를 지정했지만, 나는 콜리전과 연동하여 사용할수 있도록 만들어 두었다.
CreateGrid() 파트
// 유니티
void CreateGrid() {
grid = new Node[gridSizeX,gridSizeY];
Vector3 worldBottomLeft = transform.position - Vector3.right * gridWorldSize.x/2 - Vector3.forward * gridWorldSize.y/2;
for (int x = 0; x < gridSizeX; x ++) {
for (int y = 0; y < gridSizeY; y ++) {
Vector3 worldPoint = worldBottomLeft + Vector3.right * (x * nodeDiameter + nodeRadius) + Vector3.forward * (y * nodeDiameter + nodeRadius);
bool walkable = !(Physics.CheckSphere(worldPoint,nodeRadius,unwalkableMask));
grid[x,y] = new Node(walkable,worldPoint);
}
}
}
// 언리얼
void AGrid::CreateGrid() {
grid.SetNum(gridSizeX);
FVector worldBottomLeft = GetActorLocation() - FVector(gridWorldSizeX / 2, gridWorldSizeY / 2, 0);
for (int x = 0; x < gridSizeX; x++) {
grid[x].Nodes.SetNum(gridSizeY);
for (int y = 0; y < gridSizeY; y++) {
FVector worldPoint = worldBottomLeft + FVector(x * nodeDiameter + nodeRadius, y * nodeDiameter + nodeRadius, 0);
bool walkable = IsWalkable(worldPoint);
grid[x].Nodes[y] = FNode(walkable, worldPoint);
}
}
}
bool AGrid::IsWalkable(const FVector& WorldPoint) {
TArray<FOverlapResult> OverlapResults;
FCollisionShape CollisionShape = FCollisionShape::MakeSphere(nodeRadius);
bool bHit = GetWorld()->OverlapMultiByChannel(
OverlapResults,
WorldPoint,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel1,
CollisionShape
);
return OverlapResults.Num() == 0;
}
👉🏻 차이점
- 배열 초기화
- 유니티: new Node[gridSizeX, gridSizeY] 로 초기화해주었다.
- 언리얼: TArray 내에 TArray를 넣는 것을 허용하지 않으므로 다른 방식으로 초기화해주었다.
- walkable bool 변수
- 유니티: Physics.CheckSphere와 unwalkableMask를 사용하여 해당 레이어가 unwalkable 인지 확인하고 있다.
- 언리얼: IsWalkable 함수를 따로 선언하여 내부에서 Unwalkable Trace Channel(ECC_GameTraceChannel1)이 있는지 확인하였다.
OverlapResults에 Unwalkable 오브젝트가 들어있으면 false, 아니라면 true를 반환한다.
bHit은 Block으로 설정된 오브젝트가 있는지 판단하는 것이므로, 여기서는 쓸모없는 변수이다.
✅ Unwalkable Trace Channel 찾기
DefaultEngine.ini에서 Unwalkable을 검색하여 찾을 수 있다.
NodeFromWorldPoint 파트
// 유니티
public Node NodeFromWorldPoint(Vector3 worldPosition) {
float percentX = (worldPosition.x + gridWorldSize.x/2) / gridWorldSize.x;
float percentY = (worldPosition.z + 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];
}
// 언리얼
FNode AGrid::NodeFromWorldPoint(FVector WorldPosition) {
float percentX = (WorldPosition.X + gridWorldSizeX / 2) / gridWorldSizeX;
float percentY = (WorldPosition.Y + gridWorldSizeY / 2) / gridWorldSizeY;
percentX = FMath::Clamp(percentX, 0.0f, 1.0f);
percentY = FMath::Clamp(percentY, 0.0f, 1.0f);
int x = FMath::RoundToInt((gridSizeX - 1) * percentX);
int y = FMath::RoundToInt((gridSizeY - 1) * percentY);
return grid[x].Nodes[y];
}
👉🏻 차이점
- Clamp 함수
- 유니티: ‘Mathf.Clamp01’ 함수를 사용하여 범위를 0에서 1로 제한한다.
- 언리얼: ‘FMath::Clamp’ 함수를 사용하여 범위를 제한한다.
OnDrawGizmos() / DrawDebugGrid()
// 유니티
void OnDrawGizmos() {
Gizmos.DrawWireCube(transform.position,new Vector3(gridWorldSize.x,1,gridWorldSize.y));
if (grid != null) {
Node playerNode = NodeFromWorldPoint(player.position)
foreach (Node n in grid) {
Gizmos.color = (n.walkable)?Color.white:Color.red;
if (playerNode == n) {
Gizmos.color = Color.cyan;
}
Gizmos.DrawCube(n.worldPosition, Vector3.one * (nodeDiameter-.1f));
}
}
}
// 언리얼
void AGrid::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
DrawDebugGrid();
}
void AGrid::DrawDebugGrid() {
FNode PlayerNode = NodeFromWorldPoint(GetWorld()->GetFirstPlayerController()->GetPawn()->GetActorLocation());
for (int x = 0; x < gridSizeX; x++) {
for (int y = 0; y < gridSizeY; y++) {
FNode Nodes = grid[x].Nodes[y];
FColor NodesColor = Nodes.bWalkable ? FColor::Green : FColor::Red;
if (Nodes == PlayerNode) {
NodesColor = FColor::Blue;
}
DrawDebugBox(GetWorld(), Nodes.worldPosition, FVector(nodeRadius - 1, nodeRadius - 1, nodeRadius - 1), NodesColor);
}
}
}
👉🏻 차이점
- OnDrawGizmos의 유무
- 유니티: ‘OnDrawGizmos()’ 함수를 만들어주면 자동으로 호출된다.
- 언리얼: DrawDebugGrid() 함수를 직접 생성하여 Tick()과 연결해주었다.
- 그리는 방식
- 유니티: ‘Gizmos.DrawCube()’ 함수를 사용한다.
- 언리얼: ‘DrawDebugBox()’ 함수를 사용한다.
- 플레이어 위치
- 유니티: 직접 플레이어를 player 변수에 할당해주고 이를 사용했다.
- 언리얼: 등록되어있는 PlayerController에서 Pawn을 뽑아서 사용한다.
🏃🏻 실행
‘LiveCoding.Compile’을 쳐서 컴파일을 먼저 한다.
Grid를 화면에 배치하고, BoxCollision의 ‘셰이프-박스 크기’ 를 조절하여 Pathfinding 범위를 지정한다.
범위가 지정된 것을 확인할 수 있다. (Plane 옆의 선)
올바르게 실행되는 것을 확인하였다.
🪶 깃허브
Sebastian Lague - Unity
Me - UE5
댓글남기기