[Unity3d][Animation] Dynamic playback AniamtionClip player based on Playable

Article directory

      • 0. Reasons and goals
      • 1.Principle and demo
        • 1.Play demo1
          • TestAnimationPlayer
          • Custom playback behavior:TestAnimationBlendBehaviour
        • 2. Play demo2 and mix in layers
      • 2. Encapsulation
        • Code structure

0. Reasons and goals

Unity provides Animator and AnimatorController for playing animations. Among them, AnimatorController serves as the animation controller and needs to configure each animation State in advance.
However, for the actual development process, the method of configuring the animation state in advance is not “flexible” enough.
Suppose that a certain model has n animationClips, and not all of them are used at runtime.
If you use the playback control method of AnimatorController, you need to configure all animationClips that may be played in advance.
Therefore, is it possible to implement a way to dynamically load animationClip and play animation?

Unity provides animation-related Playables that can be used by developers. Based on this, implement an animation player that can dynamically play animationClip.

1. Principle and demo

Unity provides the following animation-related types:

  • PlayableGraph
    • Figure, associate various Playables
  • AnimationPlayableOutput
    • Finally operate the Animator. Output the calculation results to Animator.
  • AnimationClipPlayable
    • Get the Playable of AnimationClip, which is the playback curve of the animation clip.
  • AnimationMixerPlayable
    • Animation mixer, you can set the weight of different InputIndex
  • AnimationLayerMixerPlayable
    • Animation level mixer. Mainly, you can set the Mask and Addtive of the current layer.
  • PlayableBehaviour
    • Customized playback behavior. Can change the behavior during playback.

The process of implementing animation playback is:

  1. Create a PlayableGraph, that is, create a graph. All subsequent input and output are in the graph.
  2. Create the output of the graph – output to Animator:AnimationPlayableOutput.
  3. Create Playables and set them up in the graph.
  4. Set the final node of the playable in the graph to the source of AnimationPlayableOutput.
    animationPlayableOutput.SetSourcePlayable(finalPlayable,0)

Refer to demoPlayableGraph.Connect provided in Unity documentation

1.Play demo1
TestAnimationPlayer
    using UnityEngine;
    using UnityEngine.Animations;
    using UnityEngine.Playables;

    namespace AnimationPlayer
    {<!-- -->
        public class TestAnimationPlayer : MonoBehaviour
        {<!-- -->
            public bool playClip;
            public AnimationClip clip;
            public AnimationClip clip2;

            private PlayableGraph _graph;

            void Update()
            {<!-- -->
                if(playClip)
                {<!-- -->
                    playClip = false;
                    TestPlay(clip,clip2);
                }
            }

            public void TestPlay(AnimationClip clip,AnimationClip clip2)
            {<!-- -->
                // 1. Create graph
                _graph = PlayableGraph.Create($"AnimationPlayer_{<!-- -->this.gameObject.name}");
                // 2. Create output
                AnimationPlayableOutput animOutput =
                    AnimationPlayableOutput.Create(_graph, "AnimationOut", this.GetComponent<Animator>());

                // 3. Create custom playback behavior
                var blendBehaviour = ScriptPlayable<TestAnimationBlendBehaviour>.Create(_graph, new TestAnimationBlendBehaviour(), 2);

                // 4. Create two clipPlayables based on animationClip
                var clipPlayable = AnimationClipPlayable.Create(_graph, clip);
                var clipPlayable2 = AnimationClipPlayable.Create(_graph, clip2);

                // 5. Create an animated mixer
                var mixer = AnimationMixerPlayable.Create(_graph, 2);
                blendBehaviour.GetBehaviour().mixerPlayable = mixer;

                // 6. Connect clipPlayable to the mixer.
                _graph.Connect(clipPlayable, 0, mixer, 0);
                _graph.Connect(clipPlayable2, 0, mixer, 1);

                // 7. Connect the mixer to a custom playback behavior
                _graph.Connect(mixer, 0, blendBehaviour, 0);

                // 8. Set custom playback behavior as the source of output
                animOutput.SetSourcePlayable(blendBehaviour, 0);

                // 9. Play
                _graph.Play();
            }
        }
    }
Customized playback behavior: TestAnimationBlendBehaviour
    using UnityEngine;
    using UnityEngine.Animations;
    using UnityEngine.Playables;

    namespace AnimationPlayer
    {<!-- -->
      public class TestAnimationBlendBehaviour:PlayableBehaviour
      {<!-- -->
        public AnimationMixerPlayable mixerPlayable;
        public override void PrepareFrame(Playable playable, FrameData info)
        {<!-- -->
          var value = Mathf.PingPong((float)playable.GetTime(), 1);
          // Dynamically modify the two input weights of mixerPlayable
          mixerPlayable.SetInputWeight(0,value);
          mixerPlayable.SetInputWeight(1,1-value);
        }
      }
    }
