[UE5] Lyra改造計画④-1  Botスポーンのルール変更 / チームを変えるには?

Post 2023年5月6日土曜日

C++ GASでオンラインシリーズ Lyra改造 UE5 Unreal Engine オンラインゲーム

 前回の続き。

お約束
この記事作成にあたって使用した主なUnreal Engine バージョンUE 5.1.1

本日のゴール

Botスポーンのルールを変更する


C++ファイルの変更について

C++ファイルを変更して、コンパイルして使うには、ソースファイルのUE5が必要になります。

ソースファイルはEpic Game Lancherからダウンロードするわけでなく、Githubなどからダウンロードするので結構面倒です。やり方については公式に書いてあるので御覧ください。


LyraでのBot生成

Lyraでは生成されたBotでもプレイヤーでも、5秒ごとに生き返るようになっています。

これはGameplay Ability(GA)で実装されているようで、当然GAをオフにすれば、生き返りの機能はなくなるという設定が可能だったりします。これはまた後ほど・・・。

さて、LyraではBot生成が非常に分かりにくい手順で行われています。実は私も良くわかっていません(オイ)

Botの生成の設定は、

「B_ShooterGame_Elimination」で指定されている

「B_ShooterBotSpawner」というものがGameStateの一つであり、

これがBotの生成数だったり、Botの名前だったりを決めています。このGameState「B_ShooterBotSpawner」が同時にAI Controllerも指定しています。


じゃあBotに関してはこの「B_ShooterBotSpawner」だけ見ればいいのかと思われるのですが、実はそうじゃないのが我々を混乱させます。

Botやプレイヤーはチームによって、色が変わりますが、この色は「B_TeamSetup_TwoTeams」で設定しています。


また、プレイヤーもBotも、プレイのたびにMannyとQuinがランダムで選ばれるようになっていますが、この設定は「B_PickRandomCharacter」で指定されています。


つまり、プレイヤーもBotも同じファイルによってメッシュが決まっているということです。Lyraは優れた設計なのですが、ここだけは後でアレンジが難しくなるので辞めてほしかったです。(私情)


何が問題なの?

さて、上記だと何が問題なのでしょうか?

例えばチーム分けもその例です。Lyraでは、生成するBotの数は自由に決めることができますが、誰をどのチームに分けるかの設定ができません。このBotはチーム1, このBotはチーム2というような設定ができないんですね。全部自動で振られてしまいます。

Botは必ず人数が一番少ないチーム側に生成されるようになっています。プレイヤーが3人参加してチーム1に全員いたら、チーム2にBotが生成される、みたいな感じです。このため、Bot10人 vs プレイヤー5人 みたいなこともできません

また、Botのブループリントを指定することもできません。Botは「B_ShooterGame_Elimination」で指定されている「HeroData_ShooterGame」に沿って生成されます。


「HeroData_ShooterGame」では下記の通り、「B_Hero_ShooterMannequin」が指定されています。このブループリントを使って、プレイヤーもBotも生成されるという形です。


じゃあこれらの設計を回避して実装しようとなるわけですが、設計に沿わない実装をするということは、Lyraで作り上げられたシステムを使わないということになり、そうなると実装のコストがかかってしまいます。

というわけで、Lyraを改造することになります。ただ、Lyraはその基幹システムをC++で実装しているため、必然的にC++を弄ることになります…


C++が嫌な場合①、②

先に、C++が嫌な場合について記載しておきます。
C++を使うのが嫌だと言う場合は、「Get Player State」から「Lyra Player State」にCastしてSet Team IDファンクションを使うという形になると思います。

どこのブループリントにこれを置くのか?ですが「B_PickRandomCharacter」に置きました。

これは別にBotに限った話ではないので、プレイヤーに対しても使える流れです。
一例

末尾をこれに変えれば、下記の①も実現可能。


改造① Botは必ずチーム2になるようにする

チーム分けを担当しているC++ファイルは「LyraTeamCreationComponent.cpp」になります。

参照:https://forums.unrealengine.com/t/manually-setting-team-id-in-lyra/619856

これのServerChooseTeamForPlayerに関する記述を下記のように変更します。コメントアウトしている16行目はもともとあった命令です。

  1. void ULyraTeamCreationComponent::ServerChooseTeamForPlayer(ALyraPlayerState* PS)
  2. {
  3.     if (PS->IsOnlyASpectator())
  4.     {
  5.         PS->SetGenericTeamId(FGenericTeamId::NoTeam);
  6.     }
  7.     else
  8.     {
  9.         int32 SelectedTeamID;
  10.         if (PS->IsABot()) {
  11.             SelectedTeamID = 2;
  12.         }
  13.         else {
  14.             SelectedTeamID = 1;
  15.         }
  16.         //const FGenericTeamId TeamID = IntegerToGenericTeamId(GetLeastPopulatedTeamID());
  17.         const FGenericTeamId TeamID = IntegerToGenericTeamId(SelectedTeamID);
  18.         PS->SetGenericTeamId(TeamID);
  19.     }
  20. }

