BulletAnt 개발일지 (5) - SetFocus, 회전애니메이션 동기화

김펭귄·2026년 5월 14일

Today What I Learned (TIL)

목록 보기
120/139
post-thumbnail

SetFocus / ClearFocus

특정 액터를 바라보게 하고 싶으면 AIControllerSetFocus를 불러주면 된다

CachedAIController->SetFocus(TargetActor);

이때, 두 번째 인자로 Prioritiy를 넣어줄 수 있다.

namespace EAIFocusPriority
{
	typedef uint8 Type;

	inline const Type Default = 0;
	inline const Type Move = 1;
	inline const Type Gameplay = 2;

	inline const Type LastFocusPriority = Gameplay;
}

void AAIController::SetFocus(AActor* NewFocus, EAIFocusPriority::Type InPriority)
{
	// clear out existing
	ClearFocus(InPriority);

	// now set new
	if (NewFocus)
	{
		if (InPriority >= FocusInformation.Priorities.Num())
		{
			FocusInformation.Priorities.SetNum(InPriority + 1);
		}
		FocusInformation.Priorities[InPriority].Actor = NewFocus;
	}
}

이때, 두 번째 인자 (Priority)를 안 넣어주면 기본적으로 EAIFocusPriority::Gameplay이다. 그래서 ClearFocus 때도 이 인자를 넣어주어 없애야 함. 첨에 Default로 했다가 안 되어서 고생했음.

CachedAIController->ClearFocus(EAIFocusPriority::Gameplay);

클라이언트 회전 애니메이션 문제

호스트에서는 회전 애니메이션이 정상 동작했지만,
클라이언트에서는 회전 애니메이션 없이 몸만 회전하는 문제가 발생했다.

원인으로는 회전을 할 때, AIController의 회전값을 가져와서 회전을 하는데 클라이언트에는 AIController가 존재하지 않았고, Direction 계산 자체가 수행되지 않았다.

BlendSpace 방식 폐기

추가로 BlendSpace 기반 회전도 문제가 있었다.

현재 방식은:

  • Controller Rotation (타겟을 향하는 방향)
  • Enemy ForwardVector (현재 적의 정면 방향)

두 벡터의 각 차이(Direction)를 구하고, 이를 회전해야하는 값으로 BlendSpace에 넣어주어 회전시켜주었다.

  • 1번상황일 때, 적이 타겟을 바라보고 있으므로 Controller RotatorForward Vector가 동일하다.
  • 그러나, 타겟이 2번위치로 이동하게 되면 적은 아직 회전을 못 해 Forward Vector는 그대로이고, Controller Rotator는 타겟을 향하게 되며 두 벡터 사이의 각 차이(Direction)가 발생한다.

문제는 적이 타겟을 향하며 회전이 거의 끝나갈수록 각 차이가 작아지고,
BlendSpace 입력값도 작아지면서 회전 애니메이션이 거의 멈춰 보였다는 점이다.

결국:

“회전은 방향만 있을 뿐 값이 필요 없는데, BlendSpace 값 기반으로 제어하는 방식이 맞지 않는다”

고 판단했다.


회전 애니메이션 구조 변경

처음에는 회전 애니메이션도 GAS로 처리하려 했다.
동기화가 문제였으니 동기화만 해결하기 위해 무작정 GAS를 생각했던 것이었다.

  • Turn Montage 실행
  • Ability로 동기화 처리

GAS 기반 회전의 문제점

문제는 회전 도중 상태가 바뀌는 경우였다.

예를 들어:

  • 회전 중 공격 상태 진입
  • 타겟 변경
  • 이동 상태 전환

같은 상황이 발생하면:

  • 외부에서 Montage 중단 이벤트 전송
  • Ability 강제 종료
  • 상태 동기화 처리

등 추가 작업이 계속 필요해졌다.

결국:

“현재 상태에 따른 애니메이션은 Animation Blueprint가 가장 적합하다”

고 판단했다.

최종 구조

최종적으로는 회전 자체를 ABP State 기반으로 다시 구성했다.

호스트에서는:

  • 현재 회전을 하는 중인지
  • 외적으로 좌/우 회전 방향 판정

만 수행했다.

// RotateToTargetTask.cpp
float DotResult = FVector::DotProduct(ForwardDirection, ToTargetDirection);			// 각도 판단
float CrossResult = FVector::CrossProduct(ToTargetDirection, ForwardDirection).Z;	// 좌우 판단
float Threshold = FMath::Cos(FMath::DegreesToRadians(RotateThreshold));

if (DotResult < Threshold)
{
	ContextActor->bIsTurning = true;
	ContextActor->bIsTurningLeft = (CrossResult > 0);
}

그리고 이 두 변수 값만 동기화했다.

// BaseEnemyCharacter.h
UPROPERTY(BlueprintReadOnly, Replicated)
uint8 bIsTurning : 1;

UPROPERTY(BlueprintReadOnly, Replicated)
uint8 bIsTurningLeft : 1;

ABP에서는:

  • bIsTurning == true면 Rotate State 진입
  • bIsTurningLeft에 따라 좌/우 회전 애니메이션 재생

하도록 구성했다.

ABP로 State 별로 알맞은 애니메이션 재생이 가능해졌으며, 동기화 문제도 해결하였다.


Root Motion 관련 고민

다만 이후 네트워크 지연도 고민하게 됐다.

현재 구조는:

  • bool 값 동기화 시점
  • 패킷 도착 시간

에 따라 클라이언트 회전 애니메이션 시작 타이밍이 달라질 수 있었다.

결국:

“애니메이션 회전량과 실제 캐릭터 회전량이 어긋날 가능성”

이 존재했다.

그래서:

  • 실제 회전은 CharacterMovement가 처리
  • 애니메이션은 단순 연출만 담당

하는 구조도 고민했다.

즉:

  • SetFocus()와 Movement 기반으로 실제 Rotation 동기화
  • ABP는 회전 애니메이션만 재생

하는 방식이다.

하지만 테스트 결과 현재 Root Motion 기반 회전이 충분히 자연스러웠고,
회전 시간과 회전 각도를 데이터 에셋으로 맞춰주니 큰 문제 없이 동작했다.

결과적으로 현재는:

회전 상태는 ABP가 관리하고,
회전 방향 동기화는 최소 bool 값만 복제하며,
실제 회전 연출은 RM 기반 몽타주를 사용

하는 구조로 정리했다.

profile
반갑습니다

0개의 댓글