비헤비어 트리를 이용한 AI

sssukh·2024년 3월 31일
post-thumbnail

이번엔 비헤비어 트리를 이용해서 NPC에 AI를 적용해보도록 하자

AI Controller 설정


NPC의 Pawn 섹션을 보면 다음과 같은 속성들이 있다.

AutoPossessAI는 AI를 어떨때 자동으로 Possess 시킬건지를 설정하고
AIController Class는 Posses할 AIController Class를 설정한다.

우리가 원하는대로 컨트롤러를 설정하기위해 클래스를 생성한다.

AABCharacterNonPlayer::AABCharacterNonPlayer()
{
	GetMesh()->SetHiddenInGame(true);

	AIControllerClass = AABAIController::StaticClass();
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

NPC 생성자 코드에서 AutoPossessAIAIControllerClass를 직접 설정해준다.

비헤비어 트리와 블랙보드

Behavior TreeBlackboard 애셋을 각각 만들어준다.

Behavior Tree는 AI 의사결정이 이루어지는 곳이고
Blackboard는 비헤비어트리의 의사결정을 위한 데이터 저장소이다.


BehaviorTree를 다음과 같이 세팅한다.

<AABAIController.h>
public:
	AABAIController();

	void RunAI();
	void StopAI();

protected:
	virtual void OnPossess(APawn* InPawn) override;

private:
	UPROPERTY()
	TObjectPtr<class UBlackboardData> BBAsset;

	UPROPERTY()
	TObjectPtr<class UBehaviorTree> BTAsset;

OnPossess()는 폰에 빙의해서 조종할 때 발생하는 이벤트이다.

AABAIController::AABAIController()
{
	static ConstructorHelpers::FObjectFinder<UBlackboardData> BBAssetRef(TEXT("/Script/AIModule.BlackboardData'/Game/ArenaBattle/AI/BB_ABCharacter.BB_ABCharacter'"));
	if (nullptr != BBAssetRef.Object)
	{
		BBAsset = BBAssetRef.Object;
	}

	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTAssetRef(TEXT("/Script/AIModule.BehaviorTree'/Game/ArenaBattle/AI/BT_ABCharacter.BT_ABCharacter'"));
	if (nullptr != BTAssetRef.Object)
	{
		BTAsset = BTAssetRef.Object;
	}
}

void AABAIController::RunAI()
{
	// AIController내부에 Blackboard 정의되어있다.

	UBlackboardComponent* BlackboardPtr = Blackboard.Get();
	if (UseBlackboard(BBAsset, BlackboardPtr))
	{
    	bool RunResult = RunBehaviorTree(BTAsset);
		ensure(RunResult);
	}
}

void AABAIController::StopAI()
{
	// AIController내부에 BrainComponent 정의되어있다.
	UBehaviorTreeComponent* BTComponent = Cast<UBehaviorTreeComponent>(BrainComponent);
	if (BTComponent)
	{
		BTComponent->StopTree();
	}
}

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

	RunAI();
}

생성자에서 BBAsset에는 블랙보드, BTAsset에는 비헤비어 트리를 각각 등록시켜준다.

OnPossess()에서 Possess하면서 AI가 돌아가게 하는 RunAI()를 호출시킨다.

RunAI()에서는 AIController클래스 내부에 정의된 Blackboard 멤버변수를 가져와서 우리가 생성한 블랙보드를 사용하도록 한다.

StopAI()에서는 AI작동을 중지시키기 위해 비헤비어 트리 컴포넌트를 가져오는데 특이하게 BrainComponent라는 변수에 저장되어있다. 이 역시 AIController클래스 내부에 정의되어있다.

NPC 인터페이스 구현

NPC가 구현해야하는 필수적인 함수들을 담을 인터페이스 하나 생성한다.

