Creating a Grid Framework for Unreal Engine (Part 1)
The Idea
When I was brainstorming ideas for my next game development project, I decided that I wanted to create a turn-based game or, at the very least, implement common mechanics found in such games. I also wanted to incorporate a grid system, similar to what you see in XCOM or Into the Breach. As a big fan of turn-based tactics games—especially those where you control a small group of units—I felt that a grid system would be essential. Since the grid serves as the foundation for all subsequent mechanics, I chose to start by developing it. To ensure reusability, I created a plugin for the grid system.
Note
The code I will provide is not complete and won’t work if simply copied and pasted. This blog focuses more on the idea and the thought process behind it.
The Grid Plugin
The plugin is made of two major classes: ATileGrid and UGridTile. The grid class stores grid-related information such as tile size, dimensions and of course the tiles themselves. The tile class, on the other hand, stores its coordinates, index and all other tile-related data.
The Grid Class
I wanted the grid to be as flexible as possible, so the goal is to keep it as general as possible. However, we can still define some common properties that every grid should have:
- Tile Class: The class we instantiate for each tile.
- Dimensions: The number of tiles on each axis of the grid, best represented by an integer tuple.
- Tile Size: For our purposes a single number will suffice.
- Tiles: An array to access any of our tiles.
However, if we want rectangular tiles where one side is longer than the other, a single number is not enough. That said, I couldn’t find any games that use non-uniform tiles. I suspect these kinds of grids don’t make for good gameplay, so we’ll stick with a single number for tile size.
class GRIDTOOLS_API ATileGrid : public AActor {
GENERATED_BODY()
protected:
UPROPERTY(EditInstanceOnly, Category = "Grid" )
TSubclassOf<UGridTile> TileClass = UGridTile::StaticClass();
UPROPERTY(EditInstanceOnly, Category = "Grid" )
FTileCoords Dimensions = FTileCoords(5);
UPROPERTY(EditInstanceOnly, Category = "Grid" )
float TileSize = 100.0f;
UPROPERTY(VisibleAnywhere, Category = "Grid" )
TArray<UGridTile*> Tiles;
};
For the dimensions property I implemented my own struct FTileCoords, that I will mainly use for the coordinates of the tiles (hence the name). I couldn’t find a exposable integer tuple struct in the engine, so I made one myself.
USTRUCT(BlueprintType, Blueprintable)
struct FTileCoords {
GENERATED_BODY()
/**
* Row
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 X;
/**
* Column
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Y;
FTileCoords() {
Y = -1;
X = -1;
}
FTileCoords(const int32 InValue) {
Y = InValue;
X = InValue;
}
};
Taking all this into account we can create the first class of the plugin. When it comes to implementing the class for the grid, there is another decision we must make: should we inherit from the Actor or the UObject class?
I chose the Actor class as the parent for the grid class. This allows me to place a grid inside the level and configure the settings via the details panel. Additionally, I can create functions that can be called from the details panel (using CallInEditor) to generate the grid or display debug information. This also enables us to move the grid in world space, which will be important for the tile placement. More on that topic later.
With the grid setup, we can now focus on the tiles.
The Tile Class
While it’s possible to manage all the data within the grid itself, extracting that logic into a separate tile class is a much better approach. This way, you can easily swap out the tile class to create a different type of grid. If designed well, you could even replace a square tile class with a hexagonal one and create a hex grid simply by changing the tile class. I might cover that later, but no promises!
For the tile, we can define the following properties:
- Index: The index of the tile. It is assigned by the grid creating the tile and is in ascending order.
- Coordinates: The coordinates of the tile. Where X represents the row and Y the column.
- Size: The size of the tile. Assigned by the creating grid.
For now, I’ll create a base tile class that models a square tile, meaning each edge has the same length. Additionally, we’ll assume that the grid is always world-aligned, meaning there are no rotation values for the Z-axis other than zero.
UCLASS(Blueprintable, BlueprintType)
class GRIDTOOLS_API UGridTile : public UObject {
GENERATED_BODY()
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
int32 Index;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
FTileCoords Coordinates;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
float Size;
};
Creating the Grid
We can now create a blueprint inheriting from our ATileGrid class and place it in a level. Using a UFunction with a CallInEditor specifier (see below), we can now create the grid by clicking a button in the grid’s details panel.
// header
UFUNCTION(CallInEditor, Category = "Grid" )
virtual void CreateGrid();
// cpp
void ATileGrid::CreateGrid() {
ClearGrid();
if(!TileClass) {
LOG_ERROR(TEXT("No Tile class!"));
return;
}
for(int32 x = 0; x < Dimensions.X; x++) {
for(int32 y = 0; y < Dimensions.Y; y++) {
FString TileName = FString::Printf(TEXT("Tile_%d_%d"), x, y);
UGridTile* Tile = NewObject<UGridTile>(this, TileClass, FName(*TileName));
Tile->Init( y + (x * Dimensions.X), FTileCoords(x, y), TileSize, this);
Tiles.Add(Tile);
}
}
}
That’s the function I use to create the grid. Let’s go through it line by line:
- Before we can create a new grid we need to clear the existing one. That what the ClearGrid function does.
- Now we check if the tile class is set. If it’s not set we print an error and return.
- Next comes a nested for loop. In its body we instantiate a tile and initialize it.
- At first we construct a name for the tile object. This way we can identify a tile by its name.
- Next we create the tile object using the NewObject function.
- Then we initialize the tile giving the tile its index, coordinates, size and a reference to the grid object.
- Last but not least the tile is added to the tiles array
When we now call this function the grid actor will create a set of tiles. Unfortunately it in the nature of this implementation that there is nothing to see. All our tiles are virtual objects with no 3D representation, but we are going to change that in the next post.