Hello Cube
This guide walks through the simplest SAGE application: a rotating cube rendered with BabylonJS, driven by SAGE's frame loop. It covers engine boot through SageCanvas, scene creation, frame callbacks, and pause and resume. It spawns no entities; the Entity Behaviors guide is where the entity system starts.
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 cubeStep 1: The Vue component and SageCanvas
SageCanvas, from @skewedaspect/sage-vue, creates the canvas, calls createGameEngine, and emits engine-ready once the engine is up.
<template>
<SageCanvas @engine-ready="onEngineReady" :options="{ logLevel: 'info' }">
<template #default="{ loading }">
<div v-if="loading" class="loading-overlay">Loading...</div>
</template>
</SageCanvas>
</template>The options prop is the SageOptions passed to createGameEngine. The default slot renders on top of the canvas and receives a loading flag. An :entity-classes prop carries the entity class table; this example has none.
The script section sets up the imports and state:
import { ref } from 'vue';
import type { GameEngine } from '@skewedaspect/sage';
import type { Mesh, Scene } 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;gameEngine is a plain variable, not a Vue ref. Engine references are used in callbacks, never in templates, and keeping them out of Vue's reactivity avoids proxy overhead on every access.
Step 2: Setting up the level
Scene setup is isolated in its own file, which keeps the BabylonJS-specific code apart from the SAGE wiring.
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;
}
export function setupLevel(engine : GameEngine, canvas : HTMLCanvasElement) : LevelSetupResult
{
const scene = engine.engines.sceneEngine.createScene();
scene.clearColor = new Color4(0.12, 0.12, 0.14, 1);
const camera = new ArcRotateCamera('camera', Math.PI / 4, Math.PI / 3, 5, Vector3.Zero(), scene);
camera.attachControl(canvas, true);
new HemisphericLight('light', new Vector3(0, 1, 0), scene).intensity = 0.8;
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 scene comes from engine.engines.sceneEngine.createScene().
Step 3: The frame callback
Back in the Vue component, onEngineReady 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) =>
{
cube.rotation.x += ROTATION_SPEED_X * dt;
cube.rotation.y += ROTATION_SPEED_Y * dt;
scene.render();
});
engine.managers.gameManager.start();
}- Set up the level: create the scene, camera, light, and cube.
- Register a frame callback: it runs once per rendered frame.
dtis the seconds since the last frame; multiplying by it keeps the rotation speed independent of frame rate. - Start the game manager: this begins the render loop. Without
start(), nothing renders.
The callback calls scene.render() itself because this example creates its scene directly rather than through a level. With the level system, GameManager renders the current level's scene after the frame callbacks run.
Frame callbacks run after the simulation's fixed steps for the frame. This example has no simulation: no level has called rebuildSimulation, so engine.simulation is null and the fixed-step loop is skipped.
Step 4: Pause and resume
function togglePause() : void
{
if(!gameEngine) { return; }
if(paused.value)
{
gameEngine.managers.gameManager.resume();
}
else
{
gameEngine.managers.gameManager.pause();
}
paused.value = !paused.value;
}While paused, the game manager skips the fixed steps and the frame callbacks. The render loop keeps running and renders the current level's scene, so the UI stays responsive. Here, with no level, pausing stops the callback that called scene.render(), so the last frame stays on screen.
Resource cleanup
When the component unmounts, SageCanvas stops the engine and disposes the BabylonJS engine, which takes the scene and all meshes with it.
What you learned
SageCanvasboots the engine and emitsengine-ready- Scenes are created through
engine.engines.sceneEngine.createScene() gameManager.registerFrameCallback()runs once per rendered frame, after the fixed stepsgameManager.start()begins the loop;pause()andresume()control it
Next steps
- Entity Behaviors: build game objects from behaviors and entity classes
- Input System: keyboard, mouse, and gamepad controls
