Skip to content

Accessibility

Games built on Aegis2D should be playable by people with motor, audio-visual or cognitive disabilities. This page lists, for each category, what the engine provides, why, and how a game uses it. Each option is checked by unit tests, and most also end to end on the real window or audio output.

At a glance

Category Need Option Engine Ticket
Motor Cannot reach or press some keys, other layout, switch device Remappable key bindings platform::ActionMap, platform::KeyCapture #51
Motor Cannot hold a key down Auto-fire platform::LatchedAction #57
Motor Cannot use a keyboard, plays with a gamepad or an adaptive controller Gamepad on the same actions, buttons remappable platform::Gamepad, platform::ActionMap #92
Audio-visual: hearing Hearing loss, tinnitus, sensitivity to sound Master, music and effects volumes audio::Volumes, audio::AudioDevice::set_volumes() #53
Audio-visual: hearing Deaf or hard of hearing, playing muted On-screen cues for important sounds render::SoundCues #56
Audio-visual: vision Color blindness, grayscale screen Colorblind-safe player colors with a shape marker render::player_styles, render::marker_image() #52
Audio-visual: vision Low vision, playing far from the screen Scalable HUD and menu text, 100% to 200% render::TextSize, render::flow_layout(), render::BitmapFont::wrap() #55
Audio-visual: vision Photosensitive epilepsy, motion sickness Option to turn off screen flashes and shake render::ScreenEffects, render::EffectSettings #54
Cognitive Remembering the controls, knowing what to do Controls reminder and goal on the connect screen platform::control_lines(), render::draw_controls() #58
Cognitive Fewer things to handle at once, settings that are never lost Auto-fire, defaults restored in one call, settings that never block the game see Cognitive

Settings

Every option the player changes goes to one settings file in their own folder (platform::settings_dir()), outside the game folder, so it is set once and survives restarts and updates. The game reads it at start with platform::read_settings(), which never fails: a missing file is a first start, and a broken one is logged and replaced at the next save. Each option keeps its default when its value is missing or invalid.

{
    "input": {
        "up": ["Up", "W"],
        "fire": ["Enter"]
    },
    "gamepad": {
        "fire": ["South", "RightShoulder"]
    },
    "audio": {
        "master": 1.0,
        "music": 0.3,
        "effects": 1.0
    },
    "accessibility": {
        "auto_fire": true,
        "text_size": 1.5,
        "sound_cues": true,
        "flashes": false,
        "shake": false
    }
}
const auto file = platform::settings_dir("Aegis2D", "RType") / "settings.json";
const auto settings = platform::read_settings(file);
actions.load(settings, "input");                       // key bindings
actions.load_buttons(settings, "gamepad");             // gamepad bindings
volumes.load(settings, "audio");                       // then audio.set_volumes(volumes)
auto_fire.load(settings, "accessibility.auto_fire");
text_size.load(settings, "accessibility.text_size");   // then text_size.apply(style)
cues.load(settings, "accessibility.sound_cues");
effects_choice.load(settings, "accessibility");        // then effects.set_settings(...)

// When the player changes an option in a menu:
auto updated = platform::read_settings(file);          // keeps the other options
volumes.save(updated, "audio");
updated.save(file);                                    // atomic: never half written

Motor

Remappable key bindings