<ABCharacterAIInterface.h>
public:
	virtual float GetAIPatrolRadius() = 0;
	virtual float GetAIDetectRange() = 0;
	virtual float GetAIAttackRange() = 0;
	virtual float GetAITurnSpeed() = 0;

<ABCharacterNonPlayer.h>
// AI Section
protected:
	virtual float GetAIPatrolRadius() override;
	virtual float GetAIDetectRange() override;
	virtual float GetAIAttackRange() override;
	virtual float GetAITurnSpeed() override;
    
<ABCharacterNonPlayer.cpp>
 float AABCharacterNonPlayer::GetAIPatrolRadius()
{
	return 800.0f;
}

Nonplayer가 상속받아서 해당 부분을 구현해주는데 일단 AIPatrolRadius값만 먼저 정의해준다.

NPC 패트롤 기능 추가

블랙보드에 키 추가를 통해 Vector값을 갖는 PatrolPos를 생성한다.

PatrolPos는 NPC가 정찰할 위치값을 갖는다.


AI가 경로를 찾기위해서는 네비게이션 메쉬 볼륨이 필요하기 때문에 PlaceActor 창에서 NavMeshBoundsVolume을 맵에 추가해준다.


0

brush에 x,y는 10만, z는 1000 설정.
배치후에 뷰포트에서 p키를 누르면 네비메쉬가 설정된 구역을 볼 수 있다.

네비메쉬 볼륨은 구축된 영역에 대해서 길찾기 영역이 만들어진다.

그런데 우리는 클리어 조건에 따라서 새로운 영역들이 생성이 되기 때문에 정적인 영역이 아닌 동적인 영역으로 설정을 변경해줘야 한다.

이를 위해 프로젝트 세팅에 가서 네비메쉬 설정을 건드려야 한다.
Project Setting - Navigation Mesh - Runtime에 가서 Runtime Generation을 Dynamic으로 설정해준다.

이렇게 하면 새로운 맵이 생성되도 다이나믹 설정에 의해서 새로운 영역도 네비메쉬를 사용할 수 있다.

NPC가 스폰된 중심점을 의미하는 HomePos를 추가한다. 마찬가지로 벡터값을 갖는다.

정찰위치를 HomePos를 기점으로 네비메쉬가 허용되는 범위 안에서 정한다.
그래서 네비메쉬가 제공하는 범위 안의 랜덤한 위치를 가져와야 한다.

전에 생성해둔 컴포짓을 Sequence로 변경한 뒤 Moveto를 추가하고 Moveto의 Description을 PatrolPos으로 변경한다.

컴포짓이 Sequence인 경우 등록된 태스크를 왼쪽에서부터 차례로 순회하며 실행하지만 Select인 경우 등록된 태스크 중 하나만 선택해서 실행하기 때문에 대기 후 움직이는 우리의 경우에는 Sequence가 어울린다.

이제는 PatrolPos를 계산하는 태스크가 필요하기 때문에 생성해준다.

언리얼에서는 액션을 Task라고 하고 BTTask_ 라는 네이밍 규칙을 갖는다.

그래서 BTTask_FindPatrolPos의 이름을 갖고 BTNode를 상속받는 C++ 클래스를 생성한다.

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG","NavigationSystem","AIModule","GameplayTasks"});

행동트리모듈을 build.cs에 추가해줘야 작동한다.

<ABAI.h>
#pragma once

#define BBKEY_HOMEPOS TEXT("HomePos")
#define BBKEY_PATROLPOS TEXT("PatrolPos")
#define BBKEY_TARGET TEXT("Target")

블랙보드의 키값을 관리하기 편하도록 define 전처리기로 처리해준다.

void AABAIController::RunAI()
{
	// AIController내부에 Blackboard 정의되어있다.

	UBlackboardComponent* BlackboardPtr = Blackboard.Get();
	if (UseBlackboard(BBAsset, BlackboardPtr))
	{
		Blackboard->SetValueAsVector(BBKEY_HOMEPOS, GetPawn()->GetActorLocation());

		bool RunResult = RunBehaviorTree(BTAsset);
		ensure(RunResult);
	}
}

