Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question Animator root motion appears strange on clients when used

Discussion in 'Netcode for GameObjects' started by Bechmann, Nov 20, 2023.

  1. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    I want to use root motion to move my enemy characters. It looks fine on the host, but on clients, it appears incorrect. It seems like they are not utilizing the actual root motion but only updating the transform position, resulting in what seems more like floating movement than an actual walking motion that aligns with the animation. Do I need to implement any specific synchronization for root motion, or is it simply not well-suited for use with netcode?
     
  2. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    Are you using the NetworkAnimator component with the network prefab?
     
  3. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    Yes, I'm using the NetworkAnimator component and the NetworkTransform component.
     
  4. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    Ok,
    On the non-authoritative (non-owner) instances you most likely want to set Animator.applyRootMotion to false.
    The reason behind this is that the non-authoritative instances are just "mirroring" the motion (NetworkTransform) and the animation (NetworkAnimator) where the authoritative instance is what drives the animation that, in turn, drives the changes to position (motion).

    You can do this several ways...one way is to create a custom NetworkAnimator and do something like this:
    Code (CSharp):
    1.  
    2. public class RootMotionAnimator : NetworkAnimator
    3. {
    4.     public enum AuthorityModes
    5.     {
    6.         Server,
    7.         Owner,
    8.     }
    9.     public AuthorityModes AuthorityMode;
    10.     protected override bool OnIsServerAuthoritative()
    11.     {
    12.         return AuthorityMode == AuthorityModes.Server;
    13.     }
    14.  
    15.     private void ApplyRootMotion()
    16.     {
    17.         Animator.applyRootMotion = OnIsServerAuthoritative() ? IsServer : IsOwner;
    18.     }
    19.  
    20.     public override void OnNetworkSpawn()
    21.     {
    22.         // Always invoke the base when overriding NetworkAnimator's OnNetworkSpawn method
    23.         base.OnNetworkSpawn();
    24.         ApplyRootMotion();
    25.     }
    26.  
    27.     protected override void OnOwnershipChanged(ulong previous, ulong current)
    28.     {
    29.         // Always invoke the base when overriding NetworkAnimator's OnOwnershipChanged method
    30.         base.OnOwnershipChanged(previous, current);
    31.         // This handles the scenario where you are using an owner authoritative motion model
    32.         // and ownership changes.
    33.         ApplyRootMotion();
    34.     }
    35. }
    36.  

    Alternately, you can create a custom NetworkTransform and apply the same script with a minor addition of having a reference to NetworkAnimator:
    Code (CSharp):
    1.  
    2.    public class RootMotionTransform : NetworkTransform
    3.     {    
    4.         public enum AuthorityModes
    5.         {
    6.             Server,
    7.             Owner,
    8.         }
    9.         public AuthorityModes AuthorityMode;
    10.         private Animator m_Animator;
    11.         protected override bool OnIsServerAuthoritative()
    12.         {
    13.             return AuthorityMode == AuthorityModes.Server;
    14.         }
    15.  
    16.         protected override void Awake()
    17.         {
    18.             // Always invoke the base when overriding NetworkTransform's Awake method
    19.             base.Awake();
    20.             // Depending on where your Animator is located you might need to adjust this
    21.             // or just make the property public and assign it in the editor
    22.             m_Animator = GetComponent<Animator>();
    23.         }
    24.  
    25.         private void ApplyRootMotion()
    26.         {
    27.             m_Animator.applyRootMotion = OnIsServerAuthoritative() ? IsServer : IsOwner;
    28.         }
    29.  
    30.         public override void OnNetworkSpawn()
    31.         {
    32.             // Always invoke the base when overriding NetworkTransform's OnNetworkSpawn method
    33.             base.OnNetworkSpawn();
    34.             ApplyRootMotion();
    35.         }
    36.  
    37.         protected override void OnOwnershipChanged(ulong previous, ulong current)
    38.         {
    39.             // Always invoke the base when overriding NetworkTransform's OnOwnershipChanged method
    40.             base.OnOwnershipChanged(previous, current);
    41.             // This handles the scenario where you are using an owner authoritative motion model
    42.             // and ownership changes.
    43.             ApplyRootMotion();
    44.         }
    45.     }
    46.  
    Try this out and see if it resolves your issue?
     
    Last edited: Nov 20, 2023
    Bechmann likes this.
  5. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    Thanks, I'll give it a try.
     
  6. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    The script is giving me errors on the 'OnOwnershipChanged' function.

    Error CS0117 'NetworkTransform' does not contain a definition for 'OnOwnershipChanged' Assembly-CSharp E:\Unity files\Horror Shooter 18112023\Assets\Scripts\RootMotionTransform.cs 35 Active

    Error CS0115 'RootMotionTransform.OnOwnershipChanged(ulong, ulong)': no suitable method found to override Assembly-CSharp E:\Unity files\Horror Shooter 18112023\Assets\Scripts\RootMotionTransform.cs 32 Active
     
  7. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    Ahh...sorry I forgot to ask you which version of NGO you are using?
     
  8. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    NetworkBehaviour.OnOwnershipChanged is in NGO v1.7.0.
    So, if you are on an earlier version you can actually comment out that method if you aren't planning on changing ownership and/or to just want to test it.

    (sorry about that)
     
  9. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    I'm using NGO v1.6.0. I've tried both scripts, but unfortunately, they did not resolve the issue. I'm still experiencing this kind of floating from side-to-side motion on the clients, even though it looks perfectly fine on the host.
     
  10. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    Hmmm,
    Well the only way I could further assist you is to be able to see how you have everything setup (including the scripts).
    You could file an issue within the editor and include your project in the submission, then post the ticket of the issue/bug submission here so I can take a look and see what is happening.
    (Or provide me with a repository link or bare-bones project that replicates the issue)
     
  11. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    I could try to set up a really simple project for you to look at. I have removed all custom scripts from the equation in my testing, just to make sure I wasn't doing something unexpected somewhere in my code. I can replicate the issue just by having a character that only has an Animator, NetworkTransform, NetworkObject, and a NetworkAnimator component. I can't quite understand what's happening, probably because I don't have enough understanding of how the positions are carried from the server or host to the clients. One thing I have noticed in my testing is that if I use the same animation as an in-place animation with no root motion, I get the same kind of floating from side to side motion on the host if the Root Transform Position (XZ) is not checked as Baked Into Place on the animation. I'm not sure, but it seems like it has something to do with how all the different parts of the character move to simulate real motion, such as shifting weight from foot to foot, but the position that gets carried over to the clients doesn't take this into account.
     
    Last edited: Nov 22, 2023
  12. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    If you could provide a sample project, it would greatly help me...help you. ;)
     
  13. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    Of course :) and I appreciate the help. I have a project where I have deleted everything except what is needed. I have uploaded it to my Google Drive; hope that's fine. You just have to press 'S' to spawn the character and 'C' to start the animation. Just watch the feet when it's walking; the difference between the client and the host should be pretty clear.

    https://drive.google.com/file/d/1HcXlBWdSktVkri1nVR-Ebko8rmqOq6Zw/view?usp=sharing

    Also, if it's not too much, maybe you could answer why I get this warning when making a build. It seems it was there right from the start and is showing up even when building a completely empty scene.


    Unity.Tutorials.Core.Editor.BuildStartedCriterion must be instantiated using the ScriptableObject.CreateInstance method instead of new BuildStartedCriterion.

    UnityEngine.ScriptableObject:.ctor ()

    Unity.Tutorials.Core.Editor.Criterion:.ctor () (at ./Library/PackageCache/com.unity.learn.iet-framework@3.1.3/Editor/Criteria/Criterion.cs:43)

    Unity.Tutorials.Core.Editor.PreprocessBuildCriterion:.ctor ()

    Unity.Tutorials.Core.Editor.BuildStartedCriterion:.ctor ()

    UnityEngine.GUIUtility:processEvent (int,intptr,bool&)
     
  14. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    Ok,
    I see the issue indeed.
    It looks like the root motion being applied is handled completely within the blend trees and I believe that our support for blend trees (currently) is very limited. So, the only thing being synchronized with the NetworkAnimator/RootMotionAnimtor is the forward and turn parameters of the Animator.

    The best advice I could provide for this setup would be:
    • Remove the NetworkTransform itself
    • You can opt to keep the NetworkAnimator, but for two parameters (i.e. forward and turn) I might just suggest updating a NetworkVariable for both and when forward or turn changes just apply it to the client side.
    They could get out of synch depending upon the system and the rate in which each instance is updating the root motion of the animator, but they should stay relatively in synch.

    Most likely the best solution (if you want to use root motion) is to take a look at this (Non-Unity) guide on synchronizing navmesh motion with root motion (they provide a full GitHub project). The idea would be that the server side would dictate "where a zombie should go" as a point on the NavMesh (could be a NetworkVariable on the Zombie) and when updated on the client side you would just apply that new point to the client-side NavMeshAgent.

    Then the root motion would just "automatically" be synchronized with the NavMeshAgent and you can be assured that all Zombies would end up at the same destination without having to synchronize each delta along the way. This would reduce your bandwidth cost per zombie considerably (i.e. you are updating one Vector3 NetworkVariable every so often per Zombie as opposed to updating every minor delta of motion of each Zombie). It won't be a 100% perfect 1:1 match in regards to animation if you had a host and a few clients side-by-side... but it would be about as close as if you were synchronizing Animator State changes.

    If I was going to use that model with root motion...most likely the NavMeshAgent approach is what I would end up doing. I am also assuming you are going to want "many zombies"... so you might even think about using a HalfVector3 for your NavMesh "point of interest" (a NetworkVariable on the Zombie) since you won't need a perfectly precise point because it will be translated to the right tri of the NavMesh via the NavMeshAgent (i.e. as long as the point is within the tri it will pick the same one as the server side).

    But yeah, with that Animator and the BlendTrees... it won't work properly with NetworkAnimator and NetworkTransform.
     
  15. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    Thanks for the help. I'll consider how I will proceed from here, but I'm determined to make it work with or without root motion. It's just good to know that my current approach isn't worth investing more time in. I love working with Unity and really want my next game to include an option for co-op.
     
  16. Nitrousek

    Nitrousek

    Joined:
    Jan 31, 2016
    Posts:
    38
    You could try implementing OnAnimatorMove(). This overwrites root motion.

    Inside this function, you can use Animator.deltaposition, which you can use to apply the motion on owner, and clients should mirror using NetworkTransform. This is what I do in my game.

    Example code below:
    Code (CSharp):
    1.  void OnAnimatorMove()
    2.     {
    3.         if (!IsOwner)
    4.             return;
    5.  
    6.         var transform = this.transform;
    7.         var animatorDeltaPosition = Animator.deltaPosition;
    8.  
    9.             transform.position += new Vector3(
    10.                 animatorDeltaPosition.x * rootMotionMultiplierXZ,
    11.                 animatorDeltaPosition.y * rootMotionMultiplierY,
    12.                 animatorDeltaPosition.z * rootMotionMultiplierXZ
    13.             );
    14.             transform.rotation *= Animator.deltaRotation;
    15.     }
    You can remove rootmotion multipliers, I use them to change movement in different axis based on my needs.

    You will notice this new message appear on your animator, that's how you know it's working.
     
  17. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    Thank you for the suggestion. I had tried something similar, but, unfortunately, neither that nor this resolves the issue. The client still has some floating motion at the feet. It's not that much, but it's noticeable. So, it seems like I'll just have to use scripted motion, though it would have been nice to be able to use the root motion.
     
  18. Nitrousek

    Nitrousek

    Joined:
    Jan 31, 2016
    Posts:
    38
    Ok, at second look, It seems the floating is caused by NetworkTransform interpolation. So indeed my approach wouldn't fix this. I guess the method provided by Noel with just passing NavMesh destinations would work best in this scenario - but it's indeed sad that you need to resolve to this rather than this working out of the box.
     
  19. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    Definitely one of those things on my list...the NavMesh solution would be the best approach as typically you will want to have some form of "intelligent" navigation for AI and since root motion is FixedUpdate synchronized you should get the same amount of motion and generate the same path (A* is good like that) on all instances (remote or local).

    The real issue is trying to get the exact same motion (visually) by synchronizing change in the transform and animations, but you can kind of think of root motion like the CharacterController...if you want to synchronize the deltas of the authority's transform then you only want the authority instance active... otherwise on the non-authority sides you will get fighting between the network transform updates (sent every tick) and the local system (whether CharacterController or Animator root motion).

    In the end, typically with things like this sometimes the "seemingly logical" approach is not the "best approach". With the NavAgent approach, you would get the over-all same visual results and send way less data. Of course, it does require changing one's mindset when implementing this approach.
    As an example:
    You would want to implement a "NetworkStateMachine" for the zombies where each state update included a specific payload that was needed for the state. An "ambient motion" state could have a zombie navigating between two points on a NavMesh. As the authority version of the Zombie does this, you would be getting updates on the players' positions...the closer a player gets to the Zombie the more of a chance it could "hear" or "see" the players. If a zombie does react to a player, then the state could be changed to "TargetPlayer" that included the player's NetworkObjectReference and perhaps some other information. As the Zombie gets within a specific distance of the payer, the authority could add a conditional "PerformAttack" state that basically has additional information used to determine when to visually "attack"... like a final destination point on the NavMesh before switching to a root motion driven attack. During that time (like perhaps 100-300ms) the authority might send a couple of "AttackInfo" state updates that describe the type of root motion attack and whether it hit or not... etc.
    The idea being that as opposed to trying to synchronize the motion perfectly it synchronizes the events that are bound by the commonly shared constraints of the NavMesh and Animator's root motion that is driven by state updates.
    It all depends upon the style of game...but using a NavMesh is how most games handle AI navigation anyway. With a state machine approach you can also "group" AI at a distance (i.e. a NetworkGroupManager that AI registers with when more than a certain distance from the players) and send the group a "MoveToPoint" state update which the NetworkGroupManager handles spreading out the AI and migrating them to the "relative point"... which from a bandwidth perspective is much more effective then trying to make each individual AI figure out how to keep spread apart and update their unique position (each) every 33ms (i.e. per tick).

    But again... all depends upon what one is trying to achieve... if you don't have that many monsters then it might make sense to provide a more "analog-like" continual update to a monster's position.
     
  20. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    So, I watched a video and got inspired. I actually ended up finding a way to synchronize the transforms from the server to the clients without getting this floating feet effect. Therefore, I have removed the NetworkTransform from the zombies and can now use root motion without any problems. And still with everything synchronized from the server, so enemies won't risk being in different places from client to client.
     
    NoelStephens_Unity likes this.
  21. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    Nice!
    I look forward to a future sneak peek of the end result!! ;)
     
  22. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    Yes, I will post something here soon. It's pretty simple but works well for the root motion. There might be some aspects I'm not aware of, which I will have to address later. But for now, this seems to be a solution for me. However, when simulating bad network conditions, the NetworkTransform is still better at smoothing everything out, making it less noticeable. So, I want to try and see if I can improve that.
     
    Last edited: Dec 5, 2023
  23. Bechmann

    Bechmann

    Joined:
    Nov 5, 2017
    Posts:
    27
    I haven't been able to work on it much in the last couple of days, so there's definitely still some more work and optimization that could be done. However, it functions as it should, and I've also managed to make it look fine even when simulating bad network conditions. I tried several different things, including interpolation, but that created the same floating motion as in the NetworkTransform. So, I ended up only using that approach for the rotation, although I might consider just doing the same for rotaion as I do for the position, since that seems to work fine. I've removed all the code that is specific to my project since I did not create it in an empty project, similar to the one I uploaded earlier. Hope it can be usefull to you or anyone having the same problem.


    Code (CSharp):
    1. private void Update()
    2. {
    3.     if (IsServer)
    4.     {
    5.         SendPositionToServerRpc(transform.position, transform.rotation);
    6.     }
    7. }
    8.  
    9. [ServerRpc]
    10. private void SendPositionToServerRpc(Vector3 position, Quaternion rotation)
    11. {
    12.     SendPositionFromServerToClientRpc(position, rotation, NetworkTime());
    13. }
    14.  
    15. [ClientRpc]
    16. private void SendPositionFromServerToClientRpc(Vector3 position, Quaternion rotation, float serverTime)
    17. {
    18.     if (IsServer) return;
    19.  
    20.     networkPosition = position;
    21.  
    22.     State state = new State
    23.     {
    24.         timestamp = serverTime,
    25.         position = position,
    26.         rotation = rotation
    27.     };
    28.  
    29.     stateBuffer.Enqueue(state);
    30.     while (stateBuffer.Count > 20)
    31.     {
    32.         stateBuffer.Dequeue();
    33.     }
    34.  
    35. }
    36.  
    37. private void InterpolatePositionAndRotation()
    38. {
    39.     if (stateBuffer.Count == 0) return;
    40.  
    41.     float interpolationTime = NetworkTime() - interpolationBackTime;
    42.  
    43.     while (stateBuffer.Count > 1 && stateBuffer.Peek().timestamp < interpolationTime)
    44.     {
    45.         stateBuffer.Dequeue();
    46.     }
    47.  
    48.     if (stateBuffer.Count < 2) return;
    49.  
    50.     State latest = stateBuffer.Dequeue();
    51.     State previous = stateBuffer.Peek();
    52.  
    53.     float t = (interpolationTime - previous.timestamp) / (latest.timestamp - previous.timestamp);
    54.     //transform.position = Vector3.Lerp(previous.position, latest.position, t);
    55.     transform.rotation = Quaternion.Slerp(previous.rotation, latest.rotation, t);
    56. }
    57.  
    58. private struct State
    59. {
    60.     public float timestamp;
    61.     public Vector3 position;
    62.     public Quaternion rotation;
    63. }
    64.  
    65. private float NetworkTime()
    66. {
    67.     return NetworkManager.Singleton.ServerTime.TimeAsFloat;
    68. }
    69.  
    70. private void LateUpdate()
    71. {
    72.     if (!IsServer)
    73.     {
    74.         Vector3 positionError = networkPosition - transform.position;
    75.  
    76.         if (positionError.magnitude > positionErrorThreshold)
    77.         {
    78.             transform.position = Vector3.Lerp(transform.position, networkPosition, Time.deltaTime * lerpTime);
    79.         }
    80.         else
    81.         {
    82.             transform.position = networkPosition;
    83.         }
    84.  
    85.         InterpolatePositionAndRotation();
    86.     }
    87. }
     
    Last edited: Dec 5, 2023
    NoelStephens_Unity likes this.