Skip to content

Commit

Permalink
fix: in-scene placed NetworkObject will not spawn client-side when di…
Browse files Browse the repository at this point in the history
…sabled upon being despawned [MTT-4832] (Unity-Technologies#2239)

* fix
This resolves the issue with in-scene placed NetworkObjects that are disabled when despawned not being able to re-spawn again and handles synchronizing despawned in-scene placed NetworkObjects during a scene switch (LoadSceneMode.Single).

* test
Added integration test that validates disabling NetworkObjects when despawned works with currently connected clients, late joining clients, and when scene switching (LoadSceneMode.Single) while also having the server despawn the in-scene placed NetworkObject upon its first spawn (i.e. so it starts off not visible/active to the clients when they finish the scene switch).
  • Loading branch information
NoelStephensUnity authored Oct 7, 2022
1 parent 4393621 commit e10c266
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 106 deletions.
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- Fixed issue where an in-scene placed NetworkObject would not invoke NetworkBehaviour.OnNetworkSpawn if the GameObject was disabled when it was despawned. (#2239)
- Fixed issue where clients were not rebuilding the `NetworkConfig` hash value for each unique connection request. (#2226)
- Fixed the issue where player objects were not taking the `DontDestroyWithOwner` property into consideration when a client disconnected. (#2225)
- Fixed issue where `SceneEventProgress` would not complete if a client late joins while it is still in progress. (#2222)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,9 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene)
}
}

// Add any despawned when spawned in-scene placed NetworkObjects to the scene event data
sceneEventData.AddDespawnedInSceneNetworkObjects();

// Set the server's scene's handle so the client can build a look up table
sceneEventData.SceneHandle = scene.handle;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ internal void AddSpawnedNetworkObjects()
internal void AddDespawnedInSceneNetworkObjects()
{
m_DespawnedInSceneObjectsSync.Clear();
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>().Where((c) => c.NetworkManager == m_NetworkManager);
// Find all active and non-active in-scene placed NetworkObjects
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>(includeInactive: true).Where((c) => c.NetworkManager == m_NetworkManager);
foreach (var sobj in inSceneNetworkObjects)
{
if (sobj.IsSceneObject.HasValue && sobj.IsSceneObject.Value && !sobj.IsSpawned)
Expand Down Expand Up @@ -461,7 +462,6 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer)
for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i)
{
var noStart = writer.Position;
var sceneObject = m_DespawnedInSceneObjectsSync[i].GetMessageSceneObject(TargetClientId);
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle());
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash);
var noStop = writer.Position;
Expand Down Expand Up @@ -507,6 +507,15 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer)
}
}

// Write the number of despawned in-scene placed NetworkObjects
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count);
// Write the scene handle and GlobalObjectIdHash value
for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i)
{
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle());
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash);
}

var tailPosition = writer.Position;
// Reposition to our count position to the head before we wrote our object count
writer.Seek(headPosition);
Expand Down Expand Up @@ -624,6 +633,8 @@ internal void DeserializeScenePlacedObjects()
sceneObject.Deserialize(InternalBuffer);
NetworkObject.AddSceneObject(sceneObject, InternalBuffer, m_NetworkManager);
}
// Now deserialize the despawned in-scene placed NetworkObjects list (if any)
DeserializeDespawnedInScenePlacedNetworkObjects();
}
finally
{
Expand Down Expand Up @@ -746,6 +757,84 @@ internal void WriteClientSynchronizationResults(FastBufferWriter writer)
}
}

/// <summary>
/// For synchronizing any despawned in-scene placed NetworkObjects that were
/// despawned by the server during synchronization or scene loading
/// </summary>
private void DeserializeDespawnedInScenePlacedNetworkObjects()
{
// Process all de-spawned in-scene NetworkObjects for this network session
m_DespawnedInSceneObjects.Clear();
InternalBuffer.ReadValueSafe(out int despawnedObjectsCount);
var sceneCache = new Dictionary<int, Dictionary<uint, NetworkObject>>();

for (int i = 0; i < despawnedObjectsCount; i++)
{
// We just need to get the scene
ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle);
ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash);
var sceneRelativeNetworkObjects = new Dictionary<uint, NetworkObject>();
if (!sceneCache.ContainsKey(networkSceneHandle))
{
if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle))
{
var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle];
if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle))
{
var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle];

