Framework persistence across scene loads plus managed async scene loading with progress reporting, an activation gate, transition hygiene, and optional Addressables routing.
Summary
ScyllaSceneManager is the framework’s scene management service. It does two jobs:
- It keeps the framework alive across scene changes: at startup it moves the bootstrap hierarchy (the
ScyllaGameObject with every module under it) toDontDestroyOnLoad, and when a later-loaded scene carries its own bootstrap, that duplicate is deactivated and removed automatically in favor of the persistent one. Scenes stay self-contained and playable directly from the Editor, and the game never re-initializes the framework mid-session. - It wraps Unity’s scene loading in a managed pipeline: every load and unload publishes typed events on the Scylla event bus, reports progress, optionally holds at an activation gate for loading screens, runs configurable transition hygiene, and awaits registered async tasks before and after the switch.
The service is a plain C# singleton created during ScyllaBootstrap.Awake, the same shape as Time. Access it via ScyllaSceneManager.Instance. Loading is asynchronous by default: LoadSceneAsync returns a SceneLoadOperation exposing Progress (0 to 1), State, an AllowActivation gate, a Completed event, and an Awaitable so calling code can simply await the switch. Single and additive modes are both supported through one fluent options struct; ReloadActiveSceneAsync restarts the current scene in one call, and UnloadSceneAsync removes an additively loaded scene. A synchronous LoadScene exists for the rare case that genuinely cannot yield a frame. Games that live in a single scene need no calls at all; the persistence half works on its own.
Transition hygiene is what makes destructive scene changes (single loads, reloads, unloads) safe by default. Persistent state that outlives a scene can reference objects that die with it: an in-flight tween targeting a destroyed transform, a timescale modifier pushed by an effect that no longer exists, an event subscriber whose GameObject is gone. Before every destructive change the manager kills active tweens, clears the timescale-modifier stack, and prunes dead event-bus subscribers; each step is a configuration toggle and can be forced on or off per call via HygieneOverride. Additive loads skip hygiene by default since nothing is destroyed.
When the Addressables package is installed (the SCYLLA_HAS_ADDRESSABLES define), LoadAddressableSceneAsync loads scenes by address key or AssetReference through the same pipeline: same events, same gate, same hygiene. Unloading an addressable-loaded scene goes through the regular UnloadSceneAsync; the manager routes it back through Addressables internally.
Use ScyllaSceneManager for:
- Multi-scene games where the framework and its modules must survive scene changes.
- Level switching, restart-on-death, and hub-and-spoke scene flows.
- Loading screens with a progress bar and a “press any key” activation gate.
- Additive content streaming (load a district in, unload it behind the player).
- Autosaves or fade-outs that must complete before a scene change starts.
- Addressables-based scene content that is not part of the Build Settings list.
Features
- Framework persistence. The bootstrap hierarchy moves to
DontDestroyOnLoadat startup (config-gated, on by default). Modules initialize once and survive every scene change. - Duplicate-bootstrap deduplication. A scene that carries its own
Scyllabootstrap can still be played directly in the Editor; when it loads into a running session, the duplicate is deactivated and destroyed before any of its modules wake up. - Managed async loading.
LoadSceneAsync(name)/LoadSceneAsync(buildIndex)return aSceneLoadOperationwithProgress,State,Scene,Error, aCompletedevent, and anAwaitable. - Single and additive modes. One options struct covers both:
ScyllaSceneLoadOptions.Defaultis a single load;.WithAdditive()switches to additive.SetActiveSceneOnAdditiveLoad(config) or.WithSetActiveScene(true)(per call) makes the new scene active. - Activation gate.
.WithManualActivation()holds the load at 90% until code setsoperation.AllowActivation = true. The right choice for “press any key to continue” screens. - Minimum duration.
.WithMinDuration(seconds)keeps the operation from completing before the given real time has passed, so fast loads do not flash a one-frame loading screen. A config default applies to every load. - Transition hygiene. Tween kill, timescale-modifier clear, and event-bus subscriber prune before destructive changes. Three config toggles, each overridable per call with
HygieneOverride.ForceOn/ForceOff. - Transition tasks.
RegisterPreLoadTask/RegisterPostLoadTaskacceptFunc<Awaitable>hooks. Pre-load tasks run after the will-events and before hygiene (fade-outs, autosaves); post-load tasks run after the scene is up and before the operation completes (fade-ins, warm-ups). - Typed scene events. Six
ScyllaEventclasses published on the bus: will-load, loaded, will-unload, unloaded, active-scene-changed, and load-progress (throttled by a configurable epsilon). - Reload and unload.
ReloadActiveSceneAsync()restarts the active scene;UnloadSceneAsync(name)/UnloadSceneAsync(scene)remove an additively loaded scene, with progress and events. - Synchronous fallback.
LoadScene(name)performs a blocking load for code paths that cannot yield. Hygiene and events still apply; the gate, minimum duration, progress reporting, and transition tasks do not. - Addressables routing. With the Addressables package installed,
LoadAddressableSceneAsync(address)/(AssetReference)load scenes through the same pipeline, including scenes that are not in Build Settings. - Fail-fast validation. Loading a scene that is not in Build Settings, starting a single load while another operation is in flight, or loading the same scene twice concurrently throws immediately with an actionable message instead of failing somewhere inside Unity.
Concepts
A few terms appear repeatedly. Persistence means the bootstrap hierarchy lives in the DontDestroyOnLoad scene and survives scene changes. Destructive describes an operation that destroys loaded content: single loads, reloads, and unloads are destructive; additive loads are not. Hygiene is the cleanup pass (tweens, timescale modifiers, event subscribers) that runs before destructive operations. The gate is the activation hold: Unity keeps a load at progress 0.9 until activation is allowed, and the manager exposes that as an explicit flag. Operation is one SceneLoadOperation handle representing a load or unload in flight.
| Concept | Type | Notes |
|---|---|---|
ScyllaSceneManager | sealed singleton | The scene management service. Instance is the singleton; created during ScyllaBootstrap.Awake. Owns persistence, the load pipeline, hygiene, transition tasks, and the scene events. |
SceneLoadOperation | sealed class | Handle for one load or unload. SceneName, BuildIndex, Mode, IsUnload, State, Progress, Scene, Error, AllowActivation, Awaitable, Completed event. Returned by every async load method. |
ScyllaSceneLoadOptions | readonly struct | Fluent per-call options: WithAdditive, WithSetActiveScene, WithManualActivation, WithMinDuration, and the three hygiene overrides. Default is a single-mode load with automatic activation. |
SceneLoadState | enum | NotStarted, Loading, WaitingForActivation, Activating, Completed, Failed. Completed and Failed are terminal. |
HygieneOverride | enum | Tri-state per-call override for one hygiene step: UseConfig (default), ForceOn, ForceOff. |
ScyllaSceneConfiguration | ScriptableObject | Configuration asset bound to the Bootstrap’s Scene Configuration slot: persistence, active-scene policy, default minimum duration, progress epsilon, and the three hygiene toggles. |
| Scene event family | sealed ScyllaEvent classes | ScyllaSceneWillLoadEvent, ScyllaSceneLoadedEvent, ScyllaSceneWillUnloadEvent, ScyllaSceneUnloadedEvent, ScyllaActiveSceneChangedEvent, ScyllaSceneLoadProgressEvent. Subscribe via ScyllaEvents.Listen<T>. |
Concurrency follows three rules. A single-mode load requires that no other operation is in flight. Additive loads and unloads can run concurrently with each other, but not while a single-mode load is in flight, and not two operations for the same scene at once. A synchronous LoadScene must finish (one frame) before the next operation starts. Violations throw InvalidOperationException at the call site.
Recipes
These recipes cover the public surface of ScyllaSceneManager and SceneLoadOperation: loading, unloading, reloading, the activation gate, transition tasks, hygiene overrides, events, and the Addressables path. Each one is self-contained.
Switch scenes and await the result
Problem. The player finished the level and the game should move to the next scene, with the calling code continuing only after the new scene is up.
Solution. Call LoadSceneAsync and await the returned operation. The default options perform a single (non-additive) load with automatic activation: the current scene is replaced, hygiene runs, and the framework hierarchy persists through the switch. Failures do not throw from the await; the operation completes with State == SceneLoadState.Failed and the reason in Error.
using Scylla.Core.Scenes;
public async void OnLevelFinished()
{
var operation = ScyllaSceneManager.Instance.LoadSceneAsync("Level_02");
await operation.Awaitable;
if (operation.State == SceneLoadState.Failed)
{
Log.Error($"Level load failed: {operation.Error}", LogCategory.Scenes);
return;
}
/* The new scene is loaded and active here. */
}
Loading by build index works the same way: LoadSceneAsync(2). Scene names must be listed in Build Settings; a name that is not resolvable throws ArgumentException immediately rather than failing later.
Restart the current scene
Problem. The player died and the level should restart cleanly: no leftover tweens, no stale timescale effects, no dead event subscribers.
Solution. ReloadActiveSceneAsync reloads the active scene as a single load. Hygiene runs first (it is a destructive change), so the restart begins from a clean slate. This replaces the manual pattern of killing tweens and resetting time state by hand before calling Unity’s SceneManager.LoadScene.
private void OnPlayerDied()
{
ScyllaSceneManager.Instance.ReloadActiveSceneAsync();
}
Drive a loading screen with progress and a gate
Problem. A larger level needs a loading screen with a progress bar, and the switch should happen only when the player presses a key, never before the loading screen has been visible for at least a couple of seconds.
Solution. Combine WithManualActivation and WithMinDuration. The operation loads to 90%, then holds in SceneLoadState.WaitingForActivation until code sets AllowActivation = true and the minimum duration has passed. Both conditions must hold; setting the flag early is fine.
using Scylla.Core.Scenes;
private SceneLoadOperation _pending;
public void StartGatedLoad()
{
var options = ScyllaSceneLoadOptions.Default
.WithManualActivation()
.WithMinDuration(2f);
_pending = ScyllaSceneManager.Instance.LoadSceneAsync("Level_03", options);
}
private void Update()
{
if (_pending == null)
{
return;
}
_progressBar.value = _pending.Progress; /* holds at 0.9 while gated */
if (_pending.State == SceneLoadState.WaitingForActivation && _anyKeyPressed)
{
_pending.AllowActivation = true; /* one-way; cannot be revoked */
}
if (_pending.State == SceneLoadState.Completed)
{
_pending = null;
}
}
DefaultMinLoadDurationSeconds in the configuration applies a minimum duration to every load without per-call options; the per-call value takes precedence.
Stream content additively and unload it again
Problem. An open area should load neighboring districts in the background as the player approaches and drop them again behind, without ever tearing down the main scene.
Solution. Additive loads keep everything already loaded. They skip hygiene by default (nothing is destroyed) and can run concurrently, as long as no single-mode load is in flight and no two operations target the same scene. Unload by name or by Scene handle when the content is out of range.
using Scylla.Core.Scenes;
/* Load a district on top of the current scene. */
ScyllaSceneManager.Instance.LoadSceneAsync(
"District_North",
ScyllaSceneLoadOptions.Default.WithAdditive());
/* Optionally make the new scene the active one (lighting, new-object parenting). */
ScyllaSceneManager.Instance.LoadSceneAsync(
"District_East",
ScyllaSceneLoadOptions.Default.WithAdditive().WithSetActiveScene(true));
/* Drop a district that is out of range. */
ScyllaSceneManager.Instance.UnloadSceneAsync("District_North");
SetActiveSceneOnAdditiveLoad in the configuration makes every additive load set the active scene without per-call options; WithSetActiveScene overrides it per call in either direction.
Run an autosave or fade before every scene change
Problem. The game should autosave (or fade to black) before any scene change starts, and fade back in after the new scene is up, without every load call site remembering to do it.
Solution. Register transition tasks once. Pre-load tasks run after the will-events and before hygiene; post-load tasks run after the scene is loaded and before the operation completes. Both are Func<Awaitable>, so the pipeline genuinely waits for them. Unregister with the same delegate reference.
using Scylla.Core.Scenes;
using UnityEngine;
private async Awaitable AutosaveBeforeSceneChange()
{
await SaveSystem.WriteAutosaveAsync();
}
private void OnEnable()
{
ScyllaSceneManager.Instance.RegisterPreLoadTask(AutosaveBeforeSceneChange);
}
private void OnDisable()
{
ScyllaSceneManager.Instance.UnregisterPreLoadTask(AutosaveBeforeSceneChange);
}
Transition tasks run for asynchronous loads only; the synchronous LoadScene cannot await them and skips them.
Override hygiene for one call
Problem. One specific additive load brings in a cutscene overlay, and the timescale-modifier stack should be cleared for it even though additive loads normally skip hygiene. Conversely, one specific reload must preserve a running tween.
Solution. Each hygiene step accepts a per-call tri-state override. UseConfig (the default) applies the configured behavior: run on destructive operations when the config toggle is on. ForceOn and ForceOff win over the configuration in both directions.
using Scylla.Core.Scenes;
/* Additive load that clears timescale modifiers anyway. */
ScyllaSceneManager.Instance.LoadSceneAsync(
"CutsceneOverlay",
ScyllaSceneLoadOptions.Default
.WithAdditive()
.WithTimeModifierHygiene(HygieneOverride.ForceOn));
/* Reload that must not kill tweens (they animate the transition itself). */
ScyllaSceneManager.Instance.LoadSceneAsync(
"Level_01",
ScyllaSceneLoadOptions.Default
.WithTweenHygiene(HygieneOverride.ForceOff));
The three steps (WithTweenHygiene, WithTimeModifierHygiene, WithEventBusHygiene) are independent; override any subset.
React to scene changes anywhere in the game
Problem. Systems that are not the load call site (music, analytics, UI) need to know when scenes change.
Solution. Subscribe to the six scene events on the Scylla event bus (see Event System). The will-events fire before anything is destroyed, the completion events after the switch; the progress event is throttled so it only fires when progress advances by at least ProgressEventEpsilon.
using Scylla.Core.Events;
using Scylla.Core.Scenes;
private ScyllaEventSubscription _loadedSub;
private ScyllaEventSubscription _progressSub;
private void OnEnable()
{
_loadedSub = ScyllaEvents.Listen<ScyllaSceneLoadedEvent>(this, e =>
Log.Info($"Scene up: {e.Scene.name} ({e.Mode}) in {e.DurationSeconds:F2}s", LogCategory.Scenes));
_progressSub = ScyllaEvents.Listen<ScyllaSceneLoadProgressEvent>(this, e =>
_loadingBar.value = e.Progress);
}
private void OnDestroy()
{
_loadedSub?.Dispose();
_progressSub?.Dispose();
}
The event payloads: ScyllaSceneWillLoadEvent carries SceneName, BuildIndex, Mode; ScyllaSceneLoadedEvent carries Scene, Mode, DurationSeconds; ScyllaSceneWillUnloadEvent carries Scene; ScyllaSceneUnloadedEvent carries SceneName; ScyllaActiveSceneChangedEvent carries PreviousScene, NewScene; ScyllaSceneLoadProgressEvent carries SceneName, Progress, State.
Load a scene through Addressables
Problem. Scene content ships through Addressables (downloadable levels, DLC, content not in the base build) and should still go through the managed pipeline.
Solution. With the Addressables package installed, LoadAddressableSceneAsync accepts an address key or an AssetReference and behaves exactly like LoadSceneAsync: same options, same gate, same hygiene, same events. Addressable scenes must not be listed in Build Settings. Unload through the regular UnloadSceneAsync; the manager detects the addressable origin and releases the Addressables handle internally.
#if SCYLLA_HAS_ADDRESSABLES
using Scylla.Core.Scenes;
var operation = ScyllaSceneManager.Instance.LoadAddressableSceneAsync("Level_DLC_01");
await operation.Awaitable;
if (operation.State == SceneLoadState.Failed)
{
/* Invalid address or missing bundle: the Addressables error text is in operation.Error. */
Log.Error(operation.Error, LogCategory.Scenes);
}
#endif
An invalid address fails asynchronously (State == Failed, Error set), not with a thrown exception, because address resolution happens inside the Addressables system.
Load synchronously when yielding is not an option
Problem. A code path genuinely cannot yield a frame and needs the classic blocking load.
Solution. LoadScene wraps Unity’s synchronous load. Events and hygiene still apply; the gate, minimum duration, progress reporting, and transition tasks do not (they require frames to pass). Unity completes synchronous loads at the start of the next frame, so the completion events arrive one frame later, and starting another operation in the same frame throws.
ScyllaSceneManager.Instance.LoadScene("Bootstrap_Fallback");
Prefer LoadSceneAsync everywhere else; the synchronous path exists as an escape hatch, not as the default.
Check persistence state
Problem. Editor tooling or debug UI wants to display whether the framework is in persistent mode and whether a load is running.
Solution. Three read-only properties cover it. PersistenceEnabled reports whether the bootstrap hierarchy was actually moved to DontDestroyOnLoad (false when disabled by configuration or outside play mode). IsLoading is true while any managed operation is in flight. CurrentOperation is the most recently started operation still in flight, or null when idle.
var m = ScyllaSceneManager.Instance;
statusLabel.text = $"Persistent: {m.PersistenceEnabled} Loading: {m.IsLoading}"
+ (m.CurrentOperation != null ? $" {m.CurrentOperation.SceneName} {m.CurrentOperation.Progress:P0}" : string.Empty);
API Sketch
namespace Scylla.Core.Scenes
{
/* Scene management service. Created by ScyllaBootstrap; access via Instance. */
public sealed class ScyllaSceneManager
{
public static ScyllaSceneManager Instance { get; }
public bool PersistenceEnabled { get; }
public bool IsLoading { get; }
public SceneLoadOperation CurrentOperation { get; }
/* Managed async loading. */
public SceneLoadOperation LoadSceneAsync(string sceneName, ScyllaSceneLoadOptions options = default);
public SceneLoadOperation LoadSceneAsync(int buildIndex, ScyllaSceneLoadOptions options = default);
public SceneLoadOperation ReloadActiveSceneAsync();
public SceneLoadOperation UnloadSceneAsync(string sceneName, ScyllaSceneLoadOptions options = default);
public SceneLoadOperation UnloadSceneAsync(Scene scene, ScyllaSceneLoadOptions options = default);
/* Synchronous fallback (hygiene and events only; no gate, progress, or tasks). */
public void LoadScene(string sceneName, ScyllaSceneLoadOptions options = default);
/* Addressables routing (compiled when the Addressables package is installed). */
#if SCYLLA_HAS_ADDRESSABLES
public SceneLoadOperation LoadAddressableSceneAsync(string address, ScyllaSceneLoadOptions options = default);
public SceneLoadOperation LoadAddressableSceneAsync(AssetReference sceneReference, ScyllaSceneLoadOptions options = default);
#endif
/* Awaited transition hooks. */
public void RegisterPreLoadTask(Func<Awaitable> task);
public void UnregisterPreLoadTask(Func<Awaitable> task);
public void RegisterPostLoadTask(Func<Awaitable> task);
public void UnregisterPostLoadTask(Func<Awaitable> task);
}
/* Handle for one load or unload in flight. */
public sealed class SceneLoadOperation
{
public string SceneName { get; }
public int BuildIndex { get; }
public LoadSceneMode Mode { get; }
public bool IsUnload { get; }
public SceneLoadState State { get; }
public float Progress { get; } /* 0..1; holds 0.9 while gated */
public Scene Scene { get; }
public string Error { get; }
public bool AllowActivation { get; set; } /* one-way gate */
public Awaitable Awaitable { get; } /* completes on success AND failure */
public event Action<SceneLoadOperation> Completed; /* late subscribers fire immediately */
}
/* Fluent per-call options. Default = single mode, automatic activation. */
public readonly struct ScyllaSceneLoadOptions
{
public static ScyllaSceneLoadOptions Default { get; }
public LoadSceneMode Mode { get; }
public bool? SetActiveScene { get; }
public bool ManualActivation { get; }
public float? MinDurationSeconds { get; }
public HygieneOverride TweenHygiene { get; }
public HygieneOverride TimeModifierHygiene { get; }
public HygieneOverride EventBusHygiene { get; }
public ScyllaSceneLoadOptions WithMode(LoadSceneMode mode);
public ScyllaSceneLoadOptions WithAdditive();
public ScyllaSceneLoadOptions WithSetActiveScene(bool setActiveScene);
public ScyllaSceneLoadOptions WithManualActivation();
public ScyllaSceneLoadOptions WithMinDuration(float minDurationSeconds);
public ScyllaSceneLoadOptions WithTweenHygiene(HygieneOverride hygieneOverride);
public ScyllaSceneLoadOptions WithTimeModifierHygiene(HygieneOverride hygieneOverride);
public ScyllaSceneLoadOptions WithEventBusHygiene(HygieneOverride hygieneOverride);
}
/* Operation lifecycle. Completed and Failed are terminal. */
public enum SceneLoadState { NotStarted, Loading, WaitingForActivation, Activating, Completed, Failed }
/* Per-call hygiene override. */
public enum HygieneOverride { UseConfig, ForceOn, ForceOff }
/* Scene events, published on the Scylla event bus. */
public sealed class ScyllaSceneWillLoadEvent : ScyllaEvent { /* SceneName, BuildIndex, Mode */ }
public sealed class ScyllaSceneLoadedEvent : ScyllaEvent { /* Scene, Mode, DurationSeconds */ }
public sealed class ScyllaSceneWillUnloadEvent : ScyllaEvent { /* Scene */ }
public sealed class ScyllaSceneUnloadedEvent : ScyllaEvent { /* SceneName */ }
public sealed class ScyllaActiveSceneChangedEvent : ScyllaEvent { /* PreviousScene, NewScene */ }
public sealed class ScyllaSceneLoadProgressEvent : ScyllaEvent { /* SceneName, Progress, State */ }
/* Configuration asset bound to the Bootstrap's Scene Configuration slot. */
public sealed class ScyllaSceneConfiguration : ScyllaConfiguration
{
public bool PersistFrameworkAcrossScenes { get; }
public bool SetActiveSceneOnAdditiveLoad { get; }
public float DefaultMinLoadDurationSeconds { get; }
public float ProgressEventEpsilon { get; }
public bool KillTweensOnSceneChange { get; }
public bool ClearTimeModifiersOnSceneChange { get; }
public bool PruneEventSubscribersOnSceneChange { get; }
}
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Settings reference
ScyllaSceneConfiguration is bound to the Bootstrap’s Scene Configuration slot. You edit it in the Inspector and override it at runtime via the JSON config-file system (see Configuration).
| Group | Property | Type | Default | Effect |
|---|---|---|---|---|
| General | PersistFrameworkAcrossScenes | bool | true | Moves the bootstrap hierarchy to DontDestroyOnLoad at startup and deduplicates bootstraps in later-loaded scenes. |
| General | SetActiveSceneOnAdditiveLoad | bool | false | Whether additively loaded scenes become the active scene by default. Overridable per call via WithSetActiveScene. |
| Loading | DefaultMinLoadDurationSeconds | float | 0 | Minimum duration applied to every managed load. Range 0..60. Per-call WithMinDuration takes precedence. |
| Loading | ProgressEventEpsilon | float | 0.01 | Minimum progress advance before another ScyllaSceneLoadProgressEvent is published. Range 0.001..0.5. |
| Hygiene | KillTweensOnSceneChange | bool | true | Kills all active tweens before destructive scene changes. |
| Hygiene | ClearTimeModifiersOnSceneChange | bool | true | Clears the timescale-modifier stack before destructive scene changes. |
| Hygiene | PruneEventSubscribersOnSceneChange | bool | true | Removes event-bus subscriptions whose Unity objects have been destroyed before destructive scene changes. |
Best Practices
- Route every scene change through
ScyllaSceneManager. Calling Unity’sSceneManager.LoadScenedirectly skips hygiene, the events, and the transition tasks; systems listening for scene events never hear about the change. - Keep a bootstrap in every scene that should be playable on its own. The dedup logic makes this free at runtime: pressing Play on any scene works in the Editor, and in a running session the duplicate is discarded before it initializes anything.
- Await the operation when subsequent code depends on the new scene. Fire-and-forget is fine for simple switches; anything that touches objects in the new scene should
await operation.Awaitableand checkStatefirst. - Check
StateandErrorinstead of wrapping loads in try/catch. Runtime failures complete the operation asFailedwithout throwing. Only argument and concurrency violations throw, and they throw at the call site. - Use the activation gate plus a minimum duration for loading screens. The gate alone can flash-complete on small scenes; the minimum duration alone cannot wait for player input. Together they produce a loading screen that is neither a flicker nor a lock.
- Prefer additive loads for content that coexists. UI overlays, streamed districts, and debug scenes belong on top of the main scene, not instead of it. Additive loads skip hygiene and leave every running system alone.
- Register fade and autosave logic once as transition tasks. A pre-load task runs before every managed scene change, so call sites stay one-liners and the behavior cannot be forgotten on a new code path.
- Leave the hygiene defaults on. Killed tweens, cleared timescale modifiers, and pruned subscribers are exactly the state that causes post-load exceptions when it leaks across a destructive change. Opt out per call with
ForceOfffor the rare transition that must preserve state. - Use
WithSetActiveScene(true)deliberately. The active scene controls lighting settings and where new objects spawn. For streamed content the main scene usually stays active; for a full handover to an additive scene, set it explicitly. - Subscribe to the scene events for cross-cutting reactions. Music crossfades, analytics, and loading UI belong in event subscribers, not in every load call site. Dispose subscriptions in
OnDestroy. - Keep addressable scenes out of Build Settings. A scene reachable both ways confuses identity and duplicates content in the build. Pick one distribution channel per scene.
- Treat
LoadScene(synchronous) as the escape hatch. It blocks the frame, skips the gate and progress, and cannot run transition tasks. Almost every call site is better served byLoadSceneAsync.
Pitfalls
Related
- Configuration
- Time
- Bootstrap Lifecycle
- Event System
- Getting Started
- API reference:
Scylla.Core.Scenes.ScyllaSceneManager - API reference:
Scylla.Core.Scenes.SceneLoadOperation - API reference:
Scylla.Core.Scenes.ScyllaSceneLoadOptions - API reference:
Scylla.Core.Scenes.SceneLoadState - API reference:
Scylla.Core.Scenes.HygieneOverride - API reference:
Scylla.Core.Scenes.ScyllaSceneConfiguration - API reference:
Scylla.Core.Scenes.ScyllaSceneWillLoadEvent - API reference:
Scylla.Core.Scenes.ScyllaSceneLoadedEvent - API reference:
Scylla.Core.Scenes.ScyllaSceneWillUnloadEvent - API reference:
Scylla.Core.Scenes.ScyllaSceneUnloadedEvent - API reference:
Scylla.Core.Scenes.ScyllaActiveSceneChangedEvent - API reference:
Scylla.Core.Scenes.ScyllaSceneLoadProgressEvent