Last updated on

Creating a Grid Framework for Unreal Engine (Part 3)


Elevation

Before we move on to topics like movement and are highlighting, I want to talk about different elevations. This is of course purely optional if your grid is always at the same height, but different elevations can open some interesting gameplay features, like high ground.

Since we have no way to set settings on our tiles directly just yet (something like a grid editor), we need another way to determine the elevation of each tile. An automated approach would be preferable, so we don’t have to adjust the elevation for each single tile. In order to automate this process, we will use ray casts and a special collision object channel.

Grid Height

We start by giving the grid a height property.

UPROPERTY(EditInstanceOnly, Category = "Grid")
float GridHeight = 200.0f;

We use this property to define the height of the grid, basically defining its bounding box for calculating the elevations for the tiles.

Tip

I use a box collision component that I update in the OnConstruction method to draw a bounding box for the grid. This makes it easy to see, what is part of the grid and what is not.

Bounding Box Bounding box in orange

Collision Object Channels

In Unreal Engine, every kind of collision component has an Object Type. The object type itself has no immediate effect, but it allows us to query for collisions with a specific object type. So if we want to trace for an object of the type WorldStatic, we can simply use the function LineTraceSingleByObjectType with the corresponding object type. It will only return components/actors with the specific object type. While the engine comes with a bunch of predefined object types, you can also create your own. For our purpose, I created an object type Ground and a collision profile of the same name.

Grid Ground Collision Profile

The collision profile simply blocks all channels and object types similar to the World Static profile, but the object type is Ground.

Trace for Elevations

We can now create a property on the grid class, and assign the created object type.

UPROPERTY(EditAnywhere, Category = "Grid")
TEnumAsByte<ECollisionChannel> GroundChannel;

Next we implement a function for our tile class, that performs traces to find the height of the ground.

void UGridTile::TraceForElevation(UWorld* World) {
    ElevationOffsets.Empty();
    if(World) {
        FHitResult Hit;
        FVector Start;
        FVector End;
        FCollisionQueryParams Params;
        FCollisionObjectQueryParams ObjectQueryParams = FCollisionObjectQueryParams(Grid->GetGroundCollisionChannel());
        TArray<FVector> Starts;		
        const float ShrinkageFactor = Grid->GetShrinkageScaleFactor();
        Starts.Add(GetTileCenter() + FVector(-GetTileSize(), -GetTileSize(), 0.0f) * 0.5f * ShrinkageFactor);
        Starts.Add(GetTileCenter() + FVector(-GetTileSize(), GetTileSize(), 0.0f) * 0.5f * ShrinkageFactor);
        Starts.Add(GetTileCenter() + FVector(GetTileSize(), -GetTileSize(), 0.0f) * 0.5f * ShrinkageFactor);
        Starts.Add(GetTileCenter() + FVector(GetTileSize(), GetTileSize(), 0.0f) * 0.5f * ShrinkageFactor);
        for(int32 i = 0; i < 4; i++) {
            Start = Starts[i] + FVector::UpVector * (Grid->GetGridHeight() + 1.0f);
            End = Start - FVector::UpVector * Grid->GetGridHeight() * 1.1f;
            if(World->LineTraceSingleByObjectType(Hit, Start, End, ObjectQueryParams, Params)) {
                ElevationOffsets.Add(FMath::Max(Grid->GetGridHeight() + 1.0f - Hit.Distance, 0.0f));				
            } else {
                ElevationOffsets.Add(0.0f);
            }
        }
    }
}

We use an array (ElevationOffsets) to store the elevation for every corner of our tile. Make sure to use the grid’s ground collision channel, which we just created. The trace starts at the corner of a tile but is offset on the z axis, so it is at the same height as the height of the grid. We also add 1 cm to make sure that the trace starts above the tile.

When we simply cast down from the corner of a tile, we might run into a problem (see image below), if the tile and the underlying geometry have the same size. This is also the case in the title image.

collision Issue Issue with detecting different heights between two tiles.

As you can see, the trace could either belong to Tile A or Tile B, but it will collide with the higher geometry that is associated with Tile B. To prevent this issue, we scale down or offset the start locations towards the center. This way, every tile has unique trace start locations.

In the code above, this is handled by the ShrinkageFactor, which basically moves the corner points of the tile uniformly towards the center.

Lastly, we calculate the elevation offset using the trace hit’s distance and the grid height. This way, the elevation is relative to the grid.

Why four traces?

If your world only has flat plateaus of different heights, a single center ray cast would suffice. This setup is slightly more versatile, since it gives us everything we need to incorporate slopes into our grid. We might take a look into slopes at a later point.

In the next post, we are going to look at how to place actors on the grid.