Making a grid based game.

Hallo Community,

iv’e started with Unity and i managed to build an grid system with an two dimensional Array.

Little peace of my Unity C# code:




public GameObject,] board;
public GameObject field;

void BoardSetup()
    {
        for (int x = 0; x < 8; x++)
        {
            for (int y = 0; y < 8; y++)
            {
                board[x, y] = field;
                //Spawn the fields in board[8,8] and set the location to his two dimensional index
                GameObject instance = Instantiate(board[x, y], new Vector3(x, y, 0), Quaternion.identity) as GameObject; 
            }
        }
    }


I used this method to easily adress each field on my grid system through the Array index that’s also the location where there were spawned.
Since Unreal doesn’t have an two dimensional Array, i thought about putting Blueprint Actors in an TArray and adress them by it’s Location.
Im rather new to the Unreal Engine, but i completed several Tutorials.

Now to my questions:

  1. Is this method a good way, or is there another method to build an grid system with C++ and adress each field individually?
  2. How do i store an Blueprint class into an TArray? I kinda tryed it with this:


UClass* Field;
TArray<UClass> Fields;

Field = UClass*(TEXT("/Game/Blueprints/Field_BP.Field_BP"));
Fields.Init(Field, 64);


but that feels really wrong.

Thx already for your answers! :slight_smile:

Without commenting on your design, it is possible to create two dimensional arrays, which I will call a matrix or matrices. A matrix is simply an array of arrays. I know the Unreal Header Tool does prohibit a construction as TArray<TArray<T>> but you can ‘trick’ it; simply create some placeholder type, like this:

USTRUCT(BlueprintType)
struct FArrayPlaceholder
{
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Whatever)
    TArray&lt;SomeType&gt; Array;
};

In you the code that requires the matrix simply declare it as TArray<FArrayPlaceholder>> .

