Unreal - Behavior Tree(2) 플레이어 따라가기

2025. 4. 29. 16:28·Unreal5 프로젝트 다이어리

https://lucodev.tistory.com/41

 

Unreal - 비헤이비어 트리(Behavior Tree) - AI Perception

플레이어를 상대하는 인공지능 적 AI를 생성해보겠습니다언리얼에서 AI는 크게 Behavior Tree방식과 FSM 방식 두가지가 있습니다 저는 이번에는 Behavior Tree방식의 AI를 구성해보도록하겠습니다Behavior

lucodev.tistory.com

저번의 AI - Perception의 기본설정과 이어지는 내용입니다

 

 

AI가 플레이어를 향해 다가오게 해보겠습니다

 

감지한 대상을 블랙보드에 Target으로 등록해줍니다

그럼 인지한 대상을 Object형태로 Target이름의 actors[0]의 오브젝트 (aactor 의 포인터) 를 저장합니다

블랙보드에서 Object형식의 Target이름을 가진 블랙보드키를 만들면

플레이어가 AI Perception에 인지되면 BlackBoard 키가 인지된 오브젝트 즉 플레이어로 값을 받을수있습니다

플레이어의 위치를 통해서 enumclass로 state를 정의하는방식으로 ai의 행동을 제어하겠습니다

가장 기본형으로 만들어보겠습니다

● Patrol

● Chase

● Attack

● Die

총 4개의 enum class를 가지도록 해보겠습니다

시야에 보이지않을때 즉 센서인식 범위 밖에있을때는 Patrol class 즉 순찰을 하고

시야에 보일때 즉 센서범위 안으로 들어왔을떄는 Chase class 플레이어를 따라가며

센서범위 안으로 들어왔고 플레이어와의 거리가 일정이상 가까워졌을떄 Attack class 공격을 하고

AI의 hp가 0이되면 Die class 죽음상태로 전이시켜보겠습니다

 

 

빈 프로젝트로 enumclass를 만들어주겠습니다

 

#pragma once

#include "CoreMinimal.h"

UENUM(BlueprintType)
enum class EAIStateEnum : uint8
{
    Patrol  UMETA(DisplayName = "Patrol"),
    Chase   UMETA(DisplayName = "Chase"),
    Attack  UMETA(DisplayName = "Attack"),
    Die     UMETA(DisplayName = "Die")
};

 

.h에 사용할 enumclass를 정의해줍니다

 

EnumClass는 c++ 클래스를 상속받은 블루프린트를 만들수없습니다

그래서 사용할 enumcalss를 직접 따로 블루프린트로 제작해줍니다

이너머레이터를 동일하게 맞춰줍니다

 

BlackBoard에서 enumclass를 받을 blackboard key를 enum 타입으로 만들어줍니다 (AIState)

그리고 열거형타입에 방금만든 enum을 넣어줍니다

 

플레이어와의 거리, 그리고 조건에 따라 state enum을 변경시킬 service node를 생성하겠습니다

필요한 헤더 추가해주시고

.h에서 필요한것들을 정의해줍니다

.h코드

UCLASS()
class BLASTERDREAM_API UBTS_Melee : public UBTService
{
	GENERATED_BODY()
	
public:
	UBTS_Melee();
	void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
	FVector playerLoc;
	class ABasicSkeletonEnemy* skeleton;
	ASkeletonAIController* aiController;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
	EAIStateEnum currentState;

};

 

.cpp 코드입니다

