Vue 3 Integration
The @skewedaspect/sage-vue package provides a canvas wrapper component and a set of Vue 3 composables for building reactive game UI on top of the SAGE engine. Entity state, input actions, events, and timers are all exposed as Vue refs with automatic lifecycle cleanup.
Installation
npm install @skewedaspect/sage-vue@skewedaspect/sage-vue has peer dependencies on vue (3.x) and @skewedaspect/sage.
SageCanvas
SageCanvas is the root component that creates (or accepts) a SAGE engine instance and renders the WebGL canvas. All composables described below must be used inside a component that is a descendant of <SageCanvas>.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
entityDefinitions | GameEntityDefinition[] | [] | Entity definitions to register with the engine on creation. |
options | SageOptions | {} | Engine configuration passed to createGameEngine(). |
engine | GameEngine | --- | Optional pre-created engine instance. When provided, the component will not create or dispose the engine itself. |
Events
| Event | Payload | Description |
|---|---|---|
engine-ready | GameEngine | Fired once the engine has initialized and is ready to use. |
engine-error | unknown | Fired if engine creation fails. |
Slot Props
The default slot receives scoped props for rendering loading and error states:
| Slot Prop | Type | Description |
|---|---|---|
loading | boolean | true until the engine finishes initializing. |
error | unknown | The error object if initialization failed, otherwise null. |
engine | GameEngine | null | The engine instance once ready. |
Basic Usage
<template>
<SageCanvas
:entity-definitions="definitions"
:options="engineOptions"
@engine-ready="onReady"
>
<template #default="{ loading, error }">
<div v-if="loading" class="loading">Initializing engine...</div>
<div v-else-if="error" class="error">Failed to start: {{ error }}</div>
<GameHud v-else />
</template>
</SageCanvas>
</template>
<!--------------------------------------------------------------------------------------------------------------------->
<style lang="scss" scoped>
.loading, .error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 1.5rem;
}
</style>
<!--------------------------------------------------------------------------------------------------------------------->
<script setup lang="ts">
import { SageCanvas } from '@skewedaspect/sage-vue';
import type { GameEngine, GameEntityDefinition, SageOptions } from '@skewedaspect/sage';
// Components
import GameHud from './gameHud.vue';
const definitions : GameEntityDefinition[] = [
// ... your entity definitions
];
const engineOptions : SageOptions = {
// ... your engine options
};
function onReady(engine : GameEngine) : void
{
engine.start();
}
</script>
<!--------------------------------------------------------------------------------------------------------------------->External Engine
If you need to create the engine yourself (for example, to configure it before mounting or to share it across multiple components), pass a pre-created GameEngine instance via the engine prop. When you do this, SageCanvas will not create or dispose the engine -- you own its lifecycle.
<template>
<SageCanvas :engine="engine" @engine-ready="onReady">
<template #default="{ loading }">
<div v-if="loading">Attaching engine...</div>
<GameHud v-else />
</template>
</SageCanvas>
</template>
<!--------------------------------------------------------------------------------------------------------------------->
<script setup lang="ts">
import { SageCanvas } from '@skewedaspect/sage-vue';
import { createGameEngine } from '@skewedaspect/sage';
import type { GameEngine } from '@skewedaspect/sage';
// Components
import GameHud from './gameHud.vue';
// Engine created externally -- you control its lifecycle
const canvas = document.getElementById('my-canvas') as HTMLCanvasElement;
const engine = await createGameEngine(canvas, [], { logLevel: 'debug' });
function onReady(eng : GameEngine) : void
{
eng.start();
}
</script>
<!--------------------------------------------------------------------------------------------------------------------->WARNING
When using the engine prop, SageCanvas will not call stop() or dispose() on unmount. You are responsible for cleaning up the engine when it is no longer needed.
Overlay Behavior
SageCanvas renders a .sage-overlay div on top of the canvas that holds slot content. The overlay itself has pointer-events: none so clicks pass through to the canvas, but all child elements inside the overlay automatically get pointer-events: auto restored. This means your HUD buttons, menus, and other UI elements are interactive by default without any extra CSS.
useSageEngine
Returns the raw engine reference and a computed event bus. This is the foundation all other composables build on.
import { useSageEngine } from '@skewedaspect/sage-vue';
const { engine, eventBus } = useSageEngine();
// engine is ShallowRef<GameEngine | null>
// eventBus is ComputedRef<GameEventBus | null>The engine ref starts as null and is populated once the canvas mounts and the engine initializes.
Signature:
function useSageEngine() : {
engine : ShallowRef<GameEngine | null>;
eventBus : ComputedRef<GameEventBus | null>;
}WARNING
Throws an error if called outside of a <SageCanvas> component tree. Always ensure your component is a descendant of <SageCanvas>.
useSageEvent
Subscribes to engine events by pattern with automatic cleanup on unmount.
import { useSageEvent } from '@skewedaspect/sage-vue';
// Listen for all entity events using a wildcard
useSageEvent('entity:*', (event) =>
{
console.log(`Entity event: ${ event.type }`, event.payload);
});
// RegExp patterns work too
useSageEvent(/^player:/, (event) =>
{
handlePlayerEvent(event);
});The subscription is created immediately if the engine is available, or deferred via a watch until it becomes available. When the engine ref changes (e.g., hot reload), the old subscription is torn down and a new one is created. The subscription is automatically removed when the component unmounts.
Signature:
function useSageEvent(pattern : string | RegExp, handler : EventHandler) : voiduseSageAction
Returns a reactive ref bound to an input action's current value. The ref updates automatically whenever the engine publishes an action:<name> event.
import { useSageAction } from '@skewedaspect/sage-vue';
// Track whether the "jump" action is active
const jump = useSageAction('jump');
// Track an analog stick axis
const moveX = useSageAction('moveX');The ref is Ref<boolean | number> -- boolean for button-style actions (pressed/released), number for axis-style actions (analog stick deflection, trigger pressure).
Signature:
function useSageAction(actionName : string) : Ref<boolean | number>Template Example: Action Indicator
<template>
<div class="action-display">
<span :class="{ active: jump }">Jump: {{ jump }}</span>
<span>Move X: {{ moveX.toFixed(2) }}</span>
</div>
</template>
<!--------------------------------------------------------------------------------------------------------------------->
<style lang="scss" scoped>
.action-display {
position: absolute;
top: 10px;
right: 10px;
display: flex;
flex-direction: column;
gap: 4px;
color: white;
font-family: monospace;
}
.active {
color: #2ecc71;
font-weight: bold;
}
</style>
<!--------------------------------------------------------------------------------------------------------------------->
<script setup lang="ts">
import { useSageAction } from '@skewedaspect/sage-vue';
const jump = useSageAction('jump');
const moveX = useSageAction('moveX');
</script>
<!--------------------------------------------------------------------------------------------------------------------->useSageState
Returns a reactive ref synced to an entity's state, updated via entity:state-changed events. Optionally tracks a single property instead of the entire state object.
import { useSageState } from '@skewedaspect/sage-vue';
// Track entire entity state
const playerState = useSageState<PlayerState>('player-entity-id');
// Track a single property
const hp = useSageState<number>('player-entity-id', 'hp');The composable reads the initial state from the entity (if it exists at call time) and then listens for entity:state-changed events to keep the ref in sync. Only events matching the specified entityId trigger updates. Cleanup is automatic on unmount.
Signature:
function useSageState<T = unknown>(entityId : string, key ?: string) : Ref<T>Template Example: Game HUD
<template>
<div class="hud">
<div class="health-bar" :style="{ width: `${ hp }%` }">
{{ hp }} HP
</div>
<div class="score">Score: {{ playerState.score }}</div>
</div>
</template>
<!--------------------------------------------------------------------------------------------------------------------->
<style lang="scss" scoped>
.hud {
position: absolute;
top: 10px;
left: 10px;
}
.health-bar {
background: #e74c3c;
color: white;
padding: 4px 8px;
transition: width 0.3s;
}
</style>
<!--------------------------------------------------------------------------------------------------------------------->
<script setup lang="ts">
import { useSageState } from '@skewedaspect/sage-vue';
interface PlayerState
{
hp : number;
score : number;
}
const hp = useSageState<number>('player-entity-id', 'hp');
const playerState = useSageState<PlayerState>('player-entity-id');
</script>
<!--------------------------------------------------------------------------------------------------------------------->useSageTimer
Wraps the engine's GameTimer with automatic lifecycle management. All active delays and intervals created through this composable are cancelled when the component unmounts.
import { useSageTimer } from '@skewedaspect/sage-vue';
const { delay, interval, cooldown } = useSageTimer();
// One-shot delay -- cancelled automatically on unmount
delay(3000, () =>
{
showTutorialHint();
});
// Repeating interval -- also auto-cancelled
const cancelRegen = interval(1000, () =>
{
regenerateHealth();
});
// Manual cancel is still available
cancelRegen();
// Cooldowns are passive pollable objects
const attackCooldown = cooldown(500);
if(attackCooldown.ready)
{
attack();
attackCooldown.reset();
}If the engine is not yet available when a timer function is called, delay and interval return no-op cancel functions and cooldown returns a handle that is permanently not-ready. These are safe to call unconditionally.
Signature:
function useSageTimer() : {
delay : (ms : number, callback : () => void) => () => void;
interval : (ms : number, callback : () => void) => () => void;
cooldown : (ms : number) => CooldownHandle;
}Template Example: Ability Bar with Cooldowns
<template>
<div class="ability-bar">
<button
:disabled="!canFireball"
@click="castFireball"
>
Fireball
</button>
</div>
</template>
<!--------------------------------------------------------------------------------------------------------------------->
<style lang="scss" scoped>
.ability-bar {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
}
</style>
<!--------------------------------------------------------------------------------------------------------------------->
<script setup lang="ts">
import { ref } from 'vue';
import { useSageTimer, useSageEngine } from '@skewedaspect/sage-vue';
const { engine } = useSageEngine();
const { cooldown, delay } = useSageTimer();
const fireballCooldown = cooldown(2000);
const canFireball = ref(true);
function castFireball() : void
{
if(!fireballCooldown.ready) { return; }
engine.value?.eventBus.publish({ type: 'spell:fireball' });
fireballCooldown.reset();
canFireball.value = false;
// Re-enable the button after the cooldown period
delay(2000, () => { canFireball.value = true; });
}
</script>
<!--------------------------------------------------------------------------------------------------------------------->Quick Reference
| Composable | Returns | Auto-cleanup |
|---|---|---|
useSageEngine() | { engine, eventBus } | No (refs only) |
useSageEvent(pattern, handler) | void | Yes -- unsubscribes on unmount |
useSageAction(actionName) | Ref<boolean | number> | Yes -- unsubscribes on unmount |
useSageState(entityId, key?) | Ref<T> | Yes -- unsubscribes on unmount |
useSageTimer() | { delay, interval, cooldown } | Yes -- cancels delays and intervals on unmount |
TIP
All composables except useSageEngine() handle the "engine not ready yet" case gracefully. You can call them in setup() without waiting for the engine-ready event -- they will wire up their subscriptions as soon as the engine becomes available.
