Architecture Overview

24 min read

A tour of the framework’s runtime spine: the bootstrap component you drop in a scene, the C# singleton it creates, the module manager that drives the lifecycle, and the supporting subsystems every module can count on from the first frame.

Summary

Scylla has a small, fixed runtime spine: a MonoBehaviour bootstrap, a C# singleton, a module manager, and a registry of modules. Every other framework type is either a module under the bootstrap, a supporting subsystem owned by the bootstrap (time, event bus, logging), or a pure C# utility class with no lifecycle attachment. The spine is the same for every Scylla project on every platform; the moving parts are the modules your project chooses to enable.

The bootstrap is the single Unity-side entry point. ScyllaBootstrap is a sealed MonoBehaviour you place at the root of the first scene loaded; its Awake runs the singleton check, configures logging, brings up the time, event, and scene subsystems (the last moving the framework hierarchy to DontDestroyOnLoad so it survives scene loads), validates the module hierarchy, and creates the ScyllaCore singleton. ScyllaCore is a pure C# singleton (not a Unity object) that owns the ScyllaModuleManager and exposes the framework-wide convenience methods (GetModule<T>, RunWhenReady, RegisterModule, InitializeFramework, ShutdownFramework). The module manager validates the dependency graph, sorts modules topologically plus by InitPriority, and runs each one through the lifecycle (Initialize, StartModule, EnableModule). Once every module reaches the Enabled state, the framework publishes ScyllaFrameworkInitializedEvent and any code waiting via RunWhenReady runs.

Modules are MonoBehaviour components that inherit from ScyllaModule and live as direct children of the bootstrap GameObject. They auto-register in their own Awake (via base.Awake()); they declare their identity, version, dependencies, and initialization priority through a ScyllaModuleInfo; the framework runs them through a fixed lifecycle with protected hooks for each phase. The SMUT (Scylla ModUlar Topology) organizes modules into three tiers (foundation, features, gameplay). Dependencies flow only downward across tiers; same-tier modules cannot reference each other directly and instead communicate through the SEX (Scylla Event eXchange) bus.

The reason this layout works is that every subsystem the rest of the framework needs is discoverable by predictable rules. Where is the input module? ScyllaCore.Instance.GetModule<ScyllaInput>(ScyllaModuleID.INPUT). When is it safe to call? After ScyllaCore.Instance.IsInitialized is true, which is what RunWhenReady waits for. How does another module depend on it? By declaring ScyllaModuleID.INPUT as a hard dependency in Info.Dependencies; the framework’s startup pass fails fast if the dependency is missing. The bootstrap is in charge of timing; the singleton is in charge of identity; the manager is in charge of dependency-respecting order; modules implement gameplay. Each layer has one job; together they form a startup pipeline that is either fully correct or refuses to run.

Use this page for:

  • A high-level orientation before diving into individual subsystem pages.
  • Understanding where each piece lives in the source tree.
  • Picking the right page to read next based on the question you’re asking (lifecycle? Bootstrap Lifecycle; module shape? Module System; tier rules? Tiered Modules; events? Event System).
  • Quick lookup of which static entry points exist on which type.

