How to list the name of connected clients with Fishnet?

Hello. I’m having great difficulty doing something that should be simple: listing the names of users connected to the game.
I’m studying Fishnet 4 and I’m also having a lot of difficulties.
After searching a lot and collecting fragments of ideas from all corners, I came to the conclusion that I should have a list that stores all the players that connect and, with each new player connected, I should add the name to the list and update for all users the user list. But it’s not working.

This is the PlayerManager.cs script that is attached to the player prefab:

    public override void OnStartClient()
    {
        base.OnStartClient();

        if (IsOwner)
            AddPlayer();
    }

    [ServerRpc]
    void AddPlayer()
    {
        string name = GameManager.instance.AddPlayer(gameObject);
        transform.GetComponent<PlayerController>().username = name;
        UpdateUserboard();
    }

    [ObserversRpc]
    void UpdateUserboard()
    {
        HUDController.instance.UpdateUserboard();
    }

And here is the GameManager that is attached to an empty scene object that already has the NetworkObject component:

    public static GameManager instance;
    public readonly SyncList<GameObject> players = new();

    private void Awake()
    {
        instance = this;
    }

    public string AddPlayer(GameObject player)
    {
        string name = GenerateName();
        player.name = name;
        instance.players.Add(player);
        return name;
    }
// ....

And the script that updates the HUD, which is within Canvas > HUD

public void UpdateUserboard()
    {
        playerNames.text = "<b>Players:</b>";
        foreach (GameObject player in GameManager.instance.players)
        {
            playerNames.text += "
"+player.name;
        }
    }

Everything works correctly on the host (server + client), showing the name of the connected players. However, when opening a client, the host name is not displayed and the name of the client itself remains with the prefab’s default name. If you open more clients, they are also shown, but they are all displayed with default names too. It’s as if the AsyncList is being reset every time a user connects.
In fact, instead of adding a “username” field to the PlayerController, I also tried just changing the name of the gameObject, but that doesn’t work either.
I have this project on my github here.
I’ve been trying and making changes for days but this was the best I could do.

If Fish-Net works anything like NGO, which I would assume it does, then sharing a GameObject doesn’t actually share the object at all but only its “network ID”. An object’s fields aren’t shared indiscriminately both because it wouldn’t work for every field/property and because it would be incredibly wasteful to do so.

Locally, the GameObject will be looked up by its “network ID” and this finds the local instance of the object which will have only local values, eg “Player (Clone)” as its name.

If you want to share names, share the names with a SyncList playerNames.

Since this isn’t a static method, simply call:
players.**Add**(player);

The second line should simply be handled inside AddPlayer(GameObject player):
player.GetComponent<PlayerController>().username = name;

Then remove the string return type from AddPlayer.

1 Like

CodeSmile is correct that syncing GO does not sync their variables, just a reference of the GO.

You can SyncVar the name field on the GO and then SyncList the GOs, or SyncList the names separately.

You can even use statics in the NB with each name like so… This assumes each player owns an object with Player.cs
(psuedo code, from mobile)

Player.cs

private readonly SyncVar<string> _name = new();

public static Dictionary<NetworkConnection, string> AllNames = new();

private void Awake()
   _name.OnChange += on_name;


override void OnStopNetwork()
    AllNames.Remove(base.Owner);

