-100p

-10p

+10p

+100p

Unityでフォント一斉切り替え機能

Unityで全てのシーンやプレハブのフォントを一斉変更するEdiror機能の追加。
旧TextとTextMeshProの両対応。

例えばデザインより先にプログラムが先行して開発を進めている場合、途中でフォントを一斉に変えるタイミングが出てくる。
こういう場合、わざわざポチポチ全てのTextやTMPのFontを切り替えるのは面倒なので、一斉切り替えのEditor機能を作った。

今回は旧TextとTextMeshPro両方に対するEditor機能を作るので、共通するコードの部分はAssetChangeBaseというクラスにまとめた。
なので「共通処理のベースクラス」「旧Text変更のクラス」「TextMeshPro変更のクラス」の3つのスクリプトが必要になる。
各スクリプトは、Editorフォルダの下に置く必要がある。

SceneやPrefabを一斉変更するベースクラス

Unityはユーザーの手で結構システムの奥深くまで手を入れられるように作ってある。
下のクラスは、Assetに存在する全てのSceneやPrefabに対して変更を与えるためのベースクラス。
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;

namespace Furyu
{
    
/// <summary>

    
/// Project内のSceneやPrefabを変更するための便利クラス

    
/// </summary>

    public class AssetChangeBase : EditorWindow
    {
        
/// <summary>

        
/// Project内の全てのAssetから指定のファイル形式のObjectを取得

        
/// </summary>

        protected static T[] FindAssets<T>(string specified = "") where T : Object
        {
            List<T> result = new List<T>();

            string[] assetList = null;
            if (string.IsNullOrEmpty(specified))
            {
                assetList = AssetDatabase.FindAssets(string.Format("t:{0}", typeof(T).Name));
            }
            else
            {
                assetList = AssetDatabase.FindAssets(string.Format("t:{0}", specified));
            }

            for (int i = 0; i < assetList.Length; i++)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(assetList[i]);
                T asset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
                if (asset != null)
                {
                    result.Add(asset);
                }
            }
            return result.ToArray();
        }

        
/// <summary>

        
/// 新しいシーンで全プレハブを生成するだけ

        
/// </summary>

        
/// <param name="targetType">Prefab内に指定したComponentがあるものだけを生成する</param>

        protected static List<Object> InstantiateAllPrefabs(Type targetType = null)
        {
            
//新しいシーンを作り、そこで全てのプレハブを生成する

            EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);

            List<Object> instantiatedPrefabs = new List<Object>();
            var prefabsDic = FindAssets<GameObject>("Prefab");
            foreach (var item in prefabsDic)
            {
                if (targetType == null)
                {
                    instantiatedPrefabs.Add(PrefabUtility.InstantiatePrefab(item));
                }
                else
                {
                    if (item.GetComponentsInChildren(targetType, true) != null &&
                        item.GetComponentsInChildren(targetType, true).Length > 0)
                    {
                        instantiatedPrefabs.Add(PrefabUtility.InstantiatePrefab(item));
                    }
                }
            }
            return instantiatedPrefabs;
        }

        
/// <summary>

        
/// 全てのPrefabを生成してコンポーネントを操作

        
/// </summary>

        
/// <param name="callback">生成した全てのGameObjectに対して実行するcallback</param>

        
/// <param name="isApply">callbackを呼んだ後Applyするかどうか</param>

        
/// <param name="targetType">Prefab内に指定したComponentがあるものだけをApply対象にする</param>

        protected static void ChangeComponentAllPrefabs(Action<GameObject> callback, bool isApply = true, Type targetType = null)
        {
            
//後で戻ってくるので現在のシーンのパスを保存

            string curScenePath = SceneManager.GetActiveScene().path;

            List<Object> instantiatedPrefabs = InstantiateAllPrefabs(targetType);

            ChangeActionForeach(callback);

            
//PrefabのApply

            if (isApply)
            {
                int i = 0;
                foreach (var item in instantiatedPrefabs)
                {
                    EditorUtility.DisplayProgressBar("Replacing in prefab...", item.name, (float)i / instantiatedPrefabs.Count);
                    
//Unity標準のプレハブでなければApply

                    if (!PrefabUtility.IsPartOfImmutablePrefab((GameObject)item))
                    {
                        try
                        {
                            PrefabUtility.ApplyPrefabInstance((GameObject)item, InteractionMode.UserAction);
                        }
                        catch (System.Exception e)
                        {
                            Debug.LogError(e);
                            Debug.LogError("Prefabの更新ができませんでした。対象のPrefabの中身を確かめてください。");
                            EditorUtility.ClearProgressBar();
                            EditorSceneManager.OpenScene(curScenePath);
                            return;
                        }
                    }
                    i++;
                }
                EditorUtility.ClearProgressBar();
            }

            if (!String.IsNullOrEmpty(curScenePath))
            {
                EditorSceneManager.OpenScene(curScenePath);
            }
        }

        
