《土豆荣耀》重构笔记(十二)对怪物造成伤害


前言

  在上篇文章,我们已经实现了发射导弹的功能,但此时导弹并不能对怪物造成任何伤害。接下来,我们来实现对怪物造成伤害的功能。


为怪物添加血量管理

  打开Enemy.cs,可以看到目前怪物只有在场景里面行走的功能。在实现导弹对怪物造成伤害的功能之前,我们首先为怪物增加血量管理的功能。

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>();
    &#125;

    private void Start() &#123;
        // 初始化变量
        m_LayerMask = LayerMask.GetMask("Obstacle");
        m_CurrentHP = MaxHP;
        m_Hurt = false;
        m_Dead = false;
    &#125;

    private void Update () &#123;
        // 死亡之后不执行任何操作
        if(m_Dead) &#123;
            return;
        &#125;

        Collider2D[] frontHits = Physics2D.OverlapPointAll(FrontCheck.position, m_LayerMask);

        if(frontHits.Length > 0) &#123;
            m_Wander.Flip();
        &#125;
    &#125;

    // 受伤函数
    public void TakeDamage(Transform weapon, float hurtForce, float damage) &#123;
        // 减少当前的HP
        m_CurrentHP -= damage;

        // 制造击退效果
        Vector3 hurtVector = transform.position - weapon.position;
        m_Rigidbody2D.AddForce(hurtVector.normalized * hurtForce);

        // 判断当前是否第一次受伤
        if(!m_Hurt) &#123;
            m_Hurt = true;

            if(DamagedSprite != null) &#123;
                // 禁用原有的Sprite
                SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>();
                foreach(SpriteRenderer child in children) &#123;
                    child.enabled = false;
                &#125;

                // 显示怪物受伤图片
                if(BodySpriteRenderer != null) &#123;
                    BodySpriteRenderer.enabled = true;
                    BodySpriteRenderer.sprite = DamagedSprite;
                &#125; else &#123;
                    Debug.LogError("请设置BodySpriteRenderer");
                &#125;
            &#125; else &#123;
                Debug.LogWarning("请设置DamagedSprite");
            &#125;
        &#125;

        // 判断当前的是否死亡
        if(m_CurrentHP <= 0 && !m_Dead) &#123;
            m_Dead = true;
            Death();
        &#125;
    &#125;

    private void Death() &#123;
        // 禁用Wander.cs
        m_Wander.enabled = false;

        if(DeadSprite != null) &#123;
            // 禁用原有的Sprite
            SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>();
            foreach(SpriteRenderer child in children) &#123;
                child.enabled = false;
            &#125;

            // 显示怪物死亡图片
            if(BodySpriteRenderer != null) &#123;
                BodySpriteRenderer.enabled = true;
                BodySpriteRenderer.sprite = DeadSprite;
            &#125; else &#123;
                Debug.LogError("请设置BodySpriteRenderer");
            &#125;
        &#125; else &#123;
            Debug.LogWarning("请设置DeadSprite");
        &#125;

        // 将所有的Collider2D都设置为Trigger,避免和其他物体产生物理碰撞
        Collider2D[] cols = GetComponents<Collider2D>();
        foreach(Collider2D c in cols) &#123;
            c.isTrigger = true;
        &#125;

        // 随机播放死亡的音效
        if(DeathClips != null && DeathClips.Length > 0) &#123;
            int i = Random.Range(0, DeathClips.Length);
            AudioSource.PlayClipAtPoint(DeathClips[i], transform.position);
        &#125; else &#123;
            Debug.LogWarning("请设置DeathClips");
        &#125;
    &#125;
&#125;

代码说明:

  • AudioSource.PlayClipAtPoint: 是AudioSource的一个静态函数,不需要添加AudioSource组件就能在游戏场景里面播放音效
  • GetComponentsInChildren: 获取自身子物体上所有目标组件的引用,注意,也包括自身
  • enabled: 当我们将某个组件的enabled属性设置为false的时候,该组件会被禁用,相当于我们在Inspector窗口中取消该组件的勾选

  修改完成之后,我们分别为AlienShipAlienSlug的属性进行赋值:

AlienShipAlienSlug的属性设置:

  • 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-death1enemy-death2enemy-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-death1enemy-death2enemy-death3

  设置完成之后,我们将AlienShipAlienSlug的修改Apply至它们的Prefab上,完成为怪物添加血量管理的工作。


