Unity3D RPG Core | 27 设置血条显示
从这一集中,我们第一次接触到 UI 的绘制。我们将设置敌人的血条显示。
1. 设置画布
UI 相关的内容是需要显示在画布上的,所以我们在 Hierarchy 窗体中右击选择 UI - Canvas 创建一个 Canvas。之后的血条也将显示在此 Canvas 中。
如图 1 所示,我们还需要设置一下 Canvas 的属性。Render Mode 设置为 World Space,并在 Event Camera 中指定场景中的摄像头,这样才能在每个敌人的头上显示血条。Canvas 的位置也需要 Reset 一下,这样才能和场景中的坐标一一对应。

2. 制作血条预制体
血条是以透明图片叠加的形式进行显示的,因此在制作之前需要获取到一张 png 格式的图片。
2.1 导入颜色方块
默认 3D 场景中是不附带 2D Sprite 包的,因此如图 2 所示,我们在 Package Manager 中导入 2D Sprite 包。

导入成功后,我们发现 Hierarchy 窗体中右键多了 2D Object - Sprites - Square 选项,我们点击创建一个方块。
然后我们双击创建出来的 Square 对象中的 Sprite 属性,对应一张 png 图片。我们将这张图片拖拽复制一份,后续用作血条的图片。此时创建出来的 Square 对象就用不到了,我们将其删除。
其实就是要那张 png 图片,不用自己制作了😂
2.2 创建背景和前景图片
如图 3 所示,在创建 Canvas 下我们首先创建一个空的对象,用作标识血条图片对象。接着右击 UI - Image 创建血条的背景和前景。

Image 对象中的 Source Image 属性设置为我们先前获取的方块 png 图片。背景图片的 Color 属性设置为红色,前景图片的 Color 属性设置为绿色。
对于前景图片,如图 4 所示,我们还需要设置 Image Type 为 Filled。Fill Method 设置为 Horizontal,Fill Origin 设置为 Right。这时拖拽改变 Fill Amount 我们就可以看到血条改变的方式了,Fill Amount 我们在代码中进行控制修改。

这时血条就制作完成了,我们将图 3 中的 HealthBarImageHolder 拖拽到 Assets 窗体中,变成预制体。然后在 Hierarchy 窗体中的内容就可以删除了,和之前石头人的石头一样,我们通过代码来生成血条。
注意图 3 中各个对象的位置和大小,需要保持一致。
3. 指定各个敌人血条的位置
和之前石头人生成石头进行攻击的情况类似,我们需要知道敌人血条相对于敌人的相对位置。之前石头的生成位置是使用模型中现有的手臂位置,但是血条没有现成的位置,需要我们手动指定。
如图 5 所示,我们新建一个空的对象 HealthBarPoint,然后将其移动到敌人头上合适的位置。以上设置在预制体里设置,因为敌人可能不止一个,预制体里设置完成后就可以在所有复制的敌人上生效。

