게임 개발의 액션 상태 관리에 대한 나의 생각
2026년 3월 20일
본문에서는 게임 개발에서 흔히 발생하는 액션 상태의 덮어쓰기 및 복구 실패 버그를 바탕으로, 이제 막 시작하는 팀에서의 실전 경험을 결합하여 Unity에서 Active State Pool과 우선순위 동적 평가 메커니즘을 통해 확장성이 높고 결합도가 낮은 액션 상태 머신을 구축하는 방법을 공유합니다.
업무적 고충: 내가 겪은 액션 상태 덮어쓰기 버그
최근에 이제 막 시작한 지 얼마 되지 않은 팀에 합류하여 전투 시스템과 표현 계층 로직을 정리하던 중, 아주 고전적이면서도 골치 아픈 버그를 만났습니다.
구체적인 시나리오는 이렇습니다: 캐릭터에게 5초 동안 지속되는 '빙결' 디버프가 적용되어 액션이 [빙결] (제자리에 얼어붙어 떨고 있는 연출) 상태로 전환됩니다. 그런데 2초가 지났을 때, 캐릭터가 적의 기절 효과가 있는 강타를 맞아 2초 동안 지속되는 [기절] 액션(머리를 감싸고 비틀거리는 연출)이 트리거되었습니다.
진짜 문제는 기절 액션이 끝난 후에 발생했습니다: 2초간의 기절 연출이 끝났을 때, 내부 로직상 캐릭터에게는 아직 1초의 '빙결' 디버프가 남아 있습니다. 하지만 실제 액션은 [빙결] 상태로 복구되지 않고, 그대로 기본 상태인 [대기(Idle)] 상태로 돌아가 버렸습니다. 결과적으로 플레이어는 캐릭터가 멀쩡히 서서 대기 모션을 취하고 있는데 조작은 불가능한(로직상으로는 아직 얼어붙어 있기 때문) 상황을 보게 되었고, 시각적 연출과 내부 로직이 심각하게 괴리되는 결과가 초래되었습니다.
코드를 확인해 보니 원인은 간단했습니다. 팀의 이전 상태 제어 시스템이 트리거 방식이었기 때문입니다. 기절이 발생하면 Animator가 강제로 기절을 재생하게 하고, 기절이 끝나면 Animator를 단순히 기본 상태로 복구시켰을 뿐, 현재 지속 중인 다른 로직 효과를 전혀 고려하지 않았던 것입니다.
상태 관리의 보편성과 전통적 방식의 한계
저는 이 문제가 게임 개발에서 매우 보편적이라고 생각합니다. 복잡한 Buff 시스템이나 여러 상태가 중첩되는 스킬 효과가 존재하는 한, 액션 상태의 덮어쓰기와 복구 문제는 반드시 직면하게 됩니다.
초기 단계의 팀이 빠르게 결과물을 내기 위해 흔히 채택하지만, 제가 실전에서 반대하는 방식은 다음 두 가지입니다:
리스크 A: 각 시스템이 Animator 변수를 직접 조작
Buff 시스템, 스킬 시스템, 피격 시스템이 각각 직접 animator.SetBool("isFrozen", true)를 호출하는 방식입니다. 시스템이 커지면 어떤 코드가 언제 Bool 값을 false로 되돌렸는지 파악하기 어려워지고, 상태 머신이 꼬여서 디버깅이 극도로 힘들어집니다.
리스크 B: Unity Mecanim의 전이 네트워크 남용
Unity의 Animator 창은 전이(Transition) 선이 적을 때는 직관적입니다. 하지만 여기에 Speed > 0, isFrozen == true 같은 수많은 조건을 넣기 시작하면, 결국 상태 머신은 거미줄처럼 복잡해집니다. 새로운 상태를 하나 추가할 때마다 수많은 선을 새로 연결해야 하므로 유지보수 비용이 기하급수적으로 상승합니다.
나의 해결책: Active State Pool과 우선순위 동적 평가
상태가 서로 덮어씌워지고 제대로 복구되지 않는 문제와 과도한 결합도를 해결하기 위해, 저는 팀에 Active State Pool(활성 상태 풀)과 우선순위 동적 평가 메커니즘을 도입했습니다.
핵심 아이디어는 딱 하나입니다: '상태 요청'과 '상태 표현'을 완전히 분리하는 것입니다.
로직 계층(Buff나 스킬 시스템)은 자신이 원하는 액션 상태를 **등록(Register)**하고 **해제(Unregister)**하는 역할만 수행합니다. 그리고 통합된 AnimationStateManager가 현재 프레임에서 실제로 Animator에 어떤 명령을 내릴지 최종 결정합니다.
핵심 메커니즘 플로우차트:
Loading diagram...
이 구조에서는 우선순위 100인 '기절'이 끝나고 등록 해제되면, 매니저가 다시 풀을 평가하여 남아있는 최고 우선순위가 50인 '빙결'임을 확인합니다. 그러면 자동으로 Animator에게 명령을 내려 [빙결] 액션으로 되돌립니다. 이전의 버그는 자연스럽게 해결됩니다.
Unity 실전 적용: Animator를 순수하게 유지하기
위 아키텍처를 적용하면 Unity Animator 사용 방식도 바뀌어야 합니다. 복잡한 전이 선과 Trigger/Bool 조건을 버리고, Animator를 순수한 액션 플레이어로 되돌리는 것입니다.
Animator Controller에는 필요한 State들을 넣어두기만 하면 됩니다. 선을 연결할 필요가 없으며 (또는 AnyState에서 조건 없이 연결), 코드 레벨에서 Animator.CrossFade()나 Animator.Play()를 사용하여 직접 상태 전환을 제어합니다.
핵심 매니저 의사 코드 예시:
// 1. 액션 상태 요청 데이터 구조 정의
public class AnimStateRequest
{
public string StateName; // Animator 내 상태 이름
public int Priority; // 우선순위
public object Source; // 소스(해제 시 검증용, 예: 특정 Buff 인스턴스)
}
// 2. 통합 매니저
public class AnimationStateManager : MonoBehaviour
{
private Animator _animator;
private List<AnimStateRequest> _activeStates = new List<AnimStateRequest>();
private string _currentPlayingState;
// 상태 등록
public void AddStateRequest(string stateName, int priority, object source)
{
_activeStates.Add(new AnimStateRequest { StateName = stateName, Priority = priority, Source = source });
EvaluateHighestPriorityState();
}
// 상태 해제
public void RemoveStateRequest(object source)
{
_activeStates.RemoveAll(req => req.Source == source);
EvaluateHighestPriorityState();
}
// 현재 최고 우선순위 상태 평가
private void EvaluateHighestPriorityState()
{
if (_activeStates.Count == 0)
{
PlayAnimation("Idle"); // 기본 상태
return;
}
// 가장 높은 우선순위 요청 찾기
_activeStates.Sort((a, b) => b.Priority.CompareTo(a.Priority));
string targetState = _activeStates[0].StateName;
if (_currentPlayingState != targetState)
{
PlayAnimation(targetState);
}
}
// Unity Animator 구동
private void PlayAnimation(string stateName)
{
_currentPlayingState = stateName;
// CrossFade로 부드러운 전환(0.1f는 전환 시간)
_animator.CrossFade(stateName, 0.1f);
}
}
아키텍처 확장: 더 복잡한 액션 시스템을 위한 구상
이 활성 풀과 우선순위 평가 아키텍처는 게임이 복잡해짐에 따라 훌륭한 확장성을 제공합니다. 앞으로의 개발에서 다음과 같은 확장도 고려해 볼 수 있습니다:
- 동일 우선순위 충돌 처리: 만약 우선순위 50인 요청이 동시에 존재한다면, '최신 요청 우선' 혹은 '남은 지속 시간이 긴 요청 우선'과 같은 로직을 추가할 수 있습니다.
- 부위 마스크(Avatar Mask) 독립 관리: 3D 게임에서는 상체는 사격하고 하체는 피격되거나 달리는 경우가 많습니다. 이때 독립적인 두 매니저(UpperBodyPool, LowerBodyPool)를 두고 각각 평가한 후
CrossFade의 다른 Layer 인덱스로 전달하여 상하체를 완벽하게 분리할 수 있습니다. - Playable API 활용: 정밀한 프레임 관리가 필요한 액션에서는 Animator의 업데이트 지연이 문제가 될 수 있습니다. 이때 이 상태 매니저를 Unity 하위 레벨의 Playable Graph와 결합하여 C#에서 AnimationClip을 직접 믹싱함으로써 애니메이션의 주도권을 완전히 가져올 수 있습니다.
지금까지 실제 프로젝트 개발 과정에서 액션 상태 관리에 대해 느낀 저의 생각과 요약이었습니다. 이 방식은 저희 팀의 현재 고민을 해결해 주었지만, 모든 상황에 완벽한 '은탄환'은 아닐 것입니다. 저의 견해에 한계가 있을 수 있으니, 더 좋은 방안이 있거나 구현상 미흡한 점이 있다면 언제든지 소통해 주세요. 지적해 주시면 감사히 배우겠습니다.
- 본 콘텐츠는 iknowabit 팀에서 작성한 창작물입니다. 기술 지원: Unity 엔진 및 C# 고급 아키텍처 패턴 탐구 기반. *