SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

【UE5】敵AIをC++で作成してみよう! Part_2 (徘徊位置を指定、徘徊→追尾、移動速度の変化)

こんにちは!!
エンジニアの竹野です!
今回は任意の位置を徘徊させる処理をメインで説明します!
その他には、徘徊→追尾や徘徊の速度と追尾の速度を変えたりなどもやっていくので、
前回のPart_1からの流れで見ていただければわかりやすいかと、


※今回からUE5.3.2にバージョンを変更したのでPart1のUE4とは違う点があるかもしれないので、把握お願いいたします!

作業環境

windows 10
visual studio 2022
・UnrealEngine 5.3.2

プロジェクト設定

・名前     AITest
・テンプレート サードパーソン
・BPかC++   C++


任意の徘徊処理

まず初めに徘徊処理について説明します。
今回やりたい徘徊処理はEditor上で任意の位置に徘徊ポイントを設定できるものを作成したいため、
まず徘徊ポイントを指定できるアクターを作っていきましょう!


RoamingPoint.h

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "RoamingPoint.generated.h"

UCLASS()
class AITEST_API ARoamingPoint : public AActor
{
	GENERATED_BODY()
	
public:	
	ARoamingPoint();

	// 徘徊位置を要素番号で取得
	FVector GetRoamingPoint(int const index)const { return RoamingPoints[index]; }
	// 徘徊配列の要素数を取得
	int PointNum()const { return RoamingPoints.Num(); }
private:
	// 徘徊位置を任意で設定できる配列を準備
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = "AI",meta = (MakeEditWidget="true",AllowPrivateAccess = "true"))
	TArray<FVector>RoamingPoints;
};

RoamingPoint.cpp

#include "../AI/RoamingPoint.h"

ARoamingPoint::ARoamingPoint()
{
	PrimaryActorTick.bCanEverTick = false;
}

このクラスは徘徊位置を決める変数、ゲッターなどを準備するアクタークラスです。
特に処理は書かずに準備するだけなので、次にEditor側に行きアクタークラスのBPを作成してください。
作成したアクターBPの親クラスを先ほど作成したRoamingPointを指定します。


設定できたら作成したBPをレベルに配置してください。
配置したBPクラスをクリックして詳細を見ていただくと、AIRoamingPointsという配列があります。
こちらの要素を増やして任意の位置に置いていきましょう!

自分の場合は5点のポイントを設定しました!レベル上にわかりやすく表示されるので見やすいですね。


次に徘徊に必要なブラックボードキーを追加します。
保管したい情報として徘徊位置徘徊配列の要素番号があるので、
Vector型とInt型のキーを追加してください。(自分はPointIndex(Int型)、PointLocation(Vector型)を追加しました)



次にビヘイビアツリーで実行するBehaviorTreeTask(タスクと呼ばれるもの)を作成していきます。
タスクは簡単に言えば、ビヘイビアツリーで実行する内容です。(MoveToノードは追いかけるという内容のタスクです)
こちらを作成するので、C++クラスの作成から全てのクラスを選択、検索にてBTTask_BlackboardBaseというものを選択し作成します。

処理を書いていきます。

BTT_FindRoamingPoint.h

#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BTT_FindRoamingPoint.generated.h"

UCLASS()
class AITEST_API UBTT_FindRoamingPoint : public UBTTask_BlackboardBase
{
	GENERATED_BODY()
	
public:
	explicit UBTT_FindRoamingPoint(FObjectInitializer const& ObjectInitializer);
	// タスク実行内容
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)override;

private:
	// ブラックボードに保管する用のキー情報
	UPROPERTY(EditAnywhere,Category="Blackboard", meta=(AllowPrivateAccess="true"))
	FBlackboardKeySelector RoamingPointVectorKey;
};

BTT_FindRoamingPoint.cpp

#include "../AI/BTT_FindRoamingPoint.h"

// AIControllerクラス
#include "../AI/AIC_Enemy.h"
// 敵キャラクラス
#include "../AI/Enemy.h"

UBTT_FindRoamingPoint::UBTT_FindRoamingPoint(FObjectInitializer const& ObjectInitializer) :
	UBTTask_BlackboardBase{ ObjectInitializer }
{
	// BehaviorTreeのタスク欄に表示する名前
	NodeName = TEXT("Find Roaming Point");
}

EBTNodeResult::Type UBTT_FindRoamingPoint::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	if (AAIC_Enemy* AIC = Cast<AAIC_Enemy>(OwnerComp.GetAIOwner()))
	{
		if (auto* BB_Component = OwnerComp.GetBlackboardComponent())
		{
			// 現在の徘徊要素番号
			int32 index = BB_Component->GetValueAsInt(GetSelectedBlackboardKey());

			if (AEnemyCharacter* Enemy = Cast<AEnemyCharacter>(AIC->GetPawn()))
			{
				// 要素番号から相対位置を取得
				auto const roamingPoint = Enemy->GetRoamingPointActor()->GetRoamingPoint(index);
				// 相対位置からワールド位置へ変換
				auto const targetPoint = Enemy->GetRoamingPointActor()->GetActorTransform().TransformPosition(roamingPoint);
				// ブラックボードの徘徊位置キーに座標を設定
				BB_Component->SetValueAsVector(RoamingPointVectorKey.SelectedKeyName, targetPoint);

				FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
				return EBTNodeResult::Succeeded;
			}
		}
	}
	
	return EBTNodeResult::Failed;
}

