Unity一键更换TextMeshPro的字体

目录

1.背景介绍

2.具体代码

3.Undo与Dirty

(1)Undo

 (2)Dirty

4.文件路径的斜杠与反斜杠

5.该脚本的修改与拓展


1.背景介绍

        工作中常见的是,字体资源暂未确定的情况下,先要进行UI的排版布局。待到后续字体资源确定后,再进行统一的更换。

        但是单纯替换字体的方式异常繁琐,又容易遗漏,故而通过此脚本,创造一个EditorGUI,以进行一键更换字体

2.具体代码

using System.Collections.Generic;
using System.IO;
using TMPro;
using UnityEditor;
using UnityEngine;

/// <summary>
/// 该脚本允许一键更换所有TMP_Text与TMP_InputField的字体
/// 包括场景及预制体
/// </summary>
public class ChangeFontWindow : EditorWindow
{
    /*
     * MenuItem属性在编辑器上方创建了一个选项类
     * 默认的类有File, Edit, Assets, GameObject, Component, Window, Help七类
     */

    [MenuItem("Tools/TMP_Text Fonts Change")]
    public static void Open()
    {
        /*
         * GetWindow方法则直接打开了一个小窗口
         * 并调用其中的OnGUI方法
         */
        GetWindow(typeof(ChangeFontWindow));
    }

    private TMP_FontAsset fontToChange;//这个作局部变量使用,用于保护静态变量被随意修改
    private static TMP_FontAsset m_fontToChange;

    private void OnGUI()
    {
        /*
         *  EditorGUILayout.ObjectField生成了一个选项栏,允许给予特定的类型或资产
         *  其返回类型为Object,需要自己进行类型转换
         */
        fontToChange =
            EditorGUILayout.ObjectField(fontToChange, typeof(TMP_FontAsset), true, GUILayout.MinWidth(100f))
            as TMP_FontAsset;

        m_fontToChange = fontToChange;

        /*
         * GUILayout.Button方法绘制了一个按钮,按钮上的字符串便是参数中的值
         * 如果按下了按钮,则该值返回true,并随之调用设定好的函数
         */
        if (GUILayout.Button("Change Fonts In Canvas"))
        {
            ChangeInCanvas();
        }
        if (GUILayout.Button("Change Fonts In Prefabs"))
        {
            ChangeInPrefabs();
        }
    }

    #region 修改场景中的字体

    private static void ChangeInCanvas()
    {
        TMP_Text[] tmpTexts = Resources.FindObjectsOfTypeAll<TMP_Text>();
        TMP_InputField[] tmpInputs = Resources.FindObjectsOfTypeAll<TMP_InputField>();
        foreach (TMP_Text text in tmpTexts)
        {
            /*
             * Undo.RecordObject用于记录场景中的物体的修改
             * EditorUtility.SetDirty用于记录预制体中的修改
             * 拓展内容见后续
             */
            Undo.RecordObject(text, text.gameObject.name);
            text.font = m_fontToChange;
            EditorUtility.SetDirty(text);
        }

        /*
         * 注意到,TMP_InputField下的TMP_Text是无法单纯更改其字体实现的
         * 需要修改TMP_InputField的参数才可以
         */
        foreach (TMP_InputField input in tmpInputs)
        {
            //同上
            Undo.RecordObject(input, input.gameObject.name);
            input.fontAsset = m_fontToChange;
            EditorUtility.SetDirty(input);
        }

        Debug.Log("Change font in canvas succeed!");
    }

    #endregion 修改场景中的字体

    #region 修改预制体中的字体

