[언리얼 입문] BehaviorTree #2
이번엔 저번 실습에 새노드를을 추가해서 적탐색과 적추적과 공격하는 AI를 추가하는 과정을 정리해봤다.
이번엔 저번에 사용햇던 시퀀스와는달리 셀렉터 라는 노드가 새로 추가되는데
이 셀렉터노드의 역할은 if else같은 역할을 한다고 보면된다 자식노드를 왼쪽부터 순차적으로 실행해
자식노드가 실패하면 다음 자식을 실행하는 구조로 되어있고 자식이 성공하면 다른 자식은 실행되지않는다.
이런 특성을 통해 타겟을 발견하면 추적 - 공격 하고 실패시 랜덤이동하는 AI를 구성하는 실습이었다.
먼저 셀렉터에대한 조건을 추가해줘야한다.
타겟서치에대한 서비스를 새로 생성해줘야했는데 BTService를 상속받는 새 C++를 하나 만들어준다.
서비스의 역할이 뭔지 궁금해서 따로 GPT로 검색해봤는데 서비스는 다른 노드의 실행중에도 주기적으로 실행되며
노드의 센서역할을 한다하고한다.
//타겟탐지서비스 헤더
UCLASS()
class UNREALTUTORIAL_API UBTService_SearchTarget : public UBTService
{
GENERATED_BODY()
public:
UBTService_SearchTarget();
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
//타겟탐지서비스 CPP
UBTService_SearchTarget::UBTService_SearchTarget()
{
NodeName = TEXT("SearchTarget");
Interval = 1.0f;
}
void UBTService_SearchTarget::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp,NodeMemory,DeltaSeconds);
auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();
if(CurrentPawn == nullptr)
return;
UWorld* World = CurrentPawn->GetWorld();
FVector Center = CurrentPawn->GetActorLocation();
float SearchRadius = 500.f;
if(World == nullptr)
return;
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams QueryParams(NAME_None,false, CurrentPawn);
bool bResult = World->OverlapMultiByChannel(
OverlapResults,
Center,
FQuat::Identity,
ECollisionChannel::ECC_EngineTraceChannel2,
FCollisionShape::MakeSphere(SearchRadius),
QueryParams);
if(bResult)
{
for (auto& OverlapResult : OverlapResults)
{
AMyCharacter* MyCharacter = Cast<AMyCharacter>(OverlapResult.GetActor());
if(MyCharacter && MyCharacter->GetController()->IsPlayerController())
{
OwnerComp.GetBlackboardComponent()->SetValueAsObject(FName(TEXT("Target")),MyCharacter);
DrawDebugSphere(World,Center,SearchRadius, 16, FColor::Green, false, 0.2f);
return;
}
}
}
else
{
OwnerComp.GetBlackboardComponent()->SetValueAsObject(FName(TEXT("Target")),nullptr);
}
DrawDebugSphere(World,Center,SearchRadius, 16, FColor::Red, false, 0.2f);
}
이렇게 구성하면 충돌을 이용해 AI캐릭터가 1초마다 주변을 원형으로 탐지하여 플레이어 캐릭터가 감지되면 해당 캐릭터를 타겟으로 설정한다.
그뒤 BehaviorTree를 이런식으로 구성하고 자식 개체에 BlackBoard 데코레이터를 부착하여
왼쪽 셀렉터의 경우에는 Key가 있는경우의 데코레이터 오른쪽 시퀀스의경우에는 Key가 없는경우의 데코레이터로
설정하여 타겟을 찾았을경우에는 왼쪽분기 타겟을 못찾았을경우에는 오른쪽분기가 실행되도록한다.
데코레이터또한 강의에서 따로 설명이없었기에 GPT를 사용해 정보를 검색해봤는데 데코레이터란 Task의 조건부 실행을위한 기능으로 주어진 조건이 참일경우에만 Task를 실행한다고 되어있다.
공격여부를 판단하는 데코레이터는 기본적으로 존재하지않기때문에 서비스처럼 또 새로 만들어줘야한다.
이번엔 BTDecorator를 상속받는 새 C++을 생성한다.
//데코레이터 헤더
UCLASS()
class UNREALTUTORIAL_API UBTDecorator_CanAttack : public UBTDecorator
{
GENERATED_BODY()
public:
UBTDecorator_CanAttack();
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
};
//데코레이터 CPP
UBTDecorator_CanAttack::UBTDecorator_CanAttack()
{
NodeName = TEXT("CanAttack");
}
bool UBTDecorator_CanAttack::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);
auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();
if(CurrentPawn == nullptr)
return false;
auto Target = Cast<AMyCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(FName(TEXT("Target"))));
if(Target == nullptr)
return false;
return bResult && Target->GetDistanceTo(CurrentPawn) <= 200.f;
}
조건을 판단하는 노드이기때문에 true false를 리턴하는 함수가 부모내에 존재한다. 타겟과 자신과의 거리가 일정이하면
공격이 가능한것으로 판단한다.
CanAttack데코레이터를 사용해 왼쪽은 공격이 가능한경우 오른쪽은 Inverse Condition을 체크해 공격이 불가능한경우 실행되게 하여 분기를 나눈다.
마지막으로 실제로 공격을 실행할 노드를 만들어야한다. FindPatrolPos처럼 BTTask를 상속받는 새C++를 하나 만들도록한다.
//어택노드 헤더
UCLASS()
class UNREALTUTORIAL_API UBTTask_Attack : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_Attack();
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
private:
bool bIsAttacking = false;
};
//어택노드 CPP
UBTTask_Attack::UBTTask_Attack()
{
//TickTask 실행하려면 true여야함
bNotifyTick = true;
}
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp,NodeMemory);
auto MyCharacter = Cast<AMyCharacter>(OwnerComp.GetAIOwner()->GetPawn());
if(MyCharacter == nullptr)
return EBTNodeResult::Failed;
MyCharacter->Attack();
bIsAttacking = true;
MyCharacter->OnAttackEnd.AddLambda([this]()
{
bIsAttacking = false;
});
return EBTNodeResult::Succeeded;
}
void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
if(!bIsAttacking)
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
//캐릭터 CPP
void AMyCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
IsAttacking = false;
OnAttackEnd.Broadcast();
}
캐릭터 몽타주 쪽에 AI 공격 종료에대한 델리게이트를 새로하나 추가하고 AI 공격 시작과 중단을 판단하는 플래그를 추가해 노드 실행 종료를 구분해준다.
최종적으로는 이런 형태의 BehaviorTree가 완성된다. AI 패턴이 복잡해지면 효과가 좋아진다고하며 이렇게 단순한 동작만 할경우 BehaviorTree는 사전작업이 여러가지 있기때문에 구성하기가 까다롭다. 필요에따라 적절히 사용하거나
확장성있는 AI를 구축할때 사용하면 좋을거같다.