Search Unity

how to assign player number in multiplayer games

Discussion in 'Scripting' started by ryand-unity, Sep 14, 2014.

  1. ryand-unity

    ryand-unity

    Joined:
    Jan 21, 2011
    Posts:
    547
    I'm working on multiplayer player game and I need to assign player numbers as they connect to the server how would I go about doing this
     
  2. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    One idea would be if you have a list of objects that represent players, let's say a class with a username (simplest case). then, if you precreate those objects, and simply assign them to players when they connect, e.g. if you use a fixed array of pre created player reference objects. Then you can simply say; int playerID = Array.IndexOf(playerObjectsArray, playerObject);

    I hope this makes sense, if you need a code sample don't hesitate to ask! :)

    ~
    Another walk in the park.
     
  3. ryand-unity

    ryand-unity

    Joined:
    Jan 21, 2011
    Posts:
    547
    if you wouldn't mind some code would be great
     
  4. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    How are you handling connections, unity udp? or other?
     
  5. ryand-unity

    ryand-unity

    Joined:
    Jan 21, 2011
    Posts:
    547
    im using the unity master server "Network.InitializeServer(6, serverPort, useNat);"
     
  6. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
  7. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    Code (CSharp):
    1. //PlayerManager.cs
    2. //C#
    3. using UnityEngine;
    4. using System;
    5. using System.Collections;
    6. using System.Collections.Generic;
    7.  
    8. public class PlayerManager : MonoBehaviour
    9. {
    10.     public sealed class PlayerData
    11.     {//this class is used to store our connected players data
    12.         private Guid m_guid;                    //used to store the guid (for dictionary/temporary file use)
    13.         private NetworkPlayer m_networkPlayer;    //the connected player object
    14.         private bool m_assigned;                //whether this object is assigned or not
    15.        
    16.         public string guid { get { return m_guid.ToString(); } }                //readonly guid property
    17.         public NetworkPlayer networkPlayer { get { return m_networkPlayer; } }    //readonly networkPlayer property
    18.        
    19.         public string ip { get { return m_networkPlayer.ipAddress; } }    //readonly ip of player
    20.         public int port { get { return m_networkPlayer.port; } }        //readonly port of player
    21.        
    22.         public PlayerData()
    23.         {//default constructor
    24.             m_networkPlayer = default(NetworkPlayer);    //initialize all fields to unassigned state
    25.             m_assigned = false;                            //
    26.         }
    27.        
    28.         public void Assign(NetworkPlayer networkPlayer)
    29.         {//this method assigns this playerData object to a connected player
    30.             //we can only assign this object if it isn't already assigned
    31.             if (m_assigned) { throw new Exception("PlayerData object has already been assigned!"); }
    32.            
    33.             m_networkPlayer = networkPlayer;    //store the connected player object
    34.             m_guid = Guid.NewGuid();            //generate a unique global session id code for this player
    35.             m_assigned = true;                    //mark this object as assigned
    36.         }
    37.        
    38.         public void Release()
    39.         {//this method releases this object so that it can be assigned to other players later
    40.             if (!m_assigned) { return; }
    41.             m_networkPlayer = default(NetworkPlayer);    //reset all fields to unassigned state
    42.             m_assigned = false;                            //
    43.         }
    44.        
    45.         public static implicit operator bool(PlayerData playerData)
    46.         {/*    this allows you to use this object as a bool conditional,
    47.             e.g. if (playerData) { object is assigned } else { object is unassigned }    */
    48.             return playerData.m_assigned;
    49.         }
    50.     }
    51.    
    52.     public int maxPlayers;    /*    this is not user cap, use this to initialize the player stack
    53.                                 if the player stack isn't enough for the amount of players you
    54.                                 will be hosting, you'll run out of objects    */
    55.    
    56.     private PlayerData[] playerCache;                            //all the physical player objects
    57.     private Stack<PlayerData> playerStack;                        //we use a stack to only assign unused objects to players
    58.     private Dictionary<NetworkPlayer, PlayerData> playerBase;    /*    we use a dictionary to make it easier to find the matching
    59.                                                                     playerData objects for players    */
    60.                                                                    
    61.     public int playerCount { get; private set; }
    62.    
    63.     //use this to initialize component references and variables
    64.     public void Awake()
    65.     {
    66.         playerCount = 0;
    67.         playerCache = new PlayerData[maxPlayers];
    68.         playerStack = new Stack<PlayerData>();
    69.         playerBase = new Dictionary<NetworkPlayer, PlayerData>();
    70.     }
    71.    
    72.     //use this to initialize script references and components
    73.     public void Start()
    74.     {
    75.         for(int i = 0; i < maxPlayers; i++)
    76.         {
    77.             playerCache[i] = new PlayerData();
    78.             playerStack.Push(playerCache[i]);
    79.         }
    80.     }
    81.    
    82.     public void OnPlayerConnected(NetworkPlayer networkPlayer)
    83.     {//a player connected
    84.         PlayerData playerData = playerStack.Pop();    //grab an unassigned playerData object from the stack
    85.         playerData.Assign(networkPlayer);            //assign the playerData object to the new networkPlayer
    86.         playerBase.Add(networkPlayer, playerData);    //add the playerData and networkPlayer objects to the dictionary
    87.         Debug.Log("Player " + GetPlayerNumber(playerData) + " connected from " + playerData.ip + ":" + playerData.port);
    88.         playerCount++;    //count connected players
    89.     }
    90.    
    91.     public void OnPlayerDisconnected(NetworkPlayer networkPlayer)
    92.     {//a player disconnected
    93.         PlayerData playerData = playerBase[networkPlayer];    //find the playerData object in our dictionary
    94.         Debug.Log("Player " + GetPlayerNumber(playerData) + " disconnected from " + playerData.ip + ":" + playerData.port);
    95.         playerData.Release();                                //release the playerData object for re-use
    96.         playerBase.Remove(networkPlayer);                    //remove the playerData and networkPlayer objects from the dictionary
    97.         playerStack.Push(playerData);                        //push the playerData object back onto the stack for re-use
    98.         playerCount--;    //discount disconnected players
    99.     }
    100.    
    101.     private int GetPlayerNumber(PlayerData playerData)
    102.     {//get the player number, referencing the playerData object
    103.         return Array.IndexOf(playerCache, playerData);
    104.     }
    105.    
    106.     private int GetPlayerNumber(NetworkPlayer networkPlayer)
    107.     {//get the player number, referencing the networkPlayer object
    108.         return Array.FindIndex(playerCache, x => x.networkPlayer == networkPlayer);
    109.     }
    110. }
    111.  
    Here's an example of the process I defined before. I hope it's clear and concise enough that you get the idea! :)
     
    devansurf likes this.
  8. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    Now try to do the same with less code as possible... :)
     
  9. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    lol pun intended?
     
  10. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    I think sometimes we write a lot of code to experiment and learn doing, but generally is better to try and find a way to reduce code at minimun.
    Don't you agree?
     
  11. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    Sure but trust me, that code is standard practice for most servers.
    Also the PlayerData object I created, is designed to be secure too!
    It's also high performing. :)
     
  12. cmcpasserby

    cmcpasserby

    Joined:
    Jul 18, 2014
    Posts:
    315
    Also premature optimization is generally a bad idea, no point wasting time optimizing something before you even have enough of the game built to know where the slow downs are. Also in the early stages maintainability scalability is worth more than how optimized the code is.
     
  13. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    Who said anything about optimization, as far as my standards are concerned this code I posted is unoptimized, but there's nothing wrong with it either lol! :p
     
  14. cmcpasserby

    cmcpasserby

    Joined:
    Jul 18, 2014
    Posts:
    315
    that would be in response to fraconte not you
     
  15. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    I don't mean you have to do premature optimization. I just mean don't overcomplicate things: stack, dictionary, array cache, a sealed class... all to get a player number? I can be wrong but I see a lot of redundant code and memory use. Sure it's a good thing if you try to make something reusable and flexible and secure... just don't forget that less code you have, the better. :)
     
  16. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    Less code isn't "always" better, in some cases it's not, if you'll sacrifice player security then it's not a good thing.

    Also I simply presented an "easy" solution, easy in respect to how the data is handled in one place. Rather than across several variables which for some people might be ok, but the more complicated your server gets, the harder it'll get to manage, and the less scalable.

    Just another side note however, I've done a lot of server development trying to learn what's best, I've done a lot of research, and a lot of the stuff being done in this code "sample" is a lot of the stuff I find myself doing in every type of server application. If you want for instance to implement game bans, ip address is important, so I make it easy to get it direct from the playerData objects.
    Performance is saved if the objects are pre-created. A guid is useful if u wish to store session data in a dictionary or temporary file. Checking if an object is already assigned to a player is useful for bug checking and code fault tolerance, so I made it easy by making it so you can use the object as a bool conditional.

    The actual process is straight forward:

    player connects -> pop -> assign -> add -> count
    player disconnects -> release -> remove -> push -> discount

    The two methods at the end allow u to get the player number, which btw is fixed, it doesn't move, it doesn't change, even if a player disconnects. And allow you to do so easily, simply by referencing either the playerData object or the networkPlayer object. Ofcourse the function will be faster using playerData, cause it doesn't have to find an object matching networkPlayer, it's more native to the cache array. I hope this makes more sense to you now, and I welcome you to point out which code is redundant, kthx! :)

    Sorry saw the profile pic and automatically assumed it was Fraconte, however I agree with what you're saying too.
     
    Last edited: Sep 18, 2014
    pumpedbarbarous likes this.
  17. ryand-unity

    ryand-unity

    Joined:
    Jan 21, 2011
    Posts:
    547
    @TwixEmma if you would like to help me program my game i will send you what you need to help just PM me. i would very much like your help.
     
  18. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    Double checking your code I think there is something wrong: stack and dictionary get updated but never used. While cache is never updated but used to get the player number. Probably something is lost that can explain why you seem to pop empty objects from stacks.
    Besides... there is a reason to not use Network.connections?

    Anyway I am not questioning your competence: I am simply trying to understand and learn something more.

    Edit: Perhaps I am starting to understand: the stack is not a duplicate of the cache but it's pointing to the cache...
     
    Last edited: Sep 19, 2014
  19. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    Sorry I didn't mean to seem like I was *****ing at you, :3... I was trying to explain how the script worked lol.
    The stack is being populated by the Start function. And you're correct, simply points to objects in the cache that aren't assigned to players.

    I did have a think how it "could" be simplified, taking away the sealed class, only storing numbers, you could stack the numbers, and simply pull them when you need a number for a player, using a dictionary to map networkPlayers to numbers. Voila. But you don't get anything other than a player number.

    e.g.

    Code (CSharp):
    1. //NetworkManager.cs
    2. //C#
    3. using UnityEngine;
    4. using System.Collections;
    5.  
    6. public class NetworkManager : MonoBehaviour
    7. {
    8.     //use a stack to keep unassigned numbers in
    9.     private Stack<int> playerNumberStack;
    10.     //use a dictionary to assign numbers to players
    11.     private Dictionary<NetworkPlayer, int> playerNumberMap;
    12.  
    13.     public int maxPlayers;    //this is used to initialize the player number quota
    14.  
    15.     public void Awake()
    16.     {//initialize component references and variables
    17.         playerNumberStack = new Stack<int>();
    18.         playerNumberMap = new Dictionary<NetworkPlayer, int>();
    19.      
    20.         for (int i = maxPlayers; i > 0; i--)
    21.         {
    22.             playerNumberStack.Push(i);
    23.         }
    24.     }
    25.  
    26.     public void OnPlayerConnected(NetworkPlayer player)
    27.     {//player connected
    28.         int playerNumber = playerNumberStack.Pop();
    29.         //pop -> assign
    30.         playerNumberMap.Add(player, playerNumber);
    31.     }
    32.  
    33.     public void OnPlayerDisconnected(NetworkPlayer player)
    34.     {//player disconnected
    35.         if (playerNumberMap.ContainsKey(player))
    36.         {
    37.             int playerNumber = playerNumberMap[player];        //get player number from dictionary
    38.          
    39.             playerNumberMap.Remove(player);
    40.             //remove -> push
    41.             playerNumberStack.Push(playerNumber);
    42.         }
    43.     }
    44.  
    45.     public int GetPlayerNumber(NetworkPlayer player)
    46.     {//use to get a players number
    47.         if (!playerNumberMap.ContainsKey(player)) { return -1; }    //return -1 if we can't find the player
    48.         return playerNumberMap[player];        //get player number from dictionary
    49.     }
    50. }
    51.  
    A side note to this version of the code, I adjusted the stack push for loop to count from 1 to maxPlayers.

    Hope this helps! :)
     
    Fraconte likes this.
  20. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    It didn't seem even to me, but I just wanted to clarify for that 3% chance... :)
    That was a nice touch too... But now it's time for you to say goodbye to your loved stack! :)
    The only use of that stack is to pop out some ints that you have stored but it force you to be limited by maxPlayers.
    So why don't get rid of that stack and just use the old playersCount variable?

    Or perhaps better would be to just use that Array.IndexOf thing you did with playerCache and playerData but with network.connections and networkPlayer? From doc it seems an Array but I am not sure if it's doable... I have to think about it.
     
  21. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    the stack would actually be more than 5x faster than using Array.IndexOf,
    Also, if you use playerCount you end up with two problems:
    • playerA connects -> playerB connects -> playerC connects -> playerB disconnects -> playerC now has a different playerNumber, and is there for not usable for storage or array indexing purposes.
    • potential playerNumber collision, two people could end up with the same playerNumber: playerA connects -> playerB connects -> playerC connects -> playerB disconnects -> playerD connects... playerD's id is the same as playerC (this situation applies if you store the ID for each player so that it doesn't change)

    I hope this clarifies things more! :)
     
    Fraconte likes this.
  22. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    Ok... you can keep your stack for now. :)
     
  23. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    If you didn't care about number reliability you can simply make a method like follows:

    public int GetPlayerNumber(NetworkPlayer player) {
    return Array.IndexOf(Network.connections, player);
    }

    But please consider that this method is susceptible to the above two fail scenarios!
    Hope this helps! :)
     
  24. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    That was exactly what I was thinking about.
    Probably you are right as for the fail scenarios, but perhaps it depends on what happens to Network.connections when someone disconnect: is the "hole" filled or just left equal to null?
    I wonder if it's possible to get a unique identifier from the networkPlayer.guid too.
     
  25. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    I'm pretty sure the Network.connections array is just that, an array of active connections. So the array would concat when somebody disconnected.
     
  26. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    I have not tested it but it should work...
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6.  
    7. public class PlayerManager : MonoBehaviour
    8. {
    9.     public int playerCount { get; private set; }
    10.  
    11.     Dictionary<int, int> playerBase; // map Network.connections index to a unique player number
    12.     int nextPlayer;
    13.  
    14.     public void Awake()
    15.     {
    16.         playerBase = new Dictionary<int, int>();
    17.     }
    18.  
    19.     public void OnPlayerConnected(NetworkPlayer networkPlayer)
    20.     {
    21.         playerBase.Add(playerCount++, nextPlayer++);
    22.         Debug.Log("Player " + GetPlayerNumber(networkPlayer) + " connected from " + networkPlayer.ipAddress + ":" + networkPlayer.port);
    23.     }
    24.  
    25.     public void OnPlayerDisconnected(NetworkPlayer networkPlayer)
    26.     {
    27.         int playerNumber = GetPlayerNumber(networkPlayer);
    28.         Debug.Log("Player " + playerNumber + " disconnected from " + networkPlayer.ipAddress + ":" + networkPlayer.port);
    29.         playerBase.Remove(playerNumber);
    30.         playerCount--;
    31.     }
    32.  
    33.     public int GetPlayerNumber(NetworkPlayer networkPlayer)
    34.     {
    35.         int index = Array.IndexOf(Network.connections, networkPlayer);
    36.  
    37.         if (index >= 0 && playerBase.ContainsKey(index))
    38.             return playerBase[index];
    39.         else
    40.             return -1;
    41.     }
    42. }
    43.  
    If needed to retrieve the networkPlayer from the playerNumber, it's possible to add another Dictionary with the keys and values reversed and use that to find the index to Network.connections.
     
  27. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    Nice attempt, but that code will break the moment the Network.connections array changes (e.g. a player disconnects)

    The idea behind using a stack is it's a quick efficient way to assign unused numbers to new players.

    e.g.
    Let's say 3 players connect
    A = 1
    B = 2
    C = 3

    and B disconnects
    A = 1
    C = 3

    Then D connects
    A = 1
    D = 2
    C = 3

    Basically D took the slot that B left open, however with good server design this isn't a security issue, just a nice way of tracking players with non-changing number assignment.

    Then using a Dictionary with the NetworkPlayer as key and int for the number assigned, you basically secure that number to specifically that player, hope this clarifies things a bit more! :)

    One last point to note is if you're creating a server game that once started the server stops accepting connections, but users can of course end up disconnected/quit from the game. What you could do is assign player numbers just as the game starts to those connected, instead of when they connect. Then you don't need a stack, you can simply do like follows:

    Code (CSharp):
    1. Dictionary<NetworkPlayer, int> playerMap = new Dictionary<NetworkPlayer, int>;
    2. for(int i = 0; i < Network.connections.Length; i++) {
    3.         NetworkPlayer player = Network.connections[i];
    4.         playerMap.Add(player, i);
    5. }
    If a person disconnects, it wont interrupt the game or number assignments, but since the server is locked it won't allow anyone back in if they do get disconnected. Otherwise you'd need a stack to ensure only unused numbers got assigned. Hope this helps! :)
     
    Last edited: Sep 22, 2014
  28. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    Why it break? The player number is assigned from nextPlayer and is always unique.
    D = 4

    At first I used that but doing so means duplicating Network.connections.
     
  29. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    You are right... it breaks. :)
    The version with the networkPlayer worked but this fail because of connections.
     
  30. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    This is the version with networkPlayer:
    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5.  
    6. public class PlayerManager : MonoBehaviour
    7. {
    8.     public int playerCount { get; private set; }
    9.  
    10.     Dictionary<NetworkPlayer, int> playerBase;
    11.     int nextPlayer;
    12.  
    13.     public void Awake()
    14.     {
    15.         playerBase = new Dictionary<NetworkPlayer, int>();
    16.     }
    17.  
    18.     public void PlayerConnected(NetworkPlayer networkPlayer)
    19.     {
    20.         playerBase.Add(networkPlayer, nextPlayer++);
    21.         Debug.Log("Player " + GetPlayerNumber(networkPlayer) + " connected from " + networkPlayer.ipAddress + ":" + networkPlayer.port);
    22.         playerCount++;
    23.     }
    24.  
    25.     public void PlayerDisconnected(NetworkPlayer networkPlayer)
    26.     {
    27.         Debug.Log("Player " + GetPlayerNumber(networkPlayer) + " disconnected from " + networkPlayer.ipAddress + ":" + networkPlayer.port);
    28.         playerBase.Remove(networkPlayer);
    29.         playerCount--;
    30.     }
    31.  
    32.     public int GetPlayerNumber(NetworkPlayer networkPlayer)
    33.     {
    34.         if (playerBase.ContainsKey(networkPlayer))
    35.             return playerBase[networkPlayer];
    36.         else
    37.             return -1;
    38.     }
    39.  
    40.     public NetworkPlayer GetPlayer(int playerNumber)
    41.     {
    42.         foreach(var player in playerBase)
    43.         {
    44.             if(player.Value == playerNumber)
    45.             {
    46.                 return player.Key;
    47.             }
    48.         }
    49.         return default(NetworkPlayer);
    50.     }
    51. }
    52.  
    Edit: Added GetPlayer(). It's a bit slow but I don't know another way (and I am not sure is frequently needed indeed).
     
    Last edited: Sep 23, 2014
  31. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    D = 2 if you use a stack like I do.

    And if you always assign a unique number, you end up with empty slots if players disconnect e.g.

    A = 1
    B = 2
    C = 3
    D = 4
    E = 5

    B and C disconnect

    A = 1
    D = 4
    E = 5

    F connects

    F = 6

    But numbers 2, and 3 are forever lost until the server is reset. This is ok if you don't care about losing numbers, however my method ensures everybody has a number that can be sorted in a list with no spaces if new places connect and fill those spaces.
     
  32. Fraconte

    Fraconte

    Joined:
    Dec 6, 2013
    Posts:
    327
    If you want to keep all the lost connections you can just not Remove them and eventually set their networkPlayer to default. If you replace them with new connections then you lost old connections anyway so why bother? Just add them to the end of dictionary so you have at least a dictionary sorted by player (and connection) number.