Last updated on

Creating a Grid Framework for Unreal Engine (Part 4)


The Grid Entity Interface

Now that our tiles can have different elevations, we can move on to actually place something on our tiles. We could, of course, simply place an actor at any tile’s center, and it would look like this actor is standing on the corresponding tile, but we also need a relation in our data model that reflects this relation.

To model this relationship we could give the tile a simple reference to an actor, but we will be using an interface instead.

I call the interface IGridEntityInterface and it looks like this:

UINTERFACE(MinimalAPI)
class UGridEntityInterface : public UInterface {
	GENERATED_BODY()
};

class GRIDTOOLS_API IGridEntityInterface {
	GENERATED_BODY()
public:
	virtual void SetCurrentTile(UGridTile* NewTile, bool bMoveToLocation = false) = 0;
	virtual UGridTile* GetCurrentTile() = 0;
};

It has two abstract functions: One to set the current tile it is associated with and one to get the tile. You can make any purely native function abstract by adding the = 0 at the end of the declaration. This way you do not have to implement a default implementation for the interface itself, but every class that implements the interface needs to implement the function.

The tile, on the other hand, will get a pointer to an entity of the type IGridEntityInterface, an event and a function to set and get the property.

// Inside the UGridTile class
DECLARE_MULTICAST_DELEGATE_OneParam(FGridEntityDelegate, IGridEntityInterface* GridEntity);

protected:
    IGridEntityInterface* CurrentEntity = nullptr;
public:
    FGridEntityDelegate OnEntitySet;
    FGridEntityDelegate OnEntityLeft;

// ======================== Inside the cpp ===================
void UGridTile::ClearEntity() {
	if(CurrentEntity) {
		OnEntityLeft.Broadcast(CurrentEntity);
	}
	CurrentEntity = nullptr;
}

bool UGridTile::SetEntity(IGridEntityInterface* InEntity) {
	if(CurrentEntity) {
		LOGF_WARNING(TEXT("Tile %s is already occupied by %s"), *Coordinates.ToString(), *CurrentEntity->_getUObject()->GetName());
		return false;
	}		
	OnEntitySet.Broadcast(InEntity);
	CurrentEntity = InEntity;
	return true;
}

With this in place, we now have a bidirectional relation between the tile and any kind of entity occupying it.

Practical Example

Next, we create a new class AGridActor inheriting from actor and implementing the IGridEntityInterface interface. All that is left to do, is implementing the two functions from the interface:

void AGridActor::SetCurrentTile(UGridTile* NewTile) {
	CurrentTile = NewTile;
	SetActorLocation(CurrentTile->GetTileCenter());
}
UGridTile* AGridActor::GetCurrentTile() {
	return CurrentTile;
}

Note

We also set the location when we set the current tile. This ensures that the data model and the visual representation are always identical.

Testing it

To test our code, we put the following in the BeginPlay function of the AGridActor class:

UGridTile* Tile = Grid->GetTileClosestToLocation(GetActorLocation());
if(Tile) {
    Tile->SetEntity(this);
	Entity->SetCurrentTile(Tile);
} else {
    LOG_ERROR(TEXT("Could not find a tile for this Actor!"));
}

Note

One could also wrap the setting of the tile and the interface in a function of the ATileGrid class.

Now, create a blueprint of the AGridActor and place it in our level. When you press play, the actor should automatically snap to a location on the grid.

In the next chapter, we will create a utility library for grid-related functions, and learn how to highlight an area of tiles.