Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Bug Network Variable not replicating if set right after player spawn.

Discussion in 'Netcode for GameObjects' started by hoesterey, Feb 9, 2022.

  1. hoesterey

    hoesterey

    Joined:
    Mar 19, 2010
    Posts:
    659
    I am trying to set a player name variable to a network variable right after spawn and discovering its not replicating.

    As a test after setting the name on the server I then immediatly log the instance ID of the game object. I then on the server log the instance ID of the player object every frame (to prove it has not changed and I'm setting on the correct object) Then every frame I set the player name variable to the instance ID.
    upload_2022-2-8_16-49-32.png

    On the client I'm logging everytime the name network variable has changed. (in an on changed event) As you see it is NEVER set to Client Desktop-A6JVT86.
    upload_2022-2-8_16-51-28.png

    I do think this is a race condition that occors if you change a variable right after the object is spawned. I can start the client 20 times in a row and occasionally the network variable takes and is replicated properly. It is ALWAYS set on the server.

    Also the same code does successfully replicate if I slam it in a coroutine and delay setting the variable for a single frame after the player object is spawned.

    Thoughts?
     
    Last edited: Feb 9, 2022
  2. hoesterey

    hoesterey

    Joined:
    Mar 19, 2010
    Posts:
    659
    Bossroom has the same issue.

    I traced the code I think this is a race condition. I verified the object is spawned prior to the name being set. However it does not always replicate.
     
    Last edited: Feb 9, 2022
  3. hoesterey

    hoesterey

    Joined:
    Mar 19, 2010
    Posts:
    659
    Looked into this more. It behaves differently on different computers. One one slow computer I have to wait for a full second before setting the player name else it will not replicate.

    Everything else replicates AOK. Just the player name fails.

    The following "fixes" the issue. Notice I get the player object once right at the start and then set the variable multiple times. My guess is it seams to not like setting that variable while the client is loading a scene.


    Code (CSharp):
    1. IEnumerator PlayerNameSet(ulong clientId, string playerName)
    2.         {
    3.             //todo remove this hack.  Getting around some kind of odd race condition.
    4.             var networkObject = NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(clientId);
    5.             // update client's name
    6.             if (networkObject.TryGetComponent(out PersistentPlayer persistentPlayer))
    7.             {
    8.                 for (int i = 0; i < 3; i++)
    9.                 {
    10.  
    11.                     yield return 0;
    12.  
    13.                     persistentPlayer.NetworkNameState.Name.Value = playerName;
    14.                     WaitForSeconds wait = new WaitForSeconds(.5f);
    15.                     yield return wait;
    16.                 }
    17.                 //take control of spawned player object based on connection order.  
    18.                 //in "official" game host can assign re-control on reconnect if it can't match player name                  //to the object..
    19.                 if (!Core.IsOfficialGame)
    20.                 {
    21.                     NonOfficialGame_StealActorOwnership(persistentPlayer);
    22.                 }
    23.             }
    24.         }
     
    Last edited: Feb 10, 2022
  4. cosminunity

    cosminunity

    Joined:
    Mar 4, 2021
    Posts:
    14
    Hi @hoesterey,

    In order to best assist you with this, would you be able to share some more code snippets of code? We are interested in the (full) execution flow. From the original point where you are setting the variable (you said after spawn but where exactly in code) that you want to replicate all the way to the client.

    Greetings,
    Cosmin
     
  5. ThompZon

    ThompZon

    Joined:
    Sep 12, 2018
    Posts:
    7
    Are you using a `NetworkVariable` and detect changes using the events?
    It doesn't fire an event when the value is initially sync:ed and imo that's a critical bug, but it seems to be known.
    (I found it half way down this documentation page, only acknowledgement of this issue).

    I start a coroutine that checks if the value is non-default value and run the event function when it's non-default or else wait a little longer. My variable is `ulong` so I initialize it to `ulong.MaxValue` to indicate it's currently "unset".

    It's not pretty, but it "solves" the issue with the NetworkVariable events not firing on initial sync.
     
  6. hoesterey

    hoesterey

    Joined:
    Mar 19, 2010
    Posts:
    659
    Sure thing. As mentioned you have the same problem in the boss room demo.

    //Here is where I assign the player name.
    Code (CSharp):
    1. static void AssignPlayerName(ulong clientId, string playerName)
    2.         {
    3.             Debug.Assert(playerName != string.Empty, "player name is empty");
    4.             // get this client's player NetworkObject
    5.             var networkObject = NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(clientId);
    6.             // update client's name
    7.             if (networkObject.TryGetComponent(out PersistentPlayer persistentPlayer))
    8.             {
    9.                 persistentPlayer.NetworkNameState.Name.Value = playerName;
    10.             }
    11.             Debug.Assert(persistentPlayer, "could not find persistent player");
    12.             ServerConnectionManager.Instance.DelayPlayerNameSet(clientId, playerName);
    13.         }
    //My hack to get around the race conditiong. (just call this without the yield and it will happen everytime)

    Code (CSharp):
    1.  public void DelayPlayerNameSet(ulong clientID, string playerName)
    2.         {
    3.             StartCoroutine(PlayerNameSet(clientID, playerName));
    4.         }
    5.  
    6.  
    7.         IEnumerator PlayerNameSet(ulong clientId, string playerName)
    8.         {
    9.             //todo remove this hack.  Getting around some kind of odd race condition.
    10.             var networkObject = NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(clientId);
    11.             // update client's name
    12.             if (networkObject.TryGetComponent(out PersistentPlayer persistentPlayer))
    13.             {
    14.                 for (int i = 0; i < 3; i++)
    15.                 {
    16.  
    17.                     yield return 0;
    18.  
    19.                     persistentPlayer.NetworkNameState.Name.Value = playerName;
    20.                     WaitForSeconds wait = new WaitForSeconds(.5f);
    21.                     yield return wait;
    22.                 }
    23.                 if (!Core.IsOfficialGame)
    24.                 {
    25.                     NonOfficialGame_StealActorOwnership(persistentPlayer);
    26.                 }
    27.             }
    28.         }
    //assignplayername is called from Approval check (just like boss room)
    Code (CSharp):
    1. private void ApprovalCheck(byte[] connectionData, ulong clientId, NetworkManager.ConnectionApprovedDelegate connectionApprovedCallback)
    2.         {
    3.             Debug.Log("Client Connected Approval Check!");
    4.             if (connectionData.Length > k_MaxConnectPayload)
    5.             {
    6.                 connectionApprovedCallback(false, 0, false, null, null);
    7.                 return;
    8.             }
    9.  
    10.             // Approval check happens for Host too, but obviously we want it to be approved
    11.             if (clientId == NetworkManager.Singleton.LocalClientId)
    12.             {
    13.                 connectionApprovedCallback(true, null, true, null, null);
    14.                 return;
    15.             }
    16.  
    17.             ConnectStatus gameReturnStatus = ConnectStatus.Success;
    18.  
    19.             // Test for over-capacity connection. This needs to be done asap, to make sure we refuse connections asap and don't spend useless time server side
    20.             // on invalid users trying to connect
    21.             // todo this is currently still spending too much time server side.
    22.             if (m_ClientData.Count >= MAXPlayersInLobby)
    23.             {
    24.                 gameReturnStatus = ConnectStatus.ServerFull;
    25.                 //TODO-FIXME:Netcode Issue #796. We should be able to send a reason and disconnect without a coroutine delay.
    26.                 //TODO:Netcode: In the future we expect Netcode to allow us to return more information as part of
    27.                 //the approval callback, so that we can provide more context on a reject. In the meantime we must provide the extra information ourselves,
    28.                 //and then manually close down the connection.
    29.                 SendServerToClientConnectResult(clientId, gameReturnStatus);
    30.                 SendServerToClientSetDisconnectReason(clientId, gameReturnStatus);
    31.                 StartCoroutine(WaitToDisconnect(clientId));
    32.                 return;
    33.             }
    34.  
    35.             string payload = System.Text.Encoding.UTF8.GetString(connectionData);
    36.             var connectionPayload = JsonUtility.FromJson<ConnectionPayload>(payload); // https://docs.unity3d.com/2020.2/Documentation/Manual/JSONSerialization.html
    37.  
    38.             int clientScene = connectionPayload.clientScene;
    39.  
    40.             Debug.Log("Host ApprovalCheck: connecting client GUID: " + connectionPayload.clientGUID);
    41.  
    42.             //Test for Duplicate Login.
    43.             if (m_ClientData.ContainsKey(connectionPayload.clientGUID))
    44.             {
    45.                 if (Debug.isDebugBuild)
    46.                 {
    47.                     Debug.Log($"Client GUID {connectionPayload.clientGUID} already exists. Because this is a debug build, we will still accept the connection");
    48.                     while (m_ClientData.ContainsKey(connectionPayload.clientGUID)) { connectionPayload.clientGUID += "_Secondary"; }
    49.                 }
    50.                 else
    51.                 {
    52.                     ulong oldClientId = m_ClientData[connectionPayload.clientGUID].m_ClientID;
    53.                     // kicking old client to leave only current
    54.                     SendServerToClientSetDisconnectReason(oldClientId, ConnectStatus.LoggedInAgain);
    55.                     StartCoroutine(WaitToDisconnect(clientId));
    56.                     return;
    57.                 }
    58.             }
    59.  
    60.             SendServerToClientConnectResult(clientId, gameReturnStatus);
    61.  
    62.             //Populate our dictionaries with the playerData
    63.             m_ClientSceneMap[clientId] = clientScene;
    64.             m_ClientIDToGuid[clientId] = connectionPayload.clientGUID;
    65.             m_ClientData[connectionPayload.clientGUID] = new PlayerData(connectionPayload.playerName, clientId);
    66.  
    67.             connectionApprovedCallback(true, null, true, Vector3.zero, Quaternion.identity);
    68.  
    69.             // connection approval will create a player object for you
    70.             AssignPlayerName(clientId, connectionPayload.playerName);
    71.         }
    //approval check is called from the callback
    Code (CSharp):
    1. void Start()
    2.         {
    3.             m_Portal = GetComponent<ConnectionManager>();
    4.  
    5.             // we add ApprovalCheck callback BEFORE OnNetworkSpawn to avoid spurious Netcode for GameObjects (Netcode)
    6.             // warning: "No ConnectionApproval callback defined. Connection approval will timeout"
    7.             m_Portal.NetManager.ConnectionApprovalCallback += ApprovalCheck;
    8.             m_Portal.NetManager.OnServerStarted += ServerStartedHandler;
    9.             m_ClientData = new Dictionary<string, PlayerData>();
    10.             m_ClientIDToGuid = new Dictionary<ulong, string>();
    11.         }
     
  7. cosminunity

    cosminunity

    Joined:
    Mar 4, 2021
    Posts:
    14
    Hi,

    Thanks for these code snippets. May I suggest you create a bug report through Unity Bug Reporter? That way we will be able to look further into these.

    Greetings,
    Cosmin
     
  8. hoesterey

    hoesterey

    Joined:
    Mar 19, 2010
    Posts:
    659
    Case 1410757
     
    cosminunity likes this.
  9. AaronKB

    AaronKB

    Joined:
    Aug 16, 2019
    Posts:
    27
    Any progress on this? It's very annoying.
     
  10. Horst6667

    Horst6667

    Joined:
    Feb 5, 2018
    Posts:
    17
    I just tested with 1.0.0-pre9 and the issue is still present there.
     
  11. hoesterey

    hoesterey

    Joined:
    Mar 19, 2010
    Posts:
    659
    Unity did not investigate the issue I opened for weeks due to them being unable to get my project working in multiplayer, despite me literally calling this out as an issue you could repro in boss room. This process for submitting bugs and having them be taken seriously is so unbelievably unacceptable.
     
    sampenguin likes this.
  12. AaronKB

    AaronKB

    Joined:
    Aug 16, 2019
    Posts:
    27
    I think I am having the same issue again in a different place. I have a NetworkList<CustomClass> and a NetworkVariable<double> and the double is synced OK (OnChange events on the standalone client get called) but the network list doesn't get syned.

    It is remaining empty therefore causes exceptions later on.

    Other NetworkList objects I have work fine. I cannot find any meaningful difference. Initially I thought it might be because the NetworkBehaviours were on a child GO of a NetworkObject but when I moved them to the parent the problem was still there.

    There's something really fishy going on here or at the very least Unity needs to tell us how to work around it or what we are doing.

    @hoesterey Where do I look up the case. When I went to the issue tracker here and look up that case # it wasn't there.(https://issuetracker.unity3d.com/)