本当に使える!TextMeshProでの「日本語」「多言語」対応方法

はじめに

TextMeshProがUnityに標準搭載されてから、もう3年近く経とうとしています。
もう皆さん知ってると思うので、今更TextMeshProの利点をいちいち紹介したりはしません。
それでも「良いものなのは知ってるけど、日本語表示するのは大変なんでしょ?」「多言語対応ちゃんと出来るの?」という方、意外と多いんじゃないでしょうか。

ここでは、TextMeshProで日本語を表示する方法や、多言語対応する方法についてまとめます。
プレイヤー名表示やチャットなどのユーザーが自由入力する箇所も対応します。

アプローチ

先に、テラシュールブログさんのこちらの記事をお読みください。
基本的にはこちらをベースにしています。
もう丸2年前の情報なので最新のTextMeshProでは色々な問題が解決している & 多言語対応 & Addressableからフォントを読み出すという近代的な方法を紹介します。

やり方

FontAssetの準備

以下のFontAssetを準備します。
Padding: 2は文字を綺麗に出すためにアウトラインを犠牲にしています。
解像度は参考まで。

Base

  • Sampling Point Size: Auto Sizing
  • Padding: 2
  • Packing Method: Optimum
  • Atlas Resolution: 512 x 512
  • Character Set: Extended ASCII
  • Render Mode: SDFAA
  • Atlas Population Mode: Static

Language (厄介な各言語毎、ja, zh, ko, ...)

  • Sampling Point Size: Auto Sizing
  • Padding: 2
  • Packing Method: Optimum
  • Atlas Resolution: 2048 x 2048
  • Character Set: Characters from File
  • Render Mode: SDFAA
  • Atlas Population Mode: Static

Dynamic

  • Sampling Point Size: Custom Size (jaと合わせる)
  • Padding: 2
  • Packing Method: Optimum
  • Atlas Resolution: 1024 x 1024
  • Character Set: Unicode Range (Hex)
    • 何も指定しない
  • Render Mode: SDFAA
  • Atlas Population Mode: Dynamic
  • Multi Atlas Textures: true

ランタイムでは、1フォントにつきこの3枚をメモリに置いている状態になります。
256KB(512x512) + 1MB(1024x1024) + 4MB(2048x2048) = 5.25MBぐらいですね。
これをデカイと思うかどうかはハードやフォント数によって変わってきますが、そのときはテクスチャサイズを調整してください。

ちなみに、ここではDynamicは1Fontで1枚ですが、Dynamicも言語毎に分けても良いかもしれません。

Font Fallbackの設定

後はテラシュールブログさんの記事の通り、BaseのFont Fallbackとしてja、Dynamicの2つを指定することで日本語が使えます。
常用漢字に含まれていない、ゲームで使われがちな漢字として「杖」や「叩く」などがありますが、
これらもDynamicにより描画されます。

さて、「じゃあFont Fallbackにja, zh, koも突っ込めば多言語対応も完璧ですね!」と、そうはいきません。
当たり前ですがFont Fallbackに登録されているFontAssetは全てメモリに乗るので言語が増えるにつれてメモリ使用率も増えていきます。
これでは多言語対応出来てるとは言えません。

ランタイムでFont Fallbackを設定する

じゃあどうするか、ランタイムで必要なFontAssetだけ読み込んでFont Fallbackに設定しましょう。

ここからは具体的な名前がある方がやりやすいので、"Hoge"という名前のFontが使いたい、という体で進めます。
まずは全てのFontAssetのFont Fallbackを空にします。
次に、"Hoge Base", "Hoge Dynamic", "Hoge Language ja", "Hoge Language zh", "Hoge Language ko" など、それぞれのFontAssetをバラバラにAddressableに突っ込みます。
Hogeフォントを使用しているPrefab(仮に"TestWindow"とでも呼びましょう。)は、"Hoge Base"への参照を持っているようにしてください。
準備はここまで。

ランタイムでやることは

  • Addressableから"Hoge Base"を読み込む。
  • ゲームの言語設定を見てAddressableから"Hoge Language xx"を読み込み、"Hoge Base"のFont Fallbackに設定。
  • Addressableから"Hoge Dynamic"を読み込んで、"Hoge Base"のFont Fallbackに設定。
    • これは別に最初から設定してあっても良いが、EditorでDynamic使うと差分が出る問題に対応するためランタイムで処理している。(下記のQ&A参照)
  • AddressableからTestWindowを読み込むと、TestWindowが参照するHoge Baseは↑で設定したFont Fallback付きのものになっている。

