Unity3D RPG Core | 27 设置血条显示

从这一集中,我们第一次接触到 UI 的绘制。我们将设置敌人的血条显示。

1. 设置画布

UI 相关的内容是需要显示在画布上的,所以我们在 Hierarchy 窗体中右击选择 UI - Canvas 创建一个 Canvas。之后的血条也将显示在此 Canvas 中。

如图 1 所示,我们还需要设置一下 Canvas 的属性。Render Mode 设置为 World Space,并在 Event Camera 中指定场景中的摄像头,这样才能在每个敌人的头上显示血条。Canvas 的位置也需要 Reset 一下,这样才能和场景中的坐标一一对应。

图1 Canvas设置

2. 制作血条预制体

血条是以透明图片叠加的形式进行显示的,因此在制作之前需要获取到一张 png 格式的图片。

2.1 导入颜色方块

默认 3D 场景中是不附带 2D Sprite 包的,因此如图 2 所示,我们在 Package Manager 中导入 2D Sprite 包。

图2 导入 2D Sprite

导入成功后,我们发现 Hierarchy 窗体中右键多了 2D Object - Sprites - Square 选项,我们点击创建一个方块。

然后我们双击创建出来的 Square 对象中的 Sprite 属性,对应一张 png 图片。我们将这张图片拖拽复制一份,后续用作血条的图片。此时创建出来的 Square 对象就用不到了,我们将其删除。

其实就是要那张 png 图片,不用自己制作了😂

2.2 创建背景和前景图片

如图 3 所示,在创建 Canvas 下我们首先创建一个空的对象,用作标识血条图片对象。接着右击 UI - Image 创建血条的背景和前景。

图3 血条图片

Image 对象中的 Source Image 属性设置为我们先前获取的方块 png 图片。背景图片的 Color 属性设置为红色,前景图片的 Color 属性设置为绿色。

对于前景图片,如图 4 所示,我们还需要设置 Image Type 为 Filled。Fill Method 设置为 Horizontal,Fill Origin 设置为 Right。这时拖拽改变 Fill Amount 我们就可以看到血条改变的方式了,Fill Amount 我们在代码中进行控制修改。

图4 前景图片设置

这时血条就制作完成了,我们将图 3 中的 HealthBarImageHolder 拖拽到 Assets 窗体中,变成预制体。然后在 Hierarchy 窗体中的内容就可以删除了,和之前石头人的石头一样,我们通过代码来生成血条。

注意图 3 中各个对象的位置和大小,需要保持一致。

3. 指定各个敌人血条的位置

和之前石头人生成石头进行攻击的情况类似,我们需要知道敌人血条相对于敌人的相对位置。之前石头的生成位置是使用模型中现有的手臂位置,但是血条没有现成的位置,需要我们手动指定。

如图 5 所示,我们新建一个空的对象 HealthBarPoint,然后将其移动到敌人头上合适的位置。以上设置在预制体里设置,因为敌人可能不止一个,预制体里设置完成后就可以在所有复制的敌人上生效。

图5 设置血条位置

4. 编写血条控制代码

首先需要确定在哪里更新血条 UI:当敌人受到攻击的时候需要更新血条 UI,如代码清单 1 所示,我们将其放在 CharacterData 类中。

如第 4 行所示,我们定义一个血条更新的事件函数,后续在血条 UI 控制器中进行注册。更新的点在 17 行和 27 行,即敌人被攻击时。

代码清单 1 更新血条
  1. public class CharacterData : MonoBehaviour
  2. {
  3.     [HideInInspector]
  4.     public event Action<int, int> OnHealthChanged = null;
  5.  
  6.     public void ReceiveAttackDamage(AttackData attacker)
  7.     {
  8.         if (attacker.m_isCriticalHit)
  9.         {
  10.             GetComponent<Animator>().SetTrigger("Hurt");
  11.         }
  12.  
  13.         int damage = Mathf.Max(CalculateDamage(attacker) - CurrentDefence, 1);
  14.         CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);
  15.         // 更新UI
  16.         if (OnHealthChanged != null)
  17.             OnHealthChanged.Invoke(CurrentHealth, MaxHealth);
  18.         //TODO:更新经验
  19.     }
  20.  
  21.     public void ReceiveAttackDamage(int damage)
  22.     {
  23.         int finalDamage = Mathf.Max(damage - CurrentDefence, 1);
  24.         CurrentHealth = Mathf.Max(CurrentHealth - finalDamage, 0);
  25.         // 更新UI
  26.         if (OnHealthChanged != null)
  27.             OnHealthChanged.Invoke(CurrentHealth, MaxHealth);
  28.     }
  29. }

接着我们创建新的 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 属性。

代码清单 2 血条控制器
  1. public class HealthBarController : MonoBehaviour
  2. {
  3.     public GameObject m_barPrefab;
  4.     public Transform  m_barPoint;
  5.     public bool       m_alwaysVisible;
  6.     public float      m_visibleTime = 2.0f;
  7.     float             m_visibleTimeLeft;
  8.  
  9.     GameObject m_bar = null;
  10.     Image m_barForeground;
  11.     Transform m_cameraTransform;
  12.  
  13.     CharacterData m_characterData;
  14.  
  15.     private void Awake()
  16.     {
  17.         m_characterData = GetComponent<CharacterData>();
  18.         m_characterData.OnHealthChanged += UpdateHealthBar;
  19.  
  20.         m_visibleTimeLeft = m_visibleTime;
  21.     }
  22.  
  23.     private void OnEnable()
  24.     {
  25.         m_cameraTransform = Camera.main.transform;
  26.  
  27.         foreach (Canvas canvas in FindObjectsOfType<Canvas>())
  28.         {
  29.             if (canvas.renderMode == RenderMode.WorldSpace)
  30.             {
  31.                 m_bar = Instantiate(m_barPrefab, canvas.transform);
  32.                 m_barForeground = m_bar.transform.GetChild(0).GetChild(0).GetComponent<Image>();
  33.                 m_bar.SetActive(m_alwaysVisible);
  34.             }
  35.         }
  36.        
  37.     }
  38.  
  39.     private void UpdateHealthBar(int currentHealth, int maxHealth)
  40.     {
  41.         if (currentHealth <= 0)
  42.             Destroy(m_bar);
  43.         else
  44.         {
  45.             m_bar.SetActive(true);
  46.             m_visibleTimeLeft = m_visibleTime;
  47.             float fillAmount = (float)currentHealth / maxHealth;
  48.             m_barForeground.fillAmount = fillAmount;
  49.         }
  50.     }
  51.  
  52.     private void LateUpdate()
  53.     {
  54.         if (m_bar != null)
  55.         {
  56.             m_bar.transform.position = m_barPoint.position;
  57.             m_bar.transform.forward = -m_cameraTransform.forward;
  58.  
  59.             if (m_alwaysVisible == false)
  60.             {
  61.                 if (m_visibleTimeLeft <= 0)
  62.                     m_bar.SetActive(false);
  63.                 else
  64.                     m_visibleTimeLeft -= Time.deltaTime;
  65.             }
  66.         }
  67.     }
  68. }

在 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 所示,是最终的血条效果。测试的过程中遇到一些问题,暂时还不知道如何修复。一是血条的背景和前景的两张图片,看着显示会有点位置偏差。二是血条也会被虚化掉。

图6 血条效果