초보 코린이의 성장 일지

UE4 Behavior Tree, Patrol 본문

언리얼

UE4 Behavior Tree, Patrol

코오린이 2023. 7. 25. 17:32

이제 Spline을 활용하여 Patrol 기능을 만들어 볼 것이다.

1. Enemy가 타입별 다른 속도로 움직일 수 있도록 하기 위해 Task를 상속받아 클래스를 하나 생성해준다.

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "Components/CMovementComponent.h"
#include "CBTTaskNode_Speed.generated.h"

UCLASS()
class U2212_06_API UCBTTaskNode_Speed : public UBTTaskNode
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere, Category = "Type")
		ESpeedType Type;

public:
	UCBTTaskNode_Speed();

private: // 실행, 시작
	EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;


};
#include "BehaviorTree/CBTTaskNode_Speed.h"
#include "Global.h"
#include "Characters/CEnemy_AI.h"
#include "Characters/CAIController.h"

UCBTTaskNode_Speed::UCBTTaskNode_Speed()
{
	NodeName = "Speed";

}

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

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCMovementComponent* movement = CHelpers::GetComponent<UCMovementComponent>(ai);

	movement->SetSpeed(Type); // 타입에 따라 걷고, 뛰고

	return EBTNodeResult::Succeeded; // 종료 상태

}

1. Speed Type에 따라 움직이게 만들어주는 Task이며, 시작과 종료 시점을 정해주면 된다.

1. 비헤이비어 트리에서 Task Speed를 가져와서 연결해주고, Type를 Sprint로 바꿔주면 Speed가 정해지고 나서 Move To가 호출되면서 빠르게 걸어오게 된다.

1. 실행해서 확인해보면, 일정거리안에 들어오면 Player한테 빠르게 걸어오는걸 볼 수 있다.


이제 Spline를 사용해 볼 것이다.

1. Actor를 상속받아서 클래스를 생성해준다.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CPatrolPath.generated.h"

UCLASS()
class U2212_06_API ACPatrolPath : public AActor
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere, Category = "Loop")
		bool bLoop;

	UPROPERTY(EditAnywhere, Category = "Path")
		int32 Index;

	UPROPERTY(EditAnywhere, Category = "Path")
		bool bReverse;

private:
	UPROPERTY(VisibleDefaultsOnly)
		class USceneComponent* Root;

	UPROPERTY(VisibleDefaultsOnly)
		class USplineComponent* Spline;

	UPROPERTY(VisibleDefaultsOnly)
		class UTextRenderComponent* Text;

public:
	FORCEINLINE class USplineComponent* GetSpline() { return Spline; }

public:	
	ACPatrolPath();

	void OnConstruction(const FTransform& Transform) override;

protected:
	virtual void BeginPlay() override;

public:
	FVector GetMoveTo();
	void UpdateIndex();

};
#include "BehaviorTree/CPatrolPath.h"
#include "Global.h"
#include "Components/SplineComponent.h"
#include "Components/TextRenderComponent.h"

ACPatrolPath::ACPatrolPath()
{
	// 드래그 하던 중에 호출, 드래그하고 놨을때 호출되는지 체크
	bRunConstructionScriptOnDrag = false;

	CHelpers::CreateComponent<USceneComponent>(this, &Root, "Root");
	CHelpers::CreateComponent<USplineComponent>(this, &Spline, "Spline", Root);
	CHelpers::CreateComponent<UTextRenderComponent>(this, &Text, "Text", Root);

	Spline->SetRelativeLocation(FVector(0, 0, 30));
	Spline->bHiddenInGame = false;

	Text->SetRelativeLocation(FVector(0, 0, 120));
	Text->SetRelativeRotation(FRotator(0, 180, 0));
	Text->HorizontalAlignment = EHorizTextAligment::EHTA_Center;
	Text->TextRenderColor = FColor::Red;
	Text->bHiddenInGame = true;

}

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


}

void ACPatrolPath::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

#if WITH_DDITOR
	// 에디터에 있는 함수이므로, WITH_DDITOR 붙여서 사용해야한다.
	Text->Text = FText::FromString(GetActorLabel());
#endif

	Spline->SetClosedLoop(bLoop);

}

