For a pet project I am working on a simple randomly generated 3D tile map using a procedural mesh. I was struggling for a bit to find a good way to utilize the Navigation System. I have decided to post my solution here, as this principle is not very well documented.
I found it is possible to extend the UNavRelevantComponent
to modify navigation data on a 3D tilemap. The tile map is a relatively simple actor containing a procedural mesh and an array of tile data, which is built using SimplexNoise. For the sake of completion these are the relevant structs:
struct FTile
{
FTileType TileType;
float Height;
};
struct FTileType
{
UMaterial* Material;
bool Traversable = true;
};
The tile information is then stored in a simple one dimensional array on my custom ATileMap
. This data is created using a map generator. Exactly how it is created isn’t important for this.
TArray<FTile> Tiles;
As stated before, a procedurally generated mesh is generated from this tile data. The tile array indexes are converted to XY positions and vice versa. Now we have a procedural mesh with nice tiles. The question is how can we make these tiles untraversable based on the Traversable property? By default the navigation system will simply mark the whole mesh as traversable. After some digging around I found the UNavModifierComponent
and UNavRelevantComponent
which can be used to add navigation information to an actor. I created a UTileMapNavModifierComponent
to be added to the TileMap actor, providing additional nav information.
class UTileMapNavModifierComponent : public UNavRelevantComponent
{
public:
virtual void GetNavigationData(FNavigationRelevantData& Data) const override;
void CreateNavModifiers(FCompositeNavModifier& Modifiers) const;
};
void UTileMapNavModifierComponent::GetNavigationData(FNavigationRelevantData& Data) const
{
this->CreateNavModifiers(Data.Modifiers);
}
void UTileMapNavModifierComponent::CreateNavModifiers(FCompositeNavModifier& Modifiers) const
{
ATileMap* TileMap = Cast<ATileMap>(this->GetOwner());
for (int i = 0; i < TileMap->Tiles.Num(); i++)
{
// Skip tiles that are traversable
if (TileMap->Tiles[i].TileType.Traversable)
continue;
// Calculate tile world location
FVector2D Position2D = TileMap->IndexToXY(i) * TileMap->TileDimension;
FVector Position(Position2D.X, Position2D.Y, 0);
Position += TileMap->GetActorLocation();
FTransform Transform;
Transform.SetLocation(Position);
// Create a small box that is marked as blocking navigation
FBox Box(FVector(0, 0, 0), FVector(TileMap->TileDimension / 2, TileMap->TileDimension / 2, 10.0f));
// Add the box with the specified transform to the nav modifiers
Modifiers.Add(FAreaNavModifier(Box, Transform, UNavArea_Null::StaticClass()));
}
}
Hope this will help someone in the future.