
1. 项目概述与核心痛点最近在升级一个老Unity项目到2022 LTS版本时遇到了一个让人头疼的问题项目里上百个预制体Prefab的UI文本Text/TextMeshPro组件字体设置全是Arial。众所周知Unity 2022移除了内置的Arial字体这直接导致所有使用Arial的UI在运行时要么显示成丑陋的备用字体要么直接不显示中文变成一堆“口口口”。手动去Prefab里一个个改那简直是噩梦。这个项目标题“告别Arial手把手教你用Editor脚本批量修改Unity预制体字体含中文支持”就是为解决这个在Unity版本升级、项目迁移或统一视觉规范时普遍存在的“字体批量替换”痛点而生的。简单来说我们要写一个运行在Unity编辑器Editor环境下的脚本它能自动扫描你指定的文件夹比如整个Assets目录或某个UI预制体目录找到里面所有的预制体然后将其Text或TextMeshPro - Text (TMP)组件上使用的Arial字体批量、安全地替换成你指定的、支持中文的新字体如思源黑体、Noto Sans SC等。这不仅能解决因Arial缺失导致的显示问题更是项目资产管理规范化、提升团队协作效率的必备技能。无论你是独立开发者还是团队中的TA技术美术或主程掌握这项技能都能让你从繁琐的重复劳动中解放出来。2. 核心思路与方案设计2.1 为什么必须用Editor脚本面对成百上千的预制体手动操作的弊端显而易见效率低下、极易出错漏改、改错、且无法形成可复用的流程。Unity Editor脚本的核心价值在于自动化和批处理。它允许我们将重复、有规则的操作封装成一个编辑器工具一键执行。对于字体替换这种典型的“查找-判断-修改”任务Editor脚本是最佳选择。它直接操作Asset数据无需运行游戏安全且高效。2.2 方案选型Text vs TextMeshPro (TMP)Unity的UI文本系统主要有两套传统的uGUI Text组件和更现代的TextMeshPro。我们的脚本需要同时支持两者因为老项目可能混用而新项目普遍推荐TMP。传统Text组件对应UnityEngine.UI.Text类。其字体属性是Font类型。Arial在这里通常表现为一个名为“Arial”的Font资源引用。替换的本质是找到这个引用并将其替换为另一个Font资源。TextMeshPro组件对应TMPro.TextMeshProUGUI类用于UI或TMPro.TextMeshPro用于3D世界。其字体属性是TMP_FontAsset类型。虽然TMP字体资源本身更复杂包含图集、材质等但替换组件上的字体引用逻辑是相似的。我们的脚本需要能智能识别这两种组件类型并分别进行处理。一个健壮的方案是先尝试获取TMP组件因为TMP更常见如果没有再尝试获取传统Text组件。2.3 字体资源准备中文支持的关键替换掉Arial我们得找一个靠谱的“接班人”。选择新字体时必须确保其完整支持中文GB2312/GBK或更全的字符集。这里有几个常见选择思源黑体 (Source Han Sans)Google和Adobe联合开发开源免费字重齐全对简体中文SC支持极好。可以从GitHub或Adobe官网下载OTF文件。Noto Sans SCGoogle的“Noto”字体家族中的简体中文无衬线体同样开源免费旨在消除所有“豆腐块”口口口。微软雅黑Windows系统自带中文字形优美但需注意版权。在Windows平台开发且目标用户为Windows用户时可以考虑但跨平台需谨慎。其他商用字体根据项目美术风格和版权许可选择。准备工作将下载的.ttf或.otf字体文件直接拖入Unity项目的Assets目录下例如Assets/Fonts/。Unity会自动将其导入为Font资源对于传统Text或需要你手动创建TMP_FontAsset对于TMP。对于TMP在Project窗口右键点击导入的字体文件 -Create - TextMeshPro - Font Asset。这个过程会生成一个.asset文件这就是TMP组件真正使用的字体资源。务必为你的中文字体执行此操作否则TMP无法使用。脚本中我们需要提供两个字段让使用者可以分别指定替换用的Font和TMP_FontAsset。3. Editor脚本核心实现解析下面我们来一步步拆解这个Editor脚本的核心代码。我会创建一个名为BatchReplaceFontTool的脚本放在项目的Assets/Editor/文件夹下Editor文件夹下的脚本只在编辑器中运行。3.1 工具界面与变量定义首先我们需要创建一个继承自EditorWindow的类来绘制一个简单的工具窗口。using UnityEngine; using UnityEditor; using TMPro; using System.IO; using System.Collections.Generic; public class BatchReplaceFontTool : EditorWindow { // 替换目标旧字体名称通常是Arial private string oldFontName Arial; // 替换为的新字体资源 private Font replacementFont; // 用于传统Text组件 private TMP_FontAsset replacementTMPFont; // 用于TMP组件 // 搜索路径 private string searchPath Assets; // 是否包含子文件夹 private bool includeSubdirectories true; // 进度显示 private string progressInfo ; // 添加窗口菜单项 [MenuItem(Tools/批量替换字体工具)] static void Init() { BatchReplaceFontTool window (BatchReplaceFontTool)EditorWindow.GetWindow(typeof(BatchReplaceFontTool)); window.titleContent new GUIContent(批量替换字体); window.Show(); } void OnGUI() { GUILayout.Label(字体批量替换工具, EditorStyles.boldLabel); EditorGUILayout.Space(); // 输入旧字体名称 oldFontName EditorGUILayout.TextField(旧字体名称:, oldFontName); // 选择新字体资源 replacementFont (Font)EditorGUILayout.ObjectField(新字体 (For Text):, replacementFont, typeof(Font), false); replacementTMPFont (TMP_FontAsset)EditorGUILayout.ObjectField(新字体 (For TMP):, replacementTMPFont, typeof(TMP_FontAsset), false); EditorGUILayout.Space(); // 搜索设置 searchPath EditorGUILayout.TextField(搜索路径:, searchPath); includeSubdirectories EditorGUILayout.Toggle(包含子目录:, includeSubdirectories); EditorGUILayout.Space(); if (GUILayout.Button(开始扫描并替换, GUILayout.Height(40))) { if (replacementFont null replacementTMPFont null) { EditorUtility.DisplayDialog(错误, 请至少指定一种新字体资源Text或TMP。, 确定); return; } StartBatchReplace(); } // 显示进度或结果信息 if (!string.IsNullOrEmpty(progressInfo)) { EditorGUILayout.HelpBox(progressInfo, MessageType.Info); } } }关键点解析[MenuItem(Tools/批量替换字体工具)]这行代码在Unity编辑器的菜单栏Tools下添加了一个菜单项点击即可打开我们的工具窗口。我们定义了新旧字体的配置项。注意oldFontName是字符串因为我们是通过字体资源的名称来匹配需要替换的旧字体Arial。而新字体是通过资源引用直接赋值。提供了搜索路径默认是Assets你可以修改为Assets/Resources/Prefabs等更具体的路径提高扫描效率。3.2 核心替换逻辑实现接下来是核心的StartBatchReplace方法。它的工作流程是查找所有预制体 - 加载每个预制体 - 检查其GameObject上的文本组件 - 匹配并替换字体 - 保存预制体。private void StartBatchReplace() { // 1. 获取所有预制体文件路径 string[] prefabPaths Directory.GetFiles(searchPath, *.prefab, includeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); if (prefabPaths.Length 0) { progressInfo $在路径 {searchPath} 下未找到.prefab文件。; return; } int totalPrefabs prefabPaths.Length; int processedCount 0; int modifiedCount 0; // 2. 遍历每个预制体 for (int i 0; i prefabPaths.Length; i) { string path prefabPaths[i]; // 更新进度条 if (EditorUtility.DisplayCancelableProgressBar(批量替换字体, $正在处理: {Path.GetFileName(path)} ({i1}/{totalPrefabs}), (float)i / totalPrefabs)) { // 用户取消了操作 progressInfo 操作被用户取消。; EditorUtility.ClearProgressBar(); return; } // 加载预制体 GameObject prefab AssetDatabase.LoadAssetAtPathGameObject(path); if (prefab null) continue; bool isPrefabModified false; // 3. 获取预制体根对象及所有子对象包括未激活的 // 注意预制体可能嵌套需要递归查找。这里使用简单情况处理根预制体直接引用的组件。 // 更严谨的做法是遍历prefab.GetComponentsInChildrenComponent(true)但这里为清晰起见先处理直接挂在预制体根对象上的组件。 // 实际脚本中应使用递归或GetComponentsInChildren。 // 模拟处理逻辑这里我们假设需要处理预制体实例及其所有后代 // 由于在Asset模式下不能直接实例化我们需要以“编辑Asset”的模式进行这涉及到PrefabUtility。 // 更安全通用的方法是打开Prefab编辑模式或使用SerializedObject。 // 下面是一种通过PrefabUtility打开预制体进行编辑的方法 // 打开预制体进行编辑返回一个GameObject实例代表预制体根 GameObject prefabInstanceRoot PrefabUtility.LoadPrefabContents(path); // 查找所有文本组件 Text[] textComponents prefabInstanceRoot.GetComponentsInChildrenText(true); TMPro.TextMeshProUGUI[] tmpComponents prefabInstanceRoot.GetComponentsInChildrenTMPro.TextMeshProUGUI(true); // 处理传统Text组件 foreach (Text textComp in textComponents) { if (textComp.font ! null textComp.font.name.Contains(oldFontName)) { textComp.font replacementFont; isPrefabModified true; Debug.Log($在预制体 {path} 的 {textComp.gameObject.name} 上替换了Text字体。); } } // 处理TMP组件 foreach (TMPro.TextMeshProUGUI tmpComp in tmpComponents) { if (tmpComp.font ! null tmpComp.font.name.Contains(oldFontName)) { tmpComp.font replacementTMPFont; isPrefabModified true; Debug.Log($在预制体 {path} 的 {tmpComp.gameObject.name} 上替换了TMP字体。); } } // 4. 如果预制体被修改则保存 if (isPrefabModified) { PrefabUtility.SaveAsPrefabAsset(prefabInstanceRoot, path); modifiedCount; } // 5. 清理临时加载的预制体实例 PrefabUtility.UnloadPrefabContents(prefabInstanceRoot); processedCount; } // 清理进度条 EditorUtility.ClearProgressBar(); // 显示结果 progressInfo $处理完成\n扫描预制体总数: {totalPrefabs}\n成功处理: {processedCount}\n实际修改了: {modifiedCount}; // 强制刷新Asset数据库让编辑器立即显示更改 AssetDatabase.Refresh(); Debug.Log($批量替换字体完成。修改了 {modifiedCount} 个预制体。); }关键点与避坑指南PrefabUtility.LoadPrefabContents与SaveAsPrefabAsset这是Unity 2018.3后推荐的编辑预制体Asset的方法。它加载一个可编辑的预制体实例到内存我们修改这个实例后再保存回原路径。切记最后一定要调用PrefabUtility.UnloadPrefabContents来卸载否则会造成资源泄露。字体匹配逻辑textComp.font.name.Contains(oldFontName)是一种宽松匹配。有时Arial字体资源在Unity中的名字可能带有后缀如“Arial (TTF)”。使用Contains比严格相等 () 更安全。你也可以根据font.name或font.ToString()来调整匹配策略。递归查找组件GetComponentsInChildrenText(true)中的参数true表示包含未激活Inactive的GameObject。这非常重要因为UI元素可能被默认禁用。进度反馈使用EditorUtility.DisplayCancelableProgressBar显示进度条并允许用户取消长时间的操作这是编写友好编辑器工具的好习惯。AssetDatabase.Refresh()保存修改后调用确保Project窗口立即更新看到修改后的预制体。3.3 增强功能处理字体回退Fallback和材质对于TMP直接替换font属性有时可能不够。如果旧字体使用了特定的材质或材质属性如SDF缩放、描边效果直接替换字体Asset可能会导致这些效果丢失因为新的TMP_FontAsset自带其默认材质。更健壮的TMP替换策略替换字体Asset。检查并尝试保持原有的材质实例或材质属性覆盖。一个常见的做法是在替换字体后如果旧组件有自定义的字体材质fontMaterial或fontSharedMaterial我们可以尝试将新字体的默认材质属性如_FaceColor,_OutlineWidth等复制到旧材质上或者直接应用旧材质如果兼容。但这涉及材质参数拷贝比较复杂。对于大多数“单纯换字体”的需求直接替换font属性是可行的。但如果你的项目中原Arial TMP字体有特殊的材质设置你可能需要手动检查替换后的效果或编写额外的逻辑来迁移材质属性。一个简单的增强是在工具界面增加一个选项“为TMP创建并使用新的材质实例”这样替换后会生成一个基于新字体默认材质的新材质实例避免多个预制体共享材质导致意外联动修改。4. 完整脚本与使用步骤将上述代码块整合形成一个完整的BatchReplaceFontTool.cs文件放入Assets/Editor/文件夹。下面给出清晰的实操步骤。4.1 前置准备导入新字体将支持中文的.ttf/.otf文件如“SourceHanSansSC-Regular.otf”拖入Assets/Fonts/文件夹。创建TMP字体资源如果使用TMP在Project窗口右键点击该字体文件 -Create - TextMeshPro - Font Asset。将其保存在合适位置如Assets/Fonts/TMP/。备份项目在进行任何批量操作前务必使用版本控制系统如Git提交或手动复制备份整个项目。这是铁律。4.2 使用工具在Unity编辑器顶部菜单栏点击Tools - 批量替换字体工具。在打开的窗口中旧字体名称输入Arial如果你的旧字体名是别的就输入对应的名字。新字体 (For Text)从Project窗口拖入你为传统Text组件准备的Font资源例如SourceHanSansSC-Regular。新字体 (For TMP)从Project窗口拖入你创建的TMP_FontAsset资源。搜索路径默认是Assets会扫描整个项目。如果你确定预制体都在某个文件夹下可以输入更具体的路径如Assets/_Prefabs/UI以大幅提升速度。包含子目录通常勾选。点击“开始扫描并替换”。等待进度条完成。处理过程中可以在Console窗口看到详细的替换日志。完成后工具窗口会显示处理统计信息。4.3 验证结果随机打开几个被修改的预制体检查其Text或TMP组件的Font/Font Asset属性确认已更改为新字体。在场景中实例化这些预制体或在游戏运行时检查UI确认中文显示正常没有“口口口”。检查是否有预制体因为嵌套引用、字体名称不完全匹配等原因未被成功替换。我们的脚本日志会输出每个修改操作可以根据日志进行复查。5. 常见问题与排查技巧在实际操作中你可能会遇到以下问题。这里记录了我踩过的坑和解决方案。5.1 字体替换了但显示没变或变方块问题脚本执行成功预制体资源也显示字体已更改但运行游戏或预览时文字没变或变成了方块。排查字体资源本身不支持中文确认你导入的字体文件包含中文字形。对于TMP尤其要检查创建的TMP_FontAsset的字符集Atlas。默认创建时可能只包含ASCII字符。你需要选中你的TMP_FontAsset。在Inspector窗口找到Character Set从下拉框中选择Custom Set或CJK Characters如果选项存在。更可靠的方法是点击Update Atlas Texture按钮并确保Character List包含了你的项目需要用到的所有中文可以粘贴一段中文文本进去。材质/Shader问题TMP字体与材质、Shader绑定。如果新字体的默认材质Shader与旧的不同例如旧的是SDF新的是Bitmap会导致渲染异常。确保替换后的TMP组件使用的材质和Shader是合适的。字体回退Fallback设置TMP组件有Fallback Font Assets列表。如果主字体缺失某些字符会尝试用回退字体显示。检查这里是否还残留着对旧Arial字体Asset的引用如果有移除或替换它。5.2 脚本报错或无法运行问题打开工具窗口时报编译错误或点击按钮后报空引用、序列化错误。排查脚本位置确保脚本放在Assets/Editor/或其子目录下。只有放在Editor文件夹中的脚本才能使用EditorWindow、EditorUtility、PrefabUtility等编辑器API。TextMeshPro命名空间脚本开头需要using TMPro;。如果报错检查你的项目是否已正确导入TextMeshPro包Window - TextMeshPro - Import TMP Essential Resources。预制体加载失败某些预制体可能已损坏或依赖缺失。脚本中的AssetDatabase.LoadAssetAtPath返回null时会跳过。查看Console窗口是否有加载错误。权限与锁定极少数情况下如果预制体文件被其他进程如文件浏览器、Git客户端锁定可能导致保存失败。关闭可能占用文件的程序。5.3 性能优化与大规模项目处理问题项目有数千个预制体扫描过程非常慢甚至导致编辑器卡顿或无响应。优化技巧缩小搜索范围充分利用searchPath参数只扫描UI预制体所在的特定文件夹而不是整个Assets。分批次处理修改脚本每次处理一定数量如50个预制体后调用EditorUtility.UnloadUnusedAssets()和Resources.UnloadUnusedAssets()在编辑器模式下谨慎使用来释放内存然后继续下一批。可以给进度条添加“暂停/继续”功能。异步处理对于极其庞大的项目可以考虑使用EditorApplication.update回调或async/await需注意Unity编辑器的主线程限制来将任务分散到多帧执行避免阻塞主线程。缓存与过滤首次扫描时可以构建一个预制体路径列表并缓存后续操作如果路径没变可以直接使用缓存列表。或者先通过AssetDatabase的标签、类型进行初步过滤。5.4 处理嵌套预制体Prefab Variants与场景引用问题我们的脚本主要处理独立的.prefab文件。但对于嵌套预制体一个预制体引用另一个预制体或预制体变体Prefab Variant直接修改根预制体可能无法覆盖其内部引用的子预制体的字体。解决方案脚本中的GetComponentsInChildren(true)会深入到嵌套的子GameObject但如果子对象本身是一个预制体实例在Hierarchy中显示为蓝色那么修改的是这个实例的覆盖Override而不是其源预制体Asset。要修改源预制体需要递归地找到这些嵌套预制体引用并单独加载它们的源预制体路径进行处理。这增加了复杂度。一个实用的建议是先运行一遍当前脚本它能够修改所有直接挂在预制体对象上的字体引用包括嵌套实例的覆盖。然后手动检查那些重要的、作为基础模块的子预制体对它们单独再运行一次脚本。对于预制体变体修改其基体Base预制体的字体会自动被变体继承除非变体自己有覆盖。5.5 操作安全与回滚重中之重执行前必须备份。使用Git的话确保所有更改已提交或者至少stash起来。小范围测试第一次使用时先在searchPath中指定一个只包含几个测试预制体的文件夹验证脚本行为符合预期后再全量运行。记录更改脚本中的Debug.Log会输出修改记录。你也可以修改脚本将更改记录预制体路径、组件名、旧字体、新字体写入一个文本文件方便后续审计和回滚。回滚方法如果不幸改错了而你又没有备份唯一的希望是依赖版本控制的历史记录。如果连版本控制都没有那就很难完美恢复了。这再次强调了备份的重要性。这个工具脚本的核心逻辑是通用的你可以根据自己项目的具体需求进行扩展例如增加对SpriteRenderer组件上TextMesh旧版3D文本的支持或者增加字体大小、颜色等属性的批量调整功能。掌握了Editor脚本的编写思路你就拥有了自动化处理Unity项目中各类批量任务的钥匙。