namespace Fusion { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES using System.Threading.Tasks; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceProviders; #endif public class NetworkSceneManagerDefault : Fusion.Behaviour, INetworkSceneManager { /// /// If enabled and there is an already loaded scene that matches what the scene manager has intended to load, /// that scene will be used instead and load will be avoided. /// [InlineHelp] [ToggleLeft] public bool IsSceneTakeOverEnabled = true; /// /// Should all scene load errors be logged into the console? If disabled, errors can still be retrieved via the /// or . /// [InlineHelp] [ToggleLeft] public bool LogSceneLoadErrors = true; /// /// All the scenes loaded by all the managers. Used when is enabled. /// private static Dictionary _allOwnedScenes = new Dictionary(new FusionUnitySceneManagerUtils.SceneEqualityComparer()); /// /// In multiple peer mode, each runner maintains its own scene where all the newly loaded scenes /// are moved to. This is to make sure physics are properly sandboxed. /// private List _multiPeerSceneRoots = new List(); private MultiPeerSceneRoot _multiPeerActiveRoot; /// /// List of running coroutines. Only one is actually executed at a time. /// private List _runningCoroutines = new List(); /// /// For remote clients, this manager first unloads old scenes then loads the new ones. It might happen that all /// the current scenes need to be unloaded and in such case a temp scene needs to be created to ensure at least one /// scene loaded at all times. /// private Scene _tempUnloadScene; /// /// Scene used when Multiple Peer mode is used. Each loaded scene is merged into this one, allowing /// for multiple runners to have separate cross-scene physics. /// public Scene MultiPeerScene { get; private set; } /// /// Root for DontDestroyOnLoad objects. Instantiated on . /// public Transform MultiPeerDontDestroyOnLoadRoot { get; private set; } public NetworkRunner Runner { get; private set; } private bool IsMultiplePeer => Runner.Config.PeerMode == NetworkProjectConfig.PeerModes.Multiple; private bool _isLoading; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void ClearStatics() { _allOwnedScenes.Clear(); } static NetworkSceneManagerDefault() { SceneManager.sceneUnloaded += (s) => _allOwnedScenes.Remove(s); } #region INetworkSceneManager public virtual void Initialize(NetworkRunner runner) { Log.TraceSceneManager(runner, $"Initialize with {runner}"); #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES LoadAddressableScenePathsAsync(); #endif Debug.Assert(Runner == null); Runner = runner; // assign an empty scene with a separate physics stage immediately, so that they won't spawn anything on the currently active scene // an lose track of it if (IsMultiplePeer) { var scene = SceneManager.CreateScene($"{runner.name}_{runner.LocalPlayer}", new CreateSceneParameters(LocalPhysicsMode.Physics2D | LocalPhysicsMode.Physics3D)); Log.TraceSceneManager(Runner, $"Assigned an initial scene: {scene.Dump()}"); MultiPeerScene = scene; MultiPeerDontDestroyOnLoadRoot = new GameObject("[DontDestroyOnLoad]").transform; SceneManager.MoveGameObjectToScene(MultiPeerDontDestroyOnLoadRoot.gameObject, MultiPeerScene); } } public virtual void Shutdown() { Log.TraceSceneManager(Runner, $"Shutdown with {Runner}"); Runner = null; // clear owned scenes in case this manager is reused var ownedScenes = _allOwnedScenes .Where(x => x.Value == this) .Select(x => x.Key) .ToList(); foreach (var ownedScene in ownedScenes) { _allOwnedScenes.Remove(ownedScene); } _multiPeerSceneRoots.Clear(); _multiPeerActiveRoot = null; MultiPeerDontDestroyOnLoadRoot = null; var sceneToUnload = MultiPeerScene; MultiPeerScene = default; if (sceneToUnload.isLoaded) { if (!sceneToUnload.CanBeUnloaded()) { SceneManager.CreateScene($"FusionSceneManager_TempEmptyScene"); } SceneManager.UnloadSceneAsync(sceneToUnload); } } public virtual bool IsBusy { get { if (_isLoading) { return true; } if (IsMultiplePeer && _multiPeerSceneRoots.Count == 0) { // nothing to spawn on return true; } return false; } } public virtual Scene MainRunnerScene { get { if (IsMultiplePeer) { return MultiPeerScene; } else { return SceneManager.GetActiveScene(); } } } public virtual bool IsRunnerScene(Scene scene) { if (IsMultiplePeer) { return scene == MultiPeerScene; } else { return true; } } public virtual bool TryGetPhysicsScene2D(out PhysicsScene2D scene2D) { var mainScene = MainRunnerScene; if (mainScene.IsValid()) { scene2D = mainScene.GetPhysicsScene2D(); return true; } else { scene2D = default; return false; } } public virtual bool TryGetPhysicsScene3D(out PhysicsScene scene3D) { var mainScene = MainRunnerScene; if (mainScene.IsValid()) { scene3D = mainScene.GetPhysicsScene(); return true; } else { scene3D = default; return false; } } public virtual void MakeDontDestroyOnLoad(GameObject obj) { if (IsMultiplePeer) { Debug.Assert(obj.transform.parent == null || obj.transform.parent == MultiPeerDontDestroyOnLoadRoot); obj.transform.SetParent(MultiPeerDontDestroyOnLoadRoot, true); } else { DontDestroyOnLoad(obj); } } public bool MoveGameObjectToScene(GameObject gameObject, SceneRef sceneRef) { if (IsMultiplePeer) { // find the first matching scene ref foreach (var root in _multiPeerSceneRoots) { if (sceneRef != default && root.SceneRef != sceneRef) { continue; } if (sceneRef == default) { // if scene ref is not specified, use the active root, if it exists if (_multiPeerActiveRoot && root != _multiPeerActiveRoot) { continue; } } if (gameObject.scene != MultiPeerScene) { SceneManager.MoveGameObjectToScene(gameObject, MultiPeerScene); } gameObject.transform.SetParent(root.transform, true); return true; } return false; } else { if (sceneRef == default) { // do nothing, all scenes belong to the runner return true; } for (int i = 0; i < SceneManager.sceneCount; ++i) { var scene = SceneManager.GetSceneAt(i); if (scene.isLoaded && GetSceneRef(scene.path) == sceneRef) { SceneManager.MoveGameObjectToScene(gameObject, scene); return true; } } return false; } } public virtual NetworkSceneAsyncOp LoadScene(SceneRef sceneRef, NetworkLoadSceneParameters parameters) { Log.TraceSceneManager(Runner, $"Load scene {sceneRef} called with parameters: {parameters}"); return NetworkSceneAsyncOp.FromCoroutine(sceneRef, StartTracedCoroutine(LoadSceneCoroutine(sceneRef, parameters))); } public virtual NetworkSceneAsyncOp UnloadScene(SceneRef sceneRef) { Log.TraceSceneManager(Runner, $"Unload scene {sceneRef} called"); return NetworkSceneAsyncOp.FromCoroutine(sceneRef, StartTracedCoroutine(UnloadSceneCoroutine(sceneRef))); } public virtual SceneRef GetSceneRef(string sceneNameOrPath) { int buildIndex = FusionUnitySceneManagerUtils.GetSceneBuildIndex(sceneNameOrPath); if (buildIndex >= 0) { return SceneRef.FromIndex(buildIndex); } #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES // this may be a blocking call due to WaitForCompletion being used internally if (!_addressableScenesTask.IsValueCreated) { Log.WarnSceneManager(Runner, $"Going to block the thread in wait for addressable scene paths being resolved, call and await {nameof(LoadAddressableScenePathsAsync)} to avoid this."); } string[] addressableScenes; if (_addressableScenesTask.Value.Wait(TimeSpan.FromSeconds(10))) { addressableScenes = _addressableScenesTask.Value.Result; } else { Log.ErrorSceneManager(this, $"Failed to resolve addressable scene paths in 10 seconds, won't be able to resolve {sceneNameOrPath} or any other addressable scene."); addressableScenes = Array.Empty(); } var index = FusionUnitySceneManagerUtils.GetSceneIndex(addressableScenes, sceneNameOrPath); if (index >= 0) { return SceneRef.FromPath(addressableScenes[index]); } #endif return SceneRef.None; } public SceneRef GetSceneRef(GameObject gameObject) { if (IsMultiplePeer) { if (gameObject.scene != MultiPeerScene) { // not a part of this scene return default; } // find among scene roots var sceneRoot = gameObject.transform.root; foreach (var root in _multiPeerSceneRoots) { if (root.transform == sceneRoot) { return root.SceneRef; } } return default; } else { var scene = gameObject.scene; return GetSceneRef(scene.path); } } public bool OnSceneInfoChanged(NetworkSceneInfo sceneInfo, NetworkSceneInfoChangeSource changeSource) { // implement this method and return true if you want to handle scene info changes manually return false; } #endregion protected virtual IEnumerator LoadSceneCoroutine(SceneRef sceneRef, NetworkLoadSceneParameters sceneParams) { Runner.InvokeSceneLoadStart(sceneRef); Scene scene = default; using (MakeLoadingScope()) { Log.TraceSceneManager(Runner, $"LoadSceneCoroutine called with {sceneRef}, {sceneParams}"); var localPhysicsMode = sceneParams.LocalPhysicsMode; var loadSceneMode = sceneParams.LoadSceneMode; if (IsMultiplePeer) { if (localPhysicsMode != LocalPhysicsMode.None) { throw new ArgumentException($"Local physics mode is not supported in multiple peer mode", nameof(sceneParams)); } if (loadSceneMode == LoadSceneMode.Single) { // all the current scenes need to be "unloaded", except possibly for the one // that matches the sceneRef, if scene take over is enabled loadSceneMode = LoadSceneMode.Additive; try { foreach (var root in _multiPeerSceneRoots) { Log.TraceSceneManager(Runner, $"Destroying scene {sceneRef} root {root.name} due to single-mode load"); Destroy(root.gameObject); } // wait for each root to be destroyed foreach (var root in _multiPeerSceneRoots) { while (root != null) { yield return null; } } } finally { _multiPeerSceneRoots.Clear(); } } } if (IsSceneTakeOverEnabled) { // check if a loaded scene can be taken over Scene candidate = FindSceneToTakeOver(sceneRef); if (candidate.IsValid()) { Log.TraceSceneManager(Runner, $"Taking over {sceneRef}: {candidate.Dump()}"); if (candidate.GetLocalPhysicsMode() != localPhysicsMode) { throw new InvalidOperationException($"Tried to take over {candidate.Dump()} for {sceneRef}, but physics mode were different: {candidate.GetLocalPhysicsMode()} != {localPhysicsMode}"); } scene = candidate; MarkSceneAsOwned(sceneRef, candidate); if (loadSceneMode == LoadSceneMode.Single && !IsMultiplePeer) { // need to unload scenes manually, multiple peer mode is handled at the beginning of this method, because // it always needs to the manual cleanup for single mode for (int i = 0; i < SceneManager.sceneCount; i++) { var toUnload = SceneManager.GetSceneAt(i); if (toUnload != candidate) { Log.TraceSceneManager(Runner, $"Unloading {sceneRef} ({toUnload.Dump()}) due to single-mode take over of {candidate.Dump()}"); yield return SceneManager.UnloadSceneAsync(toUnload); } } } } } if (!scene.IsValid()) { #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES if (loadSceneMode == LoadSceneMode.Single) { // single mode unloads all the scenes anyway _addressableOperations.Clear(); } #endif if (sceneRef.IsIndex) { Log.TraceSceneManager(Runner, $"Loading scene {sceneRef} with build index {sceneRef.AsIndex} with mode {loadSceneMode}"); var op = SceneManager.LoadSceneAsync(sceneRef.AsIndex, new LoadSceneParameters(loadSceneMode, localPhysicsMode)); if (op == null) { throw new InvalidOperationException($"Scene not found: {sceneRef.AsIndex}"); } Debug.Assert(SceneManager.sceneCount > 0); scene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1); MarkSceneAsOwned(sceneRef, scene); Debug.Assert(scene.buildIndex == sceneRef.AsIndex); while (!op.isDone) { OnLoadSceneProgress(sceneRef, op.progress); yield return null; } } else { #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES if (!_addressableScenesTask.IsValueCreated) { Log.WarnSceneManager(Runner, $"Going to block the thread in wait for addressable scene paths being resolved, call and await {nameof(LoadAddressableScenePathsAsync)} to avoid this."); } string sceneAddress = null; foreach (var path in _addressableScenesTask.Value.Result) { if (sceneRef.IsPath(path)) { sceneAddress = path; break; } } if (sceneAddress == null) { throw new InvalidOperationException($"Unable to find addressable scene path for {sceneRef}"); } Log.TraceSceneManager(Runner, $"Loading scene {sceneRef} from addressable: {sceneAddress}"); #if FUSION_ENABLE_ADDRESSABLES_LOCAL_PHYSICS var loadSceneParameters = new LoadSceneParameters(loadSceneMode, localPhysicsMode); #else if (localPhysicsMode != LocalPhysicsMode.None) { throw new InvalidOperationException($"{nameof(LocalPhysicsMode)} is not supported in this version of Addressables"); } var loadSceneParameters = loadSceneMode; #endif var op = Addressables.LoadSceneAsync(sceneAddress, loadSceneParameters); // to get the scene a callback is used, as it fires immediately when loading finished, // compared to waiting for the coroutine to resume scene = default; op.Completed += op => { if (op.Status == AsyncOperationStatus.Succeeded) { scene = op.Result.Scene; MarkSceneAsOwned(sceneRef, scene); } }; op.Destroyed += _ => { // this will happen in MP mode when scenes are merged or when a scene is loaded in a single mode if (_addressableOperations.Remove(sceneRef)) { Log.TraceSceneManager(Runner, $"Destroyed Addressables op for {sceneRef}"); } }; _addressableOperations.Add(sceneRef, op); while (!op.IsDone) { OnLoadSceneProgress(sceneRef, op.PercentComplete); yield return null; } if (!op.IsValid()) { throw new InvalidOperationException($"Loading operation for {sceneRef} has been destroyed"); } if (op.Status == AsyncOperationStatus.Failed) { Addressables.Release(op); throw new InvalidOperationException($"Failed to load scene from addressable: {sceneAddress}"); } #else throw new InvalidOperationException($"SceneRef {sceneRef} points to an addressable scene, but FUSION_ENABLE_ADDRESSABLES is not defined"); #endif } } } yield return StartCoroutine(OnSceneLoaded(sceneRef, scene, sceneParams)); } protected virtual IEnumerator UnloadSceneCoroutine(SceneRef sceneRef) { Log.TraceSceneManager(Runner, $"UnloadSceneCoroutine called for {sceneRef}"); using (MakeLoadingScope()) { if (IsMultiplePeer) { // in multiple peer, the unload simply destroys the scene root for (int i = 0; i < _multiPeerSceneRoots.Count; ++i) { var root = _multiPeerSceneRoots[i]; if (root.SceneRef == sceneRef) { if (root == _multiPeerActiveRoot) { _multiPeerActiveRoot = null; } _multiPeerSceneRoots.RemoveAt(i); Log.TraceSceneManager(Runner, $"Destroying scene root {root.name} for {sceneRef}"); Log.TraceSceneManager(Runner, $"Started unloading {root.Scene.ToString()} for {sceneRef}"); Destroy(root.gameObject); while (root != null) { yield return null; } Log.TraceSceneManager(Runner, $"Finished unloading {root.Scene.ToString()} for {sceneRef}"); yield break; } } throw new ArgumentOutOfRangeException($"Did not find a scene to unload: {sceneRef}", nameof(sceneRef)); } else { Scene sceneToUnload = default; // find the scene to unload for (int i = 0; i < SceneManager.sceneCount; ++i) { var scene = SceneManager.GetSceneAt(i); if (GetSceneRef(scene.path) == sceneRef) { sceneToUnload = scene; break; } } if (!sceneToUnload.IsValid()) { throw new ArgumentOutOfRangeException($"Did not find a scene to unload: {sceneRef}", nameof(sceneRef)); } Log.TraceSceneManager(Runner, $"Started unloading {sceneToUnload.Dump()} for {sceneRef}"); if (!sceneToUnload.CanBeUnloaded()) { Log.WarnSceneManager(Runner, $"Scene {sceneToUnload.Dump()} can't be unloaded for {sceneRef}, creating a temporary scene to unload it"); Debug.Assert(!_tempUnloadScene.IsValid()); _tempUnloadScene = SceneManager.CreateScene($"FusionSceneManager_TempEmptyScene"); } #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES if (_addressableOperations.TryGetValue(sceneRef, out var asyncOp)) { Log.TraceSceneManager(Runner, $"Unloading addressable scene {sceneToUnload.Dump()} for {sceneRef}"); yield return Addressables.UnloadSceneAsync(asyncOp); } else #endif { Log.TraceSceneManager(Runner, $"Unloading {sceneToUnload.Dump()} for {sceneRef}"); var op = SceneManager.UnloadSceneAsync(sceneToUnload); if (op == null) { throw new InvalidOperationException($"Failed to unload {sceneToUnload.Dump()}"); } yield return op; } Log.TraceSceneManager(Runner, $"Finished unloading {sceneToUnload.Dump()} for {sceneRef}"); } } } protected virtual IEnumerator OnSceneLoaded(SceneRef sceneRef, Scene scene, NetworkLoadSceneParameters sceneParams) { Log.TraceSceneManager(Runner, $"Finished loading, starting processing {scene.Dump()} for {sceneRef}"); var sceneObjects = scene.GetComponents(includeInactive: true, out var rootObjects); // since it is impossible to get objects in deterministic order (sibling index is 0 for all root objects in builds), // scene objects need to be sorted by something that will guarantee the order Array.Sort(sceneObjects, NetworkObjectSortKeyComparer.Instance); if (IsMultiplePeer) { // create a root GO for all the gameObjects in the newly loaded scene var newSceneRoot = new GameObject($"[{scene.name}]").AddComponent(); newSceneRoot.SceneRef = sceneRef; newSceneRoot.SceneHandle = scene.handle; newSceneRoot.Scene = scene; newSceneRoot.ScenePath = scene.path; SceneManager.MoveGameObjectToScene(newSceneRoot.gameObject, scene); foreach (var rootGameObject in rootObjects) { rootGameObject.transform.SetParent(newSceneRoot.transform, true); } // store the info _multiPeerSceneRoots.Add(newSceneRoot); Log.TraceSceneManager(Runner, $"Merging {scene.Dump()} to {MultiPeerScene.Dump()} for {sceneRef}"); SceneManager.MergeScenes(scene, MultiPeerScene); if (sceneParams.IsActiveOnLoad) { _multiPeerActiveRoot = newSceneRoot; } } else { if (sceneParams.IsActiveOnLoad) { SceneManager.SetActiveScene(scene); } } // register scene objects; this will deactivate GameObjects for clients // the additional loadId parameter is passed to ensure each scene load // yields unique type ids for scene objects Runner.RegisterSceneObjects(sceneRef, sceneObjects, loadId: sceneParams.LoadId); Log.TraceSceneManager(Runner, $"Finished loading & processing {scene.Dump()} for {sceneRef}"); Runner.InvokeSceneLoadDone(new SceneLoadDoneArgs(sceneRef, sceneObjects, scene, rootObjects)); yield break; } protected virtual void OnLoadSceneProgress(SceneRef sceneRef, float progress) { Log.TraceSceneManager(Runner, $"Loading scene progress {sceneRef} ({progress:P2})"); } private Scene FindSceneToTakeOver(SceneRef sceneRef) { for (int i = 0; i < SceneManager.sceneCount; ++i) { var candidate = SceneManager.GetSceneAt(i); if (!candidate.isLoaded) { continue; } if (GetSceneRef(candidate.path) != sceneRef) { continue; } if (_allOwnedScenes.ContainsKey(candidate)) { continue; } return candidate; } return default; } private ICoroutine StartTracedCoroutine(IEnumerator inner) { var coro = new FusionCoroutine(inner); _runningCoroutines.Add(coro); coro.Completed += x => { if (LogSceneLoadErrors && x.Error != null) { Log.ErrorSceneManager(Runner, $"Failed async op: {x.Error.SourceException}"); } // remove this one from the list var index = _runningCoroutines.IndexOf((ICoroutine)x); Debug.Assert(index == 0, "Expected the completed coroutine to be the first in the list"); _runningCoroutines.RemoveAt(index); // start the next one if (index < _runningCoroutines.Count) { Log.TraceSceneManager(Runner, $"Starting enqueued coroutine {index} of {_runningCoroutines.Count}"); StartCoroutine(_runningCoroutines[index]); } }; if (_runningCoroutines.Count == 1) { // start immediately StartCoroutine(coro); } else { Log.TraceSceneManager(Runner, $"Enqueued coroutine, there are already {_runningCoroutines.Count - 1} running"); } return coro; } protected LoadingScope MakeLoadingScope() { return new LoadingScope(this); } protected void MarkSceneAsOwned(SceneRef sceneRef, Scene scene) { if (_allOwnedScenes.TryGetValue(scene, out var manager)) { Log.WarnSceneManager(Runner, $"Scene {scene.Dump()} (for {sceneRef}) already owned by {manager}"); } else { _allOwnedScenes.Add(scene, this); } } private NetworkSceneAsyncOp FailOp(SceneRef sceneRef, Exception exception) { if (LogSceneLoadErrors) { Log.ErrorSceneManager(Runner, $"Failed with: {exception}"); } return NetworkSceneAsyncOp.FromError(sceneRef, exception); } #if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES /// /// A label by which addressable scenes can be discovered. /// [InlineHelp] public string AddressableScenesLabel = "FusionScenes"; public NetworkSceneManagerDefault() { _addressableScenesTask = new Lazy>(() => GetAddressableScenes()); } public Task LoadAddressableScenePathsAsync() { return _addressableScenesTask.Value; } protected virtual Task GetAddressableScenes() { Log.TraceSceneManager(Runner, $"Locating addressable scenes with label: {AddressableScenesLabel}"); var tcs = new TaskCompletionSource(); var result = Addressables.LoadResourceLocationsAsync(AddressableScenesLabel, typeof(SceneInstance)); result.Completed += op => { try { if (op.Status == AsyncOperationStatus.Failed) { tcs.SetException(op.OperationException); } else { var paths = op.Result.Select(x => x.PrimaryKey).ToArray(); Log.TraceSceneManager(Runner, $"Found {paths.Length} addressable scenes: {string.Join(", ", paths)}"); tcs.SetResult(paths); } } finally { Addressables.Release(op); } }; return tcs.Task; } private Lazy> _addressableScenesTask; private Dictionary> _addressableOperations = new(); #endif protected sealed class MultiPeerSceneRoot : MonoBehaviour { public SceneRef SceneRef; public string ScenePath; public int SceneHandle; public Scene Scene; } protected struct LoadingScope : IDisposable { private readonly NetworkSceneManagerDefault _manager; public LoadingScope(NetworkSceneManagerDefault manager) { _manager = manager; _manager._isLoading = true; Log.TraceSceneManager(manager.Runner, "Loading scope started"); } public void Dispose() { _manager._isLoading = false; Log.TraceSceneManager(_manager.Runner, "Loading scope ended"); } } } }