// Find all active and non-active in-scene placed NetworkObjects
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>(includeInactive: true).Where((c) =>
c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList();

foreach (var inSceneObject in inSceneNetworkObjects)
{
if (!sceneRelativeNetworkObjects.ContainsKey(inSceneObject.GlobalObjectIdHash))
{
sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject);
}
}
// Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time
sceneCache.Add(networkSceneHandle, sceneRelativeNetworkObjects);
}
else
{
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative local scene handle {localSceneHandle}!");
}
}
else
{
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!");
}
}
else // Use the cached NetworkObjects if they exist
{
sceneRelativeNetworkObjects = sceneCache[networkSceneHandle];
}

// Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for
if (sceneRelativeNetworkObjects.ContainsKey(globalObjectIdHash))
{
// Since this is a NetworkObject that was never spawned, we just need to send a notification
// out that it was despawned so users can make adjustments
sceneRelativeNetworkObjects[globalObjectIdHash].InvokeBehaviourNetworkDespawn();
if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary<int, NetworkObject>());
}

if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle()))
{
m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]);
}
}
else
{
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) could not be found!");
}
}
}

/// <summary>
/// Client Side:
/// During the processing of a server sent Event_Sync, this method will be called for each scene once
Expand Down Expand Up @@ -779,72 +868,9 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager)
}
}

// Process all de-spawned in-scene NetworkObjects for this network session
m_DespawnedInSceneObjects.Clear();
InternalBuffer.ReadValueSafe(out int despawnedObjectsCount);
var sceneCache = new Dictionary<int, Dictionary<uint, NetworkObject>>();

for (int i = 0; i < despawnedObjectsCount; i++)
{
// We just need to get the scene
ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle);
ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash);
var sceneRelativeNetworkObjects = new Dictionary<uint, NetworkObject>();
if (!sceneCache.ContainsKey(networkSceneHandle))
{
if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle))
{
var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle];
if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle))
{
var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle];
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>().Where((c) =>
c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList();
// Now deserialize the despawned in-scene placed NetworkObjects list (if any)
DeserializeDespawnedInScenePlacedNetworkObjects();

foreach (var inSceneObject in inSceneNetworkObjects)
{
sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject);
}
// Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time
sceneCache.Add(networkSceneHandle, sceneRelativeNetworkObjects);
}
else
{
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative local scene handle {localSceneHandle}!");
}
}
else
{
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!");
}
}
else // Use the cached NetworkObjects if they exist
{
sceneRelativeNetworkObjects = sceneCache[networkSceneHandle];
}

// Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for
if (sceneRelativeNetworkObjects.ContainsKey(globalObjectIdHash))
{
// Since this is a NetworkObject that was never spawned, we just need to send a notification
// out that it was despawned so users can make adjustments
sceneRelativeNetworkObjects[globalObjectIdHash].InvokeBehaviourNetworkDespawn();
if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary<int, NetworkObject>());
}

if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle()))
{
m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]);
}

}
else
{
UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) could not be found!");
}
}
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,13 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!");
}
}

// Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so
// NetworkBehaviours will have their OnNetworkSpawn method invoked
if (networkObject != null && !networkObject.gameObject.activeInHierarchy)
{
networkObject.gameObject.SetActive(true);
}
}

