How to use Actions correctly in multiplayer games ?

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.)

    private void OnEnable()
    {
// HasPlayerEnoughMoney is the bool NetworkVariable which the server sets to true if player has enough money
        playerMoneyHandler.HasPlayerEnoughMoney.OnValueChanged += AllowPurchase;
    }
    private void OnDisable()
    {
        playerMoneyHandler.HasPlayerEnoughMoney.OnValueChanged -= AllowPurchase;
    }
    private void AllowPurchase(bool oldValue, bool newValue)
    {
        if (!IsOwner) { return; }
        // IF ENOUGH MONEY TO BUY
        if (newValue)
        {
            // THIS IS THE ISSUE : all players access this section ?? and call action on their side ?
            ActionsPlayer.OnItemPurchased?.Invoke(lastCollectibleConsidered); // ISSUE

            // SIMPLE FUNCTION CALLS : SOLUTION 
            //playerMoneyHandler.AddMoney(-lastCollectibleConsidered.CollectibleValue);
            //playerMagickHandling.AddMojosRecievedOnCollecting(lastCollectibleConsidered);
            //playerInventoryManager.AddCollectibleInInventory(lastCollectibleConsidered);
            //playerAbilitiesHandler.AddCollectedScroll(lastCollectibleConsidered);
        }
        else
        {
            if (Debug.isDebugBuild) { print("NOT ENOUGH MONEY : "); }
        }
    }

// INPUT : left mouse click -> confirm buy  
private void PlayerWantsToConfirmBuy(InputAction.CallbackContext callbackContext)
    {
// SETS NETWORKVARIABLE VALUE TO TRUE IF PLAYER HAS ENOUGH MONEY
        if (IsOwner) playerMoneyHandler.HasPlayerEnoughMoneyToBuy(lastCollectibleConsidered.CollectibleValue);
    }

this is for example one of the methods that subscribes to OnItemPurchased :

    private void OnEnable()
    {
        Money.OnValueChanged += OnMoneyChanged;
        ActionsPlayer.OnItemPurchased+= PayCollectibleValue;
    }

    private void OnDisable()
    {
        Money.OnValueChanged -= OnMoneyChanged;
        ActionsPlayer.OnItemPurchased-= PayCollectibleValue;

    private void AddMoney_Network(float amount)
    {
        if (Money.Value + amount <= 0.0f) { Money.Value = 0.0f; }
        else { Money.Value += amount; }
    }

    public void AddMoney(float amount)
    {
        if (IsServer)
        {
            AddMoney_Network(amount);
        }
        else
        {
           if (IsOwner) ManageMoneyOnServerRpc(amount, 1);
        }
    }
    private void PayCollectibleValue(CollectibleScriptableObject collectible)
    {
        AddMoney(-collectible.CollectibleValue);
    }
    }

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.

1 Like

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 RPC vs NetworkVariable | Unity Multiplayer Networking) you don’t need state tracking over multiple frames for this.

2 Likes

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 :

    public static Action<GameObject, GameObject> OnPlayerKilled; // playerKilled, killer

from ONE of my PLAYER’S SCRIPT. playerHealth script for example.
Something like this idk :

    private void OnEnable()
    {
        ActionsPlayer.OnPlayerKilled += OnPlayerKilled;
    }

    private void OnDisable()
    {
        ActionsPlayer.OnPlayerKilled -= OnPlayerKilled;
    }

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 :

        ActionsPlayer.OnPlayerKilled?.Invoke(player1, player2);

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 :
8874957--1212003--upload_2023-3-14_11-1-20.jpg

AND this is what I WANTED :

8874957--1212006--upload_2023-3-14_11-1-49.jpg

SOLUTION :
This is what I do now, if I want only the player1 reading this OnPlayerKilled method :

    private void OnPlayerKilled(GameObject player1, GameObject player2)
    {
        if (!ReferenceEquals(player1, gameObject)) { return; }
        (...)
    }

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.

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).