やっていることは、10行目でBotの場合はSelectedTeamIDを2にして、それ以外ではSelectedTeamIDを1にしています。

マジックナンバーなのは…まあ気にしない気にしない。

このSelectedTeamIDはint32型なので、FGenericTeamID型に変更しないとTeamIDにそのまま使うことができないので、IntegerToGenericTeamId()で型を変更しています。(17行目)

18行目でそのTeamIDをSetしている感じです。


改造② チームを手動で変更できるようにする

これはなぜ標準で搭載されてないのか分からない機能なんですが、LyraTeamSubsystemには予め「ChangeTeamForActor」というアクターのTeamを変更する関数が用意されています。ただなぜかブループリントに公開されていません。このため、これを公開することで利用することができます。

また、Subsystemなので、どこからでも関数にアクセスが可能です。

LyraTeamSubsystem.hを下記のように変更します。

  1. bool ChangeTeamForActor(AActor* ActorToChange, int32 NewTeamId);

↑変更前

  1. UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = Teams, meta = (Keywords = "Set"))
  2. bool ChangeTeamForActor(AActor* ActorToChange, int32 NewTeamId);

↑変更後

01行目を追加するようなイメージですね。

あとはLyra Team Subsystemから引っ張って、Change Team for Actorを引っ張ってきます。
これでいつでもTeamを変更できます。この命令はAuthority:つまりサーバーでないと実行できないので注意です。なのでこのノードを使う前にSwitch Has Authorityを繋いであげると良いです。



改造③ 指定したPlayerStartからのみスポーンするようにする

そもそも、キャラがスポーンされるとき、どのPlayerStartからスポーンするように選んでいるのでしょうか?

実は私は良くわかっていませんでした。

実際にはGameModeBaseでChoosePlayerStartという関数が実装されていて、そこのReturnに指定することでSpawn先を選ぶことができます。これはブループリントでも継承することができます。なのでChoosePlayerStart関数を継承して、どのプレイヤーかによって場合分けしてReturnに適宜PlayerStartを入れてやれば問題ないですね。

【参照】
UE の Seamless Travel を利用してみた // You are done!

あるいはレベル移動時にPlayerStartを指定することもできます。

【参照】
UE4レベル移動時に特定のPlayerStartから開始する方法 // エンジニアっぽくなりたい

Lyraではどのようにしているのかというと、LyraGameModeという独自のC++で書かれたゲームモードを使用しています。そこで当然ChoosePlayerStartも継承されています。

ただ、面倒なことにLyraGameModeではほとんど何もやっていません
ChoosePlayerStart「LyraPlayerSpawningManagerComponent」の「ChoosePlayerStart」関数に一任されています。

一方で、Spawn自体は Botは「LyraBotCreationComponent」の「ServerCreateBots」関数が実行しています。ただしここでSpawnするのはAIコントローラーだけで、アクターは「LyraControllerComponent_CharacterParts」が後付でスポーンさせています。

これが非常にややこしいです。


まあ何を言ってるのかというと、Lyraは処理があっちこっちに分散しており分かりにくいという話です。なんでこんなに分かりにくいのかというと、機能を付け加えたり変更したりを自由自在にするために、モジュール方式を採用しているからですね。LyraGameModeにあれこれ記述してしまうと、ルールを変更するときに非常に面倒になってしまいます。詳しくは前回の記事を参照してください。

※C++が嫌な場合は、LyraGameModeを継承した新しいBPを作成して、ChoosePlayerStartを継承したファンクションを作成してやれば一応大丈夫なはずです。ただ競合が発生するのでどうなるかはやってみないとわからない…。


まずやりたい事を一つ一つ分けていきましょう。


①Authorityアカウント、つまりサーバーのPlayerはPlayerStartタグが「Server」である PlayerStartからしかSpawnできないようにする

②ClientアカウントのPlayerはPlayerStartタグが「Client」からしかSpawnできないようにする

③BotはPlayerStartタグが「Bot」からしかSpawnできないようにする

①、②、③は前述の「LyraPlayerSpawningManagerComponent」の「ChoosePlayerStart」関数 を弄ることでなんとかなりそうですね。

  1. LyraPlayerSpawningManagerComponent.hについて下記のように変更
  2. class AController;
  3. class APlayerController;
  4. class APlayerState;
  5. class APlayerStart;
  6. class ALyraPlayerStart;
  7. class AActor;
  8. class AAIController; //追加

