AI 상태 관리는 StateTree가 담당하고,
GAS는 상태 표현 및 Gameplay 실행만 담당하도록 역할을 분리했다.
현재 흐름은 다음과 같다.
MoveToTargetActorTask 실행GameplayEffect(GE) 적용GE를 통해 State.Move 태그 부여GE 제거를 통한 State.Move 태그 제거핵심은 GameplayTag를 직접 관리하지 않고,
GameplayEffect를 통해 부여한 점이다.
MoveGEHandle = ApplyGameplayEffectToSelf(MoveStateGE)
이렇게 하면 GE가 유지되는 동안만 이동 태그가 활성화된다.
이후 이동 종료 시:
RemoveActiveGameplayEffect(MoveGEHandle)
처럼 GE만 제거하면 연결된 GameplayTag도 자동으로 제거된다.
덕분에:
같은 장점을 얻을 수 있었다.
결과적으로:
StateTree는 상태를 결정하고,
GAS는 상태를 표현한다
는 구조로 정리했다.
이동과 마찬가지로 공격 상태를 표현하기 위해 먼저 GE_Attack을 적용하고, GE에서 공격 태그(State.Combat)를 부여하는 방식으로 구성했었다.
하지만 구조를 정리하면서 이 방식이 GAS의 의도와 맞지 않는다는 걸 알게 되었다.
GameplayEffect는 기본적으로:
같은 역할에 더 가깝다.
예를 들어:
처럼 사용하는 것이 일반적인 흐름이었다.
반면 내가 원했던 건:
“GA_Attack이 실행되는 동안만 공격 태그를 유지”
하는 것이었다.
이 경우에는 GE보다 Activation Owned Tags가 더 적절했다.
그래서 구조를 다음처럼 수정했다.
GA_Attack 실행덕분에 별도의 GE를 추가/제거하지 않아도
공격 중 상태를 자연스럽게 표현할 수 있게 되었다.
결과적으로:
지속적인 상태 표현은 GE,
Ability 실행 중 상태 표현은 Activation Owned Tags
로 역할을 명확하게 분리했다.
AI가 공격을 마친 뒤, 플레이어가 바로 근처에 있어도 다시 공격하지 못하는 문제가 있었다.
원인은 공격 종료 후 항상 Move State로 전환되도록 구성한 흐름에 있었다.
기존 흐름은 다음과 같았다.
MoveToActor() 호출OnMoveCompleted()에서 다음 상태 처리문제는 대상이 이미 Acceptance Radius 내부에 있는 경우였다.
이때 MoveToActor()는 실제 이동 없이 AlreadyArrived를 반환했고,
ReceiveMoveCompleted에 바인딩해둔 OnMoveCompleted()가 호출되지 않았다.
// MoveToTartgetActorTask.cpp
// 이미 도착한 경우
else if (MoveRequestResult == EMoveRequestResult::AlreadyArrived)
{
CachedAIController->ReceiveMoveCompleted.RemoveDynamic(this,
&UMoveToTargetActorTask::OnMoveCompleted);
return; // 공격 State로 전환
}
결과적으로 AI는 이동도 하지 않고, 다음 상태 전환도 발생하지 않아 공격을 다시 수행하지 못했다.
해결 방법은 단순했다.
else if (MoveRequestResult == EMoveRequestResult::AlreadyArrived)
{
CachedAIController->ReceiveMoveCompleted.RemoveDynamic(this,
&UMoveToTargetActorTask::OnMoveCompleted);
OnMoveCompleted(...);
}
이미 도착한 상태라면 이동 완료 콜백을 직접 호출하도록 수정했다.
이후에는:
흐름이 자연스럽게 이어지도록 개선할 수 있었다.