Master Client only connected, players list is OK. Clients connect, players appear as null

Hi,

I came across a really weird issue and I am not sure what is going on. Basically if I run my game with 1 client only (i.e. only the Master Client running and no other clients are running), the players list from PhotonNetwork.playerList works correctly (i.e. when using Debug.Log() on the first element in the list it prints out its name.

However once I run the game with a Master Client and a client the list shows the first client as null despite both the Master Client and regular client have their player spawned and I can move the player no problems at all. Moreover there are no errors in the console at all and the Debug.Log() showing null is on both Master Client and regular client.

Here is the code I am using to get the playerList:

private void OnEnable() { if (PhotonNetwork.isMasterClient == false) return; originalPosition = t.position; Debug.Log(PhotonNetwork.isMasterClient); if (players[0] == null) { PhotonPlayer[] p = PhotonNetwork.playerList; for (int i = 0; i < p.Length; i++) players[i] = p[i].TagObject as GameObject; } Debug.Log(players[0]); }

For reference this code is run on an object spawned after the scene is loaded and the players have spawned. The code I am using to spawn the object that has the above code is below (in case it is needed):

if (PhotonNetwork.isMasterClient) { for (int k = 0; k < enemyPrefabs.Length; k++) { for (int i = 0; i < initialPoolCount; i++) { EnemyAI ai = PhotonNetwork.Instantiate(enemyPrefabs[k].name, Vector3.one * 100, Quaternion.identity, 0).GetComponent<EnemyAI>(); enemiesPool.Add(ai); ai.DisableEnemy(); } } poolManager.ActivateRandomEnemy(new Vector3(0, 1, 20)); }

Any ideas what may cause this issue?

Thanks

Best Answer

Answers

  • Hi @Vallar,

    I'm guessing that players should be a list / an array which contains all player objects, am I right? If so, you are trying to assign the TagObject from PhotonNetwork.playerList to your custom list / array and then print the object at index 0 from that list / array, which logs NULL to the console. So as far as I can see it seems that this TagObject is not set, when you try to access it.

    Please let me know, if you correctly assign the TagObject. You can do this for example in the Game Object's Awake or Start function or in the called OnPhotonInstantiate callback. Assigning the object should look similar to this: photonView.owner.TagObject = gameObject;.

    However there might be another problem as well: PhotonNetwork.playerList is not synchronized across the clients, which means that the order of the clients listed in this array is not the same on all clients. Please check if this is a problem, after the other issue is solved.
  • Hi @Vallar,

    I'm guessing that players should be a list / an array which contains all player objects, am I right? If so, you are trying to assign the TagObject from PhotonNetwork.playerList to your custom list / array and then print the object at index 0 from that list / array, which logs NULL to the console. So as far as I can see it seems that this TagObject is not set, when you try to access it.

    players is indeed an array of GameObject and it is assigned to the length of player connected when the object is spawned (after all players are connected and spawned).

    If the TagObject isn't assigned why does it get assigned when testing with Master Client only (no other clients are connected) and when I test again but with more clients, it doesn't get assigned? If the assigning doesn't work, shouldn't that be more of a consistent issue?


    Please let me know, if you correctly assign the TagObject. You can do this for example in the Game Object's Awake or Start function or in the called OnPhotonInstantiate callback. Assigning the object should look similar to this: photonView.owner.TagObject = gameObject;.

    Yes, I believe I am correctly assigning the .TagObject value. The current method I am using is this:

    public override void OnPhotonInstantiate(PhotonMessageInfo _info) { _info.sender.TagObject = gameObject; }

    However, I did try the photonView.owner.TagObject = gameobject instead of _info.sender.TagObject = gameObject; and the same issue persists with it.


    However there might be another problem as well: PhotonNetwork.playerList is not synchronized across the clients, which means that the order of the clients listed in this array is not the same on all clients. Please check if this is a problem, after the other issue is solved.

    What other issue? Also why isn't the list synced? Wouldn't it make sense to have the list synced across all clients?

    Either way the code above in the OP runs only on Master Client as displayed so it is run on the same client not a different one. So by my understanding players[0] should always be the Master Client (on the Master Client at least) no matter what, since it is always the first client that gets added to the list, no?

    I even tried adding if (photonView.isMine == false) return; above the _info.sender.TagObject line just in case it maybe overwritten or something but it didn't help.
  • Bump, still looking for a solution to this problem.
  • Can you confirm, that OnPhotonInstantiate actually gets called? I have a test scenario similar to your case which works fine.

    This code is on the instantiated object:
    public void OnPhotonInstantiate(PhotonMessageInfo info)
    {
        photonView = GetComponent<PhotonView>();
        photonView.owner.TagObject = gameObject;
    }
    And I'm logging the player objects to the console from OnGUI:
    if (GUILayout.Button("Print to Console"))
    {
        foreach (PhotonPlayer p in PhotonNetwork.playerList)
        {
            Debug.LogWarning(p.TagObject.ToString());
        }
    }
    So by my understanding players[0] should always be the Master Client (on the Master Client at least) no matter what, since it is always the first client that gets added to the list, no?


    No and I honestly don't know the reason why. Internally the players in the room are stored in a Dictionary by ID and PhotonPlayer object. KeyValuePairs are added by using dict[id] = object;. The values (PhotonPlayer objects) are then copied to a List which can be accessed by using PhotonNetwork.playerList. If you want to you can take a look at this yourself. All the logic related to this can be found in the NetworkingPeer class.
  • Can you confirm, that OnPhotonInstantiate actually gets called? I have a test scenario similar to your case which works fine.

    Interesting results when I tested. I put a Debug.Log() in the OnPhotonInstantiate above the TagObject assignment like so:

    Debug.Log("OnPhotonInstantiate", gameObject);

    The code runs on both client and Master Client twice. So on Master Client it runs for the Master Client's player and the normal client's player (I have 1 master and 1 regular client connected for testing). On client the same exact thing happens.

    The weird thing is that OnPhotonInstantiate for one of the objects runs BEFORE the playerList code I am using. For the other OnPhotonInstantiate runs AFTER the code is run initially (code runs twice). This happens on both Master Client and client. This also produces null on 4 occasions (Master Client, Client, first run of code including playerList and second run of code including playerList).

    I even thought about (something stupid) tagObject being overwritten or something (i.e. OnPhotonInstantiate being run multiple times on the same client) so I added a isMine == false then return; check before it (despite feeling it is pointless) and I am still getting null.


    No and I honestly don't know the reason why. Internally the players in the room are stored in a Dictionary by ID and PhotonPlayer object. KeyValuePairs are added by using dict[id] = object;. The values (PhotonPlayer objects) are then copied to a List which can be accessed by using PhotonNetwork.playerList. If you want to you can take a look at this yourself. All the logic related to this can be found in the NetworkingPeer class.

    Honestly, I don't like to play with what I don't understand and I doubt I know enough about networking to poke around so I'll leave it at that and write it off under "PUN does it this way /shrug". Thanks for pointing out where it is though, perhaps when I learn more about networking and PUN I could go in and investigate it further.
  • Can you confirm, that OnPhotonInstantiate actually gets called? I have a test scenario similar to your case which works fine.

    So I added a Debug.Log() right before the TagObject assignment like so:

    Debug.Log("OnPhotonInstantiate", gameObject);

    What happens is that the log is printed twice per client; twice on Master Client and twice on regular client (I use 1 master and 1 regular for testing).

    The weird thing is that one of the prints happen right BEFORE the code with PhotonNetwork.playerList (one in the OP) is run the first time and the other print happens AFTER the code is run the first time and before the second time.

    So it is more like Print1 > OP's code > Print2 > OP's code. This is the same on both Master Client and client.

    Op's code in both cases (Master Client and client) in both prints produce null.


    No and I honestly don't know the reason why. Internally the players in the room are stored in a Dictionary by ID and PhotonPlayer object. KeyValuePairs are added by using dict[id] = object;. The values (PhotonPlayer objects) are then copied to a List which can be accessed by using PhotonNetwork.playerList. If you want to you can take a look at this yourself. All the logic related to this can be found in the NetworkingPeer class.

    Honestly, I am not quite an advanced networking programmer as you might have guessed and I don't like to play with what I don't understand. I'll just write this off under "PUN does it this way /shrug" for now and maybe later take a look at it when I feel more confident with my abilities. Thanks for pointing it out :)
  • Bump, still looking for a solution to this.

    Sorry about the double replies. For some reason when editing the first post the thread didn't show it at all so I wrote the second reply. This happened quite a few times. For some reason the forum thinks editing = deleting sometimes.
  • The weird thing is that one of the prints happen right BEFORE the code with PhotonNetwork.playerList (one in the OP) is run the first time and the other print happens AFTER the code is run the first time and before the second time.


    On the same object, OnPhotonInstantiate is called before Unity's Awake function and furthermore before the OnEnable function. So the first two logs are from the OnPhotonInstantiate callback and the OnEnable function of the local player's object. Afterwards the same is printed for the remote player, because the Instantiation event needs to be sent from the remote client to the local client, which takes some time. This explains the order of the logs in the console.

    However, if OnPhotonInstantiate has been called on both objects, the TagObjects should have been set accordingly and there shouldn't be any NULL-Reference when accessing them in the OnEnable function.
  • The weird thing is that one of the prints happen right BEFORE the code with PhotonNetwork.playerList (one in the OP) is run the first time and the other print happens AFTER the code is run the first time and before the second time.


    On the same object, OnPhotonInstantiate is called before Unity's Awake function and furthermore before the OnEnable function. So the first two logs are from the OnPhotonInstantiate callback and the OnEnable function of the local player's object. Afterwards the same is printed for the remote player, because the Instantiation event needs to be sent from the remote client to the local client, which takes some time. This explains the order of the logs in the console.

    However, if OnPhotonInstantiate has been called on both objects, the TagObjects should have been set accordingly and there shouldn't be any NULL-Reference when accessing them in the OnEnable function.
    Ah, thanks for the explanation that makes sense with the log order.

    Regarding the null reference... what can I do to resolve/investigate this further?
  • [Deleted User]
    edited August 2018
    Regarding the null reference... what can I do to resolve/investigate this further?


    Since I'm currently out of ideas and couldn't reproduce the issue on my own, is there any chance, that you can create a repro case for me, so that I can have a closer look at the problem?
  • Regarding the null reference... what can I do to resolve/investigate this further?


    Since I'm currently out of ideas and couldn't reproduce the issue on my own, is there any chance, that you can create a repro case for me, so that I can have a closer look at the problem?
    @Christian_Simon Sure, here is the link for the test case. I tried to make it as bare minimum as possible.

    Just simple instructions on how to test (so you don't have to run through the code):
    1. Open the project in Unity Editor.
    2. Build & Run the game.
    3. Create a new room by typing a room name on the left side of the GUI you see (preferably on the Master Client so you get the debug messages).
    4. On the build out version hit Refresh (top right) and hit the long rectangle button in the Rooms list that will appear.
    5. For a second a loading screen will appear. After which you'll see some grey. That is fine, like I said bare minimum so only empty game objects are there.
    6. Look at the console in the editor and you'll see 4 debug messages two are set as "True" (these testing the isMasterClient bool) and two are "Null" these are testing the playerList[0] (not using like this just typing it here like this to give an idea).
    Let me know if there is anything you need or found anything.

    Thank you very much in advance.
  • Thank you for sharing the project.

    The problem with this project is, that you never set any TagObject and therefore the Debug.Log calls always print null to the console. I'm not sure, if you have removed setting the TagObject accidentally, so please let me know.

    However after adding this call on my own, I'm still seeing the reported problem. The good thing is, that I know what happens and how to avoid it.

    One problem is, that the MasterClient is not the first client in the PhotonNetwork.playerList when there is at least one other player in the room. Again: I don't know why this happens, I don't know why the second client is added at the beginning of the list (or the dictionary what it is internally). However this only happens with the second client who joins the room. All other clients are added at the end of the list (or dictionary) as far as I have noticed. Back to the problem: when you call Debug.Log(players[0]);, you always get the TagObject of the second client and not the MasterClient. I know that this happens accidentally. When calling this Debug.Log, the TagObject of the second client is still null, because his object is not instantiated on the first client at this time. The reason therefore is most likely the delay between sending the Instantiation call on the second client and receiving this call on the first client.

    Summing up: the first client instantiates his object, then all enemies are created and the 'problematic' code is run, then the second client's instantiation call is received by the first client.

    How to avoid the problem: I have simply done that by delaying the instantiation of the enemies by adding yield return new WaitForSeconds(2.0f); before calling poolManager.PoolEnemies(); in the InstantiateObjectDelayed function.

    Keep in mind, that the above mentioned Debug.Log calls will print the GameObject of the second client due to the order of the list.
  • Thank you for sharing the project.

    The problem with this project is, that you never set any TagObject and therefore the Debug.Log calls always print null to the console. I'm not sure, if you have removed setting the TagObject accidentally, so please let me know.

    @Christian_Simon Completely sorry, wasn't intentional I was trying to strip the project from EVERYTHING unrelated so it is easier for you to check. That is all. I apologize.


    However after adding this call on my own, I'm still seeing the reported problem. The good thing is, that I know what happens and how to avoid it.

    One problem is, that the MasterClient is not the first client in the PhotonNetwork.playerList when there is at least one other player in the room. Again: I don't know why this happens, I don't know why the second client is added at the beginning of the list (or the dictionary what it is internally). However this only happens with the second client who joins the room. All other clients are added at the end of the list (or dictionary) as far as I have noticed. Back to the problem: when you call Debug.Log(players[0]);, you always get the TagObject of the second client and not the MasterClient. I know that this happens accidentally. When calling this Debug.Log, the TagObject of the second client is still null, because his object is not instantiated on the first client at this time. The reason therefore is most likely the delay between sending the Instantiation call on the second client and receiving this call on the first client.

    Summing up: the first client instantiates his object, then all enemies are created and the 'problematic' code is run, then the second client's instantiation call is received by the first client.

    How to avoid the problem: I have simply done that by delaying the instantiation of the enemies by adding yield return new WaitForSeconds(2.0f); before calling poolManager.PoolEnemies(); in the InstantiateObjectDelayed function.

    Keep in mind, that the above mentioned Debug.Log calls will print the GameObject of the second client due to the order of the list.

    I see what you are saying but given that it is ALWAYS the second client that is having the problem wouldn't you say that this should be something Photon looks into? It isn't normal that one clients gets put into the beginning of the dictionary (not Master Client) and everything else is put in its correct order. Looks to me like a bug. Moreover, the work around doesn't feel great.

    I am not judging. I am just saying maybe this is grounds to check if it could be fixed in PUN's internals. After all I am not THAT advanced of a user so maybe my opinion is just wrong.

    Either way, thank you very much for the solution. I'd like to ask you another question though that may help me resolve this in a different way from the WaitForSeconds(). Is there a way that Photon can tell me when all players have finished spawning over the network?
  • Bump, still looking for a solution to my last question.

  • To add a new player to the player list internally, this.mActors[ID] = player; is used. You can look this up in the NetworkingPeer class. Since this is a 'standard' C# call, I wouldn't say that this is a bug. At least it doesn't seem to be a bug in Photon, as you can probably reproduce this in a .NET console application as well. I have also tried to do another call (this.mActors.Add(ID, player);) which ended in the same result.

    I see, it just felt weird that the second client gets the first position in an array. So thought perhaps it is bug in how Photon was setup and could be changed. Given what you said, I doubt that will happen -- perhaps it is something in the C# version Unity is using or something. Anyway, thanks for explaining really.



    Yes, you could do this with either custom events, the Custom Player Properties or by simply counting the OnPhotonInstantiate calls.

    Let's start with the last option: whenever OnPhotonInstantiate is called on an object instantiated by a player, the MasterClient can increase a global counter. If this counter is equal to the number of players in the room, you know that each client instantiated his object. This honestly isn't a very elegant solution and might break in terms of disconnecting players for example, but it would work for first prototyping.

    The second option is to use the RaiseEvent function. Since we already talked about this one, you already know the basics how this is working. If you want to use it for this behaviour, too, a client who just called PhotonNetwork.Instantiate can raise a certain event afterwards. When receiving this event on the MasterClient, he can again increase a global counter. In the end this option is similar to the first and basically has all the disadvantages, too.

    The third option is probably the best and most stable in terms of disconnecting players for example. Therefore a client can set a certain Custom Player Property when he has instantiated his object. Setting Custom Player Properties works by using PhotonNetwork.player.SetCustomProperties(Hashtable propertiesToSet);. Those Custom Player Properties are automatically synchronized across all clients. Whenever a player sets this properties, OnPhotonPlayerPropertiesChanged(object[] playerAndUpdatedProps) is invoked on all clients. The MasterClient can use this, to iterate through all clients' properties in order to check, if all of them are ready.

    For example:
    public void OnPhotonPlayerPropertiesChanged(object[] playerAndUpdatedProps)
    {
        PhotonPlayer player = playerAndUpdatedProps[0] as PhotonPlayer;
        Hashtable props = playerAndUpdatedProps[1] as Hashtable;
    
        if (PhotonNetwork.isMasterClient)
        {
            if (props.ContainsKey("PlayerIsReady"))
            {
                foreach (PhotonPlayer p in PhotonNetwork.playerList)
                {
                    if (!p.CustomProperties.ContainsKey("PlayerIsReady"))
                    {
                        return;
                    }
    
                    if (!(bool)p.CustomProperties["PlayerIsReady"])
                    {
                        return;
                    }
                }
    
                // Everybody is ready, we can start the game
            }
        }
    }


    I agree the last solution looks pretty good to be honest. May I ask where would you call OnPhotonPlayerPropertiesChanged()? Would I put that in let's say PlayerStats script? Or do I put it on a more "overview" object such as a GameManager? Also when is that called in terms of execution order? Would you say this called after OnPhotonPlayerInstantiated?

    Finally this checks if the player is ready using a key "PlayerIsReady" from what I understand, but where did you "add" the key? At which point?

    I did try to work with Custom Properties at one point and it didn't work out quite well and I threw all the code as I couldn't figure out how to set and get data (even with the documentation -- again a case of confusion). I'll try again and definitely would love to give it a go as this looks like how "SyncVars" are in PUN if I am not mistaken.
  • May I ask where would you call OnPhotonPlayerPropertiesChanged()? Would I put that in let's say PlayerStats script? Or do I put it on a more "overview" object such as a GameManager?


    Depends on. In this case you can add it to the GameManager as it is something 'game related'. If you use the Custom Player Properties later on for 'player related' data, you can either move it to another script or simply add another OnPhotonPlayerPropertiesChanged callback. You are not limited to just one of them.

    Also when is that called in terms of execution order? Would you say this called after OnPhotonPlayerInstantiated?


    I would say this depends on, which message gets sent first. If you really want to be sure about the execution order, you can Instantiate the object first and set the Custom Player Properties afterwards in the OnPhotonInstantiate callback (check the isMine condition here).

    Finally this checks if the player is ready using a key "PlayerIsReady" from what I understand, but where did you "add" the key? At which point?


    You do this, when setting the Custom Player Properties. See the following example:
    Hashtable ht = new Hashtable
    {
        { "PlayerIsReady", true }
    };
    
    PhotonNetwork.player.SetCustomProperties(ht);
    One hint here: if you are running into an 'Ambiguous reference' error, make sure that you are using the Hashtable from the ExitGames.Client.Photon namespace instead of the System.Collections one. You can do this by using using Hashtable = ExitGames.Client.Photon.Hashtable;.
  • May I ask where would you call OnPhotonPlayerPropertiesChanged()? Would I put that in let's say PlayerStats script? Or do I put it on a more "overview" object such as a GameManager?


    Depends on. In this case you can add it to the GameManager as it is something 'game related'. If you use the Custom Player Properties later on...
    Christian_Simons, you are a wizard! Thanks a lot for your help and explanation! You are an absolute and ace and I would give you raise if Photon admins are reading this. :D