Generating images

Generate images during gameplay with io.activities.generateImage(callId, params), which returns { mediaId } at dispatch while the image renders in the background. Render a mediaId with the built-in RenderMedia component (or useMedia for custom UI), store it in state or emit it as an event, and drive prompts from an illustrator agent. Covers the model prompting guide, idempotency, and pre-authored image assets.

Games generate images at runtime with io.activities.generateImage. The call returns at dispatch with a { mediaId } while the image renders in the background — a turn never blocks on the image model. The frontend renders the mediaId with RenderMedia, showing a placeholder until the bytes are ready.

generateImage

Call it during event processing (onEvent or a tool's execute). The first argument is a callId — a unique, descriptive name for the call site, like the first argument to agent.send.

ts
const { mediaId } = await io.activities.generateImage('scene-img', {
  prompt: 'A cozy tavern interior with warm firelight and a bard playing a lute',
  width: 1024,
  height: 1024,
})
emit({ type: 'illustration', mediaId })

Parameters:

| Field | Type | Default | Description | |-------|------|---------|-------------| | prompt | string | (required) | The whole instruction to the image model | | width | number | model default | Image width in pixels | | height | number | model default | Image height in pixels | | negativePrompt | string | — | What to avoid (model-dependent) | | model | 'flux' | 'gpt-image-2' | site default (flux) | Which image model to use | | referenceMediaIds | string[] | — | Prior generated images to use as visual references — see reference-images |

Returns { mediaId } — an opaque handle to the media item. Render it with RenderMedia / useMedia, store it in state, emit it as an event, or pass it as a reference to a later generation.

  • Idempotent. The mediaId is derived from the args, so re-calling with identical args returns the same mediaId at no extra charge — a revised prompt is a new generation with a new mediaId. Because of this, don't append a counter or timestamp to the prompt to "force" a fresh image; change the prompt meaningfully instead.
  • Errors surface on the media, not as a throw. Invalid arguments (e.g. a size the model doesn't support) throw at the call, but a generation that fails downstream (content filter, a failed reference) resolves the media to an error status — RenderMedia shows a broken-image state and useMedia reports error. No credits are charged for a failed generation.
  • Credits depend on the model and size, and each reference adds a surcharge. Don't put mediaIds in agent prompts — they're opaque handles, meaningless to an LLM.

Model prompting guide

flux (default):

  • Natural-language prompting — describe the scene in full sentences.
  • Example: "A cozy tavern interior with wooden beams, warm firelight, and a bard playing a lute in the corner".
  • Resolutions from 64×64 to 2048×2048, a multiple of 16 each side; aim for ~1MP (e.g. 1024×1024) for the best quality.
  • Negative prompts have no effect.
  • Supports referenceMediaIds.

gpt-image-2:

  • OpenAI-backed, natural-language prompting, exceptional quality — a good choice for hero shots and cover art.
  • Fixed sizes only: 1024×1024, 1024×1536, 1536×1024. Any other dimensions fail fast.
  • Slower and more expensive per image than flux; prefer flux for frequent in-turn generation.

Rendering a mediaId

RenderMedia is the drop-in way to display a generated image — it renders the <img> once ready, a pulsing placeholder while generating (and for a null mediaId), and a broken-image state on error. It behaves like an img and takes an optional alt and className.

tsx
import { RenderMedia } from '@aichatgames/sdk'

<RenderMedia media={output.mediaId} alt="The scene" />

For custom UI, subscribe to a mediaId's load state with useMedia:

tsx
import { useMedia } from '@aichatgames/sdk'

const { url, status, error } = useMedia(mediaId)
// status: 'loading' | 'generating' | 'done' | 'error'; url is present once 'done'

Image on state (persistent, mutable)

Store a mediaId in state so it persists across events and updates over time — a character portrait, a scene background, a map that changes as the state does.

ts
// State
interface State {
  portraitMediaId: string | null
}
initialState: { portraitMediaId: null }

// Backend — update the portrait
const { mediaId } = await io.activities.generateImage('portrait', { prompt })
state.set(draft => { draft.portraitMediaId = mediaId })

// Frontend — render from state (updates automatically when state changes)
{state.portraitMediaId && <RenderMedia media={state.portraitMediaId} alt="Portrait" />}

Image as event (inline chat image)

Emit a mediaId as an output event so it appears inline in the chat history — a one-off illustration that flows with the narrative rather than persisting in a panel.

ts
// Backend — emit the mediaId
emit({ type: 'illustration', mediaId })

// Frontend — render inline in chat
function Output({ output }: { output: OutputEvent }) {
  switch (output.type) {
    case 'illustration':
      return <RenderMedia media={output.mediaId} alt="Scene illustration" className="chat-image" />
    // ...
  }
}

Driving generation from the story: an illustrator agent

For AI-driven illustration, use a dedicated illustrator agent that follows the story and composes prompts on demand. Feed it story events with addMessage so it accumulates narrative context, then use transient: true + responseFormat sends to request a prompt without polluting its history — the caller runs generateImage and decides what to do with the mediaId.

upsert_text_asset({ name: "Illustrator Prompt", content: "You are an illustrator following along with a story.\nWhen asked to illustrate, compose a natural language image prompt capturing the scene.\nBe specific about composition, lighting, style, and mood." })
ts
const imagePromptSchema = z.object({
  prompt: z.string().describe('Natural language image description'),
})

io.agents.illustrator = new io.Agent('illustrator', {
  systemPromptAsset: 'Illustrator Prompt',
  model: 'fast',
})

// Keep the illustrator informed as the story progresses (persistent)
emit({ type: 'narration', text: narration })
io.agents.illustrator.addMessage({ role: 'user', content: `[Story] ${narration}` })

// Query for a prompt (transient — doesn't pollute the story context)
const { data } = await io.agents.illustrator.send('illustrate-scene', {
  message: { role: 'user', content: 'Illustrate the current scene.' },
  responseFormat: imagePromptSchema,
  transient: true,
})

// Caller controls generation — size, model, and destination
const { mediaId } = await io.activities.generateImage('scene', {
  prompt: data.prompt,
  width: 832,
  height: 1216,
})
state.set(draft => { draft.sceneMediaId = mediaId })

The illustrator's history stays clean — only story events accumulate, so compaction summarizes the narrative rather than a mix of story and prompt requests.

Pre-authored image assets

Game assets (import assets from '@game/assets') bundle pre-authored images — character portraits, backgrounds, UI graphics — that the game creator manages outside of code. Use assets for content that should be consistent across plays, and runtime generation for dynamic content. An image asset's .mediaId is string | null (null for an unfilled placeholder), and it renders and references exactly like a generated mediaId.

ts
import assets from '@game/assets'

const portrait = assets["Hero Portrait"].mediaId  // string | null
// Render it: <RenderMedia media={portrait} alt="Hero" />
// …or pass it as a generateImage reference (referenceMediaIds: [portrait]).

To keep a character visually consistent across many generated shots — the same face in new poses, or two characters sharing a frame — generate one canonical reference and pass it via referenceMediaIds. See reference-images.