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.
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
errorstatus —RenderMediashows a broken-image state anduseMediareportserror. 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; preferfluxfor 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.
import { RenderMedia } from '@aichatgames/sdk'
<RenderMedia media={output.mediaId} alt="The scene" />For custom UI, subscribe to a mediaId's load state with useMedia:
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.
// 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.
// 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." })
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.
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.