Search Unity

Resolved How to guarantee the initial NetworkVariable Value on a Client during OnNetworkSpawn()?

Discussion in 'Netcode for GameObjects' started by LaneFox, May 1, 2023.

  1. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,514
    The documentation is rather confusing on NetworkVariable as it doesn't really explain how to actually initialize them properly. What is the correct pattern to guarantee that a client will spawn the object and read the correct state of a NetworkVariable during OnNetworkSpawn()?

    • Using
      Awake()
      throws warnings, so the docs are probably just outdated there.
    • Using
      OnNetworkSpawn()
      throws warnings, so that doesn't seem right.
    • Setting before running
      Spawn()
      throws warnings...
    • Setting after running
      Spawn()
      throws warnings...
    • Setting them in
      OnSyncronize()
      doesn't even set them, since it's used for something else entirely.

    Erm.. When are we supposed to set the initial values of NetworkVariables... ?

    The only way I see that doesn't throw or fail is to just create the object on the network and sit on your hands until everyone is ready, then set the value and wait for it to replicate. If you need to do some init with that value then you need to use OnChanged callbacks to trigger something.

    If that's the case - that seems really bad. This would imply that the initial state is basically guaranteed incorrect, which is some nonsense so I'm assuming I just don't understand it correctly.
     
    Last edited: May 2, 2023
    CodeSmile likes this.
  2. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    660
    It was the case if you set network variable values straight after spawning (in the same frame, perhaps longer) then the network object would spawn on the client with those values. For example this would work:
    Code (CSharp):
    1.         public Stack CreateStack(Side side, Vector2Int position)
    2.         {
    3.             Stack stack = SpawnService.Instance().SpawnPrefab<Stack>(SpawnEntityType.Game, false);
    4.             stack.IsControlled = false;
    5.             stack.Side = side;
    6.             stack.Units = 0;
    7.             stack.StackState = StackState.Waiting;
    8.             stack.MovePoints = GameConstants.TIME_ONE_DAY;
    9.             stack.Position = position;
    10.             stack.transform.position = new Vector3(position.x + 0.5f, position.y + 0.5f);
    11.  
    12.             return stack;
    13.         }
    I'd have to re-test though to be sure it still works that way, but this is from an older project and it's now broken in Netcode 1.4.0 due to some change in CheckObjectVisibility I'm trying to get my head around.

    If you're having no luck I can try something similar in a test project.
     
  3. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,514
    It definitely does not seem to work on 1.3.1.

    Code (csharp):
    1.         public void ServerSpawnSomeObject(GameObject prefab, int id, int otherValue)
    2.         {
    3.             GameObject go = Instantiate(prefab);
    4.             StateObject behavior = go.GetComponent<StateObject>();
    5.             behavior.InitializeVars(id, otherValue);
    6.             behavior.NetworkObject.Spawn();
    7.  
    8. // heres the flow that actually happens...
    9.             // ServerSpawnSomeObject()
    10.             //
    11.             // StateObject.Awake()
    12.             // StateObject.InitializeVars()
    13.             // - NetworkVariable warning about not ready
    14.             // StateObject.OnNetworkSpawn()
    15.             // - NetVars are still default values at this point
    16.             //
    17.             // Client eventually gets the object, and values are default of course.
    18.         }
    No matter where I try to initialize the values it doesn't seem to work. There has to be a way to set the value of a NetworkVariable before shipping off the object. It makes no sense to just ship off network objects assuming that the default state of every NetworkVariable is considered correct, but the API does not offer any actual path to do this. Even setting the variables to dirty doesn't work because they're initialized and overwritten after you do that.
     
  4. fernando-a-cortez

    fernando-a-cortez

    Unity Technologies

    Joined:
    Dec 14, 2020
    Posts:
    11
    Hi,

    If you want to read the NetworkVariable's value on OnNetworkSpawn(), then you may modify the NetworkVariable's value on the server before you invoke NetworkObject.Spawn().

    The following warning is inconsequential: NetworkVariable is written to, but doesn't know its NetworkBehaviour yet. Are you modifying a NetworkVariable before the NetworkObject is spawned?
    If you read the NetworkVariable on OnNetworkSpawn() on clients it will be the server's set value pre-spawn.

    However, yes, the recommended pattern is to subscribe to a NetworkVariable's OnValueChanged callback, and define your desired behaviour when that callback is fired on clients.

    I hope that helps!
     
    CodeSmile likes this.
  5. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,514
    I am not seeing this behavior. Values set before
    Spawn()
    on the server are still
    default
    on Clients during
    OnNetworkSpawn()
    .

    Did this change since 1.3.1?
     
  6. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    660
    I just tested this in 1.2.0 and 1.4.0 and it worked with both. It's the only way I could get it to spawn on the client with the correct value. I'll have to go over my old project code to see why I thought differently.
     
  7. RikuTheFuffs-U

    RikuTheFuffs-U

    Unity Technologies

    Joined:
    Feb 20, 2020
    Posts:
    440
    Pretty much like this:

    Code (CSharp):
    1. public class ColorManager : NetworkBehaviour
    2.     {
    3.         NetworkVariable<Color32> m_NetworkedColor = new NetworkVariable<Color32>();
    4.         Material m_Material;
    5.         void Awake()
    6.         {
    7.             m_Material = GetComponent<Renderer>().material;
    8.         }
    9.         public override void OnNetworkSpawn()
    10.         {
    11.             base.OnNetworkSpawn();
    12.             if (IsClient)
    13.             {
    14.                     /* in this case, you need to manually load the initial Color to catch up with the state of the network variable.
    15.                      * This is particularly useful when re-connecting or hot-joining a session
    16.                     */
    17.                     OnClientColorChanged(m_Material.color, m_NetworkedColor.Value);
    18.                     m_NetworkedColor.OnValueChanged += OnClientColorChanged;
    19.            
    20.             }
    21.         }
    22.         public override void OnNetworkDespawn()
    23.         {
    24.             base.OnNetworkDespawn();
    25.             if (IsClient)
    26.             {
    27.                     m_NetworkedColor.OnValueChanged -= OnClientColorChanged;
    28.             }
    29.         }
    30.  
    31.         void OnClientColorChanged(Color32 previousColor, Color32 newColor)
    32.         {
    33.             m_Material.color = newColor;
    34.         }
    35. }
     
    LaneFox likes this.
  8. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,514
    Thanks for the response! This is exactly the pattern we ended up using. We implemented it as needed and bumped up to 1.4. It has been working well so far.

    It would be helpful if the documentation could verify what the system guarantees in terms of data state at critical times, such as when
    OnNetworkSpawn()
    occurs. Took me a while to try things until I could make safe assumptions.
     
    DoN2kcz and RikuTheFuffs like this.
  9. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,882
    Just to add to this, and perhaps a note for future improvement, because I've been using the SpawnManager.InstantiateAndSpawn helper method which is really convenient ... except it doesn't allow hooking into the process between Instantiate and Spawn.

    Update: posted a feature request on GitHub.

    I had to change my code from this:
    Code (CSharp):
    1. var spawnedObject = spawnManager.InstantiateAndSpawn(playerPrefab, ownerClientId,
    2. position: spawnTransform.position, rotation: Quaternion.Euler(0f, spawnAngle, 0f));
    3.  
    4. // assume "LocalPlayerIndex" NetworkVariable change attempts here
    To this:
    Code (CSharp):
    1. var spawnedObject = Instantiate(playerPrefab);
    2. spawnedObject.transform.position = spawnTransform.position;
    3. spawnedObject.transform.rotation = Quaternion.Euler(0f, spawnAngle, 0f);
    4.  
    5. var indexer = spawnedObject.GetComponent<NetworkLocalPlayerIndex>();
    6. indexer.LocalPlayerIndex = localPlayerIndex; // <== NetworkVariable
    7.  
    8. spawnedObject.SpawnWithOwnership(ownerClientId);
     
    Last edited: Feb 27, 2024
    Nyphur and LaneFox like this.