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;
}
'게임 엔진 > Unreal' 카테고리의 다른 글
[Unreal] [Example] 타이머 이용 방법 (Timer) (0) | 2020.12.15 |
---|---|
[Unreal] [Example] C++에서 Blueprint 호출, Blueprint에서 C++ 호출 (0) | 2020.11.16 |
[Unreal] 프로퍼티 관련 예제 (0) | 2020.10.08 |
[Unreal] Interface 이용 방법 (0) | 2020.10.04 |
[Unreal] [Example] Animation에서 Physics Velocity 계산 (0) | 2020.09.09 |