본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 2 - 행동 트리 모델의 구현 (12/15)

728x90
반응형

 

 

 

  • 정리
    • 행동 트리 모델의 구현
      1. 블랙보드의 설정
      2. 내비게이션 메시의 설정
      3. 인터페이스를 활용한 AI와 캐릭터 간의 분리 설계
      4. 일반 테스크와 지연 테스크의 제작과 활용
      5. 서비스와 데코레이터의 제작과 활용

 

 

행동 트리 모델의 구현 예시

 

  • 각각의 행동 및 액션을 언리얼 엔진에서는 테스크(Task)라고 한다.
  • 이 것을 구현하기 위해서는 이동할 목적지에 대한 데이터를 어딘가에 저장을 해둬야 하는데, 이 것을 위해 언리얼 엔진은 블랙보드라는 것을 제공하고 있다.

 

Add>Volumes>NavMeshBoundsVolume

 

NaviMesh가 구축이 되고 Viewport에서 P키를 입력하면 이렇게 녹색으로 길찾기 영역이 표시 된다.
Edit>Project Settings>Engine>Navigation Mesh

 

  • 동적으로 계속해서 생성되는 맵을 가졌기 때문에 Navigation Mesh에 대한 속성을 편집해줄 필요가 있다.
  • 기본적으로 Navigation MeshRuntime GenerationStatic으로 설정되어 있는데, 이를 Dynamic으로 변경해준다.
  • 새롭게 생성되는 섹션에 대해서도 NaviMesh를 사용할 수 있게 된다.

 

Add C++ Class>BTTaskNode
BTTaskNode를 상속받은 클래스를 생성하려고 하니 에러가 발생함
ArenaBattle.Build.cs에서 다음과 같은 모듈들을 추가해줘야 한다.

 

#pragma once

#define BBKEY_HOMEPOS TEXT("HomePos")
#define BBKEY_PATROLPOS TEXT("PatrolPos")
#define BBKEY_TARGET TEXT("Target")
  • ABAI.h

 

  • 블랙보드에서 사용할 변수들을 전처리문에서 define해주어 사용하는게 편리하다.
#pragma once

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

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTTask_FindPatrolPos : public UBTTaskNode
{
	GENERATED_BODY()
	
public:
	UBTTask_FindPatrolPos();

	// BTTaskNode에 있는 ExecuteTask 오버라이드
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
  • BTTask_FindPatrolPos.h
#include "AI/BTTask_FindPatrolPos.h"
#include "ABAI.h"
#include "AI/ABAIController.h"
#include "NavigationSystem.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{

}

// BTTaskNode에 있는 ExecuteTask 오버라이드
EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	// BehaviorTree를 소유한 컴포넌트의 Owner를 GetAIOwner로 가져올 수 있다. 이는 AI 컨트롤러를 상속받은 클래스의 인스턴스가 될 것
	// 이것이 어떤 폰을 빙의 하고 있다면, GetPawn을 통해서 가져올 수 있다.
	APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn)
	{
		return EBTNodeResult::Failed;
	}
	
	// NavigationSystem 가져오기
	// GetNavigationSystem에 월드값 넣어주기. ControllingPawn이 위치한 곳의 월드
	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(ControllingPawn->GetWorld());
	if (nullptr == NavSystem)
	{
		return EBTNodeResult::Failed;
	}

	// 블랙보드 값 가져오기
	// Origin은 블랙보드의 HomePos값을 가져오기
	FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(BBKEY_HOMEPOS);
	FNavLocation NextPatrolPos;

	// NavigationSystem에서 랜덤하게 Point 값을 가져오는 것 (시작위치, 반경, PointLocation을 저장해줄 변수)
	if (NavSystem->GetRandomPointInNavigableRadius(Origin, 500.0f, NextPatrolPos))
	{
		// 블랙보드의 PatrolPos를 NextPatrolPos.Location값으로 넘겨주기
		// 그러면 성공
		OwnerComp.GetBlackboardComponent()->SetValueAsVector(BBKEY_PATROLPOS, NextPatrolPos.Location);
		return EBTNodeResult::Succeeded;
	}

	return EBTNodeResult::Failed;
}
  • BTTask_FindPatrolPos.cpp

 

방금 C++ 클래스로 만든 BTTask_FindPatrolPos라는 Task를 BehaviorTree에서 사용할 수 있는 걸 볼 수 있다.

 

 