まず、AIControllerを使うので上記のようにLyraPlayerSpawningManagerComponent.hで呼び出しておきます。


  1. LyraPlayerSpawningManagerComponent.cppについて下記のようにする
  2. AActor* ULyraPlayerSpawningManagerComponent::ChoosePlayerStart(AController* Player)
  3. {
  4.     if (Player)
  5.     {
  6.         //#if WITH_EDITOR
  7.         //        if (APlayerStart* PlayerStart = FindPlayFromHereStart(Player))
  8.         //        {
  9.         //            return PlayerStart;
  10.         //        }
  11.         //#endif
  12.         TArray<ALyraPlayerStart*> StarterPoints;
  13.         TArray<ALyraPlayerStart*> ServerSpawnPoints;
  14.         TArray<ALyraPlayerStart*> ClientSpawnPoints;
  15.         TArray<ALyraPlayerStart*> BotSpawnPoints;
  16.         for (auto StartIt = CachedPlayerStarts.CreateIterator(); StartIt; ++StartIt)
  17.         {
  18.             if (ALyraPlayerStart* Start = (*StartIt).Get())
  19.             {
  20.                 StarterPoints.Add(Start);
  21.                 if ((*StartIt)->PlayerStartTag == FName(TEXT("Server"))) {
  22.                     ServerSpawnPoints.Add(Start);
  23.                 }
  24.                 else if ((*StartIt)->PlayerStartTag == FName(TEXT("Client"))) {
  25.                     ClientSpawnPoints.Add(Start);
  26.                 }
  27.                 else if ((*StartIt)->PlayerStartTag == FName(TEXT("Bot"))) {
  28.                     BotSpawnPoints.Add(Start);
  29.                 }
  30.             }
  31.             else
  32.             {
  33.                 StartIt.RemoveCurrent();
  34.             }
  35.         }
  36.         if (APlayerState* PlayerState = Player->GetPlayerState<APlayerState>())
  37.         {
  38.             // start dedicated spectators at any random starting location, but they do not claim it
  39.             if (PlayerState->IsOnlyASpectator())
  40.             {
  41.                 if (!StarterPoints.IsEmpty())
  42.                 {
  43.                     return StarterPoints[FMath::RandRange(0, StarterPoints.Num() - 1)];
  44.                 }
  45.                 return nullptr;
  46.             }
  47.         }
  48.         AActor* PlayerStart = OnChoosePlayerStart(Player, StarterPoints);
  49.         if (Player->IsLocalPlayerController() && !ServerSpawnPoints.IsEmpty()) {
  50.             PlayerStart = ServerSpawnPoints[0];
  51.         }
  52.         else if (AAIController* AICon = Cast<AAIController>(Player)) {
  53.             if (!BotSpawnPoints.IsEmpty()) {
  54.                 PlayerStart = GetFirstRandomUnoccupiedPlayerStart(Player, BotSpawnPoints);
  55.             }
  56.         }
  57.         else if (!ClientSpawnPoints.IsEmpty()) {
  58.             PlayerStart = GetFirstRandomUnoccupiedPlayerStart(Player, ClientSpawnPoints);
  59.         }
  60.         if (ALyraPlayerStart* LyraStart = Cast<ALyraPlayerStart>(PlayerStart))
  61.         {
  62.             LyraStart->TryClaim(Player);
  63.         }
  64.         return PlayerStart;
  65.     }
  66.     return nullptr;
  67. }

次にcppの方のファイルで、ChoosePlayerStartを上記のように変更します。

やっていることは、

7~12行 : Editorでの実行時は、完全ランダムにスポーンする設定になっているので、これをコメントアウトすることで無視させます。

15~17行 : LyraPlayerStartのArray変数ServerSpawnPoints, BotSpawnPoints, ClientSpawnPointsを用意しておきます。

20~46行:Worldに設置されたすべてのPlayerStartのPlayerStartTagを調べて、条件に合致していたらServer, Client, BotのそれぞれのArray変数に保存しておきます。

62~78行:サーバーの場合、つまりIsLocalControllerの場合はServerSpawnPointsの一番最初[0]のところからスポーンするように設定します。

Botの場合、つまりControllerがAIコントローラーにキャストできた場合は、BotSpawnPointsの中からランダムにスポーンするように設定します。

Clientの場合、つまり上記のいずれでもない場合は、ClientSpawnPointsの中からランダムにスポーンするように設定します。


Lyra改造④ Botの場合は特定のActorでスポーンするようにする

ちょっと長くなる予定なので次回!