if (networkObject != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,87 +8,91 @@ namespace TestProject.ManualTests
/// <summary>
/// Used for manually testing spawning and despawning in-scene
/// placed NetworkObjects
///
/// Note: We do not destroy in-scene placed NetworkObjects, but
/// users must handle visibility (rendering wise) when the in-scene
/// NetworkObject is spawned and despawned. This class just enables
/// or disabled the mesh renderer.
/// </summary>
public class DespawnInSceneNetworkObject : NetworkBehaviour
{
[Tooltip("When set, the server will despawn the NetworkObject upon its first spawn.")]
public bool StartDespawned;

private Coroutine m_ScanInputHandle;
private MeshRenderer m_MeshRenderer;

private void Start()
{
if (!IsSpawned)
{
m_MeshRenderer = GetComponent<MeshRenderer>();
if (m_MeshRenderer != null)
{
m_MeshRenderer.enabled = false;
}
}
}
// Used to prevent the server from despawning
// the in-scene placed NetworkObject after the
// first spawn (only if StartDespawned is true)
private bool m_ServerDespawnedOnFirstSpawn;

private NetworkManager m_CachedNetworkManager;

public override void OnNetworkSpawn()
{
Debug.Log($"{name} spawned!");
m_MeshRenderer = GetComponent<MeshRenderer>();
if (m_MeshRenderer != null)
{
m_MeshRenderer.enabled = true;
}

if (!IsServer)
{
return;
}

m_CachedNetworkManager = NetworkManager;

if (m_ScanInputHandle == null)
{
m_ScanInputHandle = StartCoroutine(ScanInput());
// Using the NetworkManager to create the coroutine so it is not deactivated
// when the GameObject this NetworkBehaviour is attached to is disabled.
m_ScanInputHandle = NetworkManager.StartCoroutine(ScanInput(NetworkObject));
}

// m_ServerDespawnedOnFirstSpawn prevents the server from always
// despawning on the server-side after the first spawn.
if (StartDespawned && !m_ServerDespawnedOnFirstSpawn)
{
m_ServerDespawnedOnFirstSpawn = true;
NetworkObject.Despawn(false);
}
}

public override void OnNetworkDespawn()
{
if (m_MeshRenderer != null)
{
m_MeshRenderer.enabled = false;
}
// It is OK to disable in-scene placed NetworkObjects upon
// despawning. When re-spawned the client-side will re-activate
// the GameObject, while the server-side must set the GameObject
// active itself.
gameObject.SetActive(false);

Debug.Log($"{name} despawned!");
base.OnNetworkDespawn();
}

public override void OnDestroy()
{
if (m_ScanInputHandle != null)
if (m_ScanInputHandle != null && m_CachedNetworkManager != null)
{
StopCoroutine(m_ScanInputHandle);
m_CachedNetworkManager.StopCoroutine(m_ScanInputHandle);
}
m_ScanInputHandle = null;
base.OnDestroy();
}

private IEnumerator ScanInput()
private IEnumerator ScanInput(NetworkObject networkObject)
{
while (true)
{
try
{
if (IsSpawned)
if (networkObject.IsSpawned)
{
if (Input.GetKeyDown(KeyCode.Backspace))
{
Debug.Log($"{name} should despawn.");
NetworkObject.Despawn(false);
networkObject.Despawn(false);
}
}
else if (NetworkManager.Singleton && NetworkManager.Singleton.IsListening)
{
if (Input.GetKeyDown(KeyCode.Backspace))
{
Debug.Log($"{name} should spawn.");
NetworkObject.Spawn();
networkObject.gameObject.SetActive(true);
networkObject.Spawn();
}
}
}
Expand All @@ -99,7 +103,6 @@ private IEnumerator ScanInput()

yield return null;
}

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2250,6 +2250,11 @@ PrefabInstance:
propertyPath: m_Name
value: InSceneObjectToDespawn
objectReference: {fileID: 0}
- target: {fileID: 4518755925279129999, guid: 3a854a190ab5b1b4fb00bec725fdda9e,
type: 3}
propertyPath: StartDespawned
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 3a854a190ab5b1b4fb00bec725fdda9e, type: 3}
--- !u!1 &1008611498
Expand Down Expand Up @@ -2466,7 +2471,6 @@ MonoBehaviour:
m_ProtocolType: 0
m_MaxPacketQueueSize: 128
m_MaxPayloadSize: 512000
m_MaxSendQueueSize: 4096000
m_HeartbeatTimeoutMS: 500
m_ConnectTimeoutMS: 1000
m_MaxConnectAttempts: 60
Expand Down
Loading

0 comments on commit e10c266

Please sign in to comment.