ゲームテーマ

概要

  • プレイヤーは逃げる X を追いかける
  • マップ上にいる X をすべて集めればクリア
  • X を見つけるごとにプレイヤーの速度が上がる
    • どんどん速度が上がって捕まえやすくなる、という面白さ

つくる理由

  • AI を使ってみる
  • ABP を使ってみる

Git を設定する

https://github.com/MOZGIII/ue5-gitignore

追いかける仕組みをつくる

https://namiton.hatenablog.jp/entry/2022/07/07/130735 こちらのシリーズを参考に作っていきます。

該当の記事の異なる点や気になった点のみ記載していきます。

下準備をする (記事: 0)

疑問: どうして BP_ThirdPersonCharacter を操作できるのか

画面上にあらかじめ BP_ThirdPersonCharacter を配置しておきます。 このとき、

  • PlayerStart で動的に設定された BP_ThirdPersonCharacter のみ動かせる
  • あらかじめ配置していたものは動かない

という挙動になります。

これは

  • PlayerController はたかだか 1 つの Pawn を所有する
  • ゲーム開始時に PlayerStart に配置された Pawn を PlayerController が保持する

ためです。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/possessing-pawns-in-unreal-engine

BP_ThirdPersonCharacter など、 Pawn は AI Controller というプロパティも持ちますが、PlayerController の入力があればそれも実行されるため、1体のみ動くようです。

Blackboard と BehaviourTree の違いってなに?

BlackboardBehaviourTree の違いを調べます。 https://namiton.hatenablog.jp/entry/2022/07/07/130735 https://www.docswell.com/s/alwei/KM98L5-2022-05-12-152906#p47

Blackboard は BehaviourTree で決定をおこなうためのデータ置き場です。 プレイヤー位置など、判断に必要な情報を保持する Structure のようなものです。

url: https://historia.co.jp/archives/29055/
title: "[UE5] ビヘイビアツリー(Behavior Tree)の使い方 入門編|株式会社ヒストリア"
description: "執筆バージョン: Unreal Engine 5.0 こちらの記事は過去の記事を[UE5]向けに改定したものです。 UE4向けの記事はこちら。 今回は、UE5に実装されているビヘイビアツリー(Behavior Tree)について説明していきたいと思います。 ビヘイビアツリーとは、敵やNPCなどのAIを作る上で有効な手段の一つで、キャラクターの思考・行動をツリー構造上に配置し、行動に至るまでの思考の"
host: historia.co.jp
favicon: https://historia.co.jp/wp/wp-content/uploads/2024/08/favicon.png
image: https://historia.co.jp/wp/wp-content/uploads/2022/07/01-1.png

BehaviourTree は複数のノード(Loop などの制御ノードとタスクノード) で構成され、これらで Blackboard が利用されます。

NavMesh NavMeshVolume を設定して、AI が移動できる位置を決めます。

Imgur

Player を追いかけるようにする

Behaviour Tree に MoveTo ノードを追加します。 ここで向かう先として ChaseTargetActor へ Move させます。 BehaviourTree にまだ条件などをなにも設定していないためとにかく突き進むようになります。

Imgur

BP_NPC_Controller に

  • Blackboard Data Asset から Blackboard Component を生成する
    • Blackboard Component に Player Pawn Actor の参照を設定する
  • NPC Controller の AIController で Behaviour Tree を実行する

ことでプレイヤーを追いかけるようにします。

Imgur

NPC が背後に来たときに体にズームアップしてしまう問題を治す

Imgur

Imgur

SpringArm の Do Collision Test を false にします。

Blackboard と Behaviour Tree の関係性について見る

Imgur

Use Blackboard で Blackboard Asset (UBlackboardData*) を指定しています。 そして、 Blackboard Component が返却されて、それに Set Value As Object で Player を参照させるようにしています。 ここで気になったのが Run Behaviour Tree においてこの Blackboard を参照していないのです。 どのように参照しているのかコードを見てみました。

BlackboardData を受け取って、 それを UBlackboardComponent に設定しています。 そして、 UBlackboardComponent を AIController.Blackboard に代入しています。

