ゲーム作りが大好きな人のブログ

ゲームを作るのが大好きな人のブログ。UE4とBlender、MAYA(LT)、3DCoatを使用しています!

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

はじめに

フォリッジで植えたオブジェクトを間引きたいという事がありました。

ランドスケープグラスを使えば、間引き間隔は簡単に設定できるものの、フォリッジの場合は(自分の知っている限りでは)そうもいかず。

というわけで今回はプログラムからフォリッジを割合で削除する解説をしていきます。その後、それを使ったゲーム性のあるフォリッジ削除の話をします。

サンプルコード

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負荷が跳ね上がります。

解決するには「並列化」や「オブジェクトの検索コストの削減」をしなければいけませんが頑張ればクリアできる問題かと思います。

問題点その2。ランドスケープグラスをどうする

実はランドスケープグラスも調べていて、座標取得まではできましたが削除処理が分からずタイムーオーバーで終了しました。

ランドスケープグラスは自動生成(毎フレーム監視している?)なので、今回のようなオブジェクトの消滅とは相性が悪そうです。

しかしランドスケープグラスをFoliageに変換する事も(非常にややこしいですが)可能そうで、自動化を踏まえて対応すれば面白い事が出来そうという未来までは見えました。後は努力次第ですね。

まとめ

フォリッジのアクセス周り、いかがだったでしょうか。作業の時短に繋がる内容もあればゲーム性にも繋がる話もあり、また未来もありそうだなと感じる部分もありました。ここ最近のゲームは独自性が求められていて、こういった小ネタをゲーム開発に使えると強みになる部分も多いです。できる事をもっといっぱい増やしたいですね。

というわけで締めに入らせていただこうかと。今回の記事が貴方のゲーム開発の一助になれば幸いです。それでは。