本記事の内容
・TextMeshPro容量ドカ食い問題
・問題の解決策
TextMeshProとは?
こちらで解説。このブログのアクセス数の一番多い記事はTextMeshPro関連だったりもします。


TextMeshProを使うときの悩み事
多くのUnity初心者にとって関門となるTextMeshPro。
TextMeshProを使うと、テキストにいろんな装飾をつけたり、グラデーションなどがカンタンに行えます。
使い方がわかれば表現力が大いに上がるものの、日本語がすぐに使えなかったり、容量をどか食いしたりと、いろいろと使い方に試行錯誤をしている人は多いと思います。
特に、TextMeshProはあらかじめ文字の情報をテクスチャデータとして焼き付けておくため、TextMeshProを使うと、そのフォントデータ分容量が増えてしまうという課題があります。

容量を細かく気にする必要のないPC向けゲーム等であれば神経質になる必要はないのですが、モバイル向けアプリや、WebGLでのゲームの場合は容量を厳しくチェックしていく必要があります。
ビルド時に、容量を一番食っているのがTextMeshProで作ったフォントデータだった、ということがある人も多くいるのではないでしょうか?

そこで、今回は、TextMeshProのビルド容量を可能な限り削減する、個人的最適解についてまとめました。
※ビルド容量の内訳を確認する方法は以下で解説しています

まず、結論
1.最低限のフォントだけを static データ(事前にテクスチャとして焼く)にして、それ以外をDynamicフォントに設定!
2.さらなる削減を求めるなら、大本のフォントデータ(.ttf)は、Addressablesでリモートからロード!
なぜTextMeshProは容量をドカ食いするのか?
TextMeshProは、フォントデータをそのまま使用するのではなく、あらかじめ文字ごとのデータを画像データとして事前に焼いておき(この画像データを以後Atlasと読んでます)、Atlasから必要な文字データを取ってきて表示しています。
※詳しい解説はこのページが参考になります。
文字数の多い日本語でTextMeshProのAtlasデータを作ろうとすると、巨大なサイズのデータが必要となります。
日本語は非常に文字数の多い言語で、あらゆる漢字に対応しようとすると、9000文字近い文字データが必要になります。英語だけなら記号を入れても数百字程度なのに...
これらを全部1つのテクスチャに収めようとすると、8192*4096(50MBオーバー)以上の巨大テクスチャを作る必要があります。


この容量では、初回ダウンロードまでの時間を出来るだけ短縮したいモバイルゲームや、都度ブラウザ側でロードが必要となるWebGLのゲームでは大分厳しい数値です。
対策1:使用する文字をあらかじめ絞る
一番手っ取り早くオーソドックスな対策は、使用する文字をあらかじめ絞ってstatic(静的な)フォントのAtlasデータを作ることです。
ユーザの入力が不要なゲームや、難しいフォントが必要にならないゲーム(シンプルなアクションゲームなど)では、あらかじめ使用する文字絞って難しい漢字はひらがなで表示することを割り切ってしまう、という手です。
その場合どこまでの文字を入れたらいいの?という人のために、以下のリンクで、最低限必要な日本語の文字の一覧をまとめています。

TextMeshProのFont Asset Creatorを使って、上記のリンクにあるような一部の日本語に絞ったフォントのAtlasを作るというのが一番カンタンでわかりやすい対処です。

対策2:Dynamicフォントを使う
ですが、漢字も含めてユーザ名を自由に入力できるようにしたい、チャットシステムを導入したい!などの場合、上記のようなあらかじめ文字数を絞る対応では、対応文字が足りずに困ってしまいます。
ですが、日本語全部の漢字を含めたら容量は爆増してしまう!そんなときのためにあるのが、Dynamicフォントです。
対策1で紹介した、TextMeshProのFontAssetCreatorから、あらかじめ使用する文字種を決めて、フォントのAtlasデータを作っておくのは、staticなTextMeshProのAtlasデータの作り方で、これがデフォルトです。

一方、大本のフォントファイル(拡張子が.ttfなどのファイル)から、ゲーム内で動的にフォント用のテクスチャデータを作る方法がDynamicです。
先日、Unityの公式アカウントでも方法が公開されていたので、改めて私がやり方を書くのもおこがましいのですが、一応書いておきます
やり方としては、大本のフォントを右クリックで選択し、Create > TextMeshPro > FontAssets > SDF
を選択するだけです。

出来上がったTextMeshProのファイルをインスペクタで確認すると、ModeがDynamicになっていることが確認出来ます。

じゃあフォントは全部Dynamicにして、staticのTextMeshProのデータ使わなければいいじゃん、と思う方もいるかもしれません。
まあ、ぶっちゃけそれでも、困らないケースも多いと思います(おわり)

ただ、Dynamicフォントだけに頼る場合、ゲーム内で文字を表示する都度、裏側でフォントを作る処理が常に動いていることになるので、やはり処理負荷がそこそこ高いです。
そこで、基本の文字までは処理が軽いstaticフォントを使用し、珍しい漢字が来たらDynamicフォントを使う、という切り替えができるのが望ましいです。
これを設定するには、最低限の日本語を入れたstaticフォントに対して、Fallbackとして、Dynamicフォントを入れることで対応できます。
例えば、以下のようにstaticフォントとDynamicフォントの二種類を作っておき、

