How the framework starts up in three phases, shuts down cleanly, and where you hook your own code into both moments.
Summary
ScyllaBootstrap is a sealed MonoBehaviour singleton and the only Unity component the framework requires you to place in a scene. It owns the ScyllaCore singleton, initializes the logging, time, event, and scene subsystems, validates the module hierarchy, and runs the module manager through a deterministic three-phase pipeline. Every framework type other than the bootstrap is either a module attached as a direct child of the bootstrap GameObject or a pure C# class owned by ScyllaCore.
The pipeline is intentionally fixed. Phase one runs in Unity’s Awake: singleton check, log configuration, time subsystem init, event system init, scene subsystem init, module-hierarchy validation, framework reference acquisition. Phase two runs in Unity’s Start: it schedules InitializeFramework() after a configurable delay (default 1.0 second). That delay lets your code in Start register dynamic modules before the manager pass scans for them. Phase three is the manager pass itself, kicked off by Invoke: validate dependencies (which transparently calls InjectDependencies first), initialize every module in topological plus priority order, then start and enable every module. Once every module reaches Enabled, ScyllaCore.IsInitialized flips to true and the framework publishes ScyllaFrameworkInitializedEvent.
The scene subsystem changes what “the framework is alive” means across scene loads. When ScyllaSceneManager initializes with persistence enabled (the default), it moves the bootstrap hierarchy, and with it every module, into Unity’s DontDestroyOnLoad scene. The framework then survives every scene change instead of being torn down and rebuilt per scene, so ScyllaCore.Instance and your modules stay live across loads. See Scene Manager for the full picture; the lifecycle consequences relevant here are that startup runs once for the session rather than once per scene, and that a later-loaded scene carrying its own bootstrap is an expected, supported case rather than an error.
Two failure modes abort the pipeline cleanly. A duplicate bootstrap is handled by the singleton check in Awake. When framework persistence is active and the incoming scene simply carries its own bootstrap (a scene authored to also be playable standalone in the Editor), the duplicate is deactivated so its modules never wake, then removed in favor of the persistent instance, logged at info level; nothing aborts. Without persistence, a second bootstrap in the same session is destroyed with an error log and the original continues. A hierarchy violation (a module nested deeper than a direct child of the bootstrap, or a missing dependency, or a circular dependency) sets the bootstrap’s _fatalSetupError flag and disables the component, preventing the rest of the pipeline from running. Module exceptions during initialization log a fatal error and terminate the application (Editor stops play mode; player calls Application.Quit). The fail-fast philosophy is deliberate: an incorrectly configured framework is a development bug, not a runtime condition to paper over.
The reason this matters for your game code is that almost every Scylla feature flows through ScyllaCore.Instance. Knowing when the singleton exists and when the modules are usable determines when you can safely call into the framework. RunWhenReady is the canonical hook: it fires the callback immediately if the framework is already up, and otherwise subscribes to ScyllaFrameworkInitializedEvent (with a race-safe re-check) to fire exactly once when readiness is reached. The shutdown counterpart ShutdownFramework runs modules through OnDisableModule then OnShutdown in strict reverse initialization order, publishing ScyllaFrameworkShutdownEvent first so subscribers can do last-chance work while every module is still fully operational. With persistence on you rarely call it between scenes, since the framework is meant to persist across them; it is still the right tool for application quit (called for you), integration-test teardown, and hot reload.
Use this page for:
- Understanding when each piece of the framework is alive and safe to call.
- Picking the right hook (
RunWhenReadyvsIsInitializedpolling vs direct event subscription) for a specific startup scenario. - Tuning the initialization delay for projects that need faster startup or more time to register dynamic modules.
- Diagnosing fatal-startup errors from the Console output and knowing which phase aborted.
- Implementing a controlled shutdown sequence (integration test teardown, hot-reload, or a persistence-disabled per-scene bootstrap model).
Features
- Single
ScyllaBootstrapMonoBehaviour. The only Unity component the framework requires. Sealed singleton; place it at the root of the first scene loaded and the rest of the framework comes online from there. - Three-phase startup pipeline. Phase 1 (
Awake) brings up subsystems; Phase 2 (Start) schedules the manager pass after a configurable delay; Phase 3 (InitializeFramework) validates, initializes, starts, and enables every module in dependency-respecting order. - Subsystems available before any module. Logging, time, events, and scene management come online in
Awakeso even the first module’sOnInitializecan read pause-awareDeltaTime, publish events, emit log lines, and start managed scene loads. - Framework persistence across scenes. When scene persistence is enabled (the default), the bootstrap hierarchy and every module move to
DontDestroyOnLoadduringAwake, so the framework survives scene changes instead of re-initializing per scene. See Scene Manager. - Configurable initialization delay.
ScyllaCoreConfiguration.InitializationDelay(default 1.0 s) sets the wait betweenStartandInitializeFramework. It lets your code inStartregister dynamic modules before the manager pass scans for them. - Four broadcast events.
ScyllaFrameworkInitializedEvent,ScyllaFrameworkShutdownEvent,ScyllaModuleStartedEvent,ScyllaModuleShutdownEvent. Subscribe to react to specific lifecycle moments without polling. RunWhenReadypost-init hook.ScyllaCore.Instance.RunWhenReady(action)fires the callback immediately if the framework is already up, or schedules it for after the last module reachesEnabled. Race-safe; single-shot.- Reverse-order shutdown. Modules disable and shut down in the reverse of their initialization order, so dependents finish disabling before their dependencies do. Symmetric guarantees on both ends of the pipeline.
- Pre-shutdown event.
ScyllaFrameworkShutdownEventfires before any module is disabled, so your subscribers can do last-chance work (final save, telemetry flush) while every module is still fully operational. - Fail-fast validation at every phase. Duplicate bootstrap, hierarchy violation, missing hard dependency, circular dependency, and module exceptions all abort startup with a structured error and a fatal log line. Editor stops play mode; player calls
Application.Quit. - Singleton check at
Awake. A secondScyllaBootstrapis rejected in its ownAwake. With framework persistence active it is treated as an expected duplicate (a later-loaded scene carrying its own bootstrap): the hierarchy is deactivated so its modules never wake, then removed in favor of the persistent instance, logged at info level. Without persistence it is destroyed with an error log. Either way the original instance continues uninterrupted. - Hierarchy validation at
Awakeand re-validation at phase 3. Modules must be direct children of the bootstrap GameObject; deeper nesting is rejected at startup with a structured error naming the offending GameObject. OnApplicationQuitintegration. Application quit triggersShutdownFrameworkautomatically, publishing the shutdown event, walking every module throughOnDisableModuleandOnShutdownin reverse order, then shutting down the time and scene subsystems. ManualShutdownFrameworkis available for integration tests and hot reload; with persistence on it is rarely needed for scene transitions, since the framework is designed to persist across them.
Phases and Events
The framework’s lifecycle has three startup phases, one shutdown phase, and four broadcast events. Knowing what runs in each phase makes it easier to pick the right hook for a specific piece of startup or teardown work.
A few terms worth defining before the table. Singleton check rejects a second ScyllaBootstrap in its own Awake: under framework persistence it is removed quietly as an expected duplicate from a later-loaded scene, otherwise it is destroyed with an error log. Hierarchy validation rejects modules nested deeper than a direct child of the bootstrap GameObject, and aborts startup if any module fails the check. Initialization delay is the wait between Start and the InitializeFramework invoke; default 1.0 second, configurable on Configuration via ScyllaCoreConfiguration. Reverse-order teardown means modules shut down in the reverse of their initialization order, so dependents finish disabling before their dependencies do.
| Phase | Trigger | Work performed | Side effects observable from your code |
|---|---|---|---|
1. Awake | Unity calls MonoBehaviour.Awake | Singleton check; ConfigureLogging (which initializes the config-file manager, applies overrides, registers UnityConsoleLog and FileLog receivers); InitializeTimeService (creates ScyllaTime.Instance); InitializeEventSystem (initializes the event bus); InitializeSceneService (initializes ScyllaSceneManager and, when persistence is enabled, moves the bootstrap hierarchy to DontDestroyOnLoad); module hierarchy validation; acquire ScyllaCore.Instance. Module children’s Awake runs at the same time as Unity discovers them; each module’s base Awake calls ScyllaCore.Instance.RegisterModule(this). | ScyllaBootstrap.Instance is non-null; ScyllaCore.Instance exists but IsInitialized is false; Log.* calls work; ScyllaTime.Instance.DeltaTime returns the current Unity delta; ScyllaSceneManager.Instance exists and managed loads can start; the framework hierarchy is under DontDestroyOnLoad when persistence is on; modules are Discovered but not yet Initialized. |
2. Start | Unity calls MonoBehaviour.Start | If _fatalSetupError is set from phase 1, return immediately. Otherwise read CoreConfiguration.InitializationDelay (default 1.0 second) and call Invoke(nameof(InitializeFramework), delay) to schedule the phase-3 entry point. | The framework is still in the same state as after phase 1; the only change is that an Invoke is now pending. Any code running in another component’s Start can still register modules dynamically. |
3. InitializeFramework | Invoke fires after the delay | Hierarchy re-validation; delegates to ScyllaCore.InitializeFramework which calls IScyllaModuleManager.ValidateAllDependencies (which calls InjectDependencies on every module first, then ValidateDependencies, then checks for cycles), InitializeAllModules (Initialize in topological plus priority order), StartAllModules (StartModule then EnableModule per module). | IsInitialized flips to true. ScyllaFrameworkInitializedEvent is published. Any pending RunWhenReady callbacks fire. Modules reach Enabled state and are ready for cross-module access. |
| Shutdown | OnApplicationQuit or explicit call | ScyllaCore.ShutdownFramework publishes ScyllaFrameworkShutdownEvent (while modules are still fully operational); IScyllaModuleManager.ShutdownAllModules calls DisableModule then Shutdown on every module in reverse initialization order; IsInitialized flips to false. The bootstrap variant additionally runs ScyllaTime.Shutdown and ScyllaSceneManager.Shutdown, and resets the subsystem one-shot guard so a later bootstrap re-initializes time, events, and scene management. | The framework is no longer initialized. Module references obtained via GetModule<T> should be considered invalid. Log.* still works (logging is configured once per session and is not torn down here). |
The four broadcast events let your code subscribe to specific lifecycle moments without polling.
| Event | When it fires | Subscriber count | Notes |
|---|---|---|---|
ScyllaFrameworkInitializedEvent | After IsInitialized flips to true and every module is Enabled | Multi-subscriber | RunWhenReady subscribes internally with single-shot delivery; you rarely need to subscribe directly. Useful for cross-cutting startup work (analytics init, save-game restore). |
ScyllaFrameworkShutdownEvent | Inside ShutdownFramework, before any module is disabled | Multi-subscriber | Subscribers run while modules are still fully operational. Use this for last-chance work that depends on module state (final save, telemetry flush) before teardown starts. |
ScyllaModuleStartedEvent | After each individual module reaches Started (inside StartAllModules) | Multi-subscriber | Per-module signal. Useful for editor tooling that wants to react to each module coming online during the pipeline pass rather than waiting for the framework-wide initialized event. |
ScyllaModuleShutdownEvent | When each individual module reaches Shutdown (inside ShutdownAllModules) | Multi-subscriber | Per-module shutdown signal. Fires in reverse initialization order. Useful for module-aware cleanup (for example, a save system that flushes per-module data as each module finishes). |
Recipes
These recipes trace the pipeline end to end and cover every lifecycle hook you are likely to reach for. Each one is self-contained, so jump straight to the scenario that matches what you are building. The first recipe (Bring Up the Framework) sets up the context the rest of them build on, so start there if you are new to the bootstrap.
Bring up the framework from a scene
Problem. You want the framework running in your game, with all your modules initialized and ready to use. This is the starting point for every project: one bootstrap in the scene, your modules as direct children, and a configuration asset wired up in the Inspector.
Solution. Place ScyllaBootstrap at the root of your boot scene and attach your module components as direct children of the same GameObject. The framework discovers and registers them automatically during Awake. You do not need to call InitializeFramework yourself; the bootstrap schedules it via Invoke during Start, after a configurable delay that gives any dynamic Start-time registration a chance to complete.
/* ScyllaBootstrap.Awake runs the subsystem bringup in this order.
* You do not write this code - it is what the framework does for you
* as soon as you add the component to your scene. */
private void Awake()
{
/* Singleton enforcement without disturbing the original instance. Under
* framework persistence a later-loaded scene may carry its own bootstrap
* (so it can be played standalone); that duplicate is deactivated so its
* modules never wake, then removed, logged at info level. Without
* persistence a second bootstrap is destroyed with an error log. */
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
/* Order matters: logging first (every subsequent step logs into the
* configured receivers), then time, then events, then scene management
* (its events publish on the bus, so the bus must exist first). Each is a
* synchronous initialization pass. */
ConfigureLogging(); /* config file manager init + receivers registration */
InitializeTimeService(); /* ScyllaTime.Instance becomes usable */
InitializeEventSystem(); /* event bus configured with EventConfiguration */
InitializeSceneService(); /* ScyllaSceneManager.Instance becomes usable; moves the
* bootstrap hierarchy to DontDestroyOnLoad when persistence
* is enabled, so the framework survives scene loads */
Log.System("ScyllaBootstrap awakened. Waiting for module registration ...", LogCategory.Setup);
/* Hierarchy validation runs here and again in phase 3. A module nested
* deeper than a direct child of the bootstrap fails immediately with
* a structured error that names the offending GameObject. */
if (!ValidateModuleHierarchyOrQuit())
{
_fatalSetupError = true;
enabled = false;
return;
}
/* ScyllaCore.Instance is created lazily on first access. From this
* point any code in the scene can read the singleton safely; it is
* not yet initialized (IsInitialized == false) but it exists. */
Framework = ScyllaCore.Instance;
}
Delay initialization until your modules are registered
Problem. Some of your modules are not in the scene at edit time: they are spawned by a scene manager in Start based on the loaded level, or instantiated from a prefab during the boot sequence. You need the manager pass in phase 3 to wait until those modules have had a chance to register.
Solution. The initialization delay on ScyllaCoreConfiguration is exactly this window. Lower it to 0.0 when every module is declared at edit time and you want the fastest possible startup; raise it when complex Start-time registration logic needs more room to breathe.
/* Inside ScyllaBootstrap.Start: */
private void Start()
{
/* A fatal-error flag set during Awake means the bootstrap has already
* disabled itself. Nothing to do. */
if (_fatalSetupError)
{
return;
}
/* Read from the resolved configuration asset, or the compiled default
* (1.0 second) when no asset is bound. */
const float DEFAULT_INITIALIZATION_DELAY = ScyllaCoreConfiguration.DEFAULT_INIT_DELAY;
var delay = HasCoreConfiguration
? CoreConfiguration.InitializationDelay
: DEFAULT_INITIALIZATION_DELAY;
/* Unity Invoke schedules InitializeFramework on the main thread after
* `delay` seconds of unscaled real time. ScyllaTween.Update still
* ticks during the window, so any pre-init tweens run normally. */
Invoke(nameof(InitializeFramework), delay);
}
Trace the lifecycle states through the manager pass
Problem. You want to understand exactly what happens inside phase 3 so you can reason about which module lifecycle hooks fire, in what order, and what state the framework is in when each one runs.
Solution. Phase 3 is where the SMUT (Scylla ModUlar Topology) manager does its work. The bootstrap delegates to ScyllaCore.InitializeFramework, which calls three manager methods in strict order: dependency validation (with implicit injection), topological initialization, and start-plus-enable. Once all three complete, IsInitialized flips and the framework publishes ScyllaFrameworkInitializedEvent.
/* ScyllaCore.InitializeFramework (simplified): */
public void InitializeFramework()
{
if (IsInitialized)
{
Log.Warning("Framework already initialized.", LogCategory.Core);
return;
}
try
{
/* 3a. Validate dependencies. Internally:
* - InjectDependencies(this) on every module so each module can
* call GetRequiredModule / GetOptionalModule from OnInjectDependencies.
* - ValidateDependencies() on each module (checks the Info.Dependencies
* list against registered modules).
* - ValidateNoCycles() to reject circular dependency graphs.
* Returns false on any failure; the framework throws and the rollback
* path runs to bring modules back to Discovered. */
if (!ModuleManager.ValidateAllDependencies())
{
throw new ScyllaModuleException("Framework initialization failed: dependency validation error.");
}
/* 3b. Initialize every module in topological + priority order.
* Each module's OnInitialize hook runs here. Dependencies are
* guaranteed to be in the Initialized state before any dependent. */
ModuleManager.InitializeAllModules();
/* 3c. Start every module. StartAllModules calls StartModule (fires
* OnStart) then EnableModule (fires OnEnableModule) per module.
* After this, every module is in the Enabled state. */
ModuleManager.StartAllModules();
IsInitialized = true;
Log.System("Framework initialization complete.", LogCategory.Core);
/* The broadcast fires after IsInitialized flips so any subscriber
* that re-queries the flag observes true immediately. */
ScyllaEvents.Publish(new ScyllaFrameworkInitializedEvent());
}
catch (Exception ex)
{
Log.Fatal($"Framework initialization failed with error: {ex.Message}", LogCategory.Core);
throw;
}
}
Run code when the framework is ready
Problem. You have a game system – an analytics reporter, a save-game loader, a game-state manager – that should initialize after all modules are up. Unity’s component ordering is not deterministic, and the framework’s initialization delay means you cannot rely on a specific execution order relative to your own Start.
Solution. There are three patterns for this, depending on what you need. RunWhenReady is the right default for the vast majority of cases; IsInitialized polling works for per-frame gates; direct event subscription gives you the priority knob when the callback ordering matters or when you also want to catch the shutdown event.
using Scylla.Core;
using Scylla.Core.Events;
/* Pattern 1: RunWhenReady. Either fires immediately (IsInitialized is
* already true) or subscribes once to ScyllaFrameworkInitializedEvent
* and disposes the token after firing. Race-safe: re-checks IsInitialized
* after the subscription is installed in case init completed during the
* call, which can happen in fast-startup projects with delay set to 0. */
public sealed class GameStartup : MonoBehaviour
{
private void Start()
{
ScyllaCore.Instance.RunWhenReady(() =>
{
/* Every module is Enabled at this point. Cross-module access
* via ScyllaCore.Instance.GetModule<T> is safe. */
Log.Info("Framework is up - loading save game", LogCategory.Game);
});
/* Optional priority parameter controls firing order among multiple
* ready-handlers. Default is Medium. */
ScyllaCore.Instance.RunWhenReady(
onReady: InitializeCriticalSystems,
priority: ScyllaEventPriority.High
);
}
private void InitializeCriticalSystems() { /* runs before Medium-priority handlers */ }
/* Pattern 2: IsInitialized polling. Per-frame logic that should sit
* idle until the framework is up. The property is a single boolean
* read; checking it every frame is cheap enough to do without concern. */
private void Update()
{
if (!ScyllaCore.Instance.IsInitialized)
{
return;
}
/* Framework-dependent per-frame work goes here. */
}
}
/* Pattern 3: Direct event subscription. Use when you want the priority
* knob, or when the same component needs both the initialized and shutdown
* events. Store the returned subscription and dispose it in OnDestroy. */
public sealed class TelemetryReporter : MonoBehaviour
{
private ScyllaEventSubscription _initSub;
private ScyllaEventSubscription _shutdownSub;
private void Start()
{
_initSub = ScyllaEvents.Listen<ScyllaFrameworkInitializedEvent>(
subscriber: this,
handler: evt => SendTelemetry("framework_initialized"),
priority: ScyllaEventPriority.Low /* fire after gameplay startup completes */
);
_shutdownSub = ScyllaEvents.Listen<ScyllaFrameworkShutdownEvent>(
subscriber: this,
handler: evt => SendTelemetry("framework_shutdown"),
priority: ScyllaEventPriority.High /* fire first, while modules are still up */
);
}
private void OnDestroy()
{
_initSub?.Dispose();
_shutdownSub?.Dispose();
}
private void SendTelemetry(string eventName) { /* ... */ }
}
Trigger a clean shutdown
Problem. You need a controlled teardown: you are running an integration test that needs a clean slate for the next fixture, or your editor hot-reload tooling needs to bring the framework down before re-initializing it.
Solution. ShutdownFramework broadcasts the shutdown event (while every module is still up), then tears every module down in reverse initialization order. It is called automatically on OnApplicationQuit, so for application exit you get this for free. For explicit teardown you call it yourself.
/* ScyllaCore.ShutdownFramework (simplified): */
public void ShutdownFramework()
{
if (!IsInitialized)
{
Log.Warning("Framework not initialized.", LogCategory.Core);
return; /* safe to call from OnApplicationQuit even if init never completed */
}
/* Publish first so subscribers can do last-chance work while modules
* are still Enabled. Final save, telemetry flush, network goodbye. */
ScyllaEvents.Publish(new ScyllaFrameworkShutdownEvent());
/* Reverse-order teardown: dependents shut down before their dependencies.
* Each module receives DisableModule (fires OnDisableModule) then Shutdown
* (fires OnShutdown). ScyllaModuleShutdownEvent is also published per
* module so per-module cleanup hooks can react independently. */
ModuleManager.ShutdownAllModules();
IsInitialized = false;
Log.System("Framework shutdown complete.", LogCategory.Core);
}
You can trigger it explicitly from your own code when you need deterministic teardown.
/* Manual shutdown: integration tests, hot-reload tooling, or any case
* that needs deterministic teardown before scene unload. */
ScyllaCore.Instance.ShutdownFramework();
/* The Bootstrap variant also tears down ScyllaTime and ScyllaSceneManager,
* which the core variant does not, and resets the subsystem one-shot guard so
* a later bootstrap re-initializes them. Prefer this in editor tests for a
* fully clean state. */
ScyllaBootstrap.Instance.ShutdownFramework();
Diagnose and understand fail-fast startup errors
Problem. The framework is not coming up: the Editor stops play mode immediately, or the Console is flooded with Fatal-level errors. You need to know which failure mode triggered and how to fix the underlying configuration mistake.
Solution. The bootstrap aborts startup on three categories of failure, each of which produces a specific Fatal log entry. The fail-fast philosophy is deliberate: an incorrectly configured framework is a development bug, not a runtime condition to paper over. Read the Console entry, fix the underlying cause, and run again.
/* Failure mode 1: duplicate bootstrap (without persistence). A second
* ScyllaBootstrap destroys itself in Awake without affecting the original.
* Console entry:
* "Multiple ScyllaBootstrap instances detected! Destroying duplicate."
* Fix: remove the duplicate, or make sure only one scene with a bootstrap
* is loaded at a time.
* Note: with scene persistence enabled (the default) a duplicate from a
* later-loaded scene is NOT an error - it is expected and removed quietly
* with an info-level log ("... carries its own ScyllaBootstrap; removing
* duplicate in favor of the persistent instance."). */
/* Failure mode 2: hierarchy violation. A module nested deeper than a
* direct child of the bootstrap GameObject, or a module placed elsewhere
* in the scene. Detected in Awake and re-detected in InitializeFramework
* to catch modules added or activated after the first pass. Sets
* _fatalSetupError and disables the bootstrap. The Console entry names
* the specific module path that violated the rule.
* Fix: reparent the offending GameObject to be a direct child of the
* bootstrap root. For readability groupings, use prefabs or editor-only
* containers that flatten at runtime. */
/* Failure mode 3: dependency or initialization failure in phase 3.
* ValidateAllDependencies returned false (missing dependency, version
* mismatch, or circular reference), or a module's Initialize / StartModule
* / EnableModule threw. ScyllaCore re-throws; ScyllaBootstrap catches and
* terminates: */
#if UNITY_EDITOR
EditorApplication.isPlaying = false; /* stops play mode */
#else
Application.Quit(); /* exits the player */
#endif
API Sketch
The public surface for bootstrap and lifecycle work is small. Most of your code will touch only ScyllaCore.Instance.RunWhenReady and the four lifecycle events.
namespace Scylla.Core
{
public sealed class ScyllaBootstrap : MonoBehaviour
{
public static ScyllaBootstrap Instance { get; }
public ScyllaCore Framework { get; }
/* Configuration slots. */
public ScyllaCoreConfiguration CoreConfiguration { get; }
public ScyllaLoggerConfiguration LoggerConfiguration { get; }
public ScyllaTimeConfiguration TimeConfiguration { get; }
public ScyllaEventConfiguration EventConfiguration { get; }
public ScyllaSceneConfiguration SceneConfiguration { get; }
public bool HasCoreConfiguration { get; }
public bool HasLoggerConfiguration { get; }
public bool HasTimeConfiguration { get; }
public bool HasEventConfiguration { get; }
public bool HasSceneConfiguration { get; }
/* Lifecycle entry points. Both are idempotent: re-calling on an
* already-initialized framework logs and returns. */
public void InitializeFramework();
public void ShutdownFramework();
}
public sealed class ScyllaCore
{
public static ScyllaCore Instance { get; }
public IScyllaModuleManager ModuleManager { get; }
public bool IsInitialized { get; }
public void InitializeFramework();
public void ShutdownFramework();
public void RegisterModule(IScyllaModule module);
public T GetModule<T>(string moduleID) where T : class, IScyllaModule;
public void RunWhenReady(Action onReady, ScyllaEventPriority priority = ScyllaEventPriority.Medium);
}
}
namespace Scylla.Core.Modules
{
public interface IScyllaModuleManager
{
IReadOnlyList<IScyllaModule> Modules { get; }
/* Phase-3 entry points called by ScyllaCore.InitializeFramework. */
bool ValidateAllDependencies(); /* injects dependencies, validates, checks cycles */
void InitializeAllModules(); /* topological + priority order */
void StartAllModules(); /* StartModule + EnableModule per module */
void ShutdownAllModules(); /* reverse-order teardown */
/* Registration and lookup. */
void RegisterModule(IScyllaModule module);
bool UnregisterModule(string moduleID);
IScyllaModule GetModule(string moduleID);
T GetModule<T>(string moduleID) where T : class, IScyllaModule;
bool HasModule(string moduleID);
List<IScyllaModule> GetModulesInState(ScyllaModuleState state);
int CleanupZombieModules(bool suppressWarning = false);
}
}
namespace Scylla.Core.Events
{
/* Lifecycle events published by the framework. */
public sealed class ScyllaFrameworkInitializedEvent : ScyllaEvent { }
public sealed class ScyllaFrameworkShutdownEvent : ScyllaEvent { }
public sealed class ScyllaModuleStartedEvent : ScyllaEvent { /* + module info */ }
public sealed class ScyllaModuleShutdownEvent : ScyllaEvent { /* + module info */ }
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Configuration Reference
The most commonly tuned property is the initialization delay on ScyllaCoreConfiguration. Two other Core fields shape the framework’s startup behaviour around configuration files; the full configuration surface is documented in Configuration.
| Property | Type | Default | Range | Effect |
|---|---|---|---|---|
InitializationDelay | float | 1.0 | 0.0..5.0 | Seconds between phase-2 Start and phase-3 InitializeFramework. Lower for faster startup when no dynamic registration happens; raise when complex Start registration logic needs more time. |
ConfigFilesEnabledInDebugBuild | bool | true | bool | Whether JSON config-file overrides are loaded in development and debug builds. Set to false to ignore user JSON during testing. |
ConfigFilesEnabledInProductionBuild | bool | false | bool | Same for release builds. Default is to disable JSON overrides in shipped builds so player overrides do not unbalance gameplay. |
CreateConfigFiles | bool | true | bool | Whether the framework auto-creates the Application-tier config files at startup when they are missing. |
Pair the initialization delay with your project’s actual startup needs:
0.0: no delay. Phase 3 runs immediately after phase 2. Use when every module is declared at scene-edit time and no dynamic registration happens. Saves up to 1 second of startup latency.0.5..1.0: the default range. Suitable for almost every project. Allows otherStartmethods to register modules dynamically.2.0+: long delay. Useful for tests or for projects that load and register modules from a remote source duringStart. The delay is unscaled real time, so it runs at the same speed regardless ofTime.timeScale.
Best Practices
- Keep the bootstrap in the first scene loaded. Phase 1 must run before any gameplay code touches the framework; placing the bootstrap in your boot or splash scene is the simplest way to guarantee that. With persistence on, a content scene may also carry its own bootstrap so it stays playable directly from the Editor; the duplicate is removed at load time in favor of the persistent instance, so the first bootstrap always wins. Without persistence, do not include a second bootstrap in scenes loaded later.
- Use
RunWhenReadyfor any gameplay code that depends on the framework. Unity’s component ordering is not deterministic and the initialization delay is intentional.RunWhenReadyis the safe, race-resistant gate for everything that needs the framework to be up. - Tune the initialization delay deliberately. The default
1.0second is conservative. Lower it to0.0for fast startup when no dynamic registration is needed; raise it when complexStartsetup needs more time. Watch the bootstrap logs to confirm modules register inside the window. - Subscribe to
ScyllaFrameworkShutdownEventfor cross-cutting cleanup. Final save, telemetry flush, network goodbye: all of these benefit from running while every module is still fully operational. Per-module cleanup that does not depend on other modules belongs in the module’s ownOnShutdownhook. - Catch nothing in phase-3 lifecycle methods. Module
Initialize,StartModule, andEnableModuleoverrides should not catch their own exceptions; the framework’s rollback and fail-fast path depends on exceptions propagating out. Defensive catches inside lifecycle methods produce surprising partial-initialization states. - Use the bootstrap’s configuration slots in production. The Inspector slots take precedence over the
Resources/Config/fallback. Production builds should reference the configuration assets explicitly so the build is reproducible and the version-controlled assets are visibly the ones in effect. - Cache
ScyllaCore.Instanceonly afterIsInitializedistrue. Reading the singleton property is cheap, but caching the reference before initialization is meaningless because no module is registered yet. Cache afterRunWhenReadyfires or inside anIsInitializedgate. - Order
RunWhenReadypriorities deliberately. High priority for callbacks other callbacks depend on (state mutators, manager init); Medium for general listeners; Low for handlers that should run last (analytics, UI). Mixing priorities at random produces order-dependent bugs that are difficult to reproduce. - Use
ScyllaBootstrap.Instance.ShutdownFrameworkoverScyllaCore.Instance.ShutdownFrameworkin editor tests. The bootstrap variant also tears downScyllaTimeandScyllaSceneManagerand resets the subsystem one-shot guard, which the core variant does not. Together they produce a fully clean state for the next test fixture. - Do not shut the framework down to change scenes. With persistence on (the default), the framework is meant to survive scene loads. Change scenes through
ScyllaSceneManagermanaged loads rather than a shutdown-and-rebuild; that keeps modules, subscriptions, and static state intact across the transition. Manual shutdown is for tests, hot reload, and application quit. - Treat fail-fast errors as development bugs, not runtime conditions. A duplicate bootstrap, a hierarchy violation, a missing dependency: every one is a scene-configuration mistake. Fix the underlying error rather than catching and continuing. The Console log entries name the specific failing module or violation.
- Do not subclass
ScyllaBootstrap. Configure it through Inspector fields and configuration assets. Subclassing would defeat the singleton enforcement and bypass the standard hierarchy validation.
Pitfalls
Related
- Architecture Overview
- Module System
- Tiered Modules
- Getting Started
- Configuration
- Scene Manager
- API reference:
Scylla.Core.ScyllaBootstrap - API reference:
Scylla.Core.ScyllaCore - API reference:
Scylla.Core.Events.ScyllaFrameworkInitializedEvent - API reference:
Scylla.Core.Events.ScyllaFrameworkShutdownEvent - API reference:
Scylla.Core.Modules.IScyllaModuleManager - API reference:
Scylla.Core.Config.ScyllaCoreConfiguration - API reference:
Scylla.Core.Scenes.ScyllaSceneManager