前言
  在上篇文章,我们已经实现了发射导弹的功能,但此时导弹并不能对怪物造成任何伤害。接下来,我们来实现对怪物造成伤害的功能。
为怪物添加血量管理
  打开Enemy.cs
,可以看到目前怪物只有在场景里面行走的功能。在实现导弹对怪物造成伤害的功能之前,我们首先为怪物增加血量管理的功能。
Enemy.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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
| using System.Collections; using System.Collections.Generic; using UnityEngine;
[RequireComponent(typeof(Wander))] [RequireComponent(typeof(Rigidbody2D))] public class Enemy : MonoBehaviour { [Tooltip("障碍物检测点")] [SerializeField] private Transform FrontCheck; [Tooltip("怪物的血量")] public float MaxHP = 10f; [Tooltip("怪物受伤时用来展示的图片")] public Sprite DamagedSprite; [Tooltip("怪物死亡时用来展示的图片")] public Sprite DeadSprite; [Tooltip("怪物死亡时用来展示DeadSprite")] public SpriteRenderer BodySpriteRenderer;
[Tooltip("怪物死亡时的音效")] public AudioClip[] DeathClips;
private Wander m_Wander; private Rigidbody2D m_Rigidbody2D;
private LayerMask m_LayerMask; private float m_CurrentHP; private bool m_Hurt; private bool m_Dead;
private void Awake() { m_Wander = GetComponent<Wander>(); m_Rigidbody2D = GetComponent<Rigidbody2D>(); } private void Start() { m_LayerMask = LayerMask.GetMask("Obstacle"); m_CurrentHP = MaxHP; m_Hurt = false; m_Dead = false; }
private void Update () { if(m_Dead) { return; }
Collider2D[] frontHits = Physics2D.OverlapPointAll(FrontCheck.position, m_LayerMask);
if(frontHits.Length > 0) { m_Wander.Flip(); } }
public void TakeDamage(Transform weapon, float hurtForce, float damage) { m_CurrentHP -= damage;
Vector3 hurtVector = transform.position - weapon.position; m_Rigidbody2D.AddForce(hurtVector.normalized * hurtForce);
if(!m_Hurt) { m_Hurt = true;
if(DamagedSprite != null) { SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>(); foreach(SpriteRenderer child in children) { child.enabled = false; }
if(BodySpriteRenderer != null) { BodySpriteRenderer.enabled = true; BodySpriteRenderer.sprite = DamagedSprite; } else { Debug.LogError("请设置BodySpriteRenderer"); } } else { Debug.LogWarning("请设置DamagedSprite"); } } if(m_CurrentHP <= 0 && !m_Dead) { m_Dead = true; Death(); } }
private void Death() { m_Wander.enabled = false;
if(DeadSprite != null) { SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>(); foreach(SpriteRenderer child in children) { child.enabled = false; }
if(BodySpriteRenderer != null) { BodySpriteRenderer.enabled = true; BodySpriteRenderer.sprite = DeadSprite; } else { Debug.LogError("请设置BodySpriteRenderer"); } } else { Debug.LogWarning("请设置DeadSprite"); }
Collider2D[] cols = GetComponents<Collider2D>(); foreach(Collider2D c in cols) { c.isTrigger = true; }
if(DeathClips != null && DeathClips.Length > 0) { int i = Random.Range(0, DeathClips.Length); AudioSource.PlayClipAtPoint(DeathClips[i], transform.position); } else { Debug.LogWarning("请设置DeathClips"); } } }
|
代码说明:
AudioSource.PlayClipAtPoint
: 是AudioSource
的一个静态函数,不需要添加AudioSource
组件就能在游戏场景里面播放音效
GetComponentsInChildren
: 获取自身
和子物体
上所有目标组件的引用,注意,也包括自身
enabled
: 当我们将某个组件的enabled
属性设置为false的时候,该组件会被禁用,相当于我们在Inspector
窗口中取消该组件的勾选
  修改完成之后,我们分别为AlienShip
