Unityの標準的なテキスト描画コンポーネントであるTextMeshPro (TMP)では、日本語、中国語、韓国語(CJK)などの膨大な文字数をサポートするために、「Dynamic(動的)」設定のフォントアセットを使用することが一般的です。しかし、ゲームのローカライズが進み、長文や動的に変化するテキストを大量に表示させると、「ゲームをプレイし続けていると、突然文字が『□(豆腐)』に化けてしまう」「特定の漢字だけが空白や豆腐になる」というバグに直面することがあります。本記事では、この動的フォントアトラスの枯渇問題が発生する内部メカニズムと、最適化およびバグ回避の実装手順について詳しく解説します。

1. 根本原因:動的フォントアトラスのメモリ枯渇(Atlas Exhaustion)

TextMeshProの「Dynamic」フォントアセットは、ゲームの起動時にはアトラス(複数の文字画像を1枚にまとめたテクスチャ)に文字グリフを一切保持していません。その代わり、ゲーム実行中にテキストコンポーネントに文字列が代入されるたびに、以下のプロセスを内部で実行します。

  1. 表示するテキストに含まれる文字の中に、現在の「テクスチャアトラス」にまだ描き込まれていない文字があるかチェックします。
  2. 新しい文字が見つかった場合、OSまたはゲームに内蔵されたフォントソース(TTF/OTF)からグリフデータをロードします。
  3. ロードした文字画像を、メモリ上のフォントアトラス・テクスチャの空き領域に動的に描き込み(ラスタライズ)し、UV座標を登録します。
  4. 追加された文字は次回以降、このテクスチャから高速に描画されます。

この仕組みは非常に効率的ですが、フォントアトラスのテクスチャサイズには上限(デフォルトは512x512ピクセル)が設定されています。漢字のように文字の種類が非常に多い言語では、プレイ時間が長くなり様々なテキストが表示されるにつれて、アトラス上の空き領域が徐々に消費されていきます。

アトラスが満杯になり「新しい文字を描き込む隙間」が失われた瞬間、TextMeshProはそれ以降に登場した新規の文字を描画できず、フォールバックの未定義文字である「□(豆腐)」や空白として表示してしまいます。これが「動的アトラス枯渇バグ」の正体です。

2. 解決アプローチ①:アトラス解像度とパディングの最適化

まず行うべきは、アトラスの最大解像度を適切なサイズに広げ、アトラスのスペース効率を高めることです。

  1. プロジェクトウィンドウで対象の「TextMeshPro Font Asset」(Dynamic設定のもの)を選択します。
  2. インスペクターの「Generation Settings」を展開します。
  3. Atlas Width」と「Atlas Height」のサイズを変更します。デフォルトが512x512の場合、日本語や中国語を表示するには1024x1024、あるいは多言語対応であれば2048x2048に引き上げます。
  4. Padding」の値を調整します。デフォルトの `9` はアトラス内の文字同士の余白を十分に取りますが、高解像度のアトラスではスペースを圧迫します。解像度が1024以上の場合は `5` または `4` 程度まで下げることで、アトラスに収まる文字数を最大で約1.5倍に増やすことができます(※ただし、Paddingを下げすぎると文字の輪郭が滲んだり、画面を引いたときに隣の文字のピクセルが混ざる「アーティファクト」が発生することがあるため注意してください)。
アトラスサイズ 推奨 Padding 日本語漢字の最大格納数(目安)
512 x 512 9 約150〜300文字(ひらがな・カタカナ+少量の漢字で枯渇)
1024 x 1024 5 約800〜1,200文字(一般的なJIS第1水準漢字の一部をカバー)
2048 x 2048 5 約3,000〜4,000文字(日常会話やストーリーテキストの大部分をカバー)

3. 解決アプローチ②:常用文字の事前ベイク(Static / Dynamicのハイブリッド)

完全に動的な生成だけに頼ると、ゲーム開始直後にテキストを描画するたびに「文字のロードおよびラスタライズ処理」がCPU側で走り、フレームレートのスパイク(カクつき)の原因にもなります。これを防ぐため、よく使われる文字はあらかじめアトラスにベイク(静的化)しておく「ハイブリッド方式」を採用します。

  1. TMPの「Font Asset Creator」を開きます(`Window > TextMeshPro > Font Asset Creator`)。
  2. フォントソースを選択し、「Character Set」を「Custom Characters」に設定します。
  3. テキストボックスに、ひらがな、カタカナ、英数字、および常用漢字(教育漢字など約1,000文字)を入力し、アセットを生成します。
  4. 生成したフォントアセットを選択し、インスペクターで「Atlas Population Mode」を「Dynamic」に設定します。

