UE4で大量のアクタを設置すると動作が重い

プログラムから床を生成するためにたくさんのアクタを設置した場合、それが画面内にたくさん含まれるときに20fps程度まで処理落ちしてしまいます。
ビューポートで一つ一つ手置きでマップを作るのは大変なためプログラムで壁やステージ等を作成しようと考えているのですが、どれだけ調べてもさらにたくさんのアクタを設置しても動作が軽くなる方法が分かりませんでした。
対策を教えていただけると嬉しいです。

// Fill out your copyright notice in the Description page of Project Settings.


#include "SpawnFloor.h"
#include "VanillaLib.h"
#include "Engine.h"			// GEngine ( ログ出力 )
#include "Materials/MaterialInstanceDynamic.h"
#include "BoxColor.h"
#include <string>

// Sets default values
ASpawnFloor::ASpawnFloor()
{
	// 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;
	count = 0;


	UStaticMeshComponent* visual_mesh;
	visual_mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	visual_mesh->SetupAttachment(RootComponent);
	visual_mesh->SetCollisionEnabled(ECollisionEnabled::Type::NoCollision);		// あたり判定無し
	visual_mesh->SetHiddenInGame(true);											// 描画しない

	static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Cube.Shape_Cube"));

	if (CubeVisualAsset.Succeeded())
	{
		visual_mesh->SetStaticMesh(CubeVisualAsset.Object);
		visual_mesh->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
	}
}

// Called when the game starts or when spawned
void ASpawnFloor::BeginPlay()
{
	Super::BeginPlay();

	float					size = GetActorScale().X * 100;
	FRotator				rot(0.f, 0.f, 0.f);
	FActorSpawnParameters	params;
	params.bAllowDuringConstructionScript = true;
	params.bDeferConstruction = false;
	params.bNoFail = true;
	params.Instigator = nullptr;
	params.Name = { };
	params.ObjectFlags = EObjectFlags::RF_NoFlags;
	params.OverrideLevel = nullptr;
	params.Owner = this;
	params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
	params.Template = nullptr;

	const int box_num_rx = 15;
	const int box_num_ry = 15;
	for (int ix = 0; ix < box_num_rx * 2 + 1; ix++)
	{
		for (int iy = 0; iy < box_num_ry * 2 + 1; iy++)
		{
			float color_add = VanillaLib::GetRandF(0.4f, 0.2f);
			ABoxColor::SetGlobalColor(FLinearColor(color_add, color_add, 0.8f));
			FVector pos(size * ix - size * box_num_rx, size * iy - size * box_num_ry, 130.f - VanillaLib::GetRandF(30.f));

			auto Something = GetWorld()->SpawnActor<ABoxColor>(pos, rot, params);

		}
	}
	//GEngine->AddOnScreenDebugMessage(-1, 5, FColor::White, "床を生成しました");
}

// Called every frame
void ASpawnFloor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);


	if (count == 100)
	{
	}

	count++;
}

