Skip to content

Commit 9ee8fad

Browse files
committed
Move ghost spawning to GhostSpawnManager
1 parent 7b528d4 commit 9ee8fad

6 files changed

Lines changed: 311 additions & 252 deletions

File tree

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,11 +1777,9 @@ private void OnDestroy()
17771777
var spawnManager = NetworkManager.SpawnManager;
17781778

17791779
// Always attempt to remove from scene changed updates
1780-
spawnManager?.RemoveNetworkObjectFromSceneChangedUpdates(this);
1780+
spawnManager?.MarkNetworkObjectAsDestroying(this);
17811781

17821782
#if UNIFIED_NETCODE
1783-
spawnManager?.GhostsPendingSpawn.Remove(NetworkObjectId);
1784-
spawnManager?.GhostsPendingSynchronization.Remove(NetworkObjectId);
17851783
// N4E controls this on the client, allow this if there is a ghost
17861784
if (IsSpawned && !HasGhost && !networkManager.ShutdownInProgress)
17871785
#else
@@ -3719,44 +3717,29 @@ private void Start()
37193717

37203718
private void InitGhost()
37213719
{
3722-
// All instances with Ghosts are automatically registered
3723-
if (HasGhost && NetworkObjectBridge && !GhostAdapter.IsPrefab())
3720+
if (!NetworkManager.IsListening)
37243721
{
37253722
if (NetworkManager.LogLevel == LogLevel.Developer)
37263723
{
3727-
Debug.Log($"[{nameof(NetworkObject)}] GhostBridge {name} detected and instantiated.");
3728-
}
3729-
if (GhostAdapter.WasInitialized && NetworkObjectBridge.NetworkObjectId.Value != 0)
3730-
{
3731-
RegisterGhostBridge();
3724+
Debug.LogWarning($"[{nameof(NetworkObject)}] Did not register because there is no session in progress!");
37323725
}
3726+
return;
37333727
}
3734-
}
37353728

3736-
internal void RegisterGhostBridge()
3737-
{
3738-
if (NetworkManager.LogLevel == LogLevel.Developer)
3729+
if (!HasGhost || !NetworkObjectBridge || GhostAdapter.IsPrefab())
37393730
{
3740-
Debug.Log($"[{nameof(NetworkObject)}][{nameof(NetworkObjectId)}] NetworkObjectBridge notified instance exists with assigned ID of: {NetworkObjectBridge.NetworkObjectId.Value}");
3741-
if (!NetworkManager.IsListening)
3742-
{
3743-
Debug.LogWarning($"[{nameof(NetworkObject)}] Did not register because there is no session in progress!");
3744-
return;
3745-
}
3731+
// Nothing to register
3732+
return;
37463733
}
37473734

3748-
// Set when running through integration tests in order to initially bypass the
3749-
// normal registration. This is because at this point in the instantiation process,
3750-
// NetworkObject's NetworkManager is pointing to the singleton which means all instances
3751-
// (even if intended to be for a specific client) will end up registering with whichever
3752-
// NetworkManager instance is being pointed to by the singleton.
3753-
if (NetworkSpawnManager.RegisterPendingGhost != null)
3735+
// All instances with Ghosts are automatically registered
3736+
if (NetworkManager.LogLevel == LogLevel.Developer)
37543737
{
3755-
NetworkSpawnManager.RegisterPendingGhost(this, NetworkObjectBridge.NetworkObjectId.Value);
3738+
Debug.Log($"[{nameof(NetworkObject)}] GhostBridge {name} detected and instantiated.");
37563739
}
3757-
else if (!NetworkManager.IsServer)
3740+
if (GhostAdapter.WasInitialized && NetworkObjectBridge.NetworkObjectId.Value != 0)
37583741
{
3759-
NetworkManager.SpawnManager.RegisterGhostPendingSpawn(this, NetworkObjectBridge.NetworkObjectId.Value);
3742+
NetworkManager.SpawnManager.GhostSpawnManager.RegisterGhostBridge(NetworkObjectBridge.NetworkObjectId.Value, this);
37603743
}
37613744
}
37623745
#endif