/// <summary>

        
/// 現在のシーンの全てのコンポーネントを操作

        
/// </summary>

        
/// <param name="callback">シーン上の全てのGameObjectに対して実行するcallback</param>

        
/// <param name="isSave">callbackを呼んだあとシーンをSaveするかどうか</param>

        protected static void ChangeComponentCurrentScene(Action<GameObject> callback, bool isSave = true)
        {
            ChangeActionForeach(callback);
            if (isSave)
            {
                EditorSceneManager.SaveScene(SceneManager.GetActiveScene());
            }
        }

        
/// <summary>

        
/// 全てのシーンの全てのコンポーネントを操作

        
/// </summary>

        
/// <param name="callback">シーン上の全てのGameObjectに対して実行するcallback</param>

        
/// <param name="isSave">callbackを呼んだあとシーンをSaveするかどうか</param>

        protected static void ChangeComponentAllScenes(Action<GameObject> callback, bool isSave = true)
        {
            
//後で戻ってくるので現在のシーンのパスを保存

            string curScenePath = SceneManager.GetActiveScene().path;

            
//全てのシーンを順々に読み込んでコンポーネントを操作

            SceneAsset[] scenes = FindAssets<SceneAsset>();
            for (int i = 0; i < scenes.Length; i++)
            {
                SceneAsset scene = scenes[i];
                Debug.Log("Sceneの読み込み = " + scene.name);

                EditorUtility.DisplayProgressBar("Replacing in scene...", scene.name, (float)i / scenes.Length);
                Scene loadedScene = EditorSceneManager.OpenScene(AssetDatabase.GetAssetPath(scene), OpenSceneMode.Single);
                while (!loadedScene.isLoaded) {}

                ChangeActionForeach(callback);
                if (isSave)
                {
                    EditorSceneManager.SaveScene(loadedScene);
                }
            }
            EditorUtility.ClearProgressBar();

            EditorSceneManager.OpenScene(curScenePath);
        }

        
/// <summary>

        
/// 再起的に子要素に潜る

        
/// </summary>

        private static void DeepSerch(GameObject root, System.Action<GameObject> callback)
        {
            callback(root);
            if (root.transform.childCount != 0)
            {
                for (int j = 0; j < root.transform.childCount; j++)
                {
                    GameObject targetObj = root.transform.GetChild(j).gameObject;
                    DeepSerch(targetObj, callback);
                }
            }
        }

        
/// <summary>

        
/// ルートオブジェクトを回してCallbackを呼ぶ

        
/// </summary>

        private static void ChangeActionForeach(Action<GameObject> callback)
        {
            GameObject[] rootGameObjects = SceneManager.GetActiveScene().GetRootGameObjects();
            for (int i = 0; i < rootGameObjects.Length; i++)
            {
                GameObject root = rootGameObjects[i];
                DeepSerch(root, (targetObj) =>
                {
                    callback(targetObj);
                });
            }
        }

        
/// <summary>

        
/// RootのプレハブをApply

        
/// </summary>

       protected static void AllPrefabsApply()
        {
            GameObject[] rootGameObjects = SceneManager.GetActiveScene().GetRootGameObjects();
            for (int i = 0; i < rootGameObjects.Length; i++)
            {
                var item = rootGameObjects[i];
                
//Unity標準のプレハブでなければApply

                if (!PrefabUtility.IsPartOfImmutablePrefab(item))
                {
                    try
                    {
                        PrefabUtility.ApplyPrefabInstance(item, InteractionMode.UserAction);
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogError(e);
                        Debug.LogError("Prefabの更新ができませんでした。対象のPrefabの中身を確かめてください。");
                        EditorUtility.ClearProgressBar();
                        return;
                    }
                }
            }
        }
    }
}

旧TextのFontを一斉入れ替え

下記はコードの全文。
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;

namespace Furyu
{
    
/// <summary>

    
/// 旧Text Componentに対し、指定のFontに入れ替えるEditor

    
/// </summary>

    public class FontReplacer : AssetChangeBase
    {
        private static Font[] fonts;
        private static List<string> fontsStrings;
        private static int selectFontIdx = 0;
        private static Font targetFont;

        private static bool isInitialize = false;

        
/// <summary>

        
/// 全てのフォントを取得して保存

        
/// </summary>

        private static void Reload()
        {
            fonts = FindAssets<Font>();
            fontsStrings = fonts.Select(x => x.ToString()).ToList();
        }

        [MenuItem("Tools/Font Replacer/Editor Window", priority = 100)]
        private static void Init()
        {
            FontReplacer window = (FontReplacer)GetWindow(typeof(FontReplacer));
            window.Show();
        }

