Description
When hiding network objects for a client via NetworkObject.NetworkHide, if any network objects have been shown to the client via NetworkObject.NetworkShow on the same frame prior to the hide calls, they will fail with a VisibilityChangeException on the server and remain visible for the client.
Reproduce Steps
- Start a server and connect a single client to it
- Spawn two network objects (A and B), both with visibility for the client
- Wait at least a frame and remove visibility for object A via
NetworkObject.NetworkHide
- Wait at least a frame again, and then on the same frame, show object A to the client again via
NetworkObject.NetworkShow and hide object B via NetworkObject.NetworkHide (in that order)
- Observe outcome
Actual Outcome
On the server, a VisibilityChangeException will be logged for object B with the message "The object is already hidden", and on the client they will have visibility of both object A and object B.
Expected Outcome
No exception would be logged, and the client would have visibility of object A only.
Environment
- OS: Windows 10
- Unity Version: 2021.3.16f1
- Netcode Version: 1.3.1
- Netcode Commit: 469b46f
Additional Context
This issue appears to have been introduced in a commit on December 12 of last year, where the NetworkObject.NetworkHide call was changed to incorporate a new NetworkManager.RemoveObjectFromShowingTo call. The purpose of that call appears to be for checking if the object being hidden was just told to show for the client by checking if it's in the ObjectsToShowToClient dictionary, as seen here:
// returns whether any matching objects would have become visible and were returned to hidden state
internal bool RemoveObjectFromShowingTo(NetworkObject networkObject, ulong clientId)
{
var ret = false;
if (!ObjectsToShowToClient.ContainsKey(clientId))
{
return false;
}
// probably overkill, but deals with multiple entries
while (ObjectsToShowToClient[clientId].Contains(networkObject))
{
Debug.LogWarning(
"Object was shown and hidden from the same client in the same Network frame. As a result, the client will _not_ receive a NetworkSpawn");
ObjectsToShowToClient[clientId].Remove(networkObject);
ret = true;
}
networkObject.Observers.Remove(clientId);
return ret;
}
However, this does not actually check if that dictionary contains the object in question as an entry to its list value before calling networkObject.Observers.Remove(clientId);, meaning any object that's being shown to the client on this frame/tick will cause the initial return statement to be bypassed and the object to be removed from the Observers list here, rather than in NetworkObject.NetworkHide. As a result, in the following block in the NetworkHide call, the exception noted in the outcome will be triggered and no DestroyObjectMessage will be sent to the client informing them to hide the object:
if (!NetworkManager.RemoveObjectFromShowingTo(this, clientId))
{
if (!Observers.Contains(clientId))
{
throw new VisibilityChangeException("The object is already hidden");
}
Observers.Remove(clientId);
var message = new DestroyObjectMessage
{
NetworkObjectId = NetworkObjectId,
DestroyGameObject = !IsSceneObject.Value
};
// Send destroy call
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId);
NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size);
}
I'm currently investigating a fix on my organization's branch of NGO, but I believe it may be that the networkObject.Observers.Remove(clientId); should just have an if (ret) check to ensure that the object was actually in the ObjectsToShowToClient list value. This would also prevent the exception as the code would no longer enter the above block in NetworkObject.NetworkHide for this scenario.
NOTE: at the time of this report the RemoveObjectFromShowingTo has been moved from the NetworkManager to the SpawnManager, but the code otherwise appears identical.
Description
When hiding network objects for a client via
NetworkObject.NetworkHide, if any network objects have been shown to the client viaNetworkObject.NetworkShowon the same frame prior to the hide calls, they will fail with a VisibilityChangeException on the server and remain visible for the client.Reproduce Steps
NetworkObject.NetworkHideNetworkObject.NetworkShowand hide object B viaNetworkObject.NetworkHide(in that order)Actual Outcome
On the server, a VisibilityChangeException will be logged for object B with the message "The object is already hidden", and on the client they will have visibility of both object A and object B.
Expected Outcome
No exception would be logged, and the client would have visibility of object A only.
Environment
Additional Context
This issue appears to have been introduced in a commit on December 12 of last year, where the
NetworkObject.NetworkHidecall was changed to incorporate a newNetworkManager.RemoveObjectFromShowingTocall. The purpose of that call appears to be for checking if the object being hidden was just told to show for the client by checking if it's in theObjectsToShowToClientdictionary, as seen here:However, this does not actually check if that dictionary contains the object in question as an entry to its list value before calling
networkObject.Observers.Remove(clientId);, meaning any object that's being shown to the client on this frame/tick will cause the initial return statement to be bypassed and the object to be removed from theObserverslist here, rather than inNetworkObject.NetworkHide. As a result, in the following block in the NetworkHide call, the exception noted in the outcome will be triggered and noDestroyObjectMessagewill be sent to the client informing them to hide the object:I'm currently investigating a fix on my organization's branch of NGO, but I believe it may be that the
networkObject.Observers.Remove(clientId);should just have anif (ret)check to ensure that the object was actually in theObjectsToShowToClientlist value. This would also prevent the exception as the code would no longer enter the above block inNetworkObject.NetworkHidefor this scenario.NOTE: at the time of this report the
RemoveObjectFromShowingTohas been moved from the NetworkManager to the SpawnManager, but the code otherwise appears identical.