【UE5】【C++】フォリッジで遊ぶ

ざっくり概要
- フォリッジで植えたオブジェクトをプログラムからざっくり削りたいという話
 - フォリッジをゲーム性に組み込む話
 - UE5.4で検証
 - この記事は「Unreal Engine (UE) Advent Calendar 2024 その4」15日目の記事となります
 
目次です
はじめに
フォリッジで植えたオブジェクトを間引きたいという事がありました。
ランドスケープグラスを使えば、間引き間隔は簡単に設定できるものの、フォリッジの場合は(自分の知っている限りでは)そうもいかず。
というわけで今回はプログラムからフォリッジを割合で削除する解説をしていきます。その後、それを使ったゲーム性のあるフォリッジ削除の話をします。
サンプルコード
includeとモジュールの追加は今回載せておりません。
(おそらくモジュールの追加は"Foliage"のみです)
FAutoConsoleCommandWithWorldAndArgs FoliageRemoveTest( TEXT("dev.FoliageRemoveTest") , TEXT("") , FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& Args, UWorld* World) { if (Args.Num() < 2) { return; } GEditor->BeginTransaction(NSLOCTEXT("MyTest", "RemoveFoliageTest", "Remove Foliage Test")); FString FoliageAssetName = Args[0]; double Percent = FCString::Atod(*Args[1]); for (TActorIterator<AInstancedFoliageActor> It(World); It; ++It) { AInstancedFoliageActor* FoliageActor = *It; // フォリッジコンポーネント TInlineComponentArray<UFoliageInstancedStaticMeshComponent*> ComponentArray; FoliageActor->GetComponents(ComponentArray); // フォリッジコンポーネントに入っている物を割合で削除する for (UFoliageInstancedStaticMeshComponent* Component : ComponentArray) { // 引数と同一のアセットでない場合は終了 if (FPaths::GetBaseFilename(Component->GetStaticMesh()->GetPackage()->GetPathName()) != FoliageAssetName) { continue; } TArray<int32> RemoveIndexArray; FTransform TempTransform = FTransform::Identity; for (int32 i = 0; i < Component->GetInstanceCount(); ++i) { RemoveIndexArray.Add(i); } int32 LoopNum = RemoveIndexArray.Num() * Percent; for (int32 i = 0; i < LoopNum; ++i) { int32 _index = FMath::Rand() % RemoveIndexArray.Num(); RemoveIndexArray.RemoveAt(_index); } Component->RemoveInstances(RemoveIndexArray); } // フォリッジタイプで保持している物を割合で削除する FoliageActor->ForEachFoliageInfo([FoliageAssetName, Percent](UFoliageType* FoliageType, FFoliageInfo& FoliageInfo) { if (UFoliageType_InstancedStaticMesh* IsmFoliage = Cast<UFoliageType_InstancedStaticMesh>(FoliageType)) { if (FPaths::GetBaseFilename(IsmFoliage->GetStaticMesh()->GetPackage()->GetPathName()) != FoliageAssetName) { return true; } TArray<int32> RemoveIndexArray; // for (int32 i = 0; i < FoliageInfo.Instances.Num(); ++i) { RemoveIndexArray.Add(i); } int32 LoopNum = RemoveIndexArray.Num() * Percent; for (int32 i = 0; i < LoopNum; ++i) { int32 _index = FMath::Rand() % RemoveIndexArray.Num(); RemoveIndexArray.RemoveAt(_index); } FoliageInfo.RemoveInstances(RemoveIndexArray, true); } return true; }); } GEditor->EndTransaction(); }) );
使い方
コンソールコマンドで実行します
dev.FoliageRemoveTest [StaticMeshの名前] [表示させたい割合(0.0 ~ 1.0)]
実行すると現在表示しているレベルのすべてのFoliageに対して第1引数で指定したStaticMesh名だけを第二引数の割合まで削減します。結果はこんな感じ。

上の状態はdev.FoliageRemoveTest SM_Grass 0.2みたいな感じで入力しています。
これで20%まで削減しました。削除するオブジェクトは無作為に選んでいますが、まばらに削除されていますね。
このコマンドはアンドゥにも対応しているので気に入らなければアンドゥして実行しなおすのがいいでしょう。
浪漫を求めろ!
フォリッジであれば簡単に削除することができました。しかし、ここまでやって思う事が一つあり……草が刈りたいんですよ、ボクはぁ。
剣で草を刈れたり、精霊魔法的な技で連鎖反応を起こさせたり出来るゲームがありますよね。それをUEでやりたいのです。この手の手法は世界観を伝える意味でも重要だったりします。
草を刈るために必要な仕様は何か?
端的に仕様だけ書くと「キャラクターが出す攻撃判定内にフォリッジのオブジェクトがあれば消す」です。
つまり、フォリッジ毎の座標が分かれば我ら(?)の勝ちです。そして、そのやり方は下の内容になります。
UFoliageInstancedStaticMeshComponentのオブジェクト単位の座標を得る
ComponentはUFoliageInstancedStaticMeshComponentです
FTransform Transform = FTransform::Identity; for (int32 i = 0; i < Component->GetInstanceCount(); ++i) { Component->GetInstanceTransform(i, Transform); // 後は座標を使ってhogehoge }
FFoliageInfoのオブジェクト単位の座標を得る
FoliageActorはAInstancedFoliageActorです
FoliageActor->ForEachFoliageInfo([](UFoliageType* FoliageType, FFoliageInfo& FoliageInfo) { if (UFoliageType_InstancedStaticMesh* IsmFoliage = Cast<UFoliageType_InstancedStaticMesh>(FoliageType)) { for (int32 i = 0; i < FoliageInfo.Instances.Num(); ++i) { FTransform Transform = FoliageInfo.Instances[i].GetInstanceWorldTransform(); // 後は座標を使ってhogehoge } } return true; });
浪漫部分の成果発表
youtu.be
今回の解説では攻撃判定とフォリッジオブジェクトの衝突判定回りの解説は省きますが、結果だけ見るとキャラクターの回転攻撃で周囲の草が刈り取られるというのが実現できていますね。
しかし同時に問題点も見えてきました。
問題点その1。重たい
このテストレベルのフォリッジは4万程度のオブジェクト数があります。これを攻撃判定のたびに実行するとCPU負荷が跳ね上がります。
解決するには「並列化」や「オブジェクトの検索コストの削減」をしなければいけませんが頑張ればクリアできる問題かと思います。
まとめ
フォリッジのアクセス周り、いかがだったでしょうか。作業の時短に繋がる内容もあればゲーム性にも繋がる話もあり、また未来もありそうだなと感じる部分もありました。ここ最近のゲームは独自性が求められていて、こういった小ネタをゲーム開発に使えると強みになる部分も多いです。できる事をもっといっぱい増やしたいですね。
というわけで締めに入らせていただこうかと。今回の記事が貴方のゲーム開発の一助になれば幸いです。それでは。