Search Unity

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

Resolved Implementing Dictionaries as Network Variables in Unity

Discussion in 'Netcode for GameObjects' started by jvicogSpika, Nov 21, 2023.

  1. jvicogSpika

    jvicogSpika

    Joined:
    Dec 20, 2022
    Posts:
    6
    I've been working on a project where I need to synchronize data between networked players, and I'm particularly interested in using dictionaries to manage certain aspects of the game state.

    I'm wondering if there's a way to implement dictionaries as network variables directly. Has anyone successfully achieved this, or is there a recommended approach for synchronizing dictionary data between clients and ensuring that new players receive this information upon joining?
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    4,979
    It's rather straightforward with INetworkSerializable. You can serialize keys and values into two separate lists and on the other end restore the lists in a dictionary.

    HOWEVER I always warn of using collections as NetworkVariable because every time any change to the collection occurs the entire collection gets synchronized!

    If you have ten entries it may not matter in terms of traffic. If you have hundreds, it would simply be foolish to waste that bandwidth. If it might be more (thousands?) because there is no hard cap in the game (think: player resources in a survival game) it could be game-breaking for longer game sessions.

    It is a far better approach to use RPC calls like:

    OnSetKeyValuePairServerRpc(object key, object value) { .. }
    OnRemoveKeyServerRpc(object key) { .. }


    Because RARELY do you change more than one item in a dictionary at any given time, right? ;)

    The former method either adds a new key if it does not exist, or changes the key's value if the key already exists.
    I used 'object' here but of course you can use whatever type you're using. Ideally I'd make it a generic method but I can't recall if NGO supports that.
     
    gamedevjake and RikuTheFuffs-U like this.
  3. jvicogSpika

    jvicogSpika

    Joined:
    Dec 20, 2022
    Posts:
    6
    Thank you very much, the only problem with using RPCs is the difficulty of getting the information back from the list or dictionary when someone new enters the room. That's why I'm opting for NetworkVariables, any option with which I can keep the information every time someone enters the room and it doesn't take up much bandwidth either?
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    4,979
    Same as above, with a slight alteration:

    OnSetKeyValuePairClientRpc(object key, object value) { .. }
    OnRemoveKeyClientRpc(object key) { .. }

    Basically the server relays the set and remove key/value RPCs to all clients. You just need to make sure that the client who tells the server to set or remove a key doesn't change its dictionary directly but rather waits for the server to send the response (of course you can take that shortcut with appropriate caution, eg. removing an already removed key shouldn't throw).
     
  5. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    639
    I had a stab at this using NetworkList as a guide. It seems to work, they'll be some caveats with the key/value types so I went with INetworkSerializable to keep things relatively simple.
    Code (CSharp):
    1.  public class NetDictionary<K,V> : NetworkVariableBase where K : INetworkSerializable, new() where V : INetworkSerializable, new()
    2.     {
    3.         Dictionary<K, V> dictionary;
    4.         List<NetDictionaryEvent<K,V>> dirtyEvents;
    5.  
    6.         public delegate void OnDictionaryChangedDelegate(NetDictionaryEvent<K,V> changeEvent);
    7.         public event OnDictionaryChangedDelegate OnDictionaryChanged;
    8.  
    9.         public NetDictionary(NetworkVariableReadPermission readPerm = DefaultReadPerm,
    10.             NetworkVariableWritePermission writePerm = DefaultWritePerm) : base(readPerm, writePerm)
    11.         {
    12.             dictionary = new Dictionary<K, V>();
    13.             dirtyEvents = new List<NetDictionaryEvent<K,V>>();
    14.         }
    15.  
    16.         public override void ReadField(FastBufferReader reader)
    17.         {
    18.             dictionary.Clear();
    19.  
    20.             reader.ReadValueSafe<int>(out int count);
    21.  
    22.             Debug.Log($"NetDictionary ReadField length: {reader.Length} count: {count}");
    23.  
    24.             for (int i = 0; i < count; i++)
    25.             {
    26.                 reader.ReadNetworkSerializable<K>(out K key);
    27.                 reader.ReadNetworkSerializable<V>(out V value);
    28.  
    29.                 dictionary.Add(key, value);
    30.             }
    31.         }
    32.  
    33.         public override void WriteField(FastBufferWriter writer)
    34.         {
    35.             Debug.Log("NetDictionary WriteField count: " + dictionary.Count);
    36.  
    37.             writer.WriteValueSafe<int>(dictionary.Count);
    38.  
    39.             foreach (KeyValuePair<K, V> keyValue in dictionary)
    40.             {
    41.                 writer.WriteNetworkSerializable<K>(keyValue.Key);
    42.                 writer.WriteNetworkSerializable<V>(keyValue.Value);
    43.             }
    44.         }
    45.  
    46.         public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta)
    47.         {
    48.             reader.ReadValueSafe(out int deltaCount);
    49.  
    50.             Debug.Log($"NetDictionary ReadDelta length: {reader.Length} count: {deltaCount}");
    51.  
    52.             for (int i = 0; i < deltaCount; i++)
    53.             {
    54.                 reader.ReadValueSafe(out NetEventType eventType);
    55.                 switch (eventType)
    56.                 {
    57.                     case NetEventType.Add:
    58.                         reader.ReadValueSafe<K>(out K key);
    59.                         reader.ReadValueSafe<V>(out V value);
    60.  
    61.                         Debug.Log($"NetDictionary ReadDelta {key} {value}");
    62.  
    63.                         dictionary.Add(key, value);
    64.  
    65.                         if (OnDictionaryChanged != null)
    66.                         {
    67.                             OnDictionaryChanged(new NetDictionaryEvent<K, V>
    68.                             {
    69.                                 EventType = eventType,
    70.                                 Key = key,
    71.                                 Value = value
    72.                             });
    73.                         }
    74.                         break;
    75.                 }
    76.             }
    77.         }
    78.  
    79.         public override void WriteDelta(FastBufferWriter writer)
    80.         {
    81.             Debug.Log("NetDictionary WriteDelta count: " + dirtyEvents.Count);
    82.  
    83.             writer.WriteValueSafe(dirtyEvents.Count);
    84.  
    85.             foreach (var dictionaryEvent in dirtyEvents)
    86.             {
    87.                 Debug.Log("NetDictionary WriteDelta dirtyEvent: " + dictionaryEvent);
    88.  
    89.                 writer.WriteValueSafe(dictionaryEvent.EventType);
    90.  
    91.                 switch (dictionaryEvent.EventType)
    92.                 {
    93.                     case NetEventType.Add:
    94.                         writer.WriteValueSafe<K>(dictionaryEvent.Key);
    95.                         writer.WriteValueSafe<V>(dictionaryEvent.Value);
    96.                         break;
    97.                 }
    98.             }
    99.         }
    100.  
    101.         public void Add(K key, V value)
    102.         {
    103.             dictionary.Add(key, value);
    104.  
    105.             var listEvent = new NetDictionaryEvent<K, V>()
    106.             {
    107.                 EventType = NetEventType.Add,
    108.                 Key = key,
    109.                 Value = value
    110.             };
    111.  
    112.             HandleDictionaryEvent(listEvent);
    113.         }
    114.  
    115.         private void HandleDictionaryEvent(NetDictionaryEvent<K, V> listEvent)
    116.         {
    117.             dirtyEvents.Add(listEvent);
    118.             SetDirty(true);
    119.             OnDictionaryChanged?.Invoke(listEvent);
    120.         }
    121.  
    122.         public override void ResetDirty()
    123.         {
    124.             Debug.Log("NetDictionary ResetDirty");
    125.  
    126.             base.ResetDirty();
    127.             if (dirtyEvents.Count > 0)
    128.             {
    129.                 dirtyEvents.Clear();
    130.             }
    131.         }
    132.  
    133.         public override bool IsDirty()
    134.         {
    135.             Debug.Log("NetDictionary IsDirty");
    136.             return base.IsDirty() || dirtyEvents.Count > 0;
    137.         }
    138.  
    139.         public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
    140.         {
    141.             return dictionary.GetEnumerator();
    142.         }
    143.     }
    Perhaps we can trouble @NoelStephens_Unity to explain how to do this properly. :)
     
    gamedevjake likes this.
  6. gamedevjake

    gamedevjake

    Joined:
    Aug 16, 2022
    Posts:
    25
    Thanks for your response! I'm also interested in the topic of NetworkDictionaries.

    Could you clarify something for me - what class are you putting these methods onto? Something like this?

    Code (CSharp):
    1. public class NetDictionary : NetworkBehavior { ... }
    And does this class contain a private dictionary object that the RPCs are managing?
     
  7. gamedevjake

    gamedevjake

    Joined:
    Aug 16, 2022
    Posts:
    25
    I might be misunderstanding, but I think the original question was about the scenario where, say, Client #3 joins the game several minutes after the game started, and after the dictionary-syncing RPCs have already fired. This late-joining client would not receive those RPCs, but would still need to get the up-to-date Dictionary data sync'd somehow.
     
  8. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    Don't forget that you can always use NetworkBehaviour.OnSynchronize to create your own serialization for data that is set/updated via RPC. That is invoked when a NetworkObject is first spawned on the server side and when the server is synchronizing a newly joined client.
     
  9. gamedevjake

    gamedevjake

    Joined:
    Aug 16, 2022
    Posts:
    25
    This seems interesting, but if I wanted to use a single Dictionary object to hold some shared game state, would I be able to synchronize that dictionary using the OnSynchronize serializer? My understanding is no since Dictionary is not serializable out-of-the-box, which leads me to think perhaps Unity is encouraging other patterns for storing shared state?
     
  10. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    244
    From my experience thus far, our goal has been to try to provide the tools that enable a wide variety of possibilities when it comes to making a netcode enabled Unity project. If you search for "C# serialize dictionary" you will indeed find many suggestions on how to go about doing this...but the bottom line is that all suggested paths do require some additional effort whether serializing in a non-netcode or netcode enabled application.

    We have made it (or at least have put reasonable effort towards making it) such that the majority of commonly serialized types can be serialized with minimal effort. So, taking a dictionary into account, there are many solutions that involve using Json...but Json can be expensive in string format... which then you could opt to use binary json (newtonsoft.json.bson) to get a more condensed serialization. But then there is the issue with serializing only the deltas of the dictionary as opposed to the entire dictionary...not knowing what kind of game state that is being serialized...it could possibly be that there are other approaches to handling the synchronization of game state...but I would need to know more specifics behind what kind of data types would be used for the keys and the data types used for the values and things of that nature to provide any real alternative.

    Of course, I always take a step back if a path I am taking seems more difficult than it really should be...while I can't (as in cannot endorse) point to any one particular GitHub repository, there are already several Unity friendly serializable dictionary solutions...some of which could provide a good foundation to making a "netcode friendly" serializable dictionary.

    We encourage creating. :)
     
    gamedevjake likes this.
  11. gamedevjake

    gamedevjake

    Joined:
    Aug 16, 2022
    Posts:
    25
    Appreciate the response as it does help me understand the Unity platform better. I'm working on my first large game and navigating a lot of new frameworks :)