4. 编写血条控制代码
首先需要确定在哪里更新血条 UI:当敌人受到攻击的时候需要更新血条 UI,如代码清单 1 所示,我们将其放在 CharacterData 类中。
如第 4 行所示,我们定义一个血条更新的事件函数,后续在血条 UI 控制器中进行注册。更新的点在 17 行和 27 行,即敌人被攻击时。
- public class CharacterData : MonoBehaviour
- {
- [HideInInspector]
- public event Action<int, int> OnHealthChanged = null;
- public void ReceiveAttackDamage(AttackData attacker)
- {
- if (attacker.m_isCriticalHit)
- {
- GetComponent<Animator>().SetTrigger("Hurt");
- }
- int damage = Mathf.Max(CalculateDamage(attacker) - CurrentDefence, 1);
- CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);
- // 更新UI
- if (OnHealthChanged != null)
- OnHealthChanged.Invoke(CurrentHealth, MaxHealth);
- //TODO:更新经验
- }
- public void ReceiveAttackDamage(int damage)
- {
- int finalDamage = Mathf.Max(damage - CurrentDefence, 1);
- CurrentHealth = Mathf.Max(CurrentHealth - finalDamage, 0);
- // 更新UI
- if (OnHealthChanged != null)
- OnHealthChanged.Invoke(CurrentHealth, MaxHealth);
- }
- }
接着我们创建新的 C# 脚本,并命名为 HealthBarController,为血条 UI 的控制脚本。如代码清单 2 所示,m_barPrefab 是第 2 小节中制作的血条预制体;m_barPoint 是第 3 小节中制作的血条位置;m_bar 是通过预制体复制创建出来的实际血条对象;m_barForeground 是血条前景图片,后续我们需要更改它的 FillAmount 属性;m_cameraTransform 是场景摄像机的 Transform 组件,我们根据它改变血条的显示位置。
在 Awake() 函数中,我们获取敌人身上的 CharacterData 组件,并注册 UpdateHealthBar 函数。看到第 39 行的 UpdateHealthBar 函数,如果当前血量小于等于 0,则销毁创建的血条对象;否则显示出血条,并根据当前血量和最大血量,更新前景图片的 FillAmount 属性。
- public class HealthBarController : MonoBehaviour
- {
- public GameObject m_barPrefab;
- public Transform m_barPoint;
- public bool m_alwaysVisible;
- public float m_visibleTime = 2.0f;
- float m_visibleTimeLeft;
- GameObject m_bar = null;
- Image m_barForeground;
- Transform m_cameraTransform;
- CharacterData m_characterData;
- private void Awake()
- {
- m_characterData = GetComponent<CharacterData>();
- m_characterData.OnHealthChanged += UpdateHealthBar;
- m_visibleTimeLeft = m_visibleTime;
- }
- private void OnEnable()
- {
- m_cameraTransform = Camera.main.transform;
- foreach (Canvas canvas in FindObjectsOfType<Canvas>())
- {
- if (canvas.renderMode == RenderMode.WorldSpace)
- {
- m_bar = Instantiate(m_barPrefab, canvas.transform);
- m_barForeground = m_bar.transform.GetChild(0).GetChild(0).GetComponent<Image>();
- m_bar.SetActive(m_alwaysVisible);
- }
- }
- }
- private void UpdateHealthBar(int currentHealth, int maxHealth)
- {
- if (currentHealth <= 0)
- Destroy(m_bar);
- else
- {
- m_bar.SetActive(true);
- m_visibleTimeLeft = m_visibleTime;
- float fillAmount = (float)currentHealth / maxHealth;
- m_barForeground.fillAmount = fillAmount;
- }
- }
- private void LateUpdate()
- {
- if (m_bar != null)
- {
- m_bar.transform.position = m_barPoint.position;
- m_bar.transform.forward = -m_cameraTransform.forward;
- if (m_alwaysVisible == false)
- {
- if (m_visibleTimeLeft <= 0)
- m_bar.SetActive(false);
- else
- m_visibleTimeLeft -= Time.deltaTime;
- }
- }
- }
- }
在 OnEnable() 函数中,我们获取到场景相机的 Transform 组件。接着遍历所有的 Canvas 对象,找到血条 UI 对应的 Canvas 对象。然后调用 Instantiate 生成实际的血条对象,注意这边 Instantiate 函数对应的重载原型为:
- public static T Instantiate<T>(T original, Transform parent) where T : Object;
第一个参数是复制的预制体,第二个参数是对应的父类 Transform 组件,这边需要添加到画布上,所以是 canvas.transform。
接着我们可以通过创建出来的血条对象,找到前景图片。如第 32 行所示,使用 GetChild() 方法,对象对应的层级关系见图 3。
变量 m_alwaysVisible 控制血条是否一直显示;m_visibleTime 为不一直显示时,血条的显示时间;m_visibleTimeLeft 是剩余的显示时间。血条显示控制的逻辑和攻击冷却类似,这边不再赘述。
代码完成后,就是拖拽指定血条 UI 控制器脚本,指定各个参数。这些操作都在预制体上操作会更方便。
都在预制体上操作。
5. 测试
如图 6 所示,是最终的血条效果。测试的过程中遇到一些问题,暂时还不知道如何修复。一是血条的背景和前景的两张图片,看着显示会有点位置偏差。二是血条也会被虚化掉。
