Skip to content

Vue 3 Integration

Status

SageCanvas, useSageEngine, useSageEntity, and useSageEntities work against 0.10. useSageAction still listens on a bus actions no longer travel on and has not been rebuilt.

Installation

bash
npm install @skewedaspect/sage-vue

@skewedaspect/sage-vue has peer dependencies on vue (3.x) and @skewedaspect/sage.

SageCanvas

SageCanvas renders the canvas, calls createGameEngine, and provides the engine to child components through Vue's dependency injection.

Props

PropTypeDefaultDescription
entityClassesReadonly<Record<string, GameEntityType>>{}The entity class table the simulation spawns from
optionsSageOptions{}Engine configuration passed to createGameEngine()
engineGameEnginenoneA pre-created engine. When given, the component neither creates nor disposes one.

Events

EventPayloadDescription
engine-readyGameEngineThe engine has initialized
engine-errorunknownEngine creation failed

Slot props

Slot propTypeDescription
loadingbooleantrue until the engine finishes initializing
errorunknownThe error if initialization failed, otherwise null
engineGameEngine | nullThe engine once ready

Usage

vue
<template>
    <SageCanvas :entity-classes="entityClasses" :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, SageOptions } from '@skewedaspect/sage';

    // Components
    import GameHud from './gameHud.vue';

    // Entities
    import { entityClasses } from './entities/index.ts';

    const engineOptions : SageOptions = { logLevel: 'info' };

    async function onReady(engine : GameEngine) : Promise<void>
    {
        await engine.managers.levelManager.activateLevel('arena');
        await engine.start();
    }
</script>

<!--------------------------------------------------------------------------------------------------------------------->

The component's overlay div holds the slot content above the canvas. The overlay itself passes pointer events through to the canvas; every element inside it receives them, so HUD buttons and menus are interactive without extra CSS.

On unmount, SageCanvas stops the engine and disposes the BabylonJS engine, unless the engine came in through the engine prop. If the component unmounts while createGameEngine is still in flight, the engine that resolves afterwards is stopped and disposed rather than left running.

A ResizeObserver on the container calls engine.renderEngine.resize() when the container's size changes.

useSageEngine

Returns the engine ref and a computed library event bus. Every other composable builds on it.

typescript
import { useSageEngine } from '@skewedaspect/sage-vue';

const { engine, eventBus } = useSageEngine();

// engine is ShallowRef<GameEngine | null>
// eventBus is ComputedRef<GameEventBus | null>

The ref starts as null and is set once the engine initializes. Calling it outside a <SageCanvas> subtree throws.

useSageEvent

Subscribes to the engine's library bus by pattern, with cleanup on unmount:

typescript
import { useSageEvent } from '@skewedaspect/sage-vue';

useSageEvent('level:*', (event) =>
{
    console.log(event.type, event.payload);
});

The library bus carries level, asset, trigger, and game events. Entity events and actions travel on the simulation bus, which this composable does not reach. See Events.

useSageTimer

Wraps engine.timer with cleanup on unmount. Timers requested before the engine boots are queued and started when it arrives.

typescript
import { useSageTimer } from '@skewedaspect/sage-vue';

const { delay, interval, cooldown } = useSageTimer();

const cancel = delay(3000, () => showTutorialHint());

useSageEntity and useSageEntities

A component reads an entity through a view: its state, read-only and reactive, and its operations.

vue
<script setup lang="ts">
    import { useSageEntity } from '@skewedaspect/sage-vue';
    import { PlayerEntity } from './entities/index.ts';

    const props = defineProps<{ id : string }>();

    const player = useSageEntity(PlayerEntity, props.id);
</script>

<template>
    <div v-if="player.present">
        Health: {{ player.state.health }}
        <button @click="player.ops.heal(10)">Heal</button>
    </div>
</template>

The class both types the view and checks it, so an ID naming an entity of another class leaves the view absent, and a subclass counts. useSageEntities(entityClass, tag) answers with a view per entity of that class carrying a tag, in spawn order, each view the same object across refreshes so a v-for keyed by id reuses its components.

A view refreshes once per rendered frame and keeps refreshing while the simulation is paused, so a pause menu that changes something through an operation shows it. Writing to a view's state does not compile, and at runtime reaches only the view's own copy. The entity-behaviors example's playerStats.vue is written this way.

useSageAction

useSageAction(actionName) subscribes to action:<name> on the library bus. InputManager broadcasts actions on the simulation bus, so the ref never updates. It has not been rebuilt for 0.10.

Released under the MIT License.