Skip to content

Custom Renderer

The default GameRenderer provides a complete UI out of the box, but you can build your own using the useGame hook and individual components.

The useGame hook provides the current snapshot and all action methods:

import { useGame } from '@doodle-engine/react';
function MyCustomGame() {
const { snapshot, actions } = useGame();
return (
<div>
<h1>{snapshot.location.name}</h1>
<p>{snapshot.location.description}</p>
{snapshot.dialogue && (
<div>
<strong>{snapshot.dialogue.speakerName}:</strong>
<p>{snapshot.dialogue.text}</p>
</div>
)}
{snapshot.choices.map((choice) => (
<button
key={choice.id}
onClick={() => actions.selectChoice(choice.id)}
>
{choice.text}
</button>
))}
{!snapshot.dialogue &&
snapshot.charactersHere.map((char) => (
<button
key={char.id}
onClick={() => actions.talkTo(char.id)}
>
Talk to {char.name}
</button>
))}
</div>
);
}

Wrap it with GameProvider. While content is loading, render a placeholder:

import { GameProvider } from '@doodle-engine/react';
function App() {
const [game, setGame] = useState<{
engine: Engine;
snapshot: Snapshot;
} | null>(null);
useEffect(() => {
fetch('/api/content')
.then((r) => r.json())
.then(({ registry, config }) => {
const engine = new Engine(registry, {} as GameState);
const snapshot = engine.newGame(config);
setGame({ engine, snapshot });
});
}, []);
if (!game)
return (
<div className="app-bootstrap">
<div className="spinner" />
</div>
);
return (
<GameProvider
engine={game.engine}
initialSnapshot={game.snapshot}
devTools={import.meta.env.DEV}
>
<MyCustomGame />
</GameProvider>
);
}
actions.selectChoice(choiceId: string) // Pick a dialogue choice
actions.talkTo(characterId: string) // Start conversation
actions.takeItem(itemId: string) // Pick up an item
actions.travelTo(locationId: string) // Travel via map
actions.writeNote(title, text) // Add a player note
actions.deleteNote(noteId: string) // Remove a player note
actions.setLocale(locale: string) // Change language
actions.saveGame() // Returns SaveData
actions.loadGame(saveData: SaveData) // Restore from save

You can use the pre-built components with your own layout:

import {
LoadingScreen,
DialogueBox,
ChoiceList,
LocationView,
CharacterList,
MapView,
Inventory,
Journal,
NotificationArea,
SaveLoadPanel,
} from '@doodle-engine/react';
function MyLayout() {
const { snapshot, actions } = useGame();
return (
<div className="my-layout">
<LocationView location={snapshot.location} />
{snapshot.dialogue && <DialogueBox dialogue={snapshot.dialogue} />}
<ChoiceList
choices={snapshot.choices}
onSelectChoice={actions.selectChoice}
/>
<CharacterList
characters={snapshot.charactersHere}
onTalkTo={actions.talkTo}
/>
<Inventory items={snapshot.inventory} />
<Journal quests={snapshot.quests} entries={snapshot.journal} />
{snapshot.map && (
<MapView map={snapshot.map} onTravelTo={actions.travelTo} />
)}
<NotificationArea notifications={snapshot.notifications} />
<SaveLoadPanel
onSave={actions.saveGame}
onLoad={actions.loadGame}
/>
</div>
);
}

The snapshot provides everything your renderer needs:

snapshot.location; // Current location (name, description, banner)
snapshot.dialogue; // Current dialogue node or null
snapshot.choices; // Available choices (empty if no dialogue or auto-advance)
snapshot.charactersHere; // NPCs at current location
snapshot.party; // Characters in the player's party
snapshot.inventory; // Player's items
snapshot.quests; // Active quests with current stage
snapshot.journal; // Unlocked journal entries
snapshot.variables; // Game variables (gold, reputation, etc.)
snapshot.time; // Current in-game time { day, hour }
snapshot.map; // Map data or null if disabled
snapshot.music; // Current music track
snapshot.ambient; // Current ambient sound
snapshot.notifications; // Transient notifications (shown once)
snapshot.pendingSounds; // Sound effects to play (cleared after snapshot)

When devTools={import.meta.env.DEV} is set on GameProvider or GameShell, a window.doodle object is available in your browser’s DevTools console while developing. Type doodle.inspect() to see all available commands:

doodle.setFlag("flagName") // Set a flag
doodle.clearFlag("flagName") // Clear a flag
doodle.setVariable("gold", 100) // Set a variable
doodle.getVariable("gold") // Read a variable
doodle.teleport("locationId") // Jump to a location
doodle.triggerDialogue("dialogueId") // Start a dialogue
doodle.setQuestStage("questId", "stage")
doodle.addItem("itemId")
doodle.removeItem("itemId")
doodle.inspect() // Print current state summary
doodle.inspectState() // Return raw game state object
doodle.inspectRegistry() // Return content registry object

window.doodle is only available after a game has started (after clicking New Game or Continue), because GameProvider must be mounted first.

The core engine is framework-agnostic. Use it with any UI:

import { Engine } from '@doodle-engine/core';
const engine = new Engine(registry, state);
const snapshot = engine.newGame(config);
// Render snapshot however you want
renderMyUI(snapshot);
// On user action
const newSnapshot = engine.selectChoice('choice_1');
renderMyUI(newSnapshot);

See Engine API Reference for all available methods.