I’ll address your second question first: Assuming your blueprint inherits from AActor, then you can store it with this declaration: TArray<AActor*>. Note that you are storing pointers; your declaration AActor would copy all actors you wished to insert, which 1. is not allowed due to it being managed memory 2. modifications to it would not affect the actual actor you meant but its copy. For this reason the UHT does not allow it.
Also, you can get a blueprint by its path name using a FConstructorHelper, there are multiple; see this thread for getting a blueprint object (https://answers.unrealengine.com/questions/47990/how-can-i-get-a-reference-to-a-blueprint-in-c.html). If you simply need a class use a FClassFinder instead. You can only use it in constructors though; I never understood that design choice made by Epic but maybe I am missing something so if someone could inform me I would be most grateful.

To your first question: In one of my games I have a grid system, too, though I implemented it as a graph. Your design is constant time if you know the indices of your grids though the disadvantages is that you cannot dynamically add grids; well you can but doing so would require you to copy the matrix which is a no no performance wise. So if you know your grid will always be fixed size your design is just fine.
If you, however, cannot know the size in advance, as it was in my case, or want another simple design use a graph: Every grid has a neighbour to the north, east, south and west. Thus every GridBlock has 4 pointer fields; if there is a neighbour it points to the neighbour; if there is no neighbour, it points to null. Also a nice side effect to implementing it as a graph: you can use the thousands of algorithms designed for graphs which are roaming the world, should you ever need them for you game mind you.

Cheers,
Univise

I was working on a grid based game a while back and this is how far I got.

TileMap.h



// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/Actor.h"
#include "Tile.h"
#include "Int2.h"
#include "Directions.h"
#include "TileMap.generated.h"

UCLASS(BlueprintType, Blueprintable)
class FF_API ATileMap : public AActor
{
	GENERATED_BODY()
	
private:
	USceneComponent* root;
	UPROPERTY(EditDefaultsOnly)
	class UInstancedStaticMeshComponent* tilesClear;
	UPROPERTY(EditDefaultsOnly)
	class UInstancedStaticMeshComponent* tilesRed;
	UPROPERTY(EditDefaultsOnly)
	class UInstancedStaticMeshComponent* tilesGreen;
	UPROPERTY(EditDefaultsOnly)
	class UInstancedStaticMeshComponent* tilesBlue;
	UPROPERTY(EditAnywhere)
	TSubclassOf<ATile> tileClass;
	// Used in Unreal Editor to destroy previous tiles and spawn new ones 
	UPROPERTY(EditAnywhere)
	bool resetGraph;
	// The number of tiles that run along the x axis (y in unreal)
	UPROPERTY(EditAnywhere)
	int32 width;
	// The number of tiles that run along the y axis (x in unreal)
	UPROPERTY(EditAnywhere)
	int32 height;
	// The amount of space between tiles
	UPROPERTY(EditAnywhere)
	int32 tileSize;
	// The array of tiles that make up the tile map
	UPROPERTY(VisibleAnywhere)
	TArray<ATile*> graph;
	/* int32 = Walking Range
	TArray<int32> = Path to that tile */
	TMap<int32, TArray<int32>> paths;
	UPROPERTY(EditAnywhere)
	TArray<ABaseUnit*> turnOrder;
	UPROPERTY(VisibleAnywhere)
	int32 currentTurn;

public:	
	// Sets default values for this actor's properties
	ATileMap();
	// Called when changes are made in the editor
	virtual void OnConstruction(const FTransform& Transform) override;

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	
	// Called every frame
	virtual void Tick( float DeltaSeconds ) override;

	// The number of tiles that run along the x axis (y in unreal)
	UFUNCTION(BlueprintPure, category = "Dimensions")
	int32 GetWidth();
	// The number of tiles that run along the y axis (x in unreal)
	UFUNCTION(BlueprintPure, category = "Dimensions")
	int32 GetHeight();

	// The array of tiles that make up the tile map
	UFUNCTION(BlueprintPure, category = "Graph")
	TArray<ATile*> GetGraph();
	// Returns graph[index]
	UFUNCTION(BlueprintPure, category = "Graph")
	ATile* GetTile(int32 index);
	// Returns the index of the tile in the graph
	UFUNCTION(BlueprintPure, category = "Graph")
	int32 GetTileIndex(ATile* tile);
	// Returns the index of the tile in the graph
	UFUNCTION(BlueprintPure, category = "Graph")
	FInt2 GetTileXY(ATile* tile);

	// Converts a tile's x and y to it's index 
	UFUNCTION(BlueprintPure, category = "Conversions")
	int32 XYToIndex(int32 x, int32 y);
	// Converts a tile's index to it's x and y coords
	UFUNCTION(BlueprintPure, category = "Conversions")
	FInt2 IndexToXY(int32 index);
	// Gets a tile's world transformation
	UFUNCTION(BlueprintPure, category = "Conversions")
	FVector GetTileTransform(int32 index);
	// Returns an array of all units on the map
	UFUNCTION(BlueprintPure, category = "Conversions")
	TArray<ABaseUnit*> GetAllUnits();

	// Gets a tile's neighbouring tile's index
	UFUNCTION(BlueprintPure, category = "Neighbours")
	int32 GetNeighbourIndex(int32 x, int32 y, E_Directions neighbour);
	// Gets a tile's neighbouring tiles' index
	UFUNCTION(BlueprintCallable, category = "Neighbours")
	TArray<int32> GetNeighboursIndexes(int32 index);
	// Gets a tile's neighbouring tile
	UFUNCTION(BlueprintPure, category = "Neighbours")
	ATile* GetNeighbour(int32 x, int32 y, E_Directions neighbour);
	// Gets a tile's neighbouring tiles
	UFUNCTION(BlueprintCallable, category = "Neighbours")
	TArray<ATile*> GetNeighbours(int32 index);
	// Gets a tile's neighbouring tile's index
	UFUNCTION(BlueprintPure, category = "Neighbours")
	int32 GetNeighbourIndexByTile(ATile* tile, E_Directions neighbour);
	// Gets a tile's neighbouring tiles' index
	UFUNCTION(BlueprintCallable, category = "Neighbours")
	TArray<int32> GetNeighboursIndexesByTile(ATile* tile);
	// Gets a tile's neighbouring tile
	UFUNCTION(BlueprintPure, category = "Neighbours")
	ATile* GetNeighbourByTile(ATile* tile, E_Directions neighbour);
	// Gets a tile's neighbouring tiles
	UFUNCTION(BlueprintCallable, category = "Neighbours")
	TArray<ATile*> GetNeighboursByTile(ATile* tile);

	// Gets the cost to enter a tile
	UFUNCTION(BlueprintPure, category = "Pathfinding")
	float GetCostToEnterTile(ATile* prev, ATile* next);
	// Updates the paths to every tile in walking range
	UFUNCTION(BlueprintCallable, category = "Pathfinding")
	void UpdatePaths();
	// Returns all tiles in walking range (paths.keys)
	UFUNCTION(BlueprintPure, category = "Pathfinding")
	TArray<int32> GetWalkingRange();
	// Returns the path to a tile in walking range
	UFUNCTION(BlueprintPure, category = "Pathfinding")
	TArray<int32> GetPathTo(int32 index);

	// Determines the turn order based on the weight of each unit
	UFUNCTION(BlueprintCallable, category = "Turn Order")
	void UpdateTurnOrder();
	// Removes the unit from the turn order and destroys the unit
	UFUNCTION(BlueprintCallable, category = "Turn Order")
	void DestroyUnit(ABaseUnit* unitToDestroy);
};


TileMap.cpp



// Fill out your copyright notice in the Description page of Project Settings.

#include "FF.h"
#include "TileMap.h"


// Sets default values
ATileMap::ATileMap()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = false;
	root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
	tilesClear = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("Clear Tiles"));
	tilesRed = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("Red Tiles"));
	tilesGreen = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("Green Tiles"));
	tilesBlue = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("Blue Tiles"));
	RootComponent = root;
	width = 15;
	height = 10;
	tileSize = 180;
}