staticフォントを選択してインスペクタから、FallbackFontAssetsに、Dynamicフォントを登録することでこのような構成にすることが出来ます。
私のプロジェクトでは、第一常用漢字までをstaticフォントとして作っておくことが多いです。



また、Dynamicフォントを使用する場合、大本の.ttfファイルをビルドに含める必要があります。
.ttfファイルが存在しない場合、当然ながらAtlasに焼くための元データがないので、フォントが表示されなくなってしまいます。
.ttfファイルは、TextMeshProのAtlasデータに比べれば遥かに軽量ではあるのですが、それでも1-3MBほどの容量が必要になります。
なので、更にもっと容量を削減したい場合、大本の.ttfフォントデータをAddressablesを使ってリモートサーバからロードしようぜ、というのが最後の話です。
対策3:Addressables を使って大本の.ttfデータをサーバからロードする
Unityでは、Addressablesという強力なアセット管理システムがあり、サーバなどからデータをロードしてゲーム内にとりこむことが出来ます。
詳しくはこちらで。

そのため、ゲーム起動後に、フォントの大本の.ttfファイルをリモートからロードすれば、.ttfファイルもビルド対象から除くことが出来ます。
ただ、仮にロードが失敗したときに、エラーメッセージなどにも、フォントがまったく表示されなくなってしまうのは困るので
ビルドに含めるのは
最低限の日本語を含んだstaticのTextMeshProフォントAltasデータ
リモートサーバから、初回起動時に.ttfファイルとDynamicフォントを端末にロード。
ロード後に、staticのTextMeshProのフォントのフォールバックにDynamicフォントを設定
という構成がビルド容量削減においては最適解、というのが私なりの考えです。
実際にこれを組んでみました。
Addressablesのバンドルの中身は以下のような感じ。
Dynamicフォントと、ttfファイルをビルドに含めて、リモートサーバに配置しています。

Addressablesからフォントをロードするコードはこんな感じ、UniTaskを使っています。
using UnityEngine;
using UnityEngine.AddressableAssets;
using Cysharp.Threading.Tasks;
using TMPro;
public class FontLoader : MonoBehaviour
{
[SerializeField] private string fontLabel;
[SerializeField] private TMP_FontAsset staticFont;
async void Start()
{
await LoadAndSetupFonts();
}
private async UniTask LoadAndSetupFonts()
{
var dynamicFonts = await Addressables.LoadAssetsAsync<TMP_FontAsset>(fontLabel, null);
if (dynamicFonts != null && staticFont != null)
{
foreach (var dynamicFont in dynamicFonts)
{
if (!staticFont.fallbackFontAssetTable.Contains(dynamicFont))
{
staticFont.fallbackFontAssetTable.Add(dynamicFont);
}
}
}
}
}
こうすると、実行する前(Addressablesからロードされていない状態)では、FallbackFontAssetsはブランクになり、

実行して、Addressables経由でフォントをロードすると、リモートサーバからロードしたフォントがFallbackにセットされます。

こうするとこで、.ttfファイル、Dynamicフォント用のAtlasデータをビルド容量から削減し、なおかつ.ttfに含まれる日本語をすべて表示することが出来ました。

対策3については、ネイティブアプリではありなやり方ですが、WebGLではあまりオススメではありません。
ネイティブアプリでは、一度Addressables経由でデータをダウンロードさせたら、そのデータを端末に保存し、2回目以降の起動時はダウンロード処理を行わない、といったことが可能です。
一方、WebGLでは、一度ダウンロードしたデータをキャッシュするのは難しく、ゲーム起動のたびにユーザからフォントデータをダウンロードさせる必要があるため、サーバ代にも影響してしまいます。
※もしWebGLでキャッシュできる方法があればコメントでこっそり教えて下さい...。
他の画像や3DモデルなどのAddressablesデータも含め、複数の大量のデータをまとめてダウンロードさせる前提なら、フォントデータも含めてダウンロードさせるなら、WebGLでもありです。
ですが、フォントデータのためだけにサーバからフォントデータを都度ダウンロードさせるのは、正直、費用対効果が釣り合わないのかなと、私は思いました。
まとめ
最後に、WebGLビルドの場合、これらの対応を行った場合に必要な容量の違いをまとめてみました。
Atlasの作り方にもよるので、容量はあくまで一例として見ていただけたらと。

難しい漢字を表示する必要がないゲームなら、対応1まで。
ユーザのインプットなどがあり、難しい漢字も表示が必要な場合、対応2。
対応2の中で、さらに可能な限りの容量削減を目指すなら対応3。
といったところではないでしょうか?
以上、個人的TextMeshProの最適解(?)についてでした。少しでも参考になれば幸いです。
それでは素敵なゲーム制作ライフを!
コメント