2. Play demo2, layered mixing

Just to try layer blending, so just create two layer blends.
In each layer, place only one AnimationClipPlayable (you can also mix clips in the layer, demo1 already has it)

        public void TestPlayLayerAnim()
        {<!-- -->
            _graph = PlayableGraph.Create($"AnimationPlayer_{<!-- -->this.gameObject.name}");
            AnimationPlayableOutput animOutput =
                AnimationPlayableOutput.Create(_graph, "AnimOut", this.GetComponent<Animator>());

            var blendBehaviour = ScriptPlayable<TestAnimationBlendBehaviour>.Create(_graph, new TestAnimationBlendBehaviour(), 2);

            var clipPlayable = AnimationClipPlayable.Create(_graph, clip);
            var clipPlayable2 = AnimationClipPlayable.Create(_graph, clip2);

            var mixer = AnimationMixerPlayable.Create(_graph, 2);
            blendBehaviour.GetBehaviour().mixerPlayable = mixer;


            // Level 0
            var layerMixer0 = AnimationLayerMixerPlayable.Create(_graph, 1);
            layerMixer0.SetLayerAdditive(1,false);
            
            // Level 1
            var layerMixer1 = AnimationLayerMixerPlayable.Create(_graph, 1);
            layerMixer1.SetLayerAdditive(1,true);

            //Associate clipPlayable with the hierarchy
            _graph.Connect(clipPlayable, 0, layerMixer0, 0);
            _graph.Connect(clipPlayable2, 0, layerMixer1, 0);
            
            //Associate the level with the mixer
            _graph.Connect(layerMixer0, 0, mixer, 0);
            _graph.Connect(layerMixer1, 0, mixer, 1);

            //Associate the mixer with a custom playback behavior
            _graph.Connect(mixer, 0, blendBehaviour, 0);

            animOutput.SetSourcePlayable(blendBehaviour, 0);

            _graph.Play();
        }

2. Encapsulation

The basic implementation has been run through in the demo, now let’s encapsulate it.
Since the project does not involve the layering of animations, only the normal playback and fade-in and fade-out of animations are implemented.
After encapsulation, the functions provided are:

  1. Play animation based on AnimationClip
  2. Play the animation in and out according to the AnimationClip
  3. The overall animation playback speed can be modified
  4. You can modify the playback speed of a single clip animation
