게임 엔진/Unreal

[Unreal] 인공지능 에이전트 개발 방법 (AIController, BehaviorTree, Blackboard, Decorator, Service, Task)

AlgorFati 2020. 10. 22. 11:02

AIController

먼저 AIController를 만든다.

AIController는 언리얼 인공지능을 위한 가장 핵심적인 개념이다.

Pawn에 의해 소유될 수 있고, 자체적인 로직이나 behavior tree를 이용한 로직을 통해 인공지능을 표현할 수 있다.

그리고 모든 AIController는 server에서만 존재할 수 있다.

ShooterAIController.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "PhysicsNNAnimationComponent.h"
#include "ShooterAIController.generated.h"

class APhysicsNNAnimationCharacter;
class UBehaviorTreeComponent;

UCLASS()
class AShooterAIController : public AAIController
{
	GENERATED_BODY()
public:
	virtual void Tick(float DeltaSeconds) override;
	bool IsDead() const;

	void SetAgro(AActor* AgroTarget);

	APhysicsNNAnimationCharacter* GetControlledCharacter() const { return ControlledCharacter; }

protected:
	virtual void BeginPlay() override;

private:
	UPROPERTY(EditAnywhere)
		class UBehaviorTree* AIBehavior;

	UPROPERTY()
		UBehaviorTreeComponent* BTreeComp;

	UPROPERTY()
		APhysicsNNAnimationCharacter* ControlledCharacter;
};

 

ShooterAIController.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "ShooterAIController.h"

#include "Kismet/GameplayStatics.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "PhysicsNNAnimationCharacter.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "PhysicsNNAnimationComponent.h"


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

	if (AIBehavior != nullptr)
	{
		RunBehaviorTree(AIBehavior);

		BTreeComp = Cast<UBehaviorTreeComponent>(GetBrainComponent());
		if (BTreeComp == nullptr)
		{
			UE_LOG(LogTemp, Warning, TEXT("Cast failed to UBehaviorTreeComponent!"));
		}

		ControlledCharacter = Cast<APhysicsNNAnimationCharacter>(GetPawn());
		if (ControlledCharacter != nullptr)
		{
			UE_LOG(LogTemp, Warning, TEXT("Cast failed to APhysicsNNAnimationCharacter!"));
		}

		APawn *PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
		FName BlackboardKeyName = TEXT("StartLocation");
		GetBlackboardComponent()->SetValueAsVector(BlackboardKeyName, GetPawn()->GetActorLocation());
	}
}

void AShooterAIController::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
}

bool AShooterAIController::IsDead() const
{
	if (ControlledCharacter != nullptr)
	{
		return ControlledCharacter->IsDead();
	}

	return true;
}

void AShooterAIController::SetAgro(AActor* AgroTarget)
{
	if (BTreeComp != nullptr)
	{
		FName BlackboardKeyName = TEXT("LastKnownTargetLocation");
		BTreeComp->GetBlackboardComponent()->SetValueAsVector(BlackboardKeyName, AgroTarget->GetActorLocation());
	}
}

 

BehaviorTree

BehaviorTree는 인공지능을 위한 행위 집합을 트리 형태로 표현한 것이다.

트리의 계층구조적인 특성 덕분에, 행동 단위의 순서매김 및 조합이 쉽고, 재활용하기 좋다.

이제 다음과 같은 BehaviorTree를 제작할 것이다.

 

Blackboard

BehaviorTree에서 각 행동 단위들이 모두 분리되어 있기 때문에 모듈화가 쉽다는 장점이 있지만,

반대로 이런 상황에서는 서로 공유하는 변수에 대한 처리가 어려울 수 있다. 

이런 상황을 위해 Blackboard라는 개념을 이용한다.

Blackboard는 BehaviorTree에서 사용되는 변수를 <BlackboardKey, 변수> 맵을 통해 관리한다.

어디서든 Key만 있으면 변수에 접근할 수 있다.

다음과 같이 필요한 변수들을 BlackboardKey로 정의해두면 어디서든 사용할 수 있다.

(ObjectType의 경우 Actor를 따로 특정해주지 않으면, 이용하는데 문제가 생길 수 있다.)

 

 

Decorator

Decorator는 BehaviorTree의 노드에 조건으로서 붙는 개념이다.

예를 들어 위 BehaviorTree에 "Can See Player?" 라는 Decorator가 정의되어 있는데, 이는 현재 target 변수가 유효한지 체크한다. 그래서 조건이 유효한 경우에만 하위에 행동들을 수행하도록 할 수 있다.

Decorator도 커스텀으로 제작할 수 있지만, 현재 예제에서는 Blackboard Decorator(Blackboard 변수 체킹 용도) 만 이용하고 있다.

 

 

