초보 코린이의 성장 일지

UE4 Behavior Tree 본문

언리얼

UE4 Behavior Tree

코오린이 2023. 7. 24. 18:12

Enemy가 Player를 인식할 수 있도록 만들어 볼 것이다.

1. Enemy가 무기를 들고 있도록 만들어 주기 위해 데이터 에셋을 하나 추가해준다.

2. 실행해보면 등록해준 Sword를 장착하고 있는 모습을 볼 수 있다.

1. 우선 비헤이비어 트리를 생성해주고, Wait를 하나 연결해준다.

2. 아무것도 연결이 안되어있으면 동작하는지 모르기 때문에 동작하는지 확인하기 위한 용도.

1. 만들어준 비헤이비어 트리를 선택해준다.


#pragma once

#include "CoreMinimal.h"
#include "Characters/CEnemy.h"
#include "CEnemy_AI.generated.h"

UCLASS()
class U2212_06_API ACEnemy_AI : public ACEnemy
{
	GENERATED_BODY()

private:
	UPROPERTY(EditDefaultsOnly, Category = "AI")
	// Behavior를 바꿔서 사용할 수 도 있기 때문에, 이 위치에 선언해놓고 사용
		class UBehaviorTree* BehaviorTree;

	UPROPERTY(EditDefaultsOnly, Category = "AI")
		uint8 TeamID = 2; // 0 ~ 255번까지 팀을 구별할수 있으며, 255은 중립으로 설정된다.

public:
	FORCEINLINE uint8 GetTeamID() { return TeamID; }
	FORCEINLINE class UBehaviorTree* GetBehaviorTree() { return BehaviorTree; }

};

1. TeamID와 Behavior return를 만들어서 넘겨준다.



1. 사용하게될 컨트롤러를 하나 생성해준다.

1. AI가 동작하기 위해선 특정한 함수가 필요하다. 그걸 찾기 위해 부모로 가서 가상함수를 사용할 것이다.

2. 이 두가지를 가져온다. (Possess = 빙의하다) 빙의를 해야만 움직일 수 있게된다.

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "CAIController.generated.h"

UCLASS()
class U2212_06_API ACAIController : public AAIController
{
	GENERATED_BODY()

public:
	ACAIController();

protected:
	virtual void BeginPlay() override;

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

private:
	class ACEnemy_AI* Enemy;
	class UCAIBehaviorComponent* Behavior;

};
#include "Characters/CAIController.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/BehaviorTree.h"
#include "Characters/CEnemy_AI.h"
#include "Components/CAIBehaviorComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"

ACAIController::ACAIController()
{
	CHelpers::CreateActorComponent<UBlackboardComponent>(this, &Blackboard, "Blackboard");
	CHelpers::CreateActorComponent<UAIPerceptionComponent>(this, &Perception, "Perception");

	// CreateDefaultSubobject은 동적할당 할때 사용, NewObject는 런타임때 사용
	Sight = CreateDefaultSubobject<UAISenseConfig_Sight>("Sight"); 
	Sight->SightRadius = 600; // 감지 거리
	Sight->LoseSightRadius = 800; // 잃는 거리
	Sight->PeripheralVisionAngleDegrees = 45; // 시야각
	Sight->SetMaxAge(2); // 수명시간

	Sight->DetectionByAffiliation.bDetectEnemies = true; // 적을 감지하겠다.
	Sight->DetectionByAffiliation.bDetectNeutrals = false; // 중립은 감지 x
	Sight->DetectionByAffiliation.bDetectFriendlies = false; // 아군도 현재는 감지x로 설정

	Perception->ConfigureSense(*Sight); // 감지객체 등록, 주소로 받는 이유는 여러개 사용할때 배열로 사용하면된다.
	// SetDominantSense은 우선순위 등록, Sense return 해주는 함수
	Perception->SetDominantSense(*Sight->GetSenseImplementation());

}

void ACAIController::BeginPlay()
{
	Super::BeginPlay();

	Perception->OnPerceptionUpdated.AddDynamic(this, &ACAIController::OnPerceptionUpdated);
}

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

	Enemy = Cast<ACEnemy_AI>(InPawn); // 캐스팅해서 인식시켜 놓는다.
	SetGenericTeamId(Enemy->GetTeamID()); // BP에서는 사용이 불가능한 아군, 적군 나누기 가능

	CheckNull(Enemy->GetBehaviorTree());
	// 블랙보드를 사용, 실제 사용할 블랙보드 지정
	UseBlackboard(Enemy->GetBehaviorTree()->BlackboardAsset, Blackboard);

	Behavior = CHelpers::GetComponent<UCAIBehaviorComponent>(Enemy); // Enemy에서 찾아오기
	Behavior->SetBlackboard(Blackboard); 

	RunBehaviorTree(Enemy->GetBehaviorTree()); // 블랙보드 실행
}
 
