読み上げ ( スクリーンリーダー )¶
読み上げについて¶
吉里吉里Z のゲーム画面は、OS のスクリーンリーダー ( Windows のナレーター、macOS の VoiceOver、Linux の Orca など ) から読めます。画面の部品の名前・役割・値・状態を OS のアクセシビリティ API ( UI Automation / NSAccessibility / AT-SPI ) へ渡すので、目の見えない人や見えにくい人が、読み上げを聞きながらキーボードで遊べます。
対応しているのはデスクトップ ( Windows / macOS / Linux ) の通常ビルドです。SDL3 版・Windows ネイティブ ( WINVER ) 版のどちらでも動きます。ブラウザ ( Web ) 版にはこの口がありません。
スクリーンリーダーに渡す内容は、次の 3 つから集まります。
| 出どころ | 何もしなくても読まれるか | 使う口 |
|---|---|---|
| Elements のダイアログ / パネル | 読まれる | 画面 JSON の "a11y" キーで補う |
| Layer に描いた UI ( 選択肢・メニュー・設定画面など ) | 読まれない | ElementsDialog.setGameA11y か ElementsDialog.a11yLayers |
| 任意の文 ( 本文・結果の通知など ) | — | ElementsDialog.announce |
スクリーンリーダーが繋がるまでは何もしません。繋がっていない間の負荷はほぼありません。
動く例はコアデモ src/core/data/a11y ( ギャラリーの「読み上げ ( スクリーンリーダー )」 ) にあります。
Elements のダイアログ¶
ダイアログ で出した画面は、そのまま読まれます。ボタンの文字・ラベル付きの行・グループの見出しなどから名前を、部品の種類から役割 ( ボタン・チェックボックス・スライダー… ) を、表示中の値から値を、本体が自動で決めます。入力欄は文字・単語・行の単位で読め、キャレットの移動も追えます。
自動で足りないところは、部品に "a11y" キーを足して補います。文字列なら名前、辞書なら詳しく指定します。
{ "type": "sprite_button", "id": "btn_save", "a11y": "セーブ" }
{ "type": "slider", "id": "vol_bgm",
"a11y": { "label_id": "cfg.bgm", "description_id": "cfg.bgm.help", "value_var": "vol_bgm_disp" } }
{ "type": "label", "text_var": "msg_body", "a11y": { "live": "polite" } }
{ "type": "image", "a11y": { "hidden": true } }
| キー | 意味 |
|---|---|
label / label_id |
名前 ( _id は文字列表から引くので、言語の切り替えに追従します ) |
description / description_id |
補足の説明 |
role |
役割の上書き ( "heading" / "status" など ) |
value_var |
表示する値を持つ変数の名前 |
live |
"polite" / "assertive"。text_var が変わると読み上げます |
hidden |
部品を子ごと読み上げから外します ( 飾りの画像など ) |
画面の最上位に "a11y": { "title": "…" } を置くと、画面に入ったときにその名前で読まれます。
絵だけのボタン ( sprite_button / atlas_* ) は文字を持たないので、"a11y" で名前を付けてください。
Layer に描いた UI を読ませる¶
Layer に描いた UI は Elements の外にあるので、何もしなければ読まれません。読ませる方法は 2 つあり、併用もできます。
| 方法 | 向いている場面 | 手間 |
|---|---|---|
| a11yLayers | Layer のフォーカス連鎖 ( focusable ) で操作する UI。ボタンを 1 枚ずつ Layer で作っている場合 |
小さい。真にするだけで載り、Layer にメンバを足して補う |
| setGameA11y | 1 枚の Layer に複数の項目を描いている UI ( 選択肢・一覧・メニュー ) や、フォーカスを自前で管理している UI | ノードの表をスクリプトで作って渡す |
フォーカス連鎖の Layer を自動で載せる ( a11yLayers )¶
ElementsDialog.a11yLayers = true;
これで、フォーカス連鎖に入っている Layer ( focusable かつ joinFocusChain ) が読み上げツリーに載ります。名前は hint、役割はボタンです。フォーカスは Window.focusedLayer に従います。
スクリーンリーダーで「押す」と、本体はその Layer にフォーカスを移してから Enter キーを送ります。Enter で押せる作りにしておけば、ほかに何も要りません。
読まれ方は、Layer にメンバを生やして変えます。
class AutoModeButton extends Layer
{
var a11yRole = "check_box"; // クラスで持たせるときは var で宣言する
var checked = false; // check_box は checked メンバから状態を読む
// ...
}
heading.a11yName = "セーブとロード"; // フォーカスできない見出しも載る
heading.a11yRole = "heading";
deco.a11yHidden = true; // 飾りは子ごと外す
| メンバ | 内容 |
|---|---|
a11yName / a11yRole |
名前 / 役割。どちらかがあれば、フォーカスできない Layer も載ります |
a11yValue / a11yDescription |
値 / 補足の説明 |
a11yStates |
配列か "checked,selected" の形の状態 |
a11yHidden |
真なら子ごと外します |
onA11yAction(action, arg) |
スクリーンリーダーからの操作を受けます。false を返すと既定の処理も行います |
値を持つ部品 ( スライダーなど ) の増減は、既定の処理がありません。onA11yAction で受けてください。
class VolumeSlider extends Layer
{
var a11yRole = "slider";
var value = 50;
property a11yValue { getter() { return value + "%"; } }
function onA11yAction(action, arg)
{
switch (action) {
case "increment": setValue(value + 10); return true;
case "decrement": setValue(value - 10); return true;
case "set_value": setValue(+arg); return true;
}
return false; // focus は既定の処理 ( focus() ) に任せる
}
// ...
}
読み直しは最大 150ms に 1 回です ( フォーカスが変わったときはすぐ )。a11yValue などを property にする場合は軽く保ってください。Layer の name が読み上げツリーの id ( layer:<name> ) になるので、付けておくと確認のときに指しやすくなります。
ノードの表を渡す ( setGameA11y )¶
選択肢のように、1 枚の Layer に複数の項目を描いている場合は、項目ごとのノードをスクリプトが作って渡します。
function pushChoices()
{
var nodes = [ %[ id:"choices", role:"list", name:"どうする?" ] ];
for (var i = 0; i < choices.count; i++) {
nodes.add(%[
id:"choice" + i, role:"list_item", name:choices[i], parent:"choices",
states:(i == current) ? "focusable,selected" : "focusable",
rect:[ 100, 400 + i * 60, 600, 48 ] // primary layer の座標
]);
}
// 第 2 引数はフォーカスを置くノード。選択肢を操作している間だけ渡す
ElementsDialog.setGameA11y(nodes, choiceLayer.focused ? "choice" + current : void);
}
ElementsDialog.onGameA11yAction = function(id, action, arg) {
var i = +id.substr(6); // "choice<N>"
if (action == "focus") { current = i; redraw(); pushChoices(); }
else if (action == "click") { current = i; decide(); }
} incontextof this;
- 表は呼ぶたびに丸ごと差し替わります。差分は本体が取るので、選択が変わるたびに全体を渡してかまいません。
- 本体はフォーカスや値を自分では動かしません。スクリーンリーダーから
focus/clickが来たら、ゲームの状態を変えてからsetGameA11yを呼び直します。 rectを付けると、スクリーンリーダーがその位置を枠で示します ( ナレーターの青い枠など )。ウィンドウの拡大縮小とレターボックスは本体が換算します。- 第 2 引数の
focusを渡している間は、スクリーンリーダーのフォーカスがそのノードに固定されます。a11yLayersと併用するときは、自前の UI を操作している間だけ渡してください。 - 自前で載せる Layer には
a11yHidden = trueを立て、a11yLayersの自動と二重に載らないようにします。 - ゲーム本体のノードはダイアログより下に並びます。モーダルなダイアログの表示中は隠れます。
使える役割・状態・キーの一覧は setGameA11y を参照してください。
任意の文を読ませる ( announce )¶
ElementsDialog.announce("セーブしました");
ElementsDialog.announce("体力が残りわずかです", true); // 割り込み
フォーカスの移動では読まれないこと ( 本文・操作の結果・状態の変化 ) を読ませます。最前面のダイアログか、ダイアログが無ければゲーム本体の読み上げ欄 ( live region ) に入ります。同じ文を続けて渡しても毎回読みます。
本文を読ませるかどうかを、スクリーンリーダーが繋がっているかで切り替える場合は、a11yActive を読む直前に見るか、onA11yActiveChanged で受けてください。Windows では、ウィンドウが前面になるまで接続済みになりません。
KAG3 には読み上げの拡張を入れていません。KAG 系の作品は、作品側のスクリプトでメッセージの本文を announce に渡し、選択肢を setGameA11y で載せて対応します。
確かめ方¶
- 実際に聞く: ゲームを起動してからナレーター ( Windows は Win + Ctrl + Enter ) などを起動し、ウィンドウを前面にします。
- REPL で見る:
-replfileなどで REPL を動かすと、スクリーンリーダー無しでも確かめられます ( REPL )。
| コマンド | 内容 |
|---|---|
.a11y |
読み上げツリー ( スクリーンリーダーに見えるもの ) を JSON で表示 ( Agent.a11yTree ) |
.a11ylog [N] |
読み上げログ。おおよそ何と読むかを 1 行ずつ ( Agent.a11yLog ) |
.a11ydo <node> <action> [arg] |
スクリーンリーダーと同じ経路で操作する ( Agent.a11yAction ) |
.say <text> |
announce |
読み上げログは次のような行になります。
[focus] 南へ行く, list item, selected
[polite] 「南へ行く」を選びました
[focus] 音量, slider, 50%
[value] 音量, 60%
注意点¶
setGameA11y/a11yLayers/onGameA11yActionなどはクラス全体に効く設定です。場面を抜けるときはclearGameA11y()/a11yLayers = falseで戻してください。- 対象はメインウィンドウだけです。
showModalJsonが開く専用ウィンドウと、2 枚目以降のWindowにはまだ対応していません。 - 入力欄の変換中 ( IME で確定する前 ) の文字は、アプリ側からは出しません。変換中の文字や候補は OS の IME 自体がスクリーンリーダーへ伝えます。入力欄が持つのは確定した文字だけです。
- ビルドオプション
KRKRZ_USE_A11Y( デスクトップの通常ビルドは既定 ON ) を OFF にすると、OS へは出なくなります。REPL での確認の口は残ります。 - 実行時に OS へ出したくない場合は
ElementsDialog.a11yMode = "off"にします。
仕様の全体と設計時の記録は アクセシビリティ ( スクリーンリーダー ) 対応 にあります。
関連 API¶
- ElementsDialog:
announce/a11yActive/onA11yActiveChanged/a11yMode/a11yLabel/setGameA11y/clearGameA11y/onGameA11yAction/a11yLayers - Layer: Layer に生やす読み上げ用のメンバ ( クラス説明 )
- Agent:
a11yTree/a11yLog/a11yAction