Synchronizing weapon switch/weapon swap/weapon change

Options
Hello,

I am wondering, what is the best solution for synchronizing currently equipped weapon. The problem is mostly evident when new players join the game, after there have been many sweapon switches done already.

I have come up with 4 solutions:

1) Using buffered RPCs. This works, however it has the downside that if a new player joins the game, he does all the weapon switches for each player. This is time consuming and not acceptable since these weapon switches can accumulate to a huge amount. Especially when rooms are created in a map-cycle fashion where 1 room could be active for days. However, while writing this, I was wondering if it is possible to flush the buffer for certain methods? So that every time an RPC is sent, I could flush the method buffer and just add the latest change to the buffer. This means only 1 execution is done when a player connects.

2) Using PlayerCustomProperties. This has an upside that it is easy to use, however compared to the third solution, which I will provide, this will most likely use more bandwidth. The idea is to store current weapon's index in custom properties. When new player connects, I do a loop over all photonplayers and assign their current weapon properly. I see two downsides to this, however. One is that I feel that custom properties should hold more "important" values and that it goes against the design of custom properties. Secondly, I would capture the weapon change in OnPhotonPlayerPropertiesChanged(). However, this means that if my custom properties hold additional variables, these variables will be sent as well, increasing bandwidth usage. Since This function takes object[] as a paramater, including all the properties. Or am I wrong? Do only the updated properties get sent? If this is true then I could just add if checks/switch to see, which properties were changed and act accordingly. This would be an acceptable solution.

3) Using OnSerializeView(). This means, I would simply serialize an integer. This way, if new players connect, they will automatically get the new integer. However, I am wondering if this serialize function sends the values all the time? What if I wanted to serialize the integer change only when it changes locally? If this is doable then this brings me to the next problem. If new players connect then they don't get the updated integer. They will only get this when remote changes their weapon and serializes the integer. This reminds of an RPC and I cannot see much difference there.

4) Again, using RPC's. When a new player connects, I would send an RPC to every other player, asking for their weapon index. They would send me an RPC back. Or I could use the sent RPC to trigger the OnSerializeView() even though the remote player hasn't changed their weapon locally. There is a problem with this solution though:
1. I join the game, sending an RPC to request for remote's weapon index.
2. My RPC arrives at remote's machine. At the same time remote switches weapon, sending an RPC telling everyone to switch weapon.
3. The new weapon change RPC arrives earlier than the RPC that answers my initial request.
4. On my screen, remote's weapon is momentarily switched to the one that he changed the weapon to.
5. Milliseconds later, the response RPC to my request arrives, changing remote's weapon back to the old index.

What solutions have you used? Which ones do you recommend? I'm also interested in the answers to questions I asked in my solutions.

Thank you!

