Talk to a character
Goal
A talking character (an AI NPC on Convorcher) the player walks up to, presses E at and talks to in their own words, typed or spoken, while the character answers aloud with subtitles and a moving face; the game hears what was said and how the story is going. Needs Set up talking characters and Write a character's story.
Files you will edit
src/systems/talk.tssrc/game.ts
Steps
The talk system.
src/systems/talk.tsholds the character's prefab (taggedTalks, notNpc, so the scripted dialogue leaves it alone), starts the talk onEwithin reach, asks what the player says, and ends the talk when they walk away.tsimport { Transform, conversation, prefab, query, type GameContext } from 'gameable'; import { NPC_HALF_HEIGHT, NPC_RADIUS } from '../prefabs'; /** * Tag: this NPC talks through the conversation service. Define it once: tags are matched by reference, so a second * `Talks` (one in `src/prefabs.ts` too) makes the query below find nobody. Import this one everywhere else. */ export const Talks: Record<string, never> = {}; /** The guard: `char.guard` is its character asset, `guard` its story (`src/guard.story.yaml`). */ export const GuardPrefab = prefab({ name: 'guard', body: { shape: 'capsule', dims: [NPC_RADIUS, NPC_HALF_HEIGHT], // kinematic, not `character`: two character controllers slide through each other kind: 'kinematic', mass: 70, layer: { character: true }, mask: { defaultLayer: true, staticGeometry: true, player: true, character: true }, }, character: 'char.guard', components: [Talks], }); const TALK_ID = 'guard'; const REACH = 2.2; export const talkState = { npc: 0, open: false, lastRevision: -1 }; export function talkSystem(ctx: GameContext): void { for (const event of ctx.events) { if (event.tag !== 'conversation-event' || event.val.entity !== talkState.npc) continue; const { kind, text } = event.val; if (kind === 'input' && talkState.open) conversation.command(talkState.npc, 'ask', TALK_ID, text); else if (kind === 'story') { const story = conversation.story(text); if (story && story.revision > talkState.lastRevision) talkState.lastRevision = story.revision; } else if (kind === 'error' && text.startsWith('closed:')) talkState.open = false; } if (ctx.player === 0) return; for (const npc of query(ctx.world, [Talks])) { const near = distance(ctx.player, npc) < REACH; if (!talkState.open && near && ctx.input.pressed('E')) { talkState.npc = npc; talkState.open = true; talkState.lastRevision = -1; conversation.command(npc, 'start', TALK_ID); } else if (talkState.open && npc === talkState.npc && !near) { talkState.open = false; conversation.command(npc, 'end', TALK_ID); } } } function distance(a: number, b: number): number { return Math.hypot( (Transform.x[a] ?? 0) - (Transform.x[b] ?? 0), (Transform.z[a] ?? 0) - (Transform.z[b] ?? 0), ); }An
errorthat starts withretry:keeps the talk open: the player just asks again. It is the service's fault, never the player's. For anE: talkprompt, show it fromsrc/hud.tswhentalkState.openis false and the player is in reach.Put them in the game. In
src/game.ts, addtalkSystemtosystemsand the guard tospawns:tsspawns: [place(GuardPrefab, { position: [0, 0, 4], yaw: Math.PI }, NPC_CENTRE) /* , ... */], systems: [/* ... */ talkSystem],
Verify
- Walk to the guard and press
E: the talk box opens at the bottom of the page. - Type "I need to get to my mother's farm": the guard answers aloud, the subtitle shows it and the face moves.
- A
storyevent follows the reply, withgave-reasonincompletedObjectivesonce you have said why. - Walk away: the talk box closes.
Going further
- An outcome the game owns (let them through, or not): Let the game decide a conversation.
- Hands-free voice instead of push to talk:
@getgameable/voxywithgameable/voice.
See also
- Set up talking characters — the relay and the page, once per game
- Let the game decide a conversation — an outcome the game owns
- Add NPC dialogue — scripted lines instead