Features

  • Four-layer runtime spine. Bootstrap (MonoBehaviour) -> Core (C# singleton) -> Module Manager -> Modules. Each layer has one responsibility and is owned by the layer above; the spine is identical on every platform.
  • Single Unity-side entry point. ScyllaBootstrap is the only MonoBehaviour the framework requires. Drop it at the root of the first scene loaded; the rest of the spine comes online from there.
  • Pure-C# ScyllaCore singleton. Not a Unity object. Owns the module manager, exposes GetModule<T>, RunWhenReady, RegisterModule, InitializeFramework, ShutdownFramework, IsInitialized. Access via ScyllaCore.Instance.
  • Three-phase startup pipeline. Awake brings up subsystems; Start validates the hierarchy; InitializeFramework runs every module through Initialize -> StartModule -> EnableModule in dependency-respecting order. Shutdown is the same chain in reverse.
  • Module lifecycle with protected hooks. OnInitialize, OnStart, OnEnableModule, OnDisableModule, OnShutdown, OnInjectDependencies. The framework calls each hook in deterministic order; you override the ones your module needs.
  • Auto-discovery of modules. Every ScyllaModule placed as a direct child of the bootstrap GameObject is discovered and registered automatically via base.Awake(). No central registry to maintain.
  • Dependency-respecting initialization order. The module manager validates the dependency graph at startup, computes a topological sort plus InitPriority, and fails fast on missing dependencies, cycles, or hierarchy violations.
  • Tiered architecture. Three tiers (foundation, features, gameplay) with strict downward dependency flow. Same-tier modules never reference each other; cross-tier upward dependencies are rejected at startup.
  • Time subsystem available before modules. ScyllaTime comes online during ScyllaBootstrap.Awake, so even the first module’s OnInitialize can read pause-aware DeltaTime. See Time.
  • Event bus available before modules. The static ScyllaEvents facade routes through ScyllaEventBus, also available during Awake. Modules can publish or subscribe before they finish initializing. See Event System.
  • Logging available before modules. Log.Info, Log.Warning, etc. route through ILogReceiver implementations from the first frame, so any startup failure is captured. See Logging.
  • Scene manager available before modules, and the framework persists across scenes. ScyllaSceneManager comes online during ScyllaBootstrap.Awake and, with persistence on (the default), moves the framework hierarchy to DontDestroyOnLoad so modules survive scene loads instead of re-initializing per scene. See Scene Manager.
  • RunWhenReady for post-init code. Code that needs every module fully enabled queues through ScyllaCore.Instance.RunWhenReady(action). The framework runs the action immediately if already ready, or queues it for after the last module reaches Enabled.

The runtime spine

Four layers form the framework’s spine. Each has a single responsibility; each is owned by the layer above it. Two ancillary subsystems (time and events) are owned directly by the bootstrap and exist before any module runs, so even bootstrap-phase code can use them. Two additional subsystems (logging and configuration) are static-class facades that come online during Awake before any module is touched.

A few terms appear repeatedly in this page and across the architecture docs. Bootstrap is the ScyllaBootstrap MonoBehaviour in the scene. Core is ScyllaCore.Instance, the pure-C# singleton that owns the module manager. Module manager is IScyllaModuleManager (implemented by ScyllaModuleManager), the per-process registry of modules with dependency-aware lifecycle methods. Module is a class inheriting from ScyllaModule. Subsystem is a non-module supporting system (time, events, logging, configuration) that is part of the framework’s startup but does not participate in the module pipeline.

Layer / subsystemTypeOwned byRole
ScyllaBootstrapsealed MonoBehaviour singletonUnity sceneThe single Unity-side entry point. Runs singleton check, brings up time / events / logging / scene management, validates the module hierarchy, creates ScyllaCore, schedules InitializeFramework. See Bootstrap Lifecycle.
ScyllaCoresealed C# singletonScyllaBootstrapThe framework manager. Owns IScyllaModuleManager, exposes GetModule<T>, RunWhenReady, RegisterModule, InitializeFramework, ShutdownFramework, and IsInitialized. Pure C#: not a Unity object. Access via ScyllaCore.Instance.
IScyllaModuleManagerinterface (default ScyllaModuleManager)ScyllaCoreThe per-process registry of modules. Validates the dependency graph, computes topological + priority order, runs each module through Initialize / StartModule / EnableModule on startup, and the reverse on shutdown. See Module System.
ScyllaModuleabstract MonoBehaviourscene (one per module)Base class for every module. Auto-registers in Awake. Declares identity and dependencies via Info. Exposes protected lifecycle hooks (OnInitialize, OnStart, OnEnableModule, OnDisableModule, OnShutdown, OnInjectDependencies).
ScyllaTimesealed C# singletonScyllaBootstrapPause-aware time source. Created in bootstrap Awake. Read pause-aware DeltaTime here for gameplay code; reach for UnityEngine.Time only for “ignore pause” cases. See Time.
Event bus (SEX)static facade ScyllaEvents over IScyllaEventBusScyllaBootstrapProject-wide publish/subscribe. Static ScyllaEvents.Publish<T> / Listen<T> is the entry point. Five priority levels, cancellation, weak-reference auto-cleanup. See Event System.
ScyllaSceneManagersealed C# singletonScyllaBootstrapScene management. Created in bootstrap Awake. Keeps the framework hierarchy alive across scene loads (DontDestroyOnLoad plus per-scene bootstrap dedup) and wraps Unity scene loading in a managed pipeline (typed events, progress, activation gate, transition hygiene, optional Addressables). See Scene Manager.
Log + receiversstatic facadeScyllaBootstrapCentralized logging. Log.Info, Log.Warning, etc. with LogCategory filtering. Routes through ILogReceiver implementations (Unity console + file by default). See Logging.
ScyllaConfigurationabstract ScriptableObject familyScyllaBootstrap slots + project assetsProject-level defaults plus runtime JSON overrides. Configuration assets live under Assets/Resources/Scylla/; JSON overrides at ~/Documents/{ProductName}/Config/. See Configuration.
Utility layerstatic classes + typesnone (no lifecycle)The Core utility surface: StringUtil, NumberUtil, MathUtil, ScyllaSerialization, ScyllaCompression, ScyllaCrypto, ScyllaFileUtil, RandomUtil, NoiseGenerator, Ease, DebugDraw, etc. Pure C#; no Unity attachment beyond what each helper needs.

The four spine layers (Bootstrap, Core, ModuleManager, Modules) and the five ancillary subsystems (time, events, logging, scene management, configuration) are the framework’s complete runtime contract. Everything else in Scylla.Core is either a utility class with no lifecycle or a type that participates through one of these nine surfaces.

Recipes

Each recipe below is a self-contained orientation to one layer of the spine. They do not build on each other sequentially; scan the names and jump to whichever layer you’re currently working with. Start with “Bring up the framework from a scene” if you’re setting up for the first time, since every other recipe assumes the bootstrap is already in place.

Bring up the framework from a scene

Problem. You’re wiring Scylla into a project for the first time and you want to understand what you need to drop in the scene, what happens in each startup phase, and how to reach the bootstrap from code once it’s running.

Solution. Place a single ScyllaBootstrap component at the root of the first scene loaded, add your module components as direct children of that same GameObject, and the framework discovers and registers everything automatically. The five Inspector slots bind the configuration assets for the Core, Logger, Time, Event, and Scene subsystems; empty slots fall back to Resources/Config/ defaults. The framework rejects modules nested deeper than a direct child of the bootstrap GameObject. A second ScyllaBootstrap is also rejected: with scene persistence on (the default) a later-loaded scene carrying its own bootstrap is an expected duplicate and is removed quietly in favor of the persistent instance; without persistence a second bootstrap in the same session is destroyed with an error log.

/* The minimal scene setup. Add this GameObject hierarchy to the first scene
 * loaded; the framework discovers and registers every module child. */
/*
 * Scene Hierarchy
 * +- Scylla                       (GameObject)
 *    +- ScyllaBootstrap           (component)
 *    +- ScyllaInput                (child GameObject)
 *    |  +- ScyllaInput             (component, derived from ScyllaModule)
 *    +- ScyllaUI                   (child GameObject)
 *       +- ScyllaUI                (component, derived from ScyllaModule)
 */

using UnityEngine;
using Scylla.Core;

/* Read the bootstrap from anywhere via the static Instance property. */
ScyllaBootstrap bootstrap = ScyllaBootstrap.Instance;

/* Access the configuration slots. The Has* properties report which slots
 * are explicitly bound (vs falling back to Resources/Config/). */
if (bootstrap.HasCoreConfiguration)
{
    float delay = bootstrap.CoreConfiguration.InitializationDelay;
}

/* Manual init / shutdown for tests and hot-reload tooling. The framework
 * normally drives both via Unity's Awake/Start/OnApplicationQuit. */
bootstrap.InitializeFramework();
bootstrap.ShutdownFramework();

See Bootstrap Lifecycle for the per-phase trace, the configuration slot details, and the fail-fast paths.

Access the framework singleton

Problem. You know the framework is running and you want to look up a module, check whether initialization is finished, or queue a callback that fires once every module reaches Enabled. You need to know which type is the right entry point for those operations.

Solution. ScyllaCore is a pure C# singleton, not a Unity object. There’s no GameObject, no Transform, no Inspector. Creation happens lazily on first access (typically from ScyllaBootstrap.Awake). The singleton owns the IScyllaModuleManager and exposes the framework-wide convenience methods: GetModule<T>(moduleID) for cross-module lookup, RunWhenReady(callback) for waiting on framework readiness, RegisterModule(module) for dynamic registration (rare; modules normally self-register), InitializeFramework / ShutdownFramework for the lifecycle pipeline, and IsInitialized for the readiness flag.

using Scylla.Core;

/* The static entry point. Access from any thread; the singleton is thread-safe
 * to read (mutation goes through bootstrap-only paths). */
ScyllaCore framework = ScyllaCore.Instance;

/* The readiness flag. Becomes true after every module reaches Enabled. */
bool ready = framework.IsInitialized;

/* Wait for the framework to come up. Fires the callback immediately if
 * already ready; otherwise subscribes once to ScyllaFrameworkInitializedEvent. */
framework.RunWhenReady(() =>
{
    /* Cross-module lookup. Returns null when the module is not registered.
     * Use ScyllaModuleID.* constants for built-in modules. */
    var input = framework.GetModule<ScyllaInput>(ScyllaModuleID.INPUT);
});

/* The owned manager. Read-mostly; reach for Modules / GetModulesInState for
 * inspection. See module-system.md for the full interface. */
IScyllaModuleManager manager = framework.ModuleManager;

/* Compile-time framework metadata. */
string name    = ScyllaCore.Meta.NAME;       /* "Scylla Core" */
string version = ScyllaCore.Meta.VERSION;    /* "1.0.0" */
string id      = ScyllaCore.Meta.ID;         /* ScyllaModuleID.CORE */

Write a custom module and register it

Problem. You’re adding a new gameplay system (health tracking, economy, progression) and you want it to participate in the framework’s lifecycle: initialize in order, declare its dependencies, and be discoverable by other modules.

Solution. Subclass ScyllaModule, fill in the Info property with an identity and an initPriority, and override the lifecycle hooks your system needs. Drop the component as a direct child of the bootstrap GameObject in the scene; auto-registration happens in Awake. The module manager validates the dependency graph, computes topological + priority order, and runs Initialize, StartModule, and EnableModule in the correct sequence. You can inspect the live registry through ScyllaCore.Instance.ModuleManager.

/* A minimum custom module. ScyllaModule is the abstract base; the Info
 * property identifies the module to the framework. */
public sealed class HealthModule : ScyllaModule
{
    public const string ModuleID = "Health";

    public override ScyllaModuleInfo Info => new ScyllaModuleInfo(
        id: ModuleID, name: "Health", version: "1.0.0",
        description: "Tracks player health.",
        dependencies: null, initPriority: 100
    );

    /* Lifecycle hooks fire during the framework's startup pipeline. */
    protected override void OnInitialize()
    {
        Log.Info("Health module initialized", LogCategory.Module);
    }
}

/* Inspect the registry from outside the framework. */
var manager = ScyllaCore.Instance.ModuleManager;
IReadOnlyList<IScyllaModule> all = manager.Modules;
bool hasHealth = manager.HasModule(HealthModule.ModuleID);
List<IScyllaModule> enabled = manager.GetModulesInState(ScyllaModuleState.Enabled);

See Module System for the full module surface (lifecycle hooks, dependency declarations, cross-module access patterns) and Tiered Modules for the three-tier organization that constrains which modules can depend on which.

Use pause-aware time in gameplay code

Problem. Your gameplay logic needs to freeze when the player opens the pause menu, but your UI animations and debug overlays should keep running. You want to know which time source to reach for in each case, and when it’s safe to call into it.

Solution. ScyllaTime.Instance is the pause-aware time source for gameplay. The framework creates it during bootstrap Awake, so it’s callable before any module is initialized. Read DeltaTime (pause-aware: returns 0 when paused, scaled by the active time-scale) for logic that should freeze; read UnscaledDeltaTime for UI animations and tooling that should keep running. The subsystem supports Pause / Resume / TogglePause, time-scale presets (SetSlowMotion, SetFastForward, SetNormalSpeed), smooth transitions (TransitionTimeScale), and a layered modifier stack (PushModifier / PopModifier) for stacking time effects like bullet-time.

using Scylla.Core.Time;

/* Pause-aware delta: 0 when paused, scaled by the global time scale plus
 * any active modifiers. Use for gameplay code. */
float dt = ScyllaTime.Instance.DeltaTime;

/* Unscaled: ignores pause and scale. Use for UI animations. */
float realDt = ScyllaTime.Instance.UnscaledDeltaTime;

/* Pause and resume the simulation. */
ScyllaTime.Instance.Pause();
ScyllaTime.Instance.Resume();

/* Smooth slow-motion transition. */
ScyllaTime.Instance.TransitionTimeScale(targetScale: 0.3f, realSeconds: 0.5f);

See Time for the full surface (clocks, calendars, scheduled callbacks, the modifier stack).

Publish and subscribe to events across modules

Problem. You’ve written a health system and a UI overlay and they need to talk to each other, but you don’t want the UI to hold a direct reference to the health module. Same-tier modules cannot reference each other, and even cross-tier references carry coupling costs you’d rather avoid. You need a clean way to broadcast state changes without hardwiring the sender to the receiver.

Solution. The SEX (Scylla Event eXchange) bus is the project-wide publish/subscribe layer for exactly that decoupling. Use the static ScyllaEvents facade (not the underlying ScyllaEventBus directly): ScyllaEvents.Publish<T>(event) to broadcast, ScyllaEvents.Listen<T>(subscriber, handler, priority) to subscribe. The bus delivers synchronously on the calling thread, in priority order, with five levels (Essential, High, Medium (default), Low, Cleanup). Handlers can call event.StopPropagation() to skip the cancellable middle priorities; Essential and Cleanup always run. Subscriptions return IDisposable tokens; the bus also uses weak references for automatic cleanup of destroyed subscribers.

using Scylla.Core.Events;

/* Define an event. */
public sealed class PlayerDamagedEvent : ScyllaEvent
{
    public int Amount { get; init; }
}

/* Publish. */
ScyllaEvents.Publish(new PlayerDamagedEvent { Amount = 10 });

/* Subscribe. Store the token; dispose to unsubscribe. */
ScyllaEventSubscription sub = ScyllaEvents.Listen<PlayerDamagedEvent>(
    subscriber: this,
    handler:    evt => Log.Info($"Took {evt.Amount}", LogCategory.Game),
    priority:   ScyllaEventPriority.Medium
);

sub.Dispose();

See Event System for the full surface (priorities, cancellation, weak-reference cleanup, test-bus replacement, configuration tunables).

Write structured log output with category filtering

Problem. You need diagnostic output from your module during development, but you don’t want release builds flooded with trace noise, and you want the option to filter by system so you can see only the health-system logs without the camera logs drowning them out.

Solution. Log is the static facade for runtime log output. Eight severity methods (Trace, Debug, System, Info, Notice, Warning, Error, Fatal) plus Delimiter for visual separators. Each takes an optional LogCategory for runtime filtering. The bootstrap registers a Unity-console receiver (UnityConsoleLog) and a file receiver (FileLog) during Awake; you can register additional ILogReceiver implementations. Trace and Debug calls compile out in release builds via conditional attributes, so leaving them in your code costs nothing at runtime.

using Scylla.Core;

/* The most common calls. */
Log.Info("Game started",     LogCategory.Game);
Log.Warning("Low health: 5", LogCategory.Game);
Log.Error("Save failed",     LogCategory.File);

/* Compile out in release; useful for diagnostic output. */
Log.Trace("Per-frame trace", LogCategory.Gameplay);
Log.Debug("Detailed info",   LogCategory.Gameplay);

/* Runtime filter and toggle. */
Log.FilterLevel = LogLevel.Info;
Log.IsEnabled = false;

See Logging for the full receiver / formatting / category-filter surface.

Override module config values at runtime from a JSON file

Problem. Your designer wants to tune the max health value for a playtest without a rebuild. You want to support that without exposing raw SerializeField values and without writing a one-off config loading system. The value should resolve from the Inspector default if no file exists, and from the file if it does.

Solution. Scylla’s configuration is a dual-layer system. Project-level defaults live in ScriptableObject assets (ScyllaConfiguration subclasses) bound to the bootstrap’s Inspector slots or to a module’s [SerializeField]. Runtime overrides live in JSON files at ~/Documents/{ProductName}/Config/{Name}.json (User Documents tier) or {ApplicationPath}/Config/{Name}.json (Application folder tier), with the User Documents tier winning. The framework applies JSON overrides to the in-memory asset during bootstrap Awake, so by the time any module’s OnInitialize runs, the configuration values are already the final, override-applied values.

using Scylla.Core;
using Scylla.Core.Config;
using Scylla.Core.Util.File;
using UnityEngine;

[CreateAssetMenu(menuName = "Scylla/Config/Health")]
public sealed class HealthConfiguration : ScyllaConfiguration
{
    [SerializeField] private int _maxHealth = 100;
    public int MaxHealth => _maxHealth;

    /* Read JSON values into the asset's fields. */
    public override int ApplyConfigFileOverrides(ConfigFile configFile)
    {
        if (configFile.TryGetInt("Tuning", "MaxHealth", out var max))
        {
            _maxHealth = max;
            return 1;
        }
        return 0;
    }

    /* Emit the asset's current values as a JSON file. */
    public override ConfigFile ExportToConfigFile()
    {
        var file = new ConfigFile();
        file.SetInt("Tuning", "MaxHealth", _maxHealth);
        return file;
    }
}

See Configuration for the four-tier fallback (User Documents > Application folder > Unity asset > compiled defaults), the JSON shape, and the per-asset Export action.

Reach for a utility before writing your own

Problem. You’re about to write a helper function for string hashing, noise sampling, or random number generation. Before you do, you want to know whether Core already ships something that does the same thing.

Solution. Outside the framework’s lifecycle there’s a large surface of pure C# utility classes that have no Unity attachment beyond what each helper specifically needs. StringUtil, NumberUtil, MathUtil, Interpolation, RandomUtil, NoiseGenerator, ScyllaSerialization, ScyllaCompression, ScyllaCrypto, ScyllaFileUtil, DebugDraw, Ease, and several dozen more cover the routine work that every project ends up doing. The utilities are statically reachable; they don’t depend on bootstrap timing and are callable before, during, and after the framework’s lifecycle.

using Scylla.Core.Util;
using Scylla.Core.Util.Random;
using Scylla.Core.Util.Noise;

/* Deterministic random number generator. */
IRandomSource rng = RandomUtil.CreateXoshiro256(seed: 12345UL);
int loot = rng.NextInt(min: 1, max: 100);

/* Coherent noise for procedural generation. */
var noise = new NoiseGenerator(seed: 42);
float terrain = noise.Fractal2D(x: 10f, y: 20f, NoiseSettings.Terrain);

/* String hashing that is stable across runs and platforms. */
uint hash = StringUtil.GetStableHash32("level-3");

See the Core / Utilities section of the sidebar for the full utility catalogue: Random Utils, Math Utils, Noise Utils, Serialization Utils, Crypto Utils, Compression Utils, File IO Utils, and others.

API Sketch

The framework’s top-level entry points. Most user code touches only a handful of these surfaces; the full surface of each layer is documented in its dedicated page.

namespace Scylla.Core
{
    /* The Unity-side bootstrap. */
    public sealed class ScyllaBootstrap : MonoBehaviour
    {
        public static ScyllaBootstrap Instance { get; }
        public ScyllaCore Framework { get; }
        public ScyllaCoreConfiguration   CoreConfiguration   { get; }
        public ScyllaLoggerConfiguration LoggerConfiguration { get; }
        public ScyllaTimeConfiguration   TimeConfiguration   { get; }
        public ScyllaEventConfiguration  EventConfiguration  { get; }
        public void InitializeFramework();
        public void ShutdownFramework();
    }

    /* The framework singleton. */
    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);
        public struct Meta { /* ID, NAME, SHORT_NAME, VERSION, DESCRIPTION, DOCS_URL, API_DOCS_URL */ }
    }
}

