Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Discussion Clients not accessing Network variables on Dynamically spawned network prefabs

Discussion in 'Netcode for GameObjects' started by verteal, Jul 18, 2023.

  1. verteal

    verteal

    Joined:
    Feb 18, 2022
    Posts:
    4
    Using Netcode for Game Objects, I'm not able to give clients access to variables that are attached to dynamically spawned prefabs. The game I'm designing is a multiplayer game with votes. I want all players to be able to see a GUI with buttons and press one of two buttons to cast a vote. Every minute or so I want to spawn an instance of a networked prefab (a vote) and create a GUI interface on the client side with buttons for each player. All players should see 2 buttons (let's call them buttonA & buttonB) that link to functions that increment corresponding network vars netVarA and netVarB (these variables are used as counters for the voting).

    I can make everything work great for both the host/server and clients I hardcode several game objects (one for each of the in-game votes), each with its own GUI interface and each with a pair of networked variables. In this fully working case, the host and clients all have the same info and everyone can change the variables which are instantly updated as they should be (see code below). However from a clean code perspective, this is inefficient, so I'm trying to make one dynamic networked prefab, spawn clones of the prefab, store those clones as children in a container object on the server (so that I can cycle through the clones like a list), and then access the information that is on the server from the clients (or duplicate the information using rpc).

    When I try this approach, the server side is perfect - but clients don't have access to the networked variables and I can't figure out how to update clients on the information the server/host has as it changes. I'm not sure how clients get access to the networked variables when things are hard-coded but don't when dynamically spawned prefabs are used. I've tried a combination of ClientRpc and ServerRpc calls and have not been able to fix this issue. I'd love to see a similar worked example if any exists.

    The code:
    This code works great if it's attached to every game object when the game objects are hard-coded into the game scene:

    Code (CSharp):
    1.  
    2. public NetworkVariable<int> OptA = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
    3. public NetworkVariable<int> OptB = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
    4.  
    5.    public override void OnNetworkSpawn()
    6.     {
    7.         OptA.OnValueChanged += (int previousValue, int newValue) => {};        
    8.         OptB.OnValueChanged += (int previousValue, int newValue) => {};
    9.     }
    10.  
    11.     public override void OnNetworkDespawn()
    12.     {
    13.         OptA.OnValueChanged -= (int previousValue, int newValue) => {};        
    14.         OptB.OnValueChanged -= (int previousValue, int newValue) => {};
    15.     }
    16.  
    17.     public void Start()
    18.     {
    19.         eventNetworkObject = this.GetComponent<NetworkObject>();
    20.     }
    21.  
    22. // The display of the current votes for the votes
    23.     void Update()
    24.     {
    25.         GUIvoteTotals.text = $"A: {this.OptA.Value} / B: {this.OptB.Value}";
    26.     }
    Code (CSharp):
    1.     public void SupportOptA()
    2.     {  
    3.         var playerObject = NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject();
    4.         var player = playerObject.GetComponent<HelloWorldPlayer>();
    5.        
    6.         if (player.SpendOneInfluence())
    7.         {
    8.             OptA_ServerRpc();
    9.         }
    10.     }
    11.  
    12.     [ServerRpc (RequireOwnership = false)]
    13.     public void OptA_ServerRpc() {
    14.         OptA.Value ++;
    15.     }
    16.  
    17.     // Increment B++
    18.     public void SupportOptB()
    19.     {
    20.         var playerObject = NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject();
    21.         var player = playerObject.GetComponent<HelloWorldPlayer>();
    22.  
    23.         if (player.SpendOneInfluence())
    24.         {
    25.             OptB_ServerRpc();
    26.         }
    27.      }
    28.  
    29.     [ServerRpc (RequireOwnership = false)]
    30.     public void OptB_ServerRpc()
    31.     {
    32.         OptB.Value ++;
    33.     }
    When I try to replace hardcoded game 'events/votes' with a prefab for the 'event/vote', I have an eventManager that includes this code, which does spawn events/votes, but only has info showing on those events on the server:

    Code (CSharp):
    1.     private void CreateEvent() {
    2.         Transform eventPrefabTransform = Instantiate(eventPrefab, eventPrefabContainer);
    3.         NetworkObject eventPrefabNetworkObject = eventPrefabTransform.GetComponent<NetworkObject>();
    4.         eventPrefabNetworkObject.Spawn(true);
    5.     }
    6.  
    7.     [ServerRpc]
    8.     private void CreateEvent_ServerRpc() {
    9.         CreateEvent_ClientRpc();
    10.     }
    11.  
    12.     [ClientRpc]
    13.     private void CreateEvent_ClientRpc() {
    14.         Transform eventPrefabTransform = Instantiate(eventPrefab, eventPrefabContainer);
    15.         NetworkObject eventPrefabNetworkObject = eventPrefabTransform.GetComponent<NetworkObject>();
    16.         eventPrefabNetworkObject.Spawn(true);
    17.     }
    Thanks for any insights :)
     
  2. lavagoatGG

    lavagoatGG

    Joined:
    Apr 16, 2022
    Posts:
    226
    When you call CreateEvent() on the server do the clients get a network object spawned for them? When they click button A, does SupportOptA() function gets called?
     
  3. verteal

    verteal

    Joined:
    Feb 18, 2022
    Posts:
    4
    Clicking buttons only works on the host. I can use ClientRpc to change client side 'events/votes' to have the info that the host has at the start, but even when I experiment with Rpc calls, clicking buttons A & B never works on the client side, unless I don't use a prefab and manually create/hard-code multiple one off game objects, one for each event/vote. I'm going to play around with a new approach, making a networked list of a custom data type. I'll post how that goes.
     
  4. verteal

    verteal

    Joined:
    Feb 18, 2022
    Posts:
    4
    NetworkVariable<NetworkObjectReference> might be a solution for me to access the host/server state for each prefab, I just didn't find a working example of how to implement that.
     
  5. verteal

    verteal

    Joined:
    Feb 18, 2022
    Posts:
    4
  6. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    648
    I had a go at creating a solution for this, or at least solve the main problems. The code's a little questionable to keep things simple and probably won't be initially easy to follow but might help to give you some ideas on how to solve the problem. When I get time tomorrow I can make the code properly available but here are the main classes for now.

    Code (CSharp):
    1. public class VotingSceneController : MonoBehaviour
    2. {
    3.     void Start()
    4.     {
    5.         if (!ParrelSync.ClonesManager.IsClone())
    6.         {
    7.             NetworkManager.Singleton.OnServerStarted += OnServerStarted;
    8.             NetworkManager.Singleton.StartHost();
    9.         }
    10.         else
    11.         {
    12.             NetworkManager.Singleton.StartClient();
    13.         }
    14.     }
    15.  
    16.     private void OnServerStarted()
    17.     {
    18.         SpawnService.Instance().SpawnPrefab<VoteObject>(true);
    19.     }
    20. }
    Code (CSharp):
    1. public class VoteObject : NetworkBehaviour, IVotable
    2. {
    3.     NetworkVariable<int> optionA = new NetworkVariable<int>(0);
    4.     NetworkVariable<int> optionB = new NetworkVariable<int>(0);
    5.  
    6.     public event Action<int, int> OnVoteAValueChange;
    7.     public event Action<int, int> OnVoteBValueChange;
    8.  
    9.  
    10.     public override void OnNetworkDespawn()
    11.     {
    12.         base.OnNetworkDespawn();
    13.  
    14.         optionA.OnValueChanged -= OnOptionAValueChanged;
    15.         optionB.OnValueChanged -= OnOptionBValueChanged;
    16.  
    17.         GuiManager.Singleton.RemoveVoteObject();
    18.     }
    19.  
    20.     public override void OnNetworkSpawn()
    21.     {
    22.         Debug.Log("VoteObject OnNetworkSpawn");
    23.         base.OnNetworkSpawn();
    24.  
    25.         optionA.OnValueChanged += OnOptionAValueChanged;
    26.         optionB.OnValueChanged += OnOptionBValueChanged;
    27.  
    28.         GuiManager.Singleton.AddVoteObject(this);
    29.     }
    30.  
    31.     private void OnOptionAValueChanged(int oldValue, int newValue)
    32.     {
    33.         Debug.Log($"VoteObject OnOptionAValueChanged oldValue: {oldValue} newValue: {newValue}");
    34.         OnVoteAValueChange?.Invoke(oldValue, newValue);
    35.     }
    36.  
    37.     private void OnOptionBValueChanged(int oldValue, int newValue)
    38.     {
    39.         Debug.Log($"VoteObject OnOptionBValueChanged oldValue: {oldValue} newValue: {newValue}");
    40.         OnVoteBValueChange?.Invoke(oldValue, newValue);
    41.     }
    42.  
    43.     public int GetVoteAValue()
    44.     {
    45.         return optionA.Value;
    46.     }
    47.  
    48.     public int GetVoteBValue()
    49.     {
    50.         return optionB.Value;
    51.     }
    52.  
    53.     public void AddVoteToOptionA()
    54.     {
    55.         AddVoteAServerRpc();
    56.     }
    57.  
    58.     public void AddVoteToOptionB()
    59.     {
    60.         AddVoteBServerRpc();
    61.     }
    62.  
    63.     [ServerRpc(RequireOwnership = false)]
    64.     private void AddVoteAServerRpc()
    65.     {
    66.         optionA.Value++;
    67.     }
    68.  
    69.     [ServerRpc(RequireOwnership = false)]
    70.     private void AddVoteBServerRpc()
    71.     {
    72.         optionB.Value++;
    73.     }
    74. }
    75.  
    Code (CSharp):
    1. public interface IVotable
    2. {
    3.     public void AddVoteToOptionA();
    4.     public void AddVoteToOptionB();
    5.     public int GetVoteAValue();
    6.     public int GetVoteBValue();
    7.  
    8.     public event Action<int, int> OnVoteAValueChange;
    9.     public event Action<int, int> OnVoteBValueChange;
    10. }
    Code (CSharp):
    1. public class GuiManager : MonoBehaviour
    2. {
    3.     // indexes based on position of text field in project hierarchy under canvas
    4.     const int VOTE_A_INDEX = 4;
    5.     const int VOTE_B_INDEX = 5;
    6.  
    7.     static GuiManager singleton;
    8.  
    9.     List<TextMeshProUGUI> textFields;
    10.     List<Button> buttons;
    11.  
    12.     IVotable votableObject;
    13.  
    14.     private void Awake()
    15.     {
    16.         singleton = this;
    17.     }
    18.  
    19.     void Start()
    20.     {
    21.         LoadUIElements();
    22.     }
    23.  
    24.     public void OnClickVoteA()
    25.     {
    26.         votableObject.AddVoteToOptionA();
    27.     }
    28.  
    29.     public void OnClickVoteB()
    30.     {
    31.         votableObject.AddVoteToOptionB();
    32.     }
    33.  
    34.     public void AddVoteObject(IVotable voteObject)
    35.     {
    36.         votableObject = voteObject;
    37.  
    38.         votableObject.OnVoteAValueChange += OnVoteAValueChange;
    39.         votableObject.OnVoteBValueChange += OnVoteBValueChange;
    40.  
    41.         textFields[VOTE_A_INDEX].text = votableObject.GetVoteAValue().ToString();
    42.         textFields[VOTE_B_INDEX].text = votableObject.GetVoteBValue().ToString();
    43.     }
    44.  
    45.     private void OnVoteAValueChange(int oldValue, int newValue)
    46.     {
    47.         Debug.Log($"GuiManager OnVoteAValueChange oldValue: {oldValue} newValue: {newValue}");
    48.         textFields[VOTE_A_INDEX].text = newValue.ToString();
    49.     }
    50.  
    51.     private void OnVoteBValueChange(int oldValue, int newValue)
    52.     {
    53.         Debug.Log($"GuiManager OnVoteBValueChange oldValue: {oldValue} newValue: {newValue}");
    54.         textFields[VOTE_B_INDEX].text = newValue.ToString();
    55.     }
    56.  
    57.     public void RemoveVoteObject()
    58.     {
    59.         votableObject.OnVoteAValueChange -= OnVoteAValueChange;
    60.         votableObject.OnVoteBValueChange -= OnVoteBValueChange;
    61.         votableObject = default;
    62.     }
    63.  
    64.     private void LoadUIElements()
    65.     {
    66.         Canvas canvas = FindObjectOfType<Canvas>();
    67.  
    68.         textFields = FindUiElements<TextMeshProUGUI>(canvas);
    69.  
    70.         foreach (var textField in textFields)
    71.         {
    72.             Debug.Log("textField: " + textField.name);
    73.         }
    74.  
    75.         buttons = FindUiElements<Button>(canvas);
    76.  
    77.         foreach (var button in buttons)
    78.         {
    79.             Debug.Log("button: " + button.name);
    80.         }
    81.     }
    82.  
    83.     private List<T> FindUiElements<T>(Canvas canvas)
    84.     {
    85.         List<T> elements = new List<T>();
    86.  
    87.         foreach (T element in ObjectUtils.GetChildComponents<T>(canvas))
    88.         {
    89.             elements.Add(element);
    90.         }
    91.  
    92.         return elements;
    93.     }
    94.  
    95.     public static GuiManager Singleton { get => singleton; set => singleton = value; }
    96. }