Repository navigation
Automatically invoke NetworkVariable.OnValueChanged when spawning #3186
Description
Activity
- addedstat:awaiting-triageStatus - Awaiting triage from the Netcode team.Status - Awaiting triage from the Netcode team.type:feedbackIssue contains feedback and not specific bug or feature requests.Issue contains feedback and not specific bug or feature requests.
on Dec 30, 2024 Hi @babaq,
The documentation provides the description of the behavior behind NetworkVariables, but I am going to see if we can improve upon that documentation to include additional information that will help clarify the state of NetworkVariables and when OnValueChanged is invoked.
- The server (or client) instance that has write permissions sets the initial value.
- While one might think of the network prefab's value as being the "previous state" for non-authority instances (i.e. does not have write permissions), this is actually an incorrect assumption as the "previous state" is considered the "current state" when a NetworkObject is spawned.
Take the following example into consideration:
public class MyNetworkBehaviour : NetworkBehaviour { // One way to check for default previous valu private const int InitialSomeIntValue = 10; private NetworkVariable<int> m_SomeInt = new NetworkVariable<int>(InitialSomeIntValue); private int m_InitialSomeIntValue; protected override void OnNetworkPreSpawn(ref NetworkManager networkManager) { m_InitialSomeIntValue = m_SomeInt.Value; base.OnNetworkPreSpawn(ref networkManager); } public override void OnNetworkSpawn() { m_SomeInt.OnValueChanged += OnSomeIntChanged; if (IsServer) { m_SomeInt.Value = Random.Range(20, 1000); } else { // Non-authority (no write permissions) instances can initialize // from the initial value of the NetworkVariable this way. // The initial value is when the previous and current values are // the same. OnSomeIntChanged(m_InitialSomeIntValue, m_SomeInt.Value); } base.OnNetworkSpawn(); } private void OnSomeIntChanged(int previous, int current) { Debug.Log($"SomeInt changed from ({previous}) to ({current})"); } }
What will happen is that the server will invoke OnValueChanged during OnNetworkSpawn because the value (server/authority relative) does indeed change.
For clients (non-authority when write permissions are set to server), the newly instantiated network prefab is considered to have "no state" until it is spawned. Upon being spawned, the new instance's NetworkVariable(s) are considered to be set to their "initial value" (client relative). If you need a client to do additional initialization based on NetworkVariable values when spawning, then it is recommended to create a central method (other than the subscribed OnValueChanged method) so you can invoke it both when spawned and when (after being spawned) the initial value changes.Why is it this way?
While it might seem "counter intuitive" you have to think a bit more about what being "spawned" really means. When a NetworkObject and the associated NetworkBehaviour components are considered "not spawned" then there is no known previous state. This is primarily because they have yet to be spawned and have no reference point to determine a "previous state".I know...it might sound confusing but I can help further clarify. When a NetworkObject is spawned on a non-authority/non-write permission instance the previous state and current state are considered "the same". This is to provide the baseline in which that instance can determine any changes to the state from that point in time forward. Then there is the value assigned to the NetworkVarible when it is instantiated (typically during the declaration portion of the instantiation) that may or may not be a value that a user wants to have as the "previous state" during the spawning of a non-authority instance. Some designs might not even care about the default value as that is configured by the authority instance during OnNetworkSpawn and/or a users script might not want OnValueChanged to be triggered when an instance is spawned and only cares about when it changes from the point of being instantiated and spawned forward.
Finally, if you were to have a NetworkObject pool where you are re-using instances, you could eventually end up spawning an instance that was already spawned...which you can't set the value of a NetworkVariable on a non-authority instance... so the "current" value of a despawned pooled object could be some value that you might not want to have as the previous value since it was last used. As such, it made more sense to make the previous state and current state be the same values when spawned and for delta states to only occur after the NetworkObject is spawned.
With that said, under the scenario you are describing it is recommended to just invoke the same method you are using to subscribe to the OnValueChanged event when a non-authority instance is spawned (or alternately you can create a third method that is invoked by OnNetworkSpawn and the method used to subscribe to the OnValueChanged event.
This is a base behavior of NetworkVariable that we most likely will not change.
However, you can replicate NetworkVariable by deriving from NetworkVariableBase and define your own type of NetworkVariable to get the behavior specific to your project's needs.- removedstat:awaiting-triageStatus - Awaiting triage from the Netcode team.Status - Awaiting triage from the Netcode team.
on Jan 7, 2025 I agree that when spawning, the instance of a prefab in a non-authority client should consider spawned value as current and previous value. The main concern is not about which should be current and previous values, but to sync the game states with server, not just sync
NetworkVariable, becauseNetworkVariablecould hook to an external function that do something to the game states.For example, if a
NetworkVariable<float> Freq = new(10f)have aOnValueChangedcallback that set a shader property. Then the intended initialization should be set the Freq value and also invokeOnValueChanged(Freq.Value, Freq.Value), so that rendered noise texture freq will be the intended. Meanwhile, at the non-authority client, the spawned value synced with server, but without also invokeOnValueChanged, rendered texture would be different. My suggestion is that allow user to decide whether they consider theOnValueChangedis part of initialization or not.One easy way to differentiate the two is to register
OnValueChangedinOnNetworkPrespawnorOnNetworkSpawn. Since the previous value and current value are set to same when spawned, we only need to check whetherOnValueChangedis null, and if not, invokeOnValueChanged(previous, current), which is the same asOnValueChanged(current, current)when spawned.My current solution is to gather all NetworkVaribles and call
Notify(essentiallySetDirty(true)) on server side to triggerOnValueChanged, so game states are completely synced across the network, which is why i also suggested to addNotifyfunction for NetworkVariable in #3184.one of other solutions as the way you suggested, is to call all
OnValueChangedatOnNetworkSpawnlike this:public override void OnNetworkSpawn() { nv1 += Onnv1; nv2 += Onnv2; nv3 += Onnv3; . . . Onnv1(nv1.Value,nv1.Value); Onnv2(nv2.Value,nv2.Value); Onnv3(nv3.Value,nv3.Value); . . . base.OnNetworkSpawn(); }
But this is a tedious repeated work which i don't like(especially when there are many NetowrkVariables). I think the first solution is a proper reuse the mechanism Netcode already has.
@babaq
Just so I can understand how frequently this occurs. How many derived NetworkBehaviours do you have in your project and do you know (roughly) how many unique NetworkVariables each NetworkBehaviour has? I know it might seem like I am resisting a change to this area of NetworkVariable but we have to always consider the impact it might have by changing the way something currently works (i.e. current projects that are designed around the way something works vs making a change that could effectively "break" their script logic because suddenly it is invoking OnValueChanged prior to invoking OnNetworkSpawn which would cause script using the pattern you outlined above to invoke twice during spawn).Of course, we could always look into the possibility to add another property to NetworkObject that you could set which would force every NetworkVariable on any associated NetworkBehaviour to trigger OnValueChanged during spawn. Adding something like this to each NetworkVariable would yield close to the same amount of work one would have to run through (typing a few less characters but still having to specify this kind of behavior), and so making it an "all or none" property on NetworkObject seems to be a balance between the two....where you could still have some form of granular control.
Would adding another property to NetworkObject that enabled the automatic triggering of NetworkVaraibles on the associated NetworkBehaviours help improve your development experience?
Reacted by FailCake and Konecsny Bence@NoelStephensUnity Your suggested solution would definitely help a lot of people's developing experience. In the current state of our project, we have ~15
NetworkBehaviours, and the number ofNetworkVariablesare within 5-50, ~20 on average, and we know we will have much moreNetworkBehavioursas project develop.The Documentation have only shown examples where
OnValueChangedare all registered inOnNetworkSpawn. This will remain the same for old project.But if user want to invoke their
OnValueChangedas part of initialization, they will need to registerOnValueChangedinOnNetworkPrespawn, so that whenNetworkVariablesare initialized, theirOnValueChangedare not null and then invoked withOnValueChanged(current, current). For extra safeguard, that some current project have already intended or accidentally registeredOnValueChangedinOnNetworkPrespawn, ASpawnWithInvokeingOnValueChangedproperty can be added toNetworkObjectthat toggle the check forOnValueChanged, if not null, then invoke them.- addedtype:featureNew feature, request or improvementNew feature, request or improvementtype:feature-2.xNew NGO 2.0.0 feature, request or improvementNew NGO 2.0.0 feature, request or improvementTrackingHas been added to trackingHas been added to tracking
on Jan 9, 2025 - addedpriority:mediumThis issue has medium priority and may take some time to be resolvedThis issue has medium priority and may take some time to be resolvedand removedtype:feedbackIssue contains feedback and not specific bug or feature requests.Issue contains feedback and not specific bug or feature requests.
on Jan 9, 2025 - addedstat:awaiting-triageStatus - Awaiting triage from the Netcode team.Status - Awaiting triage from the Netcode team.
on Jan 16, 2025 10 remaining items
- removedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity account
on Jun 5, 2025 @EmandM I totally understand your concern from a management perspective, but from technically perspective, @NoelStephensUnity and I have discussed the possible changes needed for this improvement, and it shouldn't add any overhead/incompatibility issues, since It didn't need to touch NetworkVariable, only Check&Evoke when object spawning.
- addedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity accountand removedstat:awaiting-responseAwaiting response from author. This label should be added manually.Awaiting response from author. This label should be added manually.
on Jun 6, 2025 - removedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity account
on Jun 11, 2025 Perhaps consider changing the example used for network variable's OnValueChanged callback to one where you would not want OnValueChanged to be called during spawn/for a late joining client. Currently it is a door that is non-functional in these cases. Alternatively keep that example and demonstrate a good method one should use to correctly sync the door actually being open or closed for late joining clients.
Reacted by Noel Stephens- addedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity account
on Apr 24, 2026 Perhaps consider changing the example used for network variable's OnValueChanged callback to one where you would not want OnValueChanged to be called during spawn/for a late joining client. Currently it is a door that is non-functional in these cases. Alternatively keep that example and demonstrate a good method one should use to correctly sync the door actually being open or closed for late joining clients.
Yikes... thank you @pmurph0305 for pointing that out.
Indeed that needs to be updated to show a more unified flow for late joining clients.- addedstat:awaiting-responseAwaiting response from author. This label should be added manually.Awaiting response from author. This label should be added manually.and removedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity account
on Apr 27, 2026 - addedStaleAdded after 30 days since stat:awaiting response was added (if it's still present)Added after 30 days since stat:awaiting response was added (if it's still present)
on May 12, 2026 - removedstat:awaiting-responseAwaiting response from author. This label should be added manually.Awaiting response from author. This label should be added manually.StaleAdded after 30 days since stat:awaiting response was added (if it's still present)Added after 30 days since stat:awaiting response was added (if it's still present)
on May 12, 2026
Feedback
When load scene with in-scene placed
NetworkObjector dynamically spawnNetworkObject, connected client would spawn the object andNetworkVariablevalue synced with the server. But theOnValueChangedis not called, even if the value has been synced with the value of server, and is different from the default value that has been set inNetworkBehaviour. The current behavior of skippingOnValueChangedwhen spawning also doesn't depend on where theOnValueChangedhas been registered, inOnNetworkSpawn,OnNetworkPreSpawnor even inAwake.This caused a lot of problems, because
NetworkVariablemay be a bool that enable/disable a renderer, or set a shader field throughOnValueChanged. SkippingOnValueChangedwould make render frame inconsistent with what's intended. This is specially annoying when late-join client starts to spawn allNetworkObject, withoutOnValueChangedthe game states would drastically different from server. The problem also shows up when a previouslyNetworkHideobject become visible again and client respwan the object but with inconsistent states from server.Suggested Changes
When spawning, all
NetworkVariableshould be synchronized, and correspondingOnValueChangedshould all get called, because clients are supposed to keep the same states as server. The server has likely changed the value ofNetworkVariableand have already called theOnValueChangedon server side, so when spawning happens, client need to not only sync all values ofNetworkVariable, but also call allOnValueChangedto keep the same states as server.An intuitive way is to register
OnValueChangedinOnNetworkPreSpawn, so that during subsequent spawning any valid callbacks registered will get called.