让导弹伤害怪物的功能

  为怪物添加血量管理之后,我们还需要为导弹添加伤害怪物的功能。根据软件设计原则的单一职责原则(Single Responsibility Principle),导弹对怪物能造成伤害,那么应该由导弹维护对怪物造成伤害的数值以及对怪物造成伤害时击退力的大小等数值。清楚了这个以后,我们修改Missile.cs的代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(CapsuleCollider2D))]
public class Missile : MonoBehaviour &#123;
    [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() &#123;
        // 获取引用
        m_Rigidbody2D = GetComponent<Rigidbody2D>();
        m_Trigger = GetComponent<CapsuleCollider2D>();
    &#125;

    private void Start() &#123;
        // 确保Body Type为Kinematic
        m_Rigidbody2D.bodyType = RigidbodyType2D.Kinematic;

        // 根据导弹朝向设置速度
        if(FacingRight) &#123;
            m_Rigidbody2D.velocity = new Vector2(Speed, 0);
        &#125; else &#123;
            m_Rigidbody2D.velocity = new Vector2(-Speed, 0);
        &#125;

        // 确保勾选了Trigger
        m_Trigger.isTrigger = true;
    &#125;

    public void Flip() &#123;
        // 更新朝向
        FacingRight = !FacingRight;

        // 修改scale的x分量实现转向
        this.transform.localScale = Vector3.Scale(
            new Vector3(-1, 1, 1),
            this.transform.localScale
        );
    &#125;

    // 导弹爆炸时调用的函数
    private void OnExplode() &#123;
        if(Explosion != null) &#123;
            // 随机生成一个四元数
            Quaternion randomRotation = Quaternion.Euler(0f, 0f, Random.Range(0f, 360f));
            // 实例化爆炸对象
            Instantiate(Explosion, transform.position, randomRotation);
        &#125; else &#123;
            Debug.LogWarning("请设置Explosion");
        &#125;

        //销毁自身
        Destroy(gameObject);
    &#125;

    private void OnTriggerEnter2D(Collider2D collider) &#123;
        // 不对角色产生任何操作
        if(collider.CompareTag("Player")) &#123;
            return;
        &#125;

        // 对怪物造成伤害
        if(collider.CompareTag("Enemy")) &#123;
            collider.GetComponent<Enemy>().TakeDamage(this.transform, HurtForce, DamageAmount);
        &#125;

        OnExplode();
    &#125;
&#125;

  修改完成之后运行游戏,然后发射导弹攻击怪物,可以看到怪物在导弹的攻击之下会受伤并死亡。


让怪物死亡后不再运动

  虽然这个时候,导弹已经能对怪物造成伤害了,但是怪物死亡之后下落的速度太慢,且水平方向上的速度不为0,我们需要加快怪物下落的速度,并让怪物死亡之后不再运动。

  首先,我们将AlienSlugAlienShipRigidbody2D组件的Gravity Scale都设置为和Player一样的3.1。然后,我们编辑Wander.cs,加入一个OnDisable函数。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Wander : MonoBehaviour &#123;
    [Tooltip("是否朝向右边")]
    [SerializeField]
    private bool FacingRight = true;

    [Tooltip("怪物水平移动的速度")]
    [SerializeField]
    private float MoveSpeed = 2f;


    //用于设置怪物对象的物理属性
    private Rigidbody2D m_Rigidbody;
    // 用于保存当前的水平移动速度
    private float m_CurrentMoveSpeed;

    // 获取组件引用
    private void Awake() &#123;
        m_Rigidbody = GetComponent<Rigidbody2D>();
    &#125;

    // 设置字段的初始值
    private void Start() &#123;
        if(FacingRight) &#123;
            m_CurrentMoveSpeed = MoveSpeed;
        &#125; else &#123;
            m_CurrentMoveSpeed = -MoveSpeed;
        &#125;
    &#125;

    // 执行和物理相关的代码
    private void FixedUpdate() &#123;
        m_Rigidbody.velocity = new Vector2(m_CurrentMoveSpeed, m_Rigidbody.velocity.y);
    &#125;

    // 在Wander.cs脚本被禁用时被调用
    private void OnDisable() &#123;
        // 设置水平方向上的速度为0
        m_Rigidbody.velocity = new Vector2(0f, m_Rigidbody.velocity.y);
    &#125;

    // 转向函数
    public void Flip() &#123;
        m_CurrentMoveSpeed *= -1;

        this.transform.localScale = Vector3.Scale(new Vector3(-1, 1, 1), this.transform.localScale);
    &#125;
&#125;

代码说明:
  OnDisable函数是Unity提供的生命周期函数之一,当脚本组件被禁用的时候,会先调用脚本组件的OnDisable函数,然后再禁用组件。

  修改完成之后,再次运行游戏,可以看到怪物死亡后的下落速度增快,且下落的过程中水平速度为0。


重构PlayerHealth.cs和Enemy.cs

  之前为了便于测试,我们在PlayerHealth.cs中实现了角色接触怪物受伤的功能。根据软件设计原则的单一职责原则(Single Responsibility Principle),因为是怪物对角色造成伤害,那么我们应该在Enemy.cs中实现怪物对角色造成伤害的功能。因此,我们在PlayerHealth.cs中去掉被怪物伤害的代码。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerHealth : MonoBehaviour &#123;
    [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() &#123;
        m_Rigidbody2D = GetComponent<Rigidbody2D>();
    &#125;

    private void Start() &#123;
        // 初始化变量
        m_CurrentHP = MaxHP;
        m_LastFreeDamageTime = 0f;
        m_InitHealthScale = HealthSprite.transform.localScale;
    &#125;

    // 受伤函数
    public void TakeDamage(Transform enemy, float hurtForce, float damage) &#123;
        // 处于免伤状态,不执行任何操作
        if(Time.time <= m_LastFreeDamageTime + FreeDamagePeriod) &#123;
            return;
        &#125;

        // 更新上次受伤害的时间
        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) &#123;
            int i = Random.Range(0, OuchClips.Length);
            AudioSource.PlayClipAtPoint(OuchClips[i], transform.position);
        &#125; else &#123;
            Debug.LogWarning("请设置OuchClips");
        &#125;

        // 角色死亡
        if(m_CurrentHP <= 0f) &#123;
            Death();
        &#125;
    &#125;

    // 更新血量条的函数
    private void UpdateHealthBar() &#123;
        if(HealthSprite != null) &#123;
            // 更新血量条颜色
            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));
        &#125; else &#123;
            Debug.LogError("请设置HealthSprite");
        &#125;
    &#125;

    // 死亡函数
    private void Death() &#123;
        // 将碰撞体设置为Trigger,避免和其他物体产生碰撞效果
        Collider2D[] cols = GetComponents<Collider2D>();
        foreach(Collider2D c in cols) &#123;
            c.isTrigger = true;
        &#125;

        // 禁用脚本
        GetComponent<PlayerController>().enabled = false;

        // 播放死亡动画
        GetComponent<Animator>().SetTrigger("Death");
    &#125;
