Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How to disable "WriteDefaults" in custom Playable/Graph animator?

Discussion in 'Animation' started by vexe, Jul 27, 2019.

  1. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Greetings,

    so I wrote this simple animator with the Playable/graph API (simple, no crossfade etc, intended for 2D). Every time I transition to an animation it seem to write the default values for unused keys in that animation.

    The player is made up of different body sprites. Head, legs, chest. etc. The example we're having is a charge attack. Once the player enters the charge state, he plays the charge animation on loop, it's supposed to only affect his feet sprite/position (that's the only keys in the charge animation), but it also resets all his other sprites making them face the wrong direction.

    I've tested this exact scenario in just pure Animator/mecanim and unchecking "Write Defaults" and I can confirm it does the correct behavior. It's just I can't seem to find a way to do this with Playables.

    Here's how I set the graph:

    Code (csharp):
    1.  
    2. public void Initialize()
    3.     {
    4.         if (!Playback) Playback = gameObject.GetComponent<Animator>();
    5.         if (!Playback) Playback = gameObject.AddComponent<Animator>();
    6.         Playback.runtimeAnimatorController = null;
    7.         Playback.cullingMode = AnimatorCullingMode.AlwaysAnimate;
    8.         Playback.updateMode = AnimatorUpdateMode.Normal;
    9.         Playback.enabled = false;
    10.  
    11.         if (States != null &&
    12.             States.Count > 0)
    13.         {
    14.             // Create the graph, mixer and ouput with connections
    15.             #if (UNITY_EDITOR)
    16.             Graph = PlayableGraph.Create("XAnimator-"+name);
    17.             #else
    18.             Graph = PlayableGraph.Create();
    19.             #endif
    20.             Graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
    21.  
    22.             MixerNode = AnimationMixerPlayable.Create(Graph, States.Count);
    23.             var AnimationOutput = AnimationPlayableOutput.Create(Graph, "Animation", Playback);
    24.             AnimationOutput.SetSourcePlayable(MixerNode);
    25.  
    26.             // Build lookup, create animation clip nodes and connect to graph
    27.             StateLookup.Clear();
    28.             for (int i = States.Count-1; i>=0; i--)
    29.             {
    30.                 var It = States[i];
    31.  
    32.                 if (It.Clip == null)
    33.                 {
    34.                     error("State clip is null at index: " + i);
    35.                     States.RemoveAt(i);
    36.                     continue;
    37.                 }
    38.          
    39.                 if (It.Clip.legacy)
    40.                 {
    41.                     error("State clip is legacy at index: " + i);
    42.                     States.RemoveAt(i);
    43.                     continue;
    44.                 }
    45.  
    46.                 if (string.IsNullOrEmpty(It.Name))
    47.                 {
    48.                     error("State name is null/empty at index: " + i);
    49.                     States.RemoveAt(i);
    50.                     continue;
    51.                 }
    52.  
    53.                 int StateHash = GetNameHash(It.Name);
    54.                 if (StateLookup.ContainsKey(StateHash))
    55.                 {
    56.                     error("Duplicate state hash: " + It.Name);
    57.                     States.RemoveAt(i);
    58.                     continue;
    59.                 }
    60.  
    61.                 StateLookup[StateHash] = It;
    62.  
    63.                 It.Node = CreateClipNode(It.Clip, i);
    64.                 It.Index = i;
    65.                 It.IsValid = true;
    66.                 It.Hash = StateHash;
    67.             }
    68.      
    69.             #if (UNITY_EDITOR)
    70.             // NOTE(Ali): Register graph so we can see it in this tool under Window/Analysis
    71.             GraphVisualizerClient.Show(Graph);
    72.             #endif
    73.         }
    74.     }
    75.  
    Here's how an animation is played:

    Code (csharp):
    1.  
    2.  
    3.     void PlayInternal(XAnimatorState State, float AtSpeed, bool ClearQueue=true)
    4.     {
    5.         if (CurrentState.IsValid)
    6.         {
    7.             MixerNode.SetInputWeight(CurrentState.Index, 0);
    8.             CurrentState.Node.Pause();
    9.         }
    10.  
    11.         Playback.enabled = true;
    12.         if (!State.KeepAnimatedValues)
    13.         {
    14.             Playback.WriteDefaultValues();
    15.         }
    16.  
    17.         MixerNode.SetInputWeight(State.Index, 1);
    18.  
    19.         State.Node.SetTime(0);
    20.         State.Node.Play();
    21.         State.Speed = AtSpeed;
    22.  
    23.         CurrentState = State;
    24.         if (ClearQueue) StateQueue.Clear();
    25.     }
    26.  
    27.  
    The graph is ticked manually in an Update, basically:

    Code (csharp):
    1.  
    2. public void Tick(float dt)
    3.  {
    4.         if (Graph.IsValid())
    5.         {
    6.             Graph.Evaluate(dt*FinalSpeed);
    7.         }
    8. }
    9.  
    This is my state class for reference. Didn't want to post everything, less noise:

    Code (csharp):
    1.  
    2. [Serializable]
    3. public class XAnimatorState
    4. {
    5.     public string Name;
    6.     public AnimationClip Clip;
    7.     public bool KeepAnimatedValues;
    8.  
    9.     [NonSerialized] public int Hash;
    10.     [NonSerialized] public int Index;
    11.     [NonSerialized] public AnimationClipPlayable Node;
    12.     [NonSerialized] public bool IsValid;
    13.     [NonSerialized] public float Speed = 1;
    14.  
    15.     public float ClipLength => Clip? Clip.length:0;
    16.     public bool IsLoop => Clip? Clip.isLooping:false;
    17. }
    18.  
    Note I tried all combinations of not doing 'WriteDefaultValues' at all, not disabling/re-enabling the Animator, etc. None seemed to have any affect.

    I suspect it's the way I'm playing the animations, maybe the output node needs to communicate to Animator to not reset values? I couldn't find any methods about writing default values except for Animator.WriteDefaultValues.

    In the attached gifs. BUG_CORRECT shows the correct expected behavior using Animator with WriteDefaults OFF in that second animation. Notice how he keeps his head the same sprite it was after the attack.

    In BUG_WRONG.gif however, he attacks, plays the 'charge' animation which resets him facing south overriding the previous facing he had (north) after the attack.

    Here's what my graph looks like:

    upload_2019-7-26_19-44-28.png

    If there's a piece of code you'd like to see that's missing for information let me know!

    Any help is appreciated, thanks!
     

    Attached Files:

  2. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,554
    According to this, disconnecting inactive playables from the mixer instead of just setting them to 0 weight should fix your issue. But even though that was written by Unity themselves, it didn't work like that when I tested it. Also, the keepStoppedPlayablesConnected they're referring to isn't exposed publicly so they probably never even tested it ...

    If you want different animations affecting different body parts individually you might need to use an AnimationLayerMixerPlayable with some masks.

    Also, why are you disabling the Animator? Doesn't that stop it from working entirely?
     
  3. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @Kybernetik I thought I saw it in that SimpleAnimation that they were turning m_Animator on/off as its needed but looking at their source code again I only see .enabled = true being set, and not false. Maybe I got the wrong impression. I tried having it always ON, still had the issue.

    I'm not sure how that layer mixer would play with 2D sprites? Is there any examples on this?

    That SimpleAnimation implementation is... umm, how do you say, interesting? I tried adding a simple bool writeDefaults but ended up having to add it to 6 different classes jumping through hoops and loops of wrapper classes... IState, State, StateInfo, StateHandle, StateImpl... lmao.

    I'll try to disconnect instead of setting weight to 0 when stopping, and connect & set weight to 1 when playing see if I have any luck! Thanks for the suggestion.
     
  4. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,554
    What I meant is that if I disable my Animator, nothing works at all, the model just stays in the T pose.

    If your body parts are different objects, then it should work exactly the same with 2D sprites as any other Generic Rig. AvatarMasks have a separate section for specifying custom Transforms instead of choosing Humanoid body parts, though I've never used it.
     
  5. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @Kybernetik so I literally boiled it down to using 1 connection now and it's still writing defaults...

    I also tried keeping the mixer with all the N inputs and just connecting one, still did the same.

    Is this what you had in mind? Not sure if I did this right.

    Mixer creation:
    Code (csharp):
    1.  
    2.             MixerNode = AnimationMixerPlayable.Create(Graph, 1, true);
    3.             MixerNode.SetInputWeight(0, 1);
    4.             var AnimationOutput = AnimationPlayableOutput.Create(Graph, "Animation", Playback);
    5.             AnimationOutput.SetSourcePlayable(MixerNode);
    6.  
    Playing:
    Code (csharp):
    1.  
    2.     void PlayInternal(XAnimatorState State, float AtSpeed, bool ClearQueue=true)
    3.     {
    4.         if (CurrentState.IsValid)
    5.         {
    6.             Graph.Disconnect(MixerNode, 0);
    7.         }
    8.      
    9.         Graph.Connect(State.Node, 0, MixerNode, 0);
    10.         State.Node.SetTime(0);
    11.         State.Node.Play();
    12.         State.Speed = AtSpeed;
    13.         CurrentState = State;
    14.         if (ClearQueue) StateQueue.Clear();
    15.     }
    16.  
    Node creation:
    Code (csharp):
    1.  
    2.     AnimationClipPlayable CreateClipNode(AnimationClip Clip)
    3.     {
    4.         var Node = AnimationClipPlayable.Create(Graph, Clip);
    5.         Node.SetApplyFootIK(false);
    6.         Node.SetApplyPlayableIK(false);
    7.         return(Node);
    8.     }
    9.  
    10.  
     
    Last edited: Jul 27, 2019
  6. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,554
    Yeah, that's pretty much what I was thinking of. I guess the Animator writes curves of everything in the PlayableGraph regardless of what is actually connected.

    Also, you don't need to call SetApplyPlayableIK(false); because it's false by default (unlike Foot IK).
     
  7. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    I also just tried ditching the mixer all together and connecting the clip nodes directly to the output. I create all the clip nodes in the graph, but only connect one at a time to the output.

    Code (csharp):
    1.  
    2.         OutputNode.SetSourcePlayable(State.Node);
    3.  
    So it's literally 2 nodes connected just like that first example in their docs.

    Doing that however produced really strange results. I didn't write frame defaults when I did the charge, but now things are all messed up. Calling Animator.WriteDefaultValues or Rebind() does nothing.

    upload_2019-7-27_0-1-8.png

    I'm running out of ideas...
     
  8. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,554
    Did you try using layers with masks? Animating separate body parts individually is exactly what they are made for.
     
  9. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Not yet, I'd have to talk to our artist about it. He did all the animations.

    Adding this Avatar Mask asset, it looks like it needs an Avatar under the Transform section. I have no idea how to create this :/ Looks like Avatars are automatically generated when importing 3D models. Not sure how to get one in 2D, I just have animation clips artist created animating sprites, positions etc.

    [Edit] So I'm able to create an avatar in code and saving it as an asset, but dragging it in the Avatar Mask field under Transform, the "Import Selection" button is still disabled...

    Code (csharp):
    1.  
    2.         Avatar Av = AvatarBuilder.BuildGenericAvatar(gameObject, string.Empty);
    3.         AssetDatabase.CreateAsset(Av, "Assets/"+name+"_Avatar.asset");
    4.  
    upload_2019-7-27_0-48-48.png

    What am I missing?
     
    Last edited: Jul 27, 2019
  10. DaseinPhaos

    DaseinPhaos

    Joined:
    Nov 20, 2017
    Posts:
    7
    I came across this problem as well. Wonder if any solution has been found yet
     
  11. Grhyll

    Grhyll

    Joined:
    Oct 15, 2012
    Posts:
    119
    Similar issue here :(

    Tested two configurations:

    - A PlayableGraph with only an AnimationClipPlayable connected to an AnimationLayerMixerPlayable with an AvatarMask: it behaves as if an inaccessible "Write Defaults" option was enabled on the state, because masked limbs reset to their position as soon as I call Evaluate on the graph (or I just cannot move them after calling Play).

    Straightforward setup code:
    Code (CSharp):
    1.         animator = GetComponentInParent<Animator>();
    2.  
    3.         playableGraph = PlayableGraph.Create();
    4.         AnimationPlayableOutput playableOutput = AnimationPlayableOutput.Create(playableGraph, "LeftHandPlayableOutput", animator);
    5.  
    6.         mixerPlayable = AnimationLayerMixerPlayable.Create(playableGraph, 1);
    7.         playableOutput.SetSourcePlayable(mixerPlayable);
    8.  
    9.         clipPlayable = AnimationClipPlayable.Create(playableGraph, clip);
    10.         mixerPlayable.ConnectInput(0, clipPlayable, 0, 1f);
    11.  
    12.         mixerPlayable.SetLayerMaskFromAvatarMask(0, mask);
    13.         mixerPlayable.SetLayerAdditive(0, false);
    - Same animation and AvatarMask, but on the Base Layer of an animator. The behaviour is the same when "Write Defaults" is enabled on the state; as soon as I uncheck it, I can freely move the rest of the limbs without them reverting to their default pose.

    It makes using AvatarMask in this configuration pretty useless :(

    Btw @vexe regarding your last (although old) post, as far as I know, the Avatar field you were wondering about is only there to generate the transforms list in the AvatarMask, but probably isn't used at runtime or has any impact, it's just an edit time helper.
     
  12. Grhyll

    Grhyll

    Joined:
    Oct 15, 2012
    Posts:
    119
    Anyone from Unity around there?
     
  13. Grhyll

    Grhyll

    Joined:
    Oct 15, 2012
    Posts:
    119
    One last bump before I give up on that and just animate a dummy, non-skinned skeletton and copy the transforms values on my model.
     
    forestrf likes this.
  14. forestrf

    forestrf

    Joined:
    Aug 28, 2010
    Posts:
    229
    I'm looking for the same thing. Can you try calling
    animator.Rebind(false)
    before changing the graph, as if it was a way to store the current state as defaults?
    It is a private method, but this is just to see if it works.

    Code (CSharp):
    1. var rebindMethod = typeof(Animator).GetMethod("Rebind", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    2. rebindMethod.Invoke(someAnimator, new object[] { false });
    It seems to work for me and after some test, It indeed seems to work as a way to "write" default values. Quite strange though. I don't know how it could backfire, for now it's working.
    For performance, the MethodInfo and object array should be cached.

    This is not the same as don't write defaults, but if this really works, it gives some margin to operate.
     
    Last edited: Dec 4, 2021