기본적인 정찰하는 AI를 만들게 되었다!
  • NPC3s +-1s의 Wait 행동을 한 뒤, NaviMesh가 유효한 범위내에서의 Origin(NPC Pawn의 Location)에서 반경 500.0fRadius내에서 랜덤하게 PatrolPos를 설정하여 MoveTo 행동을 하게 된다.

 

  • 하드코딩으로 BehaviorTree에 필요한 데이터들을 사용하는게 아닌 Interface를 통해서 NPC에서부터 간접적으로 필요한 값을 얻어오도록 구조를 변경해보자.

 

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ABCharacterAIInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UABCharacterAIInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class ARENABATTLE_API IABCharacterAIInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	// NPC가 의무적으로 가져야 될 기본적인 데이터들을 얻어올 수 있도록 함수들 선언
	virtual float GetAIPatrolRadius() = 0;
	virtual float GetAIDetectRange() = 0;
	virtual float GetAIAttackRange() = 0;
	virtual float GetAITurnSpeed() = 0;
};
  • ABCharacterAIInterface.h

 

  • NPC가 의무적으로 가져야 될 기본적인 데이터들을 얻어올 수 있도록 함수를 선언해준다.
// ...

#include "Interface/ABCharacterAIInterface.h"		// AIInterface
#include "ABCharacterNonPlayer.generated.h"

// Config 폴더의 DefaultArenaBattle.ini를 사용해서 불러들이겠다는 의미
UCLASS(config=ArenaBattle)
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase, public IABCharacterAIInterface
{
	GENERATED_BODY()
	// ...

// AI Section
protected:
	virtual float GetAIPatrolRadius() override;
	virtual float GetAIDetectRange() override;
	virtual float GetAIAttackRange() override;
	virtual float GetAITurnSpeed() override;
};
  • ABCharacterNonPlayer.h
// ABCharacterAIInterface 부분
float AABCharacterNonPlayer::GetAIPatrolRadius()
{
	return 500.0f;
}

float AABCharacterNonPlayer::GetAIDetectRange()
{
	return 0.0f;
}

float AABCharacterNonPlayer::GetAIAttackRange()
{
	return 0.0f;
}

float AABCharacterNonPlayer::GetAITurnSpeed()
{
	return 0.0f;
}
  • ABCharacterNonPlayer.cpp

 

  • ABCharacterAIInterface를 상속받아서 함수들을 오버라이드하고 NPC를 통해 값을 가져올 수 있도록 구성해준다.
// ...

#include "Interface/ABCharacterAIInterface.h"

// BTTaskNode에 있는 ExecuteTask 오버라이드
EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	// .

	// NPC를 통해서 필요한 데이터를 받아오기 위함
	IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
	if (nullptr == AIPawn)
	{
		return EBTNodeResult::Failed;
	}

	// 블랙보드 값 가져오기
	// Origin은 블랙보드의 HomePos값을 가져오기
	FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(BBKEY_HOMEPOS);
	float PatrolRadius = AIPawn->GetAIPatrolRadius();
	FNavLocation NextPatrolPos;

	// NavigationSystem에서 랜덤하게 Point 값을 가져오는 것 (시작위치, 반경, PointLocation을 저장해줄 변수)
	// 데이터들을 하드 코딩이 아닌 NPC로부터 받아오도록 변경 (500.0f -> PatrolRadius)
	if (NavSystem->GetRandomPointInNavigableRadius(Origin, PatrolRadius, NextPatrolPos))
	{
		// 블랙보드의 PatrolPos를 NextPatrolPos.Location값으로 넘겨주기
		// 그러면 성공
		OwnerComp.GetBlackboardComponent()->SetValueAsVector(BBKEY_PATROLPOS, NextPatrolPos.Location);
		return EBTNodeResult::Succeeded;
	}

	return EBTNodeResult::Failed;
}
  • BTTask_FindPatrolPos.cpp

 

  • Interface를 통해서 AIPawn이라는 변수를 만들어 NPC를 통해 데이터를 받아오도록 캐스팅해준다.
  • 이후, 하드 코딩으로 500.0f으로 Radius를 설정한 부분을 PatrolRadius로 변경해준다.

 

  • 현재의 BehaviorTree에서 플레이어를 감지하는 기능을 추가해준다.
  • 감지하는 기능은 NPC가 상시적/언제든지 진행할 수 있어야하기 때문에 정찰중에 항상 진행할 수 있도록 서비스 노드를 추가해 주는 것이 좋다.
    • 서비스 노드를 C++ 클래스로 추가해보도록 하자.
      • BTService라는 클래스가 있다.
      • BTService 클래스는 생성자에서 지정된 Interval 마다 Tick으로 행동을 체크한다.

 