namespace Scylla.Core.Modules
{
    /* Module base class. */
    public abstract class ScyllaModule : MonoBehaviour, IScyllaModule
    {
        public abstract ScyllaModuleInfo Info { get; }
        public ScyllaModuleState State    { get; }
        public bool              IsEnabled { get; }
        protected virtual void OnInitialize();
        protected virtual void OnStart();
        protected virtual void OnEnableModule();
        protected virtual void OnDisableModule();
        protected virtual void OnShutdown();
        protected virtual void OnInjectDependencies(IScyllaModuleManager moduleManager);
        protected T GetRequiredModule<T>(string moduleID) where T : class, IScyllaModule;
        protected T GetOptionalModule<T>(string moduleID) where T : class, IScyllaModule;
    }

    /* Module registry. */
    public interface IScyllaModuleManager
    {
        IReadOnlyList<IScyllaModule> Modules { get; }
        IScyllaModule       GetModule(string moduleID);
        T                   GetModule<T>(string moduleID) where T : class, IScyllaModule;
        bool                HasModule(string moduleID);
        List<IScyllaModule> GetModulesInState(ScyllaModuleState state);
        /* Framework-driven lifecycle methods: ValidateAllDependencies,
         * InitializeAllModules, StartAllModules, ShutdownAllModules. */
    }
}

