文章目录
-
- [0. 效果预览](#0. 效果预览)
- [1. 需求分析](#1. 需求分析)
- [2. Hierarchy 搭建](#2. Hierarchy 搭建)
- [3. 核心组件配置](#3. 核心组件配置)
-
- [Fill / Delay 两根条的 Image 配置(关键)](#Fill / Delay 两根条的 Image 配置(关键))
- [4. 完整代码](#4. 完整代码)
-
- [HPBarController.cs ------ 血条核心逻辑](#HPBarController.cs —— 血条核心逻辑)
- [HPBarFollow.cs ------ 血条跟随世界角色头顶](#HPBarFollow.cs —— 血条跟随世界角色头顶)
- [5. 使用方法](#5. 使用方法)
- [6. 参数说明](#6. 参数说明)
- [7. 变体与扩展](#7. 变体与扩展)
- [8. 常见问题](#8. 常见问题)
- [9. 性能 / 适配建议](#9. 性能 / 适配建议)
0. 效果预览

血条几乎是每个游戏都绕不开的 UI:显示当前生命值,受击时平滑掉血,低血量时变色预警,还要能跟着角色头顶跑。这篇把"两层延迟掉血血条 + 跟随世界物体 + 血量变色"一次做完,给一份可直接挂上去用的脚本。
1. 需求分析
核心思路:用
Image.Type = Filled的填充比例映射血量百分比;主血条立即变化,底层再放一根"延迟条"用插值慢慢追上去,受击瞬间露出的那条缝就是"掉血缓动"的视觉来源。
典型使用场景:
- 角色 / 怪物头顶血条(World Space 或屏幕跟随)
- Boss 底部大血条(带延迟掉血、护盾分段)
- 通用进度条(加载、技能 CD、经验条)
需要实现的功能点:
- 用填充比例显示当前 HP / 最大 HP
- 受击时主血条立即下降,底层缓动条延迟跟随(延迟掉血)
- 血量低于阈值时血条变色(绿 → 黄 → 红)
- 血条跟随世界中的角色头顶,始终面向相机
- 一行
SetHP接口对接战斗逻辑
前置知识:建议先读本系列 文章 1(总领篇),了解 Canvas 渲染模式、RectTransform 锚点轴心,以及屏幕空间 vs 世界空间的区别------本文的跟随逻辑直接依赖这些概念。
2. Hierarchy 搭建
血条用三层 Image 叠出来,从下到上:背景 → 延迟条 → 当前血条。
Canvas (Screen Space - Overlay)
└── HPBar ← 挂 HPBarController
├── Image_Background ← 底色(深灰/黑)
├── Image_Delay ← 延迟缓动条(Filled,偏白/黄,受击时露出)
├── Image_Fill ← 当前血量条(Filled,主色)
└── Text_HP ← "80/100" 文本(可选,TMP 或原生 Text)
层级顺序很关键:Delay 必须排在 Fill 下面(Hierarchy 中更靠上),受击时 Fill 先缩、Delay 慢慢缩,中间露出的 Delay 颜色就是缓动效果。三层 Image 的 RectTransform 要完全重叠(同样的锚点和尺寸)。
为什么用三层 Image 而不是 Slider?Slider 自带拖拽交互和 Handle,血条根本不需要点击,用 Slider 反而要去关 Interactable、删 Handle,徒增节点。直接用
Image.fillAmount更轻、更可控。
3. 核心组件配置
Fill / Delay 两根条的 Image 配置(关键)
| 对象 | 组件 | 关键参数 |
|---|---|---|
| HPBar | RectTransform | Width 120, Height 14(按需) |
| Image_Background | Image | Color 深灰,Raycast Target ❌ |
| Image_Delay | Image | Type = Filled ,Fill Method = Horizontal ,Fill Origin = Left,Color 浅黄,Raycast Target ❌ |
| Image_Fill | Image | Type = Filled ,Fill Method = Horizontal ,Fill Origin = Left,Color 绿,Raycast Target ❌ |
| Text_HP | Text / TMP | 居中对齐,Raycast Target ❌ |
三个要点:
- 三层 RectTransform 必须完全对齐:选中 Delay 和 Fill,锚点拉满(Anchor Min 0,0 / Max 1,1,Left/Right/Top/Bottom 全 0),让它们随 HPBar 一起缩放。
- 全部关掉 Raycast Target:血条不接收点击,留着只会徒增 GraphicRaycaster 的检测开销(详见文章 2 的踩坑篇)。
- Fill Method 选 Horizontal :从左到右填充。如果要做圆形 CD 条,改成
Radial 360。
4. 完整代码
HPBarController.cs ------ 血条核心逻辑
csharp
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 两层延迟掉血血条:Fill 立即变化,Delay 缓动追赶,低血量变色。
/// </summary>
public class HPBarController : MonoBehaviour
{
// ===== Inspector 引用 =====
[Header("引用")]
[SerializeField] private Image _fillImage; // 当前血量条(Filled)
[SerializeField] private Image _delayImage; // 延迟缓动条(Filled)
[SerializeField] private Text _hpText; // 血量文本(可选,可留空)
// ===== 缓动参数 =====
[Header("缓动")]
[SerializeField] private float _delaySpeed = 1.5f; // 延迟条追赶速度(每秒填充量)
[SerializeField] private float _startDelay = 0.4f; // 受击后延迟条停顿多久再开始追
// ===== 血量变色 =====
[Header("血量变色")]
[SerializeField] private bool _useGradient = true;
[SerializeField] private Gradient _hpGradient; // 由血量百分比取色(红→黄→绿)
private float _maxHP = 100f;
private float _currentHP = 100f;
private float _targetFill = 1f; // 当前血量对应的填充比(0~1)
private float _delayTimer; // 受击后停顿计时
private void Awake()
{
// 没在 Inspector 配 Gradient 时,给一套默认红→黄→绿
if (_useGradient && (_hpGradient == null || _hpGradient.colorKeys.Length == 0))
_hpGradient = BuildDefaultGradient();
}
private void Update()
{
// ===== 延迟条向目标追赶 =====
if (_delayImage == null) return;
// 受击后先停顿 _startDelay 秒,制造"血条先掉、缓动条后追"的层次感
if (_delayTimer > 0f)
{
_delayTimer -= Time.deltaTime;
return;
}
// 延迟条还没追上目标 → 用 MoveTowards 匀速逼近(比 Lerp 更可控,不会无限接近)
if (_delayImage.fillAmount > _targetFill)
{
_delayImage.fillAmount = Mathf.MoveTowards(
_delayImage.fillAmount, _targetFill, _delaySpeed * Time.deltaTime);
}
else
{
// 回血时延迟条直接对齐,不做缓动
_delayImage.fillAmount = _targetFill;
}
}
// ===== 对外接口 =====
/// <summary>初始化血条(进场时调用一次)。</summary>
public void Init(float maxHP)
{
_maxHP = Mathf.Max(1f, maxHP);
_currentHP = _maxHP;
_targetFill = 1f;
if (_fillImage != null) _fillImage.fillAmount = 1f;
if (_delayImage != null) _delayImage.fillAmount = 1f;
RefreshColorAndText();
}
/// <summary>设置当前血量(战斗逻辑每次变化都调它)。</summary>
public void SetHP(float current, float max)
{
_maxHP = Mathf.Max(1f, max);
float clamped = Mathf.Clamp(current, 0f, _maxHP);
bool isDamage = clamped < _currentHP; // 是掉血还是回血
_currentHP = clamped;
_targetFill = _currentHP / _maxHP;
// 主血条立即到位
if (_fillImage != null) _fillImage.fillAmount = _targetFill;
// 掉血时,延迟条先停顿再追;回血时立刻同步
if (isDamage) _delayTimer = _startDelay;
else if (_delayImage != null) _delayImage.fillAmount = _targetFill;
RefreshColorAndText();
}
/// <summary>受击掉血的便捷封装。</summary>
public void TakeDamage(float amount) => SetHP(_currentHP - amount, _maxHP);
/// <summary>回血的便捷封装。</summary>
public void Heal(float amount) => SetHP(_currentHP + amount, _maxHP);
// ===== 内部:刷新颜色和文本 =====
private void RefreshColorAndText()
{
if (_useGradient && _fillImage != null)
_fillImage.color = _hpGradient.Evaluate(_targetFill); // 按百分比取色
if (_hpText != null)
_hpText.text = $"{Mathf.CeilToInt(_currentHP)}/{Mathf.CeilToInt(_maxHP)}";
}
private Gradient BuildDefaultGradient()
{
var g = new Gradient();
g.SetKeys(
new[]
{
new GradientColorKey(new Color(0.85f, 0.2f, 0.2f), 0f), // 0% 红
new GradientColorKey(new Color(0.95f, 0.8f, 0.2f), 0.4f), // 40% 黄
new GradientColorKey(new Color(0.3f, 0.8f, 0.3f), 1f), // 100% 绿
},
new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) });
return g;
}
}
HPBarFollow.cs ------ 血条跟随世界角色头顶
这里用最稳的方案:血条放在 Screen Space - Overlay 的 Canvas 上,每帧把角色的世界坐标转成屏幕坐标贴上去。相比 World Space Canvas,这种方式血条永远正对屏幕、不会被透视压扁、字也永远清晰。
csharp
using UnityEngine;
/// <summary>
/// 让屏幕空间血条跟随世界中的目标(角色头顶)。
/// 血条本身在 Screen Space - Overlay 的 Canvas 下。
/// </summary>
public class HPBarFollow : MonoBehaviour
{
[SerializeField] private Transform _target; // 跟随的世界目标(角色)
[SerializeField] private Vector3 _worldOffset = new Vector3(0f, 2f, 0f); // 头顶偏移
[SerializeField] private Canvas _canvas; // 血条所在的 Canvas
private RectTransform _rect;
private Camera _uiCamera;
private void Awake()
{
_rect = transform as RectTransform;
// Overlay 模式下 worldCamera 为 null,ScreenPoint 直接当锚点用即可
_uiCamera = _canvas != null && _canvas.renderMode != RenderMode.ScreenSpaceOverlay
? _canvas.worldCamera
: null;
}
// 用 LateUpdate:等角色这一帧移动完再更新血条,避免抖动
private void LateUpdate()
{
if (_target == null) return;
Vector3 worldPos = _target.position + _worldOffset;
Vector3 screenPos = Camera.main.WorldToScreenPoint(worldPos);
// z < 0 表示目标在相机背后,隐藏血条
bool behind = screenPos.z < 0f;
if (_rect != null) _rect.gameObject.SetActive(!behind);
if (behind) return;
// 屏幕坐标 → Canvas 局部坐标
RectTransform canvasRect = _canvas.transform as RectTransform;
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvasRect, screenPos, _uiCamera, out localPoint);
_rect.localPosition = localPoint;
}
}
5. 使用方法
- 新建
Canvas(Render Mode = Screen Space - Overlay,CanvasScaler 设 Scale With Screen Size)。 - 在 Canvas 下按 [第 2 节](#第 2 节) 搭出
HPBar(Background / Delay / Fill / Text 四层)。 - 给
HPBar挂 HPBarController,把 Fill、Delay、Text 拖进对应槽位。 - 给
HPBar再挂 HPBarFollow ,把场景里的角色拖进Target,Canvas 拖进Canvas。 - 在角色的战斗脚本里拿到
HPBarController引用,Start里调一次Init(maxHP),之后每次血量变化调SetHP(current, max)(或TakeDamage/Heal):
csharp
public class Enemy : MonoBehaviour
{
[SerializeField] private HPBarController _hpBar;
private float _hp = 100f;
private void Start() => _hpBar.Init(_hp);
public void OnHit(float dmg)
{
_hp -= dmg;
_hpBar.SetHP(_hp, 100f); // 主条立即掉,缓动条延迟追
}
}
运行后受击,主血条瞬间下降,浅黄的延迟条停顿一下再缓缓追上,血量越低血条越红。
6. 参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| _delaySpeed | float | 1.5 | 延迟条追赶速度(每秒填充量,越大追得越快) |
| _startDelay | float | 0.4 | 受击后延迟条停顿时长(秒),制造层次感 |
| _useGradient | bool | true | 是否按血量百分比给 Fill 变色 |
| _hpGradient | Gradient | 红→黄→绿 | 血量颜色映射,留空用默认 |
| _worldOffset | Vector3 | (0,2,0) | 血条相对角色的世界偏移(头顶高度) |
7. 变体与扩展
变体 1:进度条 / 加载条
去掉 HPBarFollow 和延迟条,只留 Fill,外部按 0~1 设 fillAmount 即可。加载进度、技能 CD、经验条都是同一套:
csharp
_fillImage.fillAmount = loaded / total;
变体 2:圆形 CD 冷却
把 Fill 的 Image 改 Fill Method = Radial 360、Fill Origin = Top、Clockwise = ✓,技能冷却时 fillAmount 从 0 涨到 1 就是顺时针扫光的 CD 效果。
变体 3:Boss 分段血条(护盾/多血条)
用一个数组装多段血量,当前段空了切到下一段、换底色,常见于 Boss 多阶段战。核心是把 SetHP 改成对"当前段"操作,段满/段空时切换 Fill 的颜色。
8. 常见问题
Q:血条不填充,整条始终满 / 始终空?
A:99% 是 Image 的 Type 没设成 Filled,或 Fill Method 不对。Type 必须是 Filled,fillAmount 才会生效;普通 Simple 模式下改 fillAmount 没有任何反应。
Q:延迟条和主血条对不齐,露出一条边?
A:Delay 和 Fill 两个 RectTransform 没完全重叠。选中它们把锚点拉满(Min 0,0 / Max 1,1,四边 offset 全 0),让它们和 HPBar 同尺寸。Fill Origin 也要一致(都 Left)。
Q:血条跟随时疯狂抖动?
A:跟随逻辑要放在 LateUpdate 而不是 Update------Update 里角色可能还没移动完,血条会比角色慢一帧导致抖动。本文 HPBarFollow 已用 LateUpdate。
Q:角色转到相机背后,血条还显示在屏幕上?
A:WorldToScreenPoint 在目标位于相机背后时 z < 0,但 x/y 仍会算出一个镜像坐标。必须判断 screenPos.z < 0 时隐藏血条,本文已处理。
Q:大量怪物时血条很卡?
A:每个血条每帧都在 LateUpdate 算坐标 + 可能触发 Canvas Rebuild。见下方性能建议,重点是关 Raycast Target、拆 Canvas、屏外剔除。
9. 性能 / 适配建议
- 全部关闭 Raycast Target:血条不接收点击,背景/Delay/Fill/Text 的 Raycast Target 全勾掉,减少 GraphicRaycaster 每帧的射线检测(参考文章 2)。
- 血条单独拆一个 Canvas :血条每帧改
fillAmount、改位置会触发 Canvas Rebuild/Rebatch,和主 UI 放一起会带着整个 Canvas 一起重建。把所有血条放进一个独立 Canvas,把"脏"的影响隔离开。 - 屏外剔除 :
screenPos.z < 0或超出屏幕范围的血条直接SetActive(false),不渲染也不算坐标。 - 大量血条上对象池:RTS / 割草游戏几十上百个血条,频繁 Instantiate/Destroy 会造成 GC 卡顿,用对象池复用血条实例(这套机制会在后续"无限滚动列表"一篇详细展开)。
- 文本用 TMP :原生
Text每次改字会触发网格重建,血量数字频繁变化建议换 TextMeshPro,并尽量降低刷新频率(如每 0.1 秒刷一次,而非每帧)。