Skip to content

Hello Cube

This guide walks you through the simplest SAGE application: a rotating cube rendered with BabylonJS, controlled by SAGE's game loop. By the end, you will understand the core initialization flow, how SAGE manages frame callbacks, and how to integrate with Vue via SageCanvas.

The full source is in examples/src/examples/hello-cube/.

Try it live

Run this example at Examples > Hello Cube.

Project structure

hello-cube/
├── index.vue          # Vue component — UI and engine wiring
└── level/
    └── setup.ts       # BabylonJS scene, camera, light, and cube

Two files. That is all you need.

Step 1: The Vue component and SageCanvas

Every SAGE application starts with a canvas. The @skewedaspect/sage-vue package provides SageCanvas, a Vue component that creates the underlying BabylonJS engine, initializes SAGE, and emits an engine-ready event once everything is ready to go.

vue
<template>
    <SageCanvas @engine-ready="onEngineReady" :options="{ logLevel: 'info' }">
        <template #default="{ loading }">
            <!-- Overlay UI goes here -->
            <div v-if="loading" class="loading-overlay">Loading...</div>
        </template>
    </SageCanvas>
</template>

The options prop accepts engine configuration — here we set the log level. The default slot gives you a loading flag you can use for a loading screen. Everything inside the slot renders on top of the canvas.

In the script section, set up the imports and state:

typescript
import { ref } from 'vue';
import type { GameEngine } from '@skewedaspect/sage';
import type { Scene, Mesh } from '@babylonjs/core';
import { SageCanvas } from '@skewedaspect/sage-vue';

import { setupLevel } from './level/setup.ts';

const ROTATION_SPEED_X = 0.3;
const ROTATION_SPEED_Y = 0.6;

const paused = ref(false);
let gameEngine : GameEngine | null = null;

Note that gameEngine is a plain variable, not a Vue ref. SAGE engine references are used only in callbacks, never in templates. Keeping them out of Vue's reactivity system avoids unnecessary proxy overhead.

Step 2: Setting up the level

The level setup is isolated in its own file. This keeps all BabylonJS-specific code separate from SAGE patterns, which is a habit worth building early.

typescript
import {
    ArcRotateCamera, Color3, Color4, HemisphericLight,
    type Mesh, MeshBuilder, type Scene, StandardMaterial, Vector3,
} from '@babylonjs/core';
import type { GameEngine } from '@skewedaspect/sage';

export interface LevelSetupResult
{
    scene : Scene;
    camera : ArcRotateCamera;
    cube : Mesh;
}

The function uses SAGE's scene engine to create a BabylonJS scene, then builds the camera, light, and cube using standard BabylonJS APIs:

typescript
export function setupLevel(engine : GameEngine, canvas : HTMLCanvasElement) : LevelSetupResult
{
    // Create scene through SAGE's scene engine
    const scene = engine.engines.sceneEngine.createScene();
    scene.clearColor = new Color4(0.12, 0.12, 0.14, 1);

    // Camera — orbiting view centered on origin
    const camera = new ArcRotateCamera(
        'camera',
        Math.PI / 4,    // alpha (horizontal angle)
        Math.PI / 3,    // beta (vertical angle)
        5,              // radius
        Vector3.Zero(),
        scene
    );
    camera.attachControl(canvas, true);

    // Ambient light
    new HemisphericLight('light', new Vector3(0, 1, 0), scene).intensity = 0.8;

    // The cube
    const cube = MeshBuilder.CreateBox('cube', { size: 1.5 }, scene);

    const material = new StandardMaterial('cubeMaterial', scene);
    material.diffuseColor = new Color3(0.506, 0.173, 0.173);
    material.specularColor = new Color3(0.3, 0.3, 0.3);
    cube.material = material;

    return { scene, camera, cube };
}

The key detail is engine.engines.sceneEngine.createScene(). SAGE wraps BabylonJS scene creation so it can track scenes internally. Always create scenes through this API rather than calling the BabylonJS Scene constructor directly.

Step 3: The frame callback

Back in the Vue component, the onEngineReady handler ties everything together:

typescript
function onEngineReady(engine : GameEngine) : void
{
    gameEngine = engine;

    const level = setupLevel(engine, engine.canvas as HTMLCanvasElement);
    const scene = level.scene;
    const cube = level.cube;

    engine.managers.gameManager.registerFrameCallback((dt : number) =>
    {
        if(cube)
        {
            cube.rotation.x += ROTATION_SPEED_X * dt;
            cube.rotation.y += ROTATION_SPEED_Y * dt;
        }

        scene?.render();
    });

    engine.managers.gameManager.start();
}

The flow is:

  1. Set up the level — create the scene, camera, light, and cube.
  2. Register a frame callback — this function runs every frame. The dt parameter is the time in seconds since the last frame, which you should use to keep animation speed frame-rate independent.
  3. Start the game manager — this begins the game loop. Without calling start(), nothing renders.

Inside the frame callback, we rotate the cube and call scene.render() to draw the frame. In later examples you will see SAGE handle rendering automatically via its level system, but at this level of simplicity, calling render() manually keeps things transparent.

Step 4: Pause and resume

SAGE's game manager has built-in pause/resume support:

typescript
function togglePause() : void
{
    if(!gameEngine) { return; }

    if(paused.value)
    {
        gameEngine.managers.gameManager.resume();
    }
    else
    {
        gameEngine.managers.gameManager.pause();
    }

    paused.value = !paused.value;
}

When paused, the game manager stops calling your frame callback. The BabylonJS render loop itself continues running (so the UI stays responsive), but your game logic and scene.render() calls stop executing.

Resource cleanup

In this example, cleanup is handled automatically by Vue and SageCanvas. When the component unmounts, SageCanvas disposes the underlying BabylonJS engine, which takes the scene and all meshes with it.

For more complex applications where you create additional resources (event subscriptions, entity managers, physics bodies), you will want explicit cleanup in onBeforeUnmount. The later examples demonstrate this pattern.

What you learned

  • SageCanvas bootstraps the engine and provides the engine-ready event
  • Scenes are created through engine.engines.sceneEngine.createScene()
  • Frame callbacks registered with gameManager.registerFrameCallback() run every frame
  • gameManager.start() kicks off the loop; pause() and resume() control it
  • Keep BabylonJS code in separate files to maintain clean separation

Next steps

Released under the MIT License.