namespace Scylla.Core.Events
{
    public static class ScyllaEvents
    {
        public static void Publish<TEvent>(TEvent @event) where TEvent : ScyllaEvent;
        public static ScyllaEventSubscription Listen<TEvent>(object subscriber, Action<TEvent> handler,
            ScyllaEventPriority priority = ScyllaEventPriority.Medium) where TEvent : ScyllaEvent;
    }
}

namespace Scylla.Core.Time
{
    public sealed class ScyllaTime
    {
        public static ScyllaTime Instance { get; }
        public float DeltaTime         { get; }
        public float UnscaledDeltaTime { get; }
        public bool  IsPaused          { get; }
        public void  Pause();
        public void  Resume();
        public void  TogglePause();
        /* + SetSlowMotion, SetFastForward, SetNormalSpeed, TransitionTimeScale,
         * PushModifier, PopModifier, etc. */
    }
}

namespace Scylla.Core.Debug
{
    public static class Log
    {
        public static void Info   (object data, LogCategory category = LogCategory.None);
        public static void Warning(object data, LogCategory category = LogCategory.None);
        public static void Error  (object data, LogCategory category = LogCategory.None);
        /* + Trace, Debug, System, Notice, Fatal, Delimiter; plus FilterLevel,
         * IsEnabled, and the receiver-registration methods. */
    }
}

