개발/Unreal Engine / / 2022. 2. 7. 01:06

Learning Unreal 4 언리얼 공부일지 - AIController class와 Behaivor Tree 2

반응형

Behavior Tree에서 sequence 노드를 실행하기 전에 Selector를 이용할 수 있다.

Sequence의 경우 진행 도중 문제가 생겨 실패하면 다음 노드로 진행할 수 없지만, Selector의 자식 노드들 중 하나가 실패하면 다음 자식 노드를 실행하는 기능이 있어 실패시 대처가 가능하다.

 

Selector를 이용하기 위해 BTService를 상속받는 클래스를 생성하자.

 

// BTService_SearchTarget.h

UCLASS()
class UNREALINTRODUCTION_API UBTService_SearchTarget : public UBTService
{
	GENERATED_BODY()
public:
	UBTService_SearchTarget();

	// UBTService에 있는 TickNode 함수를 오버라이딩
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};

헤더에서 TickNode라는 함수를 오버라이딩하여 선언하고, 

 

// BTService_SearchTarget.cpp
#include "BTService_SearchTarget.h"

// 헤더 추가
#include "MyAIController.h"
#include "MyCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "DrawDebugHelpers.h"

UBTService_SearchTarget::UBTService_SearchTarget()
{
	NodeName = TEXT("SearchTarget");
	Interval = 1.0f;
}

// 주기적으로 실행하는 Node
void UBTService_SearchTarget::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	// 각종 정보가 담긴 OwnerComp를 가져옴
	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형태로 저장
	bool bResult = World->OverlapMultiByChannel(
		OverlapResults,
		Center,
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(SearchRadius),
		QueryParams);

	if (bResult)
	{
    	// overlap을 확인하고
		for (auto& OverlapResult : OverlapResults) 
		{
			AMyCharacter* MyCharacter = Cast<AMyCharacter>(OverlapResult.GetActor());
			if (MyCharacter && MyCharacter->GetController()->IsPlayerController())
			{
            	// overlap 된 Actor를 Target변수로 black board로 전달
				OwnerComp.GetBlackboardComponent()->SetValueAsObject(FName(TEXT("Target")), MyCharacter);

				DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Green, false, 0.2f);

				return;
			}
		}

		DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Red, false, 0.2f);
	}
	else 
	{
		OwnerComp.GetBlackboardComponent()->SetValueAsObject(FName(TEXT("Target")), nullptr);

		DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Red, false, 0.2f);
	}
}

cpp파일에서 TickNode함수안에 충돌 체크 및 충돌한 Actor객체를 Target변수로 전달하도록 정의한다.

이 Target을 통해 블랙보드에서 충돌된 캐릭터의 정보를 가져올 수 있다.

 

블랙보드에 새 키로 Object 자료형인 Target을 생성하고, 디테일에서 Base Class를 캐릭터의 클래스로 지정하였다.

 

 

작성한 BTService 클래스는 Behavior Tree에서 'Selector 우클릭 → 서비스 추가'를 통해 삽입할 수 있다.

위 사진처럼 'Selector → 데코레이터추가'를 통해 조건을 줄 수 있다.

 

위 그림처럼 Behavior Tree를 구성한 다음 필요한 Blackboard key를 Target으로 설정하면 범위 안에 캐릭터가 들어오면 AI가 접근한다.

 

위에서는 BTService 클래스를 만들어서 Selector에 넣었지만, 이번에는 BTDecorator 클래스를 상속하여 생성해보자.

 

// BTDecorator_CanAttack.h

UCLASS()
class UNREALINTRODUCTION_API UBTDecorator_CanAttack : public UBTDecorator
{
	GENERATED_BODY()

public:
	UBTDecorator_CanAttack();

	virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
};

CanAttack이라는 BTDecorater 클래스를 생성하고, 헤더에 CanculateRawConditionValue함수를 오버라이드 한다.

 

// BTDecorator_CanAttack.cpp

// 헤더 추가
#include "MyAIController.h"
#include "MyCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"

// 생성자 - 노드 이름 정의
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;

	// Target으로 catch 한 값을 가져옴
	auto Target = Cast<AMyCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(FName(TEXT("Target"))));
	if (Target == nullptr)
		return false;

	// Target과 거리 계산
	return bResult && Target->GetDistanceTo(CurrentPawn) <= 200.f;
}

cpp파일에서는 선언한 CanculateRawConditionValue함우세 Target을 가져와서 그 거리를 계산하는 코드를 작성한다.

 

이렇게 생성한 BTDecorater를 Behavior Tree에서 데코레이터로 추가할 수 있다.

 

 

이번에는 BTTaskNode 클래스를 상속한 클래스를 만들어보자.

// BTTask_Attack.h

UCLASS()
class UNREALINTRODUCTION_API UBTTask_Attack : public UBTTaskNode
{
	GENERATED_BODY()
	
public:
	UBTTask_Attack();

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

	// udpate 함수
	virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

private:
	bool bIsAttacking = false;
};

BTTask_Attack 헤더에는 ExecuteTask와 TickTask로 실행함수 및 update함수를 선언한다. 

 

// BTTask_Attack.cpp

#include "BTTask_Attack.h"
// 헤더 추가
#include "MyAIController.h"
#include "MyCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTTask_Attack::UBTTask_Attack()
{
	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;

	// OnAttackEnd 함수에 람다(무명)함수로 정의
	MyCharacter->OnAttackEnd.AddLambda([this]()
		{
			bIsAttacking = false;
		});

	return Result;
}

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

	
	if (bIsAttacking == false)
		// Behavior Tree 종료처리
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}

ExecuteTask에서 MyCharacter의 Attack함수를 호출하고, 향후 만들 OnAttackEnd라는 델리게이트에 bIsAttacking 변수에 false를 넣어 공격이 끝남을 알려주도록 한다.

TickTask에서는 FinishLatentTask를 통해 Behavior Tree를 종료처리한다.

 

공격이 끝남을 알려줄 수 있는 델리게이트를 만들기 위해 Character 클래스를 수정한다.

// MyCharacter.h

DECLARE_MULTICAST_DELEGATE(FOnAttackEnd);

UCLASS()
class UNREALINTRODUCTION_API AMyCharacter : public ACharacter
{
	/.../
    
	// 델리게이트 선언
	FOnAttackEnd OnAttackEnd;
    
	/.../
}
// MyCharacter.cpp

/.../

void AMyCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	IsAttacking = false;
	
	// Attack이 끝남을 통보
	OnAttackEnd.Broadcast();
}

/.../

OnAttackMontageEnded에서 Delegate Broadcast를 통해 bind된 함수 (위에서 람다함수로 처리한 부분)을 오브젝트가 호출하도록 한다.

 

최종적으로 CanAttack의 Sequence에 생성한 BTTaskNode Attack을 연결한다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유