具体的なコードはこちら。
しれっとUniRx, UniTaskが出てきます。

// Addressableから"Hoge Base"を読み込む。
var baseFontLoader = Addressables.LoadAssetAsync<TMP_FontAsset>("Hoge Base");
var baseFont = await baseFontLoader;

// ゲームの言語設定を見てAddressableから"Hoge Language xx"を読み込み、"Hoge Base"のFont Fallbackに設定。
var language = "ja";
var fallbackFontLoader = Addressables.LoadAssetAsync<TMP_FontAsset>($"Hoge Language {language}");
var fallbackFont = await fallbackFontLoader;
baseFont.fallbackFontAssetTable.Add(fallbackFont);

// Addressableから"Hoge Dynamic"を読み込んで、"Hoge Base"のFont Fallbackに設定。
var dynamicFontLoader = Addressables.LoadAssetAsync<TMP_FontAsset>("Hoge Dynamic");
var dynamicFont = await dynamicFontLoader;
baseFont.fallbackFontAssetTable.Add(dynamicFont);

// 日本語が出る!
var testWindow = await Addressables.InstantiateAsync<GameObject>("TestWindow");

お疲れさまでした。

Q&A

言語毎にFontAsset作らなくても全部Dynamicで良くない?

ランタイムにDynamicで文字を書くのはだいぶ重い処理です。
ハードの性能にもよるので具体的な数字は言いにくいですが、iOSでメニュー画面の文字を全部Dynamicで生成したりすると、メニュー画面を開くのに秒単位で時間がかかることがあります。

じゃあ逆にDynamicなしでJIS第二水準漢字ぐらいまで作っとけば良くない?

そういうアプローチの記事を見かけることもありますが、
1フォント毎に64MB食う巨大8kテクスチャを生成するのはオススメしかねます。

DynamicのMulti Atlas Texturesって?

1年前ぐらいに追加された機能で、↓の記事を見ると5秒で分かるのでぜひご覧ください。
【Unity】TextMesh Pro のダイナミックフォントでメインアトラスがいっぱいになったら追加のアトラステクスチャを自動で生成する方法

EditorでDynamic使ってると差分が出て他の人と衝突するんだけど?

わかる。
Editor時は動的にDynamicなFontAssetを生成するのがオススメです。

var dynamicFontLoader = Addressables.LoadAssetAsync<TMP_FontAsset>(font.DynamicFontAddress);
var dynamicFont = await dynamicFontLoader;
if (isEditor)
{
    // EditorでそのままDynamicFontを使うと上書きされてくので複製
    var source = dynamicFont;
    dynamicFont = TMP_FontAsset.CreateFontAsset(
        source.sourceFontFile,
        source.creationSettings.pointSize,
        source.atlasPadding,
        GlyphRenderMode.SDFAA,
        source.atlasWidth,
        source.atlasHeight,
        AtlasPopulationMode.Dynamic,
        source.isMultiAtlasTexturesEnabled);
}
baseFont.fallbackFontAssetTable.Add(dynamicFont);

EditorでBaseを参照してたら日本語使えなくない?

わかる。
Editorで作業するときはFontFallbackに全部突っ込んでおいてビルド時にスクリプトから剥がすと良いです。

EditorでランタイムにFontFallbackを指定したのにPlayを抜けても残ってるんだけど

Resources.UnloadAssetをちゃんと呼べば残りません。

var isEditor = Application.installMode == ApplicationInstallMode.Editor;
if (isEditor)
{
    var go = new GameObject($"{font} Disposer");
    GameObject.DontDestroyOnLoad(go);
    go.OnDestroyAsObservable().Subscribe(x =>
    {
        Resources.UnloadAsset(baseFont);
        Addressables.Release(baseFontLoader);
    });
}

なぜこんな変なisEditorのチェックの仕方をしているかはこちら。

Dynamicに追加された文字が知りたい

dynamicFont.ObserveEveryValueChanged(x => x.characterTable.Count)
    .Where(x => x > 0)
    .Subscribe(x =>
    {
        var chars = dynamicFont.characterTable
            .Select(y => Convert.ToChar(y.unicode));
        Debug.Log($"DynamicFont Update {font} {string.Join(",", chars)}"));
    });

重いのでEditorだけ or Developmentビルドのときだけにしときましょう。

スペシャルサンクス