NetworkList

Unity 6
Netcode 2.2.0

For a little multiplayer project, i am trying to keep track of the names of players (that the clients can enter when connecting) in a static NetworkList that holds a serialized struct PlayerData:

public struct PlayerData : IEquatable<PlayerData>, INetworkSerializable
{
    public ulong clientID;
    public FixedString32Bytes playerName;

    public bool Equals(PlayerData other)
    {
        return clientID == other.clientID &&
                playerName == other.playerName;

    }

    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
        serializer.SerializeValue(ref clientID);
        serializer.SerializeValue(ref playerName);
    }
}

The NetworkList is initialized directly:

private static NetworkList<PlayerData> playerDataNetworkList = new();

And after the Host approves the client connection, a new PlayerData (name of the player is sent in ConnectionData) is created and added to the list:

private void NetworkManager_ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
    string requestPlayerName = Encoding.ASCII.GetString(request.Payload);
    Debug.LogFormat($"{ConnectionPrefix} Server receives request of {requestPlayerName} from {request.ClientNetworkId}; isHost: {request.ClientNetworkId == NetworkManager.Singleton.LocalClientId}");

    if(request.ClientNetworkId == NetworkManager.Singleton.LocalClientId) //If connection is host, always approve
    {
        //Host was already added to playerDataList in StartHost()
        Debug.LogFormat($"{ConnectionPrefix} AUTO APPROVED Host");
        response.Approved = true;
        return;
    }
    else
    {
        if(playerDataNetworkList.Count >= 4)
        {
            response.Approved = false;
            return;
        }

        response.Approved = true;
        AddToPlayerList(requestPlayerName, request.ClientNetworkId);
    }
}

The Host is adding himself to this list directly after calling StartHost on the NetworkManager.

The callback subscriptions are as follows:
Host:

public void StartHost(string playerName = "Host")
{
    NetworkManager.Singleton.ConnectionApprovalCallback += NetworkManager_ConnectionApprovalCallback;
    NetworkManager.Singleton.OnClientConnectedCallback += NetworkManager_OnClientConnectedCallback;
    NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_Server_OnClientDisconnectCallback;
    Debug.LogFormat($"{ConnectionPrefix} Starting Host");
    NetworkManager.Singleton.StartHost();

    Debug.LogFormat($"{ConnectionPrefix} Adding Host to playerList");
    AddToPlayerList(playerName, OwnerClientId);
}

Client

public void StartClient()
{
    Debug.LogFormat($"{ConnectionPrefix} Trying to join game");
    NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_Client_OnClientDisconnectCallback;
    NetworkManager.Singleton.OnClientConnectedCallback += Client_OnClientConnectedCallback;
    NetworkManager.Singleton.NetworkConfig.ConnectionData = Encoding.ASCII.GetBytes("Client");
    Debug.LogFormat($"{ConnectionPrefix} Starting Client");
    NetworkManager.Singleton.StartClient();
    
}

Both OnNetworkSpawn:

public override void OnNetworkSpawn()
{
    playerDataNetworkList.OnListChanged += PlayerDataNetworkList_OnListChanged;  
}

Which will call PlayerDataNetworkList_OnListChanged, which i intend to fire an event to handle name changes for example. The Invoked event here is not important right now, but i added a Debug.Log to cross check the change on the Client:

 private void PlayerDataNetworkList_OnListChanged(NetworkListEvent<PlayerData> changeEvent)
 {
     Debug.LogFormat($"{ConnectionPrefix} PLAYERLIST CHANGED {changeEvent.Type} -> Value playerName {changeEvent.Value.playerName}/Client {changeEvent.Value.clientID} -- at Index {changeEvent.Index}, count now: {playerDataNetworkList.Count}");
     OnPlayerDataNetworkListChanged?.Invoke(this, EventArgs.Empty);
 }

For testing i use the multiplayer playmode of Unity.
I start the host and join with the client and get the following outputs:

On the Host:

And on the Client:

Now my question is: Why does the Client have three entries in the playerDataList, while the host only has the two? Why are there two entries for clientID 1 (with name “Client”)? Shouldnt it be synched with the Host, as the Client doesnt have any write permissions to the NetworkList?

I wanted to make sure there is nothing wrong with my PlayerData, so i created another NetworkList that just holds Integers, but i get the same result in having the second entry duplicated on the Client side.

I created a repository for anyone that wants to check the project:

Am i missing something here? What should i be doing different?
Help is much appreciated

You should not do that, this is likely too early.

And generally, avoid doing very different code paths for host and client. There’s no reason why you shouldn’t or couldn’t call AddToPlayerList in the host’s connection approval.

But really I’d rather wait for the player object’s OnNetworkSpawn so that you’re guaranteed not writing to the net list before networking has fully established. You need to be sure that the NetworkBehaviour containing the NetworkList has had its OnNetworkSpawn method run (IsSpawned must be true).

As to the duplication, check if you are running any “add” multiple times for some reason. Not sure if your logs make this visible or not, but having this many logs it can sometimes be hard to spot the obvious. :wink:

Well when i try to add the host to the list when its own approval is happening (which will always be approved anyway), i get a NullReferenceException saying the NetworkList is not yet available, which is exactly your point.
So i need to add the Host after the StartHost function is done.

I cant add them when the OnNetworkSpawn is called, because then i would lose the ConnectionData that is sent in the approval request payload, right? Or am i missing a functionality here?

I understand the “different code paths for host and client” and aggree. I will change it to have the same paths as much as possible.

But that aside: The AddToPlayerList function is only called on server/host side and also only two times (once when StartHost is done and once after the client is approved):

On the client side, this function is not called, but even if it would be: The permissions are write permissions for server only by default. I also tried to specifically initialize the NetworkList with the Server write permissions, but got the same behaviour:

private static NetworkList<PlayerData> playerDataNetworkList = new(writePerm: NetworkVariableWritePermission.Server);

And also also: Even if the permissions would allow the client to write them, why would it not be synched (the same) on client side and server/host side?

I must be missing something here or something else is wrong.

I took a look at your project. When the GameManager spawns on the client the list already contains both players, the client is then added again for some reason. Strictly this is a bug but I wouldn’t be adding to the list in connection approval because as far as NGO is concerned the client hasn’t connected yet. Instead store the player data temporarily and add it to the list in the OnClientConnectedCallback.

Just a note that network variables/lists shouldn’t be static (it doesn’t look necessary in this case anyway), and network lists should be instantiated in Awake, unless something has changed recently.

1 Like

As far as I’m aware you can new() the field no problem but setting the initial value before OnNetworkSpawn ran requires an extra step like calling some Init method. This was the case when spawning a network object and then setting a network variable/list value right after or even before calling NetworkObject’s spawn methods.

On another post, somebody said we should not be instantiating NetworkVariables in the Awake() function. I will try to find it again.

Yes the static List was an attempt to get rid of this problem, but it didnt help. The duplication also happens without it.

Although for me it seems like a unnecessary extra step (and saved value) to store the player data temporarily, i will check if this works better.
Probably would have to make a local list on the host and add to it on approval of client connection, then on finished connection the host would check that local list and put the player data into the NetworkList.

Still seems like a workaround for something that should not happen IMHO.

Historically instantiating the network list immediately would cause an error, and the documentation mentions to do it in Awake. Saying that I’m not seeing an error in your project so it may no longer be an issue. That’s with NGO v2 at least, I’ve not tried it in v1.

It is a bug but keep in mind connection approval is meant for either accepting or rejecting a connection, it’s just very convenient to pass other data at that time and I use it myself to save the hassle of separate messaging. What I tend to have is a connection manager to retain this data until it’s requested on client connection.