In place (#51).

Fixed controls exclude players who cannot reach or hold some keys: one hand on the keyboard, limited finger movement, a single-switch device that emits one key, or a different keyboard layout. Every action can be bound to any key, and to several keys.

  • Gameplay reads actions (platform::ActionMap), never raw keys, so changing a key changes no gameplay code.
  • A settings menu binds the next key pressed with platform::KeyCapture ("Press a key for Fire"); Escape cancels. A key drives one action: binding it takes it from the other action, and the menu is told which one, to show it as unbound.
  • Keys follow the player's layout: on AZERTY, the key labeled Z is Z. Their names in the settings file are readable and can be edited by hand.
  • The bindings are written to the player's settings file at each change and read back at start. A missing, broken or partial file never blocks the game: unreadable entries keep the default keys, and actions added by an update get their default keys.
  • Restoring the default keys is one call (ActionMap::reset_all()).
{
    "input": {
        "up": ["Up", "W"],
        "fire": ["Enter"]
    }
}

aegis2d_example_key_bindings shows the whole loop (see Getting started).

Gamepad

In place (#92).

Some players cannot use a keyboard comfortably, or play with a controller made for them: the Xbox Adaptive Controller and similar devices present themselves as a gamepad. Every action can be triggered from a gamepad as well as from the keyboard.

  • The pad drives the same actions as the keys (platform::ActionMap): the D-pad or the left stick moves the ship, A (Cross on PlayStation, B on Nintendo pads) or the right trigger fires. Gameplay does not know which one the player uses, and both work at once.
  • Any button, trigger or stick direction can be bound to any action, as keys are (ActionMap::bind(action, GamepadButton)); a button drives one action. The buttons are saved in the player's settings file next to the keys, under their own section, and restored with the defaults by ActionMap::reset_all().
  • A stick direction counts once the stick is pushed half way, and is released only below a third of the way, so a trembling hand near the edge does not make the ship flicker between moving and stopping.
  • Any pad plugged in drives the player, even while the game runs; one unplugged releases its buttons, so the ship does not keep moving or firing.
  • Auto-fire (below) works with the pad too, and the controls reminder shows the buttons as printed on the player's pad ("A / RT", "Cross / R2").

aegis2d_example_ship played with a gamepad: the controls in the corner list the pad's
buttons, D-pad or left stick to move, A or RT to fire.

{
    "gamepad": {
        "up": ["DpadUp", "LeftStickUp"],
        "fire": ["South", "RightShoulder"]
    }
}

Auto-fire

In place (#57).

A shoot 'em up asks the player to hold the fire key for most of the game, which some players cannot do: limited strength or endurance, tremor, pain, or a switch or mouth stick that sends single presses.

  • With auto-fire on (platform::LatchedAction), one press of fire starts firing and the next press stops it; the weapon keeps its rate (four shots a second) meanwhile.
  • With auto-fire off, fire follows its key as usual.
  • It works with any key the player bound to fire (see above), and the game can release it when the ship dies or the game pauses.
  • The option is kept in the player's settings file, next to the other options.
{
    "accessibility": {
        "auto_fire": true
    }
}

Audio-visual

Hearing: master, music and effects volumes

In place (#53).

Players with partial hearing loss, tinnitus or sensitivity to sound need to set the music and the effects apart. Some need to hear the effects, which carry gameplay cues, over the music. Others play with the music off, or with everything quieter. A single volume cannot serve them all.

  • Three sliders (audio::Volumes): master, music and effects, from 0 to 1. The music and effects sliders change only their own channel, and the master slider changes both. Tests measure this on the real audio output.
  • Changes apply at once, to the sounds already playing too (AudioDevice::set_volumes()).
  • Equal slider steps sound like equal changes of loudness: the gain is the square of the slider (half way is about -12 dB).
  • The sliders are kept in the player's settings file next to the key bindings, and read back at start; a missing or invalid value keeps its default.
{
    "audio": {
        "master": 1.0,
        "music": 0.3,
        "effects": 1.0
    }
}

Hearing: on-screen cues for important sounds

In place (#56).

Sounds tell the player things they need to react to: an enemy coming in, a boss, low life. Deaf and hard-of-hearing players miss them, and so does anyone playing muted. With this option on, every important sound is also shown.

  • The game calls render::SoundCues::show() each time it plays an important sound, whatever the volume, with a short caption and, when the sound has one, its source. When the sound comes from a gameplay event (audio::EventSounds), the cue listens to the same event on the bus (events.subscribe<ecs::Explosion>(...)).
  • A cue with a source is drawn on the screen edge nearest to it, with an arrow toward it ("Enemy incoming >" on the right edge, at the enemy's height). A cue without one is a caption at the bottom center.
  • Cues fade out after about two seconds and never overlap; a sound played again refreshes its cue instead of stacking another. They sit on a dark backdrop and are drawn in the style the game gives, so they follow the player's text size (see below).
  • A test plays ten seconds of a stage with the volume at zero: each important sound stays on screen for about two seconds. End to end, the example records a silent output while the enemy, the boss and low life are all shown.
  • Off by default; the choice is kept in the player's settings file.

Cues while the sound is muted: an enemy coming from the right, a boss from above, and
low life.

Vision: colorblind-safe player colors with a shape marker

In place (#52).

About one man in twelve does not tell red from green well. Some players see no color at all, or play on a grayscale screen. A player who cannot tell their ship from a teammate's cannot play. So each player has a color and a shape (render::player_styles), and the shape tells the players apart even without any color.

Player Color Source Marker Lightness in grayscale (L*) Contrast on #0A0A1A
1 blue #0072B2 Okabe-Ito circle 46 3.8:1
2 red #EE6677 Paul Tol triangle 61 6.3:1
3 yellow #F0E442 Okabe-Ito square 89 14.8:1
4 cyan #66CCEE Paul Tol diamond 77 10.7:1
  • The colors come from palettes designed for color blindness. Any two stay apart (CIE76 ΔE of 30 or more) for typical vision and when simulated for protanopia, deuteranopia and tritanopia (render::simulate()).
  • In grayscale, their lightness differs by at least 10 L*, so the four players stay distinct.
  • On a dark background, each color has a contrast of at least 3:1, the WCAG minimum for graphical objects.
  • Unit tests check all of this, so a change of palette that breaks it fails the build.
  • The marker (render::marker_image()) is a white shape outlined in black. Tinted with the player's color, the outline keeps it readable on dark backgrounds and on explosions alike. The game draws it next to the ship and on the HUD, with the player's number written next to it (render::BitmapFont), as in aegis2d_example_hud.

The four markers, tinted, on a dark background. Rows: typical vision, protanopia,
deuteranopia, tritanopia, grayscale.

Rows from the top: typical vision, protanopia, deuteranopia, tritanopia and grayscale, simulated with render::simulate() from the engine's own marker images.

aegis2d_example_hud shows it live: F3 draws the whole HUD as seen with each color vision in turn, and --color-vision achromatopsia starts in grayscale. aegis2d_example_players does the same with four ships in play, each tinted with its player's color and followed by its marker and number.

Vision: scalable HUD and menu text

In place (#55).

Players with low vision, or playing on a television far from the sofa, cannot read text drawn for a desk monitor. They can make every HUD and menu text larger, from 100% to 200% by steps of 25%.

  • The game multiplies each text style by the player's size (render::TextSize::apply()).
  • Larger text must not overlap. HUD items are laid out with render::flow_layout(), which moves an item to the next row when it no longer fits. Long texts are wrapped to their width with render::BitmapFont::wrap().
  • A test lays out a four-player HUD (score, statistics, a panel per player and a menu paragraph) at every size from 100% to 200%. No two items overlap, every item stays on the screen, and every glyph stays in its item. At 150%, the big HUD text is 36 pixels high.
  • Text drawn over the game is easier to read on a dark backdrop, as in aegis2d_example_hud. The size is kept in the player's settings file.

The HUD example at 150%: the player panels moved to a second row instead of
overlapping.

Vision: option to turn off screen flashes and shake

In place (#54).

Flashing light can trigger seizures in players with photosensitive epilepsy, and a shaking screen causes motion sickness and is hard to follow for some players. Explosions use both, so the player can turn them off.

  • Flashes and shake go through render::ScreenEffects. With the option on (EffectSettings flashes and shake set to false), explosions ask for them but nothing is drawn and the camera does not move. Turning the option on during a flash stops it at once.
  • Even with flashes on, at most three flashes start in any second, the limit of WCAG 2.3.1. A burst of explosions cannot flash faster than that.
  • The choice is kept in the player's settings file and read back at start.
{
    "accessibility": {
        "flashes": false,
        "shake": false
    }
}

aegis2d_example_effects shows it: the screen flashes on an explosion, and stays dark once F1 has turned the option on.

Cognitive

Players with attention, memory or processing difficulties, and players who tire quickly, are helped by fewer things to handle at once and by settings that are never lost or confusing. The engine provides:

  • Auto-fire (see Motor): firing no longer needs a key held for the whole game, which leaves only the movement to handle.
  • Named actions with readable keys: actions have names (up, fire) and keys have plain names (Space, LeftShift, Keypad5, platform::key_name()), so a menu or a reminder can show the controls in words.
  • Settings that never block the game: a missing, broken or partial settings file gives the default options instead of an error, and each option falls back to its default on its own.
  • One call back to the defaults: ActionMap::reset_all() restores every key, so a player who is lost in their bindings can start again.
  • No sensory overload on demand: flashes and shake can be turned off (see Vision), and the music can be lowered or muted while the effects stay audible.

Controls reminder on the connect screen

In place (#58).

Before joining, a player sees the goal of the game and their controls, so nobody has to remember them or guess them. This helps players with memory or attention difficulties, new players, and players who changed their keys.

  • The controls come from the bindings in use (platform::control_lines()), so the screen lists the player's own keys after a remap, not the defaults: "Fire Z / Left Ctrl".
  • Keys have names in words ("Left Shift", "Keypad 5", platform::key_label()), and an action without a key says "not bound".
  • With a gamepad plugged in, a game lists the pad's buttons instead, as printed on that pad: "Fire A / RT" on an Xbox pad, "Cross / R2" on a PlayStation pad (platform::control_lines(actions, style)), as aegis2d_example_ship does.
  • render::draw_controls() draws them as a table: actions in one column, their keys aligned in the next. The table follows the player's text size, and the goal is wrapped to the screen.

The connect screen of aegis2d_example_connect, listing the player's own keys: up on Up
or W, fire on Z or Left Ctrl.

Captions for important sounds (see Hearing) also help players who do not catch what a sound means.