com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,33 +1124,8 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager)
11241124
// information during synchronization.
11251125
if (serializedObject.HasGhost)
11261126
{
1127-
if (!networkManager.SpawnManager.GhostsPendingSpawn.ContainsKey(serializedObject.NetworkObjectId))
1127+
if (networkManager.SpawnManager.GhostSpawnManager.ShouldDeferGhostSceneObject(serializedObject, InternalBuffer))
11281128
{
1129-
if (networkManager.LogLevel == LogLevel.Developer)
1130-
{
1131-
UnityEngine.Debug.Log($"[{nameof(SceneEventData)}][{nameof(SynchronizeSceneNetworkObjects)}] Deferring creation of NetworkObjectId-{serializedObject.NetworkObjectId} to wait for Ghost.");
1132-
}
1133-
1134-
var newEntry = new PendingGhostSpawnEntry()
1135-
{
1136-
RegistrationTime = UnityEngine.Time.realtimeSinceStartup,
1137-
SerializedObject = serializedObject,
1138-
Buffer = new FastBufferReader(InternalBuffer, Allocator.Persistent, serializedObject.SynchronizationDataSize, InternalBuffer.Position)
1139-
};
1140-
1141-
spawnManager.RegisterGhostPendingSynchronization(newEntry);
1142-
InternalBuffer.Seek(InternalBuffer.Position + serializedObject.SynchronizationDataSize);
1143-
continue;
1144-
}
1145-
else if (networkManager.SpawnManager.GhostsPendingSpawn[serializedObject.NetworkObjectId] == null)
1146-
{
1147-
if (networkManager.LogLevel == LogLevel.Developer)
1148-
{
1149-
UnityEngine.Debug.Log($"[{nameof(SceneEventData)}][{nameof(SynchronizeSceneNetworkObjects)}] Dropping creation of NetworkObjectId-{serializedObject.NetworkObjectId} as it has an entry but no longer exists!");
1150-
}
1151-
// If it no longer exists, then just remove the entry and skip it.
1152-
InternalBuffer.Seek(InternalBuffer.Position + serializedObject.SynchronizationDataSize);
1153-
networkManager.SpawnManager.GhostsPendingSpawn.Remove(serializedObject.NetworkObjectId);
11541129
continue;
11551130
}
11561131
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
#if UNIFIED_NETCODE
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using Unity.Collections;
5+
using Unity.Netcode.Logging;
6+
using UnityEngine;
7+
8+
namespace Unity.Netcode
9+
{
10+
internal class GhostSpawnManager
11+
{
12+
private readonly NetworkManager m_NetworkManager;
13+
private readonly ContextualLogger m_Log;
14+
15+
public GhostSpawnManager(NetworkManager networkManager)
16+
{
17+
m_NetworkManager = networkManager;
18+
m_Log = new ContextualLogger((Object)networkManager, networkManager);
19+
}
20+
21+
private readonly Dictionary<ulong, NetworkObject> m_GhostsPendingSpawn = new();
22+
23+
// TODO: We might want to make this a mock interface but temporary solution to validate
24+
// the need to assure we are registering with the right NetworkManager instance when testing (everything
25+
// will use the singleton during Awake and Start when we need to register).
26+
internal delegate void RegisterPendingGhostDelegateHandler(NetworkObject networkObject, ulong networkObjectId);
27+
28+
internal static RegisterPendingGhostDelegateHandler RegisterPendingGhost;
29+
30+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
31+
private static void RuntimeInitializeOnLoad() => RegisterPendingGhost = null;
32+
33+
internal void RegisterGhostBridge(ulong networkObjectId, NetworkObject networkObject)
34+
{
35+
m_Log.Info(new Context(LogLevel.Developer, $"Registering {nameof(NetworkObject)} for ghost bridge").AddInfo(nameof(NetworkObject.NetworkObjectId), networkObjectId));
36+
37+
// Set when running through integration tests in order to initially bypass the
38+
// normal registration. This is because at this point in the instantiation process,
39+
// NetworkObject's NetworkManager is pointing to the singleton which means all instances
40+
// (even if intended to be for a specific client) will end up registering with whichever
41+
// NetworkManager instance is being pointed to by the singleton.
42+
if (RegisterPendingGhost != null)
43+
{
44+
RegisterPendingGhost(networkObject, networkObjectId);
45+
}
46+
else if (!m_NetworkManager.IsServer)
47+
{
48+
RegisterGhostPendingSpawn(networkObject, networkObjectId);
49+
}
50+
}
51+
52+
internal void RegisterGhostPendingSpawn(NetworkObject networkObject, ulong networkObjectId)
53+
{
54+
m_Log.Info(new Context(LogLevel.Developer, $"Registering {nameof(NetworkObject)} for ghost spawn").AddTag(networkObject.name).AddInfo(nameof(NetworkObject.NetworkObjectId), networkObjectId));
55+
56+
if (!m_GhostsPendingSpawn.TryAdd(networkObjectId, networkObject))
57+
{
58+
m_Log.Error(new Context(LogLevel.Normal, $"{nameof(NetworkObject)} has already been registered as a pending ghost!").AddTag(networkObject.name).AddInfo(nameof(NetworkObject.NetworkObjectId), networkObjectId));
59+
return;
60+
}
61+
62+
// TODO-REVIEW-BELOW: *** This is very likely no longer an issue with the new connection sequence ***
63+
// TODO-UNIFIED: We need a better way to preserve any hybrid instances pending NGO spawn.
64+
// Edge-Case scenario: During initial client synchronization (i.e. !m_NetworkManager.IsConnectedClient).
65+
//
66+
// Description: A client can receive snapshots before finishing the NGO synchronization process.
67+
// This is when an edge case scenario can happen where the initial NGO synchronization information
68+
// can include new scenes to load. If one of those scenes is configured to load in SingleMode, then
69+
// any instantiated ghosts pending synchronization would be instantiated in whatever the currently
70+
// active scene was when the client was processing the synchronization data. If the ghosts pending
71+
// synchronization are in the currently active scene when the new scene is loaded in SingleMode, then
72+
// they would be destroyed.
73+
//
74+
// Current Fix:
75+
// If the client is not yet synchronized, then any ghost pending spawn get migrated into the DDOL.
76+
//
77+
// Further review:
78+
// We need to make sure that we are migrating NetworkObjects into their assigned scene (if scene
79+
// management is enabled). Currently, we assume all instances were in the DDOL and just migrate
80+
// them into the currently active scene upon spawn.
81+
if (!m_NetworkManager.IsConnectedClient && !m_GhostsPendingSynchronization.ContainsKey(networkObjectId))
82+
{
83+
Object.DontDestroyOnLoad(networkObject.gameObject);
84+
}
85+
else // There is matching spawn data for this pending Ghost, process the pending spawn for this hybrid instance.
86+
{
87+
m_NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnGhostSpawned, networkObjectId);
88+
if (m_GhostsPendingSynchronization.ContainsKey(networkObjectId))
89+
{
90+
ProcessGhostPendingSynchronization(networkObjectId);
91+
}
92+
}
93+
}
94+
95+
internal bool TryGetGhostNetworkObjectForSpawn(NetworkObject.SerializedObject serializedObject, [NotNullWhen(true)] out NetworkObject networkObject)
96+
{
97+
var networkObjectId = serializedObject.NetworkObjectId;
98+
99+
// TODO-UNIFIED: Get this working somehow (or if not possible prevent this from happening prior to getting to this point)
100+
if (serializedObject.HasInstantiationData)
101+
{
102+
m_Log.Error(new Context(LogLevel.Error, $"{nameof(NetworkObject)} Pre-spawn instantiation data does not work in this version!").AddInfo(nameof(NetworkObject.NetworkObjectId), networkObjectId));
103+
}
104+
if (!m_GhostsPendingSpawn.Remove(networkObjectId, out networkObject) || networkObject == null)
105+
{
106+
m_Log.Error(new Context(LogLevel.Error, $"Attempting to spawn {nameof(NetworkObject)} with no instance to spawn!").AddInfo(nameof(NetworkObject.NetworkObjectId), networkObjectId));
107+
return false;
108+
}
109+
110+
// TODO-UNIFIED: We need a better way to preserve any hybrid instances pending NGO spawn.
111+
// NOTE: We might be able to use the NetworkSceneHandle to get the associated local scene handle to which we can use to get the targeted scene.
112+
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(networkObject.gameObject, UnityEngine.SceneManagement.SceneManager.GetActiveScene());
113+
return true;
114+
}
115+
116+
private bool m_GhostsArePendingSynchronization;
117+
private readonly Dictionary<ulong, PendingGhostSpawnEntry> m_GhostsPendingSynchronization = new();
118+
internal void RegisterGhostPendingSynchronization(PendingGhostSpawnEntry pendingGhostSpawnEntry)
119+
{
120+
var networkObjectId = pendingGhostSpawnEntry.SerializedObject.NetworkObjectId;
121+
m_Log.Info(new Context(LogLevel.Developer, $"Registering {nameof(NetworkObject.SerializedObject)} for pending synchronization").AddInfo(nameof(NetworkObject.NetworkObjectId), networkObjectId));
122+
123+
m_GhostsPendingSynchronization.TryAdd(networkObjectId, pendingGhostSpawnEntry);
124+
m_GhostsArePendingSynchronization = true;
125+
}
126+
127+
internal NetworkObject ProcessGhostPendingSynchronization(ulong networkObjectId, bool removeUponSpawn = true)
128+
{
129+
var ghostPendingSync = m_GhostsPendingSynchronization[networkObjectId];
130+
var serializedObject = ghostPendingSync.SerializedObject;
131+
var reader = ghostPendingSync.Buffer;
132+
if (removeUponSpawn)
133+
{
134+
m_GhostsPendingSynchronization.Remove(networkObjectId);
135+
}
136+
137+
if (serializedObject.IsSceneObject)
138+
{
139+
m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle);
140+
}
141+
var networkObject = NetworkObject.Deserialize(serializedObject, reader, m_NetworkManager);
142+
// TODO-UNIFIED: How do we handle the "all in-scene placed objects are spawned notification"?
143+
//if (serializedObject.IsSceneObject)
144+
//{
145+
// networkObject.InternalInSceneNetworkObjectsSpawned();
146+
//}
147+
if (removeUponSpawn)
148+
{
149+
m_GhostsPendingSynchronization.Remove(networkObjectId);
150+
m_GhostsArePendingSynchronization = m_GhostsPendingSynchronization.Count > 0;
151+
ghostPendingSync.Buffer.Dispose();
152+
}
153+
return networkObject;
154+
}
155+
156+
157+
private readonly HashSet<ulong> m_GhostSynchronizationPendingRemoval = new();
158+
159+
internal void ProcessAllGhostsPendingSynchronization()
160+
{
161+
var spawnTimeout = m_NetworkManager.NetworkConfig.SpawnTimeout;
162+
if (!m_GhostsArePendingSynchronization)
163+
{
164+
return;
165+
}
166+
foreach (var ghost in m_GhostsPendingSynchronization)
167+
{
168+
var networkObjectId = ghost.Value.SerializedObject.NetworkObjectId;
169+
if (m_GhostsPendingSpawn.ContainsKey(networkObjectId))
170+
{
171+
// Process it, but don't remove it as we handle that a little later
172+
ProcessGhostPendingSynchronization(ghost.Value.SerializedObject.NetworkObjectId, false);
173+
m_GhostSynchronizationPendingRemoval.Add(networkObjectId);
174+
}
175+
else
176+
if ((ghost.Value.RegistrationTime + spawnTimeout) < Time.realtimeSinceStartup)
177+
{
178+
m_Log.Info(new Context(LogLevel.Developer, $"Registering {nameof(NetworkObject.SerializedObject)} for pending synchronization").AddInfo(nameof(NetworkObject.NetworkObjectId), networkObjectId));
179+
180+
// Timed out entries are removed too
181+
m_GhostSynchronizationPendingRemoval.Add(ghost.Key);
182+
}
183+
}
184+
185+
foreach (var networkObjectId in m_GhostSynchronizationPendingRemoval)
186+
{
187+
var entry = m_GhostsPendingSynchronization[networkObjectId];
188+
m_GhostsPendingSynchronization.Remove(networkObjectId);
189+
entry.Buffer.Dispose();
190+
}
191+
m_GhostSynchronizationPendingRemoval.Clear();
192+
m_GhostsArePendingSynchronization = m_GhostsPendingSynchronization.Count > 0;
193+
}
194+
195+
internal void MarkNetworkObjectAsDestroying(ulong networkObjectId)
196+
{
197+
m_GhostsPendingSpawn.Remove(networkObjectId);
198+
m_GhostsPendingSynchronization.Remove(networkObjectId);
199+
}
200+
201+
/// <summary>
202+
/// Used in scene managment synchronization.
203+
/// Verifies if this <see cref="NetworkObject.SerializedObject"/> should defer its spawn until the associated GhostObject is created.
204+
/// </summary>
205+
/// <param name="serializedObject">The object to check</param>
206+
/// <param name="internalBuffer"><see cref="SceneEventData"/>'s <see cref="SceneEventData.InternalBuffer"/> to save the buffer with data for the deferred spawn.</param>
207+
/// <returns>True if the serialized object should should wait for the associated ghost object; false otherwise</returns>
208+
internal bool ShouldDeferGhostSceneObject(NetworkObject.SerializedObject serializedObject, FastBufferReader internalBuffer)
209+
{
210+
if (!m_GhostsPendingSpawn.TryGetValue(serializedObject.NetworkObjectId, out var existingObject))
211+
{
212+
m_Log.Info(new Context(LogLevel.Developer, $"Deferring creation of InScenePlaced {nameof(NetworkObject)} to wait for Ghost.").AddTag("SynchronizeSceneNetworkObjects").AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId));
213+
214+
var newEntry = new PendingGhostSpawnEntry()
215+
{
216+
RegistrationTime = Time.realtimeSinceStartup,
217+
SerializedObject = serializedObject,
218+
Buffer = new FastBufferReader(internalBuffer, Allocator.Persistent, serializedObject.SynchronizationDataSize, internalBuffer.Position)
219+
};
220+
221+
RegisterGhostPendingSynchronization(newEntry);
222+
internalBuffer.Seek(internalBuffer.Position + serializedObject.SynchronizationDataSize);
223+
return true;
224+
}
225+
226+
if (existingObject == null)
227+
{
228+
m_Log.Info(new Context(LogLevel.Developer, $"Dropping creation of InScenePlaced {nameof(NetworkObject)} as it has an entry but no longer exists!").AddTag("SynchronizeSceneNetworkObjects").AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId));
229+
230+
// If it no longer exists, then just remove the entry and skip it.
231+
internalBuffer.Seek(internalBuffer.Position + serializedObject.SynchronizationDataSize);
232+
m_GhostsPendingSpawn.Remove(serializedObject.NetworkObjectId);
233+
return true;
234+
}
235+
236+
return false;
237+
}
238+
239+
/// <summary>
240+
/// Finalizer that ensures proper cleanup of manager resources
241+
/// </summary>
242+
~GhostSpawnManager()
243+
{
244+
Shutdown();
245+
}
246+
247+
internal void Shutdown()
248+
{
249+
m_GhostsPendingSpawn.Clear();
250+
m_GhostsPendingSynchronization.Clear();
251+
}
252+
}
253+
}
254+
#endif

com.unity.netcode.gameobjects/Runtime/Spawning/GhostSpawnManager.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)