十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

UGUI受击触发血条制作:状态机、FillAmount与性能优化

UGUI受击触发血条制作:状态机、FillAmount与性能优化 开始就像是做U3D的朋友聊天直接聊项目、聊代码、聊踩坑。做游戏UI血条大概是绕不过去的一个东西。不管是打BOSS、打小怪还是玩家自己掉血屏幕上方或者头顶这几个格子基本就成了玩家判断战斗状态的“晴雨表”。很多人一上来就买插件、找资源包其实用UGUI自己写一个受击触发的血条逻辑并不复杂而且做完之后你能完全控制它的表现、性能还有扩展性比直接拿现成的舒服太多。这篇文章我就把个人项目中实际在用的这套血条方案完整拆一遍从UI布局到受击事件触发从数值映射到后期优化配合可以直接抄的代码把每一步的“为什么这么做”也讲清楚。适合正准备自己动手写血条或者已经在用UGUI做战斗UI、想优化效果的开发朋友参考。1. 内容整体设计与思路拆解1.1 先搞清楚你要做哪种血条写代码之前先别急着摆组件。血条这个东西看起来一样实际上分几种形态不同形态对应的实现思路差别挺大选错了后面会很别扭。头顶血条跟随3D角色属于World Space画布下的UI常用于小怪、NPC、玩家头顶需要处理摄像机朝向、缩放、遮挡。屏幕固定血条比如玩家自身血量、BOSS血量通常放在屏幕底部或上方属于Screen Space - Overlay或Camera模式最常见也最容易做。动态数值血条带缓动效果、渐变颜色、数字跳动、分段破坏感等属于进阶表现。受击触发弹出血条平时隐藏角色受击时才显示几秒然后自动淡出或缩小消失这就是你这个标题的核心场景。我这里讲的就是第四种也就是“受击触发”类血条但底层实现同时兼容前三种你掌握之后可以自由切换。提示受击触发血条的核心难点不在于“画一条血条”而在于“什么时候该显示、什么时候该隐藏、怎样让这个过程不突兀”。1.2 为什么选择UGUI而不是Sprite或Mesh有些人习惯用Sprite或者自定义Mesh来做血条觉得灵活。但我个人强烈推荐基于UGUI的Image组件理由有三个UGUI的Image天然支持Sprite九宫格拉伸你只需要一张很小的白底图就能通过类型Filled实现各种形状的填充效果不需要为不同比例的血条做多套美术资源。UGUI自带Canvas批量合批和重建机制只要控制好图集和层级大量血条同屏的性能压力非常小。UGUI的RectTransform让血条定位和跟随变得极其简单你可以直接用WorldToScreenPoint或者把血条放进World Space画布Unity已经帮你处理好了屏幕适配。另外我用的是Image的Filled模式而不是改RectTransform的sizeDelta这是个重要细节FillAmount的填充计算在重建时非常轻量而频繁修改宽高会触发更多的布局计算和顶点更新性能开销不在一个量级。// 推荐改变填充量 hpImage.type Image.Type.Filled; hpImage.fillMethod Image.FillMethod.Horizontal; hpImage.fillOrigin 0; hpImage.fillAmount currentHp / maxHp; // 不推荐改变宽度 // hpRectTransform.sizeDelta new Vector2(width * hpPercent, height);1.3 受击触发的显示/隐藏状态机受击触发的关键在于“短暂显示后消失”。这里如果只是简单SetActive(true)再SetActive(false)会有几个问题频繁激活/失活会导致Canvas重建开销出现和消失很生硬连续受击时逻辑会乱。我的方案是状态机思想定义四个状态隐藏、显示、保持、淡出。隐藏血条完全不可见不参与任何更新。显示受到第一次伤害时进入立即更新数值并让血条可见。保持持续显示一段时间比如3秒期间如果再次受击刷新“最后受击时间”。淡出超出保持时间后通过CanvasGroup的alpha渐变消失同时可配合轻微缩小让消失有节奏感。这套状态机让血条行为完全可控也方便后面扩展“破盾”“暴击”“元素伤害”等特殊颜色闪动逻辑。后续代码我都是围绕这个状态机来实现的。2. 核心细节解析与实操要点2.1 血条UI层级和锚点设置在动手写脚本之前UI结构要先摆对。这里我给出血条预制体的推荐结构用层级来区分“底图—填充—高光—文本”DamageableHpBar (CanvasGroup) ├── Background_Hp (Image灰色或黑底) ├── Fill_Hp (ImageFilled模式) ├── Fill_Delay (ImageFilled模式延迟掉血特效层) ├── Foreground_Hp (Image可选描边/高光) └── Text_Hp (TMP_Text可显示当前HP/最大HP)几个关键设置Fill_Hp和Fill_Delay两者需要完全对齐并且都用Filled模式、Horizontal填充这样延迟掉血特效才能在同一轴向上做追击效果。Background_Hp不要塞在Fill_Hp下面作为子物体建议平级放在前一个避免层级关系混乱导致遮挡问题。Text_Hp锚点设为居中拉伸或底部居中随血条宽度缩放这样血条宽度变化时文字仍能保持居中。关于锚点受击触发的血条一般会挂在角色头顶建议把根节点锚点设为“中下”Pivot也设为(0.5, 0)这样RectTransform的position刚好对应角色脚底位置方便后续根据角色高度做偏移。注意如果是跟随3D角色的World Space血条记得把Canvas的RenderMode设为World Space并且把Canvas的Scale设置成合适的大小比如0.01否则血条会巨大无比。2.2 FillAmount的填充方向与美术资源要求Image的Filled模式支持四种填充方式Horizontal、Vertical、Radial90、Radial180、Radial360。血条绝大多数用Horizontal就够了但要注意fillOrigin的设置fillOrigin 0 表示从左往右填充血条在左侧减少fillOrigin 1 表示从右往左填充血条在右侧减少我在项目里做了一个配置项因为有些角色设定是“左撇子”或者美术这边希望血条减少方向反过来与其改Shader不如代码里直接控制fillOrigin一行搞定。美术资源方面血条填充图建议使用纯白底图或用Sprite Editor切好九宫格颜色完全靠代码控制。这样做的好处是同一张资源可以配合不同角色染成不同颜色从暗红色到亮绿色随意切换不需要美术出多套颜色资源。public enum FillDirection { LeftToRight 0, RightToLeft 1 } public void SetFillDirection(FillDirection dir) { fillImage.fillOrigin (int)dir; }2.3 CanvasGroup的妙用整体透明度控制血条的淡入淡出最好不要单独去改Image的color因为如果有多个Image底图、填充、背景、文字就得遍历修改所有Image性能差而且容易漏。正确做法是在根节点挂一个CanvasGroup直接改alpha一次性控制所有子UI的透明度。[SerializeField] private CanvasGroup canvasGroup; private void SetBarAlpha(float alpha) { canvasGroup.alpha alpha; canvasGroup.blocksRaycasts alpha 0.1f; }这里有一个容易被忽视的细节blocksRaycasts。血条是UI如果不关闭射线检测它会挡住下面其他UI按钮的点击。当血条隐藏alpha为0时一定要把blocksRaycasts也关掉否则就是看不见的“空气墙”玩家点不到下面的按钮。2.4 受击信息的接收方式事件驱动而非轮询受击触发血条最关键的一环就是“角色受击”这个事件怎么通知到血条。很多新手会在Update里持续检测角色的血量值发现变化了就刷新血条这种轮询方式有两个问题浪费性能每帧都在做数值比较哪怕血量压根没变。无法区分“受击掉血”和“其他方式掉血”比如每秒中毒掉血、喝药回血、被治疗术回血这些都是数值变化但表现上完全不一样。正确做法是事件驱动。角色的伤害系统在真正扣血之前或者之后主动向外广播一个“受击事件”血条作为监听者接收这个事件并做出反应。public class HealthComponent : MonoBehaviour { public event Actionint, int OnDamaged; // 当前HP, 最大HP public void TakeDamage(int damage) { currentHp Mathf.Max(0, currentHp - damage); OnDamaged?.Invoke(currentHp, maxHp); } }当然如果你的项目已经有了一套战斗事件框架比如消息中心、EventBus都行直接复用即可核心原则就是“谁造成的伤害谁负责通知”而不是让血条自己轮询偷看角色状态。3. 实操过程与核心环节实现3.1 从零搭建血条预制体这里我会把预制体的搭建过程完整走一遍方便你对照操作。第一步在场景中创建一个Canvas如果场景没有的话。我建议把受击血条放在独立的Canvas下不要和主UI混在一起方便控制渲染顺序和整体显隐。Canvas的RenderMode根据你项目来选择如果是纯2D游戏角色在场景中本来就是2D坐标用Screen Space - Overlay即可血条直接根据2D坐标换算屏幕位置。如果是3D游戏建议用World Space直接把血条作为角色子物体挂载省去坐标换算的麻烦。第二步在Canvas下创建空物体命名HpBarRoot挂上CanvasGroup组件。这个就是血条的根节点。第三步在HpBarRoot下创建四个子物体Background_Hp添加Image颜色设为(0.1, 0.1, 0.1, 0.8)作为血槽底色。Fill_Hp添加ImageSprite用纯白圆形或方形Image Type设为FilledFill Method设为Horizontal颜色设为(0.9, 0.2, 0.2, 1)作为当前血量填充。Fill_Delay和Fill_Hp完全一样的配置颜色设为(1, 1, 0.3, 0.8)这个用来做延迟掉血白色或黄色残影紧跟红色血条移动。Text_Hp添加TMP_Text如果没有TMP就添加Text字体大小16粗体带阴影或描边。第四步给每个元素设置锚点让它们都相对于根节点拉伸。Background_Hp的anchorMin和anchorMax都设为(0, 0)然后设置sizeDelta为(180, 20)这样血条宽度就是180像素。Fill_Hp和Fill_Delay同理但尺寸要比背景小一圈比如(172, 14)偏移1-2像素视觉上更像内嵌的槽。第五步把HpBarRoot做成Prefab保存到项目里。3.2 血条核心控制脚本的实现接下来是重点核心控制脚本可以命名为HpBarController。这个脚本负责数值更新、显示状态机、延迟掉血协程、颜色变换。using System.Collections; using UnityEngine; using UnityEngine.UI; public class HpBarController : MonoBehaviour { public enum HpBarState { Hidden, Show, Keep, Fading } [Header(UI References)] [SerializeField] private CanvasGroup canvasGroup; [SerializeField] private Image fillImage; [SerializeField] private Image delayImage; [SerializeField] private TMP_Text hpText; [Header(Display Settings)] [SerializeField] private float showDuration 3f; [SerializeField] private float fadeDuration 0.4f; [SerializeField] private float delayFollowSpeed 3f; [SerializeField] private float shrinkScale 0.9f; private HpBarState currentState HpBarState.Hidden; private float stateTimer 0f; private int currentHp; private int maxHp; private float delayFillAmount; private void Awake() { if (canvasGroup null) canvasGroup GetComponentCanvasGroup(); SetBarAlpha(0f); gameObject.SetActive(false); } private void Update() { if (currentState HpBarState.Hidden) return; // 延迟掉血层的追击逻辑 if (Mathf.Abs(delayImage.fillAmount - fillImage.fillAmount) 0.005f) { delayImage.fillAmount Mathf.Lerp(delayImage.fillAmount, fillImage.fillAmount, Time.deltaTime * delayFollowSpeed); } else { delayImage.fillAmount fillImage.fillAmount; } switch (currentState) { case HpBarState.Hidden: break; case HpBarState.Show: stateTimer showDuration; currentState HpBarState.Keep; break; case HpBarState.Keep: stateTimer - Time.deltaTime; if (stateTimer 0f) { currentState HpBarState.Fading; } break; case HpBarState.Fading: stateTimer - Time.deltaTime; float alpha Mathf.Clamp01(stateTimer / fadeDuration); SetBarAlpha(alpha); // 轻微缩小效果 float scale Mathf.Lerp(shrinkScale, 1f, alpha); transform.localScale Vector3.one * scale; if (stateTimer -fadeDuration) { gameObject.SetActive(false); currentState HpBarState.Hidden; } break; } } public void OnDamaged(int hp, int maxHpValue) { int oldHp currentHp; currentHp hp; maxHp maxHpValue; // 如果是从隐藏状态被激活需要重新设置 if (gameObject.activeInHierarchy false) { gameObject.SetActive(true); } // 血条初始状态完全显示 SetBarAlpha(1f); transform.localScale Vector3.one; // 更新fillAmount平滑过渡而不是瞬间跳变 float targetFill (float)currentHp / (float)maxHp; fillImage.fillAmount Mathf.Max(targetFill, fillImage.fillAmount - 0.1f); // 如果受到的是加血直接跳变避免误认为掉血 if (currentHp oldHp) { fillImage.fillAmount targetFill; delayImage.fillAmount targetFill; } // 延迟层设为上一次的血量比例用于显示掉血残影 delayImage.fillAmount fillImage.fillAmount; if (hpText ! null) { hpText.text ${currentHp} / {maxHp}; } // 状态机切换到显示 currentState HpBarState.Show; stateTimer showDuration; } private void SetBarAlpha(float alpha) { canvasGroup.alpha alpha; canvasGroup.blocksRaycasts alpha 0.1f; } }这段代码有几个设计点值得展开在OnDamaged里我把fillAmount做了一个最小变化量限制Mathf.Max(targetFill, fillImage.fillAmount - 0.1f)。目的是防止受到一次非常小的伤害比如1点时血条出现肉眼几乎看不清的微小抖动。战斗中数值频繁跳动这种抖动很影响观感限制最小变化量之后只有真正有意义的伤害才会让血条动起来。对于回血情况我做了特殊处理如果是加血直接跳变到目标值不经过延迟掉血层。这样玩家看到的效果就是“掉血有残影追击回血立即生效”这是目前主流游戏通用的表现规则。3.3 受击触发的完整接入流程血条控制器写完了接下来要接入到角色身上。我以一个3D游戏角色为例完整走一遍接入流程。第一步在角色Prefab下创建一个子物体命名为HpBarAnchor放在角色头顶的合适位置。用一个空物体作为锚点方便后续调整血条位置偏移。第二步把之前做好的HpBarRoot血条预制体实例化到HpBarAnchor下面作为它的子物体。这样血条自动跟随角色移动。第三步在角色身上挂一个脚本比如叫HpBarBinder负责把血条控制器和角色的HealthComponent连接起来。public class HpBarBinder : MonoBehaviour { [SerializeField] private HpBarController hpBarController; // 自动绑定或拖拽 private HealthComponent health; private void Awake() { health GetComponentHealthComponent(); if (health null) { health GetComponentInChildrenHealthComponent(); } if (hpBarController null) { hpBarController GetComponentInChildrenHpBarController(); } } private void OnEnable() { health.OnDamaged HandleDamaged; } private void OnDisable() { health.OnDamaged - HandleDamaged; } private void HandleDamaged(int hp, int maxHp) { hpBarController.OnDamaged(hp, maxHp); } }注意如果你用的是World Space画布让血条直接在3D空间里那血条默认是朝一个方向看的。你需要决定它是朝向相机LookAt还是只沿Y轴旋转要点让血条始终面朝摄像机但不倒立。后者通常更好因为单纯的LookAt会在某些角度让血条出现翻转。如果是World Space血条Binder脚本里再加一行void LateUpdate() { // 让血条始终面向摄像机但只绕Y轴旋转 Vector3 targetPos transform.position cameraTransform.forward; transform.LookAt(targetPos, Vector3.up); }3.4 延迟掉血特效的追击算法细节延迟掉血这个效果就是那一层黄色/白色的残影在红色血条减少之后“慢半拍”地跟着缩回去。这在动作游戏里非常重要它给玩家返还了“刚才那一下到底掉了多少血”的额外信息。实现原理不复杂Fill_Delay这一层的fillAmount保存在局部变量delayFillAmount里每次Update里用Lerp向Fill_Hp的fillAmount靠近速度由delayFollowSpeed控制。delayImage.fillAmount Mathf.Lerp(delayImage.fillAmount, fillImage.fillAmount, Time.deltaTime * delayFollowSpeed);关于delayFollowSpeed这个参数的值我建议放在3-6之间。太低比如1残影半天追不上去玩家会以为卡了太高比如10残影和红色血条基本同时到就没意义了。具体数值可以边调整边看手感不同团队对这个速度的宽容度不一样。还有一个小细节Lerp本身是异步衰减的速度会越来越慢到最后可能永远差一点点。所以我在Update里加了一个阈值判断当差距小于0.005时直接拉平避免出现“残影永远追不上本体”的浮点残留。4. 常见问题与排查技巧实录4.1 血条不显示或突然消失排查这类问题一定要有顺序别一上来就怀疑代码逻辑。先检查GameObject是否激活受击触发血条默认是隐藏的如果你在Inspector里看到它没显示先确认是否真的OnDamaged被调用了。在HandleDamaged里加一个Debug.Log能看到就说明事件出来了看不到说明连接有问题。再看CanvasGroup的alpha很多时候血条其实在只是因为初始alpha0完全透明看不见。检查一下SetBarAlpha是否被调用了以及初始alpha是否设置对了。最后看Canvas的层级World Space画布如果Scale是0或者非常大血条会消失或巨大化。我见过有人把Canvas的Scale设成0结果血条死活看不见。4.2 受击时血条闪烁或跳变这是一种常见的现象红色血条和延迟残影在受击瞬间都跳到最低点然后再分开。原因是在OnDamaged里我先设置了delayImage.fillAmount fillImage.fillAmount再让delayImage开始追击导致初始状态下两个fillAmount一致。正确的逻辑应该是在受击瞬间红色血条先降下来延迟残影还停留在上一次的血量位置然后再开始追击。所以delayImage的初始值应该是“上一条血量比例”也就是调用OnDamaged之前fillImage的数值。我把这段逻辑优化一下public void OnDamaged(int hp, int maxHpValue) { int oldHp currentHp; float oldPercent oldHp 0 ? (float)oldHp / (float)maxHp : 0f; currentHp hp; maxHp maxHpValue; // 如果是掉血延迟层保留上一次的百分比 delayImage.fillAmount Mathf.Max(oldPercent, fillImage.fillAmount); // 当前血量层直接跳到目标值 float targetFill (float)currentHp / (float)maxHp; fillImage.fillAmount targetFill; // 加血情况直接同步 if (targetFill oldPercent) { delayImage.fillAmount targetFill; } ... }4.3 反复受击导致血条永远不消失如果血条一直显示、从不隐藏多半是状态机的“刷新最后受击时间”逻辑没写对。在我这套代码里每次OnDamaged都会把stateTimer重置为showDuration同时把state切回Show。理论上这样就能实现“持续受击就持续显示”。但如果你把stateTimer的引用写错了或者在Keep状态里没有正确处理再次受击比如直接切到Fading而不是重置回Show就会出现“血条还在显示但计时器已经跑完了”的情况。你可以这样处理在OnDamaged里不管当前血条处于什么状态一律重置为Show并且强制激活GameObject。public void OnDamaged(int hp, int maxHpValue) { // 强制激活并重置状态 gameObject.SetActive(true); currentState HpBarState.Show; stateTimer showDuration; ... }这样就能保证无论血条处于隐藏还是淡出状态只要有新伤害都能立刻“拉回来”完整显示。4.4 同屏大量血条的性能优化如果你要给几十个小怪同时挂血条性能问题就绕不开。有几个亲测有效的优化手段第一个是关闭多余的重建血条的Text和背景在数值不变时不重建。UGUI的Canvas重建是基于脏标记的只要你不改Image的fillAmount、不改alpha它们就不会参与重建。第二个是血条预制体的静态化处理如果很多怪物的血条结构完全相同可以考虑使用同一个Canvas把所有血条都放在其中减少Canvas数量。每个Canvas都有独立的构建批次Canvas越多DrawCall开销越多。第三个是使用对象池受击触发血条的创建和销毁如果频繁用Instantiate/DestroyGC压力不小。建议做一个简单的对象池初始化时预生成20个血条实例用的时候激活不用的时候回收。第四个是延迟更新策略对屏幕外的血条可以不更新fillAmount和追击逻辑。用Renderer.isVisible或者相机视锥检测做一下剔除能省一大截性能。优化手段原理收益Canvas合并减少Canvas数量减少批次中对象池避免反复Instantiate/Destroy中视锥剔除屏幕外的血条不更新UI逻辑高图集合并所有血条共用一张图集低4.5 血条跟随角色时的抖动World Space血条跟随角色时最让人头疼的就是抖动。原因多半是血条位置更新在Update里而角色移动在FixedUpdate或者物理引擎里两者不同步。解决方案是把血条位置更新放到LateUpdate里。void LateUpdate() { // 确保在角色动画和物理更新之后才更新血条位置 transform.position anchor.position offset; }还有一种是自转轴带来的抖动血条约在角色头顶当角色转向时血条也跟着摇摆。解决方法是把血条放到Canvas下让Canvas跟随角色血条本身不参与位置更新只更新朝向。5. 进阶技巧与扩展思路5.1 战斗数字浮动的组合表现血条不只是显示数值它能承载的战斗信息远比你想象的多。受击瞬间我们可以在血条周围弹出伤害数字、暴击数字、格挡数字这样玩家一眼能看清每一次伤害的来源和类型。我的做法是在血条预制体下面挂一个伤害数字的容器每次受击时在这个容器上生成一个TMP_Text向上飘升0.5秒后回收。数字的字体颜色根据伤害类型切换物理-白、冰-蓝、火-红、暴击-橙、治疗-绿。如果想让这个功能更通用可以单独做成一个FloatingTextManager不依赖血条任何需要弹数字的地方都可以调用。5.2 血量颜色渐变和阈值预警满血绿色、半血黄色、残血红这是经典方案。实现方法有两种直接在Fill_Hp的Image上改颜色或者用材质PropertyBlock来控制。UGUI的Image改color非常简单但要注意它会影响整个Image的透明度如果你不想让渐变过程影响Alpha建议用材质参数来控制。private void UpdateBarColor(float hpPercent) { if (hpPercent 0.5f) { fillImage.color Color.Lerp(yellow, green, (hpPercent - 0.5f) * 2f); } else { fillImage.color Color.Lerp(red, yellow, hpPercent * 2f); } }红血状态下最好再叠加一个闪烁提醒可以用一个正弦曲线控制Fill_Hp的alpha或亮度效果非常直观。5.3 分层血条护盾血量很多人做BOSS时会需要双层甚至三层血条护盾条、血条、还有虚血条刚才提到的延迟掉血。UGUI做这种分层其实非常简单只需要在Fill_Hp下面再叠一个fill层即可。// 结构扩展 HpBarRoot ├── Background_Hp ├── Shield_Fill (护盾层) ├── Fill_Hp (血量层) ├── Fill_Delay (延迟层) └── Text_Hp护盾层和血量层的fillAmount互相独立由不同的数值驱动。当护盾被打破时可以播放一个扩大的缩放动画scale从1到1.2再到1让玩家意识到“盾碎了”。5.4 屏幕边缘血条的自动避让如果你做的血条是屏幕固定的比如BOSS血条在屏幕上方会遇到一个问题当BOSS血量很低时血条上的文字比如“12 / 25000”可能会超出屏幕边缘。处理方法是基于RectTransform的anchoredPosition做边界约束。在OnDamaged里更新文本之后加一段位置矫正逻辑Vector2 pos hpText.rectTransform.anchoredPosition; float halfWidth hpText.preferredWidth * 0.5f; float minX -parentWidth * 0.5f halfWidth margin; float maxX parentWidth * 0.5f - halfWidth - margin; pos.x Mathf.Clamp(pos.x, minX, maxX); hpText.rectTransform.anchoredPosition pos;6. UI框架与底层原理的关联思考6.1 UGUI渲染到底经历了什么聊到性能优化就不得不提UGUI的底层渲染机制。很多人会用UGUI但不知道它每帧在干什么一旦遇到性能问题就抓瞎。简单说UGUI的画布Canvas在渲染前会执行一次“构建”过程把所有UI元素的网格Mesh合并成一个大的Mesh然后交给GPU绘制。这个过程分几个步骤脏标记检测哪些UI元素变化了、顶点计算、合批、Mesh上传。如果一帧里有大量血条同时变化每个血条的fillAmount都变了它们所在Canvas的所有UI元素都会重新构建一次不管这些元素有没有变化。理解了这一点你就知道为什么我建议把大量血条放到同一个Canvas并且尽量减少每帧变化的UI元素数量了。每次Update里改变fillImage.fillAmount就会让所在Canvas进入重建状态。如果你有100个血条同时更新fillAmount意味着这个Canvas每帧要重建100次虽然UGUI会做一定的优化但也顶不住这么大的量。优化策略之一就是“变动分离”把经常变化的UI元素血条放到一个Canvas下把基础UI按钮、背景、文字放到另一个Canvas下。这样血条变化时基础UI不需要跟着重建。6.2 如何从源码层面看UI重建新版Unity中你可以在Window Analysis Frame Debugger里看到Canvas的BuildBatch信息。选中UI相关的DrawCall可以看到Generated Geometry的大小这个数值越大说明Canvas需要提交的顶点数据越多。另外Unity的Profiler里有Canvas.SendWillRenderCanvases和Canvas.BuildBatch两个关键耗时项。如果SendWillRenderCanvases耗时明显增加多半是UI脏标记刷得太频繁了。实战排查技巧在血条Update里加一个调用计数比如每100帧输出一次“update rebuild count”然后看数字是否异常。正常情况下一帧只有几个血条变化如果几十个血条同时变化你就要考虑上面说的Canvas分离方案了。7. 最后再分享一个小技巧写血条这个功能看似简单但真正做完、做好、做流畅需要琢磨的点其实不少。最后我说一个很多人忽略的细节血条受击时的“卡肉感”。游戏打击感很大的来源是节奏。受击瞬间血条不只是“缩短”它还可以有一点回弹、一个白色闪烁、一瞬间略微放大再恢复。这个效果在代码里实现非常轻量能在OnDamaged里加一个协程private IEnumerator HitPunchAnim() { // 白色闪动 fillImage.color Color.white; yield return new WaitForSeconds(0.05f); fillImage.color normalColor; // 回弹 Vector3 original transform.localScale; transform.localScale original * 1.05f; yield return new WaitForSeconds(0.1f); transform.localScale original; }我之前在调这个效果时发现回弹幅度不是越大越好。1.0到1.05这个范围肉眼感觉最舒服超过1.1就会显得飘、假。还有闪白的持续时间0.05秒感觉刚好太长了会觉得糊太短了看不清。根据我个人经验受击触发血条的所有参数里最影响手感的就是“显示持续时间”和“淡出时间”。Show亮度持续3秒、Fade 0.4秒是大多数项目的合理起点。如果你做的是快节奏动作游戏可以把这个时间压到2秒0.2秒如果做的是策略游戏或者MMO4秒0.5秒会更从容。下次如果你的策划提出来“血条要有受击反馈但不能干扰战斗视野”你可以把这个方案直接给他看隐藏时机、延迟残影、白色闪动全都有了而且底层是事件驱动的不会带来不必要的性能消耗。
返回列表