AIController에서 비헤비어 트리를 실행하기 전에 블랙보드의 HomePos값을 설정해주도록 한다.

<BTTask_FindPatrolPos.h>
public:
	UBTTask_FindPatrolPos();

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

UBTTaskNode 클래스에 정의되어있는 ExecuteTask를 오버라이드해서 사용한다.

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

    APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
    if (nullptr == ControllingPawn)
    {
        return EBTNodeResult::Failed;
    }



    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(ControllingPawn->GetWorld());
    if (nullptr == NavSystem)
    {
        return EBTNodeResult::Failed;
    }
    
    IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
    if (nullptr == AIPawn)
    {
        return EBTNodeResult::Failed;
    }


    FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(BBKEY_HOMEPOS);
    float PatrolRadius = AIPawn->GetAIPatrolRadius();
    FNavLocation NextPatrolPos;

    if (NavSystem->GetRandomPointInNavigableRadius(Origin, PatrolRadius, NextPatrolPos))
    {
        OwnerComp.GetBlackboardComponent()->SetValueAsVector(BBKEY_PATROLPOS, NextPatrolPos.Location);
        return EBTNodeResult::Succeeded;

    }
    return EBTNodeResult::Failed;
}

현재 AI가 조종하는 Pawn이 있는 월드의 NavigationSystem과 AI가 Possess하는 Pawn을 가져온다.
NavigationSystem이 지원하는 GetRandomPointInNavigableRadius()를 통해 HomePos를 기준으로 정찰범위 내의 랜덤한 지점을 얻고 PatrolPos로 세팅해준다.

그리고 비헤비어 트리에 추가해준다.

패트롤 기능이 잘 동작하는 것을 볼 수 있다.

NPC 유저 감지 기능 추가

NPC가 캐릭터를 감지하는 기능을 추가해야하는데 이 기능은 항상 가동되어야 한다. 이를 위해 서비스 노드를 추가해준다.

BTService를 상속받는 클래스를 생성한다.

이 서비스 노드가 부착된 컴포짓 노드가 활성화된 상태에서는 지정한 인터벌로 계속해서 틱이 호출된다.

<BTService_Detect.h>
public:
	UBTService_Detect();

protected:
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
	
<BTService_Detect.cpp>
UBTService_Detect::UBTService_Detect()
{
	// 이름 지정
	NodeName = TEXT("Detect");
	// 틱 간격 조정
	Interval = 1.0f;
}

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

	APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn)
	{
		return;
	}

	FVector Center = ControllingPawn->GetActorLocation();
	UWorld* World = ControllingPawn->GetWorld();
	if (nullptr == World)
	{
		return;
	}

	IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
	if (nullptr == AIPawn)
	{
		return;
	}

	float DetectRadius = AIPawn->GetAIDetectRange();

	// 여럿을 감지해서 배열에 저장
	TArray<FOverlapResult> OverlapResults;
	FCollisionQueryParams CollisionQueryParam(SCENE_QUERY_STAT(Detect), false, ControllingPawn);
	bool bResult = World->OverlapMultiByChannel(
		OverlapResults,
		Center,
		FQuat::Identity,
		CCHANNEL_ABACTION,
		FCollisionShape::MakeSphere(DetectRadius),
		CollisionQueryParam
	);

	if (bResult)
	{
		for (auto const& OverlapResult : OverlapResults)
		{
			APawn* Pawn = Cast<APawn>(OverlapResult.GetActor());
			// 감지한 pawn이 플레이어인 경우
			if (Pawn && Pawn->GetController()->IsPlayerController())
			{
				OwnerComp.GetBlackboardComponent()->SetValueAsObject(BBKEY_TARGET, Pawn);
				DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Green, false, 0.2f);

				DrawDebugPoint(World, Pawn->GetActorLocation(), 10.0f, FColor::Green, false, 0.2f);
				DrawDebugLine(World, ControllingPawn->GetActorLocation(), Pawn->GetActorLocation(), FColor::Green, false, 0.27f);
				return;
			}
		}
	}

	OwnerComp.GetBlackboardComponent()->SetValueAsObject(BBKEY_TARGET, nullptr);
	DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
}

