본문 바로가기

공부/유니티

[Unity] PrimeTween으로 모듈형 애니메이션 시스템 만들기 (feat. 커맨드 패턴)

상황

유니티에서 UI나 오브젝트 연출을 만들 때 기본 Animation이나 Animator 컴포넌트를 사용하면 무겁고 불필요한 오버헤드가 발생한다. 이를 대체하기 위해 코드로 제어하는 DOTween이 널리 쓰이지만, 최근에는 가비지 컬렉션(GC)을 전혀 발생시키지 않는(Zero Allocation) PrimeTween이 압도적인 성능을 자랑하며 각광받고 있다.

하지만 애니메이션을 순수 코드로만 작성하면 목표 위치, Ease(곡선), 타이밍 등을 한땀 한땀 수정하고 플레이 모드에 진입해 확인해야 하는 번거로움이 있다. 성능은 챙기면서 유니티 인스펙터에서 애니메이션 클립을 다루듯 직관적이고 편하게 조작할 수 있는 방법이 필요했다. 그래서 PrimeTween 기반의 커스텀 모듈형 애니메이션 시스템을 직접 구축하게 되었다.


핵심 설계 및 아키텍처

이 시스템을 관통하는 가장 중요한 키워드는 커맨드 패턴, 개방-폐쇄 원칙(OCP), ReorderableList, 그리고 스크럽이다.

 

1. 커맨드 패턴과 OCP 적용
이동, 회전, 크기 조절, 대기(Delay) 등 각각의 연출을 독립된 모듈(Command)로 캡슐화했다. 새로운 연출(예: 파티클 이미션 조절, 머티리얼 색상 변경)이 필요하면 기존 시스템(실행기)의 코드를 단 한 줄도 수정할 필요 없이, 추상 클래스인 TweenCommand를 상속받는 클래스만 추가하면 된다.

 

2. ReorderableList를 활용한 직관적인 타임라인
[SerializeReference] 속성을 활용해 다양한 커맨드 모듈을 하나의 리스트에 담고, 커스텀 에디터에서 ReorderableList로 그려냈다. 기획자나 디자이너가 인스펙터 상에서 마우스 드래그 앤 드롭만으로 애니메이션 순서를 바꾸고, 그룹을 묶고, 랜덤 딜레이를 삽입할 수 있다.

 

3. 스크럽(Scrubbing) 기능과 성능(Zero GC)
PrimeTween의 강력한 시퀀스 제어 기능을 활용해 에디터 씬 뷰(Scene View)에서 슬라이더를 움직여 애니메이션을 실시간으로 확인하는 스크럽 기능을 구현했다. 플레이 모드에 들어갈 필요가 없어 작업 속도가 비약적으로 상승한다. 동시에 GC 발생이 없으므로 모바일 환경에서도 프레임 드랍 없는 최고의 성능을 보장한다.


해결 방법 및 구현

1. 커맨드 추상화 (TweenCommand.cs)

모든 애니메이션 모듈의 부모가 되는 베이스 클래스다. 다형성을 이용해 각 모듈이 어떻게 동작할지(Execute)만 스스로 정의하게 만든다.

[Serializable]
public abstract class TweenCommand {
    [HideInInspector] public bool isFolded = false;
    public virtual bool IsDelayCommand => false;

    public abstract void Execute(ref Sequence sequence, bool isJoinMode, float timeScale = 1f, float startDelay = 0f);

    public virtual void RecordStartValues(bool isForce) { }
    public virtual void RestoreStartValues() { }
}

 

2. 실행기 (TweenExecutor.cs)

게임 오브젝트에 부착되어 실제 타임라인을 관리한다. 리스트에 담긴 커맨드들을 순회하며 PrimeTween의 Sequence를 조립한다.

public class TweenExecutor : MonoBehaviour {
    [SerializeReference]
    public List<TweenCommand> commands = new List<TweenCommand>();

    private Sequence mainSequence;

    public void Play() {
        StopAllTweens();
        mainSequence = Sequence.Create(cycles: isLoop ? -1 : cycleCount);
        BuildSequence(ref mainSequence);
    }

    private void BuildSequence(ref Sequence seq) {
        bool nextIsJoinMode = false;
        float groupDelayCursor = 0f;

        foreach (var command in commands) {
            if (command == null) continue;
            
            // Join 커맨드 처리
            if (command is JoinTweenCommand) {
                nextIsJoinMode = true;
                continue;
            }

            // 개별 커맨드의 Execute를 호출해 시퀀스 조립
            command.Execute(ref seq, nextIsJoinMode, timeScale, groupDelayCursor);
            nextIsJoinMode = false;
        }
    }
}

 

3. 커스텀 에디터 & 스크러빙 (TweenExecutorEditor.cs)

인스펙터의 시각적 편의성을 위해 ReorderableList를 적용하고, 각 커맨드 타입에 따라 배경색을 다르게 칠해 가독성을 높였다. 가장 중요한 스크럽 기능은 슬라이더의 값을 PrimeTween Sequence의 progress에 대입하여 구현했다.

// 스크럽 슬라이더 UI 부분 (OnInspectorGUI 내부)
if (_target.IsScrubbing && _target.IsSequenceAlive()) {
    GUILayout.Space(10);
    EditorGUI.BeginChangeCheck();
    
    // 슬라이더 조작
    float p = EditorGUILayout.Slider("스크럽 타임라인", _target.GetSequenceProgress(), 0f, 1f);
    
    if (EditorGUI.EndChangeCheck()) {
        _target.SetSequenceProgress(p); // mainSequence.progress = p;
        SceneView.RepaintAll(); // 씬 뷰 즉각 갱신
    }
}

 

결과적으로 이 시스템은 애니메이터 컴포넌트의 고질적인 무거움을 덜어내는 동시에, 코딩 없이도 복잡한 연출을 손쉽게 짤 수 있는 환경을 만들어 주었다. 새로운 애니메이션 로직이 필요해도 구조적 수정 없이 유연하게 확장할 수 있으며, 개발과 기획 간의 이터레이션(수정 반복) 시간을 획기적으로 줄여주는 컴포넌트를 구현할 수 있었다.

 


결과물

결과물 영상