読み込んだフォントアセットのマテリアルを安全に差し替える方法
UnityでTextMesh Proのフォントアセットを読み込み、スクリプトからフォントのマテリアルを差し替える処理を行いました。
1つのフォントに対して存在する複数のマテリアルをプログラム内で動的に差し替える必要があり、その上で外部アセットにフォントを渡す必要があったからです。
// フォントアセット取得
TMP_FontAsset font = FontMap["フォント名"]; // FontMap is Dictionary
// マテリアル差し替え
font.material = FontMaterialMap["マテリアル名"]; // FontMaterialMap is Dictionary
// 外部アセットの公開メソッドにフォントを投げる
ExternalAsset.PublicMethod(font);
すると次回以降、フォントアセットを読み込めなくなって起動に失敗する事態に陥ってしまいました。
仕方なく手動でフォントアセットを作り直すと初回は動作したものの、やはり2回目以降は同じ状況です。
※エラー内容を失念。残しておけばよかったな……
実はこの時、使用したフォントアセットとマテリアルはそれぞれ下記の状態でした。
- フォントアセットは、リソースから直接読み込んだオブジェクト
→具体的にはAddressables.LoadAssetAsync()で読み込んだもの - マテリアルは、リソースから直接読み込んだ後にnew Material();したオブジェクト
ここで使っているTMP_FontAssetインスタンスはnewしたオブジェクトではなくリソースから直接読み込んだオブジェクトのままなので、このオブジェクトに値をセットすることはリソースのフォントアセット(ファイル)を直接書き換えることを意味します。
そう、上記のコードではプログラム内で生成したマテリアルをフォントアセットに設定しています。
該当シーンをDestroyしたり、ゲームを終了するなどしてオブジェクトの参照を切った時点でマテリアルはメモリの藻屑となって消えるので、フォントアセットが指し示すマテリアルが意味のないものに変わり果ててしまったわけですね。
これではフォントアセットが壊れてしまうのも納得です。
とりあえずマテリアルの差し戻し処理を行うことで解決しました。
// フォントアセット取得
TMP_FontAsset font = FontMap["フォント名"]; // FontMap is Dictionary
// 元マテリアル取得
Material sourceMat = font.material;
// マテリアル差し替え
font.material = FontMaterialMap["マテリアル名"]; // FontMaterialMap is Dictionary
// 外部アセットの公開メソッドにフォントを投げる
ExternalAsset.PublicMethod(font);
// マテリアルを差し戻し
font.material = sourceMat;
しかしそもそもの原因は、フォントアセットを読み込んだままの姿で使っていることです。
この箇所のコーディングにミスがあるとフォントアセットが壊れます。
それ、バグってしまうと最悪の場合ゲームの動作に必要なファイルが破壊されるということです。
リスクが大きすぎて怖い。
こういう時はディープコピーすれば解決しますよね。
具体的にはフォントアセットとテクスチャとマテリアルをそれぞれ再生成して差し替えします。
/// <summary>
/// テクスチャとマテリアルを再生成したフォントアセットを生成
/// </summary>
public static TMP_FontAsset CopyFontAsset(TMP_FontAsset original)
{
// フォントアセットのオブジェクトを再生成
// なおInstantiateの実行に必要なので、このメソッドを記載する対象はMonoBehaviourもしくはObjectを継承したクラスであること
TMP_FontAsset copy = Instantiate(original);
// テクスチャをコピーして生成
Texture2D origTexture = original.atlasTexture;
Texture2D newTexture = new Texture2D(origTexture.width, origTexture.height, origTexture.format, origTexture.mipmapCount, !origTexture.isDataSRGB);
Graphics.CopyTexture(origTexture, newTexture);
// マテリアルをコピーして生成
Material newMaterial = new Material(original.material);
newMaterial.SetTexture("_MainTex", newTexture);
// 生成したテクスチャとマテリアルをセット
copy.atlasTextures = new[] { newTexture };
copy.material = newMaterial;
return copy;
}
※Texture2DクラスのisDataSRGB()メソッドはUnity 2022.2.0以降で使用できます。
それより前のバージョンでは使用できないので、テクスチャがリニアならtrue、ガンマならfalseにしてください。
フォントアセットを読み込む処理で上記のメソッドにTMP_FontAssetインスタンスを渡して、返ってきたコピーをプログラム内で扱うようにしてください。
あと、必要なくなったらちゃんとDestroyしましょう。
foreach (Texture2D tex in copy.atlasTextures)
{
Destroy(tex);
}
Destroy(copy.material);
Destroy(copy);
これを忘れるとメモリリークしてしまいます。
フォントのマテリアル(テクスチャ)を差し替えるのは割とニッチな利用方法なのか、この一連の話題は調べても出てきませんでした。
たったこれだけのメソッドですが躓きポイントが多くてハマりました。
誰かが車輪の再発明をしないように祈りつつ、電子の海にこのメモを放流。