void ATileMap::OnConstruction(const FTransform & Transform)
{
	Super::OnConstruction(Transform);

	SetActorTransform(FTransform());

	if (width < 8) width = 8;
	if (height < 8) height = 8;

	UWorld* world = GetWorld();
	if (resetGraph && tileClass && world)
	{
		for (auto tile : graph) { tile->Destroy(); }
		graph.Empty();
		SetActorLocation(FVector());
		tilesClear->ClearInstances();
		tilesRed->ClearInstances();
		tilesGreen->ClearInstances();
		tilesBlue->ClearInstances();
		for (int32 i = 0; i < width * height; i++)
		{
			FInt2 xy = IndexToXY(i);
			int32 x = xy.x;
			int32 y = xy.y;

			FTransform tileTransform;
			tileTransform.SetTranslation(GetTileTransform(i));
			tilesClear->AddInstance(tileTransform);

			//ATile* const spawnedTile = Cast<ATile>(world->SpawnActor<ATile>(tileClass, GetTileTransform(i), FRotator(), FActorSpawnParameters()));
			//if (spawnedTile)
			//{
			//	spawnedTile->SetMap(this);
			//	spawnedTile->AttachRootComponentToActor(this);
			//	for (int32 i = 0; i < 6; i++)
			//	{
			//		if (GetNeighbourIndex(x, y, (E_Directions)i) != -1) spawnedTile->SetCanEnterNeighbour((E_Directions)i, true);
			//	}
			//	graph.Add(spawnedTile);
			//}
		}
		resetGraph = false;
	}
}



// Called when the game starts or when spawned
void ATileMap::BeginPlay()
{
	Super::BeginPlay();
	
}



// Called every frame
void ATileMap::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );

}



int32 ATileMap::GetWidth()				{ return width; }
int32 ATileMap::GetHeight()				{ return height; }
TArray<ATile*> ATileMap::GetGraph()		{ return graph; }



ATile* ATileMap::GetTile(int32 index)
{
	// ERROR CHECKING
	if (index > width * height - 1 || index < 0)
	{ LOG_ERROR("Attempted to get tile out of range"); return nullptr; }

	return graph[index];
}



int32 ATileMap::GetTileIndex(ATile* tile)	{ return graph.Find(tile); }
FInt2 ATileMap::GetTileXY(ATile * tile)		{ return IndexToXY(GetTileIndex(tile)); }



int32 ATileMap::XYToIndex(int32 x, int32 y)
{
	// ERROR CHECKING
	if (x < 0 || x > width || y < 0 || y > height)
	{ LOG_ERROR("Attempted to convert XY to Index out of range"); return -1; }

	return y * width + x; 
}



