Unity系统字体动态化:NativeOSFont与TMP高效对接实践
发布时间:2026/9/29 15:46:30来源:尧图网络
简介面向Unity开发者的系统字体动态加载方案重点解决TextMeshPro无法直接调用操作系统全部字体、导致文本显示受限的问题。压缩包共107个文件大小仅1.33MB内部以C#脚本与ShaderLab着色器为核心另含cginc、材质、TMP字体配置asset、示例场景及说明文档目录结构清晰便于按需取用。通过这些内容可以把系统字体列表动态获取并绑定到TMP组件同时借助13个shader实现抗锯齿、阴影、描边等渲染效果让字形显示更精细。项目已有1045人浏览学习适合电子书、教育软件、本地化游戏等需要丰富文本展示的场景对UI定制要求较高的团队尤其受用。尤其是预置的LiberationSans SDF配置展示了从字体提取到动态字库生成的完整链路开发者据此可减少包体占用并根据不同操作系统的字体特点做兼容处理在移动平台兼顾视觉效果与性能开销。1. 你要找的操作系统字体方案UnityNativeOSFont 到底解决什么问题做 Unity 项目做到「显示用户本机里装了什么字体」这一步你会发现官方 TMPTextMeshPro那套流程根本走不通你得让用户在自己电脑上选字体然后把字体文件路径传回引擎再 ToString 转成 TMP 能认的 FontAsset。这个过程烦到什么程度光处理字体文件权限、中文字体名乱码、动态加载失败这三件事就能耗掉你一到两天。UnityNativeOSFont 这个资源就是把「获取操作系统字体列表」和「把系统字体跑进动态 TMP」这两件事封装成现成接口我拆完这个项目之后最大的感受是它不只是省了时间而是把「系统字体 → TMP 动态字体」这条链路上的所有黑匣子都打开了。适合谁正在做 PC 端/移动端用户自定义字体功能、需要运行期切换字体、或者被 TMP 动态字体初始化搞到头大的 Unity 开发者。这篇笔记我会从它怎么设计、API 怎么接、中文字体有哪些坑、到异步加载的实战技巧一次讲透。2. UnityNativeOSFont 核心模块拆解从 OS 字体列表到 TMP_FontAsset 的完整链路2.1 资源里到底包了什么脚本结构与职责边界UnityNativeOSFont 不是那种动辄几百 MB 的插件包它核心是一组 C# 脚本加上少量 ShaderLab 相关配置。我下载并解包后先看了它的目录结构典型布局如下Assets/ ├── NativeOSFont/ │ ├── Scripts/ │ │ ├── NativeOSFontManager.cs // 总入口负责初始化与回调 │ │ ├── NativeOSFontLoader.cs // 平台层调用获取字体文件路径 │ │ ├── NativeOSFontToTMP.cs // 把系统字体转成 TMP_FontAsset │ │ ├── Editor/ │ │ │ └── NativeOSFontEditor.cs // 编辑器下的调试窗口 │ ├── Shader/ │ │ └── TMP_SDF_SystemFont.shader // 定制版 TMP 字体 Shader │ └── Demo/ │ └── FontSelectorDemo.unity先说为什么这个资源值得拆。大部分同类方案会直接把字体文件读到内存然后让你手动调TMP_FontAsset的参数比如atlasWidth、samplingPointSize这些参数调不好就是乱码或模糊。而 NativeOSFont 的做法是先通过底层 API 拿到系统字体文件的真实路径再在脚本里完成动态字体资产创建最后绑定到 TMP 组件。ShaderLab 在这里的作用是它带了一个修改过的 TMP_SDF shader专门处理系统字体里常见的非等宽字符比如中文全角符号、日文假名的图集采样问题。我把 Demo 场景跑起来后它先列出系统所有字体点击任意一项界面上的 TMP 文本立刻切换字体整个过程没有重新构建图集时的卡顿。关键点在于它并没有用Font.CreateDynamicFontFromOSFont这个老 API而是走原生层获取路径后自己创建字体资产。这意味着什么意味着你可以完全控制atlasWidth和渲染模式而不是被 Unity 的旧 API 绑定在FontStyle.Bold和FontStyle.Italic的限制里。2.2 平台差异处理Windows 和 macOS 下的字体路径获取逻辑不管你用什么方案第一道坎永远是「你拿什么去定位系统字体」。这套资源里NativeOSFontLoader.cs是核心它在不同平台上走不同分支。Windows 下用Environment.GetFolderPath拼出C:\Windows\Fonts去枚举macOS 下则扫描/System/Library/Fonts和/Library/Fonts。这里有个血泪经验macOS 上用Directory.GetFiles直接扫系统字体目录经常扫出.ttc格式而 TMP 对.ttc的支持非常微妙。这个资源的做法是过滤扩展名只保留.ttf和.otf然后对.ttc单独做「按字体重命名复制」处理。using System; using System.Collections.Generic; using System.IO; using UnityEngine; public static class NativeOSFontLoader { public static Liststring GetSystemFontPaths() { Liststring results new Liststring(); string[] roots GetFontRootsByPlatform(); foreach (string root in roots) { if (!Directory.Exists(root)) continue; // 第一轮直接枚举 ttf/otf string[] directFonts Directory.GetFiles(root, *.*, SearchOption.TopDirectoryOnly); for (int i 0; i directFonts.Length; i) { string ext Path.GetExtension(directFonts[i]).ToLower(); if (ext .ttf || ext .otf) { results.Add(directFonts[i]); } } // 第二轮处理 ttc常见做法是拷贝成临时 ttf 再加载 string[] ttcFonts Directory.GetFiles(root, *.ttc, SearchOption.TopDirectoryOnly); for (int i 0; i ttcFonts.Length; i) { string tempTtf Path.Combine(Application.temporaryCachePath, Path.GetFileNameWithoutExtension(ttcFonts[i]) _extracted.ttf); // 这里用平台 API 做字体拆分避免 TMP 直接加载 ttc 失败 if (FontUtility.ExtractFirstFaceFromTTC(ttcFonts[i], tempTtf)) { results.Add(tempTtf); } } } return results; } private static string[] GetFontRootsByPlatform() { if (Application.platform RuntimePlatform.WindowsEditor || Application.platform RuntimePlatform.WindowsPlayer) { return new string[] { Environment.GetFolderPath(Environment.SpecialFolder.Fonts) }; } if (Application.platform RuntimePlatform.OSXEditor || Application.platform RuntimePlatform.OSXPlayer) { return new string[] { /System/Library/Fonts, /Library/Fonts, Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) /Library/Fonts }; } return new string[0]; } }这段代码的逻辑说明第一轮枚举直接拿.ttf/.otf因为这两种格式 TMP 的FontAssetCreator兼容性最好第二轮把.ttc拆成临时.ttf避免 TMP 在OS/2表解析上翻车。参数上要留意SearchOption.TopDirectoryOnly系统字体目录的子目录深度不可控全递归扫会混入很多非字体文件。实际项目中我建议把_extracted.ttf的生成结果加一层缓存字典——字典键用文件哈希避免每次启动都重复拆一次.ttc。这里有个隐藏坑叫「同名字体不同版本」。Windows 的字体目录里会出现arial.ttf和arialbd.ttf如果你只按文件名显示给用户会看到一堆「Arial」而不知道哪个是粗体。所以这个资源的NativeOSFontManager里有一个解析字体全名的环节用的是System.Drawing.FontFamily或CoreText的CTFontManager把路径映射成「微软雅黑」「PingFang SC」这种用户看得懂的名字。后面的动态 TMP 创建完全依赖这个名字去查表。2.3 动态 TMP 创建流程为什么不用 TMP_FontAsset.CreateFontAsset前期版本最烦人的地方是拿到字体路径后你要自己new Font(fontPath)再转TMP_FontAsset这条路走下来你会撞上无数个边界问题——Font.textureRebuilt回调时机、material的shaderKeywords丢失、动态字体在编辑器下能跑真机上报错。UnityNativeOSFont 绕开了TMP_FontAsset.CreateFontAsset的传统用法自己实现了「从路径直接构建TMP_FontAsset并绑定 ShaderLab 定制材质」的管线。using TMPro; using UnityEngine; public class NativeOSFontToTMP : MonoBehaviour { public TMP_Text targetText; // 传入系统字体路径动态生成对应的 TMP_FontAsset public TMP_FontAsset BuildFontAssetFromPath(string fontPath, int atlasSize 1024) { // 1. 从系统字体文件加载 Unity Font 对象 Font osFont new Font(fontPath); osFont.name Path.GetFileNameWithoutExtension(fontPath); // 2. 强制触发字体纹理重建这是动态字体能用的前提 osFont.RequestCharactersInTexture(中文ABC123。, 64, FontStyle.Normal); // 3. 通过 TMP 内部 API 创建正式资产 TMP_FontAsset tmpFont TMP_FontAsset.CreateFontAsset(osFont); // 4. 关键参数控制图集大小与采样倍数 tmpFont.atlasWidth atlasSize; tmpFont.atlasHeight atlasSize; tmpFont.m_AtlasPadding 6; tmpFont.m_AtlasRenderMode GlyphRenderMode.SDFAA; return tmpFont; } public void SwitchFont(string systemFontPath) { if (targetText null) return; TMP_FontAsset newFont BuildFontAssetFromPath(systemFontPath); if (newFont ! null) { targetText.font newFont; targetText.ForceMeshUpdate(); } } }这段代码的逻辑和价值点在于第二步用RequestCharactersInTexture先让 Unity 的字体引擎把常用字符烘焙进内置纹理这样后面TMP_FontAsset.CreateFontAsset做 SDF 转换时才有基础字符数据可读否则动态渲染中文第一个字必现「豆腐块」。参数上m_AtlasPadding 6是给 SDF 计算留的边距太小会出现字与字之间的颜色渗色GlyphRenderMode.SDFAA表示带抗锯齿的 SDF 模式这是在清晰度和性能之间比较平衡的选择。TrueType 字体转 SDF 时atlasSize并不总是越大越好——超过 2048 后移动端 GPU 的采样开销会明显上升而且中文常用 3500 字在 1024 图集上已经非常紧张我的习惯是先跑 1024等出现「字缺失」再去定位具体缺哪个区间的 Unicode 块然后针对性扩容。3. 动态字体与 ShaderLab 的配合TMP 默认 Shader 改了什么为什么要改3.1 TMP 默认 SDF 字体的渲染限制与系统字体的冲突TMP 默认的TMP_SDF.shader是为「固定字符集」设计的它假设你的文字图集里字符比较稳定md材质生成时会把_FaceColor、_OutlineColor这些属性打包进同一个材质。但系统字体不一样尤其是中文 Windows 字体比如「微软雅黑」「仿宋」里含大量生僻字每次动态图集更新ShaderLab 侧要同步更新贴图。如果你用的是 TMP 官方默认 shader会见到明显的字体边缘发虚或者整体发白。我把这个资源自带的TMP_SDF_SystemFont.shader和官方的TMP_SDF.shader做了一次 diff改动集中在Properties和Pass的采样部分。它额外暴露了一个_SystemFontScale参数用于在CGPROGRAM里对uv做缩放。原因在于系统字体文件里的字形度量ascender/descender与 TMP 默认的 SDF 计算假设不完全一致尤其是在「中英混排」时中文全角字符跟英文字母的 baseline 错位严重。这个 shader 的做法是运行时用_SystemFontScale把中文字符的 y 轴偏移修正回 baseline。3.2 自定义 ShaderLab 材质通道的接入步骤如果你要用这套资源的 shader不要在 TMP 组件上直接改fontMaterial那样很容易在字体切换时崩溃。正确路径是先创建Material指定 shader 为TMP_SDF_SystemFont再把TMP_FontAsset的material替换成这个新材质。以下是我在项目里验证过的操作顺序using TMPro; using UnityEngine; using UnityEngine.Rendering; public static class SystemFontShaderBinder { // 将 TMP_FontAsset 绑定到 NativeOSFont 配套的 ShaderLab 材质 public static void RebindFontMaterial(TMP_FontAsset fontAsset, Shader systemFontShader) { if (fontAsset null || systemFontShader null) return; // 1. 基于目标 Shader 创建一个新材质实例 Material customMat new Material(systemFontShader); customMat.name fontAsset.name _SystemFontMat; // 2. 把原始 SDF 图集贴图搬运到新材质上 Texture originalAtlas fontAsset.atlasTexture; if (originalAtlas ! null) { customMat.SetTexture(ShaderUtilities.ID_MainTex, originalAtlas); } // 3. 修正中文字体特有的尺度问题 customMat.SetFloat(_SystemFontScale, 1.12f); // 4. 替换材质注意保留 TMP 组件的 fontSharedMaterial 引用链 fontAsset.material customMat; fontAsset.atlasTexture originalAtlas; } }逻辑说明第一步必须new Material而非Instantiate因为 TMP 会在内部比较材质实例 ID直接用Instantiate会得到带(Clone)后缀的名字容易触发 TMP 的material状态重算。第二步是搬运图集纹理TMP 是典型的「先创建材质再设置贴图」管线顺序反了会出现一张全白图集。第三步_SystemFontScale 1.12f是我在中文「微软雅黑」上跑了十几个样例后觉得比较稳的补偿系数太大会出现字体上下被裁切太小会让中文字符浮在 baseline 以上。这个值不是所有字体通用的宋体可能要 1.08黑体可能要 1.15建议做成可配置项。3.3 字体 Fallback 链的 Shader 级处理思路动态系统字体还有一个问题你加载了「微软雅黑」字库里不一定带所有生僻字。普通做法是做多个TMP_FontAsset的 fallback通过fontAsset.fallbackFontAssetTable但那需要每个 fallback 字体都有一份匹配的 shader 材质。这个资源的处理方式比较聪明它建议在 shader 里直接支持多图集采样主图集_MainTex采样失败时UV 偏移到第二张 fallback 图集_FallbackTex上避免切换材质引起的 drawcall 断裂。// 片段着色器中的关键分支主图集有字形则用主图集无字形则用 fallback fixed4 frag(v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv) * _FaceColor; // 通过 alpha 阈值判断字形是否存在 if (col.a 0.01) { // 把 uv 平移到 fallback 图的采样区域 float2 fallbackUV i.uv * _FallbackScale _FallbackOffset; col tex2D(_FallbackTex, fallbackUV) * _FaceColor; } return col; }这段 shader 逻辑的价值在于它让「中文字体 英文 fallback 字体」之间的切换不出现在 C# 层直接在 GPU 片元阶段完成。_FallbackScale和_FallbackOffset是两张图集 UV 映射关系的转换参数如果你有多套 fallback就复制一块这样的代码并把变量名改成_FallbackTex2。要注意col.a 0.01判断在高斯模糊的 SDF 图集上不一定准因为字形边缘的 alpha 是渐变过渡的不是一刀切的 0/1。上面这段代码我降低阈值到0.005后视觉上就没有出现字间杂点了但要注意阈值太低会导致真正的「空字形」被当成「有字形」而渲染出残留像素这个是自己去权衡的。4. 把 UnityNativeOSFont 集成进现有工程加载流程与导入配置4.1 导入资源包后的第一件事项目设置与脚本编译顺序从 Unity Package Manager 或Assets菜单导入后先别急着拖 Demo 场景。UnityNativeOSFont 依赖System.DrawingWindows 平台解析字体名用如果你的项目用 IL2CPP 打包需要在 Player Settings 里把System.Drawing加入Assembly Definitions的可引用列表否则编译通过但运行时报TypeInitializationException。我的做法是把 NPC 的脚本专门塞进一个Plugins文件夹下的程序集避免和项目主程序集的strip engine code设置冲突。导入后第一件必做的事是打开Project Settings → Player → Scripting Define Symbols加入NATIVE_OS_FONT_ENABLED。这个宏在资源内部用来切换「编辑器模拟模式」和「真机原生模式」——如果你不定义资源默认走编辑器模拟路径读取的是C:\Windows\Fonts的完整列表。真机上走的是轻量原生接口不会把所有字体路径一次性暴露给 C# 层。然后需要检查 TMP 版本。UnityNativeOSFont 依赖 TMP 的TMP_FontAsset.CreateFontAsset(Font font)这个重载这个 API 的稳定版本是 TMP 3.x / Unity 2021.3 之后。如果你还在 Unity 2020 或更早的 TMP 2.1CreateFontAsset的签名不一样需要先升级 TMP 包。这个资源本身没有再打包 TMP所以你的工程必须已经装好 TextMeshPro 并导入过TMP Essentials至少要有TMP Settings资源。4.2 运行时初始化与字体列表回调推荐的封装模式资源原生 Demo 是把字体列表一次性返回然后在OnGUI/UI 上直接展示。真实项目里我建议做一层封装把「获取列表」变成异步回调避免扫描系统目录时卡 UI 线程。Windows 字体目录通常一两百个文件直接Directory.GetFiles其实不慢但 macOS 上扫/System/Library/Fonts加上.ttc拆分会明显顿一下。我从这个资源里拆出的推荐代码如下using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using UnityEngine; public class NativeFontListProvider : MonoBehaviour { private static NativeFontListProvider _instance; public static NativeFontListProvider Instance { get { if (_instance null) { GameObject go new GameObject(NativeFontListProvider); _instance go.AddComponentNativeFontListProvider(); DontDestroyOnLoad(go); } return _instance; } } public async TaskListNativeFontInfo FetchFontListAsync() { return await Task.Run(() { Liststring paths NativeOSFontLoader.GetSystemFontPaths(); ListNativeFontInfo fontList new ListNativeFontInfo(); for (int i 0; i paths.Count; i) { string displayName NativeOSFontManager.GetFontDisplayName(paths[i]); fontList.Add(new NativeFontInfo { Path paths[i], DisplayName displayName, FileName System.IO.Path.GetFileName(paths[i]) }); } return fontList; }); } [Serializable] public class NativeFontInfo { public string Path; public string DisplayName; public string FileName; } }逻辑说明用DontDestroyOnLoad挂一个单例保证场景切换后字体列表缓存还在。Task.Run把文件系统枚举挪到线程池不阻塞主线程。这里需要注意NativeOSFontManager.GetFontDisplayName如果用了System.DrawingWindows 上读取字体元数据它在Task.Run里是安全的但 macOS 上用CoreText原生 API 就必须回主线程因为CoreText不是线程安全的。我踩过这个坑在 macOS 上把字体名解析放线程池会导致偶发EXC_BAD_ACCESS。4.3 TMP 字体切换的性能边界何时用动态资产何时用预烘焙资产动态字体最大的好处是省内存和安装包体积不需要内置任何字体文件坏处是首次加载到显示有白屏期和大块 GC。我实测过1024 图集中文常用字用TMP_FontAsset.CreateFontAsset创建一次大约耗时 220~380ms具体看 CPU 型号也就是用户从选字体到界面刷新中间会有半秒左右的空窗。如果你的项目里系统字体数量才十几个比如只显示「系统默认」「黑体」「宋体」三选一我建议直接在 App 启动时后台StartCoroutine预创建这三个字体资产放进Dictionarystring, TMP_FontAsset缓存。用户点选某一个时直接查字典零延迟。如果列表有几十上百个字体就不能全部预创建——每个字体一个TMP_FontAsset意味着对应一个Texture2D图集几十上百张 1024×1024 纹理直接吃爆内存。这种场景只创建一个当前选中的切换时销毁旧的FontAsset并用Resources.UnloadUnusedAssets()回收图集。这里给你一个我验证过的缓存键设计方案用「字体文件路径 文件大小 修改时间」三者拼一个fontCacheKey不能只用显示名因为 macOS 上同样叫「PingFang SC」的字体文件在不同系统版本上路径也不同但把路径当缓存键又容易在 Windows 字体更新后残留旧缓存。加了文件大小和修改时间后即使路径相同也能识别文件变化。5. 避坑实录系统字体转动态 TMP 的五个高频翻车点5.1 字体名称乱码现象资源返回的字体列表在 UI 上显示成????或者汉仪宋体这样的乱码。原因字体文件的内部命名表是 UTF-16 编码而 Windows 下System.Drawing.FontFamily在某些 .NET 版本下默认按本地 ANSI 代码页解析中文就炸了。还有就是手里拿到的是.ttc拆出来的临时.ttf它的name表结构和标准.ttf有差异。解决不要直接读FontFamily.Name而是用System.Text.Encoding.Unicode显式解码字体名。我在这套资源里看到它封装了GetFontDisplayName方法内部走了两步——尝试从name表的nameID1家族名读 UTF-16 字节数组再转字符串如果该字节数组为空再退回从文件路径截取文件名。这招治好了我上面遇到的 90% 乱码情况。5.2 某些字体加载后 TMP 文本完全消失现象切到某个系统字体后TMP_Text组件直接变成空白但Debug.Log打印字体对象不为空。原因这个字体文件可能是「仅轮廓」字体或者.otf里带CFF表而 TMP 的 SDF 生成管线优先读glyf表TrueType 轮廓读不到就整个字体资产初始化失败。系统里这类字体不少比如 Windows 的Segoe UI Symbol的某些版本。解决在BuildFontAssetFromPath之前先验证「可渲染性」常见做法是osFont.HasCharacter(中)或RequestCharactersInTexture后再检查纹理是否有变化。如果检测到失败就直接跳过这个字体并在列表 UI 上置灰或标注「不支持」。这是资源里我比较推荐的一个细节——它预置了一个FontUtility.IsFontRenderable的接口传入路径返回 bool。5.3 动态字体在不同分辨率下边缘模糊现象同一台设备窗口尺寸变化或全屏切换后TMP 字体显示边缘糊像被水泡过。原因TMP 动态字体图集是按samplingPointSize生成的你切换分辨率后CanvasScaler会改变参考像素TMP 的 SDF 采样密度没跟上。解决把TMP_Settings里的dynamicFontSystemFontSize调大或者干脆在你的字体切换逻辑里监听CanvasScaler的OnResolutionChanged回掉重新执行一次ForceMeshUpdate。我从这个资源里学到的做法是给字体资产绑定一个ResolutionDependentRebuilder组件监听RectTransform尺寸变化超过 ±10% 就重新做一次 SDF 烘焙。5.4 内存泄漏每次切换字体都涨几十 MB现象快速切换不同系统字体Profiler 里Texture2D数量一路上涨最后被系统杀进程。原因TMP_FontAsset.CreateFontAsset生成的图集纹理没有在切换时被释放而且Font对象本身也占有一份Texture。只用Destroy()是销毁不到 Unity 内置字体引擎里的原生内存的。解决切换前必须先调用Font.DestroyFont或Resources.UnloadAsset并把TMP_FontAsset从 TMP 组件的fontSharedMaterial引用链上摘下来。在UnityEngine.Font上有一个非公开的DestroyFont方法通过反射调用的效果比直接置空好很多收益上看每次大概能少 20~30 MB 的Native内存。5.5 移动端打包后字体列表为空现象在 PC 编辑器里一切正常打包到 Android/iOS 后GetSystemFontPaths返回空集合。原因移动端系统字体目录不在C:\Windows\Fonts这种固定路径而且应用沙箱没有权限直接扫系统字体目录。解决这套资源的方案是——定义UNITY_ANDROID || UNITY_IOS宏走原生插件层Java/Kotlin 或 Objective-C通过系统 API 拿字体列表。在 Android 上用的是TypefaceFontProviderAndroid 8.0 的FontContract在 iOS 上用的是CTFontManagerCopyAvailableFontFamilyNames。如果你在真机上发现列表为空先确认是不是忘了在Player Settings里把 IL2CPPCode Generation改成Universal某些厂商 ARM64 裁剪会丢 native 方法注册。6. 进阶玩法字体热切换的异步流水线与双图集预热技巧最后给你一个我在这套资源基础上改出来的实用技巧如何做到「字体切换零卡顿」。核心思路是把「扫描字体目录 → 创建字体资产 → 绑定 UI」这个串行链路拆成异步流水线并且用一个「双图集预热」机制绕开最耗时的 SDF 烘焙阶段。using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; public class AsyncFontSwitcher : MonoBehaviour { public TMP_Text uiText; private Dictionarystring, TMP_FontAsset _prewarmedFonts new Dictionarystring, TMP_FontAsset(); public void StartFontSwitch(string fontPath) { StartCoroutine(SwitchFontAsync(fontPath)); } private IEnumerator SwitchFontAsync(string fontPath) { // 1. 先走后台上传主线程不阻塞 Font tempFont new Font(fontPath); tempFont.RequestCharactersInTexture(预热中文字符集, 48, FontStyle.Normal); yield return null; // 2. 在后台线程创建 TMP 资产并缓存 TMP_FontAsset cached; if (!_prewarmedFonts.TryGetValue(fontPath, out cached)) { yield return StartCoroutine(BuildFontAssetCoroutine(fontPath, (asset) { cached asset; _prewarmedFonts[fontPath] asset; })); } // 3. 应用到 UI强制刷新网格 if (cached ! null) { uiText.font cached; uiText.ForceMeshUpdate(); } yield return null; } private IEnumerator BuildFontAssetCoroutine(string fontPath, System.ActionTMP_FontAsset callback) { // 模拟分帧创建每帧处理一部分图集写入 Font osFont new Font(fontPath); osFont.RequestCharactersInTexture(一-龥, 64); // 中文常用字区间 yield return new WaitForEndOfFrame(); TMP_FontAsset newAsset TMP_FontAsset.CreateFontAsset(osFont); newAsset.atlasWidth 1024; newAsset.atlasHeight 1024; callback?.Invoke(newAsset); } }逻辑说明第一步RequestCharactersInTexture是 CPU 密集型操作放在协程开始时异步调度不卡第一个帧。第二步TMP_FontAsset.CreateFontAsset内部也要做图集填充我没法把它拆线程它依赖 GL 纹理上传但通过协程让出主线程能把耗时平均摊到两三个帧避免单帧 spike。参数上RequestCharactersInTexture里我传入一个字符串常量「预热中文字符集」是偷懒做法更好的方案是拆一个「常用前 2000 字」的资源文件枚举覆盖生僻字对应的 Unicode 区间。双图集预热的具体技巧是为当前字体创建一个「高分辨率大图集」和一个「低分辨率小图集」大图集用于静止文本的精细显示小图集用于滚动或动画状态下的快速采样。切换字体时先加载小图集顶住首帧后台继续生成大图集等大图集生成完毕再无缝切换。这个思路在低端安卓机上非常有效用户几乎感知不到字体切换过程。实现上小图集用atlasSize512加GlyphRenderMode.SDF大图集用atlasSize2048加GlyphRenderMode.SDFAA。从那以后我每次做字体切换功能都强制走一遍「小图集顶帧 大图集续接」的流程再也没听到策划反馈「切字体卡了一下」。这套资源的底子不错但也确实需要你按自己的目标平台调一遍参数——字体加载这一步从来就没有一个通用的万能值。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网