Notifications
Article
Blending Unity Timeline with Gameplay - Character's Animation
Updated 2 months ago
790
0
How to blend character's timeline animation with gameplay

New solution (2017.3)

I said previously, that there is no easy way of blending gameplay and Timeline. I was testing Timeline in Unity 2017.2 and couldn't manage to get it working just right. I had problems with blending Timeline animation with gameplay, mostly because of the lack of blend-in blend-out options for animations. I was trying to set those options in animation clips in the Timeline, but without any luck.
It turned out to be extremely easy in 2017.3. It just works. To be able to blend character's Timeline animation with it's Animator animation, you can simply edit the Ease In Duration and Ease Out Duration of the Timeline animation clip. Here you can find two screenshots showing all the settings.
Note that the Timeline animation doesn't start in the first frame. It also works best when the animation ends before the last frame of the whole Timeline.

[OLD VERSION OF THIS POST (2017.2)]

I played a lot with Timeline recently and couldn't find a simple solution how to blend between the last frame of Timeline and current gameplay animation (controlled by the Animator component). By default, when Timeline ends, all characters will "snap" to what their Animator is currently playing. I ended up with writing two simple scripts, that I would like to share here:

AutoBlendOut.cs

This little script handles the blending between whatever was playing at the moment the AutoBlend() function was called and what is currently played by the Animator component. So when the AutoBlend() function is called, it saves a single frame, and then blends it over time with the result given by the Animator component (or whatever really). You should attach this script to your character, and make sure you pass the character's rig transform to the myRig field.
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] //A simple class to store the local position and //local rotation of a bone (transform) and the reference to that bone public class BlendedTransform { public Transform targetTransform; public Vector3 position; public Quaternion rotation; } public class AutoBlendOut : MonoBehaviour { //the reference to the rig game object (the bones should be children of that transform) public Transform myRig; //a list containing all the child game objects of the myRig transform List<BlendedTransform> allmyBones = new List<BlendedTransform>(); //a reference to the animator component Animator anim; //the blend weight. When 0 the Animator animation is being played, when 1 the last frame set by the AutoBlend //function is played float weight = 0f; //blend duration float _blendOutTime = 0.5f; //update mode set for the Animator component (we need to override it to "Normal" for the blend) AnimatorUpdateMode _animUpdateMode; private void Awake() { //getting the reference to the Animator anim = GetComponent<Animator>(); if (myRig == null) { myRig = transform; } } //call this function to start the blending process - it takes saves the current bones' positions and //rotations (a reference frame) public void AutoBlend (bool reset, float blendTime) { _animUpdateMode = anim.updateMode; //override animation update mode to Normal (only then the blend looks good) anim.updateMode = AnimatorUpdateMode.Normal; Transform[] myBones = myRig.GetComponentsInChildren<Transform>(); _blendOutTime = blendTime; for (int i = 0; i < myBones.Length; i++) { if (myBones[i] != myRig) { BlendedTransform bt = new BlendedTransform(); bt.targetTransform = myBones[i]; bt.position = myBones[i].localPosition; bt.rotation = myBones[i].localRotation; allmyBones.Add(bt); } } //if you pass a reset bool, the "Reset" trigger will be additionally called on the animator if (reset) { anim.SetTrigger("Reset"); } //here we set the weight to 1f to start with the saved reference frame weight = 1f; _blend = true; } public void SetAnimatorUpdate(AnimatorUpdateMode animUpdate) { anim.updateMode = animUpdate; } bool _blend = false; private void LateUpdate() { //we blend bones in the late update to override the Animator component BlendBones(); } void BlendBones() { //we stop blending bones when the _blend parameter is false if (!_blend) { return; } if (_blendOutTime > 0f && weight > 0f) { //continue to blend bones untill the weight reaches 0f weight -= Time.deltaTime / _blendOutTime; for (int i = 0; i < allmyBones.Count; i++) { allmyBones[i].targetTransform.localRotation = Quaternion.Lerp(allmyBones[i].targetTransform.localRotation, allmyBones[i].rotation, Mathf.Max(0f, weight)); allmyBones[i].targetTransform.localPosition = Vector3.Lerp(allmyBones[i].targetTransform.localPosition, allmyBones[i].position, Mathf.Max(0f, weight)); } } else { //stop blending bones _blend = false; //reset the update mode of the Animator anim.updateMode = _animUpdateMode; } } }

PlayableDirectorAutoBlendOut.cs

The second script should be attached to the game object containing the PlayableDirector component. It checks when the director stops playing and starts the AutoBlend() functions on the characters passed to the autoBlends[] array. It also automatically changes the extrapolation mode of the director to Hold (to make sure the last frame of the timeline is there when we call the AutoBlend() function). The script stops the director automatically.
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Playables; //Attach this script to the game object containg the PlayableDirector component public class PlayableDirectorAutoBlendOut : MonoBehaviour { //a reference to the playable director public PlayableDirector director; //if set to true, a "Reset" trigger will be called on all animated characters public bool resetCharacters = true; public bool unparentCharacters = true; //blend out duration public float blendCharactersTime = 0.5f; //a reference to all characters (AutoBlendOut components) that need the blending public AutoBlendOut[] autoBlends; public PlayableDirector[] directorsToStop; //normalized director playback time float time; private void Awake() { if (director == null) { director = GetComponent<PlayableDirector>(); } if (director != null) { //set the extraplotaion mode to Hold - we will still stop the director, but it ensures the last frame //of the animation stays long enough to be captured by the AutoBlendOut scripts director.extrapolationMode = DirectorWrapMode.Hold; } } bool _directorsStopped = false; private void Update() { //check if the director is currently playing if (director != null && director.state == PlayState.Playing) { //if so - check the normalized time of the director time = (float)(director.time / director.duration); //stops any additional directors at the start of the Timeline if (time >= 0.1f && !_directorsStopped) { for (int i = 0; i < directorsToStop.Length; i++) { directorsToStop[i].Stop(); } _directorsStopped = true; } if (time >= 1f) { //if the director is done playing, start the autoblend scripts for (int i = 0; i < autoBlends.Length; i++) { autoBlends[i].AutoBlend(resetCharacters, blendCharactersTime); //automatically unparents characters at the end of the timeline if (unparentCharacters) { autoBlends[i].transform.parent = null; } } //and finally stop the director director.Stop(); } } } }

Root motion

Make sure to bake all root motion on the currently played Animator animation (for the most part it should be the idle animation). I do not blend the transform position in those scripts, so if your Animator is moving your character, you may still observe a "snap".

Tips

  • It works best when Timeline ends with your idle animation and you blend to the idle animation as well.
  • You can achieve that by adding a transition from AnyState to your idle animation in the Animator with a condition of "Reset" trigger. The AutoBlendOut script can call the "Reset" trigger for you.
Hope it helps!

Maciej Szcześnik
Independent Game Developer - Designer
2
Comments