前言
  本篇文章的内容是实现实现角色的血量控制功能,在开始实现之前,我们需要知道角色血量控制功能
的需求是什么。
角色血量控制功能的需求
- 角色头上需要显示一个跟随角色移动的
血量条
,实时显示角色当前的血量
- 角色的
最大血量
可以任意修改
- 角色
接触怪物
时会受伤,并播放受伤音效
- 角色受伤时,除了
减少相应的血量
,还需要有一个向后击退
的效果
- 为了避免角色被怪物卡住时,出现不断受伤的问题,角色在受伤后,将在
短暂时间内
获得免伤效果
- 当角色
血量为0
时,角色死亡,播放死亡动画,游戏结束
在弄清楚并整理好需求之后,我们开始一一实现这些功能。
制作血量条
  首先,我们来制作血量条。因为血量条
要一直跟随移动,所以我们不妨将血量条
作为Player
的子物体。在Player
下新建一个名为HealthBarDisplay
的空物体,然后将Assets\Sprites\UI
下的Health
以及Health-bg
拖拽到HealthBarDisplay
下面。
它们的具体属性如下:
HealthBarDisplay
:
Health
:
Position
: (-0.8, 1.5, 0)
Color
: (0, 255, 0, 255)
Sorting Layer
: Character, Order In Layer
: 4
Health-bg
:
Position
: (0, 1.5, 0)
Sorting Layer
: Character, Order In Layer
: 4
  此时,将Health
的Scale
属性的X分量
缓慢从1减少至0
,我们可以看到血量条逐渐变短。但为了避免角色在转向时,血量条
跟着翻转,我们还需要在PlayerController.cs
中加入以下代码:
PlayerController.cs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class PlayerController : MonoBehaviour { ... [Tooltip("显示血量条的物体")] public Transform HealthBarDisplay;
...
private void Flip() { ...
if(HealthBarDisplay != null) { HealthBarDisplay.localScale = Vector3.Scale( new Vector3(-1, 1, 1), HealthBarDisplay.localScale ); } else { Debug.LogWarning("请设置HealthBarDisplay"); } } }
|
  接着,我们将HealthBarDisplay
拖拽到PlayerController.cs
下HealthBarDisplay
属性的赋值框,然后Health
的Scale
设置为(0.5, 1, 1)
,运行游戏,让角色左右翻转,可以看到血量条
不随着角色转向而翻转。停止运行游戏,将Health
的Scale
设置为(1, 1, 1)
,然后保存游戏,将我们所做的修改应用至Player
对于的Prefab。
创建血量控制脚本
  我们在Assets\Scripts\Player
下创建一个名为PlayerHealth
的C#脚本。因为角色的最大血量
需要能被修改,角色受伤时不仅要播放受伤音效
,还要有向后击退
的效果,因此我们需要在PlayerHealth.cs
脚本中添加以下代码:
PlayerHealth.cs1 2 3 4 5 6 7 8 9 10 11 12 13 14
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerHealth : MonoBehaviour { [Tooltip("角色的最大生命值")] public float MaxHP = 100f; [Tooltip("角色被怪物伤害时受到的击退力大小")] public float HurtForce = 100f; [Tooltip("角色受伤后的免伤时间")] public float FreeDamagePeriod = 0.35f; [Tooltip("角色的受伤音效")] public AudioClip[] OuchClips; }
|
  添加完毕之后,将PlayerHealth.cs
添加到物体Player
上,并将Assets\Audio\Player\Ouch
下的四个音频文件拖动到OuchClips
的赋值框,然后保存修改。
实现接触怪物时受伤
  在Unity中,当一个带Collider2D
的物体和其他带有Collider2D
的物体发生了碰撞时,将会触发OnCollisionEnter2D
,我们可以通过OnCollisionEnter2D
这个函数来获取物体的碰撞信息。那我们如何判断碰撞的物体是怪物呢?答案是利用Unity提供的Tag
,通过设置Tag
这一属性,我们可以方便地对物体进行标识。
  选中AlienSlug
,点击Tag
下拉框,然后点击Add Tag
,创建一个名为Enemy
的Tag并将AlienSlug
和AlienShip
的Tag都设置为Enemy
。最后,将AlienSlug
和AlienShip
的修改应用至Prefab。
  添加完成之后,我们在PlayerHealth.cs
脚本中添加以下代码:
PlayerHealth.cs1 2 3 4 5 6
| private void OnCollisionEnter2D(Collision2D collision) { if(collision.gameObject.tag == "Enemy") { Debug.Log("Enemy"); } }
|
  运行游戏,控制人物移动去接触怪物,可以看到Console
输出Enemy
字符串,说明已经检测到了角色和怪物发生碰撞。接下来,我们在PlayerHealth.cs
加入角色受伤的代码:
PlayerHealth.cs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public class PlayerHealth : MonoBehaviour { ... [Tooltip("角色受伤时减少的血量")] public float DamageAmount = 10f; [Tooltip("角色受伤后的免伤时间")] public float FreeDamagePeriod = 0.35f;
private float m_CurrentHP; private float m_LastFreeDamageTime;
private void Start() { m_CurrentHP = MaxHP; m_LastFreeDamageTime = 0f; }
private void OnCollisionEnter2D(Collision2D collision) { if(Time.time > m_LastFreeDamageTime + FreeDamagePeriod) { if(collision.gameObject.tag == "Enemy") { if(m_CurrentHP > 0f) { TakeDamage(collision.transform); m_LastFreeDamageTime = Time.time; } else { } } } }
public void TakeDamage(Transform enemy) { Vector3 hurtVector = transform.position - enemy.position + Vector3.up * 5f; GetComponent<Rigidbody2D>().AddForce(hurtVector * HurtForce);
m_CurrentHP -= DamageAmount;
Debug.Log(m_CurrentHP);
int i = Random.Range(0, OuchClips.Length); AudioSource.PlayClipAtPoint(OuchClips[i], transform.position); } }
|
  运行游戏,控制人物移动去接触怪物,可以看到角色在触碰怪物时,角色会受到一个击退力的作用,同时Console
