日本語を表示するために、フォント全体をそのまま積む必要はありませんでした。 Codexが作った3作品では、画面で使う文字を集め、Noto Sans JPを39,056〜74,144バイトのTTFに絞って同梱しました。
きっかけは、私が灯路を遊んで「何で表記英語なの、日本語にして」と指摘したことです。紹介ページは日本語でも、ゲームに入ると手数も操作も英語。軽量化のために残した表記を、公開後に直すことになりました。
ここでは固定文言のWebゲームを対象に、実際に使った生成スクリプトとUnity側の設定を載せます。自由入力の日本語やTextMeshProの設定は対象外です。

3作品で同梱したフォントは約39〜74KB
元にした可変フォントは9,589,900バイトでした。ウェイトを400に固定し、各ゲームのC#にある非ASCII文字と、空白を含むASCIIの32〜126を残しています。下の文字数はUnicodeコードポイントの数で、フォント内部のグリフ数とは区別しています。
| ゲーム | 収録文字 | TTF容量 |
|---|---|---|
| 灯路 | 271 | 74,144 bytes |
| リフレア | 201 | 52,700 bytes |
| ブロック崩し (Codex版) | 156 | 39,056 bytes |
これを「ゲームが9MB軽くなった」とは扱っていません。以前のゲームには元フォントを入れていなかったからです。灯路の配信4ファイルはv1.1の6,975,738バイトからv1.2の7,020,418バイトへ、44,680バイト増えました。日本語化を含む版同士の差であり、フォントだけを切り替えた比較ではありません。
KBと配信容量を分ける
この記事のKBは1,000バイトで概算しています。TTFは圧縮前、公開ゲームの数字はBrotli圧縮されたdata・wasm・frameworkとloaderの合計です。フォントのファイルサイズを、そのまま通信量の増分に足すことはできません。
サンプルを展開し、日本語フォントを生成する
日本語フォント生成サンプルZIPには、Pythonスクリプトと短いC#の文言例が入っています。Unityプロジェクト一式や元フォントは含めていません。
- 1
Pythonと配布ファイルを用意
Python 3.12.0で検証しました。ZIPを展開し、README.mdがあるフォルダで以下のコマンドを実行します。 - 2
原本とOFLを同じ場所へ
Google FontsのNoto Sans JPからNotoSansJP[wght].ttfとOFL.txtを取得。font-sourceフォルダを作り、TTFだけNotoSansJP.ttfへリネームします。 - 3
生成後に報告JSONを見る
Sample/Assets/ResourcesへTTF、OFL、info.jsonが出力されます。missingGlyphsが空配列か確認します。
フォント原本はGoogle Fontsの配布フォルダから取得します。サンプルで使う配置は次のとおりです。
README.md
subset-game-font.py
font-source/
NotoSansJP.ttf
OFL.txt
Sample/Assets/JapaneseUiText.cs
python -m pip install fonttools==4.63.0
python subset-game-font.py font-source/NotoSansJP.ttf Sample
ZIPを別フォルダへ展開して同じスクリプトを実行した結果です。短い文言例のため、3作品より収録文字数は少なくなっています。
"glyphCount": 116, "bytes": 24988, "missingGlyphs": [], "fonttools": "4.63.0"
JSONのglyphCountというキーは、このスクリプトでは収録対象のコードポイント数を保存しています。
自分のゲームへ適用するときは、最後のSampleを、AssetsがあるUnityプロジェクトのパスに置き換えます。同名の生成済みファイルは上書きされます。空白のあるパスは引用符で囲みます。
生成されるのはGameJapanese.ttf、GameJapanese-OFL.txt、GameJapanese-info.jsonの3ファイル。JSONには元ファイルのSHA256、収録文字数、TTF容量、欠落文字を残しています。今回使った原本のSHA256は次の値でした。
c2f3b4d463500a2ddcd3849cded1fceeb9fd6d1c32e6cbecd568453ba50fc68f
配布元が更新されれば同じ容量になるとは限りません。自分の結果では、自分が取得した原本のハッシュと生成条件を保存します。OFL本文を同梱し、改変したフォントのファミリー名はGameJapaneseへ変更しました。元の著作権情報は残しています。
抽出するのはC#内の文字。外部データまでは拾わない
今回の3作は文言をC#に書いていたため、抽出元をAssets配下のC#に揃えました。文字列リテラルだけを解析せず、コメント中の日本語も含めています。最小容量を追い切る方法ではありませんが、取りこぼしを減らしやすい範囲でした。
text = ''.join(p.read_text(encoding='utf-8-sig') for p in sources)
required = set(range(32, 127)) | {ord(c) for c in text if ord(c) > 127}
missing = required - set(font.getBestCmap())
if missing:
raise ValueError('Missing source glyphs: ' + repr(
''.join(chr(c) for c in sorted(missing))))
これは配布スクリプトの抜粋です。全文ではウェイト固定、サブセット生成、改名、保存後の文字対応の再照合まで行います。getBestCmap()で確認するのは文字に対応するグリフがあるかで、Unityで正しく描けるかの検査ではありません。
⚠️ この抽出方法だけでは拾えない文字
JSON・CSV・UXML、サーバーから受信する文言、プレイヤーが入力する名前は対象外です。C#にUnicodeエスケープで書いた文字も、その表記から実際の文字へ復元していません。該当するゲームは抽出元を追加する必要があります。文言を変えたら再生成し、収録されていない文字をそのまま公開しないようにします。
Unity側はUI方式ごとにフォントの設定先が違う
生成したTTFをAssets/Resources/GameJapanese.ttfに置き、Unityにインポートさせます。.metaもゲームのリポジトリへ保存しました。ファイルを置くだけで終わらず、文字を描く側へ渡します。
以下は3作の設定箇所を整理した組み込み用の断片です。textは既存のUGUI Text、rootはUIDocumentのrootVisualElementを指します。単独で起動するMonoBehaviourではありません。
// 共通。using UnityEngine;
var font = Resources.Load<Font>("GameJapanese");
if (font == null)
throw new System.InvalidOperationException("日本語フォントが見つかりません");
// UGUI(Codex版ブロック崩し)
// text: UnityEngine.UI.Text
text.font = font;
// UI Toolkit(リフレア)
// using UnityEngine.UIElements;
root.style.unityFontDefinition = FontDefinition.FromFont(font);
// IMGUI(灯路)。GUI.skinを使うのでOnGUI内で作成する。
var labelStyle = new GUIStyle(GUI.skin.label) { font = font };
GUI.Label(new Rect(20, 20, 300, 40), "灯台へ光を届ける", labelStyle);
IMGUIの実装ではスタイルを毎回作らず、初回に作って使い回しています。UI Toolkitではルートへ設定した後、子要素の表示も公開画面で確認しました。個別のスタイルが別フォントを指定していれば、その設定も確認が必要です。
TextMeshProのTextとUGUIのTextは別です。 今回の3方式のコードを、TextMeshProの設定手順として流用しないでください。
文字欠けだけでなく、全画面の収まりを見る
3作の生成時に、要求した文字が元フォントと生成後のフォントに存在することを確認しました。さらに公開Web版をPC幅と375px幅で操作し、文字と配置を見ています。スマホ幅はWindows上のChromeでの検証で、スマホ実機ではありません。
公開画面で確認したこと
説明を日本語にしたら、パドルと重なった
- 症状
- ブロック崩しの発射案内が、発射前のパドル付近に重なっていました。
- 原因
- 文字の収録検査では、画面内で他の要素と重なることまでは分かりません。
- 対処
- 説明の表示位置を上げて再ビルドし、公開版のPC・スマホ幅で確認しました。
エラー0でも、ゲームが始まっていなかった
- 症状
- ブラウザ検査の開始クリックが無視され、結果として撮った画像もタイトルのままでした。
- 原因
- ローダーの完了後にもUnityのスプラッシュが残り、その間にクリックしていました。
- 対処
- 開始前の待機を見直し、実際に開始・クリア・結果へ移った画像を確認しました。コンソールのエラー0だけでは合格にしません。
ゲーム本体のUnity検査は灯路84件、リフレア34件、Codex版ブロック崩し14件が成功しました。ただし、これらはフォント専用の検査件数ではありません。ブロック崩しのクリア・失敗画面は既存メソッドで状態を作ったUI検査で、人が全ステージを遊んだ結果とも分けています。
日本語が出ないときは、生成・参照・表示を順に調べる
| 症状 | 今回の手順で確認する場所 |
|---|---|
| 生成前に止まる | PythonからfontToolsをimportできるか、原本の隣にOFL.txtがあるか、Assets内にC#があるか。 |
| 文字不足で停止 | 要求文字が原本にあるか。絵文字などを文字として使う場合も確認する。 |
| 読み込みがnull | Resources内のファイル名と読み込み名を照合。読み込み名には.ttfを付けない。 |
| 一部の文字だけ消える | 文言追加後の再生成忘れ、外部ファイル由来の文字、別のフォント指定を調べる。 |
| 読めるが重なる | 文字サイズ、ボタン幅、画面比率と配置。フォント容量の問題とは分けて直す。 |
私はゲーム内の英語を、紹介ページの日本語だけで補えるとは考えないことにしました。今後は初版から日本語UIを作り、必要文字の生成と公開Web版の表示確認を制作手順へ入れています。