    private static void ChangeInPrefabs()
    {
        List<GameObject> prefabs = GetAllPrefabs();

        foreach (GameObject prefab in prefabs)
        {
            TMP_Text[] texts = prefab.GetComponentsInChildren<TMP_Text>(true);
            TMP_InputField[] inputFields = prefab.GetComponentsInChildren<TMP_InputField>();

            foreach (TMP_Text text in texts)
            {
                Undo.RecordObject(text, text.gameObject.name);
                text.font = m_fontToChange;
                EditorUtility.SetDirty(text);
            }

            foreach (TMP_InputField inputField in inputFields)
            {
                Undo.RecordObject(inputField, inputField.gameObject.name);
                inputField.fontAsset = m_fontToChange;
                EditorUtility.SetDirty(inputField);
            }

            //使用新方法进行储存预制体
            PrefabUtility.SavePrefabAsset(prefab);
        }

        Debug.Log("Change font in prefabs succeed!");
    }

    /// <summary>
    /// 获取所有的预制体
    /// </summary>
    /// <returns></returns>
    private static List<GameObject> GetAllPrefabs()
    {
        List<GameObject> prefabs = new List<GameObject>();
        string resourcesPath = Application.dataPath;

        //利用C#自带的查找文件方式,查找该目录下所有包含该字符串的文件地址
        //位于System.IO命名空间下
        string[] absolutePaths = Directory.GetFiles(resourcesPath, "*.prefab", SearchOption.AllDirectories);
        for (int i = 0; i < absolutePaths.Length; i++)
        {
            //如果数量比较多,有必要显示进度条
            EditorUtility.DisplayProgressBar("获取预制体……", "获取预制体中……", (float)i / absolutePaths.Length);

            /*
             * 对于文件路径,斜杠及反斜杠的讨论
             * 见下
             */
            string path = "Assets" + absolutePaths[i].Remove(0, resourcesPath.Length);
            path = path.Replace("\\", "/");
            GameObject prefab = AssetDatabase.LoadAssetAtPath(path, typeof(GameObject)) as GameObject;
            if (prefab != null)
            {
                prefabs.Add(prefab);
            }
            else
            {
                Debug.Log("预制体不存在! " + path);
            }
        }
        EditorUtility.ClearProgressBar();
        return prefabs;
    }

    #endregion 修改预制体中的字体
}

3.Undo与Dirty

        这里只谈一下简要的介绍。

(1)Undo

        Undo是将某个操作记入操作栈,便于后续撤销。例如,每次对场景中的组件、命名或其他内容有所改动时,Scene的图标上会出现一个*,表示该操作未保存,按下 Ctrl + S 之后,*消失,保存成功。但即使保存成功了,仍然可以通过 Ctrl + Z(Mac没用过,不晓得)的方式进行撤销。这就是因为我们的每一步操作都被记入了操作栈中。

        Unity官方对Undo的介绍如下:

让您可以针对要执行更改的特定对象注册撤销操作。

撤销系统可以在撤销堆栈中存储增量更改。

系统会根据事件自动将多个撤销操作组合在一起,例如,根据鼠标按下事件拆分撤销组。分组后的撤销操作将作为单个撤销操作出现和使用。要手动控制分组方法,请使用 Undo.IncrementCurrentGroup

默认情况下,系统将按照不同类型操作的硬编码顺序,从属于该组的各项操作中选择 UI 中显示的名称。要手动设置名称,请使用 Undo.SetCurrentGroupName

撤销操作会存储每个属性或每个对象的状态。这样,如论场景的大小如何,它们都可以正确缩放。

最重要的几项操作如下所述:

修改单个属性:\ Undo.RecordObject (myGameObject.transform, "Zero Transform Position");myGameObject.transform.position = Vector3.zero;

添加组件:\ Undo.AddComponent<RigidBody>(myGameObject);

创建新的游戏对象:\ var go = new GameObject();Undo.RegisterCreatedObjectUndo (go, "Created go");

销毁游戏对象或组件:\ Undo.DestroyObjectImmediate (myGameObject);

更改变换组件的父子化:\ Undo.SetTransformParent (myGameObject.transform, newTransformParent, "Set new parent");

 (2)Dirty

        Dirty是一种标记,用来标记某个不需要撤销的操作(即该操作过后没有*,可以视为就是一个保存按钮?)。这是一种较为古老的方式,现在对于很多操作,官方已经不建议使用SetDirty方法,而改用上文的Undo去记录。

        在许多给出的代码示例中,将二者混合使用,以达到修改后并保存的作用。

        具体官方对SetDirty的方法介绍如下:

