Unreal Engine 5 AI - Behavior Tree

garden cho·2025년 8월 1일
post-thumbnail

TIL(Today I Learned)

텍스트 RPG에 이어 다음 프로젝트로 3인칭 슈팅 게임을 만들게 되었다.
본격적으로 시작 전에 기획도 하고, 에셋도 찾는 등 프로젝트 준비를 한 후, 각 역할을 분담하였다.

원래는 적 AI 파트를 맡아서 하고 있었는데 중간에 애니메이션 부분을 담당할 사람이 필요해서 넘어가게 되었다.
한 주 동안 만들었던 AI와 Behavior Tree에 대한 내용을 작성해보겠다.

AI시스템의 전반적인 구조, 블랙보드와 연동된 데이터 에셋을 다른 팀원분들이 작성하였고,
나는 원거리 AI를 만들어, Beahavior Tree에 들어갈 Task를 작성했다.

BlackBoard는 AI가 갖고 있는 데이터 저장소로 사람의 두뇌와 비슷하다.
단순한 저장소 기능이긴 하지만, Blackboard에 목표나 상태 등을 저장하고 Behavior Tree의 조건이나 구체적인 Task에 활용한다.

Behavior Tree는 AI가 수행할 행동을 트리 구조로 구성한 시스템이다.
계측정으로 행동을 제어하고, 조건에 따라 분기하는 구조를 가진다.
Root 노드에서 시작하여 Selector와 Sequence 노드 그리고 Task를 조합하여 Tree를 구성한다.
Selector는 자식 노드를 순차적으로 실행하나 자식 노드가 성공하면 즉시 종료하고 다음 노드를 실행하지 않는다.
반면, Sequence는 성공하면 다음 노드를 순차적으로 실행하게 된다.

Selector나 Sequence에는 데코레이터를 추가할 수 있어 분기에 대한 조건을 설정할 수 있다.
위 Behavior Tree는 세 가지 주요 기능을 넣었다.

타겟 발견 시 스나이핑 가능한 위치를 찾으며 공격
타겟을 놓쳤을 시 마지막 위치 조사
정해진 포인트 순찰

타겟 공격은 두 노드를 병렬로 처리하여,
공격을 하면서 원거리 적이 공격할 수 잇는 다른 위치를 찾아 이동하는 것을 반복하게 만들었다.
아직 공격하는 효과는 구체적 내용을 넣지 않았다.

간단하게 Find Snipping Location Task의 코드를 살펴보면,
생성자에서 블루프린트에서 보일 노드의 이름을 설정하는 코드와
FBlackboardKeySelector 타입의 변수 선언하여 블루프린트에서 설정해줄 수 있게 한다.

UXVTask_FindSnippingLocation::UXVTask_FindSnippingLocation()
{
	NodeName = TEXT("Find Snipping Location");
	SnippingLocationKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UXVTask_FindSnippingLocation, SnippingLocationKey));
	TargetKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UXVTask_FindSnippingLocation, TargetKey), AActor::StaticClass());
}

캐스팅을 통해 필요한 변수들을 초기화한 뒤,
각 시도에서 다음 세 가지 조건을 순차적으로 평가하며 조건에 맞으면 해당 위치를 블랙보드에 저장하고 성공을 반환한다.
1. 내비게이션 시스템을 통해 현재 타겟 주변 위치에서 도달 가능한 랜덤 위치 생성
2. 생성된 랜덤 위치가 최소거리 이상 최대 거리 이하인지 판정
3. 랜덤 위치가 타겟까지의 직선 시야에 시야상 차단되는지 Line Trace로 판정

	for (int i = 0; i < 20; ++i)
	{
		FVector RandomPoint;
		if (NavSys->K2_GetRandomReachablePointInRadius(this, TargetLocation, RandomPoint, SearchRadius))
		{
			float Distance = FVector::Dist(RandomPoint, TargetLocation);
			if (Distance >= MinRange && Distance <= MaxRange)
			{
				FHitResult Hit;
				bool bVisible = !GetWorld()->LineTraceSingleByChannel(
					Hit,
					RandomPoint,
					TargetLocation,
					ECC_Visibility);
				if (bVisible)
				{
					Blackboard->SetValueAsVector(SnippingLocationKey.SelectedKeyName, RandomPoint);
					return EBTNodeResult::Succeeded;
				}
			}
		}
	}

	return EBTNodeResult::Failed;

타겟의 마지막 위치를 조사하는 부분은 아직 특별한 기능을 넣지 않고, 마지막 위치를 저장했다 MoveTo로 이동해주었다.

다음으로 정해진 포인트를 순찰하는 Task이다.

레벨에 배치된 캐릭터에 필요한 순찰 위치를 추가하도록
적 Character 클래스에 순찰 포인트를 Actor로 배열에 저장하였다.

	// 순찰 포인트
	UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "AI")
	TArray<AActor*> PatrolPoints;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "AI")
	int32 CurrentPatrolIndex = 0;

Set Patrol Point Task에서 캐릭터에 캐스팅한 후 포인트와 index를 가져와,
호출될 때마다 현재 인덱스의 액터를 블랙보드에 저장한 후, 인덱스를 1씩 증가시킨다.

	int32 Index = MyCharacter->CurrentPatrolIndex;
	AActor* NextPoint = MyCharacter->PatrolPoints[Index];
	if (!NextPoint) return EBTNodeResult::Failed;

	UBlackboardComponent* Blackboard = OwnerComp.GetBlackboardComponent();
	if (!Blackboard) return EBTNodeResult::Failed;

	Blackboard->SetValueAsObject(FName("TargetPoint"), NextPoint);

	MyCharacter->CurrentPatrolIndex = (Index + 1) % MyCharacter->PatrolPoints.Num();

	return EBTNodeResult::Succeeded;

0개의 댓글