和AlienSlug
的属性进行赋值:
AlienShip
和AlienSlug
的属性设置:
AlienShip
MaxHP
: 20
Damaged Sprite
: Assets\Sprites\Character
下的char_enemy_alienShip-damaged
Dead Sprite
: Assets\Sprites\Character
下的char_enemy_alienShip-dead
Body Sprite Renderer
: AlienShip
下的子物体char_enemy_alienShip
Death Clips
: Assets\Audio\Enemy
下的enemy-death1
、enemy-death2
和enemy-death3
AlienSlug
MaxHP
: 10
Damaged Sprite
: None
Dead Sprite
: Assets\Sprites\Character
下的char_enemy_alienSlug-dead
Body Sprite Renderer
: AlienShip
下的子物体enemy1-body
Death Clips
: Assets\Audio\Enemy
下的enemy-death1
、enemy-death2
和enemy-death3
  设置完成之后,我们将AlienShip
和AlienSlug
的修改Apply至它们的Prefab上,完成为怪物添加血量管理的工作。
让导弹伤害怪物的功能
  为怪物添加血量管理之后,我们还需要为导弹添加伤害怪物的功能。根据软件设计原则的单一职责原则(Single Responsibility Principle)
,导弹对怪物能造成伤害,那么应该由导弹维护对怪物造成伤害的数值
以及对怪物造成伤害时击退力的大小
等数值。清楚了这个以后,我们修改Missile.cs
的代码如下:
Missile.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
| using System.Collections; using System.Collections.Generic; using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(CapsuleCollider2D))] public class Missile : MonoBehaviour { [Tooltip("导弹是否朝向右边")] public bool FacingRight = true; [Tooltip("爆炸效果")] public GameObject Explosion; [Tooltip("导弹飞行的速度")] public float Speed = 25f; [Tooltip("导弹造成的伤害")] public float DamageAmount = 10f; [Tooltip("击退力的大小")] public float HurtForce = 50f;
private Rigidbody2D m_Rigidbody2D; private CapsuleCollider2D m_Trigger;
private void Awake() { m_Rigidbody2D = GetComponent<Rigidbody2D>(); m_Trigger = GetComponent<CapsuleCollider2D>(); }
private void Start() { m_Rigidbody2D.bodyType = RigidbodyType2D.Kinematic; if(FacingRight) { m_Rigidbody2D.velocity = new Vector2(Speed, 0); } else { m_Rigidbody2D.velocity = new Vector2(-Speed, 0); }
m_Trigger.isTrigger = true; }
public void Flip() { FacingRight = !FacingRight;
this.transform.localScale = Vector3.Scale( new Vector3(-1, 1, 1), this.transform.localScale ); }
private void OnExplode() { if(Explosion != null) { Quaternion randomRotation = Quaternion.Euler(0f, 0f, Random.Range(0f, 360f)); Instantiate(Explosion, transform.position, randomRotation); } else { Debug.LogWarning("请设置Explosion"); }
Destroy(gameObject); }
private void OnTriggerEnter2D(Collider2D collider) { if(collider.CompareTag("Player")) { return; }
if(collider.CompareTag("Enemy")) { collider.GetComponent<Enemy>().TakeDamage(this.transform, HurtForce, DamageAmount); }
OnExplode(); } }
|
  修改完成之后运行游戏,然后发射导弹攻击怪物,可以看到怪物在导弹的攻击之下会受伤并死亡。
让怪物死亡后不再运动
  虽然这个时候,导弹已经能对怪物造成伤害了,但是怪物死亡之后下落的速度太慢,且水平方向上的速度不为0,我们需要加快怪物下落的速度,并让怪物死亡之后不再运动。
  首先,我们将AlienSlug