FInt2 ATileMap::IndexToXY(int32 index)	
{
	// ERROR CHECKING
	if (index < 0 || index >= width * height)
	{ LOG_ERROR("Attempted to convert Index to XY out of range"); return FInt2(-1, -1); }

	return FInt2(index % width, index / width); 
}



FVector ATileMap::GetTileTransform(int32 index)
{
	// ERROR CHECKING
	if (index < 0 || index >= width * height)
	{ LOG_ERROR("Attempted to get transform of tile out of range"); }

	FInt2 xy = IndexToXY(index);
	int x = xy.x;
	int y = xy.y;
	float sin60 = FMath::Sin(FMath::DegreesToRadians(60));

	if (y % 2 != 0) { return FVector(y * sin60, x - 0.5f, 0) * tileSize; }
	else { return FVector(y * sin60, x, 0) * tileSize; }
}



TArray<ABaseUnit*> ATileMap::GetAllUnits()
{
	TArray<ABaseUnit*> allUnits;
	for (auto tile : graph)
	{
		if (tile->GetUnit())
		{
			allUnits.Add(tile->GetUnit());
		}
	}
	return allUnits;
}



int32 ATileMap::GetNeighbourIndex(int32 x, int32 y, E_Directions neighbour)
{
	// ERROR CHECKING
	if (x < 0 || x > width || y < 0 || y > height)
	{ LOG_ERROR("Attempted to get neighbour of tile out of range"); return -1; }

	// Returns the index of the desired neighbour. Returning -1 means no neighbour exists
	switch (neighbour)
	{
		case E_Directions::Left:
			if (x > 0) return XYToIndex(x - 1, y);
			return -1;
		case E_Directions::Right:
			if (x < width - 1) return XYToIndex(x + 1, y);
			return -1;
		case E_Directions::TopLeft:
			if (y % 2 == 0 && y < height - 1) return XYToIndex(x, y + 1);
			else if (y < height - 1 && x > 0) return XYToIndex(x - 1, y + 1);
			return -1;
		case E_Directions::TopRight:
			if (y % 2 == 0 && y < height - 1 && x < width - 1) return XYToIndex(x + 1, y + 1);
			else if (y < height - 1) return XYToIndex(x, y + 1);
			return -1;
		case E_Directions::BottomLeft:
			if (y % 2 == 0 && y > 0) return XYToIndex(x, y - 1);
			else if (x > 0 && y > 0) return XYToIndex(x - 1, y - 1);
			return -1;
		case E_Directions::BottomRight:
			if (y % 2 == 0 && y > 0 && x < width - 1) return XYToIndex(x + 1, y - 1);
			else if (y > 0) return XYToIndex(x, y - 1);
			return -1;
	}

	return -1;
}



TArray<int32> ATileMap::GetNeighboursIndexes(int32 index)
{
	// ERROR CHECKING
	if (index < 0 || index > graph.Num())
	{ LOG_ERROR("Attempted to get neighbours of tile out of range"); return TArray<int32>(); }

	TArray<int32> neighbours;

	FInt2 xy = IndexToXY(index);
	
	for (int32 i = 0; i < 6; i++)
	{
		int32 tile = GetNeighbourIndex(xy.x, xy.y, (E_Directions)i);
		if (tile != -1) { neighbours.Add(tile); }
	}

	return neighbours;
}



ATile* ATileMap::GetNeighbour(int32 x, int32 y, E_Directions neighbour)
{
	// ERROR CHECKING
	if (x < 0 || x > width || y < 0 || y > height)
	{ LOG_ERROR("Attempted to get neighbour of tile out of range"); return nullptr; }

	int32 index = GetNeighbourIndex(x, y, neighbour);
	if (index != -1) return graph[index];
	else return nullptr;
}



TArray<ATile*> ATileMap::GetNeighbours(int32 index)
{
	TArray<ATile*> neighbours;

	for (auto tile : GetNeighboursIndexes(index))
	{ neighbours.Add(graph[tile]); }
	
	return neighbours;
}