こちらの処理はブラックボードに保管してある現在の要素番号キー(PointIndex)から、次に向かう位置を取ってきて
その位置をブラックボードに保管している、次に向かう位置キー(PointLocation)に設定しています。



次に別タスクを作成していきます。
実装するタスク内容は、ブラックボードキーに用意してある、徘徊配列の要素番号の更新を行います。
こちらの処理で徘徊位置が次にどこになるか設定しています。

BTT_FindRoamingPointIndex.h

#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BTT_RoamingPointIndex.generated.h"

UCLASS()
class AITEST_API UBTT_RoamingPointIndex : public UBTTask_BlackboardBase
{
	GENERATED_BODY()
	
public:
	explicit UBTT_RoamingPointIndex(FObjectInitializer const& ObjectInitializer);
	// タスク実行内容
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)override;

private:
	// 次のポイントへの方向を決めます。
	// Forwardは配列要素順に、Reverseは配列要素の逆順です
	enum class EDirectionType{Forward,Reverse};

	// 初期化は順方向に
	EDirectionType Direction = EDirectionType::Forward;

	// 順方向、逆方向を決めるbool値
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="AI",meta=(AllowPrivateAccess="true"))
	bool bDirectional = false;
};

BTT_FindRoamingPointIndex.cpp

#include "../AI/BTT_RoamingPointIndex.h"

// AIControllerクラス
#include "../AI/AIC_Enemy.h"
// 敵キャラクラス
#include "../AI/Enemy.h"

UBTT_RoamingPointIndex::UBTT_RoamingPointIndex(FObjectInitializer const& ObjectInitializer) :
	UBTTask_BlackboardBase{ObjectInitializer}
{
	// BehaviorTreeのタスク欄に表示する名前
	NodeName = TEXT("Roaming Point Index");
}

EBTNodeResult::Type UBTT_RoamingPointIndex::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	if (AAIC_Enemy* AIC = Cast<AAIC_Enemy>(OwnerComp.GetAIOwner()))
	{
		if (AEnemyCharacter* enemy = Cast<AEnemyCharacter>(AIC->GetPawn()))
		{
			if (auto* const BB_Component = OwnerComp.GetBlackboardComponent())
			{
				// 徘徊要素の全ての数を取得
				int32 const AllPoints = enemy->GetRoamingPointActor()->PointNum();
				// 要素数の最低値
				int32 const MinIndex = 0;
				// 要素数の最高値
				int32 const MaxIndex = AllPoints - 1;
				// 現在の要素番号
				int32 Index = BB_Component->GetValueAsInt(GetSelectedBlackboardKey());

				// 逆方向がtrueの場合
				if (bDirectional)
				{
					// 現在の要素番号が最大値 && 順方向に進んでいたら 
					if (Index >= MaxIndex && Direction == EDirectionType::Forward)
					{
						// 逆方向へ
						Direction = EDirectionType::Reverse;
					}
					// 現在の要素番号が最小値 && 逆方向に進んでいたら
					else if (Index == MinIndex && Direction == EDirectionType::Reverse)
					{
						// 順方向へ
						Direction = EDirectionType::Forward;
					}
				}

				// 順方向の場合は要素順にポイント番号を設定 (0→1→2)
				// 逆方向の場合は要素番号を逆にポイント番号を設定 (2→1→0)
				BB_Component->SetValueAsInt(GetSelectedBlackboardKey(),
					(Direction == EDirectionType::Forward ? ++Index : --Index) % AllPoints);

				FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
				return EBTNodeResult::Succeeded;
			}
		}
	}

	return EBTNodeResult::Failed;
}

こちらの処理で行っているのは、Editorで設定した徘徊位置配列の要素数をみて、
順方向に要素数足したものをブラックボードで設定した、現在の要素番号キー(PointIndex)に設定しています。
なおbDirectionalというBool値を追加したので、そちらをtrueに設定すると順方向に進んでいき、
最後のポイントにたどり着いたら、今度は逆方向に進む(来た道を戻っていく)ことができます。




これであとはビヘイビアツリーにて以下のようなノード構成にして、それぞれのノードへキーを設定すれば完成です!

1個づつ軽く説明していくと、
1.FindRoamingPointで徘徊位置をキーに保管
2.MoveToで保管した徘徊位置に向かわせる
3.RoamingPointIndexは徘徊位置にたどり着いた後に次の徘徊位置を更新しないといけないので、現在の要素番号キーを更新
4.Waitノードは徘徊位置についた時に待ちの動きがあるとそれっぽくなる
という感じです!
結構単純な処理で設定しやすい徘徊処理ができました。



