NetworkVariable.OnValueChanged is not called on the client unless the change took place while the object existed on the client
Description
leaving original title and description for context.
NetworkShow does not set NetworkVariables
When we call NetworkHide on a dynamically created object, and later call NetworkShow, the object's network variables are not set. NetworkHide will despawn and destroy the object on the client, so when the server calls NetworkShow, we are now creating a new object on the client, except this time the network variables don't get set.
Upon further testing, the issue turned out to be that NetworkVariable.OnValueChanged is not being called on the client, UNLESS the value changed while the network object existed on the client.
So if the client connects, and the network variable already had its value set on the server, OnValueChanged will not get called on the client. Similarly, if the object is network hidden on the client side, when it becomes shown again, OnValueChanged is not called for the network variable; for a dynamically created network object it also gets destroyed on the client, not just network hidden.
Not sure if the parent hierarchy is playing a role here, but my network object is a child of another network object.
For the test, I have a Canvas that's an in-scene network object, then I dynamically (at run time) add a network object child to it (on the server), this network object has the behavior below, then I spawn this network object.
public class TestBehaviour : NetworkBehaviour
{
NetworkVariable<float> _networkVariable;
[SerializeField]
private TMP_Text _text;
private void Awake()
{
_networkVariable = new NetworkVariable<float>();
_networkVariable.OnValueChanged += OnNetworkVariableChanged;
}
private void OnNetworkVariableChanged(float previousValue, float newValue)
{
text.text = _networkVariable.Value.ToString();
}
private void Update()
{
if (_networkVariable != null)
{
_text.text = _networkVariable.Value.ToString();
}
}
public override async void OnNetworkSpawn()
{
if (!NetworkManager.Singleton.IsServer)
{
return;
}
await new WaitForSeconds(10); // give time for client to connect.
_networkVariable.Value = 69; // by now client connected, the client will have OnNetworkVariableChanged raised here.
await new WaitForSeconds(10);
NetworkObject.NetworkHide(1);
_networkVariable.Value = 12; // here we change the value while the object is hidden for the client
await new WaitForSeconds(10);
NetworkObject.NetworkShow(1); // here we show the object to the client, since this object is dynamically created, a new instance will be created, BUT OnNetworkVariableChanged is never raised.
}
}
While inspecting this scene on the client side in the editor, I see everything behaving properly except for that OnNetworkVariableChanged method, it does not get raised unless a change took place while the network object existed.
This issue was not present in Netcode v1.0.0, I started experiencing it in v1.1.0
Actual Outcome
NetworkVariable.OnValueChanged is not called on the client unless the change took place while the object existed on the client.
Expected Outcome
NetworkVariable.OnValueChanged should be called when the variable's value changes, regardless if the change took place while the object existed on the client or not.
Environment
- OS: Win10
- Unity Version: 2022.1.23f1
- Netcode Version: 1.1.0
From https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/releases
1.1.0 should be
Oct 20 https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/commit/2c69184e5f85a025455c415145be3eeecbf98446
But you list ba418fa5b600ad9eb61fab0575f12fbecc2c6520 which is
commit ba418fa5b600ad9eb61fab0575f12fbecc2c6520
Author: ashwini <[email protected]>
Date: Fri Apr 1 11:25:32 2022 -0700
chore: Bump NGO version to pre.7 (#1856)
So, I'm a bit puzzled. There's a six-month gap there. Can you specify which one correctly describe the code you are running?
Furthermore, in TestRunner, NetworkShowHideTest in PlayMode does
// hide them on one client
Show(mode == 0, false);
...
m_NetSpawnedObject1.GetComponent<ShowHideObject>().MyNetworkVariable.Value = 3;
...
Show(mode == 0, true);
and later asserts Assert.True(ShowHideObject.ClientTargetedNetworkObjects.Count == 3, $"Client-{clientNetworkManager.LocalClientId} should have 3 instances visible but only has {ShowHideObject.ClientTargetedNetworkObjects.Count}!");. Since this test is passing, NetworkVariables should be set
upon NetworkShow.
Can you please double-check?
I will double check with a simpler object, sorry about the commit number that was auto-filled, I am on the 1.1.0 release.
Upon further testing, the issue turned out to be that NetworkVariable.OnValueChanged is not being called on the client, UNLESS the value changed while the network object existed on the client.
So if the client connects, and the network variable already had its value set on the server, OnValueChanged will not get called on the client. Similarly, if the object is network hidden on the client side, when it becomes shown again, OnValueChanged is not called for the network variable; for a dynamically created network object it also gets destroyed on the client, not just network hidden.
Not sure if the parent hierarchy is playing a role here, but my network object is a child of another network object.
For the test, I have a Canvas that's an in-scene network object, then I dynamically (at run time) add a network object child to it (on the server), this network object has the behavior below, then I spawn this network object.
public class TestBehaviour : NetworkBehaviour
{
NetworkVariable<float> _networkVariable;
[SerializeField]
private TMP_Text _text;
private void Awake()
{
_networkVariable = new NetworkVariable<float>();
_networkVariable.OnValueChanged += OnNetworkVariableChanged;
}
private void OnNetworkVariableChanged(float previousValue, float newValue)
{
text.text = _networkVariable.Value.ToString();
}
private void Update()
{
if (_networkVariable != null)
{
_text.text = _networkVariable.Value.ToString();
}
}
public override async void OnNetworkSpawn()
{
if (!NetworkManager.Singleton.IsServer)
{
return;
}
await new WaitForSeconds(10); // give time for client to connect.
_networkVariable.Value = 69; // by now client connected, the client will have OnNetworkVariableChanged raised here.
await new WaitForSeconds(10);
NetworkObject.NetworkHide(1);
_networkVariable.Value = 12; // here we change the value while the object is hidden for the client
await new WaitForSeconds(10);
NetworkObject.NetworkShow(1); // here we show the object to the client, since this object is dynamically created, a new instance will be created, BUT OnNetworkVariableChanged is never raised.
}
}
While inspecting this scene on the client side in the editor, I see everything behaving properly except for that OnNetworkVariableChanged method, it does not get raised unless a change took place while the network object existed.
Hi @TheCaveOfWonders ,
First thing I need to mention:
public override async void OnNetworkSpawn()
This is definitely not supported. Any network-related notification, callback or API must return timely and not have any async/coroutine part to it. Consider it from the SDK point of view: What should the NetworkObject state be for those 10 seconds, if OnNetworkSpawn didn't finish ?
Good news, though, is that it is easy to convert to a traditional update-based approach. See below. I also used keydown instead of timers, to make testing easier.
private void OnNetworkVariableChanged(float previousValue, float newValue)
{
Debug.Log($"OnNetworkVariableChanged {previousValue} -> {newValue}");
}
private void Update()
{
if (IsServer && Input.GetKeyDown(KeyCode.A))
{
_networkVariable.Value = 69; // the client will have OnNetworkVariableChanged raised here.
}
if (IsServer && Input.GetKeyDown(KeyCode.B))
{
NetworkObject.NetworkHide(1);
}
if (IsServer && Input.GetKeyDown(KeyCode.C))
{
_networkVariable.Value = 12; // here we change the value while the object is hidden for the client
}
if (IsServer && Input.GetKeyDown(KeyCode.D))
{
NetworkObject.NetworkShow(1);
// here we show the object to the client, since this object is dynamically created, a new instance will be created, BUT OnNetworkVariableChanged is never raised.
}
}
public override void OnNetworkSpawn()
{
Debug.Log($"OnNetworkSpawn {_networkVariable.Value}");
}
Before looking at the logs the above is producing, let's answer this:
NetworkVariable.OnValueChanged is not called on the client
This is correct. OnValueChanged is only ever called if the value changed from what it was during OnNetworkSpawn. So, after a hide/show combo, you'll get a fresh OnNetworkSpawn with the new value and follow-up OnNetworkVariableChanged if the value changes from that.
I ran the above, pressed A, B, C, and D and got:
(duplicated because there was two player objects with this component)
pressed A
host
OnNetworkVariableChanged 0 -> 69
OnNetworkVariableChanged 0 -> 69
client
OnNetworkVariableChanged 0 -> 69
OnNetworkVariableChanged 0 -> 69
pressed B
pressed C
host
OnNetworkVariableChanged 69 -> 12
OnNetworkVariableChanged 69 -> 12
pressed D
client
OnNetworkSpawn 12
OnNetworkSpawn 12
I can forward this to our documentation team to check if we can make the documentation clearer, but my understanding is that by having your code check the value during OnNetworkSpawn, you can get the needed functionality.
First thing I need to mention: public override async void OnNetworkSpawn() This is definitely not supported. Any network-related notification, callback or API must return timely and not have any async/coroutine part to it. Consider it from the SDK point of view: What should the NetworkObject state be for those 10 seconds, if OnNetworkSpawn didn't finish ?
Unless I'm misunderstanding something, this is my understanding of this: OnNetworkSpawn is an empty virtual void in its base class NetworkBehaviour, and it's called synchronously in VisibleOnNetworkSpawn, meaning if we override it and make it async, that should not have an impact on the flow of VisibleOnNetworkSpawn, it will still be called synchronously up until the first await (the call from the method will return at the first await it sees), so any waiting we do will not be stopping the flow of VisibleOnNetworkSpawn.
Also since the base method is empty, this is more of an event really, it doesn't do anything besides allow us to execute some code if we want to.
Anyhow I've been using it with async/await throughout my project without any issues, but if I'm missing a crucial scenario where it will be problematic, please advise.
NetworkVariable.OnValueChanged is not called on the client This is correct. OnValueChanged is only ever called if the value changed from what it was during OnNetworkSpawn. So, after a hide/show combo, you'll get a fresh OnNetworkSpawn with the new value and follow-up OnNetworkVariableChanged if the value changes from that.
I have to disagree with you on this, OnValueChanged, in my opinion should be raised anytime the value is changed, that's the name of the method, it should require no further documentation, the name is very clear. I can confirm that it worked like so in v1.0.0
Furthermore, If we don't raise it any time the value changes, that will force us to always have additional logic in OnNetworkSpawn to handle its first change (from default/null to its first value), as opposed to a single logic in OnValueChanged, that would also mean we now always need to use OnNetworkSpawn, even if we don't need it, just to be able to handle the initial value of the network variable.
I'll kick an internal discussion with our team. I can't promise anything. On a released SDK, we should be limiting the changes to API and behaviours. In the meantime, the best I can offer is for you to put, in game code:
public override void OnNetworkSpawn()
{
OnNetworkVariableChanged(_networkVariable.Value, _networkVariable.Value);
}
The downside is that you need a line per NetworkVariable. But at least it gets you your notifications
thanks that's definitely appreciated and could save lots of boilerplate code, as I have lots of network behaviors and network variables in my project.
Closing this issue as a duplicate of #3186 as the discussion in #3186 has a clearer description of the problem.