Architecture
SAGE follows a layered, event-driven architecture inspired by iDesign principles. Each layer has a clear responsibility, and communication flows through well-defined channels -- primarily the event bus. The result is a codebase where components are loosely coupled, independently testable, and composable.
Layers at a Glance
+-----------------------------------------------------------+
| Game Engine (sage.ts) |
| Orchestrator / public API |
+-------------+-----------------+---------------------------+
| Event Bus | Entity System | Scene Engine |
| (pub/sub) | (composition) | (BabylonJS + Havok) |
+-------------+-----------------+---------------------------+
| Entity | Game Manager | Binding / Input |
| Manager | (game loop) | Manager |
+-------------+-----------------+---------------------------+
| Utilities & ID Generation |
+-----------------------------------------------------------+| Layer | Responsibility |
|---|---|
| Engine | Top-level orchestrator. createGameEngine() wires everything together and exposes the public API. |
| Managers | Coordinate subsystems: game loop, entities, levels, input bindings. Each manager owns one domain. |
| Classes / Behaviors | The building blocks. GameEntity, GameEntityBehavior, GameEventBus, etc. Pure logic lives here. |
| Utils | Cross-cutting helpers: ID generation, deep merge, type guards. No I/O, no state. |
Each layer only talks to the layer directly below it. Managers never import other managers' internals; they communicate through the event bus or through the engine facade.
Event-Driven Communication
Direct coupling between subsystems is the enemy of maintainability. SAGE avoids it by routing nearly all inter-component communication through a central GameEventBus.
// A behavior emits an event...
this.$emit({ type: 'weapon:fired', payload: { damage: 50 } });
// ...and any subscriber anywhere in the engine picks it up
eventBus.subscribe('weapon:fired', (event) =>
{
audioSystem.playSound('gunshot.wav');
});The event bus supports exact subscriptions, wildcard patterns (input:*), and regex matching. Events are delivered asynchronously via microtasks, so publishers never block on subscriber execution. See the Events page for the full API.
Data Flow: Input to Rendering
Here is what happens when a player presses a key, end to end:
- The Input Manager detects the raw keypress and publishes an
input:changedevent. - The Binding Manager maps the input to a named action and publishes an
action:<name>event. - Behaviors subscribed to that action event receive it and update entity state (e.g. set
jumping = true). - Those behaviors may emit additional events (e.g.
player:jumped), which other behaviors can react to. - The Game Manager's frame loop calls
$update(deltaTime)on every root entity, cascading to children. - Each behavior's
update()applies continuous logic (movement, physics forces, animations). - The Scene Engine and Havok Physics apply the resulting transforms to the BabylonJS scene graph.
- BabylonJS renders the frame.
Keypress -> Input Manager -> Binding Manager -> action event
-> Behavior.processEvent() -> state change -> Behavior.update()
-> Scene Engine renderThis pipeline keeps each step isolated. The input system knows nothing about rendering; behaviors know nothing about raw key codes.
Core Managers
GameManager
Controls the overall game lifecycle and the frame loop.
- Starts and stops the game
- Drives the update-render loop
- Provides
registerFrameCallback()for custom per-frame logic outside the entity system
const unsubscribe = sage.managers.gameManager.registerFrameCallback((deltaTime) =>
{
// Custom per-frame logic -- useful for debug overlays, profiling, etc.
fpsCounter.update(deltaTime);
});
// Stop receiving callbacks
unsubscribe();EntityManager
Central hub for entity creation, tracking, and querying.
- Creates entities from registered definitions (blueprints)
- Maintains indexed collections for O(1) lookup by ID, name, type, tag, or scene node
- Manages entity tags at runtime
- Handles node attachment / detachment for scene graph integration
- Supports object pooling for high-frequency create/destroy patterns
LevelManager
Organizes the game into discrete sections.
- Registers and tracks
GameLevelinstances - Handles level loading, activation, and transitions
- Manages cleanup between levels
- Registers property handlers that process scene node metadata from loaded GLB files
- Supports custom
GameLevelsubclasses for level-specific logic
BindingManager
Connects raw input to game actions.
- Registers actions (digital and analog)
- Creates bindings between physical inputs and named actions
- Manages input contexts so different game states (menu, gameplay, cutscene) use different bindings
SceneEngine
Thin wrapper around BabylonJS scene and rendering.
- Creates scenes with optional Havok physics
- Provides helpers for cameras, lights, and primitive meshes
- Loads external 3D models (GLB/GLTF)
Resource Lifecycle and Cleanup
Every manager and engine in SAGE implements the Disposable interface:
interface Disposable
{
$teardown() : Promise<void>;
}When you call gameEngine.$teardown(), cleanup cascades through every subsystem in the correct order: entities are destroyed (pools drained), event subscriptions are cleared, the physics engine is disposed, and the BabylonJS engine is shut down. No manual cleanup is needed for anything the engine created.
TIP
Level transitions also trigger partial teardown -- the LevelManager cleans up the current level's entities, scene, and physics before loading the next one. Entity pools are drained automatically during this process.
When to Use Which Layer
| You want to... | Use this |
|---|---|
| Create a game object with custom logic | Define an entity with behaviors |
| React to something happening in the game | Subscribe to an event on the event bus |
| Run logic every frame | Behavior.update() or registerFrameCallback() |
| Organize the game into sections | Create GameLevel subclasses |
| Map player input to game actions | Register actions and bindings with the BindingManager |
| Access BabylonJS directly | sage.sceneEngine.scene, sage.sceneEngine.engine |
| Add cross-cutting debug tools | registerFrameCallback() + event subscriptions |
Comparison with Other Engines
SAGE takes a code-first approach rather than an editor-first one:
| Aspect | Unity / Unreal | SAGE |
|---|---|---|
| Object model | Inheritance-heavy GameObjects / Actors | Composition via entity + behavior |
| Communication | Direct references, singletons | Event bus (pub/sub) |
| Workflow | Visual editor with scripts attached | TypeScript-first, data-driven levels |
| Physics | Built-in engine | Havok (same physics, explicit integration) |
The tradeoff is intentional: SAGE optimizes for programmers who want full control over their game's architecture without fighting an editor.
