こんにちはトイロジックのプログラマーGです。Unreal Engine は標準で多数の機能が備わっていますが、ある程度開発を続けているとプロジェクト内で「こういう機能が欲しい」という要望が出てきます。

本記事ではレベル班から実際に出た要望とそれに対して行ったエディタ拡張についてお話します。

なおUnreal Engine のC++プロジェクトを前提としています。(エンジンバージョンは 4.27.2 です)よろしくお願いいたします。

レベル班からの要望

レベルエディタでの背景制作が進んできたころ、レベルデザイン関係者(レベル班と呼んでいました)との間で次のような話題が出ました。

  • レベル内で 不必要な ShadowCast をしているメッシュを探しやすくしたい
  • レベル内で Unwalkable に設定し忘れたメッシュが無いか探したい

その頃のレベル上には数百の StaticMesh が存在していて、ひとつひとつを確認していくのが大変な状況でした。プログラマとしても無駄な設定がされているオブジェクトをすぐに見つけられる仕組みは欲しいと感じていました。

エディタ拡張の方針

そこで、レベルエディタの Viewport で該当する Mesh を強調表示できれば視覚的にわかりやすいのでは、と考えエディタの拡張に取り掛かりました。

強調表示を行う方法についてはDrawDebugMeshを使用することにしました。
エディタでの編集中にコンソールコマンドでDrawDebugMeshを呼び出す形になります。

DrawDebugMeshを利用した理由は主に以下の点です。

  • 比較的単純なコード追加でMesh形状を表示できる
  • レベルエディタの編集中でも実行できる
  • デバッグ表示用のメッシュやマテリアルなどのアセットを用意せずに済ませたかった
  • もともとデバッグ用の機能なので、レベル側への影響がない
  • 1~2時間で実装を済ませたかった

実装サンプル

以下がサンプルコードとなります(わかりやすくするため不正値のチェック等は省いています)。

コマンド登録部分

MyProjectActor.h

UCLASS()
class AMyProjectActor : public AActor
{
	GENERATED_BODY()

public:
	AMyProjectActor();

	UFUNCTION()
	void Execute_ViewUnwalkable(UWorld* world);

	UFUNCTION()
	void Execute_ViewMeshesCastShadow(UWorld* world);
};

MyProjectActor.cpp

AMyProjectActor::AMyProjectActor()
{
	if (!IsRunningCommandlet())
	{
		IConsoleManager::Get().RegisterConsoleCommand
		(TEXT("ViewMeshesUnwalkable")
			, TEXT("View unwalkable staticMeshes")
			, FConsoleCommandWithWorldDelegate::CreateUFunction(this, TEXT("Execute_ViewUnwalkable"))
			, ECVF_Default
		);


		IConsoleManager::Get().RegisterConsoleCommand
		(TEXT("ViewMeshesCastShadow")
			, TEXT("View staticMeshes casting shadow")
			, FConsoleCommandWithWorldDelegate::CreateUFunction(this, TEXT("Execute_ViewMeshesCastShadow"))
			, ECVF_Default
		);
	}
}

コマンドで実行される関数

MyProjectActor.cpp