UBTS_Melee::UBTS_Melee()
{
	NodeName = "Player Detect Service";

	Interval = 0.1f;
	RandomDeviation = 0.0f;
}

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

	UBlackboardComponent* blackBoard = OwnerComp.GetBlackboardComponent();
	AActor* targetObj = Cast<AActor>(blackBoard->GetValueAsObject("Target"));
	ASwordCharacter* targetCharacter = Cast<ASwordCharacter>(targetObj);
	skeleton = Cast<ABasicSkeletonEnemy>(OwnerComp.GetAIOwner()->GetPawn());
	aiController = Cast<ASkeletonAIController>(OwnerComp.GetAIOwner());

	FVector skeletonLoc = skeleton->GetActorLocation();

	if (targetObj == nullptr)
	{
		currentState = EAIStateEnum::Patrol;
		blackBoard->SetValueAsEnum("AIState", static_cast<uint8>(currentState));
		return;
	}
	if (targetCharacter)
	{
		
		playerLoc = targetCharacter->GetActorLocation();
		blackBoard->SetValueAsVector("PlayerLocation", playerLoc);  

		//set state enum class
		//UE_LOG(LogTemp, Warning, TEXT("Player AI Dist?: %f"), distance);
		float distance = FVector::Dist(playerLoc, skeletonLoc);
		if (distance <= 170.0f)
		{
			currentState = EAIStateEnum::Attack;  
			blackBoard->SetValueAsEnum("AIState", static_cast<uint8>(currentState)); 
		}
		else
		{
			currentState = EAIStateEnum::Chase;  
			blackBoard->SetValueAsEnum("AIState", static_cast<uint8>(currentState));  
		}
	}
}

AI Controller에서 AI Perception으로 ai의 시각을 정의하고있습니다

만약 본것이 없을때 (targetObj == nullptr) 상태를 Patrol enum class로 전이합니다

distance는 플레이어와 ai간의 거리를 의미하는 Vector입니다

그 거리에 따라 170보다 크면 Chase 상태 작으면 Attack 상태로 전이합니다

또한  타겟(플레이어)의 위치를 구해서 Vector타입으로 블랙보드의값을 전달해줍니다

 

그럼 해당 service node는 플레이어의 위치, 그리고 거리에 따라 상태를 전이하는 역할을 할당하는 노드가됩니다

 

블랙보드의 상태입니다

 

그럼 Behavior Tree의 노드를 조립해주겠습니다

아직은 따라간다와 공격한다 그리고 패트롤의 노드를 완성시키지못했음으로 wait노드로 잠시 사용하겠습니다

파란색 노드 Decorator 노드는 조건을 의미하는 일종의 if문 역할을 합니다

해당 조건에 알맞는 블랙보드키와 키값을 설정해주겠습니다

 

 

 

플레이어와의 거리가 일정이상 가까워지면 AIState는 Attack으로 변경되어 Attack 노드쪽으로 상태가 전이

거리가 일정거리 가까워지지않았으면 Chase로 상태가 바뀌고

Perception에 시야범위에서 벗어나면 Patrol상태로 전이가 되는것을 확인할수있습니다

 

그러면 Chase를 담당하는 Task_Node를 생성해보겠습니다

부모 클래스를 BTTask_BlackboardBase를 상속해서 만들어주도록하겠습니다

블랙보드를 사용하는 경우 BlackboardBase 사용하지않는경우는 그냥 BTTaskNode로 만듭니다

저희는 블랙보드값을 사용하기때문에 BTTask_BlackboardBase를 상속해서 만들겠습니다

 

Task에서 필요한 Ticktask와 excuteTask를 만들고 필요한 객체들을 선언해주겠습니다

.h

UCLASS()
class BLASTERDREAM_API UTask_SkeletonMoveToPlayer : public UBTTask_BlackboardBase
{
	GENERATED_BODY()

public:
	UTask_SkeletonMoveToPlayer();

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory) override;
	virtual void TickTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory, float deltaSeconds) override;

	ASkeletonAIController* aiController;
	UBehaviorTreeComponent* cOwnerComp;
	AActor* targetActor;
	UBlackboardComponent* blackboardComp;
	EAIStateEnum currentState;
};

 

cpp에서는 state가 Chase인 상태이니 Chase가 아니면 태스크를 종료하고 신호를 다시 root로 보내고

아니면 플레이어의 위치로 이동하면됩니다

.cpp

UTask_SkeletonMoveToPlayer::UTask_SkeletonMoveToPlayer()
{
    NodeName = TEXT("Move To Target SwordPlayer");
    bNotifyTick = true;
}

EBTNodeResult::Type UTask_SkeletonMoveToPlayer::ExecuteTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory)
{
    Super::ExecuteTask(ownerComp, nodeMemory);
    blackboardComp = ownerComp.GetBlackboardComponent();
    cOwnerComp = &ownerComp;
    aiController = Cast<ASkeletonAIController>(ownerComp.GetAIOwner());
    UObject* targetObject = blackboardComp->GetValueAsObject(GetSelectedBlackboardKey());
    targetActor = Cast<AActor>(targetObject);
    
    if (!targetActor || !aiController)
    {
        return EBTNodeResult::Failed;
    }
    return EBTNodeResult::InProgress;
}

void UTask_SkeletonMoveToPlayer::TickTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory, float deltaSeconds)
{
    Super::TickTask(ownerComp, nodeMemory, deltaSeconds);
    
    currentState = (EAIStateEnum)blackboardComp->GetValueAsEnum(TEXT("AIState"));
    if (currentState != EAIStateEnum::Chase)
    {
        aiController->StopMovement();
        FinishLatentTask(ownerComp, EBTNodeResult::Failed);
        return;
    }
    if (targetActor)
    {
        FVector currentTargetLoc = targetActor->GetActorLocation();
        aiController->MoveToLocation(currentTargetLoc);
    }
}

Node Name BT에서 노드이름을 설정해줍니다

EBTNodeResult::InProgress로 해당 노드가 신호를 가지고있게 해줍니다

TickTask에서 enum의 상태가 Chase이면 움직임을 멈추고 태스크의 신호를 종료합니다

그게 아니라면 타겟(플레이어)의 위치를 구해서 그곳으로 움직입니다

 

결과물

 

플레이어가 AI Perception에 감지되면 저희가 만든 Move To Target SwordPlayer이라는 이름을 가진

커스텀노드 가 실행됩니다

해당 커스텀도느는 플레이어를 쫒아가는 노드입니다

해당 노드가 실행되면 태스크노드에 신호가 유지되며 플레이어를 쫒아갑니다

 

 

 

 

------

만약 신호를 붙잡지않고 Move To 함수만 실행하게된다면

root와 움직이는 노드와 계속 신호를 무한으로 주고받게되어

신호가 번쩍번쩍거리는 점멸현상이 일어납니다

 

신호를 붙잡아두고 만드는게 구조상 좋은거같습니다

'Unreal5 프로젝트 다이어리' 카테고리의 다른 글

Unreal - Behavior Tree(4) AI Perception Team ID  (1) 2025.05.04
Unreal - Behavior Tree(3) 플레이어와의 상호작용  (0) 2025.05.02
Unreal - BehaviorTree(1) AI Perception  (0) 2025.04.27
Unreal - 8방향 블렌드스페이스(BlendSpace)  (0) 2025.04.27
Unreal - 미니맵 만들기  (0) 2025.04.25
'Unreal5 프로젝트 다이어리' 카테고리의 다른 글
  • Unreal - Behavior Tree(4) AI Perception Team ID
  • Unreal - Behavior Tree(3) 플레이어와의 상호작용
  • Unreal - BehaviorTree(1) AI Perception
  • Unreal - 8방향 블렌드스페이스(BlendSpace)
lucodev
lucodev
커피와 노트북 그리고 개발
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (125) N
      • Unreal5 프로젝트 다이어리 (73)
      • Unreal5 프로젝트 다이어리2 (5) N
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 다이어리 (23) N
        • Stack (3)
        • Hash (4)
        • Heap (2)
        • Sort (3) N
      • 코드 개인보관함 (8)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    unreal loading
    언리얼 로딩
    언리얼 페이드 아웃
    언리얼 컷씬
    unreal 로딩
    언리얼 foot step
    unreal look at
    언리얼 비헤이비어트리
    언리얼 시퀀스
    언리얼
    unreal 모션매칭
    언리얼 모션매칭
    언리얼 로딩창
    unreal sequence
    언리얼 behaviortree
    unreal 시퀀스
    언리얼 behavior tree
    unreal 컷씬
    언리얼 look at
    언리얼 motionmatching
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - Behavior Tree(2) 플레이어 따라가기
상단으로

티스토리툴바