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.