FVector ACPatrolPath::GetMoveTo()
{
	// Index를 받아서 return
	return Spline->GetLocationAtSplinePoint(Index, ESplineCoordinateSpace::World);

}

void ACPatrolPath::UpdateIndex()
{
	int32 count = Spline->GetNumberOfSplinePoints();

	if (bReverse) // 역방향
	{
		if (Index > 0)
		{
			Index--;

			return;
		}

		if (Spline->IsClosedLoop()) // 정해진 구역을 루프로 사용할 것인지
		{
			Index = count - 1;

			return;
		}

		// 아니라면 1로 만들어주고, bReverse 뒤집어준다.
		Index = 1;
		bReverse = false;

		return;
	}

	// 정방향
	if (Index < count - 1)
	{
		Index++;

		return;
	}

	if (Spline->IsClosedLoop())
	{
		Index = 0;

		return;
	}

	// 전부 아니라면 -2. 역방향으로 돌려주기
	Index = count - 2;
	bReverse = true;

}

1. Spline 구역을 만들어주고, 그곳을 어떻게 이동할 것인지를 체크 및 Loop로 확인해준다.


1. BP로 클래스를 생성해준다.

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:
	// 에디터에서도 지정할 수 있도록, EditAnywhere 해주기
	UPROPERTY(EditAnywhere, Category = "Patrol")
		class ACPatrolPath* PatrolPath;

public:
	FORCEINLINE class ACPatrolPath* GetPatrolPath() { return PatrolPath; }

};

1. ACEnemy_AI에 접근해서 Patrol 기능을 사용할 수 있도록  ACEnemy_AI로 가서 return 함수를 만들어준다. 


1. 위 사진을 예로 여기서 하나 주의하고 넘어가야할게 있다.

2. 어떠한 위치를 구해서 이동하게 하기 위해 위에서 만든 클래스안에 변수로 위치를 잡아서 계산해 버리면, 만일 2명의 Enemy가 각자의 Spline를 지정받아서 이동하게 만들었는데도 불구하고, Location 값을 변수로 각자 사용하게 되면 마지막으로 들어온 위치값만 받아들여 2명이 한곳으로 움직이게된다. 

3. 그렇기 때문에 Location을 사용하고 싶으면, 각자 사용하는 블랙보드가 다르기 때문에, 블랙보드에 직접 넣어서 사용해야한다. 


 

1. 블랙보드에 사용하게될 Vector 변수를 만들어준다.


#pragma once

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

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

private:
	UPROPERTY(EditAnywhere, Category = "Key")
		FName PatrolLocationKey = "Patrol_Location";

public:
	FVector GetPatrolLocation();
	void SetPatrolLocation(const FVector& InLocation);
    
};
#include "Components/CAIBehaviorComponent.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "BehaviorTree/BlackboardComponent.h"

FVector UCAIBehaviorComponent::GetPatrolLocation()
{
	return Blackboard->GetValueAsVector(PatrolLocationKey);
}

void UCAIBehaviorComponent::SetPatrolLocation(const FVector& InLocation)
{
	Blackboard->SetValueAsVector(PatrolLocationKey, InLocation);
}

1. 블랙보드에서 Key를 만들어 놓은 "Patrol_Location"을 함수로 만들어준다.


1. Spline를 따라서 움직일 수 있게 명령을 내려줄 Task를 하나 생성해준다.

#pragma once

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

UCLASS()
class U2212_06_API UCBTTaskNode_Patrol : public UBTTaskNode
{
	GENERATED_BODY()

private:
    UPROPERTY(EditAnywhere, Category = "Patrol")
        bool bDebugMode; // 디버깅 모드 on, off 선택

    UPROPERTY(EditAnywhere, Category = "Patrol")
        float AcceptanceDistance = 20; // 일정 거리에 들어오면 도달했다고 간주,

    UPROPERTY(EditAnywhere, Category = "Random")
        float RandomRadius = 1500; // 순찰 경로 없을때 랜덤 반경 범위

public:
    UCBTTaskNode_Patrol();

protected:
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

};
#include "BehaviorTree/CBTTaskNode_Patrol.h"
#include "Global.h"
#include "CPatrolPath.h"
#include "Components/SplineComponent.h"
#include "Components/CAIBehaviorComponent.h"
#include "Characters/CEnemy_AI.h"
#include "Characters/CAIController.h"
#include "NavigationSystem.h"

