언리얼 엔진 5/액션RPG 게임

[UE5] 시네마틱 보스배틀 게임 개발일지 (3) - 보스 움직임 구현(BT + EQS)

ciel45 2025. 8. 30. 16:34

이번엔 보스의 움직임을 구현해보았다.

보스의 움직임은 기본적으로 다크소울 시리즈와 같이

  • 공격 사정거리로 접근해서 1회 공격
  • 플레이어를 맴돌기
  • 일정 시간 후 다시 1회 공격

 

이런 흐름으로 가도록 할 것이다.

 

우선 애니메이션의 경우에는 에셋에 포함되어있는 애니메이션 블루프린트를 그대로 사용하였으나, BlendSpace가 어색하게 작동하고 있어 이 부분만 직접 다시 만들어주었다.

많이 해봤던 작업이기에, 어려울 부분은 없었다.

 

그리고 적을 움직여줄 AI를 구현하기 위해 Behavior Tree를 사용하였다.

지금까지 만든 BT의 전체 모양은 다음과 같다.

흐름을 간단히 요약하면, Target(플레이어)으로의 거리를 계속해서 측정하며, 그 결과에 따라 Attack 시퀀스 / Chase 시퀀스 둘 중 하나를 실행하게 된다.

 

Set Distance to Target이라 써져있는 초록색 노드가 거리를 측정해주는 BTService이다.

 

BTService란?

AttackSequence의 동작의 흐름은 다음과 같다.

  1. Focus On (플레이어를 바라봄)
  2. EQS_Strafe (플레이어 주위를 맴돔)
  3. 공격 쿨타임이 다 되었을 시 공격 수행
  4. 공격 성공 시 포커스 해제

중간에 Simple Parallel 노드를 사용하였는데, 행여나 공격 쿨타임이어서 공격에 실패하더라도, Chase Sequence로 넘어가지 않도록 하기 위해서 사용하였다.

여기서는 메인 태스크를 EQS 이동으로, 서브 태스크를 공격으로 하여 쿨타임이어도 노드가 실패하지 않고 자연스럽게 진행되도록 해주었다.

 

 

+ Strafe를 EQS로 구현한 이유:

  • 그냥 플레이어 위치, 보스 위치를 이용해 기계적으로 보스가 플레이어 주위를 맴돌게 하는 건 어렵지 않겠지만,
  • 보스를 이동시키는 데에 있어서는 그것 외에도 이동 포인트가 낭떠러지인지, 벽으로 막혀있는 지 등 고려해야 할 것이 많다.
  • EQS는 이러한 요소들도 고려해주는 기능이 제공되기에, 활용하기로 했다.

 

EQS_Strafe의 구성은 다음과 같다.

플레이어 주위에 원형으로 아이템을 쭉 깔고, 도달 가능 여부, 자신으로부터의 거리를 이용해 아이템을 선별한다.

 

Distance 조건에는 at least 1000.0이라고 써져있는데, 현재 자신이 서있는 위치의 아이템은 거르고, 그 외 아이템 중에 선택하게 하기 위해서이다.

 

여기까지의 BT에서 사용된 코드는 다음과 같다.

// BTTaskNodeFocusOn.h.cpp (Focus Off도 이와 유사)

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

	if (ABossCharacter* BossCharacter = Cast<ABossCharacter>(OwnerComp.GetAIOwner()->GetCharacter()))
	{
		BossCharacter->SetStrafing(true);
		BossCharacter->SetWalking(true);
		return EBTNodeResult::Succeeded;
	}
	else
	{
		return EBTNodeResult::Failed;
	}
}
// BTTaskNodeMeleeAttack.cpp
//...

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

	if (ABossCharacter* BossCharacter = Cast<ABossCharacter>(OwnerComp.GetAIOwner()->GetCharacter()))
	{
		BossCharacter->PerformMeleeAttack(); // 현재 미구현
		return EBTNodeResult::Succeeded;
	}
	else
	{
		return EBTNodeResult::Failed;
	}
}
// BTServiceSetDistToTarget.cpp

UBTServiceSetDistToTarget::UBTServiceSetDistToTarget()
{
	NodeName = "Set Distance to Target";
}

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

	if (UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent())
	{
		if (AActor* TargetActor = Cast<AActor>(BB->GetValueAsObject(TEXT("TargetActor"))))
		{
			if (AAIController* AIController = OwnerComp.GetAIOwner())
			{
				if (APawn* ControlledPawn = AIController->GetPawn())
				{
					float Dist = ControlledPawn->GetDistanceTo(TargetActor);
					BB->SetValueAsBool(TEXT("IsInMeleeRange"), Dist <= MeleeRange);
				}
			}
		}
	}
	
}

 

 

테스트 영상:

https://youtu.be/j357ynS74IM

 

 

 

 

 

아직 애니메이션의 전환같은건 좀 어색하지만, 움직임은 의도대로 잘 되는 것을 볼 수 있었다.

 

 

작업 내용:

https://github.com/sys010611/Titan/commit/4e0992d71709979831d9398b7f781574789a39f1