template<typename judgedrawfunc="">
void DrawDebugStaticMesh(float lifeTime, FColor drawColor, UWorld* world, JudgeDrawFunc judgeFunc)
{
	if (!world->IsEditorWorld())
		return;

	for (TObjectIterator<ustaticmeshcomponent> itr; itr; ++itr)
	{
		UStaticMeshComponent* smc = *itr;

		if (AActor* ownerActor = smc->GetOwner())
		{
#if WITH_EDITORONLY_DATA
			if (ownerActor->IsHiddenEd())
				continue;
#endif // WITH_EDITORONLY_DATA

			if (ownerActor->HasAnyFlags(RF_ClassDefaultObject))
				continue;
		}

		if(judgeFunc(smc))
		{
			UStaticMesh* staticMesh = smc->GetStaticMesh();
			if (IsValid(staticMesh))
			{
				FStaticMeshLODResources& lodResource = staticMesh->GetRenderData()->LODResources[0];
				FPositionVertexBuffer& vertexBuffer = lodResource.VertexBuffers.PositionVertexBuffer;
				FIndexArrayView indicesView = lodResource.IndexBuffer.GetArrayView();

				const FTransform& componentTransform = smc->GetComponentTransform();
				TArray<fvector> Verts;
				for (uint32 Index = 0; Index < vertexBuffer.GetNumVertices(); Index++)
				{
					const FVector vertexLocation = componentTransform.TransformPosition(vertexBuffer.VertexPosition(Index));
					Verts.Add(vertexLocation);
				}

				TArray<int32> indices;
				for (int32 i = 0; i < indicesView.Num(); ++i)
				{
					indices.Add(static_cast<int32>(indicesView[i]));
				}

				DrawDebugMesh(world, Verts, indices, drawColor, false, lifeTime);
			}
		}
	}
}

// Unwalkable に設定されている Mesh を強調表示する
void AMyProjectActor::Execute_ViewUnwalkable(UWorld* world)
{
	auto _judgeDrawFunc = [](UMeshComponent* MC) // <- Unwalkable かを判定する関数 
	{ 
		auto& walkableSlopOverride = MC->GetWalkableSlopeOverride();
		return (walkableSlopOverride.GetWalkableSlopeBehavior() == WalkableSlope_Unwalkable);
	};

	const float lifeTime = 10.f;
	const FColor drawColor = FColor::Green;

	DrawDebugStaticMesh(lifeTime, drawColor, world, _judgeDrawFunc);
}

// CastShadow が On になっている Mesh を強調表示する
void AMyProjectActor::Execute_ViewMeshesCastShadow(UWorld* world)
{
	auto _judgeDrawFunc = [](UMeshComponent* MC) // <- CastShadow フラグを判定する関数 
	{ 
		return (MC->CastShadow != 0);
	};

	const float lifeTime = 10.f;
	const FColor drawColor = FColor::Orange;

	DrawDebugStaticMesh(lifeTime, drawColor, world, _judgeDrawFunc);
}
</int32></int32></fvector></ustaticmeshcomponent></typename>

※補足
ViewportのViewModeの追加も検討しましたが、エンジンコードの調整やシェーダの追加などが必要そうだったため時間の関係で断念しました。こちらは別の機会に挑戦したいです。

動作させたところ

実際にコマンドを呼び出した様子がこちらです。エディタでの編集中に、登録した「ViewMeshesCastShadow」コマンドを実行しているところです。


影をキャストする設定になっている画面中央のActorが強調されています。

こちらは、エディタでの編集中に、登録した 「ViewMeshesUnwalkable」コマンドを実行しているところです。


画面中央の一部のStatickMeshActorだけが Unwalkable に設定されており、それが強調表示されています。

最後に

本記事では DrawDebugMesh を使っての特定の条件のオブジェクトをで可視化するエディタ拡張についてご紹介しました。

上記のサンプルはかなり単純なものですが、少しのコード変更で以下のようにもできます。

  • さらに条件文とコマンドを追加する
  • コンソールコマンドの引数で動作を変更できるようにする
  • SkeletalMeshにも適用する
  • EditorUtilityWidget から細かいパラメータを渡して実行する

…などなど。

描画に関しては DrawDebugMesh以外を利用した方法もあると思います。エディタの機能拡張は色々なアプローチがありますので、作っているゲームと作業者の要求に合わせて実装方法を選択してみてください。

小ネタではございましたが、どなたかの参考になれば幸いです。ここまで読んでいただきありがとうございました!

著者紹介 G
2010年トイロジックに新卒入社。いくつかのプロジェクトでエネミーキャラの制御、UI制作、ネットワークなどの業務を担当後、メインプログラマを経験。現在はアプリ層でシステム寄りの業務が多い。工場ゲームをこよなく愛し、対戦ゲームは恥ずかしいくらい弱い。