Search Unity

Help with switching timelines at runtime with animation controllers present

Discussion in 'Timeline' started by OtakuD, Jul 19, 2018.

  1. OtakuD

    OtakuD

    Joined:
    Apr 24, 2014
    Posts:
    49
    Hi Peeps,
    I must admit my knowledge of timelines is minimal at best at this point, currently I am trying to build a skill execution and timing system using timelines loaded into the player character but I am running into a few issues.

    The system is pretty rudimentary atm and all it does is replace a timeline in the director and binds all the objects needed before it plays, after it completes I unbind all the objects and null the playable director asset again if the sequence is complete, is this standard practice?

    A few of my timelines set the director to and from the "None" and "Hold" wrap mode states to handle transitions between them without the animations jumping back to default as they unbind themselves or wait for skill timings, this seems to cause the "defaults" of my animator controller to be overwritten as returning from the timeline will restore my character controller to the last "Held" timeline state once all timelines are completed, what am I doing wrong here? Is this the intended way to use the wrap modes?

    Any documentation or tutorials on how to blend and switch timelines would be greatly appreciated!
     
  2. tarahugger

    tarahugger

    Joined:
    Jul 18, 2014
    Posts:
    129
    The trick i have found if you want to jump to a point in time without it moving/effecting the character is that you have to first make sure its weighted to 0 in some sort of parent blend, then it will no longer be contributing to the transform, and you can jump from end to start of a timeline (or any other point in time you need). Then reset the normal weight, and it will play from the current position.

    As far as setup strategy; i went with putting a game object in my character prefabs for each timeline, Which has the added benefit of making it easy to view and modify each one. If you only need to bind to animators and objects inside the prefab then you don't need to do any code rebinding. In my case it works because the timelines are just special movement for that specific character - climbing up a wall, opening a door etc.

    Then write a timeline manager script (again within the character prefab) that holds references to each timeline and can be responsible for starting/stopping/blending them in response to requests from animation state behaviors or whatever.
     
    Last edited: Jul 20, 2018
    OtakuD likes this.
  3. OtakuD

    OtakuD

    Joined:
    Apr 24, 2014
    Posts:
    49
    Thanks for the time and effort here, I have a few questions though.

    You're talking about setting the timeline to weight 0 here or the animator?

    If you are talking about timeline weight, won't weight zero cause the animator to take over? In my situation I have multiple timeline sections containing each "phase" of the skill. A charge up for while the button is held (so the timeline needs to "hold" until the key is up in some scenarios) and then skip to a new timeline the moment the player let's go, using the played duration to calculate the skill strength. If I set the weight to zero during this transition will the held animation not pop into my idle state momentarily before playing the skill execution timeline?

    Will be experimenting with weights today regardless, thanks!
     
  4. tarahugger

    tarahugger

    Joined:
    Jul 18, 2014
    Posts:
    129
    The core output weight would cause the animator to take over i think. I am altering the outputs for the director that manages the timeline, essentially inserting a mixer between the timeline's animated root playable and the output. And then adjusting the weight on that mixer.

    The mixer has the original timeline and a clone added to it. So that you can play both at the same time (playing different parts of the same timeline) and blend between them with the weight. For example, to blend from the current time within a timeline, you set the clone to match, reset the current to play from the start, now both play at the same time and the clone blends out (playing what the original would have) while the original blends in from the new time.

    I wish there was an easier way built-in but currently these tricks are all i could come up with for proper blending between multiple timelines, points within a single timeline, and timelines back to the animator.

    That sounds like an interesting and complicated situation, ill be interested to see what you come up with if you're in a position to share.

    In your case i think you could either A) loop between two points within the timeline that represent the holding period animation, then when the conditions are correct, jump forward to continue with the rest of the animation. or B) split it into two timelines - which is what i did for a similar sort of thing:

    I have a NPC that when he walks into a navmesh link with a wall to be climbed, starts a 'Climbing Start' timeline, which includes the initial leap from the ground to the wall and blends into a climbing loop that i dragged out to loop for a more time than it will ever need (like 2 minutes). Its using root animation from the timeline, and at a certain point the distance to the top of the wall will trigger the 'Climbing End' timeline to start, the two timelines transition and then once the NPC has climbed over the ledge it transitions back into the normal animator locomotion.

    Here was the starting point for me. https://forum.unity.com/threads/fad...n-animator-can-take-over.513800/#post-3365226

    This code below is a work in progress, i will probably put together a nice example on github if people are interested but its the key piece so it should help.

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Diagnostics;
    5. using UnityEngine;
    6. using UnityEngine.Animations;
    7. using UnityEngine.Playables;
    8. using UnityEngine.Timeline;
    9. using Debug = UnityEngine.Debug;
    10.  
    11. [RequireComponent(typeof(PlayableDirector))]
    12. public class TimelordDirector : MonoBehaviour
    13. {
    14.     public PlayableDirector PlayableDirector;
    15.  
    16.     private AnimationPlayableOutput _output;
    17.     private Playable _originalSourcePlayable;
    18.     private Playable _clone;
    19.     private AnimationMixerPlayable _mixer;
    20.     private int _cloneIndex;
    21.     private int _originalIndex;
    22.     private float _decreasingWeight;
    23.     private float _increasingWeight;
    24.  
    25.     void Awake()
    26.     {
    27.         if (PlayableDirector == null)
    28.         {
    29.             PlayableDirector = GetComponent<PlayableDirector>();
    30.         }
    31.     }
    32.     private void BuildOutput()
    33.     {
    34.         PlayableDirector.Evaluate();
    35.  
    36.         if (PlayableDirector.playableGraph.IsValid())
    37.         {
    38.             _outputTrackIndex = 0;
    39.             _trackAsset = (PlayableDirector.playableAsset as TimelineAsset)?.GetOutputTrack(_outputTrackIndex);
    40.             _originalOutput = (AnimationPlayableOutput)PlayableDirector.playableGraph.GetOutputByType<AnimationPlayableOutput>(_outputTrackIndex);
    41.             _originalSourcePlayable = _originalOutput.GetSourcePlayable();
    42.             _clone = PlayableDirector.playableAsset.CreatePlayable(PlayableDirector.playableGraph, PlayableDirector.gameObject);
    43.             _mixer = AnimationMixerPlayable.Create(PlayableDirector.playableGraph, 2);
    44.             _cloneIndex = _mixer.AddInput(_clone, 0);
    45.             _originalIndex = _mixer.AddInput(_originalSourcePlayable, 0, 1f);
    46.  
    47.             if (_originalOutput.IsOutputValid() && _originalOutput.GetTarget() != null)
    48.             {
    49.                 _output = AnimationPlayableOutput.Create(PlayableDirector.playableGraph, "OverridedDirectorOutput" + GetInstanceID(), _originalOutput.GetTarget());
    50.                 _output.SetSourcePlayable(_mixer);
    51.                 _output.SetSourceOutputPort(_originalOutput.GetSourceOutputPort());
    52.                 _output.SetWeight(1f);
    53.                 _originalOutput.SetTarget(null);
    54.             }
    55.             else
    56.             {
    57.                 Debug.Log("Original Director Output is invalid");
    58.             }
    59.         }
    60.     }
    61.  
    62.     private TrackAsset _trackAsset;
    63.  
    64.     private double _outTime = -1;
    65.  
    66.     public bool IsPlaying => PlayableDirector.state == PlayState.Playing;
    67.  
    68.     public BlendType CurrentBlend { get; set; }
    69.  
    70.     public bool IsBlending => CurrentBlend != BlendType.None;
    71.  
    72.     public bool IsBlendingOut => CurrentBlend == BlendType.Out;
    73.  
    74.     public bool IsBlendingIn => CurrentBlend == BlendType.In;
    75.  
    76.     public double TimeRemaining => PlayableDirector.time - PlayableDirector.duration;
    77.  
    78.     public string Name => PlayableDirector.name;
    79.  
    80.     public enum BlendType
    81.     {
    82.         None = 0,
    83.         In,
    84.         Out,
    85.         Seek,
    86.     }
    87.  
    88.     private Action _scheduledBlendOutCallback;
    89.     private float _scheduledBlendOutDuration;
    90.     private AnimationPlayableOutput _originalOutput;
    91.     private int _outputTrackIndex;
    92.     private Stopwatch _sw = new Stopwatch();
    93.     private static bool _abortBlendIn;
    94.  
    95.  
    96.     void Update()
    97.     {
    98.         if (IsPlaying && CurrentBlend == BlendType.None)
    99.         {
    100.             if (_outTime >= 0 && PlayableDirector.time >= _outTime)
    101.             {
    102.                 CurrentBlend = BlendType.Out;
    103.  
    104.                 //Debug.Log($"{name}: Starting Scheduled Blend-Out at {PlayableDirector.time:N2}");
    105.                 _sw.Restart();
    106.  
    107.                 StartCoroutine(BlendOut(PlayableDirector, _output, _scheduledBlendOutDuration, _output.GetWeight(), () =>
    108.                 {
    109.                     _sw.Stop();
    110.                     //Debug.Log($"{name}: Finished Scheduled Blend-Out after {_sw.Elapsed.TotalSeconds:N3}ms");
    111.                     _scheduledBlendOutCallback?.Invoke();
    112.                     _scheduledBlendOutCallback = null;
    113.                     CurrentBlend = BlendType.None;
    114.                 }));
    115.             }
    116.         }
    117.     }
    118.  
    119.     public void Play(float blendInDuration = 0.5f, float blendOutDuration = 0.5f, float startTime = -1, float endTime = -1, Action onBlendInFinished = null, Action onFinished = null)
    120.     {
    121.  
    122.         //Debug.Log($"{name}: Play. CurrentBlend={CurrentBlend}");
    123.  
    124.         if (CurrentBlend == BlendType.None)
    125.         {
    126.             CurrentBlend = BlendType.In;
    127.  
    128.             if (!_output.IsOutputValid())
    129.             {
    130.                 //Debug.Log($"{name}: Rebuilding output");
    131.                 PlayableDirector.RebuildGraph();
    132.                 BuildOutput();
    133.             }
    134.  
    135.             if (_output.IsOutputValid())
    136.             {
    137.                 _scheduledBlendOutCallback = onFinished;
    138.                 _scheduledBlendOutDuration = blendOutDuration;
    139.  
    140.                 _outTime = (endTime < 0
    141.                     ? _trackAsset.start + _trackAsset.duration - blendOutDuration
    142.                     : Math.Max(0, endTime - blendInDuration)) - Time.deltaTime;
    143.  
    144.                 //Debug.Log($"{name}: scheduling OUT: {_outTime:N2}");
    145.  
    146.                 if (blendInDuration == 0f)
    147.                 {
    148.                     PlayableDirector.Play();
    149.                 }
    150.                 else
    151.                 {
    152.                     StartCoroutine(BlendIn(PlayableDirector, _output, blendInDuration, startTime, () =>
    153.                     {
    154.                         onBlendInFinished?.Invoke();
    155.                         _scheduledBlendOutCallback = null;
    156.                         CurrentBlend = BlendType.None;
    157.                     }));
    158.                 }
    159.             }
    160.             else
    161.             {
    162.                 //Debug.Log($"{nameof(TimelordDirector)}.{nameof(Play)}() failed because the graph was invalid.");
    163.             }
    164.         }
    165.         else
    166.         {
    167.             //Debug.Log($"{nameof(TimelordDirector)}.{nameof(Play)}() was called while already blending ({CurrentBlend}), and was ignored.");
    168.         }
    169.     }
    170.  
    171.     public void Stop(float blendDuration = 0.5f, Action onFinished = null)
    172.     {
    173.         Debug.Log($"{name}: Stop. CurrentBlend={CurrentBlend}");
    174.  
    175.         if (blendDuration <= 0f)
    176.         {
    177.             PlayableDirector.Pause();
    178.         }
    179.  
    180.         if (CurrentBlend != BlendType.None && CurrentBlend != BlendType.In)
    181.         {
    182.             //Debug.Log($"{nameof(TimelordDirector)}.{nameof(Stop)}() was called while already blending ({CurrentBlend}), and was ignored.");
    183.             return;
    184.         }
    185.  
    186.         if (CurrentBlend == BlendType.In)
    187.         {
    188.             _abortBlendIn = true;
    189.         }
    190.  
    191.         CurrentBlend = BlendType.Out;
    192.  
    193.         if (!_output.IsOutputValid())
    194.         {
    195.             Debug.Log($"{name}: Rebuilding output");
    196.             PlayableDirector.RebuildGraph();
    197.             BuildOutput();
    198.         }
    199.  
    200.         if (_output.IsOutputValid())
    201.         {
    202.             StartCoroutine(BlendOut(PlayableDirector, _output, blendDuration, _output.GetWeight(), () =>
    203.             {
    204.                 onFinished?.Invoke();
    205.                 CurrentBlend = BlendType.None;
    206.             }));
    207.         }
    208.         else
    209.         {
    210.             //Debug.Log($"{nameof(TimelordDirector)}.{nameof(Stop)}() failed because the graph was invalid.");
    211.         }
    212.  
    213.  
    214.     }
    215.  
    216.     public void Seek(float toTime, float blendDuration, Action onFinished = null)
    217.     {
    218.         //Debug.Log($"{name}: Seek. CurrentBlend={CurrentBlend} to={toTime}");
    219.  
    220.         if (CurrentBlend == BlendType.None)
    221.         {
    222.             CurrentBlend = BlendType.Seek;
    223.  
    224.             if (_output.IsOutputValid())
    225.             {
    226.                 StartCoroutine(SeekBlend(blendDuration, toTime, () =>
    227.                 {
    228.                     onFinished?.Invoke();
    229.                     CurrentBlend = BlendType.None;
    230.                 }));
    231.             }
    232.             else
    233.             {
    234.                 //Debug.Log($"{nameof(TimelordDirector)}.{nameof(Stop)}() failed because the graph was invalid.");
    235.             }
    236.         }
    237.     }
    238.  
    239.     private IEnumerator BlendOut(PlayableDirector director, AnimationPlayableOutput output, float blendTime, float fromWeight = 1f, Action onFinished = null)
    240.     {
    241.  
    242.         float t = blendTime - blendTime * fromWeight;
    243.  
    244.         //Debug.Log($"{name}: Started blend out from {fromWeight} weight, oW={output.GetWeight()}, t={t}");
    245.  
    246.         while (t < blendTime)
    247.         {
    248.             var weight = 1 - Mathf.Clamp01(t / blendTime);
    249.  
    250.             if (!output.IsOutputValid())
    251.             {
    252.                 //Debug.Log($"{name}: BlendOut has an invalid output graph");
    253.                 break;
    254.             }
    255.  
    256.             //Debug.Log($"{name}: BlendOut - t:{t:N2}, w={weight:N2}, dT={director.time:N2}/{director.duration:N2}");
    257.             output.SetWeight(weight);
    258.  
    259.             yield return null;
    260.             t += Time.deltaTime;
    261.         }
    262.  
    263.         if (output.IsOutputValid())
    264.         {
    265.             output.SetWeight(0);
    266.         }
    267.  
    268.         onFinished?.Invoke();
    269.  
    270.         if (director.isActiveAndEnabled)
    271.             director.Pause();
    272.     }
    273.  
    274.     private IEnumerator BlendIn(PlayableDirector director, AnimationPlayableOutput output, float blendTime, float startTime = -1, Action onFinished = null)
    275.     {
    276.         director.time = startTime > 0 ? startTime : 0;
    277.         director.Play();
    278.         output.SetWeight(0);
    279.         _abortBlendIn = false;
    280.  
    281.         float t = 0;
    282.         while (t < blendTime)
    283.         {
    284.             if (_abortBlendIn)
    285.             {
    286.                 //Debug.Log($"{name}: Aborted Blend in");
    287.                 _abortBlendIn = false;
    288.                 break;
    289.             }
    290.  
    291.             var weight = Mathf.Clamp01(t / blendTime);
    292.  
    293.             output.SetWeight(weight);
    294.  
    295.             //Debug.Log($"{name}: BlendIn - t:{t:N2}, w={weight:N2}, dT={director.time:N2}");
    296.  
    297.             yield return null;
    298.             t += Time.deltaTime;
    299.         }
    300.  
    301.         output.SetWeight(1);
    302.         onFinished?.Invoke();
    303.     }
    304.  
    305.     private IEnumerator SeekBlend(float blendTime, float startTime = -1, Action onFinished = null)
    306.     {
    307.         float t = 0;
    308.  
    309.         // Set it to play exactly what the main output is currently playing.
    310.         _clone.SetTime(PlayableDirector.time);
    311.         _clone.Play();
    312.  
    313.         //Debug.Log($"{name}: CrossFade Start - OriginalTime: {PlayableDirector.time:N4}, w={_mixer.GetInputWeight(_originalIndex)} | CloneTime {_clone.GetTime():N4}, w={_mixer.GetInputWeight(_cloneIndex)} | oW={_output.GetWeight()}");
    314.  
    315.         // todo, blend main weight if nessesary e.g seeking half-way through a blend-in/out
    316.         _output.SetWeight(1);
    317.  
    318.         // Let the clone take over control.
    319.         _mixer.SetInputWeight(_cloneIndex, 1f);
    320.         _mixer.SetInputWeight(_originalIndex, 0f);
    321.  
    322.         // Normally when time changes (and play() or evaluate() are called) it will reposition
    323.         // the transform based on what should have happened by the new point in time (relative to the timeline start).
    324.         // Which is not desirable! Instead we want everything to continue from its current position.
    325.         // This issue can be circumvented by changing a playable while inactive.
    326.         //_output.SetWeight(0);
    327.         PlayableDirector.time = startTime;
    328.         PlayableDirector.Play();
    329.         PlayableDirector.Evaluate();
    330.  
    331.         //Debug.Log($"AnimationTransform: 1) {start} 2) {cur} Dist: {Vector3.Distance(start, cur)}");
    332.  
    333.         //float t = 0f;
    334.         // Blend from the
    335.         while (t < blendTime)
    336.         {
    337.  
    338.             var normalizedTime = Mathf.Clamp01(t / blendTime);
    339.             _decreasingWeight = 1 - normalizedTime;
    340.             _increasingWeight = normalizedTime;
    341.  
    342.             _mixer.SetInputWeight(_cloneIndex, _decreasingWeight);
    343.             _mixer.SetInputWeight(_originalIndex, _increasingWeight);
    344.  
    345.             //Debug.Log($"{name}: Seek - CloneTime: {_clone.GetTime():N2}, w={_decreasingWeight:N2} | OriginalTime: {PlayableDirector.time:N2}, w={_increasingWeight:N2}");
    346.  
    347.             yield return null;
    348.             t += Time.deltaTime;
    349.         }
    350.  
    351.         _mixer.SetInputWeight(_cloneIndex, 0);
    352.         _mixer.SetInputWeight(_originalIndex, 1f);
    353.  
    354.         _clone.Pause();
    355.  
    356.  
    357.  
    358.         onFinished?.Invoke();
    359.         //Debug.Log("CrossFade Finished");
    360.     }
    361.  
    362. }
    EDIT: I put the demo together, here's the link: https://forum.unity.com/threads/demo-unitytimelordblender-a-solution-for-blending-timelines.541627/
     
    Last edited: Jul 21, 2018
    OtakuD likes this.