The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [2.0.0-pre.3] - 2024-07-23 ### Added - Added: `UnityTransport.GetNetworkDriver` and `UnityTransport.GetLocalEndpoint` methods to expose the driver and local endpoint being used. (#2978) ### Fixed - Fixed issue where deferred despawn was causing GC allocations when converting an `IEnumerable` to a list. (#2983) - Fixed issue where the realtime network stats monitor was not able to display RPC traffic in release builds due to those stats being only available in development builds or the editor. (#2979) - Fixed issue where `NetworkManager.ScenesLoaded` was not being updated if `PostSynchronizationSceneUnloading` was set and any loaded scenes not used during synchronization were unloaded. (#2971) - Fixed issue where `Rigidbody2d` under Unity 6000.0.11f1 has breaking changes where `velocity` is now `linearVelocity` and `isKinematic` is replaced by `bodyType`. (#2971) - Fixed issue where `NetworkSpawnManager.InstantiateAndSpawn` and `NetworkObject.InstantiateAndSpawn` were not honoring the ownerClientId parameter when using a client-server network topology. (#2968) - Fixed issue where internal delta serialization could not have a byte serializer defined when serializing deltas for other types. Added `[GenerateSerializationForType(typeof(byte))]` to both the `NetworkVariable` and `AnticipatedNetworkVariable` classes to assure a byte serializer is defined.(#2962) - Fixed issue when scene management was disabled and the session owner would still try to synchronize a late joining client. (#2962) - Fixed issue when using a distributed authority network topology where it would allow a session owner to spawn a `NetworkObject` prior to being approved. Now, an error message is logged and the `NetworkObject` will not be spawned prior to the client being approved. (#2962) - Fixed issue where attempting to spawn during `NetworkBehaviour.OnInSceneObjectsSpawned` and `NetworkBehaviour.OnNetworkSessionSynchronized` notifications would throw a collection modified exception. (#2962) ### Changed - Changed logic where clients can now set the `NetworkSceneManager` client synchronization mode when using a distributed authority network topology. (#2985)
385 lines
17 KiB
C#
385 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
|
|
namespace Unity.Netcode
|
|
{
|
|
/// <summary>
|
|
/// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager
|
|
/// </summary>
|
|
internal class DefaultSceneManagerHandler : ISceneManagerHandler
|
|
{
|
|
private Scene m_InvalidScene = new Scene();
|
|
|
|
internal struct SceneEntry
|
|
{
|
|
public bool IsAssigned;
|
|
public Scene Scene;
|
|
}
|
|
public bool IsIntegrationTest() { return false; }
|
|
|
|
internal Dictionary<string, Dictionary<int, SceneEntry>> SceneNameToSceneHandles = new Dictionary<string, Dictionary<int, SceneEntry>>();
|
|
|
|
public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress)
|
|
{
|
|
var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode);
|
|
sceneEventProgress.SetAsyncOperation(operation);
|
|
return operation;
|
|
}
|
|
|
|
public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress)
|
|
{
|
|
var operation = SceneManager.UnloadSceneAsync(scene);
|
|
sceneEventProgress.SetAsyncOperation(operation);
|
|
return operation;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets scene tracking
|
|
/// </summary>
|
|
public void ClearSceneTracking(NetworkManager networkManager)
|
|
{
|
|
SceneNameToSceneHandles.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops tracking a specific scene
|
|
/// </summary>
|
|
public void StopTrackingScene(int handle, string name, NetworkManager networkManager)
|
|
{
|
|
if (SceneNameToSceneHandles.ContainsKey(name))
|
|
{
|
|
if (SceneNameToSceneHandles[name].ContainsKey(handle))
|
|
{
|
|
SceneNameToSceneHandles[name].Remove(handle);
|
|
if (SceneNameToSceneHandles[name].Count == 0)
|
|
{
|
|
SceneNameToSceneHandles.Remove(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts tracking a specific scene
|
|
/// </summary>
|
|
public void StartTrackingScene(Scene scene, bool assigned, NetworkManager networkManager)
|
|
{
|
|
if (!SceneNameToSceneHandles.ContainsKey(scene.name))
|
|
{
|
|
SceneNameToSceneHandles.Add(scene.name, new Dictionary<int, SceneEntry>());
|
|
}
|
|
|
|
if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
|
|
{
|
|
var sceneEntry = new SceneEntry()
|
|
{
|
|
IsAssigned = true,
|
|
Scene = scene
|
|
};
|
|
SceneNameToSceneHandles[scene.name].Add(scene.handle, sceneEntry);
|
|
}
|
|
else
|
|
{
|
|
throw new Exception($"[Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if there is an existing scene loaded that matches the scene name but has not been assigned
|
|
/// </summary>
|
|
public bool DoesSceneHaveUnassignedEntry(string sceneName, NetworkManager networkManager)
|
|
{
|
|
var scenesWithSceneName = new List<Scene>();
|
|
|
|
// Get all loaded scenes with the same name
|
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
{
|
|
var scene = SceneManager.GetSceneAt(i);
|
|
if (scene.name == sceneName)
|
|
{
|
|
scenesWithSceneName.Add(scene);
|
|
}
|
|
}
|
|
|
|
// If there are no scenes of this name loaded then we have no loaded scenes
|
|
// to use
|
|
if (scenesWithSceneName.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If we have 1 or more scenes with the name and we have no entries, then we do have
|
|
// a scene to use
|
|
if (scenesWithSceneName.Count > 0 && !SceneNameToSceneHandles.ContainsKey(sceneName))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Determine if any of the loaded scenes has been used for synchronizing
|
|
foreach (var scene in scenesWithSceneName)
|
|
{
|
|
// If we don't have the handle, then we can use that scene
|
|
if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// If we have an entry, but it is not yet assigned (i.e. preloaded)
|
|
// then we can use that.
|
|
if (!SceneNameToSceneHandles[scene.name][scene.handle].IsAssigned)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
// If none were found, then we have no available scene (which most likely means one will get loaded)
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will find any scene entry that hasn't been used/assigned, set the entry to assigned, and
|
|
/// return the associated scene. If none are found it returns an invalid scene.
|
|
/// </summary>
|
|
public Scene GetSceneFromLoadedScenes(string sceneName, NetworkManager networkManager)
|
|
{
|
|
if (SceneNameToSceneHandles.ContainsKey(sceneName))
|
|
{
|
|
foreach (var sceneHandleEntry in SceneNameToSceneHandles[sceneName])
|
|
{
|
|
if (!sceneHandleEntry.Value.IsAssigned)
|
|
{
|
|
var sceneEntry = sceneHandleEntry.Value;
|
|
sceneEntry.IsAssigned = true;
|
|
SceneNameToSceneHandles[sceneName][sceneHandleEntry.Key] = sceneEntry;
|
|
return sceneEntry.Scene;
|
|
}
|
|
}
|
|
}
|
|
// If we found nothing return an invalid scene
|
|
return m_InvalidScene;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Only invoked is client synchronization is additive, this will generate the scene tracking table
|
|
/// in order to re-use the same scenes the server is synchronizing instead of having to unload the
|
|
/// scenes and reload them when synchronizing (i.e. client disconnects due to external reason, the
|
|
/// same application instance is still running, the same scenes are still loaded on the client, and
|
|
/// upon reconnecting the client doesn't have to unload the scenes and then reload them)
|
|
/// </summary>
|
|
public void PopulateLoadedScenes(ref Dictionary<int, Scene> scenesLoaded, NetworkManager networkManager)
|
|
{
|
|
SceneNameToSceneHandles.Clear();
|
|
var sceneCount = SceneManager.sceneCount;
|
|
for (int i = 0; i < sceneCount; i++)
|
|
{
|
|
var scene = SceneManager.GetSceneAt(i);
|
|
if (!SceneNameToSceneHandles.ContainsKey(scene.name))
|
|
{
|
|
SceneNameToSceneHandles.Add(scene.name, new Dictionary<int, SceneEntry>());
|
|
}
|
|
|
|
if (!SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
|
|
{
|
|
var sceneEntry = new SceneEntry()
|
|
{
|
|
IsAssigned = false,
|
|
Scene = scene
|
|
};
|
|
SceneNameToSceneHandles[scene.name].Add(scene.handle, sceneEntry);
|
|
if (!scenesLoaded.ContainsKey(scene.handle))
|
|
{
|
|
scenesLoaded.Add(scene.handle, scene);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new Exception($"[Duplicate Handle] Scene {scene.name} already has scene handle {scene.handle} registered!");
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<Scene> m_ScenesToUnload = new List<Scene>();
|
|
|
|
/// <summary>
|
|
/// Unloads any scenes that have not been assigned.
|
|
/// </summary>
|
|
/// <param name="networkManager"></param>
|
|
public void UnloadUnassignedScenes(NetworkManager networkManager = null)
|
|
{
|
|
var sceneManager = networkManager.SceneManager;
|
|
SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
|
|
foreach (var sceneEntry in SceneNameToSceneHandles)
|
|
{
|
|
var scenHandleEntries = SceneNameToSceneHandles[sceneEntry.Key];
|
|
foreach (var sceneHandleEntry in scenHandleEntries)
|
|
{
|
|
if (!sceneHandleEntry.Value.IsAssigned)
|
|
{
|
|
if (sceneManager.VerifySceneBeforeUnloading == null || sceneManager.VerifySceneBeforeUnloading.Invoke(sceneHandleEntry.Value.Scene))
|
|
{
|
|
m_ScenesToUnload.Add(sceneHandleEntry.Value.Scene);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
foreach (var sceneToUnload in m_ScenesToUnload)
|
|
{
|
|
SceneManager.UnloadSceneAsync(sceneToUnload);
|
|
// Update the ScenesLoaded when we unload scenes
|
|
if (sceneManager.ScenesLoaded.ContainsKey(sceneToUnload.handle))
|
|
{
|
|
sceneManager.ScenesLoaded.Remove(sceneToUnload.handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SceneManager_SceneUnloaded(Scene scene)
|
|
{
|
|
if (SceneNameToSceneHandles.ContainsKey(scene.name))
|
|
{
|
|
if (SceneNameToSceneHandles[scene.name].ContainsKey(scene.handle))
|
|
{
|
|
SceneNameToSceneHandles[scene.name].Remove(scene.handle);
|
|
}
|
|
if (SceneNameToSceneHandles[scene.name].Count == 0)
|
|
{
|
|
SceneNameToSceneHandles.Remove(scene.name);
|
|
}
|
|
m_ScenesToUnload.Remove(scene);
|
|
if (m_ScenesToUnload.Count == 0)
|
|
{
|
|
SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles determining if a client should attempt to load a scene during synchronization.
|
|
/// </summary>
|
|
/// <param name="sceneName">name of the scene to be loaded</param>
|
|
/// <param name="isPrimaryScene">when in client synchronization mode single, this determines if the scene is the primary active scene</param>
|
|
/// <param name="clientSynchronizationMode">the current client synchronization mode</param>
|
|
/// <param name="networkManager"><see cref="NetworkManager"/> instance</param>
|
|
/// <returns></returns>
|
|
public bool ClientShouldPassThrough(string sceneName, bool isPrimaryScene, LoadSceneMode clientSynchronizationMode, NetworkManager networkManager)
|
|
{
|
|
var shouldPassThrough = clientSynchronizationMode == LoadSceneMode.Single ? false : DoesSceneHaveUnassignedEntry(sceneName, networkManager);
|
|
var activeScene = SceneManager.GetActiveScene();
|
|
|
|
// If shouldPassThrough is not yet true and the scene to be loaded is the currently active scene
|
|
if (!shouldPassThrough && sceneName == activeScene.name)
|
|
{
|
|
// In additive mode we always pass through, but in LoadSceneMode.Single we only pass through if the currently active scene
|
|
// is the primary scene to be loaded
|
|
if (clientSynchronizationMode == LoadSceneMode.Additive || (isPrimaryScene && clientSynchronizationMode == LoadSceneMode.Single))
|
|
{
|
|
// don't try to reload this scene and pass through to post load processing.
|
|
shouldPassThrough = true;
|
|
}
|
|
}
|
|
return shouldPassThrough;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles migrating dynamically spawned NetworkObjects to the DDOL when a scene is unloaded
|
|
/// </summary>
|
|
/// <param name="networkManager"><see cref="NetworkManager"/>relative instance</param>
|
|
/// <param name="scene">scene being unloaded</param>
|
|
public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkManager, Scene scene)
|
|
{
|
|
bool isActiveScene = scene == SceneManager.GetActiveScene();
|
|
// Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects
|
|
// are despawned.
|
|
var localSpawnedObjectsHashSet = new HashSet<NetworkObject>(networkManager.SpawnManager.SpawnedObjectsList);
|
|
foreach (var networkObject in localSpawnedObjectsHashSet)
|
|
{
|
|
if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Only NetworkObjects marked to not be destroyed with the scene and are not already in the DDOL are preserved
|
|
if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene)
|
|
{
|
|
// Only move dynamically spawned NetworkObjects with no parent as the children will follow
|
|
if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value)
|
|
{
|
|
UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
|
|
}
|
|
}
|
|
else if (networkManager.IsServer)
|
|
{
|
|
networkObject.Despawn();
|
|
}
|
|
else // We are a client, migrate the object into the DDOL temporarily until it receives the destroy command from the server
|
|
{
|
|
UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the client synchronization mode which impacts whether both the server or client take into consideration scenes loaded before
|
|
/// starting the <see cref="NetworkManager"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <see cref="LoadSceneMode.Single"/>: Does not take preloaded scenes into consideration
|
|
/// <see cref="LoadSceneMode.Single"/>: Does take preloaded scenes into consideration
|
|
/// </remarks>
|
|
/// <param name="networkManager">relative <see cref="NetworkManager"/> instance</param>
|
|
/// <param name="mode"><see cref="LoadSceneMode.Single"/> or <see cref="LoadSceneMode.Additive"/></param>
|
|
public void SetClientSynchronizationMode(ref NetworkManager networkManager, LoadSceneMode mode)
|
|
{
|
|
var sceneManager = networkManager.SceneManager;
|
|
// In client-server, we don't let client's set this value.
|
|
// In distributed authority, since session owner can be promoted clients can set this value
|
|
if (!networkManager.DistributedAuthorityMode && !networkManager.IsServer)
|
|
{
|
|
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
|
|
{
|
|
NetworkLog.LogWarning("Clients should not set this value as it is automatically synchronized with the server's setting!");
|
|
}
|
|
return;
|
|
}
|
|
else // Warn users if they are changing this after there are clients already connected and synchronized
|
|
if (!networkManager.DistributedAuthorityMode && networkManager.ConnectedClientsIds.Count > (networkManager.IsHost ? 1 : 0) && sceneManager.ClientSynchronizationMode != mode)
|
|
{
|
|
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
|
|
{
|
|
NetworkLog.LogWarning("Server is changing client synchronization mode after clients have been synchronized! It is recommended to do this before clients are connected!");
|
|
}
|
|
}
|
|
|
|
// For additive client synchronization, we take into consideration scenes
|
|
// already loaded.
|
|
if (mode == LoadSceneMode.Additive)
|
|
{
|
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
{
|
|
var scene = SceneManager.GetSceneAt(i);
|
|
|
|
// If using scene verification
|
|
if (sceneManager.VerifySceneBeforeLoading != null)
|
|
{
|
|
// Determine if we should take this scene into consideration
|
|
if (!sceneManager.VerifySceneBeforeLoading.Invoke(scene.buildIndex, scene.name, LoadSceneMode.Additive))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If the scene is not already in the ScenesLoaded list, then add it
|
|
if (!sceneManager.ScenesLoaded.ContainsKey(scene.handle))
|
|
{
|
|
sceneManager.ScenesLoaded.Add(scene.handle, scene);
|
|
}
|
|
}
|
|
}
|
|
// Set the client synchronization mode
|
|
sceneManager.ClientSynchronizationMode = mode;
|
|
}
|
|
}
|
|
}
|