和AlienShip
上Rigidbody2D
组件的Gravity Scale
都设置为和Player
一样的3.1
。然后,我们编辑Wander.cs
,加入一个OnDisable
函数。
Wander.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
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class Wander : MonoBehaviour { [Tooltip("是否朝向右边")] [SerializeField] private bool FacingRight = true;
[Tooltip("怪物水平移动的速度")] [SerializeField] private float MoveSpeed = 2f;
private Rigidbody2D m_Rigidbody; private float m_CurrentMoveSpeed;
private void Awake() { m_Rigidbody = GetComponent<Rigidbody2D>(); }
private void Start() { if(FacingRight) { m_CurrentMoveSpeed = MoveSpeed; } else { m_CurrentMoveSpeed = -MoveSpeed; } }
private void FixedUpdate() { m_Rigidbody.velocity = new Vector2(m_CurrentMoveSpeed, m_Rigidbody.velocity.y); }
private void OnDisable() { m_Rigidbody.velocity = new Vector2(0f, m_Rigidbody.velocity.y); }
public void Flip() { m_CurrentMoveSpeed *= -1; this.transform.localScale = Vector3.Scale(new Vector3(-1, 1, 1), this.transform.localScale); } }
|
代码说明:
  OnDisable
函数是Unity提供的生命周期函数之一,当脚本组件被禁用的时候,会先调用脚本组件的OnDisable
函数,然后再禁用组件。
  修改完成之后,再次运行游戏,可以看到怪物死亡后的下落速度增快,且下落的过程中水平速度为0。
重构PlayerHealth.cs和Enemy.cs
  之前为了便于测试,我们在PlayerHealth.cs
中实现了角色接触怪物受伤的功能。根据软件设计原则的单一职责原则(Single Responsibility Principle)
,因为是怪物对角色造成伤害,那么我们应该在Enemy.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 96
| using System.Collections; using System.Collections.Generic; using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))] public class PlayerHealth : MonoBehaviour { [Tooltip("角色的最大生命值")] public float MaxHP = 100f; [Tooltip("角色的受伤音效")] public AudioClip[] OuchClips; [Tooltip("角色受伤后的免伤时间")] public float FreeDamagePeriod = 0.35f; [Tooltip("血量条")] public SpriteRenderer HealthSprite;
private float m_CurrentHP; private float m_LastFreeDamageTime; private Vector3 m_InitHealthScale;
private Rigidbody2D m_Rigidbody2D;
private void Awake() { m_Rigidbody2D = GetComponent<Rigidbody2D>(); }
private void Start() { m_CurrentHP = MaxHP; m_LastFreeDamageTime = 0f; m_InitHealthScale = HealthSprite.transform.localScale; }
public void TakeDamage(Transform enemy, float hurtForce, float damage) { if(Time.time <= m_LastFreeDamageTime + FreeDamagePeriod) { return; }
m_LastFreeDamageTime = Time.time;
Vector3 hurtVector = transform.position - enemy.position + Vector3.up * 5f; m_Rigidbody2D.AddForce(hurtVector.normalized * hurtForce);
m_CurrentHP -= damage;
UpdateHealthBar();
if(OuchClips != null && OuchClips.Length > 0) { int i = Random.Range(0, OuchClips.Length); AudioSource.PlayClipAtPoint(OuchClips[i], transform.position); } else { Debug.LogWarning("请设置OuchClips"); }
if(m_CurrentHP <= 0f) { Death(); } }
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.isTrigger = true; }
GetComponent<PlayerController>().enabled = false;
GetComponent<Animator>().SetTrigger("Death"); } }
|
代码说明:
  这里,我们移除了OnCollisionEnter2D
这个函数,修改了函数TakeDamage
的参数,并将角色当前是否处于免伤状态
和角色当前是否死亡
这两个判断放在TakeDamage
函数中执行。最后,因为之前没有涉及到Trigger
,所以在Death
函数中,我们直接禁用了角色所有Collider2D
组件,这里,我们也需要将其改为将角色的所有Collider2D组件设置为Trigger
。
  接着,我们要在Enemy.cs