Code structure
  1. ICAnimState
  • Defines the fields that will be provided externally if accessed.
    namespace AnimationPlayer
    {<!-- -->
      public interface ICAnimState
      {<!-- -->
        bool isLoop {<!-- --> get; }
        float length {<!-- --> get; }
        float speed {<!-- --> get; }

      }
    }

  1. CAnimClipState
  • Used internally by the packaged player
  • Created Playable based on AnimationClip
  • Can provide notifications for animation start and end playback
    using UnityEngine;
    using UnityEngine.Animations;
    using UnityEngine.Playables;

    namespace AnimationPlayer
    {<!-- -->
        public class CAnimClipState : ICAnimState
        {<!-- -->
            public AnimationClip clip {<!-- --> get; private set; }
            public bool isLoop => clip.isLooping;
            public float length => clip.length;
            public float speed {<!-- --> get; set; } = 1f;

            public AnimationClipPlayable clipPlayable {<!-- --> get; private set; }
            public float playTimer;
            public bool isPlaying;


            private CAnimClipState(AnimationClip clip, PlayableGraph graph)
            {<!-- -->
                this.clip = clip;
                clipPlayable = AnimationClipPlayable.Create(graph, clip);
            }

            public static CAnimClipState Create(AnimationClip clip, PlayableGraph graph)
            {<!-- -->
                return new CAnimClipState(clip, graph);
            }

            public void OnStateStart()
            {<!-- -->
                isPlaying = true;
                // You can notify the animation to start playing
                Debug.Log($"{<!-- -->clip.name} start play");
            }

            public void OnStateEnd()
            {<!-- -->
                isPlaying = false;
                // Can notify the end of animation playback
                Debug.Log($"{<!-- -->clip.name} end play");
            }
        }
    }

  1. CAnimTransition
  • Data required when animation fades in and out
    namespace AnimationPlayer
    {<!-- -->
        public class CAnimTransition
        {<!-- -->
            public CAnimClipState fromState;
            public CAnimClipState toState;
            public float transitionDuration;

            public float fromeStateFadeOutTime;
          }
    }
  
  1. CAnimPlayBehaviour Custom playback behavior (animation mixing)
    • Implemented playback animation and fade-in and fade-out playback animation
    using UnityEngine;
    using UnityEngine.Animations;
    using UnityEngine.Playables;

    namespace AnimationPlayer
    {<!-- -->
      public class CAnimPlayBehaviour : PlayableBehaviour
      {<!-- -->
              public PlayableGraph graph;
              public AnimationMixerPlayable mixerPlayable;
              private CAnimClipState _curPlayState;
              private float _playSpeed = 1f;
              private bool _isInTransition;
              private float _transitionTimer;
              private CAnimTransition _animTransition;


              public void SetPlaySpeed(float playSpeed)
              {<!-- -->
                  _playSpeed = playSpeed;
              }

              public void Play(CAnimClipState state)
              {<!-- -->
                  _curPlayState = state;
                  mixerPlayable.DisconnectInput(0);
                  mixerPlayable.DisconnectInput(1);
                  
                  // connectInput can use graph or mixerPlayable
                  // graph.Connect(_curPlayState.clipPlayable, 0, mixerPlayable, 0);
                  mixerPlayable.ConnectInput(0,_curPlayState.clipPlayable,0);
                  
                  mixerPlayable.SetInputWeight(0, 1f);

                  mixerPlayable.SetTime(0);
                  _curPlayState.clipPlayable.SetTime(0);
                  _curPlayState.playTimer = 0f;
                  _curPlayState.OnStateStart();
              }

              public void CrossFade(CAnimClipState state, float fadeDuration)
              {<!-- -->
                  var lastState = _curPlayState;
                  if (lastState == null)
                  {<!-- -->
                      Play(state);
                      return;
                  }

                  if (_curPlayState == state)
                  {<!-- -->
                      return;
                  }

                  _animTransition = _animTransition  new CAnimTransition();
                  _curPlayState = state;
                  _animTransition.fromState = lastState;
                  _animTransition.toState = _curPlayState;

                  _isInTransition = true;
                  var lastLeftTime = lastState.length - lastState.playTimer % lastState.length;
                  _animTransition.transitionDuration = Mathf.Min(lastLeftTime, fadeDuration);
                  _animTransition.fromeStateFadeOutTime = _animTransition.transitionDuration;
                  mixerPlayable.DisconnectInput(0);
                  mixerPlayable.DisconnectInput(1);

                  // connectInput can use graph or mixerPlayable
                  // graph.Connect(_curPlayState.clipPlayable, 0, mixerPlayable, 0);
                  // graph.Connect(lastState.clipPlayable, 0, mixerPlayable, 1);
                  mixerPlayable.ConnectInput(0,_curPlayState.clipPlayable,0);
                  mixerPlayable.ConnectInput(1,lastState.clipPlayable,0);

                  mixerPlayable.SetInputWeight(0, 0);
                  mixerPlayable.SetInputWeight(1, 1);

                  _curPlayState.clipPlayable.SetTime(0);
                  _curPlayState.playTimer = 0f;
                  mixerPlayable.SetTime(0);
                  _curPlayState.OnStateStart();
              }

              public override void PrepareFrame(Playable playable, FrameData info)
              {<!-- -->
                  if (_curPlayState == null) return;

                  if (_isInTransition)
                  {<!-- -->
                      _animTransition.fromeStateFadeOutTime -= info.deltaTime;
                      var fadeOutWeight = _animTransition.fromeStateFadeOutTime / _animTransition.transitionDuration;
                      if (fadeOutWeight > 0.001f)
                      {<!-- -->
                          mixerPlayable.SetInputWeight(0, 1 - fadeOutWeight);
                          mixerPlayable.SetInputWeight(1, fadeOutWeight);
                      }
                      else
                      {<!-- -->
                          mixerPlayable.SetInputWeight(0, 1f);
                          mixerPlayable.SetInputWeight(1, 0f);
                          if (!_animTransition.fromState.isLoop)
                          {<!-- -->
                              _animTransition.fromState.OnStateEnd();
                          }

                          _animTransition.fromState = null;
                          _animTransition.toState = null;
                          _animTransition.transitionDuration = 0f;

                          _isInTransition = false;
                      }
                  }


                  _curPlayState.playTimer + = info.deltaTime;
                  if (_curPlayState.isLoop)
                  {<!-- -->
                      return;
                  }

                  if (_curPlayState.playTimer >= _curPlayState.length)
                  {<!-- -->
                      _curPlayState.OnStateEnd();
                      _curPlayState = null;
                  }
              }

              public void Evaluate(float deltaTime)
              {<!-- -->
                  if (_curPlayState != null)
                  {<!-- -->
                      if (!graph.IsPlaying())
                      {<!-- -->
                          graph.Play();
                      }

                      graph.Evaluate(deltaTime * _playSpeed * _curPlayState.speed);
                  }
                  else
                  {<!-- -->
                      if (graph.IsPlaying())
                      {<!-- -->
                          graph.Stop();
                      }
                  }
              }
          }
    }
  1. CAnimationPlayer animation player
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Animations;
    using UnityEngine.Playables;

    namespace AnimationPlayer
    {<!-- -->
        public class CAnimationPlayer
        {<!-- -->
            private Dictionary<object, CAnimClipState> _stateMap = new Dictionary<object, CAnimClipState>();
            private float _playSpeed = 1f;

                public CAnimationPlayer(Animator animator)
                {<!-- -->
                    _animator = animator;
                    _Initialize();
                }

                public float playSpeed
                {<!-- -->
                    get => _playSpeed;
                    set
                    {<!-- -->
                        if (_playSpeed != value)
                        {<!-- -->
                            _playSpeed = value;
                            _player.SetPlaySpeed(_playSpeed);
                        }
                    }
                }

                public void Play(AnimationClip clip, float speed = 1f)
                {<!-- -->
                    if (!_stateMap.TryGetValue(clip, out var state))
                    {<!-- -->
                        state = CAnimClipState.Create(clip, _graph);
                        _stateMap.Add(clip, state);
                    }

                    state.speed = speed;
                    DoPlay(state);
                }

                public void CrossFade(AnimationClip clip, float fadeDuration = 0.2f, float speed = 1f)
                {<!-- -->
                    if (!_stateMap.TryGetValue(clip, out var state))
                    {<!-- -->
                        state = CAnimClipState.Create(clip, _graph);
                        _stateMap.Add(clip, state);
                    }

                    state.speed = speed;
                    DoCrossFade(state, fadeDuration);
                }

                private void DoPlay(CAnimClipState state)
                {<!-- -->
                    _player.Play(state);
                }

                private void DoCrossFade(CAnimClipState state, float duration = 0.2f)
                {<!-- -->
                    _player.CrossFade(state, duration);
                }

                #region Core Logic

                private PlayableGraph _graph;
                private bool _graphInitialized;
                private Animator _animator;
                private CAnimPlayBehaviour _player;

                private void _Initialize()
                {<!-- -->
                    if (_graphInitialized)
                    {<!-- -->
                        return;
                    }

                    _graphInitialized = true;
                    _graph = PlayableGraph.Create($"CAnimationPlayer_{<!-- -->this._animator.name}");

                    // Custom update method
                    _graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
                    var animOutPut = AnimationPlayableOutput.Create(_graph, "CAnimation", _animator);
                    var mixerPlayable = AnimationMixerPlayable.Create(_graph, 2);
                    var mixBehaviour = ScriptPlayable<CAnimPlayBehaviour>.Create(_graph, 2);
                    _player = mixBehaviour.GetBehaviour();
                    _player.graph = _graph;
                    _player.mixerPlayable = mixerPlayable;

                    _graph.Connect(mixerPlayable, 0, mixBehaviour, 0);
                    animOutPut.SetSourcePlayable(mixBehaviour);
                }

                public void Update()
                {<!-- -->
                    _player.Evaluate(Time.deltaTime);
                }

                #endregion
            }
    }

  1. play test
    using UnityEngine;

    namespace AnimationPlayer
    {<!-- -->
        public class TestCAnimationPlayer : MonoBehaviour
        {<!-- -->
            public bool playClip;
            public bool crossFade;
            public AnimationClip clip1;
            public AnimationClip clip2;
            public float playSpeed = 1f;

            private CAnimationPlayer _animPlayer;
            private Animator _animator;

            private void Awake()
            {<!-- -->
                _animator = GetComponent<Animator>();
                _animPlayer = new CAnimationPlayer(_animator);
            }

            public void Update()
            {<!-- -->
                _animPlayer.playSpeed = playSpeed;
                if(playClip)
                {<!-- -->
                    playClip = false;
                    _animPlayer.Play(clip1);
                }

                if(crossFade)
                {<!-- -->
                    crossFade = false;
                    _animPlayer.CrossFade(clip2);
                }
            }

            void LateUpdate()
            {<!-- -->
                _animPlayer.Update();
            }
        }
    }

above.