Network Object ID inside a NetworkList is different across clients

Edit:

Hey everyone,
I’ve come across an issue when trying to sync a list of NetworkObjectReference for late-joining clients with Distributed Authory.

For context, each player spawns a number of “ShooterBase” network objects from OnNetworkSpawn.

What I am trying to do is save those spawned network objects to a NetworkList using NetworkObjectReference so that new clients would be able to read them via TryGet and initialize their local List by using a simple GetComponent(). This local list is used for handling the syncing of the weapon visual by disabling and enabling the ShooterBase component.

The problem is, it works for the session owner (both the session owner and client’s List are populated), but not for the client that just joined (the client’s List is populated but not the session owner’s).

So I did a little debugging and found out that turns out the Network Object IDs are different between the session owner and the client.

Session Owner Log:


Client Log

Is this natural? If so, what’s a better way to synchronize a list of network objects for late joining player?
Thanks!

Here is the code for spawning the weapons:
PlayerWeaponHandler.cs

public NetworkList<NetworkObjectReference> weaponNetworkObjects = 
        new NetworkList<NetworkObjectReference>(null,
            NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);

    private List<ShooterBase> shooters = new List<ShooterBase>(4);


    public override void OnNetworkSpawn()
    {
        base.OnNetworkSpawn();
        currentReadiedIndex.OnValueChanged += OnReadiedIndexChanged;

        for(int i = 0; i < weaponsToSpawn.Count; i++)
        {
            if (IsOwner)
            {
                //Instantiate weapon object
                GameObject spawnedWeapon = Instantiate(weaponsToSpawn[i], gripAnchor.position, gripAnchor.rotation, gripAnchor);

                //Add to owner local List<ShooterBase>
                ShooterBase spawnedShooter = spawnedWeapon.GetComponent<ShooterBase>();
                shooters.Add(spawnedShooter);

                //Network spawn weapon object
                NetworkObject spawnedNetworkObject = spawnedShooter.NetworkObject;
                spawnedNetworkObject.Spawn();

                //Adds the spawned network object to NetworkList
                weaponNetworkObjects.Add(spawnedNetworkObject);
                spawnedNetworkObject.TrySetParent(NetworkObject, false);
            }
            else //Client reads from NetworkList<NetworkObjectReference>
            {
                Debug.Log("Network Object Id from NetworkList: " + weaponNetworkObjects[i].NetworkObjectId);
                weaponNetworkObjects[i].TryGet(out NetworkObject readNetworkObject, NetworkManager.Singleton);
                Debug.Log("Network Object from NetworkList: " + readNetworkObject); //readNetworkObject is null on late client
                if (readNetworkObject)
                {
                    shooters.Add(readNetworkObject.GetComponent<ShooterBase>());
                }
            }
        }
        InitializeWeaponStates();
    }

Okay so turns out the IDs are supposed to be different and I made a wrong turn and got confused. That was me. I understand it now. BUT, the readNetworkObject is still null. I don’t know what’s wrong with that.

Okay it’s a race condition. I tried to wait using a coroutine before calling TryGet and it works. Kinda hacky but whatever.

The client should hook into value changed events of the NetworkList and solely rely on that. You can’t expect the list to be synchronized in OnNetworkSpawn when you modify the list in that event method.

The actual flow of calls is more or less like this:

  • Owner: OnNetworkSpawn
  • Owner: adds items to network list
  • (hidden) Owner sends network list changes in the next network tick (imagine an RPC here)
  • Client: OnNetworkSpawn
  • Client: reads list (not yet received new data)
  • some network ticks pass …
  • Client receives owner’s list changes

FWIW if you’re using Netcode or any async service and you’re “waiting some time” (ie coroutine) to make something work, it’s not correct, and will almost certainly fail in some situations (ie high ping) OR it forces the game to progress more slowly due to excess waiting.

1 Like

Thank you, this actually clears up a lot of things for me. So I should just subscribe to the OnValueChanged of the NetworkList and would that be called for the late-joining client the moment they join the game?

Yeah I figured. That was honestly a temporary solution but to be clear I didn’t mean wait as in WaitForSeconds in my last reply, instead I do a while loop checking if the readNetworkObject is null.

    private IEnumerator NonOwnerSpawnRoutine(int i)
    {
        NetworkObject readNetworkObject;
        do
        {
            weaponNetworkObjects[i].TryGet(out readNetworkObject, NetworkManager.Singleton);   
            yield return null;
        }
        while (readNetworkObject == null);

        if (readNetworkObject)
        {
            bool setParent = readNetworkObject.TrySetParent(NetworkObject, false);
            shooters.Add(readNetworkObject.GetComponent<ShooterBase>());
        }
        InitializeWeaponStates();
    }

I don’t know if that’s any better though, so I will be trying the proper way using OnValueChanged. Anyway, thanks for helping!