【Unity UGUI】血条 / 进度条(HP Bar)

文章目录

    • [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 ❌

三个要点:

  1. 三层 RectTransform 必须完全对齐:选中 Delay 和 Fill,锚点拉满(Anchor Min 0,0 / Max 1,1,Left/Right/Top/Bottom 全 0),让它们随 HPBar 一起缩放。
  2. 全部关掉 Raycast Target:血条不接收点击,留着只会徒增 GraphicRaycaster 的检测开销(详见文章 2 的踩坑篇)。
  3. 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. 使用方法

  1. 新建 Canvas(Render Mode = Screen Space - Overlay,CanvasScaler 设 Scale With Screen Size)。
  2. 在 Canvas 下按 [第 2 节](#第 2 节) 搭出 HPBar(Background / Delay / Fill / Text 四层)。
  3. HPBarHPBarController,把 Fill、Delay、Text 拖进对应槽位。
  4. HPBar 再挂 HPBarFollow ,把场景里的角色拖进 Target,Canvas 拖进 Canvas
  5. 在角色的战斗脚本里拿到 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 360Fill Origin = TopClockwise = ✓,技能冷却时 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 秒刷一次,而非每帧)。
相关推荐
狼哥16863 小时前
蛋糕美食元服务_地图实现指南
ui·harmonyos
UXbot5 小时前
AI网页开发工具能替代工具吗?5大平台对比
前端·人工智能·低代码·ui·原型模式·web app
狼哥16869 小时前
蛋糕美食元服务_订单实现指南
ui·harmonyos
mxwin9 小时前
Unity Shader URP:法线如何进行光照计算
unity·游戏引擎·shader
郝学胜-神的一滴10 小时前
中级OpenGL教程 009:用环境光告别模型死黑
前端·c++·unity·godot·图形渲染·opengl·unreal
一锅炖出任易仙12 小时前
创梦汤锅学习日记day30
学习·ai·ue5·游戏引擎
星栈独行12 小时前
用 Rust + Makepad 做一个 JSON 查看器:从零到能用的全过程
开发语言·程序人生·ui·rust·json
mxwin21 小时前
Unity URP 中的法线生成完全指南
unity·游戏引擎
游乐码21 小时前
Unity基础(十五)LineRender画线功能
unity·游戏引擎