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