Playable & Animation

이오싶·2025년 10월 15일

Animation Controller는 경직적이고 불편하다. 애니메이션 FSM을 컴파일 타임에 미리 생성하여 이용해야 하고 엔진 내에서 세세하게 조정하거나 동적으로 바꾸기 어렵다.

본디 Timeline의 기능 확장을 위해 도입된 PlayablePlayable Graph를 활용하면 Animation Controller를 사용하지 않고 트리 구조의 애니메이션 블렌딩 그래프를 작성하여 적용하고 런타임에 변화를 적용하는 것이 가능하다.

Playable

Unity에서 Playable일정 시간동안 재생되는 형태의 개체를 다루기 위한 API 및 API의 기반 클래스이다.

‘일정 시간 재생’이라는 개념이 핵심으로, 기존에는 타임라인 기능을 보조하기 위해(타임라인의 애니메이션, 오디오 등 다양한 클립의 기본 클래스) 도입되었지만, 그래프 구조를 통해 플레이어블을 혼합하고 통제할 수 있는 기능이 추가된 덕분에 동적 애니메이션 블렌딩에 효과적으로 사용할 수 있다.

PlayableGraph

PlayableGraph는 애니메이션, 오디오, 비디오 등 시간 기반 데이터를 처리하기 위한 그래프 구조를 정의한다.

플레이어블 그래프는 하나 이상의 플레이어블 출력(Playable Output)을 포함하며, 해당 출력들이 어떤 플레이어블을 입력하고 조합하여 실행되어야 하는지를 정의한다.

PlayableGraph는 코드로 직접 생성/제어가 가능하다.

var graph = PlayableGraph.Create(string name);

이처럼 이름을 붙여 생성하면, 그 안에서 다양한 애니메이션 노드를 조립할 수 있다.

Playable Output

코어 PlayableOutput 타입

플레이어블 출력은 애니메이션, 오디오 클립, 스크립트와 같은 개체를 실행하는 컴포넌트와 결합하여 그래프로 구성된 플레이어블 조합을 실행하는 역할을 한다.

가령, 애니메이션 출력은 생성 시 특정 Animator 컴포넌트와 연결되며, 애니메이션 컨트롤러를 미리 결합하지 않고도 애니메이션 플레이어블의 조합 결과를 해당 컴포넌트에서 실행할 수 있게 한다.

한편 동일한 Playable Graph에 정의된 Output 및 Playable은 동일한 실행 루프에서 실행되기에, 애니메이션과 오디오 등 다양한 미디어 개체를 동기화하고 통제하기가 용이하다.

ex) 애니메이션의 특정 시점에 특정 오디오 발생시키기 등.

Playable 노드

코어 Playable 타입

Playable은 플레이어블 그래프의 노드 역할을 하는 구조체로, 클립을 실행하거나 여러 클립을 섞는 등 다른 노드와 연결되어 동작한다. 생성 시 해당 Playable이 실행되는 PlayableGraph를 인자로 요구하며, 해당 그래프에 귀속된다.

입출력과 가중치

Playable 노드는 입력 슬롯(Input)출력 슬롯(Output)을 가진다. 각 노드의 입력과 출력이 서로 연결되며 그래프 형태를 이루고, 최종 출력이 PlayableOutput에 전달되며 실행된다.

  • Input
    • 각 노드는 하나 이상의 입력 슬롯을 가진다.
    • 노드의 입력 슬롯의 개수는 Playable.SetInputCount(count)를 통해 런타임에 정할 수 있다.
    • Playable.SetInputWeight(index, weight)를 통해 각 입력의 기여도, 즉 가중치(Weight)를 조절할 수 있다.
      • 예를 들어, 두 개의 애니메이션 클립을 0.5 / 0.5 비율로 섞거나, 특정 상황에서 한쪽 클립의 가중치를 1로 줘서 전환하는 식이다.
      • 이를 활용하면 애니메이션 블렌딩, 조건부 전환을 런타임에서 동적으로 처리할 수 있다.
  • Output
    • 각 노드는 하나 이상의 노드에 실행 결과를 출력할 수 있다.
    • Playable.SetOutputCount(count)를 통해 출력 슬롯 개수를 정할 수 있다.
  • PlayableGraph.Connect를 통해 두 노드의 입력과 출력을 각각 연결할 수 있으며, 최종 출력이 PlayableOutput에 연결되어야 그래프의 결과를 출력할 수 있다.
    • 이 때 출력하는 주체를 source, 입력받는 주체를 destination으로 칭한다.

      playableGraph.Connect(IPlayable source, int sourceOutputPort, IPlayable destination, int destinationInputPort);