そのため、

  • AI Controller が直接 BlackboardComponent をメンバ変数として所持している
  • UseBlackboard では設定済みの BlackboardComponent を返している

ことがわかります。

bool AAIController::UseBlackboard(UBlackboardData* BlackboardAsset, UBlackboardComponent*& BlackboardComponent)  
{  
    if (BlackboardAsset == nullptr)  
    {       UE_VLOG(this, LogBehaviorTree, Log, TEXT("UseBlackboard: trying to use NULL Blackboard asset. Ignoring"));  
       return false;  
    }  
    bool bSuccess = true;  
    Blackboard = FindComponentByClass<UBlackboardComponent>();  
  
    if (Blackboard == nullptr)  
    {       Blackboard = NewObject<UBlackboardComponent>(this, TEXT("BlackboardComponent"));  
       REDIRECT_OBJECT_TO_VLOG(Blackboard, this);  
       if (Blackboard != nullptr)  
       {          bSuccess = InitializeBlackboard(*Blackboard, *BlackboardAsset);  
          Blackboard->RegisterComponent();  
       }    }    else if (Blackboard->GetBlackboardAsset() == nullptr)  
    {       bSuccess = InitializeBlackboard(*Blackboard, *BlackboardAsset);  
    }    else if (Blackboard->GetBlackboardAsset() != BlackboardAsset)  
    {       // @todo this behavior should be opt-out-able.  
       UE_VLOG(this, LogBehaviorTree, Log, TEXT("UseBlackboard: requested blackboard %s while already has %s instantiated. Forcing new BB.")  
          , *GetNameSafe(BlackboardAsset), *GetNameSafe(Blackboard->GetBlackboardAsset()));  
       bSuccess = InitializeBlackboard(*Blackboard, *BlackboardAsset);  
    }  
    BlackboardComponent = Blackboard;  
  
    return bSuccess;  
}

同様に RunBehaviorTree においては UBehaviorTree を引数にとっています。 BTAssetBlackboardAsset のように参照しているように、 UBehaviourTree と Blackboard は暗黙的にアセットの段階から紐づいています。

BB_NPC_Chaser を開くと、Blackboard が直接紐づいていることがわかります。

Imgur