        void OnGUI()
        {
            if (!isInitialize)
            {
                Reload();
                targetFont = fonts.First();
                isInitialize = true;
            }

            if (GUILayout.Button("フォントアセットのリロード"))
            {
                Reload();
            }

            if (fonts == null || fonts.Length == 0)
            {
                return;
            }

            selectFontIdx = EditorGUILayout.Popup("上書きするフォント:", selectFontIdx, fontsStrings.ToArray());
            targetFont = fonts[selectFontIdx];

            EditorGUILayout.Space(10);

            EditorGUILayout.LabelField("[全てのプレハブ]");
            if (GUILayout.Button("フォントを上書き"))
            {
                ChangeComponentAllPrefabs(FontChange);
            }

            EditorGUILayout.Space(10);

            EditorGUILayout.LabelField("[現在のシーン]");
            if (GUILayout.Button("フォントを上書き"))
            {
                ChangeComponentCurrentScene(FontChange);
            }

            EditorGUILayout.Space(10);

            EditorGUILayout.LabelField("[全てのシーン]");
            if (GUILayout.Button("フォントを上書き"))
            {
                ChangeComponentAllScenes(FontChange);
            }

            EditorGUILayout.Space(10);
            EditorGUILayout.LabelField("-------------");
            EditorGUILayout.LabelField("手作業で確認");
            if (GUILayout.Button("全プレハブを生成"))
            {
                InstantiateAllPrefabs();
            }
            if (GUILayout.Button("Rootの全プレハブをApply"))
            {
                AllPrefabsApply();
            }
        }

        
/// <summary>

        
/// Fontを変更

        
/// </summary>

        private void FontChange(GameObject targetObj)
        {
            Text text = targetObj.GetComponent<Text>();
            if (text != null)
            {
                if (text.font != targetFont)
                {
                    text.font = targetFont;
                    
//シーン変更を通知

                    EditorUtility.SetDirty(text);
                }
            }
        }
    }
}

上部メニューのEditor/Font Replacerからウィンドウを開ける。

上から機能の解説。
 フォントアセットリロード 
: Assets内の全てのフォントを再取得する
 上書きするフォント 
: 下のコマンドで上書きする対象のフォント
 [全てのプレハブ] 
: Assets内の全てのプレハブのフォントを置き換える
 [現在のシーン] 
: 現在開いてるシーンの全てのフォントを置き換える
 [全てのシーン] 
: Assets内の全てのシーンのフォントを置き換える
 全プレハブを生成 
: 空のシーンを新しく作り、その上にAssets上の全てのプレハブを生成する
 Rootの全プレハブをApply 
: 現在開いてるシーンに生成されてる全プレハブを更新

TextMeshProのFontを一斉入れ替え

下記はコードの全文。
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using TMPro;

namespace Furyu
{
    
/// <summary>

    
/// TextMeshPro Componentに対し、指定のFontに入れ替えるEditor

    
/// </summary>

    public class FontReplacerTmp : AssetChangeBase
    {
        private static TMP_FontAsset[] fonts;
        private static List<string> fontsStrings;
        private static int selectFontIdx = 0;
        private static TMP_FontAsset targetFont;

        private static bool isInitialize = false;

        
/// <summary>

        
/// 全てのフォントを取得して保存

        
/// </summary>

        private static void Reload()
        {
            fonts = FindAssets<TMP_FontAsset>();
            fontsStrings = fonts.Select(x => x.ToString()).ToList();
        }

        [MenuItem("Editor/Font Replacer Tmp", priority = 2)]
        private static void Init()
        {
            FontReplacerTmp window = (FontReplacerTmp)GetWindow(typeof(FontReplacerTmp));
            window.Show();
        }

        void OnGUI()
        {
            if (!isInitialize)
            {
                Reload();
                targetFont = fonts.First();
                isInitialize = true;
            }

            if (GUILayout.Button("フォントアセットのリロード"))
            {
                Reload();
            }

            if (fonts == null || fonts.Length == 0)
            {
                return;
            }

            selectFontIdx = EditorGUILayout.Popup("上書きするフォント:", selectFontIdx, fontsStrings.ToArray());
            targetFont = fonts[selectFontIdx];

            EditorGUILayout.Space(10);

            EditorGUILayout.LabelField("[全てのプレハブ]");
            if (GUILayout.Button("フォントを上書き"))
            {
                ChangeComponentAllPrefabs(FontChange);
            }

            EditorGUILayout.Space(10);

            EditorGUILayout.LabelField("[現在のシーン]");
            if (GUILayout.Button("フォントを上書き"))
            {
                ChangeComponentCurrentScene(FontChange);
            }

            EditorGUILayout.Space(10);

            EditorGUILayout.LabelField("[全てのシーン]");
            if (GUILayout.Button("フォントを上書き"))
            {
                ChangeComponentAllScenes(FontChange);
            }

            EditorGUILayout.Space(10);
            EditorGUILayout.LabelField("-------------");
            EditorGUILayout.LabelField("手作業で確認");
            if (GUILayout.Button("全プレハブを生成"))
            {
                InstantiateAllPrefabs();
            }
            if (GUILayout.Button("Rootの全プレハブをApply"))
            {
                AllPrefabsApply();
            }
        }

        
/// <summary>

        
/// Fontを変更

        
/// </summary>

        private void FontChange(GameObject targetObj)
        {
            TextMeshProUGUI text = targetObj.GetComponent<TextMeshProUGUI>();
            if (text != null)
            {
                if (text.font != targetFont)
                {
                    text.font = targetFont;
                    
//シーン変更を通知

                    EditorUtility.SetDirty(text);
                }
            }
        }
    }
}

機能としては旧Textと全く同じになる。対象TMPになっただけ。

0
0

-100p

-10p

+10p

+100p