2026-04-03 22:46:17 +07:00
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 {
/// <summary>
/// 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.
/// </summary>
[InlineHelp]
[ToggleLeft]
public bool IsSceneTakeOverEnabled = true ;
/// <summary>
/// Should all scene load errors be logged into the console? If disabled, errors can still be retrieved via the
/// <see cref="NetworkSceneAsyncOp.Error"/> or <see cref="NetworkSceneAsyncOp.AddOnCompleted"/>.
/// </summary>
[InlineHelp]
[ToggleLeft]
public bool LogSceneLoadErrors = true ;
/// <summary>
/// If enabled the scenemanager despawns all runtime spawned prefab instances (not scene objects) before unloading a scene.
/// If the peer does not have StateAuthority over the object it is destroyed instead of despawned.
/// If disabled the destroy will be indirectly done via the scene unload from Unity however it will be async and might be delayed,
/// this can lead to the scene change being synchronized in an earlier tick than the destroys.
/// </summary>
[InlineHelp]
[ToggleLeft]
public bool DestroySpawnedPrefabsOnSceneUnload = true ;
/// <summary>
/// All the scenes loaded by all the managers. Used when <see cref="IsSceneTakeOverEnabled"/> is enabled.
/// </summary>
private static Dictionary < Scene , NetworkSceneManagerDefault > _allOwnedScenes = new Dictionary < Scene , NetworkSceneManagerDefault > ( new FusionUnitySceneManagerUtils . SceneEqualityComparer ( ) ) ;
/// <summary>
/// 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.
/// </summary>
private List < MultiPeerSceneRoot > _multiPeerSceneRoots = new List < MultiPeerSceneRoot > ( ) ;
private MultiPeerSceneRoot _multiPeerActiveRoot ;
/// <summary>
/// List of running coroutines. Only one is actually executed at a time.
/// </summary>
private List < ICoroutine > _runningCoroutines = new List < ICoroutine > ( ) ;
/// <summary>
/// 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.
/// </summary>
private Scene _tempUnloadScene ;
/// <summary>
/// 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.
/// </summary>
public Scene MultiPeerScene { get ; private set ; }
/// <summary>
/// Root for DontDestroyOnLoad objects. Instantiated on <see cref="MultiPeerScene"/>.
/// </summary>
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 ) {
gameObject . transform . SetParent ( null , true ) ;
SceneManager . MoveGameObjectToScene ( gameObject , MultiPeerScene ) ;
if ( Application . isBatchMode = = false )
Runner . AddVisibilityNodes ( gameObject ) ;
}
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 ( ! TryGetAddressableScenes ( out var addressableScenes ) ) {
Log . Error ( this , $"Failed to resolve addressable scene paths, won't be able to resolve {sceneNameOrPath} or any other addressable scene." ) ;
addressableScenes = Array . Empty < string > ( ) ;
}
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 ( ) ;
}
}
}
else
{
if ( DestroySpawnedPrefabsOnSceneUnload & & loadSceneMode = = LoadSceneMode . Single )
{
for ( int i = 0 ; i < SceneManager . sceneCount ; i + + ) {
// find the scene to unload
var sceneToBeUnloaded = SceneManager . GetSceneAt ( i ) ; // will be unloaded by Unity on scene load
var sceneRefToBeUnloaded = GetSceneRef ( sceneToBeUnloaded . path ) ;
if ( sceneRefToBeUnloaded ! = SceneRef . None ) {
DestroyAllRuntimeSpawnedObjectsInScene ( sceneToBeUnloaded , sceneRefToBeUnloaded ) ;
}
}
}
}
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 ( ! TryGetAddressableScenes ( out var addressableScenes ) ) {
Log . Error ( this , $"Failed to resolve addressable scene paths, won't be able to resolve {sceneRef}" ) ;
addressableScenes = Array . Empty < string > ( ) ;
}
string sceneAddress = null ;
foreach ( var path in addressableScenes ) {
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 ) ) ;
}
if ( DestroySpawnedPrefabsOnSceneUnload ) {
DestroyAllRuntimeSpawnedObjectsInScene ( sceneToUnload , sceneRef ) ;
}
Log . TraceSceneManager ( Runner , $"Started unloading {sceneToUnload.Dump()} for {sceneRef}" ) ;
if ( ! sceneToUnload . CanBeUnloaded ( ) ) {
Log . Warn ( 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 < NetworkObject > ( 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 < MultiPeerSceneRoot > ( ) ;
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 void DestroyAllRuntimeSpawnedObjectsInScene ( Scene scene , SceneRef sceneRef ) {
Log . TraceSceneManager ( Runner , $"destroying runtime spawned NetworkObjects in scene {scene.Dump()} for {sceneRef}" ) ;
foreach ( var networkObject in Runner . GetAllNetworkObjects ( ) ) {
// This exists to ensure all object meta is destroyed when unloading the scene to prevent objects from getting despawned and spawned again repeadetly on scene unload.
// Scene objects are ignored as they can't be spawned again when the scene is unloaded.
if ( networkObject . gameObject . scene = = scene & & networkObject . NetworkTypeId . IsSceneObject = = false ) {
if ( networkObject . HasStateAuthority ) {
// despawn to ensure the object is immediately added to destroy queue. (Unity destroy callback is delayed until end of Update()
Runner . Despawn ( networkObject ) ;
} else {
Destroy ( networkObject . gameObject ) ;
}
}
}
}
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 . Error ( Runner , $"Failed async op: {x.Error.SourceException}" ) ;
}
// remove this one from the list
var index = _runningCoroutines . IndexOf ( ( ICoroutine ) x ) ;
2026-06-11 21:29:33 +07:00
Debug . AssertFormat ( index > = 0 , "Expected the completed coroutine to be the first in the list, but was: {0}" , index ) ;
2026-04-03 22:46:17 +07:00
_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 . Warn ( 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 . Error ( Runner , $"Failed with: {exception}" ) ;
}
return NetworkSceneAsyncOp . FromError ( sceneRef , exception ) ;
}
#if FUSION_ENABLE_ADDRESSABLES & & ! FUSION_DISABLE_ADDRESSABLES
/// <summary>
/// A label by which addressable scenes can be discovered.
/// </summary>
[InlineHelp]
public string AddressableScenesLabel = "FusionScenes" ;
public NetworkSceneManagerDefault ( ) {
_addressableScenesTask = new ( ( ) = > GetAddressableScenes ( ) ) ;
}
public Task LoadAddressableScenePathsAsync ( ) {
return _addressableScenesTask . Value . Task ;
}
/// <summary>
/// Creates a task that resolves addressable scene paths. By default, this method locates all the addressable scenes with
/// <see cref="AddressableScenesLabel"/> label. Override this method to provide a custom implementation. For example, user
/// might want to have a pre-defined set of addressable scenes to avoid the wait:
/// <example><code>
/// protected override GetAddressableScenesResult GetAddressableScenes() {
/// return Task.FromResult(new string[] {
/// "Assets/Scenes/AddressableScene1.unity",
/// "Assets/Scenes/AddressableScene2.unity",
/// });
/// }
/// </code></example>
/// </summary>
/// <returns>A task representing resolve operation and optionally a delegate to be invoked before the task is going to be
/// awaited synchronously</returns>
protected virtual GetAddressableScenesResult GetAddressableScenes ( ) {
Log . TraceSceneManager ( Runner , $"Locating addressable scenes with label: {AddressableScenesLabel}" ) ;
var tcs = new TaskCompletionSource < string [ ] > ( ) ;
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 new GetAddressableScenesResult {
Task = tcs . Task ,
// awaiting tasks synchronously does not play well with addressables; simply waiting will block the main thread and that's it.
// addressables *need* to have WaitForCompletion called
BeforeWaitForCompletion = ( ) = > {
if ( result . IsValid ( ) ) {
result . WaitForCompletion ( ) ;
}
} ,
} ;
}
/// <summary>
/// Returns the timeout for addressable scene paths to be resolved. By default, this method returns 10 seconds.
/// </summary>
/// <returns></returns>
protected virtual TimeSpan GetAddressableScenePathsTimeout ( ) {
return TimeSpan . FromSeconds ( 10 ) ;
}
private bool TryGetAddressableScenes ( out string [ ] addressableScenes ) {
if ( ! _addressableScenesTask . IsValueCreated ) {
Log . Warn ( Runner , $"Going to block the thread in wait for addressable scene paths being resolved, call and await {nameof(LoadAddressableScenePathsAsync)} to avoid this." ) ;
}
var t = _addressableScenesTask . Value ;
if ( ! t . Task . IsCompleted ) {
t . BeforeWaitForCompletion ? . Invoke ( ) ;
if ( ! t . Task . Wait ( GetAddressableScenePathsTimeout ( ) ) ) {
addressableScenes = null ;
return false ;
}
}
addressableScenes = t . Task . Result ;
return true ;
}
protected struct GetAddressableScenesResult {
public Task < string [ ] > Task ;
public Action BeforeWaitForCompletion ;
public static implicit operator GetAddressableScenesResult ( Task < string [ ] > task ) {
return new GetAddressableScenesResult {
Task = task ,
} ;
}
}
private Lazy < GetAddressableScenesResult > _addressableScenesTask ;
private Dictionary < SceneRef , AsyncOperationHandle < SceneInstance > > _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" ) ;
}
}
}
}