private void on_name(prev, next, asServer)
    If (base.Owner.IsValidl
       AllNames[base.Owner] = next;
1 Like

I wanted to follow-up that you can also do another really cool thing in FishNet that is super efficient and may help organize your code.

Using global nobs with our ‘Instance’ feature on the NetworkManager could be beneficial to you.

Here is an example script called PlayerNames that you will put on an object in your scene, then make a prefab out of it.

using FishNet.Connection;
using FishNet.Object;
using FishNet.Object.Synchronizing;
using FishNet.Transporting;
using System;
using System.Collections.Generic;

    public class PlayerNames : NetworkBehaviour
    {
        public event Action<NetworkConnection, string> OnNameAdded;
        public event Action<NetworkConnection, string> OnNameUpdated;
        public event Action<NetworkConnection> OnNameRemoved;

        private readonly SyncDictionary<NetworkConnection, string> _names = new();
        //SyncType is private to protect it from being modified. Values can be accessed here.
        public IReadOnlyDictionary<NetworkConnection, string> Names => _names;

        //Be notified anytime a name changes.
        private void Awake()
        {
            _names.OnChange += _names_OnChange;
        }

        //Register to the NetworkManager for easy lookups of this component!
        public override void OnStartNetwork()
        {
            base.NetworkManager.RegisterInstance<PlayerNames>(this);
        }

        //Listen for connection state changes to remove connections from names.
        public override void OnStartServer()
        {
            base.ServerManager.OnRemoteConnectionState += ServerManager_OnRemoteConnectionState;
        }
        public override void OnStopServer()
        {
            base.ServerManager.OnRemoteConnectionState -= ServerManager_OnRemoteConnectionState;
        }

        public override void OnStopNetwork()
        {
            base.NetworkManager.UnregisterInstance<PlayerNames>();
        }

        private void ServerManager_OnRemoteConnectionState(NetworkConnection arg1, RemoteConnectionStateArgs arg2)
        {
            if (arg2.ConnectionState == RemoteConnectionState.Stopped)
                _names.Remove(arg1);
        }

        //Invoke events based on change type.
        private void _names_OnChange(SyncDictionaryOperation op, NetworkConnection key, string value, bool asServer)
        {
            if (op == SyncDictionaryOperation.Add)
                OnNameAdded?.Invoke(key, value);
            else if (op == SyncDictionaryOperation.Set)
                OnNameUpdated?.Invoke(key, value);
            else if (op == SyncDictionaryOperation.Remove)
                OnNameRemoved?.Invoke(key);
        }

        //Get a connections/players name.
        public string GetName(NetworkConnection conn)
        {
            if (_names.TryGetValue(conn, out string result))
                return result;

            //Fall through, name not found.
            return "Unset";
        }

        [ServerRpc(RequireOwnership = false)]
        public void SetName(string value, NetworkConnection caller = null)
        {
            //Caller is automatically populated with whoever calls this.
            _names[caller] = value;
        }      
    }

Notice in the prefab on NetworkObject it’s set as ‘IsGlobal’ with the lowest InitializeOrder. This ensures it will always spawn before anything else. This is important because you want your PlayerNames script setup before other scripts may access it.
9767484--1399371--upload_2024-4-12_16-43-17.png

Make a new script, you can put it on your NetworkManager or anywhere really, which spawns the PlayerNames prefab soon as the server starts.

Here is an example script of using PlayerNames from a nameplate UI which could be on the player object. You can do something similar on MonoBehaviours as well by using InstanceFinder.NetworkManager.GetInstance…

public class NamePlate : NetworkBehaviour
{
    private PlayerNames _playerNames;

    public override void OnStartClient()
    {
        _playerNames = base.NetworkManager.GetInstance<PlayerNames>();
        //Listen for changes.
        _playerNames.OnNameAdded += _playerNames_OnNameAdded;
        _playerNames.OnNameUpdated += _playerNames_OnNameUpdated;
        _playerNames.OnNameRemoved += _playerNames_OnNameRemoved;
    }

    public override void OnStopClient()
    {
        //Unsubscribe.
        if (_playerNames != null)
        {
            _playerNames.OnNameAdded -= _playerNames_OnNameAdded;
            _playerNames.OnNameUpdated -= _playerNames_OnNameUpdated;
            _playerNames.OnNameRemoved -= _playerNames_OnNameRemoved;
        }
    }

    private void _playerNames_OnNameRemoved(NetworkConnection obj) { }
    private void _playerNames_OnNameUpdated(NetworkConnection arg1, string arg2) { }
    private void _playerNames_OnNameAdded(NetworkConnection arg1, string arg2) { }
}

In the end this does seem like more work and code, which admittedly it is a little more time upfront. But now you can get the name from any script without having to try and find the players object, and getting the name script on that particular object. You are separating things which you will probably use throughout your game from the player prefab which makes them more accessible and as your code becomes more complex, easier to use and more powerful.

1 Like

Hi, my client player cannot get the playerlist,
in my playercontrolller.cs

public override void OnStartClient()
    {
        base.OnStartClient();


        if (base.IsOwner)
        {
         AddPlayer(playerConnId, this.gameObject.name, this.gameObject);
        }
    }

    [ServerRpc]
    void AddPlayer(int Id, string Name, GameObject Player)
    {
        Debug.Log("ServerRpc serverAddPlayer");
        ClientList.instance.AddPlayer(Id,Name,Player);
        SetPlayerList();
    }

    [ObserversRpc]
    void SetPlayerList()
    {
        Debug.Log("ObserversRpc Set Player List");
        ClientList.instance.SetList();
    }

in my ClientList.cs

    private readonly SyncList<PlayerStruct> _players = new SyncList<PlayerStruct>();

    public void AddPlayer(int Id, string Name, GameObject Player)
    {
     
        PlayerStruct ms = new PlayerStruct();
        ms.name = Name;
        ms.connId = Id;
        ms.player = Player;
        _players.Add(ms);
       
    }


    public void SetList()
    {
        string temp_msg = "Player List:";
        foreach (PlayerStruct player in _players)
        {
            temp_msg += "\n" + player.connId + " : " + player.name;
        }

        if (PlayerListText != null)
        {
            PlayerListText.text = temp_msg;
        }
    }

Difficult to say what is going on without knowing what you tried, where you aren’t getting the results you expected, and so on.

I strongly recommend using a SyncDictionary<NetworkConnection, PlayerStruct> though. It will be a lot more beneficial in the long run and won’t use any additional resources.

Thanks for your reply.
I am using SyncDictionary now, but while my player join, i cannot call the AddPlayer function?
So do i need to use the Rpc or just [Server] is ok?

Thanks!

    private struct PlayerData
    {
        public int connId;
        public string name;
        public GameObject player;
    }
    private readonly SyncDictionary<int, PlayerData> PlayerDatas = new();

    private void Awake()
    {
        instance = this;
    }

    [Server]
    public void AddPlayer(int Id, string Name, GameObject Player)
    {

        Debug.Log("Add conn.ClientId:" + Id);

        PlayerData pD = new PlayerData
        {
            connId = Id,
            name = Name,
            player = Player
        };

        PlayerDatas[Id] = pD;

        SetList(PlayerDatas);
    }

    void SetList(SyncDictionary<int, PlayerData>_PlayerDatas)
    {

        string temp_msg = "Player List:";

        foreach (KeyValuePair<int, PlayerData> kvp in _PlayerDatas)
        {
            int id = kvp.Key;
            PlayerData data = kvp.Value;

            Debug.Log($"Player Data - ID: {id}, Name: {data.name}, Connection ID: {data.connId}");
            temp_msg += $"\n{data.connId} : {data.name}";
        }
        PlayerListText.text = temp_msg;


    }

A SyncDictionary is an associative array containing an unordered list of key, value pairs. final grade calculator

If you call AddPlayer from the server you shouldn’t have any issues.
If you are experiencing a difficult time in determining when to call AddPlayer you could use the OnSpawn callback if your script inherits from NetworkBehaviour, which I am assuming it does given it has a SyncType in it.

    public override void OnSpawnServer(NetworkConnection connection)
    {
       //Add player here.
    }

Thanks for your reply but can you tell me more,
my flow is , when user join in, he will pass the user name to the server, and the server will add his name to the playerlist and sync to all user playerlist.

so i have a gameobject(networkobject) call PlayerList with PlayerList.cs
and when player jlin with a script call MyPlayerController.cs on it, with

    public override void OnStartClient()
    {
        base.OnStartClient();
        if (base.IsOwner)
        {
              string playerName = PlayerPrefs.GetString("playername", SystemInfo.deviceName);
playerList.AddPlayer(playerConnId, playerName , this.gameObject);
            
        }

}

but what should my PlayerList.cs do to sync the playlist?

    private struct PlayerData
    {
        public int connId;
        public string name;
        public GameObject player;
    }

    private readonly SyncDictionary<int, PlayerData> PlayerDatas = new();

    [Server]
    public void AddPlayer(int Id, string Name, GameObject Player)
    {

        Debug.Log("Add conn.ClientId:" + Id);

        PlayerData pD = new PlayerData
        {
            connId = Id,
            name = Name,
            player = Player
        };

        PlayerDatas[Id] = pD;

        PlayerDatas.Dirty(Id);

        SetList(PlayerDatas);
    }

    [ObserversRpc]
    void SetList(SyncDictionary<int, PlayerData> _PlayerDatas)
    {

        string temp_msg = "Player List:";

        foreach (KeyValuePair<int, PlayerData> kvp in _PlayerDatas)
        {
            int id = kvp.Key;
            PlayerData data = kvp.Value;

            Debug.Log($"Player Data - ID: {id}, Name: {data.name}, Connection ID: {data.connId}");
            temp_msg += $"\n{data.connId} : {data.name}";
        }
        PlayerListText.text = temp_msg;
    }

Thanks

You probably want the client to call a ServerRpc telling the server what name to use for the client.

Then the server adds it as PlayerData inside the PlayerDatas SyncDictionary.
You do not need the ObserversRpc, the SyncDictionary will send to clients automatically.

See this for more: SyncDictionary | Fish-Net: Networking Evolved

Thanks, i am almost done, but may i know how to set the remove player?

i am trying to call a ServerRpc with OnStopClient() , but seem the ServerRpc didt’n run.

In MyPlayerController.cs

    public override void OnStopClient()
    {
        base.OnStopClient();
        PlayerList.instance.RemoveThePlayer(base.Owner);
    }

In playerlist.cs

    public void RemoveThePlayer(NetworkConnection conn)
    {
        PlayerLeft(conn);
    }

    [ServerRpc(RequireOwnership = false)]
    public void PlayerLeft(NetworkConnection conn)
    {
       
        PlayerDatas.Remove(conn);
}

Correct, the client is probably disconnecting at that point so it won’t be able to send the RPC. You can use OnStopServer to remove it, and just access the script directly without using a RPC.

1 Like

That could be something related to value we are giving.

In FishNet, you can get a list of connected clients using ServerManager.Clients and iterate through it to access their ClientId or any custom identifiers you’ve set. If you need to track player names, you might want to implement a dictionary that maps ClientId to a username or another identifier sent during connection. Have you tried handling this in a NetworkBehaviour on a manager object?
Thanks,
basketball stars