See the API reference for full signatures, remarks, and exception contracts on every member.

Source paths

Every framework type listed above has a canonical source path under the Core runtime tree. Use these paths when grepping for an implementation, when reading a class to understand its behavior, or when filing an issue that names a specific file. The directory layout mirrors the namespace structure: Scylla.Core.Modules.* lives under Modules/, Scylla.Core.Events.* under Events/, etc.

ComponentSource path (under Scylla/Assets/Scylla/Framework/Core/Runtime/)
ScyllaBootstrapBootstrap/ScyllaBootstrap.cs
ScyllaCoreScyllaCore.cs
ScyllaModuleModules/ScyllaModule.cs
IScyllaModuleModules/IScyllaModule.cs
ScyllaModuleManager / IScyllaModuleManagerModules/ScyllaModuleManager.cs, Modules/IScyllaModuleManager.cs
ScyllaModuleInfoModules/ScyllaModuleInfo.cs
ScyllaModuleDependencyModules/ScyllaModuleDependency.cs
ScyllaModuleStateModules/ScyllaModuleState.cs
ScyllaModuleIDModules/ScyllaModuleID.cs
ScyllaTimeTime/ScyllaTime.cs
ScyllaEventsEvents/ScyllaEvents.cs
ScyllaEventBus / IScyllaEventBusEvents/ScyllaEventBus.cs, Events/IScyllaEventBus.cs
ScyllaEventEvents/ScyllaEvent.cs
ScyllaEventPriorityEvents/ScyllaEventPriority.cs
LogDebug/Logger/Log.cs
ScyllaConfigurationConfig/ScyllaConfiguration.cs
ConfigFileTypes/Files/ConfigFile.cs
ConfigFileManagerConfig/ConfigFileManager.cs
Utility layer (Random, Math, Noise, etc.)Util/{Random,Math,Noise,Crypto,Compression,Serialization,File,Tween,DebugDraw,ProceduralMapGen,...}/

