Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NetworkList Sometimes Goes Out of Sync in Distributed Authority Setup #3280

Open
nikajavakha opened this issue Feb 9, 2025 · 2 comments
Open
Labels
stat:awaiting response Awaiting response from author. This label should be added manually. stat:awaiting triage Status - Awaiting triage from the Netcode team. type:bug Bug Report

Comments

@nikajavakha
Copy link

nikajavakha commented Feb 9, 2025

Description

In a distributed authority setup, I noticed that the NetworkList sometimes goes out of sync. For example, I have a private NetworkList called connectedPlayers. I built the game and ran three instances, where Client 1 was the session owner. Only the session owner is allowed to update connectedPlayers.

For each client connection, I added the new PlayerConnection from the session owner's side. Initially, it logged correctly for the first two clients as:
SceneEventType: SynchronizeComplete | ConnectedPlayerClientIds: 1,3,2

However, for the third client, the log showed:
SceneEventType: SynchronizeComplete | ConnectedPlayerClientIds: 1,3,2,3,2

indicating that some entries were duplicated. this not happens often(mostly happens when I run multiple built instances simultaneously, causing my PC's CPU usage to reach 100%.) so it's hard to reproduce but it makes me feel that NetworkList is unreliable to use. Is there any alternative of NetworkList? or if not could this be indicating bug in NGO?

public class NetworkPlayerManager : NetworkBehaviour
{
    private NetworkList<PlayerConnection> connectedPlayers = new();

    public override void OnNetworkSpawn()
    {
            NetworkManager.SceneManager.OnSceneEvent += OnSceneEvent;
    }

    private void OnSceneEvent(SceneEvent sceneEvent)
    {
        Debug.Log($"SceneEventType: {sceneEvent.SceneEventType} | ConnectedPlayerClientIds: {string.Join(",", connectedPlayers.ToList().Select(c => c.ClientId))}")
    }
}

using System;
using Assets.Scripts.Tools;
using Unity.Collections;
using Unity.Netcode;

namespace Assets.Scripts
{
    public struct PlayerConnection : INetworkSerializable, IEquatable<PlayerConnection>
    {
        public ulong ClientId;
        public FixedString64Bytes PlayerId;
        public FixedString64Bytes PlayerName;
        public NetworkObjectReference NetworkPlayerRef;
        
        public PlayerConnection(
            ulong clientId,
            FixedString64Bytes playerId,
            FixedString64Bytes playerName,
            NetworkObjectReference networkPlayerRef)
        {
            ClientId = clientId;
            PlayerId = playerId;
            PlayerName = playerName;
            NetworkPlayerRef = networkPlayerRef;
        }

        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter 
        {
            serializer.SerializeValue(ref ClientId);
            serializer.SerializeValue(ref PlayerId);
            serializer.SerializeValue(ref PlayerName);
            serializer.SerializeValue(ref NetworkPlayerRef);
        }

        public bool Equals(PlayerConnection other)
        {
            return ClientId == other.ClientId &&
                   PlayerId == other.PlayerId &&
                   PlayerName == other.PlayerName &&
                   NetworkPlayerRef.Equals(other.NetworkPlayerRef);
        }
    
    }
}

Environment

  • Unity Version: Unity 6 (6000.0.32f1)
  • Netcode Version: [2.2.0]
@nikajavakha nikajavakha added stat:awaiting triage Status - Awaiting triage from the Netcode team. type:bug Bug Report labels Feb 9, 2025
@NoelStephensUnity
Copy link
Collaborator

NoelStephensUnity commented Feb 12, 2025

@nikajavakha
Could you provide the script where the SessionOwner is updating the connectedPlayers property?

Optionally, you might try an alternate approach like this:

/// <summary>
/// Assuming this is on an in-scene placed NetworkObject
/// </summary>
public class NetworkPlayerManager : NetworkBehaviour
{
    private NetworkVariable<Dictionary<ulong, PlayerConnection>>  connectedPlayers = new NetworkVariable<Dictionary<ulong, PlayerConnection>>(new Dictionary<ulong, PlayerConnection>());
    private ulong LastKnownSessionOwner;
    public override void OnNetworkSpawn()
    {
        NetworkManager.OnSessionOwnerPromoted += NetworkManager_OnSessionOwnerPromoted;
        LastKnownSessionOwner = NetworkManager.CurrentSessionOwner;
        if (NetworkManager.LocalClient.IsSessionOwner)
        {
            NetworkManager.OnConnectionEvent += NetworkManager_OnConnectionEvent;
        }
        else
        {
            connectedPlayers.OnValueChanged += OnconnectedPlayersChanged;
        }
    }

    /// <summary>
    /// Upon a new session owner being promoted due to the previous one having disconnected,
    /// register for connection events and take over tracking connected players.
    /// </summary>
    private void NetworkManager_OnSessionOwnerPromoted(ulong sessionOwnerPromoted)
    {
        if (NetworkManager.LocalClientId == sessionOwnerPromoted) 
        {
            connectedPlayers.OnValueChanged -= OnconnectedPlayersChanged;
            NetworkManager.OnConnectionEvent += NetworkManager_OnConnectionEvent;
            // Handle removing the previous session owner
            if (connectedPlayers.Value.ContainsKey(LastKnownSessionOwner))
            {
                connectedPlayers.Value.Remove(LastKnownSessionOwner);
                connectedPlayers.CheckDirtyState();
            }
        }

        LastKnownSessionOwner = sessionOwnerPromoted;
    }

    protected override void OnNetworkSessionSynchronized()
    {
        // Each non-sessioin owner client will log all connected players upon finishing synchronization
        LogConnectedPlayers();
        base.OnNetworkSessionSynchronized();
    }

    private void NetworkManager_OnConnectionEvent(NetworkManager networkManager, ConnectionEventData eventData)
    {
        switch(eventData.EventType)
        {
            case ConnectionEvent.PeerConnected:
                {
                    // Add player info
                    connectedPlayers.Value.Add(eventData.ClientId, new PlayerConnection()
                    {
                        ClientId = eventData.ClientId,
                    });
                    connectedPlayers.CheckDirtyState();
                    break;
                }
            case ConnectionEvent.PeerDisconnected:
                {
                    // Remove player
                    connectedPlayers.Value.Remove(eventData.ClientId);
                    connectedPlayers.CheckDirtyState();
                    break;
                }
        }
    }

    private void LogConnectedPlayers()
    {
        Debug.Log($"ConnectedPlayerClientIds: {string.Join(",", connectedPlayers.Value.Values.Select(c => c.ClientId))}");
    }

    private void OnconnectedPlayersChanged(Dictionary<ulong, PlayerConnection> previous, Dictionary<ulong, PlayerConnection> current)
    {
        LogConnectedPlayers();
    }
}

@NoelStephensUnity NoelStephensUnity added the stat:awaiting response Awaiting response from author. This label should be added manually. label Feb 12, 2025
@nikajavakha
Copy link
Author

nikajavakha commented Feb 14, 2025

I listen for SceneEvent on the session owner's side. If the event is LoadComplete, I spawn a NetworkPlayer for the SessionOwner. If the event is SynchronizeComplete, I spawn a NetworkPlayer for the connected client. Then, each NetworkPlayer, after spawning, sends its PlayerId and PlayerName to the session owner, who adds it to the connectedPlayers list.

First, I am hosting a game, so the session owner is connected and ready. Then, I open multiple game instances simultaneously to join the session. However, the network list often goes out of sync by that time (I run all instances on the same machine at the same time, and sometimes the CPU briefly reaches 100%).

However, if I open the game instances with a 2–3 second delay, everything works correctly. During the mid-game, the network list also remains properly synchronized—I haven't noticed any issues.

Note: NetworkManager.Singleton.ConnectedClientIds is always correct, even when I open game instances simultaneously.

here is pseudo code:

public class NetworkPlayerManager : NetworkBehaviour
{
    private void OnSceneEvent(SceneEvent sceneEvent)
    {
        Debug.Log($"SceneEventType: {sceneEvent.SceneEventType} | ConnectedPlayerClientIds: {string.Join(",", connectedPlayers.ToList().Select(c => c.ClientId))}")

        if (!NetworkManager.LocalClient.IsSessionOwner)
            return;

        switch (sceneEvent.SceneEventType)
        {
            case SceneEventType.LoadComplete:
                // spawn session owner
                if (sceneEvent.ClientId == NetworkManager.LocalClientId && !HasConnectedPlayer(sceneEvent.ClientId))
                {
                    SpawnNetworkPlayer(sceneEvent.ClientId);
                }

                break;
            case SceneEventType.SynchronizeComplete:
                //spawn peer client
                SpawnNetworkPlayer(sceneEvent.ClientId);
                break;
        }
    }

    private void SpawnNetworkPlayer(ulong clientId)
    {
        var networkPlayer = Instantiate(networkPlayerPrefab);
        networkPlayer.NetworkObject.SpawnWithOwnership(clientId);
    }

    [Rpc(SendTo.Authority)]
    public void RegisterPlayerAuthorityRpc(PlayerConnection playerConnection, RpcParams rpcParams = default)
    {
        connectedPlayers.Add(playerConnection);
    }

     public bool HasConnectedPlayer(ulong clientId)
     {
            foreach (var player in connectedPlayers)
            {
                if (player.ClientId == clientId)
                    return true;
            }

            return false;
       }
}

public class NetworkPlayer : NetworkBehaviour 
{
    public override void OnNetworkSpawn(){
        if (IsOwner){
            var playerId = AuthenticationService.Instance.PlayerId
            var playerName = AuthenticationService.Instance.Profile;
            var playerConnection = new PlayerConnection(OwnerClientId, playerId, playerName, NetworkObject);
            NetworkPlayerManager.Instance.RegisterPlayerAuthorityRpc(playerConnection);
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stat:awaiting response Awaiting response from author. This label should be added manually. stat:awaiting triage Status - Awaiting triage from the Netcode team. type:bug Bug Report
Projects
None yet
Development

No branches or pull requests

2 participants