Search Unity

SimplePlayableManager script

Discussion in 'Animation' started by mangax, Jun 3, 2018.

  1. mangax

    mangax

    Joined:
    Jul 17, 2013
    Posts:
    336
    hi everyone!

    this is a simple playable manager script that handles playable states by controlling the blend weights without transitions. this script is only intended for learning purposes and as a proof of concept, you can add more animation groups as it fits (using arrays or custom classes)

    the approach is simple and straightforward, it controls and updates all weights for clips in all groups by normalizing each as needed all through single mixer. Each group contains number of clips that have specific purpose (movements or attacks or damage etc)

    as a result, it is possible to apply multiple triggers at same time instead keeping track of one state or index. Many animations can be played and effect weights without things gets out of control, you can also interrupt or limit each group of clips influence by tweaking the code a bit..

    this all liberates you from the need to implement transitions similar to mecanim, to keep track of current or next state etc. because all weights are updated at same time (movement vs damage vs etc ) this gives you more ability to apply some nice effects very easily!

    for example, like doing flick damage! (applying small damage weight while character doing other stuff.)
    another example for more complex behavior, if you can leverage mixer layer-masks, you can hook movement clips into different layer, then with some weight checking, you can allow character to keep running even while attacking or getting damage, check video..(not included in the sample script)







    just drop the script on character with animator, and play with controls on the script by pressing on trigger attributes, try pressing damage trigger multiple times then attack etc

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Playables;
    5. using UnityEngine.Animations;
    6.  
    7. [RequireComponent(typeof(Animator))]
    8. public class SimplePlayableManager : MonoBehaviour {
    9.  
    10.     [Header("Controls")]
    11.     public float BlendSpeed = 0.1f;
    12.     [Range(0f, 1f)]
    13.     public float InputMovement;
    14.     public bool TriggerAttack;
    15.     public bool TriggerDamage;
    16.  
    17.  
    18.  
    19.     [Header("Animation Clips")]
    20.     public AnimationClip clipIdle;
    21.     public AnimationClip clipMove;
    22.     public AnimationClip clipAttack01;
    23.     public AnimationClip clipAttack02;
    24.     public AnimationClip clipDamage01;
    25.     public AnimationClip clipDamage02;
    26.  
    27.  
    28.  
    29.     //keeping track of indexes in mixer
    30.     int _IdleIndex;
    31.     int _MoveIndex;
    32.     int _Attack01Index;
    33.     int _Attack02Index;
    34.     int _Damage01Index;
    35.     int _Damage02Index;
    36.  
    37.     float _IdleWeight  ;
    38.     float _MoveWeight ;
    39.     float _Attack01Weight ;
    40.     float _Attack02Weight  ;
    41.     float _Damage01Weight ;
    42.     float _Damage02Weight ;
    43.  
    44.  
    45.     //we need duration for non-looping non-default animations
    46.     float _AttackDuration;
    47.     float _DamageDuration;
    48.  
    49.  
    50.     //track activity
    51.     int ActiveAttackIndex;
    52.     int ActiveDamageIndex;
    53.  
    54.  
    55.     //its important to control different animation groups through balance in case some group exceed in weights
    56.     float _MovementBalance;
    57.     float _AttackBalance;
    58.     float _DamageBalance;
    59.  
    60.  
    61.  
    62.     PlayableGraph _PlayableGraph;
    63.     AnimationPlayableOutput _AnimationPlayableOutput;
    64.     AnimationMixerPlayable _Mixer;
    65.  
    66.  
    67.  
    68.     private void Awake()
    69.     {
    70.  
    71.  
    72.  
    73.         _PlayableGraph = PlayableGraph.Create();
    74.  
    75.         _PlayableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
    76.  
    77.         _AnimationPlayableOutput = AnimationPlayableOutput.Create(_PlayableGraph, "AnimatorPManager", GetComponent<Animator>());
    78.  
    79.         _Mixer = AnimationMixerPlayable.Create(_PlayableGraph);
    80.  
    81.         _Mixer.SetOutputCount(1);
    82.  
    83.         _AnimationPlayableOutput.SetSourcePlayable(_Mixer);
    84.  
    85.  
    86.         /* creating & adding playable clips to mixer, after adding, an index# will be returned to store it*/
    87.         _IdleIndex = _Mixer.AddInput(AnimationClipPlayable.Create(_PlayableGraph, clipIdle),0,1f);
    88.         _MoveIndex = _Mixer.AddInput(AnimationClipPlayable.Create(_PlayableGraph, clipMove), 0);
    89.         _Attack01Index = _Mixer.AddInput(AnimationClipPlayable.Create(_PlayableGraph, clipAttack01), 0);
    90.         _Attack02Index = _Mixer.AddInput(AnimationClipPlayable.Create(_PlayableGraph, clipAttack02), 0);
    91.         _Damage01Index = _Mixer.AddInput(AnimationClipPlayable.Create(_PlayableGraph, clipDamage01), 0);
    92.         _Damage02Index = _Mixer.AddInput(AnimationClipPlayable.Create(_PlayableGraph, clipDamage02), 0);
    93.  
    94.  
    95.      _PlayableGraph.Play();
    96.  
    97.     }
    98.  
    99.     public void SendEvent(string str)
    100.     { }
    101.  
    102.     void CheckInputs()
    103.     {
    104.         if (TriggerAttack)
    105.         {
    106.             TriggerAttack = false;
    107.  
    108.             //alternate with animations
    109.             if (ActiveAttackIndex == 1)
    110.             {
    111.                 ActiveAttackIndex = 2;
    112.                 _AttackDuration = clipAttack02.length;
    113.                var playable = (AnimationClipPlayable)_Mixer.GetInput(_Attack02Index);
    114.                 playable.SetTime(0.0);
    115.                 playable.Play();
    116.             }
    117.             else
    118.             {
    119.                 ActiveAttackIndex = 1;
    120.                 _AttackDuration = clipAttack01.length;
    121.                 var playable = (AnimationClipPlayable)_Mixer.GetInput(_Attack01Index);
    122.                 playable.SetTime(0.0);
    123.                 playable.Play();
    124.             }
    125.  
    126.             //we set duration less than clip length, so blends out just before animation ends.
    127.             _AttackDuration *= 0.8f;
    128.  
    129.             //inturrupt damage animations by setting active index to 0
    130.             ActiveDamageIndex = 0;
    131.  
    132.         }
    133.  
    134.  
    135.         if (TriggerDamage)
    136.         {
    137.             TriggerDamage = false;
    138.  
    139.  
    140.             if (ActiveDamageIndex == 1)
    141.             {
    142.                 ActiveDamageIndex = 2;
    143.                 _DamageDuration = clipDamage02.length;
    144.  
    145.                 var playable = (AnimationClipPlayable)_Mixer.GetInput(_Damage02Index);
    146.                 playable.SetTime(0.0);
    147.                 playable.Play();
    148.             }
    149.             else
    150.             {
    151.                 ActiveDamageIndex = 1;
    152.                 _DamageDuration = clipDamage01.length;
    153.  
    154.                 var playable = (AnimationClipPlayable)_Mixer.GetInput(_Damage01Index);
    155.                 playable.SetTime(0.0);
    156.                 playable.Play();
    157.             }
    158.  
    159.             _DamageDuration *= 0.8f;
    160.  
    161.             //inturrupt attack
    162.             ActiveAttackIndex = 0;
    163.         }
    164.     }
    165.     void Update () {
    166.  
    167.         //check if player triggered something
    168.         CheckInputs();
    169.  
    170.         //set balances to 1f
    171.         _MovementBalance = _AttackBalance = _DamageBalance = 1f;
    172.  
    173.         //track attack and damage activity by tracking their duration
    174.         if (ActiveAttackIndex > 0 )
    175.         {
    176.             _AttackDuration -= Time.deltaTime;
    177.             if (_AttackDuration < 0f) ActiveAttackIndex = 0;
    178.         }
    179.  
    180.         if (ActiveDamageIndex > 0)
    181.         {
    182.             _DamageDuration -= Time.deltaTime;
    183.             if (_DamageDuration < 0f) ActiveDamageIndex = 0;
    184.         }
    185.  
    186.  
    187.         //update attack and damage weights depending on active Index
    188.         if (ActiveAttackIndex==1) _Attack01Weight += BlendSpeed;   else  _Attack01Weight -= BlendSpeed;
    189.         if(ActiveAttackIndex==2) _Attack02Weight += BlendSpeed;    else  _Attack02Weight -= BlendSpeed;
    190.         _Attack01Weight = Mathf.Clamp01(_Attack01Weight);
    191.         _Attack02Weight = Mathf.Clamp01(_Attack02Weight);
    192.  
    193.  
    194.         if (ActiveDamageIndex == 1)_Damage01Weight += BlendSpeed;else _Damage01Weight -= BlendSpeed;
    195.         if (ActiveDamageIndex == 2)_Damage02Weight += BlendSpeed;else _Damage02Weight -= BlendSpeed;
    196.         _Damage01Weight = Mathf.Clamp01(_Damage01Weight);
    197.         _Damage02Weight = Mathf.Clamp01(_Damage02Weight);
    198.  
    199.         //default indexes
    200.         _IdleWeight = 1f - InputMovement;
    201.         _MoveWeight = InputMovement;
    202.  
    203.  
    204.  
    205.         /*  make sure everything equally weighted for each animation group*/
    206.         var totalAttacksWeight = _Attack01Weight + _Attack02Weight;
    207.  
    208.         if (totalAttacksWeight > 1f)
    209.         {
    210.             _Attack01Weight = _Attack01Weight / totalAttacksWeight;
    211.             _Attack02Weight = _Attack02Weight / totalAttacksWeight;
    212.             totalAttacksWeight = 1f;
    213.  
    214.         }
    215.  
    216.         var totalDamagesWeight = _Damage01Weight + _Damage02Weight;
    217.  
    218.         if (totalDamagesWeight > 1f)
    219.         {
    220.             _Damage01Weight = _Damage01Weight / totalDamagesWeight;
    221.             _Damage02Weight = _Damage02Weight / totalDamagesWeight;
    222.             totalDamagesWeight = 1f;
    223.         }
    224.  
    225.         //check if groups exceed 1f,then it must be reduced by editing the balance variables
    226.          var totalWeights = totalAttacksWeight + totalDamagesWeight;
    227.         //if all weights exceed 1f,then it must be reduced so all weights equals to 1f
    228.         //by simply dividing each with total weight so we distribute the weights
    229.         if (totalWeights>1f)
    230.         {
    231.  
    232.             _AttackBalance = totalAttacksWeight / totalWeights;
    233.             _DamageBalance = totalDamagesWeight / totalWeights;
    234.  
    235.             totalWeights = 1f;
    236.         }
    237.  
    238.         //default weight is always 1f minus other weights influence..
    239.         _MovementBalance = Mathf.Clamp01(1f - totalWeights);
    240.  
    241.  
    242.         //update mixer with new weights + balance value from each group
    243.         //this way each blend type is contained and do not over exceed their influence
    244.         _Mixer.SetInputWeight(_IdleIndex, _IdleWeight * _MovementBalance);
    245.         _Mixer.SetInputWeight(_MoveIndex, _MoveWeight * _MovementBalance);
    246.  
    247.         _Mixer.SetInputWeight(_Attack01Index, _Attack01Weight * _AttackBalance);
    248.         _Mixer.SetInputWeight(_Attack02Index, _Attack02Weight * _AttackBalance);
    249.  
    250.         _Mixer.SetInputWeight(_Damage01Index, _Damage01Weight * _DamageBalance);
    251.         _Mixer.SetInputWeight(_Damage02Index, _Damage02Weight * _DamageBalance);
    252.     }
    253. }
    254.  
     
    Last edited: Jun 6, 2018
  2. MadeFromPolygons

    MadeFromPolygons

    Joined:
    Oct 5, 2013
    Posts:
    3,983
    this is great will try this when home from work :)
     
    mangax likes this.
  3. MadeFromPolygons

    MadeFromPolygons

    Joined:
    Oct 5, 2013
    Posts:
    3,983
    Just wanted to say that this in combination with @seant_unity comments have helped me to understand playable api and as an extension of that, timelines api too.

    I now have an animator that blends arbitrary clips, but can also blend to, play and then blend back from timelines too for stuff like attack animation with sound or footsteps with sound.

    For now i am doing footstep sounds via animation events, but looking for a better way to do the base movements too. What I would really like is to have everything inside timelines so all clips can be married to sounds etc, and then have all of that blend similar to this.

    @mangax have you any ideas how to do this same system, but with timelineAssets instead of animation clips? Assuming that each timeline asset has the animationtrack at the same position with same object as bindings etc? The main thing I am struggling with is I am not sure how to read out that info from the asset so that I can set up mixer and inputs outputs graph etc

    Any help you can give would be great!

    EDIT: @seant_unity @mangax @Mecanim-Dev actually I just thought, would a really easy way to do this be to have all the timelines have ease in + ease out on their clips at start and end, and then simply call Play on the playable director, adjusting wrap mode as needed? Perhaps I am overthinking how much code is needed for such a thing?
     
  4. mangax

    mangax

    Joined:
    Jul 17, 2013
    Posts:
    336
    @GameDevCouple_I to be honest i didn't delve into timeline assets, am happy with things done directly through scripts.

    generally i prefer doing things by scripts over editor or assets managments.
     
  5. MadeFromPolygons

    MadeFromPolygons

    Joined:
    Oct 5, 2013
    Posts:
    3,983
    But timeline supports scripts? It is precisely that reason we are using it, so we can create sequence-able clips to drive behaviours, that we can sequence accurately on a frame by frame basis so that rendering is exact.

    Doing stuff like: https://blogs.unity3d.com/2018/04/05/creative-scripting-for-timeline/

    is why we need the timeline, authoring massive behaviour stacks where SFX and VFX and animation and values are all changed frame by frame and need blending, its just not doable in a intuitive way without timeline. Problem is I cant work out how to blend from one timeline to another without poses going strange or jumping and I feel like it is a simple bit of knowledge but in all the threads I ask people either dont understand my question or have over complicated answers :/

    Either way your script helped me get my head around the base class playable and how that works + playable graph etc, and understand how it interacts with animator component
     
  6. mangax

    mangax

    Joined:
    Jul 17, 2013
    Posts:
    336
    @GameDevCouple_I sounds cool! am very convinced about what you are saying! i can see that you can do things that blends between in-game cinematic and gameplay.. am thinking to use it for in-game special events, because am worried that it may have an overhead or performance penalty for using high level tool such timeline.

    currently am more focused into using playable api in gameplay focused stuff.. and its so far it has been more pleasant than using Animator or mecanim!
     
    Last edited: Jun 6, 2018
  7. tarahugger

    tarahugger

    Joined:
    Jul 18, 2014
    Posts:
    129
    Hey i really appreciate you posting this code, its exactly the sort of example that I've been looking for.

    @GameDevCouple_I Im not sure how much of a performance hit this would produce but you change the input from an AnimationClip to PlayableDirector. And drag on a child game object that contains a timeline and director.

    Code (CSharp):
    1.  
    2.         if (TriggerDance)
    3.         {
    4.             if (clipDance01.state == PlayState.Paused)
    5.             {
    6.                 clipDance01.time = 0f;
    7.                 clipDance01.Play();
    8.             }
    9.             else
    10.             {
    11.                 TriggerDance = false;
    12.             }        
    13.         }
    14.  
    You can also use:

    Code (CSharp):
    1. DirectorControlPlayable.Create(_PlayableGraph, directorMove)
    However from my tests, even if you blend a playing DirectorControlPlayable down to 0 in either an AnimationLayerMixerPlayable or AnimationMixerPlayable, it just ignores you and keeps playing it.
     
    Last edited: Jun 11, 2018
    MadeFromPolygons likes this.
  8. LAJAPPS

    LAJAPPS

    Joined:
    Nov 18, 2018
    Posts:
    1
    @mangax, can you share or explain animation playable for the masks so that you can allow character to keep running even while attacking or getting damage...In your above video, it was not included in the sample script and trying to achieve this. Thanks