bool AAIController::RunBehaviorTree(UBehaviorTree* BTAsset)  
{  
    // @todo: find BrainComponent and see if it's BehaviorTreeComponent  
    // Also check if BTAsset requires BlackBoardComponent, and if so    // check if BB type is accepted by BTAsset.  
    // Spawn BehaviorTreeComponent if none present.    // Spawn BlackBoardComponent if none present, but fail if one is present but is not of compatible class  
    if (BTAsset == NULL)  
    {       UE_VLOG(this, LogBehaviorTree, Warning, TEXT("RunBehaviorTree: Unable to run NULL behavior tree"));  
       return false;  
    }  
    bool bSuccess = true;  
  
    // see if need a blackboard component at all  
    UBlackboardComponent* BlackboardComp = Blackboard;  
    if (BTAsset->BlackboardAsset && (Blackboard == nullptr || Blackboard->IsCompatibleWith(BTAsset->BlackboardAsset) == false))  
    {       bSuccess = UseBlackboard(BTAsset->BlackboardAsset, BlackboardComp);  
    }

逃げる NPC を作成する

https://www.youtube.com/watch?v=lDxuLRlmcMg こちらの動画を参考に作っていきます。

https://qiita.com/manuo/items/cc332b71aedb70ad6bfc こちらの記事もわかりやすかったです。

逃げる NPC をつくるには EQS を利用して、環境を Test してもっともよい環境を発見し、その場所についてなにかおこなうようにします。 今回の場合は

  • プレイヤーに見えない
  • かつ一番遠い

場所をスコア高く評価するようにし、そこに移動するようにします。

  • BP_NPC_EscaperCharacter
  • BP_NPC_EscaperController

を用意します。 そして、 BP_NPC_EscaperController を Character が参照するようにします。 これで BP_NPC_EscaperController で操作されるようになります。

Imgur

Environment Query Context を作成します。 EQS ジェネレーターで作成された地点をテスト(スコア&フィルタ)するときに、その計算で利用するための任意の値を設定します。

今回はプレイヤーである BP Third Person Character Actor を渡します。 Get Player Pawn でもよいのですが、後述の TestingPawn で評価が正常に行えるか確認したいため Get Actor Of Class で行っています。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/eqs-node-reference-contexts-in-unreal-engine

Imgur

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/environment-query-system-quick-start-in-unreal-engine 続いて Environment Query EQ_NPC_Escaper を作成します。

SimpleGrid ジェネレーターを利用してグリッド点を作成します。 このグリッド点を Test で評価します。

Imgur

3 つのテストを追加します。

  • Trace
    • EQC_EscaperContext でプレイヤーを渡す
    • 各アイテム(Grid Generator で生成された点) から EQC_EscaperContext に Trace を送って当たるか当たらないかでフィルタする
  • PathExist
    • EnvQueryContext_Querier コンテキストを使う
    • Querier = AI Pawn のため、AI Pawn から到達できるアイテムのみフィルタされる
  • Distance
    • EQC_EscaperContext でプレイヤーを渡す
    • プレイヤーから「近い・遠い」場所をスコア評価する
    • 今回はもっとも近い場所を高い評価にする
      • Inverse Linear

にすることでプレイヤーから見えない位置のなかでできるだけ近い場所に移動させるようにします。

Imgur Imgur Imgur

UE エディタ上で実行前にスコア値をデバッグ計算させたい場合は EQS_TestingPawn を利用します。 EQS_TestingPawn を使うと、画面上に配置したときにスコアを計算してくれます。 これで正しく EQS 設定が行えているかデバッグできます。

  • テスト用として配置した BP_ThirdPersonCharacter から見える位置は Trace(0)
  • 移動経路がない場所(画面右上)は Pathfinding(1)
  • 移動できる場所のうちできるだけ近い場所が点数が高い

ようになっていることがわかります。

Imgur

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/environment-query-testing-pawn-in-unreal-engine

BehaviourTree と Blackboard を作成します。 よく見るとビヘイビアツリーに利用する Blackboard を設定する場所がありますね。

Imgur

Run EQS Query で Query を実行します。 Query Template でクエリを設定するようにしましょう。

クエリで「目指すべき EQS Item」が定まるため、それを Blackboard の PlayerPos (Vector) に設定します。 あとは Move To で該当の PlayerPos に移動させます。

Imgur Imgur

ここまでで動作させると下記のように逃げるようになりました。 ただ、「EQS で移動場所を決める & MoveTo する」 を交互におこなうため滑らかな移動ではなく、止まって考えて動くような、滑らかでない挙動となりました。

滑らかに逃げるようにする

' と 1, 2, 3, 4 で PIE にデバッガ表示できるようにしました。 これで確認するとやはり「EQS で移動先を決める & MoveTo で移動する」を繰り返すため断続的な移動になる、という問題が発生しました。

Imgur

そのため滑らかに移動するようにします。

https://www.youtube.com/watch?v=1BsKjcEoToo めいくさんの動画を参考にさせていただきました。

Imgur

Service で指定した時間ごとに Blackboard の値を更新します。 今回は 0.4 ~ 0.6s ランダムごとに EQS をつかって PlayerPos 変数を更新します。

また、くわえて Move To で Observed Blackboard Value Tolerance にチェックボタンをつけます。 これで Blackboard の値が更新されるたびに MoveTo の目的先を変更できます。

Imgur Imgur

プレイヤーから一定距離離れたら追いかけなくなる AI に変更する

追いかける AI が、プレイヤーがどこにいても追いかけ続ける、という挙動になってしまっています。 これを一定距離離れたら諦めるようにします。

https://namiton.hatenablog.jp/entry/2022/07/12/133104

BT_NPC_Chaser で Add Service を行い、距離を計算するサービスを作成します。 変数として

  • ChaseTargetActorKey
  • ChaseTargetDistanceKey

を用意します。 これら2つの変数は両方 BlackboardKeySelector であり、 Blackboard の Key を参照する用の型のようです。

これら ChaseTargetActorKey, DistanceKey を利用して、 Blackboard の値を取得・更新します。

コードを見るとわかりますが、 Service が設定されているビヘイビアツリーが利用している Blackboard を取得し、その Blackboard へ値を設定、もしくは取得しています。

ビヘイビアツリーと Blackboard がかなり密結合になっている設計だなぁと思いますが、ツリーと Blackboard は実質セットで同じものなので問題ないのですね。

Imgur

void UBTFunctionLibrary::SetBlackboardValueAsFloat(UBTNode* NodeOwner, const FBlackboardKeySelector& Key, float Value)  
{  
    if (UBlackboardComponent* BlackboardComp = GetOwnersBlackboard(NodeOwner))  
    {       BlackboardComp->SetValue<UBlackboardKeyType_Float>(Key.SelectedKeyName, Value);  
    }
}

作成した Service をビヘイビアツリー上で Add Service で設定します。 Blackboard の Key と Service の変数の紐づけも行います。

Imgur

Imgur

Service に Debug Print を追加したところ、ちゃんとプレイヤーとの距離が設定されていることがわかりました。

Imgur

正常に距離計算が行われていることがわかったため、Behaviour Tree に条件判定を組み込みます。

プレイヤーとの距離が 1250 未満であれば Move To を実行するようにします。 Distance Service は [0.4, 0.6] s で更新されるため、 1250 以上距離を離せば MoveTo が止まります。

具体的な作業としては Decorator (条件式) を Composite Sequence ノードに設定します。 1250 より遠い場合は Abort になり Self を設定しているため子孫ノード+自身の実行を停止します。

もし 1250 以上の場合は Selector がどれか 1 つ成功するまで実行しようとするため、 Wait 1s を実行します。

これを繰り返すことで

  • プレイヤーとの距離が近ければ追いかける
  • プレイヤーとの距離が遠ければ停止して 1s 待機する

AI になりました。

Imgur

逃げる AI も距離を考慮するようにする

BTS_NPC_Chaser_DistanceBTS_NPC_Distance に rename しました。 これは Chaser と Escaper 両方で共有して利用するためです。

この Service を持っている Pawn と Target Actor Key の距離を計算し、 Target Distance Key に設定する、というだけの Service です。

特定のプレイヤーや特定の Chaser/Escaper に依存していないため、共通して利用できます。 Service は特定の要素に依存しないように汎用的な Actor などを渡すのがよさそうですね。

Imgur

Service で距離を計算し、それを Decorator で 800 未満であれば移動する、のようにします。

Imgur

UI や当たり判定処理を実装する

AI の移動ロジックさえできてしまえば UI や当たり判定処理は比較的かんたんです。 2 日間でまずは1本作ってみよう、と考えていたため UI などは非常に大雑把に作っています…。 依存がすごい適当…。

当たり判定

CapsuleComponent の Hit イベントに処理を書きます。

Imgur

引っかかった点

  • Set Input Mode Game Only
  • Set Input Mode UI Only

これらを Title 画面、そしてインゲームでちゃんと設定する必要があります。

UI 操作のみ受け付ける 状態で BP_ThirdPersonCharacter を操作しようとしても動かせません。

  • インゲームをプレイする
  • 敗北して UI が表示される
    • UI Only
  • UI 上の PlayAgain を押す
  • 再度インゲームをプレイする
    • UI Only のままだと ThirdPersonCharacter を操作できない

というのも、上記の問題が発生しました。

そのため、ちゃんと各ゲーム開始時に UI の操作モードは設定しないとだめですね。

ビルドされたアプリをウィンドウモードで立ち上げる

デフォルトのビルド&パッケージ設定だとフルスクリーンでアプリが起動します。

https://forums.unrealengine.com/t/game-start-in-fullscreen-by-default/71693/2 https://www.versluis.com/2023/09/how-to-package-an-unreal-engine-project-with-windowed-mode/

Blueprint もしくは ini で起動サイズを設定します。 今回は Blueprint でやりました。

最後に

https://www.youtube.com/watch?v=xMPdh3bkPSo&list=PLyUfKah-HyoPlQuITsVJ0m0ylj9Z5JUhF&index=1

2 日間集中して

  • Blackboard
  • BehaviourTree
  • EQS

を学べたのでよかったです。

次はもっとクオリティ高いものを開発します。