ゲームテーマ
概要
- プレイヤーは逃げる X を追いかける
- マップ上にいる X をすべて集めればクリア
- X を見つけるごとにプレイヤーの速度が上がる
- どんどん速度が上がって捕まえやすくなる、という面白さ
つくる理由
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 の違いってなに?
Blackboard と BehaviourTree の違いを調べます。 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 を設定する
NavMesh NavMeshVolume を設定して、AI が移動できる位置を決めます。
Player を追いかけるようにする
Behaviour Tree に MoveTo
ノードを追加します。
ここで向かう先として ChaseTargetActor
へ Move させます。
BehaviourTree にまだ条件などをなにも設定していないためとにかく突き進むようになります。
BP_NPC_Controller に
- Blackboard Data Asset から Blackboard Component を生成する
- Blackboard Component に Player Pawn Actor の参照を設定する
- NPC Controller の AIController で Behaviour Tree を実行する
ことでプレイヤーを追いかけるようにします。
NPC が背後に来たときに体にズームアップしてしまう問題を治す
SpringArm の Do Collision Test
を false にします。
Blackboard と Behaviour Tree の関係性について見る
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 を引数にとっています。 BTAsset→BlackboardAsset のように参照しているように、 UBehaviourTree と Blackboard は暗黙的にアセットの段階から紐づいています。
BB_NPC_Chaser を開くと、Blackboard が直接紐づいていることがわかります。
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 で操作されるようになります。
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/environment-query-system-quick-start-in-unreal-engine
続いて Environment Query EQ_NPC_Escaper
を作成します。
SimpleGrid ジェネレーターを利用してグリッド点を作成します。
このグリッド点を Test
で評価します。
3 つのテストを追加します。
- Trace
- EQC_EscaperContext でプレイヤーを渡す
- 各アイテム(Grid Generator で生成された点) から EQC_EscaperContext に Trace を送って当たるか当たらないかでフィルタする
- PathExist
- EnvQueryContext_Querier コンテキストを使う
- Querier = AI Pawn のため、AI Pawn から到達できるアイテムのみフィルタされる
- Distance
- EQC_EscaperContext でプレイヤーを渡す
- プレイヤーから「近い・遠い」場所をスコア評価する
- 今回はもっとも近い場所を高い評価にする
- Inverse Linear
にすることでプレイヤーから見えない位置のなかでできるだけ近い場所に移動させるようにします。
UE エディタ上で実行前にスコア値をデバッグ計算させたい場合は EQS_TestingPawn を利用します。 EQS_TestingPawn を使うと、画面上に配置したときにスコアを計算してくれます。 これで正しく EQS 設定が行えているかデバッグできます。
- テスト用として配置した BP_ThirdPersonCharacter から見える位置は Trace(0)
- 移動経路がない場所(画面右上)は Pathfinding(1)
- 移動できる場所のうちできるだけ近い場所が点数が高い
ようになっていることがわかります。
BehaviourTree と Blackboard を作成します。 よく見るとビヘイビアツリーに利用する Blackboard を設定する場所がありますね。
Run EQS Query で Query を実行します。 Query Template でクエリを設定するようにしましょう。
クエリで「目指すべき EQS Item」が定まるため、それを Blackboard の PlayerPos (Vector)
に設定します。
あとは Move To で該当の PlayerPos に移動させます。
ここまでで動作させると下記のように逃げるようになりました。 ただ、「EQS で移動場所を決める & MoveTo する」 を交互におこなうため滑らかな移動ではなく、止まって考えて動くような、滑らかでない挙動となりました。
滑らかに逃げるようにする
'
と 1, 2, 3, 4 で PIE にデバッガ表示できるようにしました。
これで確認するとやはり「EQS で移動先を決める & MoveTo で移動する」を繰り返すため断続的な移動になる、という問題が発生しました。
そのため滑らかに移動するようにします。
https://www.youtube.com/watch?v=1BsKjcEoToo めいくさんの動画を参考にさせていただきました。
Service で指定した時間ごとに Blackboard の値を更新します。 今回は 0.4 ~ 0.6s ランダムごとに EQS をつかって PlayerPos 変数を更新します。
また、くわえて Move To で Observed Blackboard Value Tolerance
にチェックボタンをつけます。
これで Blackboard の値が更新されるたびに MoveTo の目的先を変更できます。
プレイヤーから一定距離離れたら追いかけなくなる 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 は実質セットで同じものなので問題ないのですね。
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 の変数の紐づけも行います。
Service に Debug Print を追加したところ、ちゃんとプレイヤーとの距離が設定されていることがわかりました。
正常に距離計算が行われていることがわかったため、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 になりました。
逃げる AI も距離を考慮するようにする
BTS_NPC_Chaser_Distance
を BTS_NPC_Distance
に rename しました。
これは Chaser と Escaper 両方で共有して利用するためです。
この Service を持っている Pawn と Target Actor Key
の距離を計算し、 Target Distance Key
に設定する、というだけの Service です。
特定のプレイヤーや特定の Chaser/Escaper に依存していないため、共通して利用できます。 Service は特定の要素に依存しないように汎用的な Actor などを渡すのがよさそうですね。
Service で距離を計算し、それを Decorator で 800 未満であれば移動する、のようにします。
UI や当たり判定処理を実装する
AI の移動ロジックさえできてしまえば UI や当たり判定処理は比較的かんたんです。 2 日間でまずは1本作ってみよう、と考えていたため UI などは非常に大雑把に作っています…。 依存がすごい適当…。
当たり判定
CapsuleComponent の Hit イベントに処理を書きます。
引っかかった点
- 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
を学べたのでよかったです。
次はもっとクオリティ高いものを開発します。