UCBTTaskNode_Patrol::UCBTTaskNode_Patrol()
{
	NodeName = "Patrol";

	bNotifyTick = true; // Tick 반드시 켜줘야 실행된다.
}

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

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCAIBehaviorComponent* behavior = CHelpers::GetComponent<UCAIBehaviorComponent>(ai);

	// PatrolPath가 있을때
	if (!!ai->GetPatrolPath())
	{
		FVector moveToPoint = ai->GetPatrolPath()->GetMoveTo();
		behavior->SetPatrolLocation(moveToPoint);

		if (bDebugMode)
			DrawDebugSphere(ai->GetWorld(), moveToPoint, 25, 25, FColor::Green, true, 5);

		return EBTNodeResult::InProgress;
	}

	FVector location = ai->GetActorLocation();

	// Task를 보유한 AI가 사용하고 있는 네비게이션 Mesh를 구하기
	UNavigationSystemV1* navSystem =  FNavigationSystem::GetCurrent<UNavigationSystemV1>(ai->GetWorld());
	CheckNullResult(navSystem, EBTNodeResult::Failed); // 존재하지 않는다면, Failed

	FNavLocation point(location); // 초기값을 현재 위치로 정해주기.
	while (true)
	{
		if (navSystem->GetRandomPointInNavigableRadius(location, RandomRadius, point))
			break;

	}

	behavior->SetPatrolLocation(point.Location);

	if (bDebugMode)
		DrawDebugSphere(ai->GetWorld(), point.Location, 25, 25, FColor::Green, true, 5);

	return EBTNodeResult::InProgress; // 대기
}

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

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCAIBehaviorComponent* behavior = CHelpers::GetComponent<UCAIBehaviorComponent>(ai);

	FVector location = behavior->GetPatrolLocation();
	EPathFollowingRequestResult::Type result = controller->MoveToLocation(location, AcceptanceDistance, false);

	switch (result)
	{
		case EPathFollowingRequestResult::Failed: // 못가는 곳이면,
		{
			// FinishLatentTask로 종료 시키기
			FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
		}
		break;
		
		case EPathFollowingRequestResult::AlreadyAtGoal: // 도달 했다면,
		{
			if (ai->GetPatrolPath()) // 순찰중이라면,
				ai->GetPatrolPath()->UpdateIndex(); // 순찰 다음 위치 구해주기.

			// 위에 조건이 아니라면 끝내고, 위에 다시 올라가서 조건 찾기
			FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
		}
		break;
	}
	

}

1. 순찰 경로를 돌다가 일정 거리안에 들어온다면, Player를 따라가도록 만들어준다.

2. 그게 아니라면 랜덤한 범위안에 계속 Patrol하도록 만들어준다.


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");
        PublicDependencyModuleNames.Add("NavigationSystem");

    }
}

1. 컴파일을 해보면 링크에러가 발생한다 빌드를 추가해준다.

2. "NavigationSystem" 추가.


Patrol 설정
Approach 설정

1. 우선 전체적인 노드 상태.

1. Debug Mode를 켜주고 확인해본다.

1. Emeny 2명은 Spline를 따라 도는걸 확인할 수 있으며, 중앙에 Enemy는 랜덤으로 돌고있다.

1. 블랙보드 Key에 Patrol_Loaction에 있는 Instance Synced를 체크해준다.

2. 그렇게되면 다 같은 위치로 가게된다.

3. 이유로는 같은 블랙보드를 사용하고 있는 객체들은 같은 값들을 공유하므로, 이동 또한 공유하게 된다.


https://www.youtube.com/watch?v=6BDhMDRVGPc 

 

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

UE4 Behavior, AI Action  (2) 2023.07.27
UE4 Behavior, AI Equip  (0) 2023.07.26
UE4 Behavior Tree  (0) 2023.07.24
UE4 Feet IK, Behavior, HealBar  (0) 2023.07.21
UE4 Parkour Final, Feet IK  (0) 2023.07.20
Comments