int32 ATileMap::GetNeighbourIndexByTile(ATile * tile, E_Directions neighbour)
{
	// ERROR CHECKING
	if (!tile)
	{ LOG_ERROR("Attempted to get neighbour of null tile"); return -1; }

	FInt2 xy = IndexToXY(GetTileIndex(tile));
	int32 x = xy.x;
	int32 y = xy.y;

	return GetNeighbourIndex(x, y, neighbour);
}



TArray<int32> ATileMap::GetNeighboursIndexesByTile(ATile * tile)
{
	// ERROR CHECKING
	if (!tile)
	{ LOG_ERROR("Attempted to get neighbours of null tile"); return TArray<int32>(); }

	return GetNeighboursIndexes(GetTileIndex(tile));
}



ATile* ATileMap::GetNeighbourByTile(ATile * tile, E_Directions neighbour)
{
	// ERROR CHECKING
	if (!tile)
	{ LOG_ERROR("Attempted to get neighbour of null tile"); return nullptr; }

	FInt2 xy = IndexToXY(GetTileIndex(tile));
	int32 x = xy.x;
	int32 y = xy.y;

	return GetNeighbour(x, y, neighbour);
}



TArray<ATile*> ATileMap::GetNeighboursByTile(ATile * tile)
{
	// ERROR CHECKING
	if (!tile)
	{ LOG_ERROR("Attempted to get neighbours of null tile"); return TArray<ATile*>(); }

	return GetNeighbours(GetTileIndex(tile));
}



float ATileMap::GetCostToEnterTile(ATile* prev, ATile* next)
{
	if (!next->GetIsWalkable()) return 1000;
	return 1.0f;
}



void ATileMap::UpdatePaths()
{
	// Keeps track of which FTile comes before this one on the way to the end
	TMap<int32, int32> prev = TMap<int32, int32>();
	// Keeps track of how much it cost to enter the FTile from the prev FTile
	TMap<int32, float> dist = TMap<int32, float>();
	// Keeps track of which FTiles we haven't checked yet
	TArray<int32> unvisited = TArray<int32>();
	// The tiles that the current unit can walk to
	TArray<int32> walkingRange;
	// The updated walking range and paths to those tiles
	TMap<int32, TArray<int32>> updatedPaths;

	// Sets the default value for all FTiles
	for (int32 i = 0; i < graph.Num(); i++)
	{
		// If it is not where we started from, tell it we don't know anything about it
		if (i != GetTileIndex(turnOrder[currentTurn]->GetTile()))
		{
			dist.Add(i, 1000);
			prev.Add(i, -1);
		}
		// Otherwise, it cost nothing to enter and there is no FTile before it
		else
		{
			dist.Add(i, 0);
			prev.Add(i, -1);
		}
		// TODO Change to: If is walkable == true, then add to unvisited
		unvisited.Add(i);
	}

	// Checks all the FTiles until we find the end FTile
	while (unvisited.Num() > 0)
	{
		int32 currentTile = -1;
		// TODO Add comment
		for (auto tile : unvisited)
		{
			if (currentTile == -1 || dist[tile] < dist[currentTile])
			{
				currentTile = tile;
			}
		}
		unvisited.Remove(currentTile);
		// If the tile is in walking range
		if (dist[currentTile] <= turnOrder[currentTurn]->GetMoveRemaining())
		{
			// Add to walking range
			walkingRange.Add(currentTile);
			// Foreach neighbour
			for (int32 i = 0; i < 6; i++)
			{
				if (graph[currentTile]->GetCanEnterNeighbour((E_Directions)i))
				{
					FInt2 xy = IndexToXY(currentTile);
					int32 tile = GetNeighbourIndex(xy.x, xy.y, (E_Directions)i);

					float alt = dist[currentTile] + GetCostToEnterTile(graph[currentTile], graph[tile]);
					if (alt < dist[tile])
					{
						dist[tile] = alt;
						prev[tile] = currentTile;
					}
				}
			}
		}
	}

	// Find the path to each of the tiles in the walking range
	for (auto tile : walkingRange)
	{
		// Keep going back from the end to the start, adding the FTiles on the way to a 
		// list of FTiles. We know that we arrived at the beginning because it will be the 
		// only one on the path that has NULL for it's prev
		TArray<int32> reversedPath = TArray<int32>();
		int32 curr = tile;
		while (curr != -1)
		{
			reversedPath.Add(curr);
			curr = prev[curr];
		}
		// path.Reverse();
		TArray<int32> path = TArray<int32>();
		for (int32 i = reversedPath.Num() - 1; i > -1; i--)
		{
			path.Add(reversedPath*);
		}
		// Add path found to Paths
		updatedPaths.Add(tile, path);
	}
	updatedPaths.Remove(GetTileIndex(turnOrder[currentTurn]->GetTile()));
	// Update the current paths
	paths = updatedPaths;

}