#pragma once

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

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTService_Detect : public UBTService
{
	GENERATED_BODY()
	
public:
	UBTService_Detect();

protected:
	// Tick노드
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
  • BTService_Detect.h
#include "AI/BTService_Detect.h"
#include "ABAI.h"
#include "AIController.h"
#include "Interface/ABCharacterAIInterface.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Physics/ABCollision.h"
#include "DrawDebugHelpers.h"

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);

	// OwnerComp를 사용해서 Pawn 가져오기
	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();

	// 감지되는 플레이어가 다수라고 가정했을때 MultiByChannel, 결과값은 TArray로 들어오게된다.
	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)
		{
			// Pawn을 가져와서 플레이어인지 확인하기
			APawn* Pawn = Cast<APawn>(OverlapResult.GetActor());
			if (Pawn && Pawn->GetController()->IsPlayerController())
			{
				// 플레이어라면 해당 Pawn을 Target으로 설정하고 DrawDebug를 하도록
				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);
}
  • BTService_Detect.cpp

 

BehaviorTree의 Sequence에 Add Service를 하면 방금 만든 서비스 C++ 클래스의 Detect 노드가 보이게 된다.
Detect 서비스 노드를 붙여준다.

 

 

 

정찰하면서 플레이어를 감지하는 기능이 추가되었다.
  • 플레이어가 감지되었을 경우 쫓아가서 공격하는 기능을 추가하도록 하자.

 

기존 행동 트리에 Selector와 언리얼 기본 Decorator인 BlackBoard를 사용

  • Selector와 언리얼 기본 DecoratorBlackBoard를 사용하여 Target이 설정되어 있을 때에만 왼쪽 셀렉터 컴포짓이 수행되도록 하고, Target이 설정되어 있지 않으면 오른쪽 시퀀스 컴포짓이 수행되도록 해준다.
  • 또한, 오른쪽 컴포짓이 진행되는 동안에 Target을 감지하게 되면 현재 진행하고 있는 컴포짓을 중단하고 바로 Target을 쫓아갈 수 있도록 Flow Control>Notify ObserverObserver aborts를 설정해준다.
    • Notify Observer - On Value Change
      • 값이 변경될 때
    • Observer aborts - Self
      • 현재 진행하고 있는 컴포짓 중단

 

 

  • 공격을 해야하는지, 아니면 쫓아가야 되는지에 대한 판단을 수행하기 위해서 BTDecorator 노드를 사용해서 Target이 공격범위 내에 있는지에 대해 확인하는 노드 추가하기 (BTDecorator 클래스 생성)
  • 범위 내에 있다면 공격하는 태스크 추가 (BTTaskNode 클래스 생성)
  • 범위 내에 있는지 판단하기 위한 UBTDecorator_AttackInRange 추가 (BTDecorator 클래스 생성)

 

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTDecorator.h"
#include "BTDecorator_AttackInRange.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTDecorator_AttackInRange : public UBTDecorator
{
	GENERATED_BODY()
	
public:
	UBTDecorator_AttackInRange();
	
protected:
	// Decorator의 경우 CalculateRawConditionValue라는 함수를 구현 해줘야 한다.
	virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
};
  • BTDecorator_AttackInRange.h
// Fill out your copyright notice in the Description page of Project Settings.


#include "AI/BTDecorator_AttackInRange.h"
#include "ABAI.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Interface/ABCharacterAIInterface.h"

UBTDecorator_AttackInRange::UBTDecorator_AttackInRange()
{
	// Decorator 이름
	NodeName = TEXT("CanAttack");
}

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

	// Pawn을 가져오기
	APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn)
	{
		return false;
	}

	// Pawn으로부터 Interface 가져오기
	IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
	if (nullptr == AIPawn)
	{
		return false;
	}

	// Target값을 가져오기
	APawn* Target = Cast<APawn>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(BBKEY_TARGET));
	if (nullptr == Target)
	{
		return false;
	}

	// Target까지의 거리를 구하기
	// AttackRange안에 Target이 있다면 true
	float DistanceToTarget = ControllingPawn->GetDistanceTo(Target);
	float AttackRangeWithRadius = AIPawn->GetAIAttackRange();
	bResult = (DistanceToTarget <= AttackRangeWithRadius);

	return bResult;
}
  • BTDecorator_AttackInRange.cpp

 

  • 다른 BehaviorTree와 비슷하게 Pawn을 가져오고, Interface를 가져와준다.
  • Target(Player)이 감지와 더불어 공격범위내에 존재하는지 파악하기 위해 Target값을 가져와준다.
  • ABCharacterAIInterface에서 AttackRange에 대한 데이터를 가져와 주고, GetDistanceTo함수를 통해 Target까지의 거리를 불러와 비교하여 bool값으로 리턴해준다.

 

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ABCharacterAIInterface.generated.h"

