ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FGUI字体描边实战:原理、方案与性能优化

FGUI字体描边实战:原理、方案与性能优化 做游戏UI这些年被文字看不清逼到墙角的情况太多了活动底图是大面积高光渐变白色标题放上去直接隐身战斗飘字落在五颜六色的特效上黄字红字全糊成一团。每到这种时候最实用的一招就是给字体加描边。你要用UGUI拖一个Outline组件几秒钟就搞定但换成FGUIFairyGUI事情就没那么直接——编辑器里能配描边代码里也能改描边可你大概率会遇到改了不生效、描边被裁掉一半、动态字体下边缘全是毛刺这些幺蛾子。这篇文章就把我在Unity项目里用FGUI做字体描边的完整经验做个梳理从底层原理讲到代码实现再到实测踩坑和性能账按这个顺序看完基本就能自己把这个功能玩明白。1. 先看懂FGUI文字渲染链路再决定描边怎么做1.1 动态字体与位图字体描边走的不是同一条路FGUI里的文字组件主要是GTextField、GRichTextField和GTextInput它们本身并不存文字的成品图而是通过FGUI的字体系统去取每个字符的字形。以Unity端为例动态字体走Unity的Font引擎和TextGenerator字形会被实时合入一张动态字图集位图字体则是从BMFont制作好的贴图里直接抠字。这一步的差异直接决定了描边的实现方式。动态字体下FGUI运行时并不会给文字生成一张描边后的纹理它用的是伪描边技术——把当前字符的字形沿周围一圈偏移绘制若干份用描边颜色先铺出一层轮廓骨架最后再把正常颜色的字形画在正中覆盖。我翻过FGUI在Unity端的实现偏移绘制一般是按8个方向各来一次也就是说一个小字形最终会被画9遍。这个方案的好处是灵活颜色、粗细都能随时改代价就是网格顶点量和overdraw成倍上涨这笔账我们放到后面细算。位图字体则完全不同。位图字体的字形是从预设贴图里抠出来的你没法在运行时对一个静态的四边形做几何偏移来模拟描边所以TextFormat里的描边字段在位图字体上基本不生效。想让位图字带描边正确做法是让美术在BMFont工具里导出字符集时把描边直接烘焙进字体贴图。1.2 核心数据都在TextFormat里不管是编辑器里勾选描边还是代码里动态设置最终都汇到同一个数据结构——TextFormat。Unity SDK里跟描边相关的字段主要有三个hasOutline描边总开关false时后面两个字段会被忽略outlineColor描边颜色outlineSize描边粗细单位是逻辑像素代码路径非常短GTextField txt this.GetChild(title).asTextField; TextFormat format txt.textFormat; format.hasOutline true; format.outlineColor new Color32(0, 0, 0, 255); format.outlineSize 2; txt.textFormat format;这里有个容易在Code Review时被揪出来的行为TextFormat是引用类型getter返回的就是组件内部持有的那个对象。你改完字段不赋回去FGUI根本感知不到样式变了自然也不会重建文字网格。这个赋回动作的机制我们放到第三大节细讲先记住结论——get之后必须set。1.3 编辑器里的描边和代码描边是同一套机制FGUI编辑器属性面板里文字元件有描边和阴影两个配置项。美术勾上描边、填好颜色和粗细发布到引擎后UI包加载时这些数据会被灌进TextFormat。所以编辑器描边和代码描边本质上是同一件事区别只在赋值时机和灵活性。理解了这一层为什么编辑器不生效为什么代码能改这类问题就都迎刃而解了。2. 编辑器描边美术配置最省心但三个限制要提前知道2.1 配置路径和发布注意FGUI编辑器里给文本加描边的路径比较简单双击FGUI工程里的某个组件进入编辑状态从资源库拖一个文字元件到编辑区选中该元件右侧属性面板找到字符分组展开描边勾选启用设置颜色和粗细保存并发布UI包发布后Unity客户端刷新UI包运行时就能看到描边效果。这里有个容易被忽略的点编辑器里的编辑分辨率和游戏实际运行时的UI缩放比例不一定一致描边尺寸会跟随整个UI的缩放一起缩放。比如编辑分辨率下填的2px在0.5倍缩放的真机上实际显示大约就是1px。做UI规范时最好固定一个标准的reference resolution让美术和程序用同一把尺子说话否则经常会出现编辑器里刚好、真机上太细的扯皮。2.2 限制一静态配置扛不住动态状态编辑器描边最省心但也最死板。如果你遇到按钮未选中时白字黑描边、选中后金字金描边这种需求编辑器里那一份配置就不够用了。FGUI并没有把描边暴露成可以参与Tween的属性GTween能驱动GObject的坐标、缩放、透明度这些常规属性但TextFormat不是GObject的属性你不写代码去改它就只能一直维持编辑器里配好的那一套。所以我的习惯是界面层级稳定、文案固定的标题类文本用编辑器配描边凡是会跟随交互状态变化的文本一律预留代码接口。宁可一开始多写几行封装也别等美术需求提过来再返工。2.3 限制二编辑器效果和真机效果有偏差这是FGUI一个老生常谈的问题FGUI编辑器底层是Flash那套文本渲染而Unity端是Unity自己的字体渲染两边对同一款TTF字体的字形还原、抗锯齿、字间距处理都有细微差异。具体到描边就是——编辑器里看着刚刚好的2px描边到Unity真机上可能显得细了反过来也有显得粗的情况。尤其是中文字体笔画结构复杂偏差更明显。所以编辑器里的描边效果只能作为参考上线前必须在目标平台过一遍确认。2.4 限制三位图字体时编辑器的描边并不可控这一点跟1.1节呼应如果你的文字元件用的是BMFont导出的位图字体那编辑器里的描边配置大概率是看起来配了实际上没动静。这不是操作问题是渲染路径不支持。项目里需要和美术提前对齐位图字体的描边必须在字体制作阶段烘焙进去FGUI编辑器不会再帮你加一层。否则提测之后就会收到为什么这个按钮的描边没有的bug单。3. 运行时描边TextFormat的正确打开方式3.1 最小可运行示例运行时给FGUI文本加描边核心代码不长但完整流程是四步using FairyGUI; GTextField title this.GetChild(title).asTextField; // 1. 获取当前样式 TextFormat format title.textFormat; // 2. 修改描边字段 format.hasOutline true; format.outlineColor new Color32(30, 30, 30, 255); format.outlineSize 2; // 3. 写回样式触发文字网格重建 title.textFormat format;第3步是灵魂。前面说了TextFormat getter拿到的是组件内部同一个对象的引用所以第2步改字段时对象内容其实已经变了。但FGUI只会在textFormat的setter里打样式脏了需要重建网格的标记你不赋回去它就不知道要重画视觉上就是纹丝不动。我见过太多人卡在这一步打日志看format里的值全是对的屏幕却什么都没发生。3.2 动态开关与清理关闭描边时强烈建议把hasOutline直接置false而不是把outlineSize改成0。有些SDK版本对hasOutline为true但size为0的边界处理并不一致可能出现描边残留或者异常渲染。直接关总开关语义最清晰也不会踩到版本差异format.hasOutline false; title.textFormat format;3.3 实战交互状态切换的标准化封装我在项目里会把文字样式管理收敛成一个接口业务层不直接碰TextFormat。原因很简单TextFormat字段太多业务各自散改容易把别人的样式冲掉。封装之后大概是这样的public void SetTitleStyle(GTextField label, string text, Color mainColor, bool outline false, Color outlineColor default, int outlineSize 2) { TextFormat format label.textFormat; format.color mainColor; format.hasOutline outline; if (outline) { format.outlineColor outlineColor; format.outlineSize outlineSize; } label.text text; label.textFormat format; }调用侧SetTitleStyle(btnTitle, 领取奖励, Color.white, true, Color.black, 2);这样业务代码只需要关心我要什么状态不用纠结TextFormat的细节也方便后续扩展阴影、渐变字体。注意我上面把text赋值放在textFormat赋值之前让文字内容和样式合并到同一次刷新里执行少一次网格重建性能上能省一点是一点。3.4 GRichTextField的差异GRichTextField同样有textFormat属性基础文字的描边用法和GTextField一致。但富文本内部一旦出现font这类标签局部文字就会使用标签里的样式覆盖基础格式。也就是说你给GRichTextField配了一套带描边的textFormat结果富文本里有些片段用了font color#FFE066那些片段很可能就没有描边了。遇到同一个富文本里一部分字要描边、一部分不要的需求别在全局textFormat上纠结拆成多个文本组件控制关联更靠谱。4. 想要细腻或立体的描边多层文本叠加方案4.1 什么时候需要绕开TextFormatTextFormat的描边简单够用但有两处硬伤其一描边颜色是单色的做不了渐变描边其二outlineSize本质是8方向偏移的像素数想加粗到能当厚描边字用的程度字形会明显发糊偏移也不均匀。遇到这两种需求我建议直接换多层文本叠加的思路这也是老引擎时代做UI描边最通用的土办法。4.2 双层文本模拟厚描边原理很朴素底层放一个颜色等于描边色的文本字号比主文字大2到4号坐标对齐上层放真正的内容。两层叠在一起底层露出一圈边缘看起来就是厚描边。// 底层模拟描边 GTextField strokeLayer UIPackage.CreateObject(Common, Label).asTextField; strokeLayer.SetPosition(x, y, 0); strokeLayer.text 首充礼包; strokeLayer.textFormat bottomFormat; // fontSize26, color黑色 // 顶层主文字 GTextField mainLayer UIPackage.CreateObject(Common, Label).asTextField; mainLayer.SetPosition(x, y, 0); mainLayer.text 首充礼包; mainLayer.textFormat topFormat; // fontSize22, color金色 // 注意层级strokeLayer要add在前mainLayer add在后 container.AddChild(strokeLayer); container.AddChild(mainLayer);这里有个关键认知FGUI里放大字号是整体缩放字形并不是沿字形轮廓向外均匀扩展所以字号差带来的描边厚度在不同笔画方向上是近似但非均匀的字越复杂越明显。因此这个方案适合做厚描边标题不适合追求精细的1px细描边。另外两层文本的text字段要同步更新建议封装一个函数统一赋值避免只改了一层导致穿帮。4.3 三层文本描边加阴影组合如果需求是描边之外还要阴影而且阴影要有明显偏移三层叠放更稳最底层是阴影深色文本向下偏移几个像素中间层是描边放大字号的描边色顶层是主文字。层级顺序不能乱阴影必须在最下面否则主文字的描边会覆盖阴影的一部分效果就残了。4.4 8方向叠加均匀细描边的土办法想要严格均匀的细描边又暂时没有Shader条件还有个土办法用8个底层文本分别往上、下、左、右、左上、右上、左下、右下各偏移1像素颜色统一为描边色中间再盖主文字。这样做出来的描边是均匀的代价是一个文本占掉9个GObject实例。我只在极少数必须细描边又没有Shader的场景用过而且只用于静态大标题。真机验证过可用但维护成本偏高能用烘焙字体替代就尽量替代。5. 高频踩坑实录裁切、不刷新、字体兼容5.1 描边被裁掉先查组件边界和溢出属性描边配好了但右边和下边总有一侧被切掉——这个问题出现频率很高。绝大多数情况跟FGUI无关是文本组件的显示边界不够。FGUI里文字组件的宽高定义了排版区域而伪描边是向外偏移的字形偏移出来的那部分超出了组件边界自然就没了。解决办法很简单给文字组件四周各留出outlineSize加2左右的余量。另外检查一下组件在编辑器里的溢出属性如果设成了隐藏或滚动之类的模式描边大概率会被裁需要调回可见或者调整尺寸。我排查这类问题时习惯先在编辑器里把组件临时拉大20像素看描边是否恢复用二分法快速定位到底是边界问题还是渲染问题。5.2 改了TextFormat但UI纹丝不动这个坑我见的最多原因九成是没赋回。因为getter返回的是引用你改字段时数据已经改了但FGUI不感知于是你Debug时打印format里的值全是对的屏幕上却什么都没有。// 错误示范只拿不改回 TextFormat format title.textFormat; format.hasOutline true; // 正确示范改完必须写回 title.textFormat format;还有一成场景是赋回了但又被覆盖。比如同一帧里有个统一的文本刷新函数在后面执行把样式重新赋值了一遍你的描边就被吞掉了。排查时我在改动点前后各打一条日志把hasOutline的值打出来对照时间线立刻能分清是没生效还是被覆盖。5.3 小字号动态字体描边发虚、毛刺TTF动态字体下把fontSize设到12到16像素这种小字号再开描边经常会出现边缘发虚、像毛刺一样的感觉。这是伪描边的固有缺陷偏移量是整数像素字号越小1像素偏移在字形里占的比例越大视觉上越脏。我的处理经验描边size用小值1或者2不要贪大字号尽量用能被4整除的规格偏移表现在实践中更均匀换用笔画更粗的字体比如思源黑体Medium或Bold再配小描边比细体字加粗描边干净太多真正重要的标题直接走烘焙描边字体一步到位5.4 位图字体描边时有时无如果文本组件用的字体类型是位图字体TextFormat里的hasOutline等字段我实测在部分版本中是不生效的。不要依赖它。碰到描边时有时无的问题第一件事不是查代码而是先确认当前文字用的字体是不是位图字体、有没有在BMFont端烘焙描边。5.5 描边带来的加粗错觉描边会让字形闭合区域里的空隙被填充掉一部分视觉上整个字明显变粗。这在排版上是双刃剑横幅标题开了描边更醒目但正文小字开了描边字和图标之间的间距会显得不协调。我给UI走查时经常发现开了描边之后文字变挤了不一定是锚点问题就是视觉膨胀。调整间距时要记得把描边的膨胀量算进去。6. 描边的性能账顶点、合批和资源选型6.1 顶点和三角形数直接乘9按FGUI动态字体描边的实现方式一个字形的网格由1份变成9份8个偏移轮廓加1个原字形。如果SDK对size1做过4方向优化会好一些但大体量级就是这个数字。一段20个字的文本无描边大概40个三角形开描边直接到360个三角形。低端安卓机上如果一个界面同时存在几十个动态描边文本每次文本刷新时的网格重建和最终渲染的overdraw都会成为肉眼可见的卡顿点。6.2 频繁更新的文本慎开描边描边的开销是假如文本变化就要重建网格叠加出来的。服务器滚屏公告、实时飘字、倒计时这类每秒都在变的文本尽量不要开运行时描边。实在要保证可读性优先用阴影替代——阴影本质也是偏移绘制但整体开销通常比描边小要是阴影也扛不住就把带描边做成状态位图状态变化时换图。6.3 资源选型与团队规范我最后会在团队里定一份简单的选型表避免每个开发凭感觉乱来场景推荐方案原因固定文案的UI标题美术烘焙带描边字体运行时零开销效果可控动态文案、低频变化运行时TextFormat描边灵活成本适中高频滚屏、飘字阴影或状态位图切换避免每帧重建大网格渐变、发光等特殊描边多层文本叠加突破单色限制按这个规范去定UI标准基本能在效果和性能之间找到还不错的平衡点。FGUI的字体描边说穿了就是一个看着容易、用起来全是细节的功能。它最核心的那套机制——TextFormat、伪描边、位图与动态字体双路径——并不复杂复杂的是它和排版、字体、性能、多语言这些模块咬合在一起后的各种隐性约束。我个人在项目里沉淀下来的习惯就三条静态标题能编辑器配就别写代码动态样式走统一封装接口特殊效果先想烘焙字体再想多层文本最后才考虑自定义Shader。把这个功能当成一个独立的小系统去维护比每次需求来了临时改一改要省心得多。
返回列表