Search Unity

Question How do I make a "Ready" button for a multiplayer game?

Discussion in 'Multiplayer' started by MusteA69, Jan 14, 2024.

  1. MusteA69

    MusteA69

    Joined:
    Dec 20, 2022
    Posts:
    4
    I want to make a multiplayer card game for a school project. The game has 6 turns and both players have to press "Ready" in order for the game to advance to the next turn (like Marvel Snap).

    Both players have a NetworkVariable<bool> attached to their prefab which becomes true after the button is pressed and the button becomes uninteractable until the start of the next turn.
    I had a lot of problems when trying to implement this so I made a simple test scene with just a button where all I wanted was to have the button increment a NetworkVariable if the button was pressed by the host and decrement if it was pressed by the client.
    The problem is that it doesn't update on either side and the NetworkObject component that's on the GameObject with the script for testing isn't spawned on the Host's side by default. I have to spawn it in the Inspector or via code and from what I understood from the documentation it should have spawned by default (it does for the other GameObjects and it is in the Network Manager prefab list like the others). This GameObject and the button are spawned in the scene, before (?) the players join.

    upload_2024-1-14_7-0-59.png

    This is what it looks after I spawn the NetworkObject component on the Host:

    upload_2024-1-14_7-17-0.png

    This is what it looks on the Client:
    (I get 4 errors with the default values)
    upload_2024-1-14_7-17-55.png upload_2024-1-14_7-24-15.png

    Here is the code I initially tested with:
    The button's onClick() is assigned the pressedBy() function.

    Code (CSharp):
    1. using Unity.Netcode;
    2. using UnityEngine;
    3.  
    4. public class ReadyButton : NetworkBehaviour
    5. {
    6.  
    7.     [SerializeField]
    8.     public NetworkVariable<int>test; //test integer
    9.    
    10.  
    11.     [ServerRpc(RequireOwnership =false)]
    12.     public void RpcExampleServerRpc(int type)
    13.     {
    14.         if (type==1)
    15.         {
    16.             Debug.Log("ServerRpc called by the host!!");
    17.             test.Value++;
    18.         }
    19.         else
    20.         {
    21.             Debug.Log("ServerRpc called by the client!");
    22.             test.Value--;
    23.         }
    24.     }
    25.     public void presedBy()
    26.     {
    27.         //initially tried with a ServerRpc but it only worked when called by the Host (and only incremented on the Host's side)
    28.         //couldn't make the Client's side work since it has to be on the Server
    29.         if(NetworkManager.IsServer)
    30.         {
    31.             test.Value++;
    32.             //RpcExampleServerRpc(1);
    33.            
    34.         }
    35.         else
    36.         {  
    37.             //this was a little different initially, if RpcExampleServerRpc(2) ran like it is now it would give a missing Dictionary error
    38.             test.Value--;
    39.             //RpcExampleServerRpc(2);
    40.         }
    41.        
    42.     }
    43.     void Start()
    44.     {
    45.        
    46.         if(NetworkManager.IsServer)
    47.         {
    48.             //Spawning the Host's NetworkObject Component
    49.             this.GetComponent<NetworkObject>().Spawn();
    50.             test.Value=0;
    51.         }
    52.        
    53.     }
    54.  
    55. }
    56.  
    It didn't work so I tried this code (which also doesn't work):
    The button's onClick() is assigned the onPress() function.


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using Unity.Netcode;
    4. using UnityEngine;
    5.  
    6. public class NetVar : NetworkBehaviour
    7. {
    8.     [SerializeField]
    9.     public NetworkVariable<int> number;
    10.  
    11.  
    12.     public void onPress()
    13.     {
    14.        
    15.         number.Value++;
    16.        
    17.     }
    18. }
    19.  

    At this point I'm lost. I don't know what else to test or change to make the button work. I didn't have these problems when I made a ServerRpc button in the menu (which is the first scene) to test its functionality.
     
  2. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    660
    Is ReadyButton already in the scene, as it looks like it needs to be. Make liberal use of debug logging to see what's going on.

    As your player is spawning fine here's a quick example using that to trigger a change in the network bool variable.

    This class creates the connection and finds the correct local player.

    Code (CSharp):
    1.  public class ReadySceneController : MonoBehaviour
    2.     {
    3.         Player player;
    4.  
    5.         void Start()
    6.         {
    7.             Application.targetFrameRate = 15;
    8.  
    9.             NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
    10.  
    11.             if (!ParrelSync.ClonesManager.IsClone())
    12.             {
    13.                 NetworkManager.Singleton.StartHost();
    14.             }
    15.             else
    16.             {
    17.                 NetworkManager.Singleton.StartClient();
    18.             }
    19.         }
    20.  
    21.         private void OnClientConnected(ulong clientId)
    22.         {
    23.             Debug.Log("ReadySceneController OnClientConnected: " + clientId);
    24.  
    25.             if (NetworkManager.Singleton.IsHost)
    26.             {
    27.                 if(clientId == NetworkManager.ServerClientId)
    28.                 {
    29.                     player = NetworkManager.Singleton.LocalClient.PlayerObject.GetComponent<Player>();
    30.                 }
    31.             }
    32.             else
    33.             {
    34.                 player = NetworkManager.Singleton.LocalClient.PlayerObject.GetComponent<Player>();
    35.             }
    36.         }
    37.  
    38.         public void OnClickReady()
    39.         {
    40.             Debug.Log("ReadySceneController OnClickReady");
    41.  
    42.             player.IsReady = true;
    43.         }
    44.     }

    And the player class where the owner has permission to change the bool value and trigger the OnValueChanged call.

    Code (CSharp):
    1.     public class Player : NetworkBehaviour
    2.     {
    3.         [SerializeField] NetworkVariable<bool> isReady = new NetworkVariable<bool>(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
    4.  
    5.         private void Awake()
    6.         {
    7.             isReady.OnValueChanged += OnReadyChanged;
    8.         }
    9.  
    10.         private void OnReadyChanged(bool previousValue, bool newValue)
    11.         {
    12.             Debug.Log($"Player {NetworkObjectId} isReady: {newValue}");
    13.         }
    14.  
    15.         public bool IsReady { get => isReady.Value; set => isReady.Value = value; }
    16.     }
    Arguably using an rpc over client permission is the better option and it shouldn't take much to work that way instead.
     
  3. MusteA69

    MusteA69

    Joined:
    Dec 20, 2022
    Posts:
    4
    It works! Thank you very much.
     
    cerestorm likes this.
  4. MusteA69

    MusteA69

    Joined:
    Dec 20, 2022
    Posts:
    4
    Follow-up question: How do I make it so both players' isReady value becomes false after the turn counter increases?

    Code (CSharp):
    1. private IEnumerator PlayGame()
    2.     {
    3.         while (currentTurn.Value < Turns.Value)
    4.         {
    5.             yield return new WaitUntil(() => player1.isReady.Value==true && player2.isReady.Value==true); //works
    6.  
    7.             if(currentTurn.Value>Turns.Value)
    8.             {
    9.                 break;
    10.             }
    11.            
    12.             currentTurn.Value++;
    13.  
    14.             player1.isReady.Value=false; //works
    15.             player2.isReady.Value=false; //doesn't work
    16.          
    17.            
    18.             yield return new WaitForSeconds(1.0f);
    19.  
    20.         }
    21.  
    22.         Debug.Log("Game Over");
    23.     }
    24.  
    When I run this code it says: "InvalidOperationException: Client is not allowed to write to this NetworkVariable".
    I understand why it happens: The Player's NetworkVariableWritePermission is set to Owner and it can't be set to Everyone.
    I want to know how to fix this or a workaround. I tried ClientRpcs and I got a missing dictionary error.
     
  5. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    660
    Easiest option would be to remove the client permission and instead send an rpc when the client is ready and have the host change the isReady value itself. If you want to keep the client permission you could use a change in the currentTurn value to trigger resetting the ready value but I'd likely go with the first option.
     
    b4guw1x likes this.