CC2の楽屋裏

ゲーム制作会社サイバーコネクトツー公式ブログ

*

第008回:ゲームクリエイターは破壊表現が好き。

      2021/03/11

皆さんボンジョルノ(イタリアのあいさつ)!
乱読派プログラマ親泊です。

現代ゲームにおいて木や草、石や小物を大量に表示するのはほぼ必須項目となっています。
UE4において、これらのオブジェクトを大量に生成するために用意されているのがInstancedStaticMeshComponentです。
そして、ゲームクリエイターはこれらのオブジェクトを破壊させるのがなぜか好きです。

今回はこのInstancedStaticMeshComponentを利用しているオブジェクトを破壊する実装をした時の失敗と回避方法をご紹介します。

InstancedStaticMeshComponentについて

まずInstancedStaticMeshComponentではPerInstanceSMDataという配列にトランスフォームの情報のみを持たせ、登録したメッシュをその位置に表示するという方法で実装されています。

tb008_00

tb008_01

この配列にAddInstance、RemoveInstanceでトランスフォーム情報を追加することによって、メッシュを増やしたり減らしたりすることができます。

tb008_02

tb008_03

インスタンス破壊表現の実装

私は最初「おっ、消去できるな。」とシンプルにRemoveInstanceで削除する方法で実装しました。

しかし、ここで問題が発生しました。
この実装方法では、たまたま複数のインスタンスが同時に消去されると、なぜか全く関係ないインスタンスが消去されます。

これはRemoveInstanceでPerInstanceSMDataの配列が詰められ、配列番号がずれてしまうことが原因でした。

回避策

これの回避策としては1つ思い付き、2つアドバイスをいただきました。

1.破壊するインスタンスのトランスフォーム情報を覚えて、配列を全検索する。
これは私が思いついた愚策です…。
メリット:配列の順番に関係なくインスタンスを消すことができる。
デメリット:配列が大きければ大きいほど重い。

InstancedStaticMeshはいかに軽く大量のオブジェクトを表示するかという課題を解決するためのものなのに、大量に配置できないという自己矛盾を引き起こしてしまいました…。

2.消去せずスケールを0にする。
メリット:簡単に実装できて、正しい見た目を実現できる。
デメリット:見えないだけでそこにいる。継承クラスのHierarchicalInstancedStaticMeshで利用するとカリングされない。

SampleCode
void UDrewDebugInstancedStaticMeshComp::InstanceScaleToZero(int32 InstanceIndex)
{
	FTransform InstanceTransform;
	GetInstanceTransform(InstanceIndex, InstanceTranform);

	FTranform ZeroScaledTransform = InstanceTransform;
	ZeroScaledTransform.SetScale3D(FVector::ZeroVector);

	// Transformを更新する.
	UpdateInstanceTransform(Index, ZeroScaledTransform);
}

こちらはアドバイスしてもらった方法です。
カリングされないので、見えないインスタンスがパフォーマンスを圧迫してしまいます。といってもパフォーマンスが上がらないだけで、下がることはないので、気になるほどではありませんでした。
UpdateInstanceTransformで同期をとらなければならないことに注意しましょう。

3.配列の末尾と交換して、末尾を消去する。
メリット:配列の大きさに関係なく等価のパフォーマンスで消去できる。
デメリット:要素の入れ替え処理が少し複雑で、工夫が必要。

SampleCode
void UDrewDebugInstancedStaticMeshComp::RemoveSwapInstance(int32 InstanceIndex)
{
	// 末尾と入れ替える.
	int32 LastIndex = PerInstanceSMData.Num() - 1;
	if(InstanceIndex != LastIndex)
	{
		// FInstancedStaticMeshInstanceData は Tramsformしかメンバが無いので不必要。
		// PerInstanceSMData[InstanceIndex] = PerInstanceSMData[LastIndex];

		// Transformを更新する.    
		UpdateInstanceTransform(InstanceIndex, FTransform(PerInstanceSMData[LastIndex].Transform));
	}
	RemoveInstance(LastIndex);
}

こちらもアドバイスしてもらった方法です。
配列の末尾と交換して消去は昔からよく使われているテクニックのようで、これなら配列番号のずれを気にせずにインスタンスを消去することができます。
UpdateInstanceTransformで同期をとらないと、コリジョンとメッシュが別々の場所に発生するなどの不具合が発生するので、注意が必要です。

また、この方法は要素の入れ替えが起きる為、同時に複数の削除を行う場合に注意が必要になります。
削除対象のIndex値は入れ替えが行われる前の並びを元にしているので、削除順番によっては既に削除された要素を指す場合があります。
方法はいくつかあると思いますが、Indexの大きい順から削除する方法で問題を回避します。

SampleCode
void UDrawDebugInstancedStaticMeshComp::ExecuteRemoveInstances()
{
	// Indexが大きい順にソート.
	RemoveInstanceIndices.Sort([](int32 A, int32 B)
	{
		return A > B;
	});

	for (int32 RemoveIndex : RemoveInstanceIndices)
	{
		RemoveSwapInstance(RemoveIndex);
	}

	RemoveInstanceIndices.Empty(RemoveInstanceIndices.Num());
}

おわりに

さて、今回のTechBlogいかがだったでしょうか?
インスタンシングの技術は大量のオブジェクトを表示する現代ゲームの必須技術です。
いかに軽く・大量にをコンセプトにしているため、様々な制約が生じますが、大変便利なのでうまく利用してゲームに生かしていきましょう!

皆さんのご意見、ご要望をお待ちしています。下記 CC2技術ブログのメッセージフォームにて、お気軽にお問い合わせください。

「CC2技術ブログ」へのご意見・ご要望はこちらから

 - CC2技術ブログ ,

  関連記事

CC2技術ブログ
第006回 コマンドラインからConfig設定を上書きする時の注意点

皆さん、こんにちは。サイバーコネクトツーでテクニカルサポートのマネージャーをして …

第003回 UE4のショートカットキーの仕組み

今回は ツール系テクニカルアーティスト しばはらがお送りします。 突然ですが、P …

tb004_09
第004回 UE4のアセットを一括修正する

ツール系テクニカルアーティストのしばはらです。 プロジェクトが進んで、ある程度ア …

第001回 UE4のコンテンツブラウザフィルターを自作する

はじめまして サイバーコネクトツーでツール系テクニカルアーティストをしております …

tb002_04
第002回 アクタのトランスフォームの実態

皆さんストラーズドヴィーチェ(ロシアのあいさつ)! 今回の担当は、新人プログラマ …

tb004_15
第005回 Tick関数がどう処理されているかUnrealC++を追ってみよう!

皆さんグーテンターク(ドイツのあいさつ)! CC2TechBlog今回の担当は新 …

スクリーンショット (47)
第007回UE4のアセットの参照方法について、そのロードの違い

皆さんボンジュール(フランスのあいさつ)! ランダムのスペルが覚えられないプログ …