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 |