&#125;

代码说明:
  这里,我们移除了OnCollisionEnter2D这个函数,修改了函数TakeDamage的参数,并将角色当前是否处于免伤状态角色当前是否死亡这两个判断放在TakeDamage函数中执行。最后,因为之前没有涉及到Trigger,所以在Death函数中,我们直接禁用了角色所有Collider2D组件,这里,我们也需要将其改为将角色的所有Collider2D组件设置为Trigger

  接着,我们要在Enemy.cs中加入怪物对角色造成伤害的代码。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Wander))]
[RequireComponent(typeof(Rigidbody2D))]
public class Enemy : MonoBehaviour &#123;
    [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() &#123;
        // 获取引用
        m_Wander = GetComponent<Wander>();
        m_Rigidbody2D = GetComponent<Rigidbody2D>();
    &#125;

    private void Start() &#123;
        // 初始化变量
        m_LayerMask = LayerMask.GetMask("Obstacle");
        m_CurrentHP = MaxHP;
        m_Hurt = false;
        m_Dead = false;
    &#125;

    private void Update () &#123;
        // 死亡之后不执行任何操作
        if(m_Dead) &#123;
            return;
        &#125;

        Collider2D[] frontHits = Physics2D.OverlapPointAll(FrontCheck.position, m_LayerMask);

        if(frontHits.Length > 0) &#123;
            m_Wander.Flip();
        &#125;
    &#125;

    private void OnCollisionEnter2D(Collision2D collision) &#123;
        // 对角色造成伤害
        if(collision.gameObject.CompareTag("Player")) &#123;
            collision.gameObject.GetComponent<PlayerHealth>().TakeDamage(this.transform, HurtForce, DamageAmount);
        &#125;
    &#125;

    // 受伤函数
    public void TakeDamage(Transform weapon, float hurtForce, float damage) &#123;
        // 减少当前的HP
        m_CurrentHP -= damage;

        // 制造击退效果
        Vector3 hurtVector = transform.position - weapon.position;
        m_Rigidbody2D.AddForce(hurtVector.normalized * hurtForce);

        // 判断当前是否第一次受伤
        if(!m_Hurt) &#123;
            m_Hurt = true;

            if(DamagedSprite != null) &#123;
                // 禁用原有的Sprite
                SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>();
                foreach(SpriteRenderer child in children) &#123;
                    child.enabled = false;
                &#125;

                // 显示怪物受伤图片
                if(BodySpriteRenderer != null) &#123;
                    BodySpriteRenderer.enabled = true;
                    BodySpriteRenderer.sprite = DamagedSprite;
                &#125; else &#123;
                    Debug.LogError("请设置BodySpriteRenderer");
                &#125;
            &#125; else &#123;
                Debug.LogWarning("请设置DamagedSprite");
            &#125;
        &#125;

        // 判断当前的是否死亡
        if(m_CurrentHP <= 0 && !m_Dead) &#123;
            m_Dead = true;
            Death();
        &#125;
    &#125;

    private void Death() &#123;
        // 禁用Wander.cs
        m_Wander.enabled = false;

        if(DeadSprite != null) &#123;
            // 禁用原有的Sprite
            SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>();
            foreach(SpriteRenderer child in children) &#123;
                child.enabled = false;
            &#125;

            // 显示怪物死亡图片
            if(BodySpriteRenderer != null) &#123;
                BodySpriteRenderer.enabled = true;
                BodySpriteRenderer.sprite = DeadSprite;
            &#125; else &#123;
                Debug.LogError("请设置BodySpriteRenderer");
            &#125;
        &#125; else &#123;
            Debug.LogWarning("请设置DeadSprite");
        &#125;

        // 将所有的Collider2D都设置为Trigger,避免和其他物体产生物理碰撞
        Collider2D[] cols = GetComponents<Collider2D>();
        foreach(Collider2D c in cols) &#123;
            c.isTrigger = true;
        &#125;

        // 随机播放死亡的音效
        if(DeathClips != null && DeathClips.Length > 0) &#123;
            int i = Random.Range(0, DeathClips.Length);
            AudioSource.PlayClipAtPoint(DeathClips[i], transform.position);
        &#125; else &#123;
            Debug.LogWarning("请设置DeathClips");
        &#125;
    &#125;
&#125;

代码说明:
  这里,我们主要增加了HurtForceDamageAmount这两个字段,并增加了OnCollisionEnter2D函数,用于调用对角色造成伤害的TakeDamage函数。

  添加完毕之后,运行游戏,此时怪物能正常对角色造成伤害,没有出现Bug,重构顺利完成。


对参数的一些小调整

  至此,所有的代码实现工作都做完了。但是为了游戏的可玩性和平衡性,我们需要对参数进行一些小调整。这里,我们调整的思路是,血量低的怪物,移动速度快,对角色造成的伤害也高

AlienShipAlienSlug的属性设置:

  • AlienShip
    • Move Speed: 2
    • Damage Amount: 10
    • MaxHP: 20
  • AlienSlug
    • Move Speed: 3
    • Damage Amount: 20
    • MaxHP: 10

  修改完成之后,将AlienShipAlienSlug产生的修改Apply至它们的Prefab上,并保存场景产生的修改。


后言

  至此,我们所有的工作都已完成,本篇文章涉及到的一些数值参数,大家可以根据自己的喜好进行修改。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay10分支下看到,读者可以clone这个仓库到本地进行查看。


参考链接

  1. Unity的AudioSource
  2. AudioSource的API使用

文章作者: RainbowCyan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 RainbowCyan !
 上一篇
《土豆荣耀》重构笔记(十三)实现放置炸弹的功能 《土豆荣耀》重构笔记(十三)实现放置炸弹的功能
前言  在上篇文章,我们已经实现了对怪物造成伤害的功能,但此时我们只有发射导弹一种攻击方式。为了增加游戏的可玩性,我们将制作炸弹,玩家可以通过放置炸弹来对范围内的怪物造成伤害。在开始制作炸弹之前,我们先梳理一下和炸弹有关
下一篇 
《土豆荣耀》重构笔记(十一)实现发射导弹的功能 《土豆荣耀》重构笔记(十一)实现发射导弹的功能
前言  在实现了怪物攻击角色的功能之后,我们接下来需要实现玩家攻击怪物的功能。玩家攻击怪物的方式有发射导弹和放置炸弹。导弹在场景中会以恒定速率飞行,碰到物体时爆炸,若碰到怪物则对怪物造成伤害。接下来,我们开始制作能伤害怪
  目录