この設定により、ゲームの初期起動時には常用漢字やひらがながあらかじめ高画質なSDFテクスチャとしてアトラスにベイクされており、プレイヤー名やチャット、めったに使われない難読漢字が入力されたときのみ、アトラスの余白エリアに動的に追加されるようになります。これにより、メモリの枯渇確率とパフォーマンスのスパイクの両方を劇的に低減できます。

4. 解決アプローチ③:C#スクリプトによるアトラス満杯イベントの検知と自動リセット

どれだけ最適化を施しても、ユーザーが数時間プレイを続け、無制限にチャットや多言語会話を入力するような環境では、いずれアトラスが満杯になるリスクを完全にゼロにすることはできません。そのため、「アトラスが満杯になったことをプログラムで検知し、安全にアトラスをクリアして再起動する」という二重のセーフティネットを実装します。

TextMeshProはアトラスの空き容量がなくなって文字の追加に失敗した際、TMPro_EventManager.fontTextureRebuilt などのイベントをトリガーします。これを利用し、アトラス内の文字数が上限値に近づいたか、あるいは再構築の失敗を検知してアトラスを自動リセットするマネージャーコードを作成します。

using UnityEngine;
using TMPro;

public class TMPFontAtlasManager : MonoBehaviour
{
    public TMP_FontAsset targetFontAsset;
    [Tooltip("アトラスに格納されている文字数がこの値を超えたら、次回ロード画面等でクリアする")]
    public int warningCharacterCount = 2500;

    void OnEnable()
    {
        // TextMeshProのフォントアセット変更イベントを購読
        TMPro_EventManager.FONT_PROPERTY_WAS_CHANGED.AddListener(OnFontPropertyChanged);
    }

    void OnDisable()
    {
        TMPro_EventManager.FONT_PROPERTY_WAS_CHANGED.RemoveListener(OnFontPropertyChanged);
    }

    private void OnFontPropertyChanged(bool isChanged, Object obj)
    {
        // 変更されたアセットが対象のフォントアセットか確認
        if (obj == targetFontAsset)
        {
            CheckAtlasStatus();
        }
    }

    public void CheckAtlasStatus()
    {
        if (targetFontAsset == null) return;

        // 現在動的に追加されている文字(Glyph)の数を確認
        int currentGlyphCount = targetFontAsset.glyphTable.Count;
        
        // デバッグログでテクスチャアトラスの状況を監視
        Debug.Log($"[TMP Font Atlas] Current Glyphs in Atlas: {currentGlyphCount} / {targetFontAsset.atlasWidth}x{targetFontAsset.atlasHeight}");

        if (currentGlyphCount > warningCharacterCount)
        {
            Debug.LogWarning("[TMP Font Atlas] アトラスの格納文字数が閾値を超えました。アトラスをクリアしてリセットします。");
            ClearDynamicAtlas();
        }
    }

    /// <summary>
    /// 動的アトラスの文字キャッシュを全消去してリセットする
    /// ※この処理を実行すると、画面上のテキストが一瞬再描画されます。
    /// シーン遷移時やロード画面などのタイミングで呼び出すことを推奨します。
    /// </summary>
    public void ClearDynamicAtlas()
    {
        if (targetFontAsset == null || targetFontAsset.atlasPopulationMode != AtlasPopulationMode.Dynamic) return;

        // 動的キャッシュのクリアを実行
        targetFontAsset.ClearFontAssetData(true);
        
        // 画面上の全TMPテキストコンポーネントに変更を通知して再描画を促す
        TMPro_EventManager.ON_FONT_PROPERTY_CHANGED(true, targetFontAsset);
        
        Debug.Log("[TMP Font Atlas] 動的フォントアトラスのキャッシュが正常にリセットされました。");
    }
}

このマネージャーをゲームに組み込むことで、アトラスの文字数が設定した閾値を超えた場合に、シーン切り替えなどの裏で ClearDynamicAtlas() を実行させることが可能になり、プレイヤーが画面上で豆腐バグを目撃する前に問題を完全に隠蔽・解決することができます。