Docs Design

Contents

The game's own accessibility options

The October audit's Wave 4 item "Localization and the game's own accessibility options", part two (docs/design/audit-2026-10.md). Before it, the Options page had volumes, text size (to 200%, XAG 101), screen shake and flashes. A person who can't hear had no way to know a beast had snorted behind them. A colour-blind person had no help telling a red from a green. Nobody could move a key.

Three things close the audit's gap, each one a guideline that players with disabilities rank among the most important (the Game Accessibility Guidelines; Xbox Accessibility Guidelines 104 and 107):

  • Captions for sounds. Captions show the sounds that matter, not only speech, and say where each came from.
  • A colour filter. It moves apart the colours each kind of colour blindness confuses.
  • Controls. A person can put any of the game's keys where they need it.

They are the person's options, like the others: kept on their device, never in the simulation, so no state hash, save, replay or trace sees them.

Captions

  • A Sound may carry a caption, a few words for what it is: beast snorts, bell rings. The words go in without brackets, and the player adds them. Caption the sounds that tell the player something they could miss, warnings above all (a creature's tell before it strikes, a sentry's alarm). Leave out the player's own steps, jumps and clicks: captioning those is noise, as broadcast captioning guidance says. Loops and beds are never captioned.
  • The menu offers Captions (off, on) in a game where any Sound has a caption. Off is the default. Once they are on, two rows follow it (and go when they are off, so the page stays short for everyone else): Caption size, 100%, 150% or 200% of the game's text (the person's Text size and all, whole pixels of the face), and Caption background, how solid the strip behind each is: 100% (the slate's glass, as before), 50% or 0%. Under 100% the words are outlined in the glass's ink a pixel of the face out each way, so they stand out from whatever is behind (the Game Accessibility Guidelines and XAG 104 ask for both choices). Godot's own outline draws nothing for a pixel face, which L3 caught: "[thud]" stands 2.0:1 from what is behind it.
  • kit.audio says a caption when its cue is asked for, inside the tick ([GwSoundCaptions]), not when it plays. So a caption follows the game's events, and a sound not baked in time to play is still captioned. A sound too far away to be heard is not captioned.
  • A caption up on screen reads ← [beast snorts]. The arrow points to the side of the frame the sound came from when it is more than 3 m from the listener. The side is worked out from the camera's turn, so it is right in a top-down 2D world, an isometric view and a chase camera alike. Captions stay up for 3 seconds of the game's time, at most three at once, newest at the bottom. A caption said again while it is up comes up anew instead of twice.
  • kit.ui draws them over the HUD and under the menu ([GwUiCaptions]). Each line is bright text on a strip of the slate's glass, low in the middle of the frame, in the slate's face at the UI's pitch, so the person's Text size grows captions too. They stack up from above any of the game's own text they would cover (a HUD message grown to 200% sits where they would), as TV captions move off a name on screen. When they can't all fit between the game's text above and below (big captions on a phone, between Thornwick's quest line and its message), the oldest give way, never the newest; L3 caught them over the quest line first.
  • Music is captioned too. A Score's section may carry a caption ("tense music"), said as "♪ tense music ♪" when the game turns to that section. kit.music reads which section the game asks for once a tick from its state ([GwMusicView] ticked), never from the audio clock, so a shot shows it the same whatever the bake did, and it is said once per turn, before the music reaches its bar line. Caption a change that tells the player something: Thornwick's and Tideline's danger music, Saltwell's fight and its lull. A score that never changes needs none.
  • Captions are text, so a Locale translates them (snd.beast_tell:caption, score.thornwick:sections/danger/caption). Their letters are checked like any text's; kit.ui's face draws ♪.