Marks target object as dirty.

You can use SetDirty when you want to modify an object without creating an undo entry, but still ensure the change is registered and not lost. If the object is part of a Scene, the Scene is marked dirty. If the object may be part of a Prefab instance, you additionally need to call PrefabUtility.RecordPrefabInstancePropertyModifications to ensure a Prefab override is created.

If you do want to support undo, you should not call SetDirty but rather use Undo.RecordObject prior to making changes to an object, since this will both mark the object as dirty (or the object's Scene if it is part of a Scene) and provide an undo entry in the editor. You should still also call PrefabUtility.RecordPrefabInstancePropertyModifications if the object may be part of a Prefab instance.

When you create editor UI for manipulating an object, such as a custom editor to modify serialized properties on a component or asset, if possible, you should use the SerializedProperty system using SerializedObject.FindPropertySerializedObject.UpdateEditorGUILayout.PropertyField, and SerializedObject.ApplyModifiedProperties. This will automatically mark the object as dirty, create an undo entry, and ensure Prefab overrides are created if relevant.

        而在Prefab的处理中,参考了官方给出的建议,用 PrefabUtility.SavePrefabAsset(prefab) 方法对预制体的修改进行保存。

4.文件路径的斜杠与反斜杠

        在网上看过不少文章,不同的作者对于符号定义不一致,看起来会略显混乱。故而在讨论之前,首先规定符号及名称的对应关系。纯属个人理解,不一定正确。

        /,又称斜杠,正斜杠,左斜杠。就是键盘上跟?在一起的那个键。

        \,又称反斜杠,右斜杠。就是键盘上在Enter上面的那个键。

        在Linux, Unix系统上,均是使用 /(斜杠)分隔文件路径。

        Windows系统上,起初 /(斜杠)用来表示DOS命令提示符的指令,为了便以区分,Windows采用 \(反斜杠)分隔文件路径。随着DOS的淘汰, /(斜杠)在Windows上也能够用以分隔文件路径。

        即,/(斜杠)适用于所有系统而 \(反斜杠)仅适用于Windows系统。

        由于Windows起初的规定,使得系统很多获取文件路径的结果都成了以 \(反斜杠)分隔。比如,Win11系统获取文件地址,可以右键菜单直接复制文件地址,粘贴出来之后得到的结果如下:

        "D:\Program Files\Unity\2019.4.36f1c1\Editor\Unity.exe"

        甚至还很贴心的加上了引号...

        除此之外,使用 Directory.GetFiles 方法,得到的文件路径也是以 \(反斜杠)分隔。例如,该脚本放入任意Unity项目中,断点获取其路径的值,输出如下:

        "E:/UnityLibrary/MyProjects/TestProject/Assets\\Addressables\\Prefabs\\DialogPanel.prefab"

        注意到,在前面用的还是 /(斜杠),而到了后面就变成了 \(反斜杠)。这是因为前面的路径,在代码中我们使用的是Unity的Application.dataPath,不妨理解,Unity为了更好地跨平台,使用了 /(斜杠)分隔,但是后续C#获取路径的方式又变成了获取Windows的路径,成了 \(反斜杠)。为了统一性,在代码中将所有的 \(反斜杠)统一替换成了 /(斜杠)。

        而具体的,是C#获取路径的结果就是 \(反斜杠),还是由于在Windows平台运行导致获取路径结果为 \(反斜杠)暂时不明。有兴趣的话不妨在其他系统上尝试一下。我个人倾向于是Windows系统的原因。

5.该脚本的修改与拓展

        可以看到,我们可以对该脚本进行稍加修改,替换Text的字体;或者通过对背景Image添加Tag的方式,一键对所有的背景进行更换。实用性还是比较广的。

        具体效果展示:

猜你喜欢

转载自blog.csdn.net/TidyDogg/article/details/126678796