TArray<int32> ATileMap::GetWalkingRange()
{
	TArray<int32> OUTkeys;
	paths.GetKeys(OUTkeys);
	return OUTkeys;
}



TArray<int32> ATileMap::GetPathTo(int32 index)	
{ 
	if (!paths.Contains(index))
	{ LOG_ERROR("Attempted to get path of tile out of walking range"); }

	return paths[index]; 
}



void ATileMap::UpdateTurnOrder()
{
	turnOrder.Empty();

	TArray<ABaseUnit*> allUnits = GetAllUnits();
	int lowWeight = allUnits[0]->GetWeight();
	int highWeight = lowWeight;

	// Find out what the lowest and highest weight is
	for(auto unit : allUnits)
	{
		if (unit->GetWeight() < lowWeight)
		{
			lowWeight = unit->GetWeight();
		}
		if (unit->GetWeight() > highWeight)
		{
			highWeight = unit->GetWeight();
		}
	}

	// Makes a list of units sorted by weight. Faster units get more turns
	for (int x = lowWeight; x <= highWeight * 3; x++)
	{
		for(auto unit : allUnits)
		{
			if (x % unit->GetWeight() == 0)
			{
				turnOrder.Add(unit);
			}
		}
	}
}



void ATileMap::DestroyUnit(ABaseUnit * unitToDestroy)
{
	// i is the number of turns the unit to destroy has had so far
	int32 i = 0;
	for (int32 j = 0; j < currentTurn; j++)
	{
		if (turnOrder[j] == unitToDestroy)
		{
			i++;
		}
	}
	
	// Subtract i from the currentTurn to keep the currentUnit the same
	turnOrder.Remove(unitToDestroy);
	currentTurn -= i;
	unitToDestroy->Destroy();
}



Tile.h



// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/Actor.h"
#include "BaseUnit.h"
#include "Tile.generated.h"

UCLASS(BlueprintType, Blueprintable)
class FF_API ATile : public AActor
{
	GENERATED_BODY()
	
private:
	UPROPERTY(EditAnywhere)
	bool isSpawnPoint;
	UPROPERTY(EditAnywhere)
	bool isWalkable;
	UPROPERTY(EditAnywhere)
	bool canEnterLeft;
	UPROPERTY(EditAnywhere)
	bool canEnterTopLeft;
	UPROPERTY(EditAnywhere)
	bool canEnterTopRight;
	UPROPERTY(EditAnywhere)
	bool canEnterRight;
	UPROPERTY(EditAnywhere)
	bool canEnterBottomRight;
	UPROPERTY(EditAnywhere)
	bool canEnterBottomLeft;
	UPROPERTY(EditAnywhere)
	ABaseUnit* unit;
	UPROPERTY(VisibleAnywhere)
	class ATileMap* map;
	UPROPERTY(VisibleAnywhere)
	class UStaticMeshComponent* mesh;
	
public:	
	// Sets default values for this actor's properties
	ATile();

	// Called when changes are made in the editor
	virtual void OnConstruction(const FTransform& Transform) override;

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	
	// Called every frame
	virtual void Tick( float DeltaSeconds ) override;

	UFUNCTION(BlueprintPure, category = "Pathfinding")
	bool GetIsSpawnPoint();
	UFUNCTION(BlueprintPure, category = "Pathfinding")
	bool GetIsWalkable();
	UFUNCTION(BlueprintPure, category = "Pathfinding")
	bool GetCanEnterNeighbour(E_Directions neighbour);

