Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

Question [Netcode for Gameobjects] Multiplayer Player Sync Problem In Moving Spaceship

Discussion in 'Multiplayer' started by unity_A8EA531F8FAD850AE5C6, May 23, 2024.

  1. unity_A8EA531F8FAD850AE5C6

    unity_A8EA531F8FAD850AE5C6

    Joined:
    Dec 13, 2023
    Posts:
    2
    Hey everyone, I've been dealing with an issue in my multiplayer game for the past week and I could really use some help. Essentially, I have a game where players are aboard a small spaceship, which is currently controlled by the host for simplicity. I want players to be able to move around freely on the spaceship while it moves, too. Initially, I thought parenting the players to the ship would solve this, but I quickly found out it caused a lot of problems, so I abandoned that approach.

    Instead, I coded a solution where the players' positions are manually set to match the ship's position while still allowing them to move independently. This works perfectly in single player mode, but when it comes to multiplayer, issues arise. From the client's perspective (player 2), everything looks fine when looking at the host. Both the host and the client appear to be where they should be. However, when looking through the host's perspective, the client seems to be out of sync, and I can't figure out why.

    Both the ship and the player prefabs have a client network transform. Since I'm still a beginner, I might be making an obvious mistake, so any help would be greatly appreciated.
    here are the scripts:

    the FollowTransformShip that is attached to the player:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Unity.Netcode;
    5.  
    6.  
    7. public class FollowTransformShip : NetworkBehaviour
    8. {
    9.     public Transform shipTransform;
    10.     public Vector3 lastShipPosition;
    11.     public Quaternion lastShipRotation;
    12.  
    13.     private void Start()
    14.     {
    15.         UpdateLastTransform();
    16.     }
    17.  
    18.     public void SetTargetTransform(Transform targetTransform)
    19.     {
    20.         this.shipTransform = targetTransform;
    21.         UpdateLastTransform();
    22.     }
    23.  
    24.     private void LateUpdate()
    25.     {
    26.         if (!IsLocalPlayer)
    27.             return;
    28.         if (shipTransform == null)
    29.             return;
    30.  
    31.         Vector3 shipMovement = shipTransform.position - lastShipPosition;
    32.         transform.position += shipMovement;
    33.  
    34.         Quaternion shipRotationChange = shipTransform.rotation * Quaternion.Inverse(lastShipRotation);
    35.         transform.rotation = shipRotationChange * transform.rotation;
    36.  
    37.         Vector3 positionOffset = shipTransform.position - transform.position;
    38.         positionOffset = shipRotationChange * positionOffset;
    39.         transform.position = shipTransform.position - positionOffset;
    40.  
    41.         UpdateLastTransform();
    42.     }
    43.  
    44.     private void UpdateLastTransform()
    45.     {
    46.         if (shipTransform != null)
    47.         {
    48.             lastShipPosition = shipTransform.position;
    49.             lastShipRotation = shipTransform.rotation;
    50.         }
    51.     }
    52. }
    the player script:
    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Netcode;
    3.  
    4. public class Player : NetworkBehaviour
    5. {
    6.     [SerializeField] private float moveSpeed = 7f;
    7.     [SerializeField] private float lookSpeed = 2f;
    8.     [SerializeField] private Transform playerCameraTransform;
    9.     [SerializeField] public Transform objectGrabPointTransform;
    10.     [SerializeField] private LayerMask pickUpLayerMask;
    11.     [SerializeField] private LayerMask spaceshipLayerMask;
    12.     [SerializeField] private LayerMask openHatchLayerMask;
    13.     [SerializeField] private PlayerVisual playerVisual;
    14.     [SerializeField] private TextManager textManager;
    15.  
    16.     private ObjectGrabbable objectGrabbable;
    17.     private bool isWalking;
    18.     private float verticalLookRotation;
    19.     private bool isPilotingSpaceship = false;
    20.     public SpaceShip spaceShip;
    21.     public static Player LocalInstance { get; private set; }
    22.  
    23.     private void Start()
    24.     {
    25.         if (IsOwner)
    26.         {
    27.             LocalInstance = this;
    28.             if (textManager == null)
    29.             {
    30.                 textManager = FindObjectOfType<TextManager>();
    31.             }
    32.         }
    33.     }
    34.  
    35.     public override void OnNetworkSpawn()
    36.     {
    37.         if (IsLocalPlayer)
    38.         {
    39.             GetComponentInChildren<Camera>().enabled = true;
    40.             Cursor.lockState = CursorLockMode.Locked;
    41.             Cursor.visible = false;
    42.         }
    43.         else
    44.         {
    45.             var cameras = GetComponentsInChildren<Camera>();
    46.             foreach (var cam in cameras)
    47.             {
    48.                 cam.enabled = false;
    49.             }
    50.         }
    51.         SetPlayerColor();
    52.     }
    53.  
    54.     private void SetPlayerColor()
    55.     {
    56.         PlayerData playerData = KitchenGameMultiplayer.Instance.GetPlayerDataFromClientId(OwnerClientId);
    57.         if (playerVisual != null)
    58.         {
    59.             playerVisual.SetPlayerColor(KitchenGameMultiplayer.Instance.GetPlayerColor(playerData.colorId));
    60.         }
    61.     }
    62.  
    63.     private void Update()
    64.     {
    65.         if (!IsOwner)
    66.         {
    67.             return;
    68.         }
    69.         if (!isPilotingSpaceship)
    70.         {
    71.             HandleMovement();
    72.         }
    73.         else
    74.         {
    75.             HandleSpaceshipControls();
    76.         }
    77.         HandleMouseLook();
    78.         HandleInteraction();
    79.     }
    80.  
    81.     private void HandleSpaceshipControls()
    82.     {
    83.         if (!IsLocalPlayer)
    84.             return;
    85.  
    86.         if (spaceShip != null)
    87.         {
    88.             if (Input.GetKeyDown(KeyCode.Q)) spaceShip.SwitchCameras();
    89.             float horizontal = Input.GetAxis("Horizontal");
    90.             float vertical = Input.GetAxis("Vertical");
    91.             float verticalMovement = 0f;
    92.  
    93.             if (Input.GetKey(KeyCode.Space))
    94.             {
    95.                 verticalMovement = 1f;
    96.             }
    97.             else if (Input.GetKey(KeyCode.LeftControl))
    98.             {
    99.                 verticalMovement = -1f;
    100.             }
    101.  
    102.             Vector3 moveDirection = new Vector3(horizontal, 0f, vertical);
    103.             float mouseX = Input.GetAxis("Mouse X") * lookSpeed;
    104.             float mouseY = Input.GetAxis("Mouse Y") * lookSpeed;
    105.  
    106.             verticalLookRotation -= mouseY;
    107.             verticalLookRotation = Mathf.Clamp(verticalLookRotation, -90f, 90f);
    108.             GetComponentInChildren<Camera>().transform.localRotation = Quaternion.Euler(verticalLookRotation, 0f, 0f);
    109.  
    110.             spaceShip.MoveAndRotateShip(moveDirection, verticalMovement, mouseX);
    111.         }
    112.         else
    113.         {
    114.             Debug.Log("NO SHIP");
    115.         }
    116.     }
    117.  
    118.     private void OnTriggerEnter(Collider other)
    119.     {
    120.         if (!IsLocalPlayer)
    121.             return;
    122.         if (other.CompareTag("SpaceshipTrigger"))
    123.         {
    124.             spaceShip = other.GetComponentInParent<SpaceShip>();
    125.             if (spaceShip != null)
    126.             {
    127.                 spaceShip.AttachToShipServerRpc(this.NetworkObject);
    128.             }
    129.         }
    130.     }
    131.  
    132.     private void OnTriggerExit(Collider other)
    133.     {
    134.         if (!IsLocalPlayer)
    135.             return;
    136.  
    137.         if (other.CompareTag("SpaceshipTrigger"))
    138.         {
    139.             SpaceShip ship = other.GetComponentInParent<SpaceShip>();
    140.             if (ship != null)
    141.             {
    142.                 ship.DeattachToShipServerRpc(this.NetworkObject);
    143.             }
    144.         }
    145.     }
    146.  
    147.     private void HandleInteraction()
    148.     {
    149.         if (!IsLocalPlayer || textManager == null) return;
    150.  
    151.         float pickUpDistance = 4f;
    152.         RaycastHit hit;
    153.  
    154.         if (Physics.Raycast(playerCameraTransform.position, playerCameraTransform.forward, out hit, pickUpDistance, pickUpLayerMask))
    155.         {
    156.             HandleObjectInteraction(hit);
    157.         }
    158.         else if (Physics.Raycast(playerCameraTransform.position, playerCameraTransform.forward, out hit, pickUpDistance, spaceshipLayerMask))
    159.         {
    160.             HandleSpaceshipInteraction(hit);
    161.         }
    162.         else if (Physics.Raycast(playerCameraTransform.position, playerCameraTransform.forward, out hit, pickUpDistance, openHatchLayerMask))
    163.         {
    164.             HandleHatchInteraction(hit);
    165.         }
    166.         else
    167.         {
    168.             textManager.ClearToolTipText();
    169.         }
    170.     }
    171.  
    172.     private void HandleObjectInteraction(RaycastHit hit)
    173.     {
    174.         if (objectGrabbable == null)
    175.         {
    176.             if (hit.transform.TryGetComponent(out objectGrabbable))
    177.             {
    178.                 textManager.SetToolTipText("Click to pick up");
    179.                 if (Input.GetKeyDown(KeyCode.Mouse0))
    180.                 {
    181.                     objectGrabbable.GrabServerRpc(this.NetworkObject);
    182.                 }
    183.             }
    184.         }
    185.         else
    186.         {
    187.             objectGrabbable.DropServerRpc();
    188.             objectGrabbable = null;
    189.         }
    190.     }
    191.  
    192.     private void HandleSpaceshipInteraction(RaycastHit hit)
    193.     {
    194.         Rigidbody rb = GetComponent<Rigidbody>();
    195.  
    196.         if (!isPilotingSpaceship)
    197.         {
    198.             textManager.SetToolTipText("Click to pilot the ship");
    199.             if (Input.GetKeyDown(KeyCode.Mouse0))
    200.             {
    201.                 isPilotingSpaceship = true;
    202.                 rb.isKinematic = true;
    203.             }
    204.         }
    205.         else
    206.         {
    207.             textManager.SetToolTipText("Click to stop piloting the ship");
    208.             if (Input.GetKeyDown(KeyCode.Mouse0))
    209.             {
    210.                 isPilotingSpaceship = false;
    211.                 rb.isKinematic = false;
    212.                 rb.velocity = Vector3.zero;
    213.             }
    214.         }
    215.     }
    216.  
    217.     private void HandleHatchInteraction(RaycastHit hit)
    218.     {
    219.         if (spaceShip != null)
    220.         {
    221.             if (!spaceShip.isHatchOpen)
    222.             {
    223.                 textManager.SetToolTipText("Click to open Hatch");
    224.                 if (Input.GetKeyDown(KeyCode.Mouse0))
    225.                 {
    226.                     spaceShip.OpenHatch();
    227.                 }
    228.             }
    229.             else
    230.             {
    231.                 textManager.SetToolTipText("Click to close Hatch");
    232.                 if (Input.GetKeyDown(KeyCode.Mouse0))
    233.                 {
    234.                     spaceShip.CloseHatch();
    235.                 }
    236.             }
    237.         }
    238.     }
    239.  
    240.     private void HandleMouseLook()
    241.     {
    242.         if (!IsLocalPlayer || Cursor.lockState != CursorLockMode.Locked || Cursor.visible)
    243.             return;
    244.  
    245.         if (!isPilotingSpaceship)
    246.         {
    247.             float mouseX = Input.GetAxis("Mouse X") * lookSpeed;
    248.             float mouseY = Input.GetAxis("Mouse Y") * lookSpeed;
    249.  
    250.             verticalLookRotation -= mouseY;
    251.             verticalLookRotation = Mathf.Clamp(verticalLookRotation, -90f, 90f);
    252.  
    253.             transform.Rotate(Vector3.up * mouseX);
    254.             GetComponentInChildren<Camera>().transform.localRotation = Quaternion.Euler(verticalLookRotation, 0f, 0f);
    255.         }
    256.     }
    257.  
    258.     private void HandleMovement()
    259.     {
    260.         if (!IsLocalPlayer)
    261.             return;
    262.  
    263.         float horizontal = Input.GetAxis("Horizontal");
    264.         float vertical = Input.GetAxis("Vertical");
    265.  
    266.         Vector3 moveDir = transform.right * horizontal + transform.forward * vertical;
    267.         float moveDistance = moveSpeed * Time.deltaTime;
    268.  
    269.         transform.position += moveDir * moveDistance;
    270.         isWalking = moveDir != Vector3.zero;
    271.     }
    272.  
    273.     public bool IsWalking()
    274.     {
    275.         return isWalking;
    276.     }
    277. }
    278.  
    the spaceship script
    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Netcode;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5.  
    6. public class SpaceShip : NetworkBehaviour
    7. {
    8.     [SerializeField] private float speed = 10f;
    9.     [SerializeField] private float rotationSpeed = 100f;
    10.     [SerializeField] private float acceleration = 2f;
    11.     [SerializeField] private float deceleration = 2.5f;
    12.     [SerializeField] private float maxTiltAngleZ = 15f;
    13.     [SerializeField] private float maxTiltAngleX = 15f;
    14.     [SerializeField] private float tiltSpeed = 1f;
    15.     [SerializeField] private Transform hatch;
    16.     [SerializeField] public bool isOnPlanet;
    17.     [SerializeField] public bool isHatchOpen;
    18.     [SerializeField] private List<Camera> cameras;
    19.     private int currentCameraIndex = 0;
    20.  
    21.     private Vector3 currentVelocity;
    22.     private float currentRotationSpeed;
    23.  
    24.     // Interpolation variables
    25.     private Vector3 targetPosition;
    26.     private Quaternion targetRotation;
    27.     private float interpolationFactor = 0.1f; // Adjust as needed for smoothing
    28.  
    29.     private Coroutine hatchCoroutine;
    30.  
    31.     private void Start()
    32.     {
    33.         if (IsServer)
    34.         {
    35.             targetPosition = transform.position;
    36.             targetRotation = transform.rotation;
    37.         }
    38.     }
    39.  
    40.     public void MoveAndRotateShip(Vector3 direction, float verticalMovement, float mouseX)
    41.     {
    42.         if (!IsOwner)
    43.             return;
    44.  
    45.         UpdateRotationSpeed(mouseX);
    46.  
    47.         Vector3 desiredVelocity = CalculateDesiredVelocity(direction, verticalMovement);
    48.  
    49.         currentVelocity = Vector3.Lerp(currentVelocity, desiredVelocity, acceleration * Time.deltaTime);
    50.  
    51.         if (direction == Vector3.zero && verticalMovement == 0)
    52.         {
    53.             currentVelocity = Vector3.Lerp(currentVelocity, Vector3.zero, deceleration * Time.deltaTime);
    54.         }
    55.  
    56.         transform.position += currentVelocity * Time.deltaTime;
    57.  
    58.         if (!isOnPlanet)
    59.         {
    60.             RotateShip(direction, verticalMovement);
    61.         }
    62.  
    63.         // Update server with the new position and rotation
    64.         if (IsServer)
    65.         {
    66.             targetPosition = transform.position;
    67.             targetRotation = transform.rotation;
    68.             UpdateShipPositionServerRpc(transform.position, transform.rotation);
    69.         }
    70.     }
    71.  
    72.     private void UpdateRotationSpeed(float mouseX)
    73.     {
    74.         if (Mathf.Abs(mouseX) > 0.01f)
    75.         {
    76.             currentRotationSpeed = mouseX * Time.deltaTime * 10;
    77.         }
    78.     }
    79.  
    80.     public void SwitchCameras()
    81.     {
    82.         if (cameras == null || cameras.Count == 0)
    83.             return;
    84.  
    85.         // Deactivate the current camera
    86.         cameras[currentCameraIndex].gameObject.SetActive(false);
    87.  
    88.         // Move to the next camera index
    89.         currentCameraIndex = (currentCameraIndex + 1) % cameras.Count;
    90.  
    91.         // Activate the next camera
    92.         cameras[currentCameraIndex].gameObject.SetActive(true);
    93.     }
    94.  
    95.     private Vector3 CalculateDesiredVelocity(Vector3 direction, float verticalMovement)
    96.     {
    97.         Vector3 desiredVelocity = transform.forward * direction.z * speed;
    98.         desiredVelocity += transform.right * direction.x * speed;
    99.         desiredVelocity += transform.up * verticalMovement * speed;
    100.         return desiredVelocity;
    101.     }
    102.  
    103.     private void RotateShip(Vector3 direction, float verticalMovement)
    104.     {
    105.         float tiltAngleX = Mathf.Clamp(-verticalMovement * maxTiltAngleX, -maxTiltAngleX, 0);
    106.         float tiltAngleZ = -direction.x * maxTiltAngleZ;
    107.  
    108.         Quaternion targetRotation = Quaternion.Euler(tiltAngleX, transform.rotation.eulerAngles.y, tiltAngleZ);
    109.         transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, tiltSpeed * Time.deltaTime);
    110.  
    111.         transform.Rotate(Vector3.up * currentRotationSpeed);
    112.     }
    113.  
    114.     [ServerRpc]
    115.     private void UpdateShipPositionServerRpc(Vector3 position, Quaternion rotation)
    116.     {
    117.         targetPosition = position;
    118.         targetRotation = rotation;
    119.     }
    120.  
    121.     private void Update()
    122.     {
    123.         if (IsServer)
    124.             return;
    125.  
    126.         // Interpolate position and rotation for non-owning clients
    127.         transform.position = Vector3.Lerp(transform.position, targetPosition, interpolationFactor);
    128.         transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, interpolationFactor);
    129.     }
    130.  
    131.     public void OpenHatch()
    132.     {
    133.         isHatchOpen = true;
    134.         if (hatchCoroutine != null)
    135.         {
    136.             StopCoroutine(hatchCoroutine);
    137.         }
    138.         hatchCoroutine = StartCoroutine(RotateHatch(new Vector3(-25, 0, 0)));
    139.     }
    140.  
    141.     public void CloseHatch()
    142.     {
    143.         isHatchOpen = false;
    144.  
    145.         if (hatchCoroutine != null)
    146.         {
    147.             StopCoroutine(hatchCoroutine);
    148.         }
    149.         hatchCoroutine = StartCoroutine(RotateHatch(new Vector3(0, 0, 0)));
    150.         Debug.Log("close");
    151.     }
    152.  
    153.     private IEnumerator RotateHatch(Vector3 targetLocalEulerAngles)
    154.     {
    155.         Quaternion initialRotation = hatch.localRotation;
    156.         Quaternion targetRotationHatch = Quaternion.Euler(targetLocalEulerAngles);
    157.         float elapsedTime = 0f;
    158.         float duration = 1f / tiltSpeed; // Adjust the duration based on tiltSpeed
    159.  
    160.         while (elapsedTime < duration)
    161.         {
    162.             hatch.localRotation = Quaternion.Lerp(initialRotation, targetRotationHatch, elapsedTime / duration);
    163.             elapsedTime += Time.deltaTime;
    164.             yield return null;
    165.         }
    166.  
    167.         hatch.localRotation = targetRotationHatch;
    168.         hatchCoroutine = null;
    169.     }
    170.  
    171.     [ServerRpc(RequireOwnership = false)]
    172.     public void AttachToShipServerRpc(NetworkObjectReference target)
    173.     {
    174.         AttachToShipClientRpc(target);
    175.     }
    176.  
    177.     [ClientRpc]
    178.     public void AttachToShipClientRpc(NetworkObjectReference target)
    179.     {
    180.         if (target.TryGet(out NetworkObject player))
    181.         {
    182.             if (player.GetComponent<Player>() == true)
    183.             {
    184.                 Debug.Log("attach to ship " + player);
    185.                 player.gameObject.GetComponent<FollowTransformShip>().SetTargetTransform(this.transform);
    186.             }
    187.         }
    188.     }
    189.  
    190.     [ServerRpc(RequireOwnership = false)]
    191.     public void DeattachToShipServerRpc(NetworkObjectReference target)
    192.     {
    193.         DeattachToShipClientRpc(target);
    194.     }
    195.  
    196.     [ClientRpc]
    197.     public void DeattachToShipClientRpc(NetworkObjectReference target)
    198.     {
    199.         if (target.TryGet(out NetworkObject player))
    200.         {
    201.             if (player.gameObject.GetComponent<Player>() == true)
    202.             {
    203.                 Debug.Log("deattached from ship " + player);
    204.                 player.gameObject.GetComponent<FollowTransformShip>().SetTargetTransform(null);
    205.             }
    206.         }
    207.     }
    208. }
    209.  
    Untitledvideo-MadewithClipchamp-ezgif.com-video-to-gif-converter.gif



    View attachment 1418421

    View attachment 1418424
     

    Attached Files:

  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,922
    Are you using dynamic RigidBody? This will incur lag because the physics simulation happens on the host side.

    Networked physics for gameplay is something you must not even attempt as a newcomer to multiplayer. You may have noticed that practically no online multiplayer game uses actual physics for gameplay, it's only used for visual fx.

    In this case you could use the good old trick: don't move the spaceship at all. Move everything else in relation to the spaceship. For instance, rather than moving the spaceship forward it stays in place, and the planets in front of you are moved towards the spaceship in the opposite direction the spaceship is moving. This provides the same movement effect even though your ship remains stationary. ;)
     
  3. unity_A8EA531F8FAD850AE5C6

    unity_A8EA531F8FAD850AE5C6

    Joined:
    Dec 13, 2023
    Posts:
    2
    The challenge with moving the world instead of the ship is that it could lead to complications when some players are on planets and others are on the spaceship. Despite these potential issues, could this approach still be viable?
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,922
    You can (and should) separate players on each planet and in space from each other. You can't expect to have a seamless transition between planetary surface and space in a networked game due to the scale of planets and space, unless we're talking Super Mario Galaxy style "planets".

    Consider that you have about 5,000 to 15,000 units in any direction to work with before you have to recenter everything and everyone inside that bubble. Otherwise weird positioning / rounding errors occurs which also trickle through to shaders and animations, each at their own distances. 5k units distance is the safe bet where no issues will be apparent.

    See Minecraft long travel videos to see what these rounding issues look like.