// 공격이 끝났는지를 알려주기위한 델리게이트
DECLARE_DELEGATE(FAICharacterAttackFinished);

class ARENABATTLE_API IABCharacterAIInterface
{
	// ...

	// 캐릭터(NPC)에게 넘겨주기 위한 델리게이트 함수
	virtual void SetAIAttackDelegate(const FAICharacterAttackFinished& InOnAttackFinished) = 0;

	// AI의 공격을 구현
	// 공격의 경우에는 바로 끝나는 액션이 아니다. 공격 시작 -> 몽타주 재생 -> 몽타주가 끝나야지만 공격이 끝났다라고 할 수 있다.
	virtual void AttackByAI() = 0;
};
  • IABCharacterAIInterface.h
#pragma once

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

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UBTTask_Attack : public UBTTaskNode
{
	GENERATED_BODY()
	
public:
	UBTTask_Attack();

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
  • BTTask_Attack.h
#include "AI/BTTask_Attack.h"
#include "AIController.h"
#include "Interface/ABCharacterAIInterface.h"

UBTTask_Attack::UBTTask_Attack()
{

}

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

	// AIController가 가진 Pawn을 획득
	APawn* ControllingPawn = Cast<APawn>(OwnerComp.GetAIOwner()->GetPawn());
	if (nullptr == ControllingPawn)
	{
		return EBTNodeResult::Failed;
	}

	// Interface를 통해 공격 명령
	IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
	if (nullptr == AIPawn)
	{
		return EBTNodeResult::Failed;
	}

	// 공격이 시작하기 전 델리게이트를 바인딩해준다.
	// 지연 테스크
	FAICharacterAttackFinished OnAttackFinished;
	OnAttackFinished.BindLambda(
		[&]()
		{
			// 람다식을 통해 Task가 끝나면 Succeeded를 해준다.
			FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
		}
	);
	// 델리게이트 바인딩
	AIPawn->SetAIAttackDelegate(OnAttackFinished);

	// 공격의 경우에는 바로 끝나는 액션이 아니다. 공격 시작 -> 몽타주 재생 -> 몽타주가 끝나야지만 공격이 끝났다라고 할 수 있다.
	// AttackByAI를 통해 공격을 시작해주고 Result는 Inprogress로 설정해준 다음, 공격이 끝난 이후에 Succeded 값으로 반환해주면 된다.
	AIPawn->AttackByAI();
	return EBTNodeResult::InProgress;
}
  • BTTask_Attack.cpp

 

  • 공격의 경우에는 바로 끝나는 액션이 아니다. 공격 시작 -> 몽타주 재생 -> 몽타주가 끝나야지만 공격이 끝났다라고 할 수 있다.
  • AttackByAI를 통해 공격을 시작해주고 ResultInprogress로 설정해준 다음, 공격이 끝난 이후에 Succeded 값으로 반환해주면 된다.
  • ABCharacterAIInterface에서 공격이 끝났는지를 알려주기 위한 델리게이트를 만들어주고 캐릭터에게 넘겨주기 위한 델리게이트 함수와 AI의 공격을 위한 함수들을 만들어 준다.

 

	virtual void NotifyComboActionEnd();		// 몽타주 없이 공격이 끝났는지를 파악하기 위한 함수
  • ABCharacterBase.h
// ...

// 콤보 끝
void AABCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
	// Assertion 함수를 사용해서 몽타주가 끝날땐 CurrentCombo가 절때 0이 될 수 없으니, 검증 + 0이 나오면 에러
	ensure(CurrentCombo != 0);
	CurrentCombo = 0;
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);			// 다시 움직임 가능하게 변경

	// 콤보가 끝났을때 호출
	NotifyComboActionEnd();
}

// 콤보가 끝났을때 호출
void AABCharacterBase::NotifyComboActionEnd()
{

}
 // ...
  • ABCharacterBase.cpp

 

  • 기본 ABCharacterBase 클래스에 NPC의 콤보공격이 끝났을때 호출되는 가상함수를 만들어주고 콤보공격이 끝나게 되면 호출될 수 있도록 구성해준다.
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase, public IABCharacterAIInterface
{
	// ...
	// 델리게이트를 담을 변수
	FAICharacterAttackFinished OnAttackFinished;

	// ABCharacterBase에 있는 콤보가 끝났을때 호출하는 함수 override
	virtual void NotifyComboActionEnd() override;
	};
}
  • ABCharacterNonPlayer.h

 

