Responding to Events
React to dialogue events in your game code. Trigger animations, play sounds, update UI, or log analytics when things happen in the dialogue.
Prerequisites
Before starting, you should:
Overview
We'll connect dialogue events to game systems like audio, animation, and analytics.
- Register event callbacks
- Handle node transitions
- Respond to choices
- Debug with action and condition events
Step 1: Register Event Callbacks
Pass callbacks in the runner options:
import { createDialogueRunner } from '@motioneffector/dialogue'
const runner = createDialogueRunner({
onDialogueStart: (dialogue) => {
console.log(`Starting dialogue: ${dialogue.id}`)
},
onDialogueEnd: (dialogueId, endNode) => {
console.log(`Dialogue ${dialogueId} ended at node: ${endNode?.text}`)
},
onNodeEnter: (node, speaker) => {
console.log(`Entered node: ${node.text.substring(0, 50)}...`)
},
onNodeExit: (node) => {
console.log(`Exited node`)
},
onChoiceSelected: (choice, index) => {
console.log(`Player chose option ${index}: ${choice.text}`)
}
})
Step 2: Handle Node Transitions
onNodeEnter fires every time a new node is displayed. Use it to update your UI:
const runner = createDialogueRunner({
speakers: {
npc: { name: 'Elder', portrait: 'elder.png' }
},
onNodeEnter: (node, speaker) => {
// Update dialogue box
dialogueUI.setText(node.text)
// Update speaker display
if (speaker) {
dialogueUI.showSpeaker(speaker.name, speaker.portrait)
} else {
dialogueUI.hidePortrait() // Narrator text
}
// Play voice line if tagged
if (node.tags?.includes('voiced')) {
audioManager.playVoice(node.speaker, node.text)
}
},
onNodeExit: (node) => {
// Clean up previous node
audioManager.stopVoice()
}
})
Step 3: Respond to Choices
onChoiceSelected fires when the player makes a selection:
const runner = createDialogueRunner({
onChoiceSelected: (choice, index) => {
// Play click sound
audioManager.play('ui_click')
// Log for analytics
analytics.track('dialogue_choice', {
choice: choice.text,
index: index,
tags: choice.tags
})
// Trigger animation based on choice tags
if (choice.tags?.includes('aggressive')) {
characterAnimator.play('angry_gesture')
} else if (choice.tags?.includes('friendly')) {
characterAnimator.play('smile')
}
}
})
Step 4: Debug Events
onActionExecuted and onConditionEvaluated help debug complex dialogues:
const runner = createDialogueRunner({
onActionExecuted: (action, result) => {
console.log(`Action executed:`, action)
console.log(`Result:`, result)
},
onConditionEvaluated: (condition, result) => {
console.log(`Condition:`, condition)
console.log(`Passed:`, result)
}
})
Complete Example
import { createDialogueRunner, DialogueDefinition } from '@motioneffector/dialogue'
// Mock game systems
const gameUI = {
showDialogue: (text: string, speaker?: string) => {
console.log(speaker ? `[${speaker}]: ${text}` : text)
},
hideDialogue: () => console.log('--- Dialogue closed ---'),
showChoices: (choices: string[]) => {
choices.forEach((c, i) => console.log(` ${i + 1}. ${c}`))
}
}
const audioManager = {
play: (sound: string) => console.log(`[Audio] Playing: ${sound}`),
playMusic: (track: string) => console.log(`[Music] Playing: ${track}`)
}
const analytics = {
track: (event: string, data: any) => console.log(`[Analytics] ${event}:`, data)
}
// Create runner with all events
const runner = createDialogueRunner({
speakers: {
wizard: { name: 'Archmage Thorn', portrait: 'wizard.png' },
player: { name: 'Hero' }
},
onDialogueStart: (dialogue) => {
audioManager.playMusic('mysterious_theme')
analytics.track('dialogue_started', { id: dialogue.id })
},
onDialogueEnd: (dialogueId, endNode) => {
gameUI.hideDialogue()
analytics.track('dialogue_completed', {
id: dialogueId,
endNode: endNode?.text?.substring(0, 30)
})
},
onNodeEnter: (node, speaker) => {
gameUI.showDialogue(node.text, speaker?.name)
if (node.tags?.includes('dramatic')) {
audioManager.play('dramatic_sting')
}
},
onChoiceSelected: (choice, index) => {
audioManager.play('ui_select')
analytics.track('choice_made', {
text: choice.text,
index
})
},
onActionExecuted: (action, result) => {
if (action.type === 'callback') {
console.log(`[Callback] ${action.name} returned:`, result)
}
}
})
const dialogue: DialogueDefinition = {
id: 'wizard-meeting',
startNode: 'intro',
nodes: {
intro: {
speaker: 'wizard',
text: 'Ah, you have finally arrived.',
tags: ['dramatic'],
next: 'choice'
},
choice: {
speaker: 'wizard',
text: 'I have a task for you. Will you accept?',
choices: [
{ text: 'What is it?', next: 'explain' },
{ text: 'I accept blindly', next: 'accept', tags: ['brave'] },
{ text: 'I refuse', next: 'refuse' }
]
},
explain: {
speaker: 'wizard',
text: 'Retrieve the Crystal of Ages from the dungeon.',
next: 'choice'
},
accept: {
speaker: 'wizard',
text: 'Excellent! Your courage will be rewarded.',
tags: ['dramatic'],
isEnd: true
},
refuse: {
speaker: 'wizard',
text: 'A pity. Leave my tower.',
isEnd: true
}
}
}
async function main() {
await runner.start(dialogue)
// Simulate player input
await runner.choose(0) // "What is it?"
await runner.choose(1) // "I accept blindly"
}
main()
Variations
Async Event Handlers
Event handlers can be async, but they don't block the dialogue:
onNodeEnter: async (node, speaker) => {
await loadVoiceFile(node.speaker)
playVoice()
}
Conditional Event Logic
Check node properties to handle different scenarios:
onNodeEnter: (node, speaker) => {
if (node.isEnd) {
showEndDialogueAnimation()
}
if (node.speaker === 'villain') {
playVillainTheme()
}
}
Using Tags for Event Routing
onNodeEnter: (node, speaker) => {
node.tags?.forEach(tag => {
switch (tag) {
case 'shake_screen':
camera.shake()
break
case 'flash':
screen.flash()
break
case 'pause':
dialogue.waitForInput()
break
}
})
}
Troubleshooting
Event Not Firing
Symptom: Your callback never runs.
Cause: Callback not passed to runner options.
Solution: Verify you passed the callback when creating the runner, not after.
Event Fires Multiple Times
Symptom: onNodeEnter fires more than expected.
Cause: Auto-advancing nodes fire enter events too.
Solution: This is correct behavior. Each node entered fires the event. Filter by node.text or tags if needed.
See Also
- The DialogueRunner - Runner configuration
- Actions - What triggers onActionExecuted
- State API - Event subscription with on()