void ACAIController::OnUnPossess()
{
	Super::OnUnPossess();

}

1. 팀을 지정해주고, 비헤이비어를 찾아서 실행시켜준다.


 

1. 만들어준 블랙보드를 비헤이비어 트리에 할당해준다.


1. 위에 만들어놓은 AI_Controller을 상속받아 클래스를 하나 생성해준다.


1. 만들어준 AI_Controller을 선택해준다.

1. 실행해보면, 이름을 출력해주는 위젯에 설정해준 AI_Controller이 위에 나오는걸 볼 수 있다.

1. 이제 실행하면 비헤이비어 트리까지 돌아가는걸 확인할 수 있다.


1. Player로가서 IGenericTeamAgentInterface를 상속받아 준다.

2. Team을 Player도 사용할 것이기 때문

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Components/CStateComponent.h"
#include "Characters/ICharacter.h"
#include "Parkour/CParkourComponent.h"	
#include "GenericTeamAgentInterface.h"
#include "CPlayer.generated.h"

UCLASS()
class U2212_06_API ACPlayer 
	: public ACharacter
	, public IICharacter
	, public IGenericTeamAgentInterface
{
	GENERATED_BODY()

private:
	UPROPERTY(EditDefaultsOnly, Category = "Team")
		uint8 TeamID = 1;
        
public:
	FGenericTeamId GetGenericTeamId() const override { return FGenericTeamId(TeamID); }
};

1. Enemy가 2번으로 지정되어 있기 때문에, Player는 1번으로 지정해준다.

2. 부모에 있는 함수  GetGenericTeamId를 오버라이딩 해준다.


#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "CAIController.generated.h"

UCLASS()
class U2212_06_API ACAIController : public AAIController
{
	GENERATED_BODY()

private:
	UPROPERTY(VisibleAnywhere)
		class UAIPerceptionComponent* Perception;

protected:
	virtual void BeginPlay() override;
    
private:
	UFUNCTION()
		void OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors);
private:
	class UAISenseConfig_Sight* Sight; // 감지
};
#include "Characters/CAIController.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/BehaviorTree.h"
#include "Characters/CEnemy_AI.h"
#include "Components/CAIBehaviorComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"

void ACAIController::BeginPlay()
{
	Super::BeginPlay();

	// Perception 업데이트,
	Perception->OnPerceptionUpdated.AddDynamic(this, &ACAIController::OnPerceptionUpdated);
}

void ACAIController::OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors)
{
	TArray<AActor*> actors;

	// 어떤 감지객체로 감지되었는지, 감지될 객체 return 
	Perception->GetCurrentlyPerceivedActors(nullptr, actors);

	// 하나 이상 감지되었다면,
	if (actors.Num() > 0)
	{
		CLog::Log(actors[0]->GetName());

		return;
	}
}

 

1. Perception을 사용해서 Sight 감지 시키게 만들어준다.

2. 로그로 우선 객체를 감지했는지 확인해 본다.

1. Detect Neutrals를 체크해준다.

2. Player가 우선 팀번호를 선택해주지 않아 중립으로 지정되어 있다. Enemy가 감지할수 있도록 설정해준다.

1. 실행해서 확인해보면, Enemy가 범위안에 들어온 Player를 감지하게되고, 왼쪽 출력 로그에도 누구를 감지했는지 출력되는걸 볼 수 있다.


1. 출력로그 쪽에 오류 문구가 발생했는지 데이터 테이블이 없어서 뜨게된 것이다. Enemy_Melee로가서 데이터 테이블을 등록해준다.


#include "Characters/CAIController.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/BehaviorTree.h"
#include "Characters/CEnemy_AI.h"
#include "Components/CAIBehaviorComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"

void ACAIController::OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors)
{
	TArray<AActor*> actors;

	// 어떤 감지객체로 감지되었는지, 감지될 객체 return 
	Perception->GetCurrentlyPerceivedActors(nullptr, actors);

	// 하나 이상 감지되었다면,
	if (actors.Num() > 0)
	{
		//CLog::Log(actors[0]->GetName());
		Blackboard->SetValueAsObject("Target", actors[0]); // 타겟으로 Object 등록

		return;
	}

	// 감지 안되었다면 null.
	Blackboard->SetValueAsObject("Target", nullptr);

}

1. 로그로 Player가 인식되는걸 확인해 봤으니, 주석처리 해준다.

2. Object로 Player를 감지할 수 있도록 Target로 등록해준다.


#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CAIBehaviorComponent.generated.h"

