Last updated on

Creating a Grid Framework for Unreal Engine (Part 2)


The Goal

With our grid in place, we now need a way to interact with it. To achieve this, it’s essential that we can actually see our tiles. The goal is to create a method for highlighting a single tile.

Prerequisites

Before we can start with the visuals we need to add a location to our tiles. For that purpose I created a beveled cube in Blender a placed them in a 8x11 grid.

Grid

Keep in mind that while the cube aligns perfectly with our grid, the cube and the tile objects are in no way connected to each other. In the image, the grid is selected, and you can see that the origin of the grid is in the bottom-left corner. It is also world-aligned, meaning the grid’s forward axis aligns with the world forward axis, and the same applies to the right and up axes.

When determining the location of a tile, we will use the grid’s location as the point of reference:

// header
virtual FVector GetTileLocation() const;
virtual FVector GetTileCenter() const;

// cpp
FVector UGridTile::GetTileLocation() const { 
 return Grid->GetActorLocation() + FVector(Coordinates.X * Size, Coordinates.Y * Size, 0.0f);
}

FVector UGridTile::GetTileCenter() const { 
 return GetTileLocation() + FVector(Size * 0.5f, Size * 0.5f, 0);
}

As you can see, we always use the grid location as our reference. This way, we can move the grid freely in the world, and our tiles will always be relative to it. It’s also worth noting that the tile location is always the corner that is closest to the grid location, instead of the center of the tile. Now we are nearly ready to tackle the visualization, but there is still one function missing. I’ll explain the purpose of this function later, but for now, you’ll just have to trust me.

FVector UGridTile::GetCornerLocationAtIndex(const int32 InIndex) {
 if(InIndex == 0) {    
  return GetTileLocation(true) + FVector(0,0,0);
 }
 if(InIndex == 1) {
  return GetTileLocation(true) + FVector(0,Size,0);
 }
 if(InIndex == 2) {
  return GetTileLocation(true) + FVector(Size,0,0);
 }
 if(InIndex == 3) {
  return GetTileLocation(true) + FVector(Size,Size,0);
 }
 LOG_WARNING("Invalid Index!");
 return FVector::Zero();
}

This function retrieves the location of every corner of the tile using a given index.

Highlight a Tile

To highlight a single tile, we create an actor with a decal component. The decal should be the same size as a tile. If we want to a highlight a specific tile now we can simply get the tile’s center location and move the highlight to that location. The result might look like this:

Tile Highlight Grid with a single tile highlight

But what if we don’t have a reference to the tile we want to highlight? This is often the case when we want to highlight the tile the cursor is hovering over. To achieve this, we need to add at least one new function to the grid class:

UGridTile* ATileGrid::GetTileAtLocation(const FVector InLocation) {
 
  const FVector CorrectedLocation = InLocation - GetActorLocation();
  // Convert location to tile index
  const int32 Y = FMath::Floor(CorrectedLocation.Y / TileSize);
  const int32 X = FMath::Floor(CorrectedLocation.X / TileSize);
  if(Y < 0 || X < 0 || Y >= Dimensions.Y || X >= Dimensions.X) {
   return nullptr;
  }
  const int32 TileIndex = Y + X * Dimensions.Y;
  //LOG_INT("Tile Index", TileIndex);
  if(Tiles.IsValidIndex(TileIndex)) {
   return Tiles[TileIndex];
  } 
 return nullptr;
}

UGridTile* ATileGrid::GetTileClosestToLocation(const FVector InLocation) {
 float BestDistance = BIG_NUMBER;
 UGridTile* Result = Tiles.Num() > 0 ? Tiles[0] : nullptr;

 for (UGridTile* GridTile : Tiles) {
  const float Distance = FVector::DistSquared(GridTile->GetTileCenter(), InLocation);
  if(Distance < BestDistance) {
   BestDistance = Distance;
   Result = GridTile;
  }
 }

 return Result;
}

These two functions kind of do the same but work slightly differently. In both cases, we assume we have a location of type FVector.  Possibly obtained by doing a line trace to the cursor location or some other means.

GetTileAtLocation: First, we transform the given cursor location into the local space of the grid. Then we divide the x and y values by the tile size and round the result down. This gives us the coordinates for the tile at the given location, assuming there is a tile there. If there is no tile at the location, then we return null. This function is pretty efficient, because it always takes the same amount of time, no matter the input or the grid size.

GetTileClosestToLocation: This function simply iterates the entire grid and compares the distance from the tile center to the given location. It will always return a valid tile, but it is less efficient since it potentially iterates the entire grid to find a fitting tile.

If you call the function every frame, I would go with GetTileAtLocation, otherwise both function should be totally fine. Just keep in mind to check the returned tile for a possible null pointer.

Now we can move the highlight actor to the hovered tile. See the blueprint code below for an example:

Tile Highlight Blueprint code to move a highlight actor to the hovered grid tile.

Next we’ll take a look at elevation.