Wayne Johnson and I have presented the Playable API at GDC 2016 and we would like to share with you what we have talked about.

The Playable is a new graph-like API in Unity that allows to have precise programmatic control over Animation and Audio. We are also planning on having video use the same API.

Please be aware that the API is still experimental as we are still working on making it very slick and easy to use. The examples provided here work with Unity 5.4. We expect that there will be changes in the API again before it’s integrated into one of our future releases. Keep an eye on our Roadmap for updates.

The simple case : Playing a clip.

One of the main goals of the playable system is to make sure simple things are simple to do. So let’s start with the basics and play a clip.

#using UnityEngine.Experimental.Director; ... AnimationClip clip ; // assign to your clips AnimationClipPlayable playable = AnimationClipPlayable.Create(clip); GetComponent<Animator>().Play(playable); 1 2 3 4 5 #using UnityEngine.Experimental.Director; . . . AnimationClip clip ; // assign to your clips AnimationClipPlayable playable = AnimationClipPlayable . Create ( clip ) ; GetComponent < Animator > ( ) . Play ( playable ) ;

Easy ? Now, we can control time and speed simply like this:

playable.speed = speed; playable.time = time; 1 2 playable . speed = speed ; playable . time = time ;

A step further : Creating a dynamic Blend Tree

BlendTrees allow to blend animation values between different AnimationClips or even between other BlendTrees. Mecanim’s BlendTrees are quite powerful, but they need to be built in the Editor. With Playables, it’s now possible to create and control BlendTrees at runtime. We can do that using the AnimationMixerPlayable:

AnimationClip backwardClip; // assign to your clips AnimationClip forwardClip; // assign to your clips AnimatorMixerPlayable backForward = AnimationMixerPlayable.Create(); AnimationClipPlayable backwardPlayable = AnimationClipPlayable.Create(backwardClip); AnimationClipPlayable forwardPlayable = AnimationClipPlayable.Create(forwardClip); backForward.AddInput(backwardPlayable); backForward.AddInput(forwardPlayable); GetComponent<Animator>().Play(backForward); 1 2 3 4 5 6 7 8 9 10 AnimationClip backwardClip ; // assign to your clips AnimationClip forwardClip ; // assign to your clips AnimatorMixerPlayable backForward = AnimationMixerPlayable . Create ( ) ; AnimationClipPlayable backwardPlayable = AnimationClipPlayable . Create ( backwardClip ) ; AnimationClipPlayable forwardPlayable = AnimationClipPlayable . Create ( forwardClip ) ; backForward . AddInput ( backwardPlayable ) ; backForward . AddInput ( forwardPlayable ) ; GetComponent < Animator > ( ) . Play ( backForward ) ;

Then, we control the weights using:

float someWeight; // set value dynamically backForward.SetInputWeight(0, someWeight); backForward.SetInputWeight(1, 1.0f-someWeight); 1 2 3 float someWeight ; // set value dynamically backForward . SetInputWeight ( 0 , someWeight ) ; backForward . SetInputWeight ( 1 , 1.0f - someWeight ) ;

Pro Tip : make sure the sum of the weights is 1.0f.

Custom Playable : Crossfade

While designing the Playable API, one of our key goals was for the system to be extensible. The CustomAnimationPlayable allows you to write your own Playable logic. The secret sauce here is the PrepareFrame callback that automatically gets called when the Playable graph is traversed by the game loop. PrepareFrame is where the blending/weighting logic of the CustomPlayable happens.

In the following example, we implemented a CrossFade Playable that looks and behaves a lot like the Legacy animation system’s CrossFade:

