[Sebastian Lague] A* Pathfinding (E02: node grid) Part 2

업데이트:     Updated:

카테고리:

태그: , ,

이 포스팅은 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();

}

👉🏻 차이점

  1. 반올림 함수
    • 유니티: ‘Mathf.RoundToInt’ 함수 사용
    • 언리얼: ‘FMath::RoundToInt’ 함수 사용
  2. 바운드 계산
    • 유니티: ‘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;
}

👉🏻 차이점

  1. 배열 초기화
    • 유니티: new Node[gridSizeX, gridSizeY] 로 초기화해주었다.
    • 언리얼: TArray 내에 TArray를 넣는 것을 허용하지 않으므로 다른 방식으로 초기화해주었다.
  2. walkable bool 변수
    • 유니티: Physics.CheckSphere와 unwalkableMask를 사용하여 해당 레이어가 unwalkable 인지 확인하고 있다.
    • 언리얼: IsWalkable 함수를 따로 선언하여 내부에서 Unwalkable Trace Channel(ECC_GameTraceChannel1)이 있는지 확인하였다.
      OverlapResults에 Unwalkable 오브젝트가 들어있으면 false, 아니라면 true를 반환한다.
      bHit은 Block으로 설정된 오브젝트가 있는지 판단하는 것이므로, 여기서는 쓸모없는 변수이다.

✅ Unwalkable Trace Channel 찾기

image

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];
}

👉🏻 차이점

  1. 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);
		}
	}
}

👉🏻 차이점

  1. OnDrawGizmos의 유무
    • 유니티: ‘OnDrawGizmos()’ 함수를 만들어주면 자동으로 호출된다.
    • 언리얼: DrawDebugGrid() 함수를 직접 생성하여 Tick()과 연결해주었다.
  2. 그리는 방식
    • 유니티: ‘Gizmos.DrawCube()’ 함수를 사용한다.
    • 언리얼: ‘DrawDebugBox()’ 함수를 사용한다.
  3. 플레이어 위치
    • 유니티: 직접 플레이어를 player 변수에 할당해주고 이를 사용했다.
    • 언리얼: 등록되어있는 PlayerController에서 Pawn을 뽑아서 사용한다.

🏃🏻 실행

image

‘LiveCoding.Compile’을 쳐서 컴파일을 먼저 한다.

Grid를 화면에 배치하고, BoxCollision의 ‘셰이프-박스 크기’ 를 조절하여 Pathfinding 범위를 지정한다.

image

범위가 지정된 것을 확인할 수 있다. (Plane 옆의 선)

image

올바르게 실행되는 것을 확인하였다.

🪶 깃허브

Readme Card
Sebastian Lague - Unity
Readme Card
Me - UE5

Techniques 카테고리 내 다른 글 보러가기

댓글남기기