void AABCharacterNonPlayer::SetAIAttackDelegate(const FAICharacterAttackFinished& InOnAttackFinished)
{
	OnAttackFinished = InOnAttackFinished;
}

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

// 콤보 공격이 끝났을때 해당 함수가 호출됨
void AABCharacterNonPlayer::NotifyComboActionEnd()
{
	Super::NotifyComboActionEnd();

	// 콤보 공격이 끝났을 때 OnAttackFinished 함수 실행
	OnAttackFinished.ExecuteIfBound();
}
  • ABCharacterNonPlayer.cpp

 

  • NPC의 공격이 끝났을때를 알려주기 위한 델리게이트를 담을 변수를 설정해주고, ABCharacterBase에 있는 콤보 공격이 끝났을 때 호출되는 함수인 NotifyComboActionEnd()함수를 override해주며 델리게이트 함수인 OnAttackFinished함수를 실행해준다.
  • 그렇게되면 BTTask_Attack 클래스에서 람다로 바인딩된 함수가 실행되며, ResultSucceded로 변경될 것이다.

 

Attack을 하기위한 AttakRange를 체크하는 Decorator추가, Player를 Detect했을때만 공격하는 서비스 추가

 

  • Target이 존재하면 (Player), Selector를 통해 공격범위내에 있는지 판단하고, 공격 범위에 있다면 Attack, 그렇지 않으면 추격하는 행동 트리를 구성한다.
  • Detect했다면 다시 파악하도록 서비스도 추가해주었다.

 

 

 

Target이 존재하면 추적하고, 공격 범위안에 도달하면 Target을 공격한다.
  • 하지만 이 경우 추적하면서 공격 범위안에 들어오면 어느정도는 맞겠지만, NPC의 공격 범위안에 들어오더라도 해당 방향으로 회전하지는 않을 것이다.

 

다음 상황과 같이 감지도 되고, 공격범위안으로 Target이 있지만 회전하지 못하고 계속해서 공격하는 모습
  • 이를 해결하기위해 Target방향으로 Turn을 하는 Task를 구현해주자. (BTTaskNode 클래스 생성)

 

#pragma once

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

UCLASS()
class ARENABATTLE_API UBTTask_TurnToTarget : public UBTTaskNode
{
	GENERATED_BODY()
	
public:
	UBTTask_TurnToTarget();

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
  • BTTask_TurnToTarget.h
#include "AI/BTTask_TurnToTarget.h"
#include "ABAI.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Interface/ABCharacterAIInterface.h"

UBTTask_TurnToTarget::UBTTask_TurnToTarget()
{
	NodeName = TEXT("Turn");
}

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

	// Pawn 가져오기
	APawn* ControllingPawn = Cast<APawn>(OwnerComp.GetAIOwner()->GetPawn());
	if (nullptr == ControllingPawn)
	{
		return EBTNodeResult::Failed;
	}

	// Target으로 설정된 Pawn 가져오기
	APawn* TargetPawn = Cast<APawn>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(BBKEY_TARGET));
	if (nullptr == TargetPawn)
	{
		return EBTNodeResult::Failed;
	}

	// AIPawn 가져오기
	IABCharacterAIInterface* AIPawn = Cast<IABCharacterAIInterface>(ControllingPawn);
	if (nullptr == AIPawn)
	{
		return EBTNodeResult::Failed;
	}

	// AI TurnSpeed 가져와서 바라봐야하는 Vector를 구한뒤, Pawn(NPC)을 TurnSpeed의 속도로 로테이션 시켜준다.
	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;
}
  • BTTask_TurnToTarget.cpp

Simple Parallel로 동시에 실행할 수 있도록

 

  • 기본적인 생성자와 Pawn(NPC), Target을 가져오는 ExecuteTask를 구성해준 뒤, 회전시켜주는 로직을 구성해준다.
  • BehaviorTree에서 공격 부분은 Selector가 아닌 Simple Parallel 컴포짓으로 변경해준뒤, 두 행동 모두 실행하도록 해준다. 다만, 메인 행동은 Attack으로 동작할 수 있도록 해준다.

 

 

공격을 함과 동시에 회전을 할 수 있게 되었다.

 

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해 | 이득우 - 인프런

이득우 | 또 하나의 언리얼 엔진이라고도 불리는 네트웍 멀티플레이어 프레임웍을 학습합니다. 네트웍 멀티플레이어 게임을 제작할 때 반드시 알아야 하는 주요 개념, 내부 동작 원리, 최적화

www.inflearn.com

 

 

 

 

728x90