Moving platform client desync

I have created a vertical moving platform which on a players personal screen is perfectly synced, no jittering and you keep your velocity. However for any other player when watching someone on the elevator, the player is sinking through the floor a little when going upwards and hovering when going downward. Any idea how to fix this? Ive tried the platform as a rigid body which made it 10x worse, ive used a client network transform as well as a network transform. My players use client network transforms and are controlled by character controllers.

using UnityEngine;
using Unity.Netcode;
using System.Collections.Generic;

public class VerticalPlatform : NetworkBehaviour
{
    #region Variables
    [Header("Settings")]
    [SerializeField] private float m_speed;
    [SerializeField] private Vector3 m_startPosition;
    [SerializeField] private Vector3 m_endPosition;

    private float m_direction;
    private Dictionary<ulong, NetworkObject> m_playersOnPlatform = new Dictionary<ulong, NetworkObject>();
    #endregion

    #region Mono
    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.T))
        {
            m_direction = 1.0f;
        }

        if(Input.GetKeyDown(KeyCode.Y))
        {
            m_direction = -1.0f;
        }
    }

    private void FixedUpdate()
    {
        if(m_direction == 0.0f) return;

        if(IsServer)
        {
            // Move the platform
            if (m_direction == 1.0f) MoveUp();
            if (m_direction == -1.0f) MoveDown();
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        // Check if collider belongs to a player
        var obj = other.GetComponent<NetworkObject>();
        if(obj != null && obj.GetComponent<PlayerMovement>() != null)
        {
            // Try set parent
            obj.TrySetParent(transform, true);

            // Ensure player is snapped to the platform's position on parenting
            var relativePosition = transform.InverseTransformPoint(obj.transform.position);
            obj.transform.position = transform.TransformPoint(relativePosition);

            // Add to dictionary
            m_playersOnPlatform.Add(obj.OwnerClientId, obj);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        // Check if collider belongs to a player
        var obj = other.GetComponent<NetworkObject>();
        if(obj != null && obj.GetComponent<PlayerMovement>() != null)
        {
            // Check if players on platform
            if(m_playersOnPlatform.ContainsKey(obj.OwnerClientId))
            {
                // Try remove parent
                m_playersOnPlatform[obj.OwnerClientId].TryRemoveParent(true);

                            // Ensure player is snapped to the platform's position on parenting
            var relativePosition = transform.InverseTransformPoint(obj.transform.position);
            obj.transform.position = transform.TransformPoint(relativePosition);

                // Remove from dictionary
                m_playersOnPlatform.Remove(obj.OwnerClientId);
            }
        }
    }
    #endregion

    #region Platform
    private void MoveUp()
    {
        // Increase y value
        var newPos = transform.position;
        newPos.y += m_speed * Time.fixedDeltaTime;

        // Update transform
        transform.position = newPos;

        // Check if y value has reached target
        if(transform.position.y >= m_endPosition.y)
        {
            // Set to target
            transform.position = m_endPosition;

            // Clear direction
            m_direction = 0.0f;
        }

        UpdatePositionClientRPC(transform.position);
    }

    private void MoveDown()
    {
        // Decrease y value
        var newPos = transform.position;
        newPos.y -= m_speed * Time.fixedDeltaTime;

        // Update transform
        transform.position = newPos;

        // Check if y value has reached target
        if(transform.position.y <= m_startPosition.y)
        {
            // Set to target
            transform.position = m_startPosition;

            // Clear direction
            m_direction = 0.0f;
        }

        UpdatePositionClientRPC(transform.position);
    }

    [ClientRpc]
    private void UpdatePositionClientRPC(Vector3 newPosition)
    {
        if(IsServer) return;

        transform.position = newPosition;
    }
    #endregion
}
using Unity.Netcode;
using UnityEngine;

public class PlayerMovement : NetworkBehaviour
{
    #region Variables
    [Header("Camera")]
    [SerializeField] private Transform m_camera;
    [SerializeField] private float m_sensitivity;

    [Header("Movement")]
    [SerializeField] private float m_walkSpeed;
    [SerializeField] private float m_jumpHeight;
    private bool m_isGrounded;

    private CharacterController m_controller;
    #endregion

    #region Mono
    private void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
    }

    private void Update()
    {
        if(IsOwner)
        {
            m_isGrounded = m_controller.isGrounded;

            HandleCameraInput();
            HandleMoveInput();
        }
    }
    #endregion

    #region Network
    public override void OnNetworkSpawn()
    {
        // Owner only
        if(IsOwner)
        {
            m_controller = GetComponent<CharacterController>();
        }
    }
    #endregion

    #region Camera
    private void HandleCameraInput()
    {
        // Get raw input
        var input = PlayerInput.Instance.GetRawCameraInput() * m_sensitivity;

        // Rotate camera (up and down)
        m_camera.localRotation = Quaternion.Euler(new Vector3(input.y, 0.0f, 0.0f));;

        // Rotate body (left and right)
        transform.rotation *= Quaternion.Euler(new Vector3(0.0f, input.x, 0.0f));
    }
    #endregion

    #region Motor
    private void HandleMoveInput()
    {
        // Handle jump input
        HandleJumpInput();

        // Get raw input
        var input = PlayerInput.Instance.GetRawMoveInput();
        input.x *= m_walkSpeed;
        input.y *= Time.deltaTime;
        input.z *= m_walkSpeed;

        // Apply movement
        m_controller.Move(transform.TransformDirection(input));
    }

    private void HandleJumpInput()
    {
        // Get jump input
        if(Input.GetKeyDown(KeyCode.Space) && m_isGrounded)
        {
            PlayerInput.Instance.SetVelocity(PlayerInput.Instance.GetVelocity() + Mathf.Sqrt(m_jumpHeight * -2.0f * Physics.gravity.y));
        }

        // Add gravity
        if(!m_isGrounded)
        {
            PlayerInput.Instance.SetVelocity(PlayerInput.Instance.GetVelocity() + Physics.gravity.y * Time.deltaTime);
        }

        // Reset velocity
        if(m_isGrounded && PlayerInput.Instance.GetVelocity() < 0.0f)
        {
            PlayerInput.Instance.SetVelocity(-2.0f);
        }
    }
    #endregion
}
Netcode Moving Platform Desync
1 Like

