Scene Router
The scene router orchestrates transitions between scenes using a finite state machine. Define states, transitions, and navigate programmatically.
Scenes are defined separately with
defineScene(). See Scenes.
Defining a Router
defineSceneRouter() declares states and transitions:
import { defineSceneRouter } from '@gwenjs/core/scene'
import { MenuScene, GameScene, GameOverScene } from './scenes'
export const AppRouter = defineSceneRouter({
initial: 'menu',
routes: {
menu: {
scene: MenuScene,
on: { START: 'game' },
},
game: {
scene: GameScene,
on: { PAUSE: 'pause', GAME_OVER: 'gameOver' },
},
gameOver: {
scene: GameOverScene,
on: { RESTART: 'game', MENU: 'menu' },
},
},
})initial— the starting state (must be a key inroutes)on— maps event names to target statesoverlay: true— the scene is rendered on top of the previous scene (useful for pause menus)
Navigating
Call useSceneRouter() inside an actor or system to get a handle, then call .send() to trigger transitions:
import { defineActor, onUpdate, useComponent } from '@gwenjs/core/actor'
import { useSceneRouter } from '@gwenjs/core/scene'
import { AppRouter } from '../router'
import { Health } from '../components'
import { PlayerPrefab } from './prefabs/Player'
export const PlayerActor = defineActor(PlayerPrefab, () => {
const nav = useSceneRouter(AppRouter)
const health = useComponent(Health)
onUpdate(async () => {
if (health.value <= 0) {
await nav.send('GAME_OVER') // transitions to 'gameOver'
}
})
return {}
})Handle API
const nav = useSceneRouter(AppRouter)
await nav.send('START') // trigger transition
nav.can('START') // check if transition is valid
nav.current // current state name
nav.params // params passed on transitionPassing Params
Pass data when sending an event:
async () => {
await nav.send('START', { level: 2, difficulty: 'hard' })
}
// In the GameScene:
export const GameScene = defineScene('game', () => ({
systems: [GameSystem],
onEnter: async () => {
const nav = useSceneRouter(AppRouter)
const params = nav.params
console.log('Starting level', params.level)
},
}))Scene Lifecycle
When a transition fires:
onExitof the current scene is called (unlessoverlay: true)onEnterof the target scene is called- Systems from the old scene are deregistered, new ones registered
export const GameScene = defineScene('Game', () => ({
systems: [PlayerSystem, EnemySystem],
onEnter: async () => {
console.log('Game scene loaded!')
await loadAssets()
},
onExit: () => {
console.log('Game scene unloading')
cleanup()
},
}))Overlay Scenes
Set overlay: true to keep the previous scene loaded and rendered behind the new one:
const AppRouter = defineSceneRouter({
initial: 'game',
routes: {
game: { scene: GameScene, on: { PAUSE: 'pause' } },
pause: {
scene: PauseScene,
overlay: true, // Game keeps running behind pause menu
on: { RESUME: 'game' },
},
},
})When you transition to pause:
- Game scene stays loaded (systems keep running)
- Game scene stays rendering (behind pause UI)
onExitis not called on the game sceneonEnteris called on the pause scene- Physics and update logic continue for the game scene
When you return from pause:
onExitis called on pause scene- Game scene resumes immediately (
onEnteris not called again)
Validation
defineSceneRouter() validates at definition time:
initialmust be a key inroutes- All transition targets must be valid route keys
Errors are thrown immediately (not at runtime), so misconfigured routers are caught during development.
Registering the Router
Register the router in gwen.config.ts as a module option:
// gwen.config.ts
export default defineConfig({
modules: [
['@gwenjs/core', { router: AppRouter }],
],
})The router is passed as a module option, not as a standalone engine.use() call.
Complete Example
// src/router.ts
import { defineSceneRouter } from '@gwenjs/core/scene'
import { MenuScene, GameScene, GameOverScene } from './scenes'
export const AppRouter = defineSceneRouter({
initial: 'menu',
routes: {
menu: {
scene: MenuScene,
on: { START: 'game' },
},
game: {
scene: GameScene,
on: { PAUSE: 'pause', GAME_OVER: 'gameOver' },
},
gameOver: {
scene: GameOverScene,
on: { RESTART: 'game', MENU: 'menu' },
},
},
})
// gwen.config.ts
import { defineConfig } from '@gwenjs/app'
import { AppRouter } from './router'
export default defineConfig({
modules: [
['@gwenjs/core', { router: AppRouter }],
],
})API Summary
defineSceneRouter(options) | Declare the FSM |
useSceneRouter(router) | Get runtime handle inside actor/system |
nav.send(event, params?) | Trigger a transition (async) |
nav.can(event) | Check if transition is valid |
nav.current | Current state name |
nav.params | Params passed to current state |
nav.onTransition(fn) | Subscribe to state changes |
nav.onTransition((from, to) => {
console.log(`Transitioned from ${from} to ${to}`)
})