Animator root motion appears strange on clients when used

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?

Are you using the NetworkAnimator component with the network prefab?

Yes, I’m using the NetworkAnimator component and the NetworkTransform component.

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:

public class RootMotionAnimator : NetworkAnimator
{
    public enum AuthorityModes
    {
        Server,
        Owner,
    }
    public AuthorityModes AuthorityMode;
    protected override bool OnIsServerAuthoritative()
    {
        return AuthorityMode == AuthorityModes.Server;
    }

    private void ApplyRootMotion()
    {
        Animator.applyRootMotion = OnIsServerAuthoritative() ? IsServer : IsOwner;
    }

    public override void OnNetworkSpawn()
    {
        // Always invoke the base when overriding NetworkAnimator's OnNetworkSpawn method
        base.OnNetworkSpawn();
        ApplyRootMotion();
    }

    protected override void OnOwnershipChanged(ulong previous, ulong current)
    {
        // Always invoke the base when overriding NetworkAnimator's OnOwnershipChanged method
        base.OnOwnershipChanged(previous, current);
        // This handles the scenario where you are using an owner authoritative motion model
        // and ownership changes.
        ApplyRootMotion();
    }
}

Alternately, you can create a custom NetworkTransform and apply the same script with a minor addition of having a reference to NetworkAnimator:

   public class RootMotionTransform : NetworkTransform
    {     
        public enum AuthorityModes
        {
            Server,
            Owner,
        }
        public AuthorityModes AuthorityMode;
        private Animator m_Animator;
        protected override bool OnIsServerAuthoritative()
        {
            return AuthorityMode == AuthorityModes.Server;
        }

        protected override void Awake()
        {
            // Always invoke the base when overriding NetworkTransform's Awake method
            base.Awake();
            // Depending on where your Animator is located you might need to adjust this
            // or just make the property public and assign it in the editor
            m_Animator = GetComponent<Animator>();
        }

        private void ApplyRootMotion()
        {
            m_Animator.applyRootMotion = OnIsServerAuthoritative() ? IsServer : IsOwner;
        }

        public override void OnNetworkSpawn()
        {
            // Always invoke the base when overriding NetworkTransform's OnNetworkSpawn method
            base.OnNetworkSpawn();
            ApplyRootMotion();
        }

        protected override void OnOwnershipChanged(ulong previous, ulong current)
        {
            // Always invoke the base when overriding NetworkTransform's OnOwnershipChanged method
            base.OnOwnershipChanged(previous, current);
            // This handles the scenario where you are using an owner authoritative motion model
            // and ownership changes.
            ApplyRootMotion();
        }
    }

Try this out and see if it resolves your issue?

1 Like

Thanks, I’ll give it a try.

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

Ahh…sorry I forgot to ask you which version of NGO you are using?

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)

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.

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)

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.

If you could provide a sample project, it would greatly help me…help you. :wink:

Of course :slight_smile: 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&)

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.

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.

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:

 void OnAnimatorMove()
    {
        if (!IsOwner)
            return;

        var transform = this.transform;
        var animatorDeltaPosition = Animator.deltaPosition;

            transform.position += new Vector3(
                animatorDeltaPosition.x * rootMotionMultiplierXZ,
                animatorDeltaPosition.y * rootMotionMultiplierY,
                animatorDeltaPosition.z * rootMotionMultiplierXZ
            );
            transform.rotation *= Animator.deltaRotation;
    }

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.

1 Like

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.

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.

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.

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.

1 Like