React Hooks
A React hook is a function that gives a component access to shared state or behavior. Doodle Engine’s hooks provide game state, player actions, audio, and input handling.
useGame
Section titled “useGame”Access the shared game state and actions from any component inside a GameProvider.
import { useGame } from '@doodle-engine/react';
function MyComponent() { const { snapshot, actions } = useGame();
return ( <div> <h1>{snapshot.location.name}</h1> <button onClick={() => actions.talkTo('bartender')}> Talk to {snapshot.charactersHere[0]?.name} </button> </div> );}Returns
Section titled “Returns”interface GameContextValue { snapshot: Snapshot; actions: { selectChoice: (choiceId: string) => void; continueDialogue: () => void; talkTo: (characterId: string) => void; travelTo: (locationId: string) => void; writeNote: (title: string, text: string) => void; deleteNote: (noteId: string) => void; setLocale: (locale: string) => void; setPlayerProfile: (profile: PlayerProfileInput) => void; saveGame: () => SaveData; loadGame: (saveData: SaveData) => void; dismissInterlude: () => void; };}Requirements
Section titled “Requirements”Must be used inside a GameProvider. Throws an error if used outside.
Actions
Section titled “Actions”Each action calls the corresponding engine method and updates the snapshot:
| Action | Description |
|---|---|
selectChoice(choiceId) |
Pick a dialogue choice |
continueDialogue() |
Advance past a text-only dialogue node |
talkTo(characterId) |
Start conversation with a character |
travelTo(locationId) |
Travel to a map location |
writeNote(title, text) |
Add a player note |
deleteNote(noteId) |
Remove a player note |
setLocale(locale) |
Change language |
setPlayerProfile(profile) |
Complete a requested player profile |
saveGame() |
Returns SaveData (doesn’t update snapshot) |
loadGame(saveData) |
Restores state and updates snapshot |
dismissInterlude() |
Clears a pending interlude from the snapshot |
useAudioSettings
Section titled “useAudioSettings”Read and update the persistent volume settings from AudioSettingsProvider.
import { useAudioSettings } from '@doodle-engine/react';
function MusicVolume() { const audio = useAudioSettings();
return ( <input aria-label="Music volume" type="range" min="0" max="1" step="0.1" value={audio.musicVolume} onChange={(event) => audio.setMusicVolume(Number(event.target.value)) } /> );}The returned object contains masterVolume, musicVolume, soundVolume, and
voiceVolume, plus a setter for each value. The hook must be used inside
AudioSettingsProvider.
useAudioManager
Section titled “useAudioManager”Manages audio playback as the snapshot changes. Pass the current volume values each time the component renders. Active audio updates when those values change.
Store volume state in AudioSettingsContext or another application store and pass the values to the hook.
import { useAudioManager, useAudioSettings, AudioSettingsProvider } from '@doodle-engine/react';
function MyGame() { const { snapshot } = useGame(); const audioSettings = useAudioSettings();
const { stopAll } = useAudioManager(snapshot, { masterVolume: audioSettings.masterVolume, musicVolume: audioSettings.musicVolume, soundVolume: audioSettings.soundVolume, voiceVolume: audioSettings.voiceVolume, });
return <button onClick={stopAll}>Stop Audio</button>;}
// Wrap in AudioSettingsProvider for persistent volume state<AudioSettingsProvider> <GameProvider engine={engine} initialSnapshot={snapshot}> <MyGame /> </GameProvider></AudioSettingsProvider>Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
snapshot |
Snapshot |
Current game snapshot |
options |
AudioManagerOptions |
Volume levels and crossfade config |
Options
Section titled “Options”Volume values are reactive: when they change, all active audio elements update immediately.
| Option | Type | Default | Description |
|---|---|---|---|
masterVolume |
number |
1.0 |
Master volume multiplier (0-1) |
musicVolume |
number |
0.7 |
Music channel volume (0-1) |
soundVolume |
number |
0.8 |
Sound effects volume (0-1) |
voiceVolume |
number |
1.0 |
Voice channel volume (0-1) |
crossfadeDuration |
number |
1000 |
Music crossfade duration in ms |
Returns
Section titled “Returns”interface AudioManagerControls { stopAll: () => void;}Audio Channels
Section titled “Audio Channels”The hook manages four channels:
| Channel | Source | Behavior |
|---|---|---|
| Music | snapshot.music |
Loops, crossfades between tracks |
| Ambient | snapshot.ambient |
Loops, swaps on location change |
| Voice | snapshot.dialogue?.voice |
Plays dialogue voice lines |
| Sound | snapshot.pendingSounds |
One-shot effects, cleared after playing |
Automatic Behavior
Section titled “Automatic Behavior”- When
snapshot.musicchanges, the current track crossfades to the new one - When
snapshot.ambientchanges, the ambient track swaps immediately - When
snapshot.dialogue?.voiceis present, the voice file plays - All entries in
snapshot.pendingSoundsare played as one-shot effects - Volume levels are applied as
channelVolume × masterVolume - Audio paths are resolved by the engine before reaching this hook. Write bare filenames in YAML content.
useUISounds
Section titled “useUISounds”Hook for interface sounds such as clicks and menus opening or closing. useAudioManager handles audio from game content, while useUISounds handles the renderer’s controls.
import { useUISounds } from '@doodle-engine/react';
function MyUI() { const uiSounds = useUISounds({ basePath: 'assets/audio/ui', volume: 0.5, sounds: { click: 'click.ogg', menuOpen: 'menu_open.ogg', menuClose: 'menu_close.ogg', }, });
return ( <button onClick={() => { uiSounds.playClick(); doSomething(); }} > Click Me </button> );}Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Enable/disable UI sounds |
basePath |
string |
'assets/audio/ui' |
Base path for UI sound files |
volume |
number |
0.5 |
Volume level (0-1) |
sounds |
object |
— | Custom sound file names |
sounds.click |
string |
'click.ogg' |
Click sound file |
sounds.hover |
string |
'hover.ogg' |
Hover sound file |
sounds.menuOpen |
string |
'menu_open.ogg' |
Menu open sound file |
sounds.menuClose |
string |
'menu_close.ogg' |
Menu close sound file |
Returns
Section titled “Returns”interface UISoundControls { playClick: () => void; playHover: () => void; playMenuOpen: () => void; playMenuClose: () => void; playSound: (key: string) => void; setEnabled: (enabled: boolean) => void; setVolume: (volume: number) => void; enabled: boolean; volume: number;}Usage with GameShell
Section titled “Usage with GameShell”GameShell uses useUISounds internally. Configure via the uiSounds prop:
<GameShell registry={registry} config={config} manifest={manifest} projectId={PROJECT_ID} uiSounds={{ basePath: 'assets/audio/ui', volume: 0.5, sounds: { click: 'click.ogg' }, }}/>Pass uiSounds={false} to disable UI sounds entirely.
useInputAction
Section titled “useInputAction”Register a renderer input command handler. Use this for keyboard commands in custom renderer surfaces, panels, and overlays.
import { InputProvider, useInputAction } from '@doodle-engine/react';
function DialogueControls({ choices, onChoice, onContinue }) { useInputAction( ({ command, choiceIndex }) => { if (command === 'confirm' && choices.length === 0) { onContinue(); return true; }
if ( choiceIndex !== undefined && choiceIndex < choices.length ) { onChoice(choices[choiceIndex].id); return true; }
return false; }, { priority: 0 } );
return null;}
<InputProvider> <DialogueControls choices={snapshot.choices} onChoice={actions.selectChoice} onContinue={actions.continueDialogue} /></InputProvider>;GameShell already includes InputProvider. GameRenderer creates a provider
boundary when used standalone, so its built-in keyboard handling works with or
without GameShell.
Commands
Section titled “Commands”Keyboard input is translated into these commands:
| Command | Default keyboard input |
|---|---|
confirm |
Enter, Space |
cancel |
Escape |
choice1-choice9 |
Number keys 1-9 |
next |
ArrowDown, ArrowRight |
previous |
ArrowUp, ArrowLeft |
The command type also includes continue, openInventory, openJournal,
openMap, and openMenu so custom renderers can dispatch higher-level UI
commands when they need them.
Priority
Section titled “Priority”Higher-priority handlers receive commands first. Return true to consume the
command, which stops it from reaching lower-priority handlers.
Recommended priorities:
| Surface | Priority |
|---|---|
| Full-screen video/interlude | 300 |
| Modal or panel overlay | 150 |
| Shell pause/settings | 50 |
| Dialogue choices/Continue | 0 |
Input events from text fields, textareas, selects, and contenteditable elements are ignored by the default keyboard adapter so typing into player notes does not trigger game commands.
useInputRouter
Section titled “useInputRouter”Access the current input router directly. Use useInputAction for component
commands and useInputRouter when connecting another input source.
import { useInputRouter } from '@doodle-engine/react';
function CustomInputBridge() { const router = useInputRouter();
useEffect(() => { if (!router) return;
// When your custom input layer detects the confirm action: router.dispatchCommand({ command: 'confirm', source: 'programmatic', }); }, [router]);
return null;}Save-slot helpers
Section titled “Save-slot helpers”Use these helpers when a custom renderer needs its own save interface. Generate the storage key from the project’s stable ID, then use that same key for every operation.
import { latestSave, saveStorageKeyForProject, useGame, writeSave,} from '@doodle-engine/react';import { PROJECT_ID } from './project';
function SaveButtons() { const { actions } = useGame(); const saveKey = saveStorageKeyForProject(PROJECT_ID);
const save = () => { writeSave(localStorage, saveKey, actions.saveGame(), 'manual'); }; const load = () => { const savedGame = latestSave(localStorage, saveKey); if (savedGame) actions.loadGame(savedGame); };
return ( <> <button onClick={save}>Save</button> <button onClick={load}>Load latest</button> </> );}| Helper | Result |
|---|---|
saveStorageKeyForProject(id) |
Validated storage key for one project |
listSaves(storage, key) |
All quick, auto, and manual slots in display order |
hasSaves(storage, key) |
Whether at least one slot exists |
writeSave(storage, key, save, kind?, options?) |
Written slot; quick and auto overwrite their existing kind |
loadSave(storage, key, id) |
Saved game for one slot, or null |
deleteSave(storage, key, id) |
Removes one slot |
latestSave(storage, key) |
Most recent saved game, or null |
See Save & Load for project isolation, slot behavior, and migration details.