Skip to content

Talking characters ​

A talking character is an NPC the player talks to in their own words, typed or spoken, and who answers in their own voice, with a face that moves as they speak. It is an AI character with a persona: what they are like, what they know, what they hide, what they want from the conversation. Gameable runs it on Convorcher, the hosted conversation service at talk.gameable.com, which writes the replies, speaks them and judges how the conversation is going.

For scripted lines with fixed choices, use the third-person template's dialogue system (recipe Add NPC dialogue). It is simpler and never surprises you.

The pieces ​

PieceWhereWhat it does
The story filesrc/<id>.story.yaml in the gameThe character: personality, voice, knowledge, phases and objectives
The relaygameable({ talk: true }) in vite.config.tsHolds the API key on the server, uploads the story files, relays the conversation and the microphone
The pagetalkingCharacters() from gameable/conversation in src/main.tsVoice, face, subtitles, a talk box, events for the game
The gameconversation.command and conversation-events in src/game.tsStarts and ends the conversation, asks, and decides every outcome

The API key is the one from your Gameable account (Account → Developers), in .env.local as GAMEABLE_API_KEY. It stays on the dev server; the page never sees it.

The story file ​

Convorcher's story YAML. One file per character.

yaml
spec_version: '1.0'
story:
  id: gate-guard # what the relay names the story; unique in your account
  title: 'The gate guard'
  version: '1.0'
  start_phase: questioning
  character:
    name: gate-guard # the character's slug: its voice and face are looked up by it
    voice_id: onwK4e9ZLuTAKqWW03F9 # optional: a voice; a studio character's is its Voice in the studio
    personality: |
      You are Brannoc, the night guard at the city's north gate. Gruff, tired, not unkind.
      Speak one or two short sentences per turn. Never decide whether the player may pass:
      ask questions, react, and say what you think. The game decides.
  global_facts:
    public: ['The gate is closed at night by order of the council.']
    private: ['A courier with a sealed letter may pass at any hour.']
phases:
  questioning:
    description: 'The player wants to pass. Find out who they are and why.'
    tone: 'wary, curt'
    max_turns: 0 # 0: no limit. The game counts attempts, never the service.
    objectives:
      - id: claims-courier
        description: 'The player says they carry an urgent sealed letter.'
        completion:
          {
            type: player_state,
            condition: 'The player has said plainly that they carry a sealed letter for someone in the city.',
          }
        urgency: normal
        tactics: ['Ask who the letter is for.']
      - id: story-contradicted
        description: 'The player contradicts what they said earlier.'
        completion:
          {
            type: player_state,
            condition: 'The player has said something that contradicts an earlier answer.',
          }
        urgency: normal
  • Objectives are signals, not verdicts. A second model (the storyteller) reads each turn and marks objectives complete. It is a judgement, one turn behind, and it can be wrong: the game decides what a completed objective means.
  • max_turns: 0. A phase that fails after N turns would let a slow or failed service turn cost the player an attempt. The game counts attempts itself.
  • phases.<id>.triggers (condition, goto) move the story between phases; facts add knowledge per phase.

Faces ​

The service animates a face with the voice when it has a lip-sync model for the character. A character without one of its own gets Myra's model if its story says gender: female, Isaac's if gender: male (both ARKit-52 weights, which every studio head plays); with neither, it speaks with a still face, and the dev server's log says so. Another model for a character: gameable({ talk: { characters: { elora: { story: 'src/elora.story.yaml', a2f: '…' } } } }); other defaults: talk: { faces: { female: '…', male: '…' } }.

What the game hears ​

The page sends the game conversation-events on the entity the conversation was started on:

kindtext
inputwhat the player typed or said: the game decides whether to ask it
subtitleeach sentence the character says, as it is spoken
storythe story snapshot after a turn: read it with conversation.story(text)
statusconnecting, ready (a reply finished), thinking, speaking, closed
errorretry: … (ask again; the session is open, and it must not cost the player an attempt) or closed: …

conversation.story(text) returns { revision, currentPhase, completedObjectives, playerState, transitionCause }. Snapshots come after each turn and can lag a turn behind: act on a new revision, not on ready.

Publishing ​

gameable({ talk }) runs in vite dev and vite preview. A static build has no server: a published game needs a relay of its own, createConversationRelay from gameable/conversation/relay, with the key on that server.