Colour filter

  • The menu offers Colour filter: off, Protanopia, Deuteranopia, Tritanopia (red-, green- and blue-blindness). These are the names players look for in a game's options.
  • A filter is a layer over everything the stage draws ([GwColourFilter], layer 128): the world, the HUD, the menu. It daltonizes (Fidaner, Lin and Ozguven):
    1. Work out the frame as that person sees it, using Machado, Oliveira and Fernandes (2009) at full severity, in linear light.
    2. Shift what they miss of each colour into the channels they do see: green and blue for red- and green-blindness, red and green for blue-blindness. Grey loses nothing and is drawn as it was. Each kind's commonly confused pair ends up three to five times further apart as that person sees it (the engine test measures this).
  • The filter reads the frame drawn under it through the screen texture. The Compatibility renderer copies it on the desktop and on the web alike, and a shot or tour of a stage with a filter shows it.
  • A filter helps, but it does not replace good design: a game should never carry meaning in colour alone. The playbook says so.

Controls

  • The menu offers Controls where the game has controls to change and the person has a keyboard or a pad (not a phone's touch alone). It opens a page with a row for each action a person uses in play ([GwControls]):
    • A button action is one row.
    • A stick is four rows, Move ↑, Move ←, Move ↓, Move →, one per direction of its keys.
    • Actions only menus read (menu: true) are left out: the menu and the slate have keys of their own.
    • A row is called by the action's name (InputMap actions[].name, translatable), else by its id as words.
  • A row shows what is bound on the device the person used last, as a game's prompts follow the hands: the key as their layout prints it, the pad's button (A, D-pad →, Right stick), or the mouse button (Left click); the key when the action has nothing on that device. All three side by side cut the action's name on the options list.
  • OK on a row waits, and whatever is pressed next is the row's new one on its own device: a key (taken by where it sits on the keyboard, as the game's own keys are), a pad's button or trigger, or a mouse button. On a stick's row, moving a pad's stick hands that stick to the action, so a left-handed person swaps move and aim. A row ignores what it can't take (a button on a stick's row, the mouse on a stick's) and goes on waiting. Esc or the pad's Start keeps the old one: they are the menu's own and are never taken.
  • A row changes its action's first key (a stick direction's first of its four), first pad button or first mouse button. It keeps the others. If another row had the new one as its first, that row gets the old one in exchange, so no two rows share a key or a button; a row that had nothing on that device to give leaves the other with none there. The same exchange happens with a later key of the same action. Other actions' later keys stay as they are, because a game may mean them to be shared (confirm on E, like use).
  • Reset controls puts every key and button back as the game binds them.
  • The person's own are kept as each changed action's whole list on its device (keys, pad, mouse in the options file). They are put over the InputMap whenever it is installed (GwGameOptions.keyed), and installed again as one changes. The game hears actions, never keys, so tours, replays and bots are untouched. Key hints already read the live InputMap, so a hint names the new key at once, and while a pad is in hand they name its buttons ([A] use, docs/design/touch-controls.md §8).

Tool runs

A tool run plays at the defaults, so its frames are the same on every machine. Given a person's options (--options FILE, an Options file as the player keeps one), it is drawn as that person sees it:

echo '{"kind":"Options","captions":100,"colourFilter":2}' > /tmp/o.json
node core/src/cli/main.ts tour --tour playtests/look.tour --options /tmp/o.json --out shots/ pack
node core/src/cli/main.ts shot --options /tmp/o.json --out f.png pack

The options are presentation only, so the run ends on the same state hash. Colour filter's levels are 1 for protanopia, 2 for deuteranopia and 3 for tritanopia.

A tool run can also be drawn through a colour-blind person's eyes: --sees deuteranopia (or protanopia, tritanopia) puts the same Machado simulation the filter starts from over the whole frame, after any filter the options turn on. It is never a person's option; it is how an author, or a check, looks at the game as they do.

node core/src/cli/main.ts shot --sees deuteranopia --out f.png pack

Checking a game as a person sees it

A Playtest names the person its frames are drawn for, and L3 then checks what they can read and tell apart:

{ "kind": "Playtest", "id": "test.look_low_vision", "tour": "playtests/look.tour",
  "options": { "textSize": 200, "captions": true }, "seed": 11, "ticks": 60000 }
{ "kind": "Playtest", "id": "test.look_deutan", "tour": "playtests/look.tour",
  "sees": "deuteranopia", "seed": 11, "ticks": 60000 }
  • options are the Options page's: textSize (100, 125, 150, 200), captions, captionSize (100, 150, 200), captionBackground (0, 50, 100), colourFilter (a kind), shake (0, 50, 100), flashes. L3 writes them as an Options file beside the run's tour and gives the player --options, on the monitor and on the phone. Text at 200% must still fit, whole and over nothing (text_fits), as every frame's text must.
  • Captions are text like any other: a shot reports each line (screen captions, widget line1…), so a caption cut short or too small is red. A Playtest with captions on in a game whose Sounds have none is warned (playtest.captions_unheard).
  • sees draws the frames as that person sees them. readable then measures each subject's colour difference from the ground round it by their eyes, and text_contrast each text's: a red foe on green grass that reads by hue alone goes red for deuteranopia. With options.colourFilter as well, it checks that the filter keeps the two apart.
  • Each frame carries person, and a red one's reason starts with who could not read it (seen with deuteranopia; text 200%, captions on: readable: …).
  • Options and sees change only the frames, so a Playtest that takes no shot (a bot, the explorer, steps with no shot) is refused (playtest.person_unseen).

Every game, through colour-blind eyes

Without a Playtest asking, L3 measures every subject in every frame as each kind sees it, from the pixels it already has: the readable check's ΔE from the ring round it, with the frame put through the same Machado matrices (seenBy on the check, core/src/verify/colour-blind.ts). A subject that reads for everyone by its body, falls under the need (10) for a kind and has lost at least a third of everyone's ΔE to it melts into its ground for that person: it was told apart by hue alone. (Tideline's keeper at dusk, 10.8 for everyone and 9.8 for protanopia, barely reads for anyone; colour blindness isn't what loses it.) That is a signal, not a failure, since a person with the filter on may still tell it apart. For example:

L3 colour: thornback.0 melts into the ground round it for deuteranopia (noticed, tell: ΔE 4.1
through their eyes, 31.5 through everyone's; needs 10). Give it a lightness of its own, an outline
or a shape that tells it apart, then a Playtest with "sees": "deuteranopia" keeps it so

A run's digest carries its signals, so the Director hears of a foe that reads by hue alone in the turn that made it. On the samples none does: Thornwick's closest is Odo at his forge, 21.8 for everyone and 14.7 through green-blind eyes.

Seeing it in the Studio

Game ▸ See it as draws the game playing in the Studio through red-blind, green-blind or blue-blind eyes, or everyone's: at once on the game playing, and on each game played after it, in the middle and in a window of its own (the player's --sees). An author looks at their game as those players will.

Text at 200% on a phone held upright

A Playtest naming that person found it at once: on Thornwick, 15 of 17 phone frames cut a HUD line ("Find the bell at the shrine i…") or ran one into another, where the monitor's frames all fit. kit.ui's text now reflows as it grows and a talk's box grows to hold its words (docs/design/phone-text.md, "Compact layout"), which left 8; the HUD's quieter lines yield (its quest line, errand and eat hint give way to a message while there's no room for both), which left 1: a choice's words, its three answers and its hints, where the answers' pane covered the question's first lines. On a phone the answers now join the question in its box, under its words and over its hints, and all 17 phone frames are green.

Not yet

  • Fog Signal at 200% on a phone held upright: 3 of its 8 phone frames are red (5 were, before the answers joined their question and a list's long row wrapped). The lamp room's question is seven lines, too long to leave room for an answer under it: its words need to page or scroll. The title's line about the game runs into "read on", and the ending's words run under "THE END".

From docs/design/accessibility.md in the Gamewright repository.