using System.Collections.Generic; using System; using System.Linq; using Unity.Collections; using UnityEngine.SceneManagement; namespace Unity.Netcode { /// /// The different types of scene events communicated between a server and client.
/// Used by for messages.
/// Note: This is only when is enabled.
/// See also:
/// ///
public enum SceneEventType : byte { /// /// Load a scene
/// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Both server and client are notified a load scene event started ///
Load, /// /// Unload a scene
/// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Both server and client are notified an unload scene event started. ///
Unload, /// /// Synchronizes current game session state for newly approved clients
/// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Server and Client receives a local notification (server receives the ClientId being synchronized). ///
Synchronize, /// /// Game session re-synchronization of NetworkObjects that were destroyed during a event
/// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Both server and client receive a local notification
///
ReSynchronize, /// /// All clients have finished loading a scene
/// Invocation: Server Side
/// Message Flow: Server to Client
/// Event Notification: Both server and client receive a local notification containing the clients that finished /// as well as the clients that timed out(if any). ///
LoadEventCompleted, /// /// All clients have unloaded a scene
/// Invocation: Server Side
/// Message Flow: Server to Client
/// Event Notification: Both server and client receive a local notification containing the clients that finished /// as well as the clients that timed out(if any). ///
UnloadEventCompleted, /// /// A client has finished loading a scene
/// Invocation: Client Side
/// Message Flow: Client to Server
/// Event Notification: Both server and client receive a local notification. ///
LoadComplete, /// /// A client has finished unloading a scene
/// Invocation: Client Side
/// Message Flow: Client to Server
/// Event Notification: Both server and client receive a local notification. ///
UnloadComplete, /// /// A client has finished synchronizing from a event
/// Invocation: Client Side
/// Message Flow: Client to Server
/// Event Notification: Both server and client receive a local notification. ///
SynchronizeComplete, } /// /// Used by for messages /// Note: This is only when is enabled.
/// See also: ///
internal class SceneEventData : IDisposable { internal SceneEventType SceneEventType; internal LoadSceneMode LoadSceneMode; internal Guid SceneEventProgressId; internal uint SceneEventId; internal uint SceneHash; internal int SceneHandle; // Used by the client during synchronization internal uint ClientSceneHash; internal int ClientSceneHandle; /// Only used for scene events, this assures permissions when writing /// NetworkVariable information. If that process changes, then we need to update this internal ulong TargetClientId; private Dictionary> m_SceneNetworkObjects; private Dictionary m_SceneNetworkObjectDataOffsets; /// /// Client or Server Side: /// Client side: Generates a list of all NetworkObjects by their NetworkObjectId that was spawned during th synchronization process /// Server side: Compares list from client to make sure client didn't drop a message about a NetworkObject being despawned while it /// was synchronizing (if so server will send another message back to the client informing the client of NetworkObjects to remove) /// spawned during an initial synchronization. /// private List m_NetworkObjectsSync = new List(); /// /// Server Side Re-Synchronization: /// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned, /// the server will compile a list and send back an Event_ReSync message to the client. /// private List m_NetworkObjectsToBeRemoved = new List(); private bool m_HasInternalBuffer; internal FastBufferReader InternalBuffer; private NetworkManager m_NetworkManager; internal List ClientsCompleted; internal List ClientsTimedOut; internal Queue ScenesToSynchronize; internal Queue SceneHandlesToSynchronize; /// /// Server Side: /// Add a scene and its handle to the list of scenes the client should load before synchronizing /// Since scene handles are not the same per instance, the client builds a server scene handle to /// client scene handle lookup table. /// Why include the scene handle? In order to support loading of the same additive scene more than once /// we must distinguish which scene we are talking about when the server tells the client to unload a scene. /// The server will always communicate its local relative scene's handle and the client will determine its /// local relative handle from the table being built. /// Look for usage to see where /// entries are being added to or removed from the table /// /// /// internal void AddSceneToSynchronize(uint sceneHash, int sceneHandle) { ScenesToSynchronize.Enqueue(sceneHash); SceneHandlesToSynchronize.Enqueue((uint)sceneHandle); } /// /// Client Side: /// Gets the next scene hash to be loaded for approval and/or late joining /// /// internal uint GetNextSceneSynchronizationHash() { return ScenesToSynchronize.Dequeue(); } /// /// Client Side: /// Gets the next scene handle to be loaded for approval and/or late joining /// /// internal int GetNextSceneSynchronizationHandle() { return (int)SceneHandlesToSynchronize.Dequeue(); } /// /// Client Side: /// Determines if all scenes have been processed during the synchronization process /// /// true/false internal bool IsDoneWithSynchronization() { if (ScenesToSynchronize.Count == 0 && SceneHandlesToSynchronize.Count == 0) { return true; } else if (ScenesToSynchronize.Count != SceneHandlesToSynchronize.Count) { // This should never happen, but in the event it does... throw new Exception($"[{nameof(SceneEventData)}-Internal Mismatch Error] {nameof(ScenesToSynchronize)} count != {nameof(SceneHandlesToSynchronize)} count!"); } return false; } /// /// Server Side: /// Called just before the synchronization process /// internal void InitializeForSynch() { if (m_SceneNetworkObjects == null) { m_SceneNetworkObjects = new Dictionary>(); } else { m_SceneNetworkObjects.Clear(); } if (ScenesToSynchronize == null) { ScenesToSynchronize = new Queue(); } else { ScenesToSynchronize.Clear(); } if (SceneHandlesToSynchronize == null) { SceneHandlesToSynchronize = new Queue(); } else { SceneHandlesToSynchronize.Clear(); } } internal void AddSpawnedNetworkObjects() { m_NetworkObjectsSync.Clear(); foreach (var sobj in m_NetworkManager.SpawnManager.SpawnedObjectsList) { if (sobj.Observers.Contains(TargetClientId)) { m_NetworkObjectsSync.Add(sobj); } } m_NetworkObjectsSync.Sort(SortNetworkObjects); } /// /// Server Side: /// Used during the synchronization process to associate NetworkObjects with scenes /// /// /// internal void AddNetworkObjectForSynch(uint sceneIndex, NetworkObject networkObject) { if (!m_SceneNetworkObjects.ContainsKey(sceneIndex)) { m_SceneNetworkObjects.Add(sceneIndex, new List()); } m_SceneNetworkObjects[sceneIndex].Add(networkObject); } /// /// Client and Server: /// Determines if the scene event type was intended for the client ( or server ) /// /// true (client should handle this message) false (server should handle this message) internal bool IsSceneEventClientSide() { switch (SceneEventType) { case SceneEventType.Load: case SceneEventType.Unload: case SceneEventType.Synchronize: case SceneEventType.ReSynchronize: case SceneEventType.LoadEventCompleted: case SceneEventType.UnloadEventCompleted: { return true; } } return false; } /// /// Server Side: /// Sorts the NetworkObjects to assure proper instantiation order of operations for /// registered INetworkPrefabInstanceHandler implementations /// /// /// /// private int SortNetworkObjects(NetworkObject first, NetworkObject second) { var doesFirstHaveHandler = m_NetworkManager.PrefabHandler.ContainsHandler(first); var doesSecondHaveHandler = m_NetworkManager.PrefabHandler.ContainsHandler(second); if (doesFirstHaveHandler != doesSecondHaveHandler) { if (doesFirstHaveHandler) { return 1; } else { return -1; } } return 0; } /// /// Client and Server Side: /// Serializes data based on the SceneEvent type () /// /// to write the scene event data internal void Serialize(FastBufferWriter writer) { // Write the scene event type writer.WriteValueSafe(SceneEventType); // Write the scene loading mode writer.WriteValueSafe(LoadSceneMode); // Write the scene event progress Guid if (SceneEventType != SceneEventType.Synchronize) { writer.WriteValueSafe(SceneEventProgressId); } // Write the scene index and handle writer.WriteValueSafe(SceneHash); writer.WriteValueSafe(SceneHandle); switch (SceneEventType) { case SceneEventType.Synchronize: { WriteSceneSynchronizationData(writer); break; } case SceneEventType.Load: { SerializeScenePlacedObjects(writer); break; } case SceneEventType.SynchronizeComplete: { WriteClientSynchronizationResults(writer); break; } case SceneEventType.ReSynchronize: { WriteClientReSynchronizationData(writer); break; } case SceneEventType.LoadEventCompleted: case SceneEventType.UnloadEventCompleted: { WriteSceneEventProgressDone(writer); break; } } } /// /// Server Side: /// Called at the end of a event once the scene is loaded and scene placed NetworkObjects /// have been locally spawned /// internal void WriteSceneSynchronizationData(FastBufferWriter writer) { // Write the scenes we want to load, in the order we want to load them writer.WriteValueSafe(ScenesToSynchronize.ToArray()); writer.WriteValueSafe(SceneHandlesToSynchronize.ToArray()); // Store our current position in the stream to come back and say how much data we have written var positionStart = writer.Position; // Size Place Holder -- Start // !!NOTE!!: Since this is a placeholder to be set after we know how much we have written, // for stream offset purposes this MUST not be a packed value! writer.WriteValueSafe((int)0); int totalBytes = 0; // Write the number of NetworkObjects we are serializing writer.WriteValueSafe(m_NetworkObjectsSync.Count()); for (var i = 0; i < m_NetworkObjectsSync.Count(); ++i) { var noStart = writer.Position; var sceneObject = m_NetworkObjectsSync[i].GetMessageSceneObject(TargetClientId); writer.WriteValueSafe(m_NetworkObjectsSync[i].gameObject.scene.handle); sceneObject.Serialize(writer); var noStop = writer.Position; totalBytes += (int)(noStop - noStart); } // Size Place Holder -- End var positionEnd = writer.Position; var bytesWritten = (uint)(positionEnd - (positionStart + sizeof(uint))); writer.Seek(positionStart); // Write the total size written to the stream by NetworkObjects being serialized writer.WriteValueSafe(bytesWritten); writer.Seek(positionEnd); } /// /// Server Side: /// Called at the end of a event once the scene is loaded and scene placed NetworkObjects /// have been locally spawned /// Maximum number of objects that could theoretically be synchronized is 65536 /// internal void SerializeScenePlacedObjects(FastBufferWriter writer) { var numberOfObjects = (ushort)0; var headPosition = writer.Position; // Write our count place holder (must not be packed!) writer.WriteValueSafe((ushort)0); foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects) { foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value) { if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId)) { // Write our server relative scene handle for the NetworkObject being serialized writer.WriteValueSafe(keyValuePairBySceneHandle.Key); // Serialize the NetworkObject var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId); sceneObject.Serialize(writer); numberOfObjects++; } } } var tailPosition = writer.Position; // Reposition to our count position to the head before we wrote our object count writer.Seek(headPosition); // Write number of NetworkObjects serialized (must not be packed!) writer.WriteValueSafe(numberOfObjects); // Set our position back to the tail writer.Seek(tailPosition); } /// /// Client and Server Side: /// Deserialize data based on the SceneEvent type. /// /// internal void Deserialize(FastBufferReader reader) { reader.ReadValueSafe(out SceneEventType); reader.ReadValueSafe(out LoadSceneMode); if (SceneEventType != SceneEventType.Synchronize) { reader.ReadValueSafe(out SceneEventProgressId); } reader.ReadValueSafe(out SceneHash); reader.ReadValueSafe(out SceneHandle); switch (SceneEventType) { case SceneEventType.Synchronize: { CopySceneSynchronizationData(reader); break; } case SceneEventType.SynchronizeComplete: { CheckClientSynchronizationResults(reader); break; } case SceneEventType.Load: { unsafe { // We store off the trailing in-scene placed serialized NetworkObject data to // be processed once we are done loading. m_HasInternalBuffer = true; // We use Allocator.Persistent since scene loading could take longer than 4 frames InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, reader.Length - reader.Position); } break; } case SceneEventType.ReSynchronize: { ReadClientReSynchronizationData(reader); break; } case SceneEventType.LoadEventCompleted: case SceneEventType.UnloadEventCompleted: { ReadSceneEventProgressDone(reader); break; } } } /// /// Client Side: /// Prepares for a scene synchronization event and copies the scene synchronization data /// into the internal buffer to be used throughout the synchronization process. /// /// internal void CopySceneSynchronizationData(FastBufferReader reader) { m_NetworkObjectsSync.Clear(); reader.ReadValueSafe(out uint[] scenesToSynchronize); reader.ReadValueSafe(out uint[] sceneHandlesToSynchronize); ScenesToSynchronize = new Queue(scenesToSynchronize); SceneHandlesToSynchronize = new Queue(sceneHandlesToSynchronize); // is not packed! reader.ReadValueSafe(out int sizeToCopy); unsafe { if (!reader.TryBeginRead(sizeToCopy)) { throw new OverflowException("Not enough space in the buffer to read recorded synchronization data size."); } m_HasInternalBuffer = true; // We use Allocator.Persistent since scene synchronization will most likely take longer than 4 frames InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, sizeToCopy); } } /// /// Client Side: /// This needs to occur at the end of a event when the scene has finished loading /// Maximum number of objects that could theoretically be synchronized is 65536 /// internal void DeserializeScenePlacedObjects() { try { // is not packed! InternalBuffer.ReadValueSafe(out ushort newObjectsCount); for (ushort i = 0; i < newObjectsCount; i++) { InternalBuffer.ReadValueSafe(out int sceneHandle); // Set our relative scene to the NetworkObject m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(sceneHandle); // Deserialize the NetworkObject var sceneObject = new NetworkObject.SceneObject(); sceneObject.Deserialize(InternalBuffer); NetworkObject.AddSceneObject(sceneObject, InternalBuffer, m_NetworkManager); } } finally { InternalBuffer.Dispose(); m_HasInternalBuffer = false; } } /// /// Client Side: /// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned, /// the server will compile a list and send back an Event_ReSync message to the client. This is where the /// client handles any returned values by the server. /// /// internal void ReadClientReSynchronizationData(FastBufferReader reader) { reader.ReadValueSafe(out uint[] networkObjectsToRemove); if (networkObjectsToRemove.Length > 0) { var networkObjects = UnityEngine.Object.FindObjectsOfType(); var networkObjectIdToNetworkObject = new Dictionary(); foreach (var networkObject in networkObjects) { if (!networkObjectIdToNetworkObject.ContainsKey(networkObject.NetworkObjectId)) { networkObjectIdToNetworkObject.Add(networkObject.NetworkObjectId, networkObject); } } foreach (var networkObjectId in networkObjectsToRemove) { if (networkObjectIdToNetworkObject.ContainsKey(networkObjectId)) { var networkObject = networkObjectIdToNetworkObject[networkObjectId]; networkObjectIdToNetworkObject.Remove(networkObjectId); networkObject.IsSpawned = false; if (m_NetworkManager.PrefabHandler.ContainsHandler(networkObject)) { if (m_NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId)) { m_NetworkManager.SpawnManager.SpawnedObjects.Remove(networkObjectId); } if (m_NetworkManager.SpawnManager.SpawnedObjectsList.Contains(networkObject)) { m_NetworkManager.SpawnManager.SpawnedObjectsList.Remove(networkObject); } NetworkManager.Singleton.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); } else { UnityEngine.Object.DestroyImmediate(networkObject.gameObject); } } } } } /// /// Server Side: /// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned, /// the server will compile a list and send back an Event_ReSync message to the client. /// /// internal void WriteClientReSynchronizationData(FastBufferWriter writer) { //Write how many objects need to be removed writer.WriteValueSafe(m_NetworkObjectsToBeRemoved.ToArray()); } /// /// Server Side: /// Determines if the client needs to be slightly re-synchronized if during the deserialization /// process the server finds NetworkObjects that the client still thinks are spawned. /// /// internal bool ClientNeedsReSynchronization() { return (m_NetworkObjectsToBeRemoved.Count > 0); } /// /// Server Side: /// Determines if the client needs to be re-synchronized if during the deserialization /// process the server finds NetworkObjects that the client still thinks are spawned but /// have since been despawned. /// /// internal void CheckClientSynchronizationResults(FastBufferReader reader) { m_NetworkObjectsToBeRemoved.Clear(); reader.ReadValueSafe(out uint networkObjectIdCount); for (int i = 0; i < networkObjectIdCount; i++) { reader.ReadValueSafe(out uint networkObjectId); if (!m_NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId)) { m_NetworkObjectsToBeRemoved.Add(networkObjectId); } } } /// /// Client Side: /// During the deserialization process of the servers Event_Sync, the client builds a list of /// all NetworkObjectIds that were spawned. Upon responding to the server with the Event_Sync_Complete /// this list is included for the server to review over and determine if the client needs a minor resynchronization /// of NetworkObjects that might have been despawned while the client was processing the Event_Sync. /// /// internal void WriteClientSynchronizationResults(FastBufferWriter writer) { //Write how many objects were spawned writer.WriteValueSafe((uint)m_NetworkObjectsSync.Count); foreach (var networkObject in m_NetworkObjectsSync) { writer.WriteValueSafe((uint)networkObject.NetworkObjectId); } } /// /// Client Side: /// During the processing of a server sent Event_Sync, this method will be called for each scene once /// it is finished loading. The client will also build a list of NetworkObjects that it spawned during /// this process which will be used as part of the Event_Sync_Complete response. /// /// internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) { try { // Process all NetworkObjects for this scene InternalBuffer.ReadValueSafe(out int newObjectsCount); for (int i = 0; i < newObjectsCount; i++) { // We want to make sure for each NetworkObject we have the appropriate scene selected as the scene that is // currently being synchronized. This assures in-scene placed NetworkObjects will use the right NetworkObject // from the list of populated InternalBuffer.ReadValueSafe(out int handle); m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(handle); var sceneObject = new NetworkObject.SceneObject(); sceneObject.Deserialize(InternalBuffer); var spawnedNetworkObject = NetworkObject.AddSceneObject(sceneObject, InternalBuffer, networkManager); if (!m_NetworkObjectsSync.Contains(spawnedNetworkObject)) { m_NetworkObjectsSync.Add(spawnedNetworkObject); } } } finally { InternalBuffer.Dispose(); m_HasInternalBuffer = false; } } /// /// Writes the all clients loaded or unloaded completed and timed out lists /// /// internal void WriteSceneEventProgressDone(FastBufferWriter writer) { writer.WriteValueSafe((ushort)ClientsCompleted.Count); foreach (var clientId in ClientsCompleted) { writer.WriteValueSafe(clientId); } writer.WriteValueSafe((ushort)ClientsTimedOut.Count); foreach (var clientId in ClientsTimedOut) { writer.WriteValueSafe(clientId); } } /// /// Reads the all clients loaded or unloaded completed and timed out lists /// /// internal void ReadSceneEventProgressDone(FastBufferReader reader) { reader.ReadValueSafe(out ushort completedCount); ClientsCompleted = new List(); for (int i = 0; i < completedCount; i++) { reader.ReadValueSafe(out ulong clientId); ClientsCompleted.Add(clientId); } reader.ReadValueSafe(out ushort timedOutCount); ClientsTimedOut = new List(); for (int i = 0; i < timedOutCount; i++) { reader.ReadValueSafe(out ulong clientId); ClientsTimedOut.Add(clientId); } } /// /// Used to release the pooled network buffer /// public void Dispose() { if (m_HasInternalBuffer) { InternalBuffer.Dispose(); m_HasInternalBuffer = false; } } /// /// Constructor for SceneEventData /// internal SceneEventData(NetworkManager networkManager) { m_NetworkManager = networkManager; SceneEventId = XXHash.Hash32(Guid.NewGuid().ToString()); } } }