Service

그리고 Service를 만든다.

Service는 BehaviorTree에서 어떤 노드를 수행할 때 그 노드에서 특정 시간마다 수행되는 작업을 넣어주기에 적합하다.

예를 들어서 ai agent가 0.05초마다 타겟을 갱신해주어야 한다면, 이와 같은 작업은 service로 구현할 수 있다.

BTService_RootService.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "BTService_RootService.generated.h"

/**
 * 
 */
UCLASS()
class UBTService_RootService : public UBTService
{
	GENERATED_BODY()

	virtual void InitializeFromAsset(UBehaviorTree& Asset) override;

public:
	UBTService_RootService();

protected:
	virtual void TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds) override;

private:

	UPROPERTY(EditAnywhere, Category = Blackboard)
		struct FBlackboardKeySelector SetKeyForIsMovable;

	UPROPERTY(EditAnywhere, Category = Blackboard)
		struct FBlackboardKeySelector SetKeyForShouldGetup;
};

 

BTService_RootService.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTService_RootService.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/Pawn.h"
#include "AIController.h"
#include "PhysicsNNAnimationCharacter.h"

UBTService_RootService::UBTService_RootService()
{
	NodeName = "Update Root Service Tick";
}

void UBTService_RootService::InitializeFromAsset(UBehaviorTree& Asset)
{
	Super::InitializeFromAsset(Asset);
	UBlackboardData* BBAsset = GetBlackboardAsset();
	if (ensure(BBAsset))
	{
		SetKeyForIsMovable.ResolveSelectedKey(*BBAsset);
		SetKeyForShouldGetup.ResolveSelectedKey(*BBAsset);
	}
}

void UBTService_RootService::TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	if (OwnerComp.GetAIOwner() == nullptr)
	{
		return;
	}

	APawn* Pawn = OwnerComp.GetAIOwner()->GetPawn();
	APhysicsNNAnimationCharacter* Character = Cast<APhysicsNNAnimationCharacter>(Pawn);
	
	
	if (Character != nullptr)
	{
		if (Character->IsRagdollOnGround())
		{
			FName BlackboardKeyName = SetKeyForShouldGetup.SelectedKeyName;
			OwnerComp.GetBlackboardComponent()->SetValueAsBool(BlackboardKeyName, true);
		}
	}
	
	
	FName IsMovableBK = SetKeyForIsMovable.SelectedKeyName;

	if (Character != nullptr)
	{
		bool bIsMovable = Character->IsMovable();

		if (bIsMovable)
		{
			OwnerComp.GetBlackboardComponent()->SetValueAsBool(IsMovableBK, bIsMovable);
			return;
		}
	}

	OwnerComp.GetBlackboardComponent()->ClearValue(IsMovableBK);
}

 

BTService_TargetIfSeen.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "BTService_TargetIfSeen.generated.h"

/**
 * 
 */
UCLASS()
class UBTService_TargetIfSeen : public UBTService
{
	GENERATED_BODY()

	virtual void InitializeFromAsset(UBehaviorTree& Asset) override;

public:
	UBTService_TargetIfSeen();

protected:
	virtual void TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds) override;

private:
	AActor* GetClosestActorInSphereRange(AActor* InOwner);

private:

	UPROPERTY(EditAnywhere, Category = Blackboard)
		struct FBlackboardKeySelector SetKeyForTarget;

	UPROPERTY(EditAnywhere)
		float ViewRange;
};

 

 

BTService_TargetIfSeen.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTService_TargetIfSeen.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/Pawn.h"
#include "AIController.h"
#include "Kismet/KismetMathLibrary.h"

UBTService_TargetIfSeen::UBTService_TargetIfSeen()
{
	NodeName = "Update Target If Seen";
}

void UBTService_TargetIfSeen::InitializeFromAsset(UBehaviorTree& Asset)
{
	Super::InitializeFromAsset(Asset);
	UBlackboardData* BBAsset = GetBlackboardAsset();
	if (ensure(BBAsset))
	{
		SetKeyForTarget.ResolveSelectedKey(*BBAsset);
	}
}

