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 cubeTwo 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.
<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:
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.
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:
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:
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:
- Set up the level — create the scene, camera, light, and cube.
- Register a frame callback — this function runs every frame. The
dtparameter is the time in seconds since the last frame, which you should use to keep animation speed frame-rate independent. - 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:
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
SageCanvasbootstraps the engine and provides theengine-readyevent- Scenes are created through
engine.engines.sceneEngine.createScene() - Frame callbacks registered with
gameManager.registerFrameCallback()run every frame gameManager.start()kicks off the loop;pause()andresume()control it- Keep BabylonJS code in separate files to maintain clean separation
Next steps
- Input System — add keyboard, mouse, and gamepad controls
- Entity Behaviors — build game objects from composable behaviors
