Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

Question How to use Actions correctly in multiplayer games ?

Discussion in 'Netcode for GameObjects' started by edin97, Sep 17, 2022.

  1. edin97

    edin97

    Joined:
    Aug 9, 2021
    Posts:
    58
    Hello,
    I'm having trouble using Actions for my multiplayer game.

    I have a shop, on item purchased I want to invoke a unity Action so my other classes can do their job on item purchased. Before confirming the purchase, I ask the server if the player has enough money, since Rpc's always return void, I'm using a bool NetworkVariable (HasPlayerEnoughMoney) , which the server sets to true if the player has enough money. If the player has enough money, I send an Action OnItemPurchased. Issue is : I subscribe to this HasPlayerEnoughMoney.OnValueChanged and OnItemPurchased with every player. So for some reason when the host buys an item, it deducts money from other players too. (clients buying doesn't take money from server) BUT, replacing the OnItemPurchased Action invoke by simple functions calls, works. no issues.

    Here are the simple methods : Both scripts are on a player -> if we have 2 players, each script will be read 4 times here (owner+copy on both sides.)
    Code (CSharp):
    1.    
    2.     private void OnEnable()
    3.     {
    4. // HasPlayerEnoughMoney is the bool NetworkVariable which the server sets to true if player has enough money
    5.         playerMoneyHandler.HasPlayerEnoughMoney.OnValueChanged += AllowPurchase;
    6.     }
    7.     private void OnDisable()
    8.     {
    9.         playerMoneyHandler.HasPlayerEnoughMoney.OnValueChanged -= AllowPurchase;
    10.     }
    11.     private void AllowPurchase(bool oldValue, bool newValue)
    12.     {
    13.         if (!IsOwner) { return; }
    14.         // IF ENOUGH MONEY TO BUY
    15.         if (newValue)
    16.         {
    17.             // THIS IS THE ISSUE : all players access this section ?? and call action on their side ?
    18.             ActionsPlayer.OnItemPurchased?.Invoke(lastCollectibleConsidered); // ISSUE
    19.  
    20.             // SIMPLE FUNCTION CALLS : SOLUTION
    21.             //playerMoneyHandler.AddMoney(-lastCollectibleConsidered.CollectibleValue);
    22.             //playerMagickHandling.AddMojosRecievedOnCollecting(lastCollectibleConsidered);
    23.             //playerInventoryManager.AddCollectibleInInventory(lastCollectibleConsidered);
    24.             //playerAbilitiesHandler.AddCollectedScroll(lastCollectibleConsidered);
    25.         }
    26.         else
    27.         {
    28.             if (Debug.isDebugBuild) { print("NOT ENOUGH MONEY : "); }
    29.         }
    30.     }
    31.  
    32. // INPUT : left mouse click -> confirm buy  
    33. private void PlayerWantsToConfirmBuy(InputAction.CallbackContext callbackContext)
    34.     {
    35. // SETS NETWORKVARIABLE VALUE TO TRUE IF PLAYER HAS ENOUGH MONEY
    36.         if (IsOwner) playerMoneyHandler.HasPlayerEnoughMoneyToBuy(lastCollectibleConsidered.CollectibleValue);
    37.     }
    38.  
    this is for example one of the methods that subscribes to OnItemPurchased :
    Code (CSharp):
    1.  
    2.     private void OnEnable()
    3.     {
    4.         Money.OnValueChanged += OnMoneyChanged;
    5.         ActionsPlayer.OnItemPurchased+= PayCollectibleValue;
    6.     }
    7.  
    8.     private void OnDisable()
    9.     {
    10.         Money.OnValueChanged -= OnMoneyChanged;
    11.         ActionsPlayer.OnItemPurchased-= PayCollectibleValue;
    12.  
    13.     private void AddMoney_Network(float amount)
    14.     {
    15.         if (Money.Value + amount <= 0.0f) { Money.Value = 0.0f; }
    16.         else { Money.Value += amount; }
    17.     }
    18.  
    19.     public void AddMoney(float amount)
    20.     {
    21.         if (IsServer)
    22.         {
    23.             AddMoney_Network(amount);
    24.         }
    25.         else
    26.         {
    27.            if (IsOwner) ManageMoneyOnServerRpc(amount, 1);
    28.         }
    29.     }
    30.     private void PayCollectibleValue(CollectibleScriptableObject collectible)
    31.     {
    32.         AddMoney(-collectible.CollectibleValue);
    33.     }
    34.     }
    Can someone tell me what I'm doing wrong here please ? I would prefer using actions so I don't have to make my methods public. Also I don't understand how the action is been called from 2 different players since I use an input to ask the server if the player has enough money. Input should be read only on the owner player since the copy doesn't recieve inputs ? Thanks in advance.
     
    firebird721 likes this.
  2. SamuelBellomoUnity

    SamuelBellomoUnity

    Unity Technologies

    Joined:
    Sep 24, 2020
    Posts:
    33
    Is this a client hosted game or a dedicated game server? Cause you usually don't want to have monetization controlled by a client, that'd be exposed to cheating super easily. You'd want a service for this or use a DGS.
    If on DGS, netvars are replicated by default to all clients. so a netvar change server side will get replicated to all clients. you'd either want to change your read permissions on the netvar or use a client RPC. For a transitory event like this, I'd use a client RPC (see this page https://docs-multiplayer.unity3d.com/netcode/current/learn/rpcvnetvar) you don't need state tracking over multiple frames for this.
     
    REDACT3D_ and CreativeChris like this.
  3. edin97

    edin97

    Joined:
    Aug 9, 2021
    Posts:
    58
    Hello, I have no idea what you are talking about brother, but thanks for the reply !

    I might have explained myself wrong. I was talking about fake currency from a fake in game shop. which I was invoking an action (unity action/event or whatever they are called) when player confirmed his purchase.

    I figured the issue long time ago but forgot about this thread. Since someone liked it recently, maybe they had the same issue as me. I'm going to write a detailed example in hopes it helps someone in the future.

    Here is what the problem was. As a beginner in multiplayer/networking coding I forgot that you have your player spawned on your build/game but also the other clients player. So, when subscribing to an Action FOR EXAMPLE :
    Code (CSharp):
    1.     public static Action<GameObject, GameObject> OnPlayerKilled; // playerKilled, killer
    2.  
    from ONE of my PLAYER'S SCRIPT. playerHealth script for example.
    Something like this idk :
    Code (CSharp):
    1.  
    2.     private void OnEnable()
    3.     {
    4.         ActionsPlayer.OnPlayerKilled += OnPlayerKilled;
    5.     }
    6.  
    7.     private void OnDisable()
    8.     {
    9.         ActionsPlayer.OnPlayerKilled -= OnPlayerKilled;
    10.     }
    BUT. Since this script is on A PLAYER, if I instantiate my player then instantiate the other client's player, BOTH players have the playerHealth script attached to them, so all the players on my side (with that script) will be subscribed to "ActionsPlayer.OnPlayerKilled" and that's the issue. If you trigger this action ON YOUR SIDE, something like this :
    Code (CSharp):
    1.         ActionsPlayer.OnPlayerKilled?.Invoke(player1, player2);
    2.  
    You end up with your player calling OnPlayerKilled, but, also all the other client's player calling it.

    Here
    I drew it : THIS IS WHAT WAS HAPPENING :
    upload_2023-3-14_11-1-20.png


    AND this is what I WANTED :

    upload_2023-3-14_11-1-49.png

    SOLUTION :
    This is what I do now, if I want only the player1 reading this OnPlayerKilled method :
    Code (CSharp):
    1.     private void OnPlayerKilled(GameObject player1, GameObject player2)
    2.     {
    3.         if (!ReferenceEquals(player1, gameObject)) { return; }
    4.         (...)
    5.     }
    Since this script is on both player1 AND player2, "gameObject" for player1 will be... player1 ! and "gameObject" for player2 will be ... player2 ! So player2 will return and player1 will read the method. I always give some kind of "id" when calling an action/event so the concerned GameObject can read the function and all the others just return. (in this case I give the "gameObject" itself as the "ID" and check with ReferenceEquals function.)

    Hope this helps someone bc I was inexperienced too, and this solution might not even be the most performant, but I would have loved someone giving it to me or having a clear example of what is going on with actions/events in a multiplayer scene.
     
    Last edited: Mar 14, 2023
  4. RikuTheFuffs-U

    RikuTheFuffs-U

    Unity Technologies

    Joined:
    Feb 20, 2020
    Posts:
    440
    The right way is to handle the money checks + subtraction on the server through a NetworkVariable, then sending all clients a ClientRPC that calls a client-only event on all clients (or on selected ones through ClientRpcParams, depending on your use case).