Search Unity

Safe to modify a Network Variable before NetworkObject is spawned ?

Discussion in 'Netcode for GameObjects' started by Goreduc, Feb 2, 2023.

  1. Goreduc

    Goreduc

    Joined:
    Jun 26, 2020
    Posts:
    11
    Hello,

    I saw several threads on this subject, but can't find the answer to a simple question : is it safe to modify a Network Variable before NetworkObject is spawned ?
    In my context, my network variable contains important informations. Without theses, the object state is invalid.

    Let's take a simple NetworkBehaviour to explain my problem.
    (I can reproduce the same behaviour with complexe network variables with custom serialization)

    Code (CSharp):
    1. public class TestNetObj : NetworkBehaviour {
    2.     public NetworkVariable<int> MyValue = new();
    3.  
    4.     private void Awake() {
    5.         Debug.Log($"[Awake] {MyValue.Value}");
    6.     }
    7.  
    8.     private void Start() {
    9.         Debug.Log($"[Start] {MyValue.Value}");
    10.     }
    11.  
    12.     public override void OnNetworkSpawn() {
    13.         base.OnNetworkSpawn();
    14.         MyValue.OnValueChanged += OnValueChanged;
    15.         Debug.Log($"[OnNetworkSpawn] {MyValue.Value}");
    16.     }
    17.  
    18.     public override void OnNetworkDespawn() {
    19.         base.OnNetworkDespawn();
    20.         MyValue.OnValueChanged -= OnValueChanged;
    21.     }
    22.  
    23.     private void OnValueChanged(int pPrevious, int pNext) {
    24.         Debug.Log($"[OnValueChanged] Previous : {pPrevious}, Next : {pNext}");
    25.     }
    26. }
    If I well understood the documentation, to instantiate and configure my object on server side, I should do :
    Code (CSharp):
    1. Void MyFactory.Create1() {
    2.     TestNetObj newNetObj = Instantiate(PrefabTestNetObj );
    3.     newNetObj.GetComponent<NetworkObject>().Spawn();
    4.     newNetObj.MyValue.Value = 12;
    5. }
    If I start the host, launch MyFactory.Create1, and then connect a client, these are the logs on client's side :
    • |Awake] 0
    • [OnNetworkSpawn] 12
    Nice, all my object is well configured at network spawn.

    But if the client was connected before call to MyFactory.Create1 on host, I got these logs :
    • [Awake] 0
    • [OnNetworkSpawn] 0
    • [OnValueChanged] Previous : 0, Next : 12
    2 serializations appended, and my object was not correctly setup on "OnNetworkSpawn". 2 messages, and moreover I need to wait for "OnValueChanged" to have the right state.

    If I try something more intuitive for me (fully configuring my object before spawning):
    Code (CSharp):
    1. void MyFactory.Create2() {
    2.         TestNetObj newNetObj = Instantiate(PrefabNetObj);
    3.         newNetObj.MyValue.Value = 12;
    4.         newNetObj.GetComponent<NetworkObject>().Spawn();
    5. }
    I run the same tests as before.
    Late joining or already join client :
    • |Awake] 0
    • [OnNetworkSpawn] 12
    In both cases, everything seems OK
    The only one problem it's I get warning messages on Host's side :
    • NetworkVariable is written to, but doesn't know its NetworkBehaviour yet. Are you modifying a NetworkVariable before the NetworkObject is spawned?

    In conclusion, for me
    • the test 1 is less intuitive, need to have synchronisations flags on clients side, and a broken state during a time.
    • The test 2 is more intuitive, works well, but have a warning.
    Should I safetly ignore this warning or dit I missed something ?

    Thanks in advance !
    Thomas
     

    Attached Files:

  2. RikuTheFuffs-U

    RikuTheFuffs-U

    Unity Technologies

    Joined:
    Feb 20, 2020
    Posts:
    440
    Hey Goreduc, thanks for trying this out. I asked this internally and will come back to you when I have an answer.
     
    CodeSmile and Goreduc like this.
  3. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,975
    Good thing NGO is open source. :)

    This warning occurs here:
    Code (CSharp):
    1.         internal void MarkNetworkObjectDirty()
    2.         {
    3.             if (m_NetworkBehaviour == null)
    4.             {
    5.                 Debug.LogWarning($"NetworkList is written to, but doesn't know its NetworkBehaviour yet. " +
    6.                                  "Are you modifying a NetworkList before the NetworkObject is spawned?");
    7.                 return;
    8.             }
    9.  
    10.             m_NetworkBehaviour.NetworkManager.MarkNetworkObjectDirty(m_NetworkBehaviour.NetworkObject);
    11.         }
    It basically tells us that the "dirty" flag couldn't be set on the NetworkObject. Considering that you're spawning a new object and the NetworkVariable is synchronized correctly in your second case I would say you can ignore that warning.
     
  4. Goreduc

    Goreduc

    Joined:
    Jun 26, 2020
    Posts:
    11
    Thanks CodeSmile
    I agree with you, but I'm not fully sure about my NetworkVariable being synchronized correctly in all cases
    In my current tests it seems to work, but don't know if it may fail in some situations

    And if my case works fine, I don't understand what case this warning protect us from :)
    When looking in NGO code, it looks like when spawning an object, all network variables are considered as dirty and serialized
     
  5. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,975
    Maybe nothing. :)

    It wouldn't be the first well-meant warning about a potential issue where the developer putting that warning in had a completely different use case in mind. Or was just being very cautious and conservative thinking that this should not happen in the first place. Or over time technology changed and made that warning superfluous but since it doesn't occur under normal situations, no one thought about removing it. Or ... the list goes on. :D
     
    Goreduc likes this.
  6. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    259
    This has indeed been a question and there are definitively some improvements needed in this area of NGO. Some things to contemplate about setting a NetworkVariable prior to spawn:
    • You will not know which relative instance does or does not have authority to set this value
      • i.e. If it is owner write permissions, then every relative instance (owners and non-owners) will apply some value to it (this might be "ok" and the warning is a pesky thing to have to see each time you enter play mode).
    • Since in-scene placed NetworkObjects are instantiated (Awake) and started prior to spawning (done while the scene is loaded) and dynamically spawned NetworkObjects are instantiated, spawned, and then started, there is an opportunity with dynamically spawned NetworkObjects during Start to know who can and cannot write and you can set the value at that time without the warning.
      • However, if you use the same component on in-scene placed NetworkObjects you will still get this warning.
    With that said, there is a way to "have your cake and eat it too" with NetworkVariable<T> (haven't tested this approach with NetworkList). Below is an example script that provides you with a way to work around the warning issue when assigning a value prior to the associated NetworkObject being spawned and works with both in-scene placed and dynamically spawned NetworkObjects:
    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Netcode;
    3.  
    4. public class PreSpawnNetworkVariable<T> : NetworkVariable<T>
    5. {
    6.     /// <summary>
    7.     /// Handles setting the value when used in a NetworkBehaviour attached
    8.     /// to an in-scene placed and dynamically spawned NetworkObject
    9.     /// </summary>
    10.     public void SetValuePreSpawn(T value)
    11.     {
    12.         var networkBehaviour = GetBehaviour();
    13.         if (networkBehaviour != null && networkBehaviour.IsSpawned)
    14.         {
    15.             if (WritePerm == NetworkVariableWritePermission.Server && !networkBehaviour.NetworkManager.IsServer)
    16.             {
    17.                 // Ignore the attempted change
    18.                 return;
    19.             }
    20.             if (WritePerm == NetworkVariableWritePermission.Owner && !networkBehaviour.IsOwner)
    21.             {
    22.                 // Ignore the attempted change
    23.                 return;
    24.             }
    25.         }
    26.         Value = value;
    27.     }
    28.  
    29.     public override void SetDirty(bool isDirty)
    30.     {
    31.         if (GetBehaviour() != null)
    32.         {
    33.             base.SetDirty(isDirty);
    34.         }
    35.     }
    36.  
    37.     public PreSpawnNetworkVariable(T value = default,
    38.         NetworkVariableReadPermission readPerm = DefaultReadPerm,
    39.         NetworkVariableWritePermission writePerm = DefaultWritePerm)
    40.         : base(value, readPerm, writePerm)
    41.     {
    42.     }
    43. }
    44.  
    45. public class NetworkVarTest : NetworkBehaviour
    46. {
    47.     private PreSpawnNetworkVariable<int> m_PreSpawnSetNoWarnings = new PreSpawnNetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
    48.  
    49.     private void PreSpawnSetNoWarningsValueChanged(int oldValue, int newValue)
    50.     {
    51.         Debug.Log($"Updated m_PreSpawnSetNoWarnings's value to {m_PreSpawnSetNoWarnings.Value}!");
    52.     }
    53.  
    54.     private void Awake()
    55.     {
    56.         m_PreSpawnSetNoWarnings.OnValueChanged += PreSpawnSetNoWarningsValueChanged;
    57.         m_PreSpawnSetNoWarnings.Value = 5;
    58.     }
    59.  
    60.     private void Start()
    61.     {
    62.         // Avoids issue with in scene placed and dynamically spawned NetworkObjects
    63.         m_PreSpawnSetNoWarnings.SetValuePreSpawn(10);
    64.     }
    65.  
    66.     public override void OnNetworkSpawn()
    67.     {
    68.         if (IsOwner)
    69.         {
    70.             m_PreSpawnSetNoWarnings.Value = 20;
    71.         }
    72.         Debug.Log($"[Client-{NetworkManager.LocalClientId}] m_PreSpawnSetNoWarnings's value is: {m_PreSpawnSetNoWarnings.Value}!");
    73.         base.OnNetworkSpawn();
    74.     }
    75. }
    76.  
    Attaching this to an in-scene placed NetworkObject and running a host as a stand alone build and a client in the editor you would see this console output:
    upload_2023-2-3_15-3-17.png
    Some things to note (these points are written client-side relative when I was testing this component attached to an in-scene placed NetworkObject...so the server is the default owner of in-scene placed NetworkObject):
    • You will see that subscribers to the OnValueChanged will be notified (prior to spawn) that the value was updated.
      • You should note that you will get no notification when a non-owner spawns as authority has taken over and the object is spawned with the owner's value set. (intentionally set the script this way to point this aspect out)
    • You will notice that setting the value directly does not generate any warnings, this is handled in the overridden "SetDirty" method where I just don't mark it dirty if there is no assigned NetworkBehaviour.
    • To handle the issue with in-scene placed and dynamically spawned NetworkObjects, I made an ancillary SetValuePreSpawn method that will not try to set the value if the instance does not have authority and the associated NetworkObject is spawned (to avoid the errors you would get otherwise).

    Either case, this small code snippet should provide you with an alternative way to handle setting NetworkVariable<T> prior to spawn and might also provide additional insight behind the question as to when it is or is not "ok" to set a NetworkVariable. The answer is that it should be "ok" at any time, but there are some on-going internal (pending) discussions as to the best way to handle resolving this issue.
    (For the time being, you can use the above PreSpawnNetworkVariable<T> derived class that provides a work around and allows you to set it at any point without any warnings)
     
    Last edited: Feb 3, 2023
    Goreduc and CodeSmile like this.
  7. Goreduc

    Goreduc

    Joined:
    Jun 26, 2020
    Posts:
    11
    Thanks a lot for the detailled answer and the solution ! :)

    However I tried to implement the code you proposed, but
    doesn't exist for me.
    I can find "m_NetworkBehaviour" in NetworkVariableBase but not accessible for me (private protected)
    No accessor found.
    I work with Netcode 1.1.0 from October 20 2022

    An other question about the proposed "SetDirty" override, isn't it a problem that "m_dirty" is not set to false if "m_NetworkBehaviour" is null ?
     
  8. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    259
    @Goreduc
    *yikes!*
    I apologize. I was using the develop branch and that method is not available in NGO v1.1 or v1.2, but it will be available in the up-and-coming update (I believe it will be v1.3). So, I provided a solution to something you don't have access to at this time. :oops:

    Of course, if you wanted to test this approach (until the update is released) you can replace your com.unity.netcode.gameobjects entry in your project's manifest.json file to look like this:
    Code (CSharp):
    1. "com.unity.netcode.gameobjects": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git?path=com.unity.netcode.gameobjects#develop"
    I would backup your manifest file, make the change, and if you want to switch it back just use the original backed up manifest file (or you can keep it until the updated package is available).

    Regarding the dirty flag, if your NetworkVariable does not have an assigned NetworkBehaviour then it is not yet initialized which means the associated NetworkObject is not spawned... so the dirty flag really has no "relevance" until the associated NetworkObject is spawned (i.e. no need to worry about synchronization since you are just setting its initial value prior to being spawned).
     
  9. Goreduc

    Goreduc

    Joined:
    Jun 26, 2020
    Posts:
    11
    Hello,

    Thanks for your answer !

    Well, I tried to use the develop branch, but I got a bunch of errors or warnings and my project doesn't work anymore with it (when I try to solve them, I can start the server, but client doesn't connect anymore)
    I rolledback
    Perhaps too soon to switch to this branch.
    I can bear ignoring the warning until the version is released ;-)

    For information, I got the following warning/errors :
    - Warning: [Netcode] NetworkPrefab cannot be null (NetworkPrefab at index: -1)
    - Warning: [Netcode] Removing invalid prefabs from Network Prefab registration: {SourceHash: 0, TargetHash: 0}
    - Warning: [Netcode] Runtime Network Prefabs was not empty at initialization time. Network Prefab registrations made before initialization will be replaced by NetworkPrefabsList.
    - Exception : ArgumentException: Invalid generic arguments
    Parameter name: typeArguments
    System.Reflection.RuntimeMethodInfo.MakeGenericMethod (System.Type[] methodInstantiation) (at <75633565436c42f0a6426b33f0132ade>:0)
    Unity.Netcode.Editor.NetworkBehaviourEditor.RenderNetworkVariable (System.Int32 index) (at Library/PackageCache/com.unity.netcode.gameobjects@66b11d0af1/Editor/NetworkBehaviourEditor.cs:85)

    As I got strange behaviour in editor, this may be associated with a NetworkVariable with the following declaration :
    [Serializable]
    public struct Data : INetworkSerializeByMemcpy {
    public int Value;
    public FixedString32Bytes Name;
    }
     
  10. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    259
    Yeah, we are much closer to the next release so it might be easier to use that.
     
    Goreduc likes this.