Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Join us on Thursday, June 8, for a Q&A with Unity's Content Pipeline group here on the forum, and on the Unity Discord, and discuss topics around Content Build, Import Workflows, Asset Database, and Addressables!
    Dismiss Notice

Resolved Spawning as PlayerObject with ClientNetworkTransform

Discussion in 'Netcode for GameObjects' started by afavar, Dec 8, 2022.

  1. afavar

    afavar

    Joined:
    Jul 17, 2013
    Posts:
    50
    I have a player prefab which has a ClientNetworkTransform component. The server instantiates the player and spawns it as PlayerObject.

    Code (CSharp):
    1. var player = GameManager.Instance.steamPlayers[i];
    2. var startPosition = spawnPositions[i].position;
    3. var startRotation = spawnPositions[i].rotation;
    4. var go = Instantiate(playerPrefab, startPosition, startRotation);
    5. go.GetComponent<NetworkObject>().SpawnAsPlayerObject(player.clientId);
    In the server(host), the player is instantiated at the correct position. However, when it is spawned as the PlayerObject, it teleports to the default prefab transform. I think the initial position from the server doesn't get synced with the client and since the client has the authority over sending transform position, my initial position set from the server is overridden.

    Also I find this issue on github over a year ago and it seems to be the same problem. There is a comment which I don't think it is the case now:

    "ClientNetworkTransform is not part of the framework and we do not actively support it."

    ClientNetworkTransform just inherits from the NetworkTransform and overrides the authority. This is a super crucial feature if you think about it. I need to be able to spawn a player (with client authority over transform) at a given location. This is just a basic spawning mechanic where it works as expected on other networking libraries I have tried so far.
     
    Last edited: Dec 8, 2022
    Mj-Kkaya and mberketatar like this.
  2. RikuTheFuffs-U

    RikuTheFuffs-U

    Unity Technologies

    Joined:
    Feb 20, 2020
    Posts:
    363
    Hi @afavar , you can say where the player will spawn using 2 things:

    1. A ClientRPC (after the player has spawned)
    2. A NetworkVariable that you would set before the player spawns

    As a user I agree with you that the expected behaviour would be that the player spawns where the server said it would.

    About this, ClientNetworkTransform is now an utility class listed in the documentation, but it's still not part of Netcode For GameObjects as client-authoritative movement is (in general) an unsafe practice that exposes your game to some types of cheats (I.E: movement hacks)
     
    Mj-Kkaya likes this.
  3. afavar

    afavar

    Joined:
    Jul 17, 2013
    Posts:
    50
    Hi @RikuTheFuffs-U , thank you for your answer. I have thought several other ideas as u have mentioned but I have couple questions in mind to be sure I am doing it right.

    1. For this method to work reliably, where and when should I call the RPC? If I call a ClientRPC from the server spawned object directly, the object might not be spawned on the client yet so the same teleportation problem will occur since the transform will be overriden when the object is spawned by the owner client. I am thinking about calling a ServerRPC in OnNetworkSpawn method but only if we are the owner client. Then, the server can call a ClientRPC to send the spawn position. (I believe there is no way to run a ClientRPC for a spesific client for the time being so it will be called on all clients.) Am I on the right path or is it a wrong assumption?

    2. Again where and when should I do this? Actually, I am using this for changing character models. The server is setting an int value before spawning the player(before calling the Spawn() method of the network object) which I then use to change character model when the object is spawned. This works but when the object is spawned on a client, a warning messages occurs saying that the network variable was written before the spawn. Again, this works but it doesn't seem right when I look at the warning. Also, in the documents it is saying:

    "you should only set the value of a NetworkVariable when first initializing it or if it is spawned. It is not recommended setting a NetworkVariable when the associated NetworkObject is not spawned."

    If I set it inside the Awake method, will the updated value be accesible in the OnNetworkSpawn method on the other clients when the object is spawned at their side as well? Will I have to subscribe to on change callback for the clients? Will the behaviour change time to time so I should check both?

    Btw, I understand that the client-authoritative movement is an unsafe practice. Developing a server authoritative competetive game is an extremely diffucult task to do it right for indie devs and studios IMO. However, this can be negligible for certain types of games that aren't focusing on the competetive part. Coop games (Raft, Valheim etc.) can be a perfect example for this. They are definitely one of the most popular genre in the gaming market. Since Unity is the most popular game engine among indie devs/studios, many devs will be willing to develop such games and ending up using Netcode. To be honest, I believe many developers will be using Netcode to develop client-authoritative games maybe more than the server-authoritative approach. Because it is a lot more simple to implement and it just works great for coop games that aren't competetive. However, client-authoritative tools are seem to be getting treated like a stepchild for the time being. At least, this issue should be stated in the docs. It is not an edge case, it is just spawning a player with client-authoritive movement at a certain location.
     
    Last edited: Dec 9, 2022
    Mj-Kkaya likes this.
  4. RikuTheFuffs-U

    RikuTheFuffs-U

    Unity Technologies

    Joined:
    Feb 20, 2020
    Posts:
    363
    Hey @afavar I investigated this a bit more and got a comprehensive answer from one of our netcode engineers, Noel. It refers to NGO 1.2.0, but the concepts very likely apply to previosus versions as well:

    "For both owner (i.e. ClientNetworkTransform) and server authoritative (i.e. NetworkTransform) setting the position of a player can be done in NetworkBehaviour.OnNetworkSpawn.

    Code (CSharp):
    1. Below is a script usage pattern example:
    2.    /// <summary>
    3.    /// Place this on the NetworkManager
    4.    /// </summary>
    5.    public class SpawnPointManager : MonoBehaviour
    6.    {
    7.        public List<GameObject> SpawnPoints;  // Populate with spawn positions in-editor
    8.  
    9.        private NetworkManager m_NetworkManager;
    10.  
    11.        private void Awake()
    12.        {
    13.            m_NetworkManager = GetComponent<NetworkManager>();
    14.        }
    15.  
    16.        public Vector3 GetRandomSpawnPoint()
    17.        {
    18.            if (m_NetworkManager.IsServer && SpawnPoints != null && SpawnPoints.Count > 0)
    19.            {
    20.                return SpawnPoints[(int)Random.Range(0, SpawnPoints.Count - 1)].transform.position;
    21.            }
    22.            return Vector3.zero;
    23.        }
    24.    }
    25.  
    26.    /// <summary>
    27.    /// Place this on the Player
    28.    /// </summary>
    29.    public class MyPlayerObject : NetworkBehaviour
    30.    {
    31.        public override void OnNetworkSpawn()
    32.        {
    33.            if (IsServer)
    34.            {
    35.                var spawnPointManager = NetworkManager.gameObject.GetComponent<SpawnPointManager>();
    36.                if (spawnPointManager != null)
    37.                {
    38.                    // Set transform value here
    39.                    transform.position = spawnPointManager.GetRandomSpawnPoint();
    40.                }
    41.            }
    42.            base.OnNetworkSpawn();
    43.        }
    44.    }
    So far, this has been pretty universal for almost all scenarios (i.e. using a RigidBody approach with NetworkRigidBody).

    However, the CharacterController component seems to be a bit unique. A user can still use the usage pattern above (i.e. MyPlayerObject and SpawnPointManager) but should also follow the below usage pattern when they are using ClientNetworkTransform:

    Code (CSharp):
    1.  
    2.    public class NetcodeCharacterController : NetworkBehaviour
    3.    {
    4.        private CharacterController m_CharacterController;
    5.  
    6.        private void Awake()
    7.        {
    8.            m_CharacterController = GetComponent<CharacterController>();
    9.            if (m_CharacterController != null)
    10.            {
    11.                // Start every netcode cloned instance with CharacterController disabled
    12.                m_CharacterController.enabled = false;
    13.            }
    14.        }
    15.  
    16.        public override void OnNetworkSpawn()
    17.        {
    18.            // Owner enables when spawned
    19.            if (IsOwner && m_CharacterController != null)
    20.            {
    21.                m_CharacterController.enabled = true;
    22.            }
    23.  
    24.            base.OnNetworkSpawn();
    25.        }
    26.    }
    The idea is that everyone starts with the CharacterController disabled and only the owner of the NetworkObject should enable the CharacterController within OnNetworkSpawn.

    The exact specifics as to why this needs to occur, so far, I believe has to do with how CharacterController handles physics (motion and collision).

    Haven't had a chance to look over the source code for CharacterController to determine the exact reason behind this...but after debugging the issue a bit the above is what I would recommend if a user wants to have the server apply a spawn point to a player object that uses ClientNetworkTransform and CharacterController. "

    On thing to keep in mind is that, having any kind of player-input-related component enabled by default on prefabs is a mistake in a multiplayer environment. You should always enable such components for the local player, after they spawn.

    Does this help?
     
  5. afavar

    afavar

    Joined:
    Jul 17, 2013
    Posts:
    50
    Hi @RikuTheFuffs-U , thanks a lot for your comprehensive answer and kudos to Noel for the clarification.

    I have ended up with something similar before I have seen your answer. I was about to make several tests and share my results.

    I have used a NetworkVariable<Vector3> to store the spawn position that is set by the server in the OnNetworkSpawn method. I verified that client was getting the right position data when the player object is spawned on the client side as well. However, setting the transform.position of the player was not working so I got suspicious about some other stuff was causing a problem. It was because of the CharacterController just like you have mentioned. I did something similar to what you said and only enabled CharacterController after setting the transform.position on the local client, which worked out well!

    Based on your answer, I don't have to use a NetworkVariable<Vector3> as server can just set the transform.position for the clients. I will remove the NetworkVariable to simplyfy the logic. That is good to know.

    Since many people, like me, might use the character controller from the Unity starter assets, it might be a good idea to give a hint about this in the docs as well.
     
    Mj-Kkaya likes this.
  6. RikuTheFuffs-U

    RikuTheFuffs-U

    Unity Technologies

    Joined:
    Feb 20, 2020
    Posts:
    363
    Mj-Kkaya likes this.
  7. afavar

    afavar

    Joined:
    Jul 17, 2013
    Posts:
    50
    Hey @RikuTheFuffs-U , thanks for the extra info. Quick questions though:

    1. In the ClientDriven Sample, server calls a ClientRpc when the object is spawned. I believe the object will be spawned immediately on the Server and it will be spawned on a client with a network latency. So, if the server calls a ClientRpc and the object hasn't spawned on the client yet, what will happen? Will the client still receive the RPC after the object is spawned on it's local machine?

    2. Also, in the example you have provided above, it seems possible that the server can set the transform position in OnNetworkSpawn. So, a ClientRpc is not needed if this approach is used. However, the docs says "ClientNetworkTransforms can be updated by owners only, which means the server can't update the player's position directly."

    It is nice to have we have options to handle spawn points for the ClientNetworkTransforms. But, IMO these are all nontrivial solutions. Ideally, it should be simple as server instantiating and spawning the object with an initial location for the client owned object.
     
    Mj-Kkaya and ledbetterMRI like this.
  8. RikuTheFuffs-U

    RikuTheFuffs-U

    Unity Technologies

    Joined:
    Feb 20, 2020
    Posts:
    363
    Hey @afavar , I asked this to the ClientDriven team and they got back with this answer:
    1. RPC and spawn are sent in the same frame, so should be sent at the same time. Order of operations should be deterministic. So order should be: client connects. host approves connection and spawn player prefab. spawn is called server side. client RPC with position is sent. lag. client receives spawn message. in the same frame, instantiates player and calls OnNetworkSpawn. Client RPC is triggered which sets the position.
    2. Spawning in ClientDriven was written pre-NGO 1.0 release, things might have changed since...
     
    Mj-Kkaya, ledbetterMRI and afavar like this.
  9. afavar

    afavar

    Joined:
    Jul 17, 2013
    Posts:
    50
    Huge thanks for you and the team as well for clarifying this.
     
  10. Mj-Kkaya

    Mj-Kkaya

    Joined:
    Oct 10, 2017
    Posts:
    38
    Hi folks.
    I really want to say thank both of you!.
    This conversation is real so much helped me!
    I'm using "ClientNetworkTransform" , "Unity starter asset FPS" and "SpawnAsPlayerObject" method :)
    This is exactly what I was looking for!
    Best regards.
     
    RikuTheFuffs-U likes this.