窗口输出当前的生命值。
更新血量条的显示
  接下来,我们要根据角色当前的生命值来实时更新血量条的显示,也就是我们需要根据角色当前的生命值,来更新HealthBarDisplay
的子物体Health
的Scale
和Color
。我们在PlayerHealth.cs
中加入以下代码:
PlayerHealth.cs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class PlayerHealth : MonoBehaviour { ... [Tooltip("血量条")] public SpriteRenderer HealthSprite;
... private Vector3 m_InitHealthScale;
private void Start() { ... m_InitHealthScale = HealthSprite.transform.localScale; }
public void TakeDamage(Transform enemy) { ...
UpdateHealthBar();
... }
private void UpdateHealthBar() { if(HealthSprite != null) { HealthSprite.color = Color.Lerp(Color.green, Color.red, 1 - m_CurrentHP * 0.01f); HealthSprite.transform.localScale = Vector3.Scale(m_InitHealthScale, new Vector3(m_CurrentHP * 0.01f, 1, 1)); } else { Debug.LogError("请设置HealthSprite"); } } }
|
  将HealthBarDisplay
的子物体Health
拖动到HealthSprite
的赋值框,运行游戏,控制人物移动去接触怪物,可以看到当角色的生命值变化时,血量条
也随之更新。
控制角色的死亡
  最后,我们还需要控制角色的死亡。我们知道,当角色死亡时,不能再和场景中的任何物体发生交互
,玩家也不能再控制角色
。因此,我们在PlayerHealth.cs
中加入以下代码:
PlayerHealth.cs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class PlayerHealth : MonoBehaviour { ...
public void TakeDamage(Transform enemy) { ...
if(m_CurrentHP > 0f) { ... } else { Death(); }
... }
private void Death() { Collider2D[] cols = GetComponents<Collider2D>(); foreach(Collider2D c in cols) { c.enabled = false; }
GetComponent<PlayerController>().enabled = false;
GetComponent<Animator>().SetTrigger("Death"); } }
|
  运行游戏,控制人物移动去接触怪物,可以看到当角色的生命值减少至0
时,角色播放死亡动画,且不与场景中的其他物体发生交互,玩家也不能再控制角色。将Player
的修改应用至Prefab,并保存场景产生的修改。
PlayerHealth.cs的完整代码
  此时,PlayerHealth.cs
的完整代码如下所示:
PlayerHealth.cs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerHealth : MonoBehaviour { [Tooltip("角色的最大生命值")] public float MaxHP = 100f; [Tooltip("角色被怪物伤害时受到的击退力大小")] public float HurtForce = 100f; [Tooltip("角色的受伤音效")] public AudioClip[] OuchClips; [Tooltip("角色受伤时减少的血量")] public float DamageAmount = 10f; [Tooltip("角色受伤后的免伤时间")] public float FreeDamagePeriod = 0.35f; [Tooltip("血量条")] public SpriteRenderer HealthSprite;
private float m_CurrentHP; private float m_LastFreeDamageTime; private Vector3 m_InitHealthScale;
private void Start() { m_CurrentHP = MaxHP; m_LastFreeDamageTime = 0f; m_InitHealthScale = HealthSprite.transform.localScale; }
private void OnCollisionEnter2D(Collision2D collision) { if(Time.time > m_LastFreeDamageTime + FreeDamagePeriod) { if(collision.gameObject.tag == "Enemy") { if(m_CurrentHP > 0f) { TakeDamage(collision.transform); m_LastFreeDamageTime = Time.time; } else { Death(); } } } }
public void TakeDamage(Transform enemy) { Vector3 hurtVector = transform.position - enemy.position + Vector3.up * 5f; GetComponent<Rigidbody2D>().AddForce(hurtVector * HurtForce);
m_CurrentHP -= DamageAmount;
UpdateHealthBar();
int i = Random.Range(0, OuchClips.Length); AudioSource.PlayClipAtPoint(OuchClips[i], transform.position); }
private void UpdateHealthBar() { if(HealthSprite != null) { HealthSprite.color = Color.Lerp(Color.green, Color.red, 1 - m_CurrentHP * 0.01f); HealthSprite.transform.localScale = Vector3.Scale(m_InitHealthScale, new Vector3(m_CurrentHP * 0.01f, 1, 1)); } else { Debug.LogError("请设置HealthSprite"); } }
private void Death() { Collider2D[] cols = GetComponents<Collider2D>(); foreach(Collider2D c in cols) { c.enabled = false; }
GetComponent<PlayerController>().enabled = false;
GetComponent<Animator>().SetTrigger("Death"); } }
|
后言
  至此,我们已经完成了角色的血量控制功能,本篇文章提到的数值参数都可以根据自己的喜好进行调整。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay7
分支下看到,读者可以clone这个仓库到本地进行查看。
参考链接
- Tags