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 registry; 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';
// setupLevel comes from ./level/setup.ts, which Step 2 covers.
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, so nothing about the engine needs to be reactive.
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 { SceneEngine } from '@skewedaspect/sage';
export interface LevelSetupResult
{
scene : Scene;
camera : ArcRotateCamera;
cube : Mesh;
}
export function setupLevel(sceneEngine : SceneEngine) : LevelSetupResult
{
const scene = 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(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 sceneEngine.createScene(). setupLevel takes the scene engine alone, not the whole GameEngine, since building a scene is all it does here.
Step 3: The frame callback
Back in the Vue component, onEngineReady ties everything together:
async function onEngineReady(engine : GameEngine) : Promise<void>
{
gameEngine = engine;
const level = setupLevel(engine.engines.sceneEngine);
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();
});
await engine.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 engine:
engine.start()runs the before-start and start lifecycle hooks around starting the game manager's render loop. Without it, 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 stepsengine.start()runs the lifecycle hooks and begins the loop;gameManager.pause()andresume()control it
Next steps
- Entity Behaviors: build game objects from behaviors and entity classes
- Input System: keyboard, mouse, and gamepad controls