UENUM(BlueprintType)

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class U2212_06_API UCAIBehaviorComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	class ACharacter* GetTarget();

};
#include "Components/CAIBehaviorComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "BehaviorTree/BlackboardComponent.h"

ACharacter* UCAIBehaviorComponent::GetTarget()
{
	return Cast<ACharacter>(Blackboard->GetValueAsObject(TargetKey));

}

1. 쓸 타입으로 태스팅해서, Target를 알 수 있게 등록해준다.


1. 블랙보드로 가서 키를 등록해준다. 사용하게될 Character로 지정해준다.

1. Enum으로도 키를 하나 등록해준다.

2. Enum은 등록 방법이 카테고리에서 타입을 선택해주는게 아니다.

3. C++에서는 위에 보이는 CAIBehaviorComponent에 enum class EAIStateType으로 등록해준 Enum을 그대로 동일하게 입력 해줘야하며, 입력하고 엔터를 치는 순간 자연스럽게 Enum Type에 C++에 등록해준 Enum이 저절로 입력된다.

4. 엔터를 눌렀을때 Type가 안들어 온다는건 오타를 냈다는 뜻이거나 등록이 안된것이다.

5. 또한 앞에 이름에 접두사 E_AIState 이렇게 써버리면 언리얼에서 특수문자로 인식해서 잘못된 방법이며, 뒤에 _바를 붙이는건 상관이없다.


1. Service에서 계속 Updete하면서 정해진 Target를 감지하게 만들어 주기위해 클래스를 생성해준다.

#pragma once

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

UCLASS()
class U2212_06_API UCBTService_Melee : public UBTService
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere, Category = "Action")
		float ActionRange = 150; // 공격 범위

public:
	UCBTService_Melee();

protected:
	void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

};
#include "BehaviorTree/CBTService_Melee.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "Characters/CAIController.h"
#include "Characters/CEnemy_AI.h"
#include "Components/CStateComponent.h"
#include "Components/CAIBehaviorComponent.h"

UCBTService_Melee::UCBTService_Melee()
{
	NodeName = "Melee";

	Interval = 0.1f; // Tick 호출 간격
	RandomDeviation = 0.0f; // 

}

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

	// 실행에 주체는 ACAIController가 주축이 된다.
	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);
	UCAIBehaviorComponent* aiState = CHelpers::GetComponent<UCAIBehaviorComponent>(ai);

	// Hitted 되었을때,
	if (state->IsHittedMode())
	{
		aiState->SetHittedMode();

		return;
	}

	ACharacter* target = aiState->GetTarget();

	if (target == nullptr)
	{
		// 타겟이 없으면, Patrol로 타겟을 찾게 된다
		aiState->SetPatrolMode();

		return;
	}

	// 범위 안에 들어왓다면,
	float distance = ai->GetDistanceTo(target);
	if (distance < ActionRange)
	{
		// 공격 모드
		aiState->SetActionMode();

		return;
	}

	// 공격모드가 아니라면 추격 모드.
	aiState->SetApproachMode();

}

1. 블랙보드에 등록된 Target Key에 관하여 어떠한 동작을 취할 것인지 설정해준다.


using UnrealBuildTool;

public class U2212_06 : ModuleRules
{
	public U2212_06(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicIncludePaths.Add(ModuleDirectory);


        PublicDependencyModuleNames.Add("Core");

        PrivateDependencyModuleNames.Add("CoreUObject");
        PrivateDependencyModuleNames.Add("Engine");
        PrivateDependencyModuleNames.Add("InputCore");
        PrivateDependencyModuleNames.Add("Niagara");

        PrivateDependencyModuleNames.Add("AIModule");
        PublicDependencyModuleNames.Add("GameplayTasks");
    }
}

1. 컴파일해보면 모듈 오류가 뜨므로, GamePlayTasks를 등록해준다.


 

1. Approach에서 Key와 Blackboard를 선택해준다.

2. MoveTo에서 Blackboard Key를 Target로 등록해준다.


1. 네비게이션 메시 범위에 Enemy가 들어가있고, 이제 Player이 범위안에 들어가면 감지하며 쫒아오게 된다.

1. Player에게 다가오게 된다.


https://www.youtube.com/watch?v=DirV-eMK7Ss 

 

'언리얼' 카테고리의 다른 글

UE4 Behavior, AI Equip  (0) 2023.07.26
UE4 Behavior Tree, Patrol  (0) 2023.07.25
UE4 Feet IK, Behavior, HealBar  (0) 2023.07.21
UE4 Parkour Final, Feet IK  (0) 2023.07.20
UE4 Parkour Climb  (0) 2023.07.19
Comments