생성자에서는 노드의 이름과 틱 간격을 조정해준다.

여기서의 TickNode()는 전 강좌에서 활용했던 OverlapMultiByChannel()으로 유저 캐릭터를 감지해내는데 전과 다른 점은 TArray에 결과를 저장해서 여러 물체들을 감지할 수 있다는 점이다.

감지 결과에 플레이어가 있는 경우, 블랙보드의 Target이라는 값에 세팅을 해준다.

그리고 디버깅을 통해 보기 편하도록 그림을 추가해준다.

<ABCharacterNonPlayer.cpp>
float AABCharacterNonPlayer::GetAIDetectRange()
{
	return 400.0f;
}

그리고 AIDetectRange()도 잊지말고 추가해준다.

블랙보드에 Target이라는 새로운 키값을 추가해주고 베이스 클래스를 Pawn으로 설정해준다.

Sequence에 우리가 생성한 서비스를 추가해주면서 NPC가 우리를 감지할 수 있게 되었다.

AI가 캐릭터를 공격-추격 하는 경우는 캐릭터를 감지해서 Target이 세팅되었을 경우이기 때문에 비헤비어 트리를 살짝 수정해주고 기본 데코레이터인 블랙보드를 추가해준다.

OnTarget으로 이름을 변경해주고 블랙보드 키 Target이 세팅되었을 때만 들어가도록 한다.
반대로 다른 컴포짓에는 Target이 세팅되지 않을 때만 들어가도록 한다.

그런데 타겟을 탐지하지 못하고 정찰을 하는 중간에 타겟을 탐지하더라도 기존 행동을 중단하지 못한다. 이를 수정하기 위해 Notify Observer의 값을 On Result Change로 변경해줘서 값이 변경된 경우 abort 시키도록 한다.

NPC 공격 추가

공격 기능을 추가하기위해 공격을 해야하는지 추격을 해야하는지 판단을 하기위해서 타겟이 공격범위 내에 있는지 확인하는 Decorator클래스를 추가해주고 공격을 하는 태스크를 추가해준다.

왼쪽 컴포짓 부분을 다음과 같이 수정해준다.

Attack으로 빠지는 Selector 컴포짓과 MoveTo로 빠지는 Selector 컴포짓으로 분리시키고 각각 방금 생성한 AttackInRange데코레이션을 추가해준다.

대상이 Range안에 들어올경우Attack이 있는 컴포짓으로 빠지게, 대상이 Range안에 있지 않은 경우 MoveTo컴포짓으로 빠지게 해야한다. 그래서 MoveTo컴포짓에는 Condition속성에 Inverse Condition을 체크해준다.

그리고 대상이 Range안에 갑자기 들어왔을 때 바로 공격할 수 있도록 Flow ControlObserver abortsBoth로 설정해준다.

그러면 값이 변경되면 지금 행동을 중단하고 의사결정을 처음부터 다시하도록 한다.

<BTDecorator_AttackInRange.h>
public:
	UBTDecorator_AttackInRange();

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

생성한 데코레이터 코드로 가서 CalculateRawConditionValue()라는 함수를 오버라이드해준다. 이 함수는 데코레이션의 컨디션 값을 계산하는 함수이다.

<UBTDecorator_AttackInRange.cpp>
UBTDecorator_AttackInRange::UBTDecorator_AttackInRange()
{
	NodeName = TEXT("CanAttack");
}