특히나 각 입출력의 연결과 가중치를 런타임에 정할 수 있다는 특징이 Playable을 사용한 애니메이션을 Animation Controller를 사용하는 경우보다 유연하고 강력하게 만든다.

시스템 구조

Playable 및 PlayableGraph, PlayableOutput 등 Playable API에서 제공되는 대부분의 타입은 구조체 타입이다. 이 구조가 조금 특이한데, 기본적으로 구조체는 Call by Value 타입이기에 하나의 개체를 여러 레퍼런스를 통해 통제하는 데에 부적절하기 때문이다.

그러나 Playable API의 이러한 구조체 타입들은 모두 래퍼(Wrapper)들로, 실제 실행 루프를 따라 Playable을 재생하는 개체들은 네이티브 메모리에 저장되고 각 구조체들은 해당 개체의 레퍼런스만을 가지는 형태이다. 따라서, 구조체를 통해 실제 Playable 개체를 통제할 수 있는 핸들을 전달할 수 있게 된다.

그리고 해당 개체들은 GC에 의해 관리되지 않는 비관리 리소스이며, Destroy()를 통해 명시적으로 해제되지 않으면 메모리 누수가 발생한다. 다행히도, PlayableGraph를 파괴하는 것 만으로 해당 그래프 내에서 생성된 모든 Playable 및 PlayableOutput 노드는 잇달아 파괴되므로, 각 개체를 하나하나 추적하여 직접 관리할 필요는 적다.

PlayableExtensions

매우 다양한 타입의 Playable이 존재하는데, Playable 자체는 구조체인 만큼 각 구조체는 상속관계가 없으며 대신 IPlayable 인터페이스를 구현하는 형태로 정의된다. 이때 IPlayable에 이들이 공유해야 하는 메서드를 선언하고 각 구조체에 동일한 정의를 중복하여 작성하는 불편을 피하기 위해, PlayableExtensions에 정의된 확장 메서드를 많이 사용한다.

확장 메서드는 기존 클래스, 구조체, 인터페이스의 원본 코드를 수정하지 않고 새로운 메서드를 추가할 수 있게 해주는 기능이다.
1. static 클래스 안에 선언해야 한다.
2. 메서드 자신도 static 이어야 한다.
3. 첫 번째 매개변수(parameter)의 타입 앞에 this 한정자를 붙인다. 이 this가 붙은
매개변수가 확장 메서드가 적용될 타입이 되며, 해당 타입의 동적 메서드처럼 사용 가능하다.

// 예시: PlayableExtensions.Play
public static void Play<U>(this U playable) where U : struct, IPlayable
// IPlayable 구조체의 동적 메서드처럼 사용 가능
playable.Play();

주로 사용되는 메서드(PlayableExtensions 포함)

  • Playable.Play() 플레이어블을 재생한다.
  • Playable.Pause() 플레이어블을 정지한다.
  • Playable.SetInputCount(int value) 플레이어블 노드의 입력 개수를 결정한다.
  • Playable.SetInputWeight(int inputIndex, float weight) 플레이어블 노드의 입력 포트의 가중치를 결정한다.
  • Playable.SetOutputCount(int value) 플레이어블 노드의 출력 개수를 결정한다.
  • Playable.GetDuration(), Playable.SetDuration(double value) 플레이어블의 duration(지속시간)을 반환하거나 결정한다. 단위는 초(s).
  • Playable.GetTime(), Playable.SetTime(double value) 플레이어블의 현재 로컬 시간(플레이어블 시작 이후로 지난 시간)을 반환하거나 결정한다. 단위는 초(s).
  • Playable.GetSpeed(), Playable.SetSpeed(double value) 플레이어블의 실행 속도 배수를 받아오거나 결정한다. 기본값은 1.
  • PlayableGraph.Connect(IPlayable source, int sourceOutputPort, IPlayable destination, int destinationInputPort) 두 플레이어블의 입출력을 연결한다.
  • PlayableOutput.SetSourcePlayable(IPlayable source) 플레이어블 출력의 source를 연결한다.
  • PlayableGraph.Destroy() 플레이어블 그래프와 종속된 모든 플레이어블, 플레이어블 출력을 파괴(할당 해제)한다.

Animation

이제 애니메이션 실행을 위해 정의된 Playable에 대해 알아보고, 어떻게 Animation Controller를 대체할 수 있는지 알아보자.

AnimationPlayableOutput

애니메이션을 담당하는 PlayableOutput 구조체. 생성 시 애니메이션을 실행시킬 Animator 컴포넌트와 연결되어 출력을 전달한다.

var output = AnimationPlayableOutput.Create(PlayableGraph graph, string name, Animator animator); // 생성
output.SetSourcePlayable(IPlayable sourcePlayable); // 출력 소스 연결

