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.
Using useGame
Section titled “Using useGame”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> );}Available Actions
Section titled “Available Actions”actions.selectChoice(choiceId: string) // Pick a dialogue choiceactions.talkTo(characterId: string) // Start conversationactions.takeItem(itemId: string) // Pick up an itemactions.travelTo(locationId: string) // Travel via mapactions.writeNote(title, text) // Add a player noteactions.deleteNote(noteId: string) // Remove a player noteactions.setLocale(locale: string) // Change languageactions.saveGame() // Returns SaveDataactions.loadGame(saveData: SaveData) // Restore from saveMixing Individual Components
Section titled “Mixing Individual Components”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> );}Snapshot Structure
Section titled “Snapshot Structure”The snapshot provides everything your renderer needs:
snapshot.location; // Current location (name, description, banner)snapshot.dialogue; // Current dialogue node or nullsnapshot.choices; // Available choices (empty if no dialogue or auto-advance)snapshot.charactersHere; // NPCs at current locationsnapshot.party; // Characters in the player's partysnapshot.inventory; // Player's itemssnapshot.quests; // Active quests with current stagesnapshot.journal; // Unlocked journal entriessnapshot.variables; // Game variables (gold, reputation, etc.)snapshot.time; // Current in-game time { day, hour }snapshot.map; // Map data or null if disabledsnapshot.music; // Current music tracksnapshot.ambient; // Current ambient soundsnapshot.notifications; // Transient notifications (shown once)snapshot.pendingSounds; // Sound effects to play (cleared after snapshot)Dev Tools Console API
Section titled “Dev Tools Console API”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 flagdoodle.clearFlag("flagName") // Clear a flagdoodle.setVariable("gold", 100) // Set a variabledoodle.getVariable("gold") // Read a variabledoodle.teleport("locationId") // Jump to a locationdoodle.triggerDialogue("dialogueId") // Start a dialoguedoodle.setQuestStage("questId", "stage")doodle.addItem("itemId")doodle.removeItem("itemId")doodle.inspect() // Print current state summarydoodle.inspectState() // Return raw game state objectdoodle.inspectRegistry() // Return content registry objectwindow.doodle is only available after a game has started (after clicking New Game or Continue), because GameProvider must be mounted first.
Building Without React
Section titled “Building Without React”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 wantrenderMyUI(snapshot);
// On user actionconst newSnapshot = engine.selectChoice('choice_1');renderMyUI(newSnapshot);See Engine API Reference for all available methods.