@iiProLivo
You might take a look at this example of handling platforms and non-rigidbody character controllers here.

It does use NGO v2 which requires Unity 6, but there are some additional feature sets used from NGO v2 that allows for smooth parenting transitions and keeping the character controller in synch with the platform (specific to the issue you are describing).

There is a moving platform that has the motion of an elevator in the example and I think it should provide you with a good starting point.

Let me know if this helps?

1 Like

Hey have you found a fix for it ?
I’m also facing the same issue

@Codex_23
Did you look at the example I linked to above?

i’m not using unity 6 I’m using 2022.3.17f1 so I can’t use NGO v2 also i’m not parenting the player to the moving platform rather i’m updating the player’s position ( moving platform’s position + player’s position relative to the moving platform ) in late update

@Codex_23

You should still look at the example to see how it is done, but NGO v2.x has several things that allow you to more easily move something around on the platform… but it is the same principle in the implementation within the example as it would be for NGO v1.x (minus the auto-switching from world to local).

With v1.x you would need to switch to local space but also teleport since the interpolators are not automatically recalculated for the local to world space (or vice versa) transitions.

1 Like

@NoelStephens_Unity
Thanks for the reply
I did saw the example but I don’t quite understand how it resolves my issue
I use client network transform with interpolate enabled for the players
I have a platform that moves vertically
Whenever the platform goes up the players on top of the platform either sinks into the platform or floats slightly above the platform (on a client’s screen the player character owned by them works properly but the other players are floating)
By debugging what I found out is when the platform moves the client sends the new position of the player to the server but before the server gets the position the platform starts moving and it causes the player to sink
It’s more noticeable with interpolate enabled

@Codex_23

If you look at the CharacterController example and actually run it you will see that it has all sorts of examples of moving platforms and how it handles transitioning.

This example does not use Rigidbody as that can be more complicated to achieve.
The primary concept is that you want to parent players under the moving platform (the example uses a trigger to accomplish this) so motion and rotation becomes local relative to the platform.

Now, if you are using a Rigidbody for your players then I would recommend not including a rigidbody under your platforms (but still use colliders obviously) as that will just cause fighting between the physics bodies (both kinematic and non-kinematic depending upon who owns the platform…which typically would be the server).

Let me know if the example makes more sense?

Thanks for the reply, I tried this method of parenting the player to the moving platform but it didn’t solve the issue (could be because my player has a rigidbody and the movement logic is based on the rigidbody) but i somehow solved it by writing a new script for syncing the player transform instead of using the client network transform

1 Like