中加入怪物对角色造成伤害的代码。
Enemy.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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
| using System.Collections; using System.Collections.Generic; using UnityEngine;
[RequireComponent(typeof(Wander))] [RequireComponent(typeof(Rigidbody2D))] public class Enemy : MonoBehaviour { [Tooltip("角色受伤时减少的血量")] public float DamageAmount = 10f; [Tooltip("角色被怪物伤害时受到的击退力大小")] public float HurtForce = 500f; [Tooltip("障碍物检测点")] [SerializeField] private Transform FrontCheck; [Tooltip("怪物的血量")] public float MaxHP = 10f; [Tooltip("怪物受伤时用来展示的图片")] public Sprite DamagedSprite; [Tooltip("怪物死亡时用来展示的图片")] public Sprite DeadSprite; [Tooltip("怪物死亡时用来展示DeadSprite")] public SpriteRenderer BodySpriteRenderer;
[Tooltip("怪物死亡时的音效")] public AudioClip[] DeathClips;
private Wander m_Wander; private Rigidbody2D m_Rigidbody2D;
private LayerMask m_LayerMask; private float m_CurrentHP; private bool m_Hurt; private bool m_Dead;
private void Awake() { m_Wander = GetComponent<Wander>(); m_Rigidbody2D = GetComponent<Rigidbody2D>(); } private void Start() { m_LayerMask = LayerMask.GetMask("Obstacle"); m_CurrentHP = MaxHP; m_Hurt = false; m_Dead = false; }
private void Update () { if(m_Dead) { return; }
Collider2D[] frontHits = Physics2D.OverlapPointAll(FrontCheck.position, m_LayerMask);
if(frontHits.Length > 0) { m_Wander.Flip(); } }
private void OnCollisionEnter2D(Collision2D collision) { if(collision.gameObject.CompareTag("Player")) { collision.gameObject.GetComponent<PlayerHealth>().TakeDamage(this.transform, HurtForce, DamageAmount); } }
public void TakeDamage(Transform weapon, float hurtForce, float damage) { m_CurrentHP -= damage;
Vector3 hurtVector = transform.position - weapon.position; m_Rigidbody2D.AddForce(hurtVector.normalized * hurtForce);
if(!m_Hurt) { m_Hurt = true;
if(DamagedSprite != null) { SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>(); foreach(SpriteRenderer child in children) { child.enabled = false; }
if(BodySpriteRenderer != null) { BodySpriteRenderer.enabled = true; BodySpriteRenderer.sprite = DamagedSprite; } else { Debug.LogError("请设置BodySpriteRenderer"); } } else { Debug.LogWarning("请设置DamagedSprite"); } } if(m_CurrentHP <= 0 && !m_Dead) { m_Dead = true; Death(); } }
private void Death() { m_Wander.enabled = false;
if(DeadSprite != null) { SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>(); foreach(SpriteRenderer child in children) { child.enabled = false; }
if(BodySpriteRenderer != null) { BodySpriteRenderer.enabled = true; BodySpriteRenderer.sprite = DeadSprite; } else { Debug.LogError("请设置BodySpriteRenderer"); } } else { Debug.LogWarning("请设置DeadSprite"); }
Collider2D[] cols = GetComponents<Collider2D>(); foreach(Collider2D c in cols) { c.isTrigger = true; }
if(DeathClips != null && DeathClips.Length > 0) { int i = Random.Range(0, DeathClips.Length); AudioSource.PlayClipAtPoint(DeathClips[i], transform.position); } else { Debug.LogWarning("请设置DeathClips"); } } }
|
代码说明:
  这里,我们主要增加了HurtForce
和DamageAmount
这两个字段,并增加了OnCollisionEnter2D
函数,用于调用对角色造成伤害的TakeDamage
函数。
  添加完毕之后,运行游戏,此时怪物能正常对角色造成伤害,没有出现Bug,重构顺利完成。
对参数的一些小调整
  至此,所有的代码实现工作都做完了。但是为了游戏的可玩性和平衡性,我们需要对参数进行一些小调整。这里,我们调整的思路是,血量低的怪物,移动速度快,对角色造成的伤害也高
。
AlienShip
和AlienSlug
的属性设置:
AlienShip
Move Speed
: 2
Damage Amount
: 10
MaxHP
: 20
AlienSlug
Move Speed
: 3
Damage Amount
: 20
MaxHP
: 10
  修改完成之后,将AlienShip
和AlienSlug
产生的修改Apply至它们的Prefab上,并保存场景产生的修改。
后言
  至此,我们所有的工作都已完成,本篇文章涉及到的一些数值参数,大家可以根据自己的喜好进行修改。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay10
分支下看到,读者可以clone这个仓库到本地进行查看。
参考链接
- Unity的AudioSource
- AudioSource的API使用