SPARKCREATIVE Tech Blog

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

【UE4】敵AIをC++で作成してみよう! Part_1

こんにちは!!
エンジニアの竹野です!
自分が趣味で作っている敵AIを、Partごとに分けて説明していこうと思います。
今回はUE4のAI機能、BehaviorTree、Blackboard、AIControllerの基礎的な使い方をC++で実装し、
敵の追尾を作成していきます!

作業環境

windows 10
visual studio 2019
・UnrealEngine 4.27.2

プロジェクト設定

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

準備

まずエディターを立ち上げたらコンテンツに、AIフォルダを作成しその中に敵キャラ(BP)を入れておきましょう。

次に作成したAIフォルダで右クリックし、ビヘイビアツリー、ブラックボードを追加します。
自分はBT_EnemyBB_Enemyと名付けました。

ビヘイビアツリーというのは、AIの行動の選択を指定するために使用される機能です。(行動遷移)
ブラックボードというのは、AIの頭脳と呼ばれておりAIに必要な情報を格納している機能という感じです。(キーという変数的な感じで情報を持てます)



作成したブラックボードを開き、新規キー → Objectを選択。

作成したキーの名前をPlayerにして、Base ClassをActorにしましょう。
これでPlayerの情報を入れるためのキーを前もって作成しておきます。


次にAIキャラクター、AIControllerをC++で実装していきます!
BihaviorTree、Blackboard、AIControllerは3点セットという感じの解釈で大丈夫です。

コンテンツのC++クラスの中で右クリックを押し、新規C++クラス…を選択します。
親クラスはCharacterを選択します。ファイル名は自分はEnemyCharacterにしました。
そしてクラス作成を押し、VisualStudioにいくと敵のキャラクタークラスが作成されています。




次にもう一回エディターを立ち上げて、AIControllerクラスを作成します。
同じようにC++クラスの中で右クリックを押し、新規C++クラス…を選択します。
次に親クラスの選択画面の右上の、すべてのクラスを表示にチェックを入れます。

検索でAIControllerと検索を入れましょう。
そしてAIControllerという基底クラスが出てくるので選択してください。
ファイル名はAIC_Enemyにしました。

C++コード

次にコードを書いていきます!
最初にプロジェクト名.Build.csを開き、
PublicDependencyModuleNames.AddRange(new string[] {});に"AIModule"を追加しましょう。
これをしないと後に出てくるUBehaviorTreeComponentなどの呼び出しで参照エラーが出てしまいます。
AITest.Build.cs

using UnrealBuildTool;

public class AITest : ModuleRules
{
	public AITest(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] { 
			"Core", 
			"CoreUObject", 
			"Engine", 
			"InputCore", 
			"HeadMountedDisplay",
			"AIModule", // 追加
		});
	}
}

ここからはエネミーの設定です!

EnemyCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "EnemyCharacter.generated.h"

UCLASS()
class AITEST_API AEnemyCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	AEnemyCharacter();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

public:
	UPROPERTY(VisibleAnywhere, Category = "AI")
		class UPawnSensingComponent* PawnSensingComp;

	UFUNCTION()
		void OnSeePlayer(APawn* Pawn);

};

EnemyCharacter.cpp

#include "EnemyCharacter.h"
#include "Perception/PawnSensingComponent.h"
#include "Kismet/KismetSystemLibrary.h"

#include "AITestCharacter.h"
#include "AIC_Enemy.h"

AEnemyCharacter::AEnemyCharacter()
{
	PrimaryActorTick.bCanEverTick = true;

	PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSensingComp"));

	// 視野
	PawnSensingComp->SetPeripheralVisionAngle(60.f);
	// 見える範囲
	PawnSensingComp->SightRadius = 2000;
	PawnSensingComp->OnSeePawn.AddDynamic(this, &AEnemyCharacter::OnSeePlayer);
}

void AEnemyCharacter::BeginPlay()
{
	Super::BeginPlay();

}

void AEnemyCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void AEnemyCharacter::OnSeePlayer(APawn* Pawn)
{
	AAIC_Enemy* AIController = Cast<AAIC_Enemy>(GetController());
	// プレイヤー
	AAITestCharacter* player = Cast<AAITestCharacter>(Pawn);

	if (AIController && player)
	{
		// AIControllerにプレイヤー情報を設定
		AIController->SetPlayerKey(player);
	}

	// 視野に入ったら画面に"See"と表示
	UKismetSystemLibrary::PrintString(this, "See", true, true, FColor::Blue, 2.f);
}

EnemyCharacterでは、UPawnSensingComponentによる敵自身の視界範囲を決め、
視界に入ったプレイヤー情報(Pawn)をAIControllerに渡しています。

AIC_Enemy.h

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AITestCharacter.h"
#include "AIC_Enemy.generated.h"