次に追加処理を書いていきます!
内容は冒頭でも説明した、徘徊中に発見→追尾徘徊と追尾の移動速度の変化を追加します。
※前回(Part1)の視界判定や追跡する処理が必要です。そちらの処理がある前提で進めていきます。
↓前回
【UE4】敵AIをC++で作成してみよう! Part_1 - SPARKCREATIVE Tech Blog


徘徊から追跡処理

まず初めに、発見したかどうかの情報をブラックボードに保管します。
bFindPlayerというbool型のキーを作成しました。



そしたらコード側へ行き前回作成した、
AIControllerクラスの発見時にプレイヤー情報をブラックボードキーに保管を行っている、
SetPlayerKeyという関数内で以下の処理を書きます。

AIC_Enemy.cpp

void AAIC_Enemy::SetPlayerKey(APawn* player)
{
	ensure(BlackboardComp);
  
	// 追加--------------------------------------------------------------------
	// 発見時にブラックボードに作成したフラグをたてる
	FName SeeKeyName = "bFindPlayer";
	BlackboardComp->SetValueAsBool(SeeKeyName, true);
	//-------------------------------------------------------------------------
	
	// ブラックボードで作成したPlayerというキーにプレイヤー情報を入れる
	BlackboardComp->SetValueAsObject(PlayerKeyName, player);
}

発見時に先ほど作成したブラックボードキーをたてておきます。


次にビヘイビアツリーを開き以下の型にしてください。

左側のMoveToノードにはPlayer情報を入れてください。(前回の処理)



そしたら追跡側、徘徊側両方のSequenceノードを右クリック、デコレーターを追加で一番上のBlackboardを選択してください。



次に追跡側で追加したデコレーターをクリックし以下画像の設定をしてください。

次に徘徊側で追加したデコレーターをクリックし以下画像の設定をしてください。

デコレーターを追加したことによって、以下のノードの処理を通すか通さないか設定できるようになります!
先ほど作成した発見時にフラグを立てるブラックボードキーで制御していて、フラグがたったら追跡ノードに行き、
フラグが立たない限り徘徊ノードを回り続けるという処理ができました!
こちらで、最初は徘徊を行い、途中でプレイヤーを発見した際に追跡する敵キャラができました。


徘徊、追跡の移動速度処理

次に徘徊時と追跡時で同じ速度だと少し違和感があるので、
徘徊時は歩き、追跡時は走って追いかけてくるということをやろうと思います。


そこで、サービスというものを追加して速度の変化をつけていきます。
C++クラスの追加→検索でBTSと記入するとBTService_BlackboardBaseが出てくるので、
そちらを選択しクラスを作成してください。



ではコードを書いていきます。
BTS_ChangeSpeed.h

#include "CoreMinimal.h"
#include "BehaviorTree/Services/BTService_BlueprintBase.h"
#include "BTS_ChangeSpeed.generated.h"

UCLASS()
class AITEST_API UBTS_ChangeSpeed : public UBTService_BlueprintBase
{
	GENERATED_BODY()
	
public:
	UBTS_ChangeSpeed();
	virtual void OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)override;
private:
	// プレイヤー速度
	// 走り時の速度の初期値は600
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="AI",meta=(AllowPrivateAccess="yes"))
	float Speed = 600.f;
};

BTS_ChangeSpeed.cpp

#include "../AI/BTS_ChangeSpeed.h"

// AIControllerクラス
#include "../AI/AIC_Enemy.h"
// 敵キャラクラス
#include "../AI/Enemy.h"
// キャラの動きに関するクラス
#include "GameFramework/CharacterMovementComponent.h"

UBTS_ChangeSpeed::UBTS_ChangeSpeed()
{
	bNotifyBecomeRelevant = true;
	// BehaviorTreeのサービス欄に表示する名前
	NodeName = TEXT("Change Speed");
}

void UBTS_ChangeSpeed::OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	Super::OnBecomeRelevant(OwnerComp, NodeMemory);
	if (AAIC_Enemy* AIC = Cast<AAIC_Enemy>(OwnerComp.GetAIOwner()))
	{
		if (AEnemyCharacter* Enemy = Cast<AEnemyCharacter>(AIC->GetPawn()))
		{
			// Editorのサービス詳細で設定された速度を、敵キャラに設定
			Enemy->GetCharacterMovement()->MaxWalkSpeed = Speed;
		}
	}
}

キャラのCharacterMovementで速度を設定できるので、
BehaviorTree内のサービス詳細にて、速度を設定できるような仕組みにしました。



EditorのBehaviorTreeにいっていただき、追跡、徘徊のSequenceノードを右クリック、
サービスを追加があるのでそちらから作成したChangeSpeedを選択してください。



サービスをクリックしたら、AI/Speedという作成した変数があるので、
追跡側のサービスはいじらずに、徘徊側のSpeedを300に設定します。



これで徘徊時は歩き、追跡時は走って追いかけてくるという処理ができました!


最後に

これで任意の位置徘徊、徘徊からの追跡、速度の変化を実装できます!
お疲れ様でした!