// 追記 -------------------------------------------------------------------
// Called when the game starts or when spawned
void ASpawnFloor::BeginPlay()
{
Super::BeginPlay();

	float					size = GetActorScale().X * 100;
	FRotator				rot(0.f, 0.f, 0.f);
	FActorSpawnParameters	params;
	params.bAllowDuringConstructionScript = true;
	params.bDeferConstruction = false;
	params.bNoFail = true;
	params.Instigator = nullptr;
	params.Name = { };
	params.ObjectFlags = EObjectFlags::RF_NoFlags;
	params.OverrideLevel = nullptr;
	params.Owner = this;
	params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
	params.Template = nullptr;


	//auto my_actor = GetWorld()->SpawnActor<AActor>(AActor::StaticClass());
	//my_actor->SetActorLocation(FVector(0, 0, 140));
	//UInstancedStaticMeshComponent* ISMComp = NewObject<UInstancedStaticMeshComponent>(my_actor);
	//ISMComp->RegisterComponent();
	////ISMComp->SetStaticMesh(StaticLoadObject(UMaterial::StaticClass(), nullptr, TEXT("/Game/StarterContent/Materials/M_Basic_Wall")));
	//ISMComp->SetFlags(EObjectFlags::RF_Transactional);
	//this->AddInstanceComponent(ISMComp);
	


	//TActorIterator<ABoxColor> box_color_iterator(GetWorld());
	//ABoxColor* box_color_actor = *box_color_iterator;
	//
	//TArray<UInstancedStaticMeshComponent*> components;
	//box_color_actor->GetComponents<UInstancedStaticMeshComponent>(components);



	auto my_actor = GetWorld()->SpawnActor<ABoxColor>(FVector(0.f, 0.f, 140.f), rot, params);
	UInstancedStaticMeshComponent* ISMComp = NewObject<UInstancedStaticMeshComponent>(my_actor, UInstancedStaticMeshComponent::StaticClass());
	//ISMComp->AttachToComponent(box_color_actor->GetRootComponent(), FAttachmentTransformRules::KeepWorldTransform);
	//ISMComp->SetStaticMesh(StaticLoadObject(UMaterial::StaticClass(), nullptr, TEXT("/Game/StarterContent/Materials/M_Basic_Wall")));
	ISMComp->SetFlags(EObjectFlags::RF_Transactional);
	//ISMComp->SetWorldLocation(pos);


	//UStaticMeshComponent* visual_mesh;
	//visual_mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	//visual_mesh->SetupAttachment(RootComponent);

	//auto material_instance =
	//	visual_mesh->CreateAndSetMaterialInstanceDynamicFromMaterial(
	//		0, Cast<UMaterial>(StaticLoadObject(UMaterial::StaticClass(), nullptr, TEXT("/Game/StarterContent/Materials/M_Basic_Wall"))));
	////// マテリアルインスタンスへスカラーパラメーターを設定する。与える値の型は float
	////material_instance->SetScalarParameterValue("scalar_parameter_name", 1.23f);
	////// マテリアルインスタンスへベクターパラメーターを設定する。与える値の型は FLinearColor
	//material_instance->SetVectorParameterValue("Color", FLinearColor(0.5f,0.5f,0.5f));

	//static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Cube.Shape_Cube"));

	//if (CubeVisualAsset.Succeeded())
	//{
	//	//visual_mesh->SetStaticMesh(CubeVisualAsset.Object);
	//	GEngine->AddOnScreenDebugMessage(-1, 5, FColor::White, "Succeeded");
	//	//ISMComp->SetStaticMesh(CubeVisualAsset.Object);
	//}
	ISMComp->RegisterComponent();
	//this->AddInstanceComponent(ISMComp);
	
	FTransform transform;
	transform.SetScale3D(FVector(1.f, 1.f, 1.f));


	const int box_num_rx = 15;
	const int box_num_ry = 15;
	for (int ix = 0; ix < box_num_rx * 2 + 1; ix++)
	{
		for (int iy = 0; iy < box_num_ry * 2 + 1; iy++)
		{
			float color_add = VanillaLib::GetRandF(0.4f, 0.2f);
			ABoxColor::SetGlobalColor(FLinearColor(color_add, color_add, 0.8f));
			FVector pos(size * ix - size * box_num_rx, size * iy - size * box_num_ry, 130.f - VanillaLib::GetRandF(30.f));

			//auto Something = GetWorld()->SpawnActor<ABoxColor>(pos, rot, params);
		
			transform.SetLocation(pos);
			ISMComp->AddInstance(transform);
		}
	}

	//for (int ix = 0; ix < box_num_rx * 2 + 1; ix++)
	//{
	//	for (int iy = 0; iy < box_num_ry * 2 + 1; iy++)
	//	{
	//		TArray<UActorComponent*> currentICs = this->GetInstanceComponents();
	//		for (UActorComponent* ic : currentICs)
	//			ic->DestroyComponent();
	//	}
	//}

	//GEngine->AddOnScreenDebugMessage(-1, 5, FColor::White, "床を生成しました");
}
1 Like

1つのメッシュを大量に描画する場合はInstanced Static MeshまたはHierarchical Instanced Static Meshを利用することが一般的です。

多くのアクターが画面内に含まれるときにFPSが落ちるということなのでInstanced Static Meshは有効に働くかと思われます。

以下は参考資料です

UE4 Instanced Static MeshとHierarchical Instanced Static Meshの違い

Using Instanced Static Meshes in C++?

Construction Script と Instanced Static Mesh

Unreal* Engine 4 Optimization Tutorial, Part 2

1 Like

1つのメッシュを大量に描画する場合はInstanced Static MeshまたはHierarchical Instanced Static Meshを利用することが一般的です。

