Skip to content

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                    |
+-----------------------------------------------------------+
LayerResponsibility
EngineTop-level orchestrator. createGameEngine() wires everything together and exposes the public API.
ManagersCoordinate subsystems: game loop, entities, levels, input bindings. Each manager owns one domain.
Classes / BehaviorsThe building blocks. GameEntity, GameEntityBehavior, GameEventBus, etc. Pure logic lives here.
UtilsCross-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.

typescript
// 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:

  1. The Input Manager detects the raw keypress and publishes an input:changed event.
  2. The Binding Manager maps the input to a named action and publishes an action:<name> event.
  3. Behaviors subscribed to that action event receive it and update entity state (e.g. set jumping = true).
  4. Those behaviors may emit additional events (e.g. player:jumped), which other behaviors can react to.
  5. The Game Manager's frame loop calls $update(deltaTime) on every root entity, cascading to children.
  6. Each behavior's update() applies continuous logic (movement, physics forces, animations).
  7. The Scene Engine and Havok Physics apply the resulting transforms to the BabylonJS scene graph.
  8. BabylonJS renders the frame.
Keypress -> Input Manager -> Binding Manager -> action event
    -> Behavior.processEvent() -> state change -> Behavior.update()
    -> Scene Engine render

This 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
typescript
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 GameLevel instances
  • Handles level loading, activation, and transitions
  • Manages cleanup between levels
  • Registers property handlers that process scene node metadata from loaded GLB files
  • Supports custom GameLevel subclasses 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:

typescript
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 logicDefine an entity with behaviors
React to something happening in the gameSubscribe to an event on the event bus
Run logic every frameBehavior.update() or registerFrameCallback()
Organize the game into sectionsCreate GameLevel subclasses
Map player input to game actionsRegister actions and bindings with the BindingManager
Access BabylonJS directlysage.sceneEngine.scene, sage.sceneEngine.engine
Add cross-cutting debug toolsregisterFrameCallback() + event subscriptions

Comparison with Other Engines

SAGE takes a code-first approach rather than an editor-first one:

AspectUnity / UnrealSAGE
Object modelInheritance-heavy GameObjects / ActorsComposition via entity + behavior
CommunicationDirect references, singletonsEvent bus (pub/sub)
WorkflowVisual editor with scripts attachedTypeScript-first, data-driven levels
PhysicsBuilt-in engineHavok (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.

Released under the MIT License.