bool UBTDecorator_AttackInRange::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
	bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);

	APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn)
	{
		return false;
	}

	IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
	if (nullptr == AIPawn)
	{
		return false;
	}

	APawn* Target = Cast<APawn>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(BBKEY_TARGET));
	if (nullptr == Target)
	{
		return false;
	}

	float DistanceToTarget = ControllingPawn->GetDistanceTo(Target);
	float AttackRangeWithRadius = AIPawn->GetAIAttackRange();
	bResult = (DistanceToTarget <= AttackRangeWithRadius);
	return bResult;
}

생성자에서는 이름, CalculateRawConditionValue()는 타겟과 폰의 거리를 구하고 AI에 등록한 공격 범위를 비교해서 대상이 범위 안에 있는지의 여부를 리턴해주면 된다.

AttackRange를 지정하는데 실제로는 AttackRange + AttackRadius 가 되야한다.

<ABCharacterStatComponent.h>
	FORCEINLINE float GetAttackRadius()	const { return AttackRadius; }

	UPROPERTY( VisibleInstanceOnly, Category = Stat, MEta = (AllowPrivateAccess = "true"))
	float AttackRadius;
    
<ABCharacterStatComponent.cpp>
UABCharacterStatComponent::UABCharacterStatComponent()
{
	CurrentLevel = 1;
	AttackRadius = 50.0f;
}

그래서 스탯에 AttackRadius를 추가해서 Get함수를 정의하고 생성자에서 초기화해준다.

<ABCharacterBase.cpp>
void AABCharacterBase::AttackHitCheck()
{
	// ...
    const float AttackRadius = Stat->GetAttackRadius();
    // ...
}

기존 CharacterBaseAttackRadius값을 가져오는 부분을 수정해주고

<ABCharacterNonPlayer.cpp>
float AABCharacterNonPlayer::GetAIAttackRange()
{
	return Stat->GetTotalStat().AttackRange + Stat->GetAttackRadius() * 2;
}

이렇게 GetAIAttackRange()리턴 값에 넣어준다.

공격에 경우에는 공격이 시작되고 몽타주 재생이 끝나야만 공격이 끝났다고 할 수 있다.
그렇기 때문에 공격명령을 내렸다고 태스크가 성공으로 끝나면 안된다.

<ABCharacterAIInterface.h>
DECLARE_DELEGATE(FAICharacterAttackFinished);

virtual void SetAIAttackDelegate(const FAICharacterAttackFinished& InOnAttackFinished) = 0;
virtual void AttackByAI() = 0;

그렇기 때문에 인터페이스에 공격기능을 하는 함수와 공격이 끝났을 때 호출시킬 델리게이트를 선언해준다.
그리고 델리게이트를 넘기기위한 함수 SetAIAttackDelegate() 역시 추가해준다.

<ABCharacterNonPlayer.h>
virtual void SetAIAttackDelegate(const FAICharacterAttackFinished& InOnAttackFinished) override;
virtual void AttackByAI() override;

FAICharacterAttackFinished OnAttackFinished;

<ABCharacterNonPlayer.cpp>
void AABCharacterNonPlayer::SetAIAttackDelegate(const FAICharacterAttackFinished& InOnAttackFinished)
{
	OnAttackFinished = InOnAttackFinished;
}

void AABCharacterNonPlayer::AttackByAI()
{
	ProcessComboCommand();
}

델리게이트를 변수로 받아주고 유저 캐릭터가 공격입력이 들어왔을 때 ProcessComboCommand()가 호출되었던 것과 동일하게 NPC 역시 동일하게 해준다.

<ABCharacterBase.h>
	virtual void NotifyComboActionEnd();

<ABCharacterBase.cpp>
void AABCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
    ensure(CurrentCombo != 0);
    CurrentCombo = 0;
    GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);

    NotifyComboActionEnd();
}

void AABCharacterBase::NotifyComboActionEnd()
{
}