AnimationClipPlayable

단일 애니메이션 클립을 실행하는 Playable 구조체.

var clipPlayable = AnimationClipPlayable.Create(PlayableGraph graph, AnimationClip clip);

AnimationControllerPlayable

AnimationController를 실행하는 Playable. 기존 AnimationController의 FSM 구조를 사용하고자 할 때 유용하다.

var controllerPlayable = AnimatorControllerPlayable.Create(PlayableGraph graph, AnimationController controller);

AnimationMixerPlayable

여러 애니메이션을 블렌딩하는 Playable.

AnimationController의 Blend Tree처럼, 입력으로 받은 여러 애니메이션 Playable을 가중치에 따라 블렌딩한다.

var mixer = AnimationMixerPlayable.Create(PlayableGraph graph, int inputCount);

AnimationLayerMixerPlayalbe

애니메이션을 Layer에 따라 블렌딩하는 Playable.

AnimationController의 Layer 기능처럼, 입력으로 받은 여러 애니메이션 Playable을 레이어로 나누고 각 레이어의 가중치에 따라 블렌딩한다. 이 때 각 레이어에 AvatarMask를 적용하여 해당 마스크에만 애니메이션을 적용하는 것도 가능하다.

var layerMixer = AnimationLayerMixerPlayable.Create(PlayableGraph graph, int inputCount);
layerMixer.SetLayerMaskFromAvatarMask(uint layerIndex, AvatarMask mask);

커스텀 Playable

PlayableBehaviour

Unity는 사용자가 직접 커스텀 로직을 가진 Playable을 만들 수 있도록 PlayableBehaviour 클래스를 제공한다.

  • 주요 메서드:
    • OnGraphStart, OnGraphStop → 그래프 실행/종료 시 호출.
    • OnBehaviourPlay, OnBehaviourPause → Playable 재생/일시정지 시 호출.
    • PrepareFrame → 매 프레임 업데이트 직전에 호출.

예를 들어, 특정 조건에서 애니메이션 속도를 변경하거나, 커스텀 효과(예: 카메라 흔들림)를 삽입할 수 있다.

public class SpeedControlPlayable : PlayableBehaviour
{
    public float speed = 1f;

    public override void PrepareFrame(Playable playable, FrameData info)
    {
        playable.SetSpeed(speed);
    }
}

PlayableBehaviour를 통해 정의된 커스텀 플레이어블은 ScriptPlayable<T> 형태로 생성하여 그래프에 추가할 수 있다.

ScriptPlayable<SpeedControlPlayable> speedControl = ScriptPlayable<SpeedControlPlayable>.Create(playableGraph);
SpeedControlPlayable speedBehaviour = speedControl.GetBehaviour();
speedBehaviour.speed = 0.7f;  // 플레이어블 실행 속도를 조정

위 예제에서 speedBehaviourspeed를 조정하면 해당 플레이어블의 속도를 실시간으로 변경할 수 있다.

Playable Graph Visualizer

위 내용에 플레이어블 그래프의 구조를 시각화한 도식이 포함되어 있지만, PlayableGraph는 기본적으로 스크립트를 통해 생성 및 조정되며 Animation Controller나 Shader Graph처럼 GUI를 통해 생성하거나 수정하는 기능을 기본적으로 제공하지 않는다.

위 도식은 Playable Graph Visualizer라는 별도 패키지를 사용하여 이미 생성된 PlayableGraph를 시각화한 모습이다. 해당 패키지는 생성된 PlayableGraph의 노드 연결 관계 및 가중치, 실행 여부 등을 실시간으로 보여주어 디버깅을 간편하게 해주며, 직접 PlayableGraph를 조정하는 기능은 제공하지 않는다.

Unity 6.2에서 도입된 Graph Toolkit을 사용하면 Playable Graph를 GUI로 조정하는 툴을 별도로 만드는 것도 가능할지도 모르겠다.

사용법

https://github.com/Unity-Technologies/graph-visualizer

위 레포지토리에서 git URL을 복사하여 Package Manager을 통해 다운받는다.

https://github.com/Unity-Technologies/graph-visualizer.git

이후 Window > Analysis > PlayableGraph Visualizer 메뉴를 클릭해 Visualizer를 사용한다.

PlayableGraph가 런타임에 생성될 경우 해당 시점에만 PlayableGraph를 확인할 수 있으므로 게임 실행 전에 그래프가 보이지 않는다고 당황하지 말자.

좌측 상단 드롭다운 메뉴에서 생성된 PlayableGraph를 선택하여 볼 수 있다.

profile
Iosif2510

0개의 댓글