class CrossFadePlayable : CustomAnimationPlayable { private float m_TransitionDuration = 1.0f; private float m_TransitionTime = Mathf.Infinity; private float m_TransitionStart = 0.0f; private bool m_InTransition = false; private AnimationMixerPlayable mixer; public CrossFadePlayable() { mixer = AnimationMixerPlayable.Create(); Playable.Connect(mixer,this); mixer.AddInput(Playable.Null); mixer.AddInput(Playable.Null); } public void Play(AnimationPlayable playable) { Playable.Connect(playable, mixer, 0, 0); } public void Crossfade(AnimationPlayable playable, float transitionDuration) { Playable.Connect(playable, mixer, 0, 1); m_TransitionDuration = transitionDuration; m_TransitionTime = 0.0f; m_TransitionStart = Time.time; m_InTransition = true; } public override void PrepareFrame(FrameData info) { if (m_TransitionTime <= m_TransitionDuration) { m_TransitionTime = Time.time - m_TransitionStart; float weight = Mathf.Clamp01(m_TransitionTime / m_TransitionDuration); mixer.SetInputWeight(0, 1.0f - weight); mixer.SetInputWeight(1, weight); } else if (m_InTransition) { AnimationPlayable destinationPlayable = mixer.GetInput(1).CastTo(); mixer.RemoveAllInputs(); Playable.Connect(destinationPlayable,mixer, 0, 0); mixer.SetInputWeight(0, 1.0f); m_InTransition = false; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class CrossFadePlayable : CustomAnimationPlayable { private float m_TransitionDuration = 1.0f ; private float m_TransitionTime = Mathf . Infinity ; private float m_TransitionStart = 0.0f ; private bool m_InTransition = false ; private AnimationMixerPlayable mixer ; public CrossFadePlayable ( ) { mixer = AnimationMixerPlayable . Create ( ) ; Playable . Connect ( mixer , this ) ; mixer . AddInput ( Playable . Null ) ; mixer . AddInput ( Playable . Null ) ; } public void Play ( AnimationPlayable playable ) { Playable . Connect ( playable , mixer , 0 , 0 ) ; } public void Crossfade ( AnimationPlayable playable , float transitionDuration ) { Playable . Connect ( playable , mixer , 0 , 1 ) ; m_TransitionDuration = transitionDuration ; m_TransitionTime = 0.0f ; m_TransitionStart = Time . time ; m_InTransition = true ; } public override void PrepareFrame ( FrameData info ) { if ( m_TransitionTime <= m_TransitionDuration ) { m_TransitionTime = Time . time - m_TransitionStart ; float weight = Mathf . Clamp01 ( m_TransitionTime / m_TransitionDuration ) ; mixer . SetInputWeight ( 0 , 1.0f - weight ) ; mixer . SetInputWeight ( 1 , weight ) ; } else if ( m_InTransition ) { AnimationPlayable destinationPlayable = mixer . GetInput ( 1 ) . CastTo ( ) ; mixer . RemoveAllInputs ( ) ; Playable . Connect ( destinationPlayable , mixer , 0 , 0 ) ; mixer . SetInputWeight ( 0 , 1.0f ) ; m_InTransition = false ; } } }

We are also working on another callback for the CustomPlayable which will be ProcessFrame. In the case of animation, this will allow to write directly into the animation stream and will give users the power to have their own IK, procedural animation, physic based system, live Mocap etc…

Putting it all together with AnimatorControllerPlayable

Another very nice thing about the API is that you can actually have AnimatorControllers in your graphs. Yes! This means that you can blend between StateMachines! This allows objects and props to supply their own animation StateMachine.

For instance, let’s say you have a weapon and you want the weapon to “teach” the player how to use it. The weapon will provide the AnimatorController (StateMachine) to the Player and will ask to crossfade to it.

public class WeaponCollider : MonoBehaviour { public RuntimeAnimatorController WeaponStateMachine; private AnimatorControllerPlayable controllerPlayable; public void OnTriggerEnter(Collider other) { controllerPlayable = AnimationControllerPlayable.Create(WeaponStateMachine); other.gameObject.GetComponent<Animator>().CrossFade(controllerPlayable); } } 1 2 3 4 5 6 7 8 9 10 11 public class WeaponCollider : MonoBehaviour { public RuntimeAnimatorController WeaponStateMachine ; private AnimatorControllerPlayable controllerPlayable ; public void OnTriggerEnter ( Collider other ) { controllerPlayable = AnimationControllerPlayable . Create ( WeaponStateMachine ) ; other . gameObject . GetComponent < Animator > ( ) . CrossFade ( controllerPlayable ) ; } }

The CharacterPlayableHandler is a simple interface over a CrossFade Playable.

Seeing is believing

It’s very important to be able to visualize the Playable graphs during runtime. In order to do so, we have created the GraphVisualizer, an open source editor-suite that allows previewing Playable graph.

You can download it right now from BitBucket at https://bitbucket.org/Unity-Technologies/playablegraphvisualizer.

There are examples included and it’s very very simple to use!

So what about audio ?

During the GDC Dev Day talk we also presented a Playable API for audio, which works in a similar way as the current Animation Playable API.

The API allows a very powerful, low level control of audio concepts within Unity. It will not only allow programmers to do new and interesting things with Audio in Unity, but will also serve as the foundation of future audio tooling in the Editor.

We would have liked to have presented a section outlining code examples for Audio Playables, however this API is currently in flux and will not be present in the 5.4 release cycle.

We’re looking forward to showing you the Audio Playables once we have finalised the API!

That is it for now!

We hope this gets you excited about Playables. As we said, this is still stuff we are working on, there are a couple of things that we want to change and improve on before we move it out of the experimental stage.

So please, try it and tell us what you think about it!