UCLASS()
class AITEST_API AAIC_Enemy : public AAIController
{
	GENERATED_BODY()
	
public:
	AAIC_Enemy(const class FObjectInitializer& ObjectInitializer);

public:
	void SetPlayerKey(APawn* player);

	UFUNCTION()
		AAITestCharacter* GetPlayerKey();

	UPROPERTY()
		UBehaviorTreeComponent* BehaviorComp;

	UPROPERTY()
		UBlackboardComponent* BlackboardComp;

	UPROPERTY(EditDefaultsOnly, Category = AI)
		FName PlayerKeyName;

protected:
	// AIControllerのPawn所持
	virtual void OnPossess(class APawn* InPawn) override;

	// AIControllerのPawn所持解除
	virtual void OnUnPossess() override;

	virtual void BeginPlay() override;

	UPROPERTY(EditDefaultsOnly, Category = AI)
		class UBehaviorTree* BehaviorTree;

	FORCEINLINE UBehaviorTreeComponent* GetBehaviorComp() const { return BehaviorComp; }
	FORCEINLINE UBlackboardComponent* GetBlackboardComp() const { return BlackboardComp; }
};

AIC_Enemy.cpp

#include "AIC_Enemy.h"
#include "UObject/ConstructorHelpers.h"

AAIC_Enemy::AAIC_Enemy(const class FObjectInitializer& ObjectInitializer)
{
	BehaviorComp = ObjectInitializer.CreateDefaultSubobject<UBehaviorTreeComponent>(this, TEXT("BehaviorComp"));
	BlackboardComp = ObjectInitializer.CreateDefaultSubobject<UBlackboardComponent>(this, TEXT("BlackboardComp"));

	// 作成したビヘイビアツリーを設定
	ConstructorHelpers::FObjectFinder<UBehaviorTree> BTFinder(TEXT("/Game/AI/BT_Enemy.BT_Enemy"));
	BehaviorTree = BTFinder.Object;

	PlayerKeyName = "Player";
}

void AAIC_Enemy::BeginPlay()
{
	Super::BeginPlay();
}

void AAIC_Enemy::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	// AIControllerがPawn所持した際にBBとBTを使用
	BlackboardComp->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
	BehaviorComp->StartTree(*BehaviorTree);
}

void AAIC_Enemy::OnUnPossess()
{
	Super::OnUnPossess();
	BehaviorComp->StopTree();
}

void AAIC_Enemy::SetPlayerKey(APawn* player)
{
	ensure(BlackboardComp);

	// ブラックボードで作成したPlayerというキーにプレイヤー情報を入れる
	BlackboardComp->SetValueAsObject(PlayerKeyName, player);
}

AAITestCharacter* AAIC_Enemy::GetPlayerKey()
{
	ensure(BlackboardComp);

	return Cast<AAITestCharacter>(BlackboardComp->GetValueAsObject(PlayerKeyName));
}

AIC_Enemyではブラックボード、ビヘイビアツリーの使用だったりブラックボードで作成したキーに情報を入れています。

BP、ビヘイビアツリー設定

実行してエディターを開きましょう!
開いたらAIエネミーのBPを開いてください。

開いたらクラス設定 → 親クラスを作成したC++クラス、EnemyCharacterを選択しましょう。

クラスのデフォルト → ポーン → AI Controller Classを作成したC++クラスAIC_Enemyを選択します。
これでC++で設定したものがエネミーに適応されます。

次にレベルに先ほどC++クラスを適応したBP_Enemyをレベルに配置します。
するとエネミーを中心に視界判定がデバック機能で見えます。
実際にプレイして視界判定に入ったら、PrintStringで左上にSeeと出たらうまく動いています!


ここまでうまく動いていたら後はBT_Enemyを開いてビヘイビアツリーに設定をしましょう。
ルートから引っ張りSelectorを選択、SelectorからMove To を選択しましょう。
そしてMove Toの設定でブラックボード → Blackboard Keyという所に自分が作成し、
C++側でプレイヤー情報を入れたPlayerというキーを選択しましょう!

Move Toというタスクはキーの情報の位置に向かう機能です。


最後に左上のアクタを配置の検索でナビメッシュと調べたらナビメッシュバウンズボリュームが出てくるので、
それをレベルに置いてあげて拡大してあげましょう。
これはMove Toで追尾する際の範囲だと覚えていれば大丈夫です。
(例えばエネミーが入ってはいけないエリアでナビメッシュを置かなければ追尾をやめてくれます)



これで敵の視界に入ると追尾してくるようになりました!

最後に

今回は敵の追尾という基礎的なものの紹介でした!
これからPartごとに分けて敵AIに関するものを紹介していこうと考えているのでご期待ください。
ではまた会いましょう!