HISMC performance issue: C++ vs Blueprints

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

With no luck in solving the performance issue in the editor when the C++ based blueprint is selected, I turned to searching for a workaround until some feedback on this comes through. The idea is simple, how to modify values in a blueprint and have them reflected to any of its instances in the level. Luckily, user DEDRICK had a solution to that here.

So, for now the C++ version works OK as long as its instance in the level is not selected. Instead having a child blueprint will behave nicely. Which leads me to ask why we have this behaviour with pure blueprint instance straight out of the box, but with C++ based blueprints this is not the case?

Just wanted to say that I think I’m still having this issue.

The issue doesn’t happen for me if I spawn the HISMC in the constructor, but if I spawn it in OnConstruction then I get the really bad editor performance as well.

Don’t know if there was another workaround

1 Like

I have spent way way way too many hours this week troubleshooting and profiling to resolve the same symptoms…to no avail. I spent this time because I kept assuming it was issues with my code and the way I was implementing my logic…but now I think this is an issue with the editor?

For context, I am working on an M2 Studio Mac, UE 5.5

I prototyped a grid system using blueprints and it is working perfect.
I use the BP’s construction script to generate all instances of the instanced static mesh and related variables and structs ‘on the fly’ so that I can see changes instantly in the editor. I have multisphere trace calls per instance as well, fill many map variables, etc.
I can create ~450 instances of a simple hexagon mesh during the BP’s construction script and it is very performant.

Now that I am doing some heavy logic, like A* pathfinding, I decided to re-create my grid creation/manipulation as a C++ actor class.

It works. Functionality is accurate when comparing to my original BP.

But performance is insanely poor…only when in editor interacting with the actor or when launching PIE, or when PIE closes.
Selecting the C++ actor with ~450 instances results in an editor freeze of ~18 seconds.
In the Editor Details panel for the actor, changing a variable that calls the OnConstruction function will produce the same freeze.

I have all logic implemented just as I did with the BP…and nothing is in the logs to help me deduce.
I have used FPlatformTime::Seconds() everywhere to print to the logs duration times, and the freezes are always after the grid functions are called, so editor only.

Of course, there could be an issue with my code implementation, but there are several threads in the forums that match this one and our problems…without resolutions, over years.
Seems like a strange editor problem.
Because of this, or my lack of experience/expertise, I am abandoning doing any instance static mesh stuff with C++.
~20 second freezes sprinkled continuously throughout the development life of my project is just not acceptable. :frowning:
The solution for me is using the BP that exhibits none of the editor performance issues.