ゲームの文字が四角い箱(豆腐)になる原因 — フォントのグリフ欠落を実測で切り分ける
画面の文字が空の四角になっているとき、原因はほぼフォントです。そのコードポイントに対応する字形をフォントが持っていない、という一点だけの話で、文字列そのものは正しいままです。文字コードの設定を触る前に、画面ではなく文字列の値をログに出してください。期待どおりのコードポイントが入っていれば、データは正しく、描くものが無かっただけです。直す対象はフォントです。
切り分けは1分で終わります。時間がかかるのはその後で、どのフォントを積むか、サイズはどこまで許すか、アトラスにどの文字を焼くかという判断です。以下の数値は、cmapテーブルを読むスクリプトで手元のmacOSの10書体を実測した結果と、そこからの計算です。
豆腐・文字化け・置換文字(U+FFFD)を見分ける
3つはどれも文字が壊れて見える症状ですが、フォントが原因なのは最初の1つだけです。1行まるごと均一な四角ならその文字体系のカバー範囲がゼロ、ばらばらの文字だけ四角なら部分的な欠落で、現場ではこちらが多いです。
「設定」のコードポイントはU+8A2DとU+5B9Aです。UTF-8では e8 a8 ad e5 ae 9a、CP932では 90 dd 92 e8 になります。UTF-8のバイト列をCP932として読むと、險ュ螳 という並びに置換文字が続きます。読めそうな別の文字に化けた状態、いわゆる文字化けです。逆にCP932のバイト列をUTF-8として読むと U+FFFD、U+0752、U+FFFD になります。ほとんどのバイトがUTF-8として成立しないためです。どちらもデータが変わっています。フォントが原因のときは、コードポイントはU+8A2DとU+5B9Aのままです。
表示されている四角も、どこかのフォントのグリフです。Appleのプラットフォームでは多くの場合、LastResortという小さなシステムフォントが描いています。実測で2.5KB、グリフは7個。それでいてcmapのformat 13サブテーブルで、Unicodeの全コードポイント1,114,112個を主張します。同等のものを持たないゲームエンジンは、主フォントの.notdefをそのまま出すか、何も出しません。
症状 文字列の値 原因 ------------------------- ----------------------- ------------------------------ 空または枠だけの四角 コードポイントは正しい フォントにその字形が無い 読めるが違う文字 コードポイントが別物 別のコードページとして読まれた U+FFFD のひし形や四角 値にU+FFFDが入っている そのエンコーディングとして不正
フォントが「その字を持っている」の実測値
フォントはcmapというテーブルを持っています。コードポイントからグリフ番号への対応表です。cmapにコードポイントが無い、あるいはグリフ0に向いていれば、そのフォントは「この字は描けない」と宣言していることになります。OSのフォールバック処理やエンジンの照会APIが読んでいるのはまさにこの宣言なので、測る価値があるのはcmapのカバー範囲です。空白のグリフに向けることもできるので保証ではありませんが、cmapの穴は常に本物の穴です。
比較対象の文字集合は手打ちではなくコーデックから生成しました。右4列は括弧内の字数に対するカバー率、cmap収録はUnicodeサブテーブルのみの数、サイズはファイル全体で、複数書体を束ねたファイルのグリフ数は先頭の1書体分です。
この表で日本のチームに効くのは、日本語フォントと韓国語フォントの行です。日本語フォントはJIS X 0208の6,355字を100パーセント持っていますが、ハングル音節11,172字は1字も持っていません。中国語フォント2種も同じくゼロです。つまり韓国語対応は、日本語や中国語対応のついでには付いてきません。逆に韓国語フォントはハングルを11,172字すべて持ちながら、JIS X 0208は64.3パーセントしかありません。第1水準2,965字のうち522字、第2水準3,390字のうち1,749字が欠けており、欠けている第1水準の先頭は「亜」です。珍しい漢字だけの話ではない、ということです。
もう一つの罠が100パーセントという数字の中に隠れています。表の簡体字フォントはJIS X 0208を全字カバーしますが、同じコードポイントを大陸の字形で描きます。Unicodeでは漢字が地域ごとの字形を持つためです。カバレッジ検査は通り、レビューは通りません。カバレッジは下限で、目標ではありません。ドット絵など低解像度のUIで字形をどう保つかは 日本語と英語のピクセルフォント選び に詳しく書いてあります。
書体 書体数 サイズ グリフ cmap収録 かな(176) JIS(6,355) ハングル(11,172) GB2312(6,763) --------------------- ------ --------- ------- --------- ---------- ----------- ----------------- ------------- Arial 1 755.1 KB 3381 2792 0.0% 0.0% 0.0% 0.0% Menlo 4 2.1 MB 3157 2727 0.0% 0.0% 0.0% 0.0% Osaka 1 3.5 MB 8121 7318 98.9% 100.0% 0.0% 49.3% YuGothic Medium 1 10.5 MB 23060 15914 100.0% 100.0% 0.0% 66.9% Hiragino Sans GB W3 4 22.4 MB 29352 29318 100.0% 100.0% 0.0% 100.0% PingFang 24 74.6 MB 49533 33258 96.0% 99.6% 0.0% 99.7% Apple SD Gothic Neo 18 52.8 MB 18662 18067 96.0% 64.3% 100.0% 39.7% Apple Color Emoji 2 183.2 MB 3844 1469 0.0% 0.0% 0.0% 0.0% Arial Unicode MS 1 22.2 MB 50377 38917 98.9% 100.0% 100.0% 100.0% LastResort 1 2.5 KB 7 1114112 100.0% 100.0% 100.0% 100.0%
日本語フォントに差し替えると別の言語が壊れる
四角が並ぶと、UIフォントを大きなCJKフォントに丸ごと差し替えたくなります。実測はそれが別の豆腐と交換するだけだと言っています。表のOsakaには、トルコ語に必要なドット無しiとセディラ付きsのグリフがありません。ベトナム語のストローク付きd、ホーン付きu、ブレーブとドット下が付いたa、サーカムフレックスとアキュートが付いたe、ドット下付きuもありません。もう一方の日本語フォントもベトナム語の4字を欠き、簡体字フォントはさらにセディラ付きの大文字Cを欠いています。ラテン文字専用フォントはこれらを全部持っています。
サイズは増えますが、1書体ずつ比べると噂ほどではありません。ラテン文字フォントは3,381グリフで755KB。日本語の2書体は8,121グリフで3.5MB、23,060グリフで10.5MBでした。5倍から14倍で、100倍ではありません。極端なのはカラー絵文字フォントで、1,469コードポイントに対して183MBあります。複数サイズのビットマップ画像を持っているためです。絵文字をフォントとして同梱するかどうかは、インストールサイズの数字が付いた判断になります。文字列側の扱いは ゲームテキストの絵文字とサロゲートペア にまとめました。
つまり正解は1書体ではありません。文字体系ごとの主フォントと、順序を明示したフォールバックの一覧です。
エンジンのフォールバックは自分で組んで同梱する
ブラウザやOSのUIツールキットは文字体系ごとのフォールバック連鎖を持ち、主フォントにグリフが無ければ順に辿ります。ゲーム以外で豆腐をあまり見ないのはこれのおかげです。ゲームエンジンは独自のテキスト描画を持ち、初期状態はフォント1つ、フォールバックの一覧は空です。
Unityでは、TextMesh Proのフォントアセットがレンダリング済みグリフのアトラスを持ちます。フォールバックは二段で、フォントアセット個別のFallback Font Assetsを見た後にプロジェクト全体の設定側の一覧を見ます。生成モードと参照順は、TextMesh Pro公式ドキュメントのFont Asset Creatorとフォールバックの章で確認してください。
Unrealでは合成フォントが仕組みです。既定の書体に加えて文字範囲ごとにサブ書体を割り当てられるので、ラテン・日本語・韓国語をひとつのフォントアセットの中で別ファイルに向けられます。範囲の書き方と適用されるキャッシュモードは、公式ドキュメントの合成フォントの章にあります。
Godotではフォントリソースに Fallbacks の配列があり、エンジンが順に辿ります。配列と動的キャッシュの挙動は公式ドキュメントのフォントの章にあります。
3つとも、連鎖は同梱したものの範囲しか辿れません。制作機にはインストール済みだがビルドに入っていないフォントを指すフォールバックは、クリーンな検証端末でだけ四角になります。これがこの不具合がプレイヤーまで届く最も多い経路です。前後の設定は Unityのローカライズ全体像 と Unreal Engineのローカライズ全体像 を参照してください。
アトラスに焼く文字集合と、出荷前の検査
静的アトラスは選んだ文字集合から生成します。手を抜きたくなるのは「いまの翻訳ファイルに出てくる文字だけ」にすることで、プレイヤーが名前を入力するまで、チャットが開くまで、次のパッチが新しい文字を追加するまでは動きます。そのどれもQAを通ったビルドで四角になります。
焼く集合は勘ではなく実数で見積もります。下の表が言語別の必要字数と、アトラスの枚数計算です。重なりは期待より小さく、JIS X 0208とGB2312の共通はわずか3,330字しかありません。日本語対応済みのビルドに中国語を足すと、3,433字が純増します。テクスチャメモリに収まるかどうかを決めるのも、勘ではなくこの計算です。
出荷後まで持つ運用は決まっています。固定のUI文字集合は静的に焼き、プレイヤーが入力・受信しうる文字は動的アトラスに載せてフォールバックを後ろに置く。対象プラットフォームで動的生成が使えないなら、出荷する言語のレパートリー全体を焼きます。
文字列の値そのものが間違っていたなら、それは別の不具合です。文字化けの原因と直し方 から続けてください。日本語のテキストを最初から整える話は 英語ゲームの日本語ローカライズ にあります。
- 同梱フォントのcmapを、全言語の全文字列の文字集合と突き合わせ、欠けがあればビルドを失敗させる。描画は不要で、数十行で済みます。
- エンジン側の照会があるならそれも使う。ファイルだけでなくアトラスとフォールバックの一覧まで反映されるためです。TextMesh Proには描けない文字の一覧を返す呼び出しがあり、Godotのフォントには1文字ずつの照会があります。Unrealは合成フォントで宣言した範囲を確認します。
- 翻訳対象の文字だけでなく、プレイヤーが生む文字も対象にする。表示名やチャット、プラットフォームのアカウントから取る文字列は、どの文字体系が来てもおかしくありません。
- 開発用フォントが入っていないクリーンな端末で、出荷する全言語を確認する。複数地域の名前が並ぶランキングのように、文字体系が混ざる画面を重点的に見ます。
- 言語を追加するときは、翻訳が入る前に走らせる。この時点で見つかればフォントの選定で済み、審査で見つかればパッチになります。
事前生成が必要な字数 字数 --------------------------------------------- ------ JIS X 0208 第1+第2水準漢字 6,355 かな(ひらがな+カタカナ) 176 GB2312 漢字(簡体字) 6,763 Big5 漢字(繁体字、実測コーデックの受理数) 13,061 ハングル音節 U+AC00-D7A3 11,172 上記3系統の漢字の合計(重複を除く) 16,289 ASCII+かな+3系統+ハングルの合計 27,732 4096x4096のアトラス1枚に入るグリフ数 32px + 余白2px -> 120 x 120 = 14,400 (27,732字なら2枚) 48px + 余白2px -> 81 x 81 = 6,561 (27,732字なら5枚)