A guided walkthrough of the handful of building blocks every Scylla project ends up using: modules, events, time, logging, and configuration.
Summary
This page is a guided walkthrough of the five things used in almost every Scylla project: writing a custom module, publishing and subscribing to events, working with time and pause, routing log output by category, and exposing tunable values through a configuration asset. After the Install walkthrough and the First Bootstrap setup, this is the surface that gameplay code actually touches every day. Read it top to bottom; each section builds on the previous one and the running example (a small HealthModule) gradually accumulates the patterns covered.
The Scylla framework is opinionated about which abstraction does what. ScyllaModule is the single base class for everything that has a lifecycle (initialize, start, enable, disable, shutdown) and a discoverable identity (ScyllaModuleInfo). ScyllaEvents is the static facade for the project-wide event bus; modules publish from one place and subscribe from another without coupling to each other. ScyllaTime is the pause-aware time source that gameplay code reads instead of UnityEngine.Time, so slow-motion and pause flow through one central knob. Log is the static logger that every framework and gameplay log entry flows through, with categories that filter cleanly. ScyllaConfiguration is the base for tunable values, with a ScriptableObject for project defaults and a JSON file for end-user overrides.
The patterns covered are deliberately minimal. Production code adds error handling, defensive null checks, and proper cleanup in OnShutdown. The module-system, event-system, time, logging, and configuration pages cover the full surface of each subsystem; this tour is the first lap that connects them. By the end the reader has a running module, a typed event flowing between two places, a time-aware update tick, a categorized log line, and a config file the player could edit on disk.
Use this page for:
- Writing a first custom module and seeing it discovered by the framework.
- Publishing and subscribing to events across module boundaries without direct references.
- Working with
ScyllaTimeinstead ofUnityEngine.Timeso pause and slow-motion behave correctly. - Routing log output through
Logwith the rightLogCategoryfor runtime filtering. - Exposing tunable values through a
ScyllaConfigurationsubclass and letting the player override them via JSON. - Hooking gameplay code into framework readiness via
ScyllaCore.Instance.RunWhenReady.
Features
ScyllaModulebase class. Inherit, overrideInfo, override the lifecycle hooks the module needs. Auto-discovery, dependency resolution, and ordered initialization are handled by the framework.- Required
Infodeclaration.ScyllaModuleInfocarries ID, name, version, description, dependencies, andInitPriority. The framework reads it once at registration; the module stays focused on behaviour. - Six lifecycle hooks.
OnInitialize,OnStart,OnEnableModule,OnDisableModule,OnShutdown,OnInjectDependencies. Override only the ones the module needs; the rest are no-ops. - Static
ScyllaEventsfacade.Publish<T>(evt)andListen<T>(subscriber, handler, priority)route through the singleton event bus. Modules talk to each other through typed events without holding direct references. IDisposablesubscription tokens.Listenreturns anScyllaEventSubscription. Dispose inOnShutdownto remove the listener cleanly; the bus also reaps destroyed Unity objects automatically via weak references.- Five event priority levels.
Essential,High,Medium(default),Low,Cleanup. Priority controls firing order;EssentialandCleanupalways fire even when a handler cancels propagation. ScyllaTime.Instancefor pause-aware time.DeltaTimereturns 0 when paused and respects every active time-scale modifier.UnscaledDeltaTimealways runs for UI animations and frame-budget code.Pause,Resume,TransitionTimeScale, modifiers. The runtime time-scale knob covers slow-motion, fast-forward, hit-stop, and full gameplay pause through one consistent API.- Static
Logfacade with categories. Eight severity levels (Trace,Debug,System,Info,Notice,Warning,Error,Fatal) plus 28LogCategoryvalues for runtime filtering.TraceandDebugcompile out of release builds. ScyllaConfigurationfor tunables. Subclass to expose project defaults through the Inspector; overrideApplyConfigFileOverrides(ConfigFile)to read JSON overrides at runtime. The four-tier fallback (User Documents -> Application -> Resources -> compiled defaults) is automatic.ConfigFilefor JSON.TryGetInt,TryGetFloat,TryGetString,TryGetBool,TryGetColorplusSet*counterparts. Group/property structure mirrors JSON object/value shape; round-trips cleanly.ScyllaCore.Instance.RunWhenReady. Single hook for “do this once the framework is fully up”. Fires immediately when already ready; otherwise queues for the readiness event. Race-safe; single-shot.
Concepts
A few terms appear repeatedly. Module is a MonoBehaviour that inherits from ScyllaModule and lives as a direct child of the bootstrap GameObject; the framework discovers it automatically. Event is a class that inherits from ScyllaEvent; the static ScyllaEvents.Publish/Listen facade carries instances across module boundaries. Subscription is the IDisposable token returned by Listen; disposing it removes the listener (this is how the framework prevents leaked subscriptions). Configuration asset is a ScriptableObject of type ScyllaConfiguration that holds project-level default values, optionally overridden at runtime by a JSON file. Pause-aware time means the value reflects whether the framework is paused; reading ScyllaTime.Instance.DeltaTime returns 0 while paused, but UnscaledDeltaTime keeps running.
| Concept | Type | Notes |
|---|---|---|
ScyllaModule | abstract MonoBehaviour | Base class for every module. Required override: Info returning a ScyllaModuleInfo. Optional lifecycle hooks: OnInitialize, OnStart, OnEnableModule, OnDisableModule, OnShutdown. See Module System. |
ScyllaModuleInfo | sealed class | Read-only metadata returned by Info. Positional constructor: (id, name, version, description, dependencies, initPriority). Drives discovery order, dependency validation, and module browser UI. |
ScyllaEvent | abstract class | Base for every event type. Custom events inherit from it and add typed payload fields. Instances are passed to ScyllaEvents.Publish<T>. |
ScyllaEvents | static facade | Project-wide entry point. Publish<T>(evt) and Listen<T>(subscriber, handler, priority) route through the singleton IScyllaEventBus. The bus itself can be replaced via ScyllaEvents.SetBus for testing. |
ScyllaEventSubscription | sealed disposable | Returned by Listen. Holding onto the token and disposing it on OnShutdown removes the listener. Disposing twice is safe. |
ScyllaTime | sealed singleton (ScyllaTime.Instance) | Pause-aware time source. Read DeltaTime instead of UnityEngine.Time.deltaTime for gameplay code. Supports Pause, Resume, TogglePause, TransitionTimeScale, layered modifiers, normal / slow-motion / fast-forward presets. |
Log | static class | Routes log calls through any number of ILogReceiver implementations. Methods are Trace, Debug, System, Info, Notice, Warning, Error, Fatal, plus Delimiter. Each takes an optional LogCategory for filtering. |
LogCategory | enum | Pre-defined categories: Analytics, Camera, Character, Command, Console, Core, Data, Editor, Event, File, Game, Gameplay, Graphics, Input, Lifecycle, Module, Network, Renderer, None, and more. |
ScyllaConfiguration | abstract ScriptableObject | Base for project configuration assets. Override ApplyConfigFileOverrides(ConfigFile) to read JSON values into the asset’s fields; override ExportToConfigFile() to emit the asset as a JSON ConfigFile. Used by every module that exposes settings. |
ConfigFile | sealed class | In-memory representation of a JSON config file. Use TryGetInt, TryGetFloat, TryGetString, TryGetBool, TryGetColor, plus the Set* counterparts. See Config Files for the full surface. |
Walkthrough
The sections below stitch the five patterns into one running example: a HealthModule that exposes a MaxHealth setting, publishes a typed event when health changes, listens for the same event from another script, logs each change, and reads pause-aware time. Each section builds on the previous one.
1. A custom module
We want to track the player’s health in one place that every other system can find, and a module is the natural home for that kind of long-lived gameplay state. Modules are MonoBehaviour components that inherit from ScyllaModule. The minimum override is the Info property, which returns a ScyllaModuleInfo describing the module. Lifecycle hooks (OnInitialize, OnStart, OnEnableModule, OnDisableModule, OnShutdown) are optional and run in the corresponding framework phases.
using Scylla.Core;
public sealed class HealthModule : ScyllaModule
{
/* Public constant: callers reference this when querying for the module via
* ScyllaCore.Instance.GetModule<HealthModule>(HealthModule.ModuleID). */
public const string ModuleID = "Health";
/* Required override: the framework reads this once during discovery.
* Positional constructor: id, name, version, description, dependencies, priority. */
public override ScyllaModuleInfo Info => new ScyllaModuleInfo(
id: ModuleID,
name: "Health",
version: "1.0.0",
description: "Tracks player health and publishes change events.",
dependencies: null, /* no hard module dependencies */
initPriority: 100 /* default mid-range priority */
);
/* Internal state. */
private int _health;
private int _maxHealth = 100;
/* Fires once during the framework's Initialize pass. Configuration is
* already applied at this point. Other modules may not yet be Started. */
protected override void OnInitialize()
{
_health = _maxHealth;
Log.Info("Health module initialized", LogCategory.Game);
}
/* Fires when the module is enabled. May fire multiple times during a
* session if the module is disabled and re-enabled. Subscribe to events
* here so subscriptions match the enable/disable lifecycle. */
protected override void OnEnableModule()
{
/* Subscriptions go here; see section 3 below. */
}
/* Fires once at framework shutdown. Dispose subscriptions, release
* resources, save any pending state. */
protected override void OnShutdown()
{
Log.Info("Health module shutting down", LogCategory.Game);
}
}
Save the script, return to the Editor, let the compile finish, then add the HealthModule component to a new direct child of the bootstrap GameObject (Scylla). The framework discovers it the next time the scene plays.
2. Logging with categories
Anyone who has shipped a game with a sea of logs knows the cost of skipping categories: by month two, half the output is irrelevant and the half that matters is buried. The static Log class routes messages through any number of ILogReceiver implementations (Unity console, file, custom). Methods exist for each log level (Trace, Debug, System, Info, Notice, Warning, Error, Fatal); each takes an optional LogCategory for runtime filtering. Trace and Debug are compiled out in release builds via conditional attributes, so leaving them in user code costs nothing at runtime.
using Scylla.Core;
/* The simplest call: level + message. Routed through every registered
* ILogReceiver. The default receiver pair (Unity console + file) is set up
* during ScyllaBootstrap.Awake. */
Log.Info("Game started");
/* Add a category for filtering. The Unity console shows the category as a
* prefix; receivers can filter by category at runtime. */
Log.Info("Player spawned", LogCategory.Game);
Log.Info($"Loaded {assetCount} assets", LogCategory.Core);
/* Levels in increasing severity. Trace and Debug compile out in release. */
Log.Trace ("Per-frame trace info", LogCategory.Gameplay);
Log.Debug ("Detailed diagnostic", LogCategory.Gameplay);
Log.System("Framework state change", LogCategory.Lifecycle); /* internal framework category */
Log.Info ("Normal informational", LogCategory.Game);
Log.Notice("Worth attention", LogCategory.Network);
Log.Warning("Recoverable problem", LogCategory.File);
Log.Error ("Operation failed", LogCategory.Core);
Log.Fatal ("Unrecoverable error", LogCategory.Lifecycle);
/* Visual separator: useful between logical phases (level load, mode change). */
Log.Delimiter(LogCategory.Game);
/* Runtime filter level. Trace and Debug below the FilterLevel are dropped
* before reaching any receiver. */
Log.FilterLevel = LogLevel.Info;
/* Global enable / disable. */
Log.IsEnabled = false;
3. Publishing and subscribing to events
Sometimes a system needs to announce something to the rest of the game without knowing or caring who listens: damage was taken, a checkpoint was reached, a quest stage advanced. The event bus is the right shape for that. Define a class that inherits from ScyllaEvent, publish instances with ScyllaEvents.Publish<T>, and listen for them with ScyllaEvents.Listen<T>. The bus delivers the event to every subscriber synchronously in priority order; subscribers receive only events of the exact type they listened for.
using Scylla.Core;
using Scylla.Core.Events;
/* The event type. Inherit from ScyllaEvent and add typed payload fields.
* The init-only properties make the event effectively immutable after
* construction, which is the right shape for an event payload. */
public sealed class PlayerHealthChangedEvent : ScyllaEvent
{
public int OldHealth { get; init; }
public int NewHealth { get; init; }
}
The publisher constructs the event and hands it to the bus. Any number of subscribers can listen for the same type.
/* Inside the module that changes health. */
public void TakeDamage(int amount)
{
var previous = _health;
_health = Mathf.Max(0, _health - amount);
/* Publish: the bus delivers to every subscriber of PlayerHealthChangedEvent
* synchronously, in priority order. The static facade routes through the
* singleton IScyllaEventBus (replaceable for tests via ScyllaEvents.SetBus). */
ScyllaEvents.Publish(new PlayerHealthChangedEvent
{
OldHealth = previous,
NewHealth = _health
});
}
The subscriber takes an IDisposable token from Listen and disposes it during shutdown. Holding onto the token is required: the bus holds only weak references for cleanup, and disposing the token is the explicit “I am no longer listening” signal.
public sealed class HealthUIController : MonoBehaviour
{
private ScyllaEventSubscription _subscription;
private void Start()
{
/* Wait until the framework is up before subscribing. Listen here
* after RunWhenReady or in a module's OnEnableModule. */
ScyllaCore.Instance.RunWhenReady(() =>
{
/* Listen takes the subscriber object (for tracking), the typed
* handler delegate, and an optional priority. The token must be
* stored and disposed when the listener should stop receiving. */
_subscription = ScyllaEvents.Listen<PlayerHealthChangedEvent>(
subscriber: this,
handler: evt => UpdateHealthBar(evt.OldHealth, evt.NewHealth),
priority: ScyllaEventPriority.Medium /* default */
);
});
}
private void OnDestroy()
{
/* Dispose to unsubscribe. Disposing twice is safe (the token tracks
* its own disposal state). Skipping this leaks the subscription until
* the bus's periodic cleanup notices the destroyed subscriber. */
_subscription?.Dispose();
}
private void UpdateHealthBar(int oldHealth, int newHealth)
{
Log.Info($"Health changed: {oldHealth} -> {newHealth}", LogCategory.Game);
}
}
4. Cross-module access
Events cover the loose, broadcast-shaped conversations between systems, but plenty of cases still want a direct method call: the combat module calling into the health module to apply damage is the canonical example. Modules can hold references to other modules through GetRequiredModule<T>(moduleID) (throws if missing, the right shape for a hard dependency) and GetOptionalModule<T>(moduleID) (returns null, the right shape for a soft dependency). The helpers are protected on ScyllaModule so they are available only inside another module’s body. Outside a module, use ScyllaCore.Instance.GetModule<T>(moduleID) which behaves like the optional variant.
public sealed class CombatModule : ScyllaModule
{
public const string ModuleID = "Combat";
public override ScyllaModuleInfo Info => new ScyllaModuleInfo(
id: ModuleID,
name: "Combat",
version: "1.0.0",
description: "Damage resolution.",
dependencies: new List<ScyllaModuleDependency>
{
/* Hard dependency: framework validates at startup that Health is present. */
new ScyllaModuleDependency(HealthModule.ModuleID, minVersion: "1.0.0")
},
initPriority: 110 /* runs after Health (priority 100) */
);
/* Cache the dependency reference once. The protected helpers are safe to
* call inside OnInjectDependencies, OnInitialize, OnStart, and later. */
private HealthModule _health;
protected override void OnInjectDependencies(IScyllaModuleManager moduleManager)
{
/* GetRequiredModule throws if Health is missing - the failure is
* caught by the framework's startup pass and reported with the
* specific missing-dependency error. */
_health = GetRequiredModule<HealthModule>(HealthModule.ModuleID);
}
public void DealDamage(int amount)
{
_health.TakeDamage(amount);
}
}
Outside a module the entry point is the singleton.
ScyllaCore.Instance.RunWhenReady(() =>
{
/* GetModule returns null when the module is not registered. */
var health = ScyllaCore.Instance.GetModule<HealthModule>(HealthModule.ModuleID);
if (health != null)
{
/* Use the module. */
}
});
5. Working with time
Pause is one of those features that touches every gameplay system in the codebase, and the cheapest way to make it work cleanly is to read time from one central place from day one. ScyllaTime is the pause-aware time source for gameplay code. Reading DeltaTime instead of UnityEngine.Time.deltaTime routes the value through the framework’s pause and time-scale system automatically: pausing the framework freezes DeltaTime to 0 (so gameplay does not advance), changing the global time scale slows or speeds up DeltaTime proportionally, and per-system time-scale modifiers can apply on top. UnscaledDeltaTime is the unaffected counterpart for UI and tooling that should keep running during pause.
using Scylla.Core.Time;
using UnityEngine;
public sealed class HealthRegenerator : MonoBehaviour
{
private void Update()
{
/* Pause-aware delta. Returns 0 when the framework is paused, scaled by
* the active TimeScale and any active modifiers when running. */
float dt = ScyllaTime.Instance.DeltaTime;
/* Unscaled delta: ignores pause and time-scale. Use for UI animations
* that should keep running even when gameplay is paused. */
float realDt = ScyllaTime.Instance.UnscaledDeltaTime;
/* Regenerate at 5 HP per gameplay second. Pauses correctly when the
* framework is paused; speeds up under fast-forward; slows under
* slow-motion. */
_accum += 5f * dt;
while (_accum >= 1f)
{
_health.Heal(1);
_accum -= 1f;
}
}
private int _hp;
private float _accum;
private HealthModule _health;
}
The pause and time-scale API covers the common cases.
/* Pause: gameplay deltas freeze, unscaled keeps running. */
ScyllaTime.Instance.Pause();
ScyllaTime.Instance.Resume();
ScyllaTime.Instance.TogglePause();
bool paused = ScyllaTime.Instance.IsPaused;
/* Time-scale presets. */
ScyllaTime.Instance.SetNormalSpeed(); /* 1.0 */
ScyllaTime.Instance.SetSlowMotion(scale: 0.3f); /* 0.3, default 0.5 when called without args */
ScyllaTime.Instance.SetFastForward(scale: 2f); /* 2.0, default 2.0 when called without args */
/* Direct scale set. */
ScyllaTime.Instance.SetTimeScale(0.5f);
/* Smooth transition: ramp the scale over realSeconds with optional easing.
* Returns a handle that fires onComplete when the transition finishes. */
var transition = ScyllaTime.Instance.TransitionTimeScale(
targetScale: 0.3f,
realSeconds: 0.5f,
onComplete: () => Log.Info("Slow motion engaged", LogCategory.Game)
);
/* Layered modifiers: push and pop time-scale modifiers for stacking effects.
* Useful when multiple systems need to slow time independently. */
var slowAuraHandle = ScyllaTime.Instance.PushModifier(new TimeScaleModifier(
sourceID: "SlowAura",
scale: 0.5f
));
/* Effective scale: the product of every active modifier and the global scale. */
float effective = ScyllaTime.Instance.EffectiveTimeScale;
/* Remove the modifier when the effect ends. */
ScyllaTime.Instance.PopModifier(slowAuraHandle);
6. Configuration: tunable values
There is always a moment in a project’s life when a balance value needs to change without a rebuild: a QA tester wants to retry a level with MaxHealth = 200, a designer wants to test a faster regen rate, an end user wants to tweak a feel knob the developer never shipped a slider for. ScyllaConfiguration is the path for that. The configuration asset is a ScriptableObject (created via the Assets > Create menu) that holds project-level defaults; at runtime, a JSON file in the player’s Documents folder can override the values without touching the build. Define a configuration class, expose it as a [SerializeField] on the module, and override the two virtual methods that map between the asset’s fields and the JSON form.
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;
[SerializeField] private float _regenPerSecond = 5f;
[SerializeField] private bool _enableLowHealthScreen = true;
public int MaxHealth => _maxHealth;
public float RegenPerSecond => _regenPerSecond;
public bool EnableLowHealthScreen => _enableLowHealthScreen;
/* Override the base name used for the JSON file name on disk. The base
* implementation returns the asset name; this override locks the
* filename across renames of the asset. */
public override string GetConfigFileBaseName() => "Health";
/* Read the JSON file into the in-memory asset. Returns the number of
* values that were actually applied. Override one TryGet call per
* configurable field; defaults survive when a value is missing. */
public override int ApplyConfigFileOverrides(ConfigFile configFile)
{
var applied = 0;
if (configFile.TryGetInt("Tuning", "MaxHealth", out var max))
{
_maxHealth = max;
applied++;
}
if (configFile.TryGetFloat("Tuning", "RegenPerSecond", out var regen))
{
_regenPerSecond = regen;
applied++;
}
if (configFile.TryGetBool("UI", "LowHealthScreen", out var lowHP))
{
_enableLowHealthScreen = lowHP;
applied++;
}
return applied;
}
/* Emit the asset's current values as a JSON file. The default
* ExportToConfigFile in ScyllaConfiguration returns null; override to
* support the "Export Config File" Inspector button. */
public override ConfigFile ExportToConfigFile()
{
var file = new ConfigFile();
file.SetInt ("Tuning", "MaxHealth", _maxHealth);
file.SetFloat ("Tuning", "RegenPerSecond", _regenPerSecond);
file.SetBool ("UI", "LowHealthScreen", _enableLowHealthScreen);
return file;
}
}
Reference the configuration from the module via [SerializeField] (or by name through the configuration registry). The framework applies any matching JSON file at startup, so the values reaching the module’s OnInitialize are already the final, override-applied values.
public sealed class HealthModule : ScyllaModule
{
public const string ModuleID = "Health";
[SerializeField] private HealthConfiguration _config;
public override ScyllaModuleInfo Info => new ScyllaModuleInfo(
id: ModuleID, name: "Health", version: "1.0.0",
description: "Tracks player health.", dependencies: null, initPriority: 100
);
protected override void OnInitialize()
{
/* The configuration is already applied. _config.MaxHealth reflects
* any JSON override that the player or QA tester provided. */
_health = _config.MaxHealth;
Log.Info($"Health module ready: max={_config.MaxHealth}", LogCategory.Game);
}
private int _health;
}
The JSON file lives at ~/Documents/{ProductName}/Config/Health.json (Windows / macOS) and uses the group / property structure defined by the Set* calls in ExportToConfigFile.
{
"Tuning": {
"MaxHealth": 200,
"RegenPerSecond": 10.0
},
"UI": {
"LowHealthScreen": false
}
}
7. Hooking into framework readiness
A lot of subtle startup bugs come from gameplay scripts assuming the framework is up the moment their own Awake or Start runs. The initialization delay (default 1.0 second) intentionally happens after Start, so that assumption breaks. The safe pattern is ScyllaCore.Instance.RunWhenReady(callback): the framework invokes the callback immediately if it is already ready, or queues it for the readiness event if not. The optional priority parameter controls firing order among multiple ready-handlers.
using UnityEngine;
using Scylla.Core;
using Scylla.Core.Events;
public sealed class GameStartup : MonoBehaviour
{
private void Start()
{
/* Medium priority is the default. */
ScyllaCore.Instance.RunWhenReady(OnFrameworkReady);
/* High priority: fires before Medium and Low handlers. */
ScyllaCore.Instance.RunWhenReady(InitializeCriticalSystems, ScyllaEventPriority.High);
/* Low priority: fires after every other handler. Useful for UI that
* should appear once all systems have warmed up. */
ScyllaCore.Instance.RunWhenReady(FinalizeUI, ScyllaEventPriority.Low);
}
private void OnFrameworkReady()
{
/* The framework is fully up. Every module is in the Enabled state.
* Cross-module queries via ScyllaCore.Instance.GetModule<T> are safe. */
Log.Info("Scylla is up", LogCategory.Game);
}
private void InitializeCriticalSystems() { /* ... */ }
private void FinalizeUI() { /* ... */ }
}
API Sketch
The surface touched by the patterns in this tour spans several namespaces. The grouping below mirrors how user code typically reaches into the framework: module base, event bus facade, time singleton, log facade, configuration base.
namespace Scylla.Core
{
public sealed class ScyllaCore
{
public static ScyllaCore Instance { get; }
public bool IsInitialized { get; }
public T GetModule<T>(string moduleID) where T : class, IScyllaModule;
public void RunWhenReady(Action onReady, ScyllaEventPriority priority = ScyllaEventPriority.Medium);
}
public abstract class ScyllaModule : MonoBehaviour, IScyllaModule
{
public abstract ScyllaModuleInfo Info { 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;
}
public sealed class ScyllaModuleInfo
{
public ScyllaModuleInfo(string id, string name, string version,
string description = "", List<ScyllaModuleDependency> dependencies = null,
int initPriority = 100);
}
}
namespace Scylla.Core.Events
{
public abstract class ScyllaEvent { /* base for typed events */ }
public static class ScyllaEvents
{
public static IScyllaEventBus Bus { get; }
public static void SetBus(IScyllaEventBus bus);
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;
}
public sealed class ScyllaEventSubscription : IDisposable
{
public void Dispose();
}
public enum ScyllaEventPriority { /* High, Medium, Low, plus integer variants */ }
}
namespace Scylla.Core.Time
{
public sealed class ScyllaTime
{
public static ScyllaTime Instance { get; }
/* Pause-aware time. */
public float DeltaTime { get; } /* scaled, 0 when paused */
public float UnscaledDeltaTime { get; } /* unaffected by pause / scale */
public float FixedDeltaTime { get; }
public float TimeSinceStartup { get; }
public float UnscaledTimeSinceStartup { get; }
public float RealTimeSinceStartup { get; }
public int FrameCount { get; }
public bool IsPaused { get; }
public float TimeScale { get; set; }
public float EffectiveTimeScale { get; }
/* Pause / resume. */
public void Pause();
public void Pause(PauseMode mode);
public void Resume();
public void TogglePause();
/* Time-scale presets and transitions. */
public void SetNormalSpeed();
public void SetSlowMotion (float scale = -1f);
public void SetFastForward(float scale = -1f);
public void SetTimeScale (float scale);
public SmoothTimeScaleTransition TransitionTimeScale(float targetScale, float realSeconds,
Action onComplete = null, Func<float, float> easing = null);
/* Layered modifiers. */
public TimeScaleModifierHandle PushModifier(TimeScaleModifier modifier);
public bool PopModifier(TimeScaleModifierHandle handle);
public int RemoveModifiersBySourceID(string sourceID);
public void ClearModifiers();
public IReadOnlyList<TimeScaleModifier> GetActiveModifiers();
}
}
namespace Scylla.Core.Debug
{
public static class Log
{
public static bool IsEnabled { get; set; }
public static LogLevel FilterLevel { get; set; }
public static void Trace (object data, LogCategory category = LogCategory.None);
public static void Debug (object data, LogCategory category = LogCategory.None);
public static void System (object data, LogCategory category = LogCategory.None);
public static void Info (object data, LogCategory category = LogCategory.None);
public static void Notice (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);
public static void Fatal (object data, LogCategory category = LogCategory.None);
public static void Delimiter(LogCategory category = LogCategory.None);
public static void RegisterLogReceiver (ILogReceiver logReceiver);
public static void UnregisterLogReceiver(ILogReceiver logReceiver);
}
public enum LogLevel { Trace, Debug, System, Info, Notice, Warning, Error, Fatal }
public enum LogCategory { Analytics, Camera, Character, Command, Console, Core,
Data, Editor, Event, File, Game, Gameplay, Graphics,
Input, Lifecycle, Module, Network, None, Renderer, /* ... */ }
}
namespace Scylla.Core.Config
{
public abstract class ScyllaConfiguration : ScriptableObject
{
public virtual string GetConfigFileName();
public virtual string GetConfigFileBaseName();
public virtual bool IsConfigFileEnabledForCurrentBuild();
public virtual int ApplyConfigFileOverrides(ConfigFile configFile); /* override */
public virtual ConfigFile ExportToConfigFile(); /* override */
}
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Where to next
- Architecture. Overview, Module System, Bootstrap Lifecycle, Tiered Modules, Event System.
- Core systems. Configuration, Logging, Time.
- Per-module documentation. Each Scylla module has its own page set; browse the sidebar for Input, Graphics, UI, TextMode, Data, Console, Stats, Camera.
- Utility reference. Random Utils, Math Utils, Tween Utils, Serialization Utils, File IO Utils, Crypto Utils, Compression Utils, and others under Core / Utilities.
Best Practices
- Lock in a module’s
Infoearly. TheID,Name, andVersionare part of the module’s contract. Renaming them later forces a migration of any save data or configuration that referenced them. Pick deliberately at module-creation time and treat the chosen values as a public API. - Cache module references in
OnInjectDependencies, not inUpdate. RepeatedGetRequiredModule<T>lookups in a hot path add up; the framework lookup is cheap but not free, and the validation work it does is wasted when the reference is known to be valid. - Always store the
Listenreturn value. Discarding the subscription token makes the listener effectively permanent. Assign to a private field whose lifetime matches the listener’s lifetime; dispose inOnShutdown(for modules) orOnDestroy(for MonoBehaviours that subscribed inStart). - Prefer
ScyllaTime.Instance.DeltaTimeoverUnityEngine.Time.deltaTimefor gameplay code. Pause-aware time is the right default; reserveUnityEngine.Timefor UI animations and tooling that should ignore pause. - Use specific
LogCategoryvalues from the first log call. Adding categories retroactively is a chore; picking the right category up front makes runtime filtering instant. - Trace and Debug calls are free in release builds. Both compile out via conditional attributes, so leaving them in user code does not cost anything in shipped builds. Use them liberally for diagnostic output.
- Define event payload fields as
init-only properties. The init modifier produces effectively-immutable event payloads, which is the right shape because event handlers run synchronously and should not mutate the event mid-dispatch. - Pick event priorities deliberately.
Highfor handlers that other handlers depend on (state mutators);Mediumfor general listeners;Lowfor handlers that should run last (analytics, UI). Mixing priorities at random produces order-dependent bugs. - Use
OnInjectDependenciesfor cross-module references andOnInitializefor self-contained setup. Splitting the work this way keeps the lifecycle phases distinct and makes it obvious which references are guaranteed to be valid at each step. - Wire
Pauseto game pause events andResumeto unpause events. Without this, gameplay time keeps running while the rest of the simulation is “paused”, producing “the UI is paused but the timer is not” bugs. - Export the configuration file once, then version it. Click
Export Config Fileon the configuration asset’s Inspector, commit the resulting JSON to version control, and treat that file as the seed for any future tuning. The file is also what ships in the user-overrides location for player-editable tuning. - Use
RunWhenReadyfor one-shot startup,IsInitializedpolling for per-frame gates. Both patterns are appropriate in different contexts; mixing them (waiting for ready via a poll) wastes frames.
Pitfalls
Related
- Install
- First Bootstrap
- Architecture Overview
- Module System
- Event System (SEX)
- Configuration
- Logging
- Time
- API reference:
Scylla.Core.ScyllaModule - API reference:
Scylla.Core.ScyllaModuleInfo - API reference:
Scylla.Core.ScyllaCore - API reference:
Scylla.Core.Events.ScyllaEvent - API reference:
Scylla.Core.Events.ScyllaEvents - API reference:
Scylla.Core.Events.ScyllaEventBus - API reference:
Scylla.Core.Events.ScyllaEventSubscription - API reference:
Scylla.Core.Events.ScyllaEventPriority - API reference:
Scylla.Core.Time.ScyllaTime - API reference:
Scylla.Core.Debug.Log - API reference:
Scylla.Core.Debug.LogCategory - API reference:
Scylla.Core.Debug.LogLevel - API reference:
Scylla.Core.Config.ScyllaConfiguration - API reference:
Scylla.Core.Util.File.ConfigFile