Best Practices

  • Put exactly one ScyllaBootstrap in the first scene loaded. Modules go as direct children of its GameObject; the bootstrap refuses to initialize if any module is nested deeper. Putting the bootstrap in a boot or splash scene that always runs first is the simplest way to guarantee the framework is up before your gameplay code touches it.
  • Use ScyllaCore.Instance.RunWhenReady(...) for any code that touches a module from outside the framework. Unity’s component ordering is not deterministic; the framework’s initialization delay is intentional. RunWhenReady handles both the “already up” and “still starting” cases with the same call shape.
  • Treat ScyllaCore as the singleton it is. It is a pure C# class, not a Unity object. Do not try to attach it to a GameObject; do not call FindObjectOfType<ScyllaCore>(). Use ScyllaCore.Instance.
  • Use the static ScyllaModuleID.* constants for built-in modules. The constants survive renames and avoid typo-driven dependency failures that only show up at runtime. Custom modules should expose their own ModuleID constant for the same reason.
  • Declare cross-module dependencies in Info.Dependencies. The framework’s validation runs at startup and fails fast with an actionable error. Discovering missing dependencies in Update produces hard-to-diagnose runtime errors.
  • Wire configuration assets through the bootstrap Inspector slots in production. The Resources fallback exists for tests and quick prototyping; production builds should reference the configuration assets explicitly so the version-controlled assets are visibly the ones in effect.
  • Use ScyllaTime.Instance.DeltaTime for gameplay code. Pause-aware time is the right default; reserve UnityEngine.Time.deltaTime for UI animations and tooling that should ignore pause.
  • Use the static ScyllaEvents facade for events. It is the recommended entry point; touching IScyllaEventBus or ScyllaEventBus directly is for test bus replacement and custom dispatch implementations.
  • Use Log with specific LogCategory values from the first log call. Adding categories retroactively is a chore; picking the right category upfront makes runtime filtering instant.
  • Reach for the utility layer before writing custom helpers. The Core utility surface covers most routine work; skim the sidebar (Random, Math, Noise, Serialization, Crypto, Compression, File IO, Debug Draw, etc.) before implementing a helper that almost certainly already exists.
  • Read the dedicated subsystem page when working on that subsystem. This page is an orientation; the per-page detail is where the per-API signatures, lifecycle hooks, and behavioral guarantees live.

Pitfalls