void UBTService_TargetIfSeen::TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	AActor* OwnerActor = OwnerComp.GetAIOwner()->GetPawn();
	AActor* TargetActor = GetClosestActorInSphereRange(OwnerActor);
	FName Target = SetKeyForTarget.SelectedKeyName;

	if (TargetActor == nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("UBTService_TargetIfSeen TargetActor is nullptr!!"));
		OwnerComp.GetBlackboardComponent()->ClearValue(Target);
		return;
	}

	if (OwnerComp.GetAIOwner() == nullptr)
	{
		return;
	}

	if (OwnerComp.GetAIOwner()->LineOfSightTo(TargetActor))
	{
		APawn* ThisPawn = OwnerComp.GetAIOwner()->GetPawn();
		FVector TargetLoc = TargetActor->GetActorLocation();
		FVector Location = ThisPawn->GetActorLocation();

		FVector Forward = ThisPawn->GetActorForwardVector();
		FVector ToTarget = (TargetLoc - Location).GetSafeNormal();

		float Dot = FVector::DotProduct(Forward, ToTarget);

		if (Dot > -0.5f)
		{
			OwnerComp.GetBlackboardComponent()->SetValueAsObject(Target, TargetActor);
			if (Dot > 0)
			{
				FRotator LookAtRot = UKismetMathLibrary::FindLookAtRotation(Location, TargetLoc);

				FRotator TargetRot = FRotator(0.0f, LookAtRot.Yaw, 0.0f);
				FRotator NewRot = UKismetMathLibrary::RInterpTo(
					ThisPawn->GetActorRotation(), TargetRot, DeltaSeconds, 10.0f);

				ThisPawn->SetActorRotation(NewRot);
			}
			return;
		}
	}

	OwnerComp.GetBlackboardComponent()->ClearValue(Target);
}

AActor* UBTService_TargetIfSeen::GetClosestActorInSphereRange(AActor* InOwner)
{
	EObjectTypeQuery ObjType = UEngineTypes::ConvertToObjectType(ECollisionChannel::ECC_Pawn);
	TArray<AActor*> ActorsToIgnore = TArray<AActor*> { InOwner };
	TArray<AActor*> OutActors;
	FVector SpherePos = InOwner->GetActorLocation();
	float SphereRadius = ViewRange;

	if (UKismetSystemLibrary::SphereOverlapActors(
		InOwner->GetWorld(),
		SpherePos,
		SphereRadius,
		TArray<TEnumAsByte<EObjectTypeQuery>> { ObjType },
		nullptr,
		ActorsToIgnore,
		OutActors))
	{
		//DrawDebugSphere(GetWorld(), SpherePos, 100.0f, 26, FColor(181, 0, 0), true, -1, 0, 2);

		float MinDistance = TNumericLimits<float>::Max();
		AActor* ClosestActor = nullptr;

		for (int i = 0; i < OutActors.Num(); ++i)
		{
			AActor* FoundActor = OutActors[i];
			float Distance = FoundActor->GetDistanceTo(InOwner);

			if (Distance < MinDistance)
			{
				MinDistance = Distance;
				ClosestActor = FoundActor;
			}
		}

		if (ClosestActor == nullptr)
		{
			UE_LOG(LogTemp, Warning, TEXT("Target is nullptr"));
		}

		return ClosestActor;
	}
	return nullptr;
}

 

 

BTService_UpdateTargetLocation.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "BTService_UpdateTargetLocation.generated.h"

/**
 * 
 */
UCLASS()
class UBTService_UpdateTargetLocation : public UBTService
{
	GENERATED_BODY()

	virtual void InitializeFromAsset(UBehaviorTree& Asset) override;

public:
	UBTService_UpdateTargetLocation();

protected:
	virtual void TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds) override;

private:
	UPROPERTY(EditAnywhere, Category = Blackboard)
		struct FBlackboardKeySelector KeyForTarget;

	UPROPERTY(EditAnywhere, Category = Blackboard)
		struct FBlackboardKeySelector SetKeyForLastTargetLocation;
};

 

 

BTService_UpdateTargetLocation.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTService_UpdateTargetLocation.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/Pawn.h"
#include "AIController.h"
#include "Kismet/KismetMathLibrary.h"

UBTService_UpdateTargetLocation::UBTService_UpdateTargetLocation()
{
	NodeName = "Update Target Location";
}

void UBTService_UpdateTargetLocation::InitializeFromAsset(UBehaviorTree& Asset)
{
	Super::InitializeFromAsset(Asset);
	UBlackboardData* BBAsset = GetBlackboardAsset();
	if (ensure(BBAsset))
	{
		KeyForTarget.ResolveSelectedKey(*BBAsset);
		SetKeyForLastTargetLocation.ResolveSelectedKey(*BBAsset);
	}
}

void UBTService_UpdateTargetLocation::TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	UObject* Obj = OwnerComp.GetBlackboardComponent()->GetValueAsObject(KeyForTarget.SelectedKeyName);

	if (Obj == nullptr)
		return;

	AActor* TargetActor = Cast<AActor>(Obj);

	if (TargetActor == nullptr)
		return;

	OwnerComp.GetBlackboardComponent()->SetValueAsVector(
		SetKeyForLastTargetLocation.SelectedKeyName, TargetActor->GetActorLocation());
}

 

 

 

Task

다음으로 Task를 만든다.

