Hi! I’m working on a simple hex tile map generator as part of a research for my dissertation. I successfully recreated the blueprint from [this video][1] with some minor tweaks to accommodate hexagonal tiles. I also used hierarchical static meshes as I read that they can handle LOD. Next step was to implement this in C++ so I created a C++ class and a blueprint based on it. I get the same result regarding where the tile instances are placed in the world, however there is a noticeable performance drop for the C++ class for the same amount of tiles (from 80 FPS for blueprint, to 2-5 FPS for C++). Also, increasing the number of tiles is quite fast for the Blueprint version (100x100 tiles just a slight delay), but for the C++ instance it starts choking for more than 25x25 tiles. Both instances are using construction script to add the tiles.
To be more precise, the performance suffers only in the editor, but not during the game play. I’ve checked the GPU profiler and apparently biggest chunk of time is spent on PostProcessSelectionOutlineBuffer. So I tested selecting nothing, selecting the Blueprint instance and then selecting C++ instance. Sure enough, when nothing is selected editor runs at >100 FPS, with the Blueprint instance selected it drops to ~80 FPS and then with the C++ instance selected it freezes, fills the RAM to 4 GB (my machine has 8 GB total), and after several seconds it responds with frame rate below 5 FPS. Deselecting the C++ instance takes as long as well, and it frees up the RAM (1.4 GB with nothing selected). I hope I’m doing something very wrong in my code and thus having this performance hit and that someone will catch what is wrong. I’m using UE 4.21.1 built from source code. All screenshots are bellow the code.
.h file:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MapGenerator.generated.h"
class UHierarchicalInstancedStaticMeshComponent;
class TileData;
// TODO: Reference - https://answers.unrealengine.com/questions/551199/dynamic-2d-array-using-tarray.html
// A helper struct for creating a 2D array
USTRUCT()
struct FTileData {
GENERATED_BODY()
public:
TArray<TileData*> ColumnArray;
TileData* operator[] (int32 i) { return ColumnArray[i]; }
void Add(TileData* m) { ColumnArray.Add(m); }
};
UCLASS()
class ACO_NPC_BEHAVIOUR_API AMapGenerator : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMapGenerator();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
// Gets called when instance of this class is placed in the scene
virtual void OnConstruction(const FTransform& Transform) override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(VisibleDefaultsOnly, Category = "Generator Setup")
USceneComponent* Root;
UPROPERTY(EditAnywhere, Category = "Generator Setup|Tile Types")
UHierarchicalInstancedStaticMeshComponent* InstancedSMC_Generic;
UPROPERTY(EditAnywhere, Category = "Generator Setup|Tile Types")
UHierarchicalInstancedStaticMeshComponent* InstancedSMC_Plain;
UPROPERTY(EditAnywhere, Category = "Generator Setup|Tile Types")
UHierarchicalInstancedStaticMeshComponent* InstancedSMC_Water;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
UStaticMesh* TileStaticMesh;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
int32 Rows = 8;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
int32 Columns = 8;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
float TilePitch = 0.f;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
float TileYaw = 90.f;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
float TileRoll = 0.f;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
float TileScaleX = 1.f;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
float TileScaleY = 1.f;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
float TileScaleZ = 1.f;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
float TileLength = 200.f;
UPROPERTY(EditAnywhere, Category = "Generator Setup")
float TileHeight = 173.2051f;
UPROPERTY(VisibleAnywhere, Category = "Generator Setup|Info")
TArray<FTileData> TileDataSet;
UPROPERTY(VisibleAnywhere, Category = "Generator Setup|Info")
TMap<FString, FTileData> TileDataMap;
};
.cpp file:
// Fill out your copyright notice in the Description page of Project Settings.
#include "MapGenerator.h"
// Game includes
#include "Public/TileData.h"
// Engine includes
#include "Engine/StaticMesh.h"
#include "Components/SceneComponent.h"
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
// Sets default values
AMapGenerator::AMapGenerator()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
Root = CreateDefaultSubobject<USceneComponent>(TEXT("RootSceneComponent"));
if (!SetRootComponent(Root))
{
UE_LOG(LogTemp, Error, TEXT("Unable to set the Root Component in MapGenerator.cpp!"));
}
InstancedSMC_Generic = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("HierarchicalInstancedSMC_Generic"));
InstancedSMC_Generic->AttachToComponent(Root, FAttachmentTransformRules::KeepRelativeTransform);
InstancedSMC_Plain = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("HierarchicalInstancedSMC_Plain"));
InstancedSMC_Plain->AttachToComponent(Root, FAttachmentTransformRules::KeepRelativeTransform);
InstancedSMC_Water = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("HierarchicalInstancedSMC_Water"));
InstancedSMC_Water->AttachToComponent(Root, FAttachmentTransformRules::KeepRelativeTransform);
}
// Called when the game starts or when spawned
void AMapGenerator::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AMapGenerator::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Construction script
void AMapGenerator::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
// Null-checks
if (InstancedSMC_Generic == nullptr
|| InstancedSMC_Plain == nullptr
|| InstancedSMC_Water == nullptr
|| Root == nullptr) return;
//Register all the components
RegisterAllComponents();
// Hierarchical Static Mesh Component setup
InstancedSMC_Generic->CreationMethod = EComponentCreationMethod::UserConstructionScript;
InstancedSMC_Plain->CreationMethod = EComponentCreationMethod::UserConstructionScript;
InstancedSMC_Water->CreationMethod = EComponentCreationMethod::UserConstructionScript;
InstancedSMC_Generic->ClearInstances();
InstancedSMC_Plain->ClearInstances();
InstancedSMC_Water->ClearInstances();
InstancedSMC_Generic->SetFlags(RF_Transactional);
InstancedSMC_Plain->SetFlags(RF_Transactional);
InstancedSMC_Water->SetFlags(RF_Transactional);
const auto tileHeight = TileLength * FMath::Sqrt(3) / 2;
const auto tileOffsetX = 3 * TileLength / 4;
const auto tileOffsetY = tileHeight / 2;
for (auto i = 0; i < Rows; i++)
{
for (auto j = 0; j < Columns; j++)
{
FVector location;
location.X = GetRootComponent()->GetComponentLocation().X + j * tileOffsetX;
location.Y = GetRootComponent()->GetComponentLocation().Y + i * tileHeight;
if (j % 2 != 0) { location.Y = location.Y - tileOffsetY; }
location.Z = GetRootComponent()->GetComponentLocation().Z;
FTransform transform(
FRotator(TilePitch, TileYaw, TileRoll),
location,
FVector(TileScaleX, TileScaleY, TileScaleZ)
);
if (i % 3 == 0 && j % 3 == 0)
{
InstancedSMC_Generic->AddInstanceWorldSpace(transform);
}
else if (i % 5 == 0 && j % 5 == 0)
{
InstancedSMC_Water->AddInstanceWorldSpace(transform);
}
else if (i % 7 == 0 && j % 7 == 0)
{
// No tiles
}
else
{
InstancedSMC_Plain->AddInstanceWorldSpace(transform);
}
// TODO: maybe replace this array with DataTable
// TileDataSet[i].Add(NewTileData);
}
}
// UE_LOG(LogTemp, Warning, TEXT("Total instances: %d"),
// InstancedSMC_Generic->GetInstanceCount()
// + InstancedSMC_Plain->GetInstanceCount()
// + InstancedSMC_Water->GetInstanceCount()
// );
}
GPU profiler:
No selection:
Blueprint selection:
C++ selection:
EDIT: crucial section of the pure blueprint - the beginning of it is just calculating hex specific stuff