<ABCharacterNonPlayer.cpp>
void AABCharacterNonPlayer::NotifyComboActionEnd()
{
	Super::NotifyComboActionEnd();
	OnAttackFinished.ExecuteIfBound();
}

공격이 끝나는 시점을 알기위해 ComboActionEnd()라는 함수를 사용했는데 이 함수의 인자에 UAnimMontage가 사용되기 때문에 껄끄럽다.

그래서 NotifyComboActionEnd()라는 함수를 만들어서 ComboActionEnd()이 끝나는 시점에 호출하도록 하고 NPC 코드에서 상속받아 사용하도록 한다.

<BTTask_Attack.h>
public:
	UBTTask_Attack();

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    
<BTTask_Attack.cpp>
 EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

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

	IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
	if (nullptr == AIPawn)
	{
		return EBTNodeResult::Failed;
	}

	FAICharacterAttackFinished OnAttackFinished;
	OnAttackFinished.BindLambda(
		[&]()
		{
			FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
		}
	);

	AIPawn->SetAIAttackDelegate(OnAttackFinished);
	AIPawn->AttackByAI();
	return EBTNodeResult::InProgress;
}

Inprogress를 리턴한 다음에 Task가 끝났다고 알려주는 함수로 FinishLatentTask()가 있다. 이를 아까 생성한 델리게이트에 람다를 이용해서 묶어주면 공격이 끝나는 시점에 델리게이트가 이 함수를 호출해서 Task가 성공적으로 끝마치도록 할 것이다.

이렇게 마무리하면 남아있는 문제들이 있는데 공격을 시작하면 다음 추격까지 방향을 바꾸지 않는 문제와 플레이어가 쓰러져도 NPC가 계속 공격하는 문제이다.

Detect 서비스를 컴포짓에 부착시키면 타겟이 감지되지 않으면 타겟 값을 null로 변환시켜 정찰하는 행동을 하게된다.

다른 문제는 Target으로 회전하는 태스크를 추가해서 해결한다.

<BTTask_TurnToTarget.h>
public:
	UBTTask_TurnToTarget();

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

<BTTask_TurnToTarget.cpp>
    
UBTTask_TurnToTarget::UBTTask_TurnToTarget()
{
	NodeName = TEXT("Turn");
}

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

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

	APawn* TargetPawn = Cast<APawn>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(BBKEY_TARGET));
	if (nullptr == TargetPawn)
	{
		return EBTNodeResult::Failed;
	}


	IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
	if (nullptr == AIPawn)
	{
		return EBTNodeResult::Failed;
	}

	float TurnSpeed = AIPawn->GetAITurnSpeed();
	FVector LookVector = TargetPawn->GetActorLocation() - ControllingPawn->GetActorLocation();
	LookVector.Z = 0.0f;
	FRotator TargetRot = FRotationMatrix::MakeFromX(LookVector).Rotator();
	ControllingPawn->SetActorRotation(FMath::RInterpTo(ControllingPawn->GetActorRotation(), TargetRot, GetWorld()->GetDeltaSeconds(),TurnSpeed));

	return EBTNodeResult::Succeeded;
}

<ABCharacterNonPlayer.cpp>
float AABCharacterNonPlayer::GetAITurnSpeed()
{
	return 2.0f;
}

PawnTarget을 바라보는 방향을 구해서 NPC의 턴 속도값을 가져와서 RInterpTo()함수로 부드럽게 회전하도록 한다.

공격후 회전하는 것이 아닌 공격과 회전이 동시에 이루어져야하기 때문에 기존의 Selector대신
Simple Parallel을 이용해서 동시에 태스크 두개가 동작하도록 한다.

강의에서는 여기까지 설명했지만 NPC가 공격하면서 캐릭터 방향으로 계속 회전하는게 게임마다 다르겠지만 나는 부자연스럽다는 생각이 들어서 이 부분은 나중에 수정해봐야겠다.

profile
한번 해보자

0개의 댓글