Task는 BehaviorTree의 말단에 붙는 노드로 각 행위를 표현하기에 적합하다. 

 

 

BTTask_ClearBlackboardValue.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

/**
 * 
 */
UCLASS()
class UBTTask_ClearBlackboardValue : public UBTTask_BlackboardBase
{
	GENERATED_BODY()

public:
	UBTTask_ClearBlackboardValue();

protected:
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory) override;
};

 

BTTask_ClearBlackboardValue.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTTask_ClearBlackboardValue.h"
#include "BehaviorTree/BlackboardComponent.h"


UBTTask_ClearBlackboardValue::UBTTask_ClearBlackboardValue()
{
	NodeName = "Clear Blackboard Value";
}

EBTNodeResult::Type UBTTask_ClearBlackboardValue::ExecuteTask(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	OwnerComp.GetBlackboardComponent()->ClearValue(GetSelectedBlackboardKey());

	return EBTNodeResult::Succeeded;
}

 

 

BTTask_Getup.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_Getup.generated.h"

/**
 * 
 */
UCLASS()
class UBTTask_Getup : public UBTTaskNode
{
	GENERATED_BODY()

	virtual void InitializeFromAsset(UBehaviorTree& Asset) override;

public:
	UBTTask_Getup();

protected:
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory) override;

private:
	UPROPERTY(EditAnywhere, Category = Blackboard)
		struct FBlackboardKeySelector GetKeyForShouldGetup;
};

 

 

BTTask_Getup.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "BTTask_Getup.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "PhysicsNNAnimationCharacter.h"

UBTTask_Getup::UBTTask_Getup()
{
	NodeName = "Getup";
}

void UBTTask_Getup::InitializeFromAsset(UBehaviorTree& Asset)
{
	Super::InitializeFromAsset(Asset);

	UBlackboardData* BBAsset = GetBlackboardAsset();
	if (BBAsset)
	{
		GetKeyForShouldGetup.ResolveSelectedKey(*BBAsset);
	}
	else
	{
		UE_LOG(LogBehaviorTree, Warning, TEXT("Can't initialize task: %s, make sure that behavior tree specifies blackboard asset!"), *GetName());
	}
}

EBTNodeResult::Type UBTTask_Getup::ExecuteTask(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	if (OwnerComp.GetAIOwner() == nullptr)
	{
		return EBTNodeResult::Failed;
	}

	APhysicsNNAnimationCharacter* Character =
		Cast<APhysicsNNAnimationCharacter>(OwnerComp.GetAIOwner()->GetPawn());
	if (Character == nullptr)
	{
		return EBTNodeResult::Failed;
	}

	FName GetupKeyName = GetKeyForShouldGetup.SelectedKeyName;
	bool bShouldGetup = OwnerComp.GetBlackboardComponent()->GetValueAsBool(GetupKeyName);

	if (bShouldGetup)
	{
		Character->Getup();
		OwnerComp.GetBlackboardComponent()->ClearValue(GetupKeyName);
	}

	return EBTNodeResult::Succeeded;
}

 

BTTask_Shoot.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

/**
 * 
 */
UCLASS()
class UBTTask_Shoot : public UBTTask_BlackboardBase
{
	GENERATED_BODY()
public:
	UBTTask_Shoot();

protected:
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory) override;
};

 

 

BTTask_Shoot.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTTask_Shoot.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "PhysicsNNAnimationCharacter.h"
#include "TestUtils.h"

UBTTask_Shoot::UBTTask_Shoot()
{
	NodeName = "Shoot";
}

EBTNodeResult::Type UBTTask_Shoot::ExecuteTask(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	if (OwnerComp.GetAIOwner() == nullptr)
	{
		return EBTNodeResult::Failed;
	}

	APhysicsNNAnimationCharacter* Character = 
		Cast<APhysicsNNAnimationCharacter>(OwnerComp.GetAIOwner()->GetPawn());
	if (Character == nullptr)
	{
		return EBTNodeResult::Failed;
	}

	FName Target = GetSelectedBlackboardKey();
	UObject* Object = OwnerComp.GetBlackboardComponent()->GetValueAsObject(Target);

	if (Object != nullptr)
	{
		ACharacter* TargetCharacter = Cast<ACharacter>(Object);

		if (TargetCharacter != nullptr)
		{
			int RandomIndex = FMath::Rand() % TestUtils::OverlapTestBoneNames.Num();
			FName TargetBoneName = TestUtils::OverlapTestBoneNames[RandomIndex];

			FVector BoneLoc = TargetCharacter->GetMesh()->GetBoneLocation(TargetBoneName);
			Character->FireToTarget(BoneLoc);
		}
	}

	return EBTNodeResult::Succeeded;
}