Not too sure if this will help you, but a while back I tried to make a tactical rpg but gave up because i decided it was too big a project to handle by myself. But I got most of a tile map working-ish converting from a Unity tutorial. This is what I had (though a lot of it I’d do differently now that I’m more experienced). I’m pretty I had it using hexes instead of squares, but you’d just have to change the way we define neighbours to have it work with squares.
This is the actor I placed in the world, and I set up some other actor called Tile or something, and I spawned a tile for each tile in the tilemap, and I used the Tile to interact with the TileMap
.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "GameFramework/Actor.h"
#include "TileMap.generated.h"
UENUM(BlueprintType)
enum class ETileTypes : uint8 {
TT_Plains UMETA(DisplayName = "Plains"),
TT_Roads UMETA(DisplayName = "Roads"),
TT_Forest UMETA(DisplayName = "Forest"),
TT_Swamp UMETA(DisplayName = "Swamp"),
TT_Desert UMETA(DisplayName = "Desert"),
TT_Mountain UMETA(DisplayName = "Mountain"),
TT_Air UMETA(DisplayName = "Air")
};
// Struct that holds data about a location required for pathfinding.
USTRUCT(BlueprintType)
struct FTile {
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(BlueprintReadOnly)
int32 height;
UPROPERTY(BlueprintReadOnly)
ETileTypes tileType;
UPROPERTY(BlueprintReadOnly)
TArray<int32> neighbours;
FTile() {
height = 0;
tileType = ETileTypes::TT_Plains;
}
};
// A simple structure used to store 2 int32's
USTRUCT(BlueprintType)
struct FInt2 {
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 x;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 y;
FInt2() {
x = 0;
y = 0;
}
FInt2(int32 _x, int32 _y) {
x = _x;
y = _y;
};
};
/* Actor responsible for holding all the data about the terrain being fought on and
all the required functions for interacting with the terrain, such as pathfinding.
Holds a 1D array of FTile that is treated like a 2D array via the Conversion Functions.
*/
UCLASS()
class TRPG_API ATileMap : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ATileMap();
// Called when the game starts or when spawned
virtual void BeginPlay() override;
// Called every frame
virtual void Tick( float DeltaSeconds ) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Dimensions")
bool resetMap = false;
// How high 1 unit of height represents in the world
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Dimensions")
float tileHeight = 50.0f;
// The size of the meshes used to represent the tiles in the graph
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Dimensions")
float tileSize = 200.0f;
// The width of the graph, number of tiles along the X-axis
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Dimensions")
int32 mapWidth = 10;
// The height of the graph, number of tiles along the Y-axis
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Dimensions")
int32 mapHeight = 10;
// The array of tiles used in gameplay. Treated like a 2D array using the conversion functions
UPROPERTY(BlueprintReadOnly)
TArray<FTile> graph;
// Returns what would be the X and Y of a tile if the graph was a 2D array
UFUNCTION(BlueprintPure, Category = "Conversions")
FInt2 IndexToXY(const int32 index);
// Returns the index of a tile using what would be it's X and Y if the graph was a 2D array
UFUNCTION(BlueprintPure, Category = "Conversions")
int32 XYToIndex(const FInt2 XY);
// Returns the tile's transform.position based on the tileSize
UFUNCTION(BlueprintPure, Category = "Conversions")
FVector IndexToTransform(const int32 index);
// Clears the graph and resize it to the MapWidth x the MapHeight
UFUNCTION(BlueprintCallable, Category = "Map Generation")
void InitializeMap();
// Assigns all tiles their neighbouring tiles indexes
UFUNCTION(BlueprintCallable, Category = "Map Generation")
void AssignNeighbours();
// Returns the shortest path from 1 tile to another, if a path exist
// If no path exist, returns an empty array of int32
UFUNCTION(BlueprintPure, Category = "Pathfinding")
TArray<int32> GeneratePathTo(const int32 start, const int32 end);
// Sets the height of the tile at the index given
UFUNCTION(BlueprintCallable, Category = "Map Generation")
void SetHeightAt(const int32 index, float height);
// Sets the tile type of the tile at the index given
UFUNCTION(BlueprintCallable, Category = "Map Generation")
void SetTileTypeAt(const int32 index, const ETileTypes tileType);
};
.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "TRPG.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;
}
// Called when the game starts or when spawned
void ATileMap::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void ATileMap::Tick( float DeltaTime )
{
Super::Tick( DeltaTime );
}
// Returns what would be the X and Y of a tile if the graph was a 2D array
FInt2 ATileMap::IndexToXY(const int32 index) {
return FInt2(index % mapWidth, index / mapWidth);
}
// Returns the index of a tile using what would be it's X and Y if the graph was a 2D array
int32 ATileMap::XYToIndex(const FInt2 XY) {
return XY.y * mapWidth + XY.x;
}
// Returns the tile's transform.position based on the tileSize
FVector ATileMap::IndexToTransform(const int32 index) {
int x = IndexToXY(index).x;
int y = IndexToXY(index).y;
float sin60 = FMath::Sin(FMath::DegreesToRadians(60));
if (y % 2 != 0) { return FVector(x - 0.5f, -y * sin60, 0) * tileSize; }
else { return FVector(x, -y * sin60, 0) * tileSize; }
}
// Clears the graph and resize it to the MapWidth x the MapHeight
void ATileMap::InitializeMap() {
graph.Empty();
graph.Init(FTile(), mapWidth * mapHeight);
}
// Assigns all tiles their neighbouring tiles indexes
void ATileMap::AssignNeighbours() {
// Foreach FTile in the graph
for (int i = 0; i < mapWidth * mapHeight; i++) {
int32 x = IndexToXY(i).x;
int32 y = IndexToXY(i).y;
// Add tile on the left
if (x > 0) {
graph*.neighbours.Add(XYToIndex(FInt2(x - 1, y)));
}
// Add tile on the right
if (x < mapWidth - 1) {
graph*.neighbours.Add(XYToIndex(FInt2(x + 1, y)));
}
// If the Y is even
if (y % 2 == 0) {
if (y > 0) {
// Add tile on the bottom left
graph*.neighbours.Add(XYToIndex(FInt2(x, y - 1)));
if (x < mapWidth - 1) {
// Add tile on the bottom right
graph*.neighbours.Add(XYToIndex(FInt2(x + 1, y - 1)));
}
}
if (y < mapHeight - 1) {
// Add tile on the top left
graph*.neighbours.Add(XYToIndex(FInt2(x, y + 1)));
if (x < mapWidth - 1) {
// Add tile on the top right
graph*.neighbours.Add(XYToIndex(FInt2(x + 1, y + 1)));
}
}
}
// If the Y is odd
else {
// Add tile on the bottom right
graph*.neighbours.Add(XYToIndex(FInt2(x, y - 1)));
if (x > 0) {
// Add tile on the bottom left
graph*.neighbours.Add(XYToIndex(FInt2(x - 1, y - 1)));
}
if (y < mapHeight - 1) {
// Add tile on the top right
graph*.neighbours.Add(XYToIndex(FInt2(x, y + 1)));
if (x > 0) {
// Add tile on the top left
graph*.neighbours.Add(XYToIndex(FInt2(x - 1, y + 1)));
}
}
}
}
}
// Returns the shortest path from start to end, if a path exist
// If no path exist, returns an empty array of int32
// Uses -1 to represent a NULL tile
TArray<int32> ATileMap::GeneratePathTo(const int32 start, const int32 end) {
// 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>();
// Sets the default value for all FTiles
for (int32 i = 0; i < mapWidth * mapHeight; i++) {
// If it is not where we started from, tell it we don't know anything about it
if (i != start) {
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 u = -1;
// TODO Add comment
for (auto tile : unvisited) {
if (u == -1 || dist[tile] < dist) {
u = tile;
}
}
// Move on if we found the the end FTile
if (u == end) {
break;
}
unvisited.Remove(u);
// TODO Add comment
for (auto tile : graph.neighbours) {
// TODO Change +1 to +Cost to enter tile
float alt = dist + 1;
if (alt < dist[tile]) {
dist[tile] = alt;
prev[tile] = u;
}
}
}
// Break out of function if no path to the end was found
if (prev[end] == -1) {
return TArray<int32>();
}
// 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 = end;
while (curr != -1) {
reversedPath.Add(curr);
curr = prev[curr];
}
// TODO Change to: path.Reverse();
TArray<int32> path = TArray<int32>();
for (int32 i = reversedPath.Num() - 1; i > -1; i--) {
path.Add(reversedPath*);
}
return path;
}
// Sets the height of the tile at the index given
void ATileMap::SetHeightAt(const int32 index, float height) {
graph[index].height = height / tileHeight;
}
// Sets the tile type of the tile at the index given
void ATileMap::SetTileTypeAt(const int32 index, const ETileTypes tileType) {
graph[index].tileType = tileType;
}