Comments

  • Frosty
    Options
    I first tried out the buffered RPCs solution. The problem that I got was the problem I described. Too many RPCs sent and executed when a player connects. This was not acceptable.

    Then I tried to use OnSerialize method. But since it really ripped my code structure apart, making me put variables here and there, the code quickly became messy. While it almost kind of worked, it coupled the architecture too much and I was not pleased.

    Finally, I implemented the CustomProperties solution. As only the changed variable is sent in the hashtable, bandwidth loss between this and OnSerialize() should not be big. I still believe that OnSerialize would use less bandwidth since it has to send only an integer in my case, rather than a hashtable, containing an integer. However, thanks to this solution, I was able to keep my architecture decoupled and seems like I got everything working.

    It would be nice if any of the devs could answer me if using custom properties solution is efficient and fast way to synchornize properties such as active weapon and health.

    Thank you!
  • Tobias
    Options
    I think that the solution with the Player Properties is best.
    Yes, the value is now part of a Hashtable but it's only sent when changed, instead of being sent all the time. Also, joining players get the value early.
    OnPhotonSerializeView() would send whatever you pass into the stream. It's only an int more than what you would send without this info (plus 1byte type info) but this is being sent all the time. 10 or more times/sec.
    Delta Compression would be a solution to that but eats performance.
    An RPC would be OK maybe, unless weapon changes are frequent. When someone joins, everyone would have to send the RPC though and the joining player doesn't know the weapon of anyone at first.

    I think the solution is fine.
    Except when you notice that you're flooding the network, you probably don't have to worry a lot about it.

    Do you know this page?
    https://doc.photonengine.com/en-us/pun/current/manuals-and-demos/synchronization-and-state
  • Bunzaga
    Options
    I also do this in the PlayerProperties. I went through your same discovery process as well, RPC first, but then went straight to PlayerProperites. I can post a snippet of my code, if you want to compare notes or whatever.
    
    private void onPhotonPlayerPropertiesChanged(PhotonPlayer aPhotonPlayer, ExitGames.Client.Photon.Hashtable aHash)
    {
        if(aPhotonPlayer.ID == PhotonNetwork.player.ID) { Debug.Log("photon player is the local player, just move along..."); return; }
    // I cache this locally when this class is created
        if (_photonView.ownerId == aPhotonPlayer.ID)
        {
            foreach (string key in aHash.Keys)
            {
                if (aHash[key] == null) { Debug.Log("the key is null."); continue; }
                Debug.Log("key is: " + key);
                switch (key)
                {
                    case "slot_RightHand":
                        ItemTemplate rightTemplate = TitleData.ItemTemplates.Where(x => x.Id == (string)aHash[key]).First();
                        WeaponInstance rightHand = new WeaponInstance((WeaponTemplate)rightTemplate);
                        rightHand.Equip(View);
                        break;
                    case "slot_LeftHand":
                        ItemTemplate leftTemplate = TitleData.ItemTemplates.Where(x => x.Id == (string)aHash[key]).First();
                        WeaponInstance leftHand = new WeaponInstance((WeaponTemplate)leftTemplate);
                        leftHand.Equip(View);
                        break;
                }
            }
        }
    }
    I use StrangeIOC, so when it refers to 'View', just kind of replace that with 'this'. Just remember to parse each current player when a new player joins. this is in my non-local client instantiation:
    ExitGames.Client.Photon.Hashtable aHash = PhotonPlayer.Find(_photonView.ownerId).customProperties;
                
    foreach (string key in aHash.Keys)
    {
        if (aHash[key] == null) { continue; }
        switch (key)
        {
            case "slot_RightHand":
                ItemTemplate rightTemplate = TitleData.ItemTemplates.Where(x => x.Id == (string)aHash[key]).First();
                WeaponInstance rightHand = new WeaponInstance((WeaponTemplate)rightTemplate);
                rightHand.Equip(View);
                break;
            case "slot_LeftHand":
                ItemTemplate leftTemplate = TitleData.ItemTemplates.Where(x => x.Id == (string)aHash[key]).First();
                WeaponInstance leftHand = new WeaponInstance((WeaponTemplate)leftTemplate);
                leftHand.Equip(View);
                break;
        }
    }
  • Frosty
    Options
    Hey,

    Tobias, yes I've read the page, however the problem was that weapon change is somewhere between the frequent and infrequent update. While players play the game normally, it could be considered as an infrequent update, if they go crazy and swap the weapons in a rapid succession (for whatever reason) then more of a frequent update.

    Bunzaga, the code I came up with is this:

    public void OnPhotonPlayerPropertiesChanged(object[] playerAndUpdatedprops)
    {
    PhotonPlayer player = playerAndUpdatedprops[0] as PhotonPlayer;
    ExitGames.Client.Photon.Hashtable props = playerAndUpdatedprops[1] as ExitGames.Client.Photon.Hashtable;

    int leftWeaponIndex = -1;
    int rightWeaponIndex = -1;

    if (props["leftWeaponIndex"] != null) { leftWeaponIndex = (int)props["leftWeaponIndex"]; }
    if (props["rightWeaponIndex"] != null) { rightWeaponIndex = (int)props["rightWeaponIndex"]; }

    if (PlayerManager.instance.GetPlayerById(player.ID) == null || player == PhotonNetwork.player) { return; }
    if (leftWeaponIndex >= 0) { PlayerManager.instance.GetPlayerById(player.ID).leftHand.ChangeWeaponRemote(leftWeaponIndex, true); }
    if (rightWeaponIndex >= 0) { PlayerManager.instance.GetPlayerById(player.ID).rightHand.ChangeWeaponRemote(rightWeaponIndex, false); }
    }