多くのアクターが画面内に含まれるときにFPSが落ちるということなのでInstanced Static Meshは有効に働くかと思われます。

以下は参考資料です

UE4 Instanced Static MeshとHierarchical Instanced Static Meshの違い

Using Instanced Static Meshes in C++?

Construction Script と Instanced Static Mesh

Unreal* Engine 4 Optimization Tutorial, Part 2

ご回答ありがとうございます、頂いたURLの確認や実際に試してからまた返信させていただきます。

頂いたリンクを参考にinstanced static meshについて調べてみたのですが、実際に生成することができませんでした。
質問欄に追記で試行錯誤した結果のコードを貼りましたので、可能であれば問題点を教えていただけると嬉しいです。
色のついたボックスを座標を指定して複数個生成したいです。

Instanced Static Meshを利用したボックスアクターの生成処理を書いてみたので箇条書きですが紹介します。

ヘッダファイルに次のような変数を宣言

private:
	// Instanced Static Meshにセットするスタティックメッシュ
	UPROPERTY()
		class UStaticMesh* UseInstancedMesh;

CPPファイルのコンストラクタで必要なアセットを読み込む

    // CubeアセットをContentから取得
    static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Cube.Shape_Cube"));
    if (CubeVisualAsset.Succeeded())
    {
        UseInstancedMesh = CubeVisualAsset.Object;

        // Cubeアセットに適用する自作マテリアルをContentから取得
        static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/M_Cube.M_Cube"));
        if (MaterialAsset.Succeeded())
        {
            UseInstancedMesh->SetMaterial(0, MaterialAsset.Object);
        }
    }

CPPファイルのBeginPlay関数にInstanced Static Mesh Componentを作成し複数個のキューブを生成させる

	Super::BeginPlay();

    if (UseInstancedMesh)
    {
        // Instanced Static MeshではDynamic Material Instanceは使えない.

        // thisが無いとRegisterComponent()でエラー
        UInstancedStaticMeshComponent* ISMComp = NewObject<UInstancedStaticMeshComponent>(this);
        ISMComp->RegisterComponent();
        ISMComp->SetStaticMesh(UseInstancedMesh);

        float size = GetActorScale().X * 100;

        FTransform SpawnTransform;
        const int box_num_rx = 15;
        const int box_num_ry = 15;
        for (int ix = 0; ix < box_num_rx * 2 + 1; ix++)
        {
            for (int iy = 0; iy < box_num_ry * 2 + 1; iy++)
            {
                float color_add = FMath::FRandRange(0.2f, 0.4f);
                FVector pos(size * ix - size * box_num_rx, size * iy - size * box_num_ry, 130.f - FMath::FRandRange(-30.f, 30.f));
                SpawnTransform.SetLocation(pos);
                ISMComp->AddInstance(SpawnTransform);
            }
        }
    }
1 Like

キューブに適用するマテリアルは以下のように組んでいます。

今回紹介したInstanced Static MeshですがDynamic Material Instanceがうまく機能しません。正確に言えばAdd Instanceしたメッシュ毎にDynamic Material Instanceの値を変えて適用することが出来ません。Set Vector Parameter Value等で色を変えるとすべてのメッシュが同じ色になります。

ですので、今回はマテリアル上でAdd Instanceしたメッシュ毎に色を変更するようにしました。

Instanced Static Meshを使ってなおかつ狙った場所にあるメッシュの色を変えたいという場合はいくらかコツが必要なようです。参考資料を載せておきます。

【UE4】Instanced Static Meshで部分的に色を変える

サンプルコードを書いていただきありがとうございます。
ブロックがしっかりと生成されているのは確認できました。
しかし、頂いたコードをそのままコピーし、マテリアルのパスを仮に以下に変更して実行してみたのですが、
/Game/StarterContent/Materials/M_Basic_Wall.M_Basic_Wall
マテリアルが適用されずに黒の格子状のブロックが表示されてしまいます。
通常のマテリアルではいけないのでしょうか?

すいません。書き忘れていました。

マテリアルには Used Instanced Static Meshes というチェックボックスにチェックを入れています。
マテリアルのDetailsパネルに Instanced と入力されると候補が出ますのでそれにチェックを入れてください。

311716-ss.png

1 Like

無事に床を生成することができました、有難うございましたm(__)m