	UFUNCTION(BlueprintCallable, category = "Pathfinding")
	void SetCanEnterNeighbour(E_Directions neighbour, bool canEnter);

	UFUNCTION(BlueprintPure, category = "Pathfinding")
	ABaseUnit* GetUnit();
	UFUNCTION(BlueprintPure, category = "Pathfinding")
	ATileMap* GetMap();

	void SetMap(class ATileMap* tileMap);
	void SetUnit(ABaseUnit* newUnit);
};



Tile.cpp



// Fill out your copyright notice in the Description page of Project Settings.

#include "FF.h"
#include "TileMap.h"
#include "Tile.h"


// Sets default values
ATile::ATile()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = false;
	mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TileMesh"));
	RootComponent = mesh;
	isWalkable = true;
}



void ATile::OnConstruction(const FTransform & Transform)
{
	Super::OnConstruction(Transform);

	if (isWalkable) SetActorHiddenInGame(false);
	else
	{
		SetActorHiddenInGame(true);
		isSpawnPoint = false;

		canEnterLeft = false;
		canEnterTopLeft = false;
		canEnterTopRight = false;
		canEnterRight = false;
		canEnterBottomRight = false;
		canEnterBottomLeft = false;
	}

	if (!canEnterLeft && !canEnterTopLeft && !canEnterTopRight && !canEnterRight && !canEnterBottomRight && !canEnterBottomLeft)
	{
		isWalkable = false;
		SetActorHiddenInGame(false);
	}

	if (map)
	{
		ATile* neighbour;
		for (int32 i = 0; i < 6; i++)
		{
			E_Directions direction = (E_Directions)i;
			E_Directions oppositeDirection = (E_Directions)((i + 3) % 6);
			neighbour = map->GetNeighbourByTile(this, direction);
			if (GetCanEnterNeighbour(direction) && neighbour) neighbour->SetCanEnterNeighbour(oppositeDirection, true);
			else if (neighbour) neighbour->SetCanEnterNeighbour(oppositeDirection, false);
			else SetCanEnterNeighbour(direction, false);
		}
	}

}



// Called when the game starts or when spawned
void ATile::BeginPlay()
{
	Super::BeginPlay();
	
}



// Called every frame
void ATile::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );

}



bool ATile::GetIsSpawnPoint()	{ return isSpawnPoint; }
bool ATile::GetIsWalkable()		{ return isWalkable; }



bool ATile::GetCanEnterNeighbour(E_Directions neighbour)
{
	switch (neighbour)
	{
		case E_Directions::Left:		return canEnterLeft;
		case E_Directions::TopLeft:		return canEnterTopLeft;
		case E_Directions::TopRight:	return canEnterTopRight;
		case E_Directions::Right:		return canEnterRight;
		case E_Directions::BottomRight: return canEnterBottomRight;
		case E_Directions::BottomLeft:	return canEnterBottomLeft;
	}
	return false;
}



void ATile::SetCanEnterNeighbour(E_Directions neighbour, bool canEnter)
{
	switch (neighbour)
	{
		case E_Directions::Left:		canEnterLeft = canEnter; break;
		case E_Directions::TopLeft:		canEnterTopLeft = canEnter; break;
		case E_Directions::TopRight:	canEnterTopRight = canEnter; break;
		case E_Directions::Right:		canEnterRight = canEnter; break;
		case E_Directions::BottomRight:	canEnterBottomRight = canEnter; break;
		case E_Directions::BottomLeft:	canEnterBottomLeft = canEnter; break;
	}
}



ABaseUnit* ATile::GetUnit()	{ return unit; }
ATileMap* ATile::GetMap()	{ return map; }

void ATile::SetMap(ATileMap* tileMap)		{ map = tileMap; }
void ATile::SetUnit(ABaseUnit* newUnit)		{ unit = newUnit; }



This is set up for a hex based game, but changing it to squares isn’t too much work. You just have to change the neighbours and the tile position. I think I was doing something with instanced static meshes instead of spawning an actor for every tile, but just comment out the instanced static mesh stuff and uncomment the stuff to spawn the Tile actor and it should work.

I also think that there was this weird error where it wouldn’t spawn the tiles because the transform returned from GetTileTransform was invalid or something, and resetting my computer fixes the issue when it happened, idk why it happened, there’s nothing wrong with the code as far as i can see.

Looking back I would have done a few things differently, but yeah.

Edit: I made blueprint children of the Tile actor to set the mesh and stuff.

@Univise Thx for your answer! The thing that every grid has a neighbour in all directions to adress is a good idea!

@Ispheria Thx for the code! Im definitly going through it. Mind if i send you messages if i dont understand parts of your code?

Messaging me isn’t a great idea, because i only get internet for like an hour once a week. A lot of it was based on a Unity pathfinding tutorial though, https://www.youtube.com/watch?v=kYeTW2Zr8NA I just made it hex based and changed it to calculate the neighbours and stuff when it needs it instead of storing it because i don’t like the idea of storing thousands of ints. And it set it up so you can enter a tile from one side and not another, so you can put like a fence or something in between tiles.

The youtube link will help me out then. Thx

Here’s a simple one I did

Here’s the class to store the actors in the grid


UCLASS()
class GRIDBASE_API AGridBaseBoard : public AActor
{
	GENERATED_BODY()

public:
	/**
	*/
	AGridBaseBoard( const FObjectInitializer& ObjectInitializer );

	/**
	*/
	UFUNCTION( Category = GridBase, BlueprintCallable )
	virtual void SetActorAtCell( const FGridBaseCoord& Coord, AActor* Actor );

	/**
	*/
	UFUNCTION( Category = GridBase, BlueprintCallable )
	AActor* GetActorAtCell( const FGridBaseCoord& Coord, bool& bOutOfBound ) const;

	/** Get linear index
	*/
	int32 GetLinearIndex(const FGridBaseCoord& Coord) const { return (Coord.y * NumColumns) + Coord.x; }

public:
	/** number of columns
	*/
	UPROPERTY( Category = GridBase, EditAnywhere, BlueprintReadOnly )
	int32 NumColumns;

	/** number of rows
	*/
	UPROPERTY( Category = GridBase, EditAnywhere, BlueprintReadOnly )
	int32 NumRows;

	/** the actors in the gridlity
	*/
	UPROPERTY( Category = GridBase, VisibleAnywhere, BlueprintReadOnly )
	TArray<AActor*> Actors;
};

Here’s how you set actor at specified coordinate, the important function here is GetLinearIndex, it translates a coordinate into a linear index, which is very useful!


void AGridBaseBoard::SetActorAtCell( const FGridBaseCoord& Coord, AActor* Actor )
{
	check( 0 <= Coord.x && Coord.x < NumColumns );
	check( 0 <= Coord.y && Coord.y < NumRows );
	const int32 LinearIndex = GetLinearIndex(Coord);
	check( 0 <= LinearIndex && LinearIndex < Actors.Num() );
	Actors[LinearIndex] = Actor;
}


Here’s how you get actor at specified coordinate


AActor* AGridBaseBoard::GetActorAtCell( const FGridBaseCoord& Coord, bool& bOutOfBound ) const
{
	bOutOfBound = true;
	if( 0 <= Coord.x && Coord.x < NumColumns )
	{
		if( 0 <= Coord.y && Coord.y < NumRows )
		{
			const int32 LinearIndex = GetLinearIndex(Coord);
			check( 0 <= LinearIndex && LinearIndex < Actors.Num() );
			bOutOfBound = false;
			return Actors[LinearIndex];
		}
	}
	return nullptr;
}


The coordinate structure is just a simple x, y field


USTRUCT(BlueprintType)
struct GRIDBASE_API FGridBaseCoord
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GridBase)
	int32 x;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GridBase)
	int32 y;
};


Basically you SPawn your tile actor, anything you want and do SetActorAtCell to set it, similar to this maybe (I didn’t actually compile the below code, just as a demonstration)


AGridBaseBoard* Board = SpawnActor<AGridBaseBoard>();
ATileActor* Actor = SpawnActor<ATileActor>(ATileActor::StaticClass());
// set actor at <1,1>
FGridBaseCoord Coord;
Coord.x = 1;
Coord.y = 1;
Board->SetActorAtCell(Coord, Actor);

@polytopey thx alot for the code! :slight_smile: