● 신기술을 사용해볼까

5.5까지는 비헤이비어 트리(Behavior Tree)를 사용하여 AI 캐릭터 움직임을 제어했다. 내가 스스로 공부할 때도, 팀 프로젝트 개발에도 NPC 행동에는 이 비헤이비어 트리로 결정했다. 하지만 5.6부터는 공식 템플릿부터 시작해서 스테이트 트리(State Tree)라는 것을 밀어주기 시작했다. 그래서 기왕 에픽 게임즈가 새로운 기술을 써보라고 들이미는데 이것을 이용하여 NPC가 돌아다니게 만들 것이다.
물론 여전히 비헤이비어 트리를 쓸 수는 있다. 스테이트 트리가 비헤이비어 트리의 상위 기술이라고 단정짓기는 어렵고 서로 다른 방향의 기술이라고 봐야 한다. 스테이트 트리가 나중에 만들어진 만큼 더 직관적인 모습을 보이지만 암묵적인 상태 표현에 익숙하다면 스테이트 트리가 불편하게 느낄 수 있다. 테스크의 유연성은 오히려 비헤이비어 트리가 더 좋다.
스테이트 트리에 대한 정보를 알기 위해 언리얼 엔진의 공식 문서를 참고하자.

일단 사전 준비로, 스테이트 트리 관련 플러그인이 있는지 확인한다. 5.6버전에서 프로젝트를 생성했다면 당연히 있겠지만 일단 확인한다고 나쁠건 없다.



AI 관련이니 콘텐츠 브라우저에서 AI라는 폴더를 만들고 거기에 스테이트 트리를 생성한다.
● AI 컨트롤러에서 사용 준비하기
class UStateTreeAIComponent;
UCLASS()
class HIDDENTHIEF_API AMainGameAIController : public AAIController
{
GENERATED_BODY()
public:
AMainGameAIController();
virtual void OnPossess(APawn* InPawn) override;
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = true))
UStateTreeAIComponent* StateTreeAIComp;
};
생성만 하고 방치했던 AI 컨트롤러를 사용해 스테이트 트리를 시작하게 만들 것이다. 스테이트 트리 AI 컴포넌트 (State Tree AI Component) 가 필요하다.
AMainGameAIController::AMainGameAIController()
{
StateTreeAIComp = CreateDefaultSubobject<UStateTreeAIComponent>(TEXT("StateTreeAI"));
bStartAILogicOnPossess = true;
}
void AMainGameAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
StateTreeAIComp->StartLogic();
}
AI 캐릭터에 컨트롤러가 빙의하면 움직여야 하니 bStartAILogicOnPossess를 true로 해야 한다. 빙의 직후 스테이트 트리도 실행해야 하니 OnPossess함수에서 스테이트 트리 AI 컴포넌트에 StartLogic 함수를 실행한다.

작성한 코드를 기반으로 하는 블루프린트를 생성한다.

스테이트 트리 AI 컴포넌트와 함께 경로 이동 컴포넌트가 생성되었는데 지금은 쓸 생각이 없으니 무시한다.

이쯤에서 오류가 나온다면 스테이트 트리 AI 컴포넌트 때문이다. 디테일 화면에서 미리 만들었던 스테이트 트리를 지정해주면 된다.
코드에서 빙의시 시작하게 했으니 트리 자동 시작(Star tLogic Automatically)은 켤 필요가 없다.

AI 컨트롤러 블루프린트를 만들었으니 캐릭터에서도 블루프린트로 만든 컨트롤러로 재지정한다.
● 간단한 스테이트 트리 만들기
이 프로젝트에서 NPC는 그냥 돌아다니기만 할 뿐이다. 그래서 스테이트 트리의 내용도 비교적 간단할 것이다.

스테이트 트리를 열고 에셋 디테일을 변경한다. 스테이트 트리 AI 컴포넌트를 지정해주면 AI 컨트롤러 클래스와 컨텍스트 액터 클래스를 지정하는 항목이 생긴다. 전자는 AI 컨트롤러를, 후자는 AI 캐릭터를 지정한다. (둘 다 블루프린트로 만든 것을 지정한다.)

같은 화면에서 상태 패널을 보면 상태를 편집할 수 있다. 아마 이게 핵심으로 보인다. 나는 AI가 돌아다니기만 하도록 만들 것이므로 대기 상태와 이동 상태 2가지만 있으면 된다. 패널의 상단에 있는 상태 추가 버튼으로 2개의 상태를 추가한다. 이름은 Wait, Move로 적었다.

Wait 상태의 디테일을 보면 뭔가 많아 보이지만 그 중 '태스크'와 '트랜지션'만 필요할 것 같다.
이름 그대로 기다려야 하니 태스크에는 '딜레이'가 필요하다. 그러면 기다릴 시간과 랜덤 편차를 지정할 수 있다. 기간은 3으로, 랜덤 편차를 2로 하면 최소 1초, 최대 5초 기다릴 것이다. 대기했다가 이동할 것이니 영구 실행은 false로 한다.
트랜지션은 현 상태에서 다른 상태로 이동하는 기능인 것 같다. 그래서 대기후 이동하기 위해 트랜지션 대상을 Move로 했다. Trigger를 On State Completed로 하면 무조건 대기가 끝나고 Move 상태로 이동할 것이다.

Move 상태다. 이동이 끝나면 Root로 이동하여 캐릭터의 대기와 이동을 반복할 것이다.
여기서 사용한 ST_MoveTask는 내가 직접 만든 태스크다.
● 스테이트 트리의 상태에 사용할 태스크 직접 만들기


태스크를 C++로 만드려면 StateTreeTaskBlueprintBase를 부모로 하여 클래스를 생성해야 한다.
public:
UST_MoveTask(const FObjectInitializer& ObjectInitializer);
protected:
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) override;
virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) override;
부모 함수의 C++ 코드를 보니 이렇게 2개의 함수를 오버라이딩 하면 될 것 같다. EnterState는 태스크 시작시 실행하는 함수다. Tick는 액터에서 보던 그 함수다. 형태가 다르지만 목적은 비슷하다.
생성자는 매개변수가 있는 형태인데 이렇게 하지 않으면 적절한 생성자가 없다면서 컴파일 에러가 발생한다.
private:
//목적지와의 거리가 이 값 이하라면 목적지 도착으로 인식
UPROPERTY(EditAnywhere, Category = "Task", meta = (AllowPrivateAccess = true))
float AcceptanceRadius;
//AI 컨트롤러
UPROPERTY()
AMainGameAIController* AIController = nullptr;
//AI 캐릭터
UPROPERTY()
AMainGameAIChar* AICharacter = nullptr;
//이동 목적지
FVector MoveDestination;
이 태스크가 사용할 변수를 선언한다. 캐릭터 이동을 위해 AI 컨트롤러가 필요하고 목적지 지정을 위해 AI 캐릭터가 필요하다. 목적지를 저장할 FVector 변수도 필요하다. 목적지에 정확하게 도착하게 만들면 로직이 복잡해질 것 같아 어느정도 가까이 가면 도착한 것으로 취급하려고 하는데 그러기 위한 float 변수도 추가했다.
UST_MoveTask::UST_MoveTask(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
bShouldCallTick = true;
AcceptanceRadius = 50.0f;
MoveDestination = FVector::ZeroVector;
}
내가 찾은 바에 따르면 이 생성자는 부모의 생성자도 호출해줘야 한다. 그리고 Tick 사용을 위해 bShouldCallTick를 true로 지정한다.
EStateTreeRunStatus UST_MoveTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition)
{
//이 태스크를 처음으로 실행한 경우 AI컨트롤러와 AI캐릭터를 할당한다
if (!IsValid(AICharacter))
{
AMainGameAIChar* AIChar = Cast<AMainGameAIChar>(GetOwnerActor(Context));
if (IsValid(AIChar))
{
AICharacter = AIChar;
}
else
{
return EStateTreeRunStatus::Failed;
}
AMainGameAIController* AICon = Cast<AMainGameAIController>(AIChar->GetController());
if (IsValid(AICon))
{
AIController = AICon;
}
else
{
return EStateTreeRunStatus::Failed;
}
}
//무작위 목적지로 이동
MoveDestination = AICharacter->GetRandomDestination();
switch (AIController->MoveToLocation(MoveDestination, AcceptanceRadius))
{
case EPathFollowingRequestResult::AlreadyAtGoal:
return EStateTreeRunStatus::Succeeded;
case EPathFollowingRequestResult::RequestSuccessful:
return EStateTreeRunStatus::Running;
default:
return EStateTreeRunStatus::Failed;
}
}
이 태스크를 제일 처음 실행할 때 AI 컨트롤러 및 AI 캐릭터를 가져온다.
AI컨트롤러를 통해 AI 캐릭터에게 이동 명령을 내릴 수 있다. 여기서 사용한 AI 캐릭터의 함수는 후술한다.
이 함수의 반환값은 EStateTreeRunStatus라는 enum으로, 태스크의 성공, 실패 또는 실행중을 표현할 수 있다. 여기서 Succeeded나 Failed를 반환하면 태스크 완료로 취급되어 Tick 함수를 실행하지 않을 것이다.
EStateTreeRunStatus UST_MoveTask::Tick(FStateTreeExecutionContext& Context, const float DeltaTime)
{
if (!IsValid(AIController))
{
return EStateTreeRunStatus::Failed;
}
//이동 상태에 따라 결과 반환
EPathFollowingStatus::Type MoveStatus = AIController->GetMoveStatus();
if (MoveStatus == EPathFollowingStatus::Moving)
{
return EStateTreeRunStatus::Running;
}
return EStateTreeRunStatus::Succeeded;
}
Tick 함수도 반환값이 EStateTreeRunStatus다. 진행 상황에 따라 적절한 값을 골라 반환한다.
AI 컨트롤러에 이동 명령을 내렸으면 GetMoveStatus()함수를 통해 이동 상태를 확인할 수 있는데, 이동중이면 태스크를 계속 실행하고 그게 아니라면 태스크가 완료하도록 만들었다.
private:
//네비게이션 시스템
UPROPERTY()
UNavigationSystemV1* NavigationSystem = nullptr;
void AMainGameAIChar::BeginPlay()
{
Super::BeginPlay();
bIsDeath = false;
//이하 서버 전용
if (!IsLocallyControlled())
{
return;
}
//네비게이션 시스템
NavigationSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
if (!IsValid(NavigationSystem))
{
UE_LOG(LogTemp, Error, TEXT("%s : NavigationSystem이 유효하지 않습니다."), *GetNameSafe(this));
}
}
FVector AMainGameAIChar::GetRandomDestination()
{
if (bIsDeath)
{
return FVector::ZeroVector;
}
FNavLocation NewLocation;
if (!NavigationSystem->GetRandomPoint(NewLocation))
{
return FVector::ZeroVector;
}
return NewLocation.Location;
}
AI 캐릭터에 추가한 함수는 네비게이션 시스템을 이용하여 무작위 위치를 반환하는 함수다. 이 함수를 만들려면 캐릭터가 네비게이션 시스템을 알고 있어야 하니 관련 변수를 추가했으며 BeginPlay에서 할당한다. 참고로 서버에서 다뤄야 하니 클라이언트에서의 실행을 막기 위해 IsLocallyControlled 함수를 사용했다.
● 일단 완성하긴 했는데...

테스트를 위해 혼자서 하는 Standalone로 실행했다.

스스로 기다리고 이동하는 것을 반복하는 기능은 완성했다. 그런데 이동시 걷는 애니메이션을 실행하지 않는다. 기존에 만들어진 애니메이션 클래스와 연동이 안되는 모양이다. 그리고 방향 전환시에도 연출없이 즉시 방향을 돌리니 어색하다. 이제 이 문제를 해결해야겠다.
'언리얼엔진 학습 및 과제' 카테고리의 다른 글
| 액터 리플리케이션을 이용한 게임 제작 (9) (0) | 2026.01.06 |
|---|---|
| 액터 리플리케이션을 이용한 게임 제작 (8) (0) | 2026.01.05 |
| 액터 리플리케이션을 이용한 게임 제작 (6) (0) | 2025.12.24 |
| 액터 리플리케이션을 이용한 게임 제작 (5) (0) | 2025.12.23 |
| 액터 리플리케이션을 이용한 게임 제작 (4) (0) | 2025.12.22 |