Unity3D RPG Core | 26 反击石头人

石头人的击退技能加上远程攻击,让近身攻击变得困难。这节内容增加和石头的交互:我们通过点击石头人扔的石头,让其砸向石头人,进而造成反击。

1. 实现石头对角色造成伤害

石头既可以对角色造成伤害,也可以对石头人造成伤害。此处我们首先定义石头当前的状态:

  • public enum RockState
  • {
  •     HitPlayer,
  •     HitEnemy,
  •     HitNothing
  • }

我们通过碰撞来判断是否产生伤害。因为之前石头上增加了刚体组件,所以可以继承 OnCollisionEnter 函数。我们的主要实现逻辑都在 OnCollisionEnter() 中完成。

如代码清单 1 所示,因为石头一开始跑出来是要攻击角色的,所以首先将石头的状态设置为攻击角色(第 19 行)。OnCollisionEnter() 中首先完成攻击角色的功能。因为攻击角色状态下,石头还会碰到地面等物体,所以需要判断是否碰到了角色,这边通过标签判断(第 43 行)。

石头碰撞到角色后,会将角色击退,实现是兽人击退的逻辑是一样的(第 45 至 47 行),并产生眩晕(第 49 行)。伤害产生逻辑在 TakeDamage() 函数中(第 59 至 64 行),更新人物角色的生命值数据。最后将石头的状态更新为 HitNothing 状态。

代码清单 1 对人物造成伤害
  1. public class RockController : MonoBehaviour
  2. {
  3.     Rigidbody m_rigidbody;
  4.     GameObject m_target = null;
  5.  
  6.     public float m_force  = 20;
  7.     public int   m_damage = 5;
  8.     public enum RockState
  9.     {
  10.         HitPlayer,
  11.         HitEnemy,
  12.         HitNothing
  13.     }
  14.     RockState m_rockState;
  15.     Vector3   m_direction;
  16.     // Start is called before the first frame update
  17.     void Start()
  18.     {
  19.         m_rockState = RockState.HitPlayer;
  20.         m_rigidbody = GetComponent<Rigidbody>();
  21.         FlyToTarget();
  22.     }
  23.  
  24.     public void SetTarget(GameObject target)
  25.     {
  26.         m_target = target;
  27.     }
  28.  
  29.     void FlyToTarget()
  30.     {
  31.         if (m_target != null)
  32.         {
  33.             m_direction = (m_target.transform.position - transform.position + Vector3.up).normalized;
  34.             m_rigidbody.AddForce(m_force * m_direction, ForceMode.Impulse);
  35.         }
  36.     }
  37.  
  38.     private void OnCollisionEnter(Collision collision)
  39.     {
  40.         switch (m_rockState)
  41.         {
  42.             case RockState.HitPlayer:
  43.                 if (collision.gameObject.CompareTag("Player"))
  44.                 {
  45.                     NavMeshAgent agent = collision.gameObject.GetComponent<NavMeshAgent>();
  46.                     agent.isStopped = true;
  47.                     agent.velocity = m_direction * m_force;
  48.  
  49.                     collision.gameObject.GetComponent<Animator>().SetTrigger("Dizzy");
  50.  
  51.                     TakeDamage(collision.gameObject.GetComponent<CharacterData>(), m_damage);
  52.  
  53.                     m_rockState = RockState.HitNothing;
  54.                 }
  55.                 break;
  56.         }
  57.     }
  58.  
  59.     void TakeDamage(CharacterData defender, int damage)
  60.     {
  61.         int finalDamage = Mathf.Max(damage - defender.CurrentDefence, 1);
  62.         defender.CurrentHealth = Mathf.Max(defender.CurrentHealth - finalDamage, 0);
  63.         //TODO:更新UI
  64.     }
  65. }
  66.  

攻击角色的功能实现好了之后,我们可以将人物的血量调成 1 进行测试。可以看到石头碰撞到角色后,角色被击退,被死亡(伤害逻辑正确)。

2. 实现石头对石头人造成伤害

如代码清单 2 所示,和对角色造成伤害的逻辑一样,如果碰撞到了石头人则对石头人造成伤害。此处是不想石头对其他敌人造成伤害,特定判断需要是石头人。判断石头人的方式是寻找物体上是否有石头人的代码组件(第 22 行)。

代码清单 2 对石头人造成伤害
  1. public class RockController : MonoBehaviour
  2. {
  3.     private void OnCollisionEnter(Collision collision)
  4.     {
  5.         switch (m_rockState)
  6.         {
  7.             case RockState.HitPlayer:
  8.                 if (collision.gameObject.CompareTag("Player"))
  9.                 {
  10.                     NavMeshAgent agent = collision.gameObject.GetComponent<NavMeshAgent>();
  11.                     agent.isStopped = true;
  12.                     agent.velocity = m_direction * m_force;
  13.  
  14.                     collision.gameObject.GetComponent<Animator>().SetTrigger("Dizzy");
  15.  
  16.                     TakeDamage(collision.gameObject.GetComponent<CharacterData>(), m_damage);
  17.  
  18.                     m_rockState = RockState.HitNothing;
  19.                 }
  20.                 break;
  21.             case RockState.HitEnemy:
  22.                 if (collision.gameObject.GetComponent<GolemController>())
  23.                 {
  24.                     TakeDamage(collision.gameObject.GetComponent<CharacterData>(), m_damage);
  25.                     Destroy(gameObject);
  26.                 }
  27.                 break;
  28.         }
  29.     }
  30. }

造成伤害的逻辑写好了,还剩下点击石头进行交互的功能。我们把点击交互的功能复用之前点击攻击的功能,石头交互的逻辑放在之前 PlayerController.Hit() 的动画事件函数中。

2.1 添加可攻击物体标签

为了判断攻击的物体是敌人还是石头,如图 1 所示,我们为石头指定一个新创建的 Attackable 标签。

图1 添加新标签

添加好新标签之后,我们回到 PlayerController 中。如代码清单 3 所示,我们改变 Hit() 函数中逻辑,如果当前攻击的对象是可攻击物体,则调用 HitAttackableObject();如果是敌人,则还是之前的伤害计算逻辑。

HitAttackableObject() 中实现可交互物体的逻辑。如果是石头,并且石头还需要是无攻击状态的,则改变它的状态为攻击敌人的状态。同时朝人物面向方向“投掷”石头。

是人物面向方向,需要瞄准😉。

代码清单 3 石头交互
  1. public class PlayerController : MonoBehaviour
  2. {
  3.     void HitAttackableObject()
  4.     {
  5.         RockController rock = m_enemyObj.GetComponent<RockController>();
  6.         if (rock != null &&
  7.             rock.m_rockState == RockController.RockState.HitNothing)
  8.         {
  9.             rock.m_rockState = RockController.RockState.HitEnemy;
  10.             m_enemyObj.GetComponent<Rigidbody>().AddForce(transform.forward * 20, ForceMode.Impulse);
  11.         }
  12.     }
  13.  
  14.     void HitEnemy()
  15.     {
  16.         CharacterData enemyCharacterData = m_enemyObj.GetComponent<CharacterData>();
  17.         m_attackData.TakeDamage(enemyCharacterData);
  18.     }
  19.  
  20.     // Animation Event
  21.     void Hit()
  22.     {
  23.         if (m_enemyObj.CompareTag("Attackable"))
  24.             HitAttackableObject();
  25.         else
  26.             HitEnemy();
  27.     }
  28. }

2.2 鼠标逻辑

目前 Attackable 的标签鼠标还不能响应,我们需要回到 MouseManager 中添加。如代码清单 4 所示,Attackable 的识别响应逻辑和 Enemy 的一样。

代码清单 4 鼠标逻辑
  1. public class MouseManager : Singleton<MouseManager>
  2. {
  3.     public void SetCursorTexture(RaycastHit raycastHit)
  4.     {
  5.         string tag = raycastHit.collider.gameObject.tag;
  6.         if (tag == "Ground")
  7.             Cursor.SetCursor(m_textureMove, new Vector2(16, 16), CursorMode.Auto);
  8.         else if (tag == "Enemy")
  9.             Cursor.SetCursor(m_textureAttack, new Vector2(16, 16), CursorMode.Auto);
  10.         else if (tag == "Attackable")
  11.             Cursor.SetCursor(m_textureAttack, new Vector2(16, 16), CursorMode.Auto);
  12.     }
  13.  
  14.     public void DoMouseEvent(RaycastHit raycastHit)
  15.     {
  16.         if (Input.GetMouseButtonDown(0))
  17.         {
  18.             string tag = raycastHit.collider.gameObject.tag;
  19.             if (tag == "Ground")
  20.                 OnMoveMouseClick.Invoke(raycastHit.point);
  21.             else if (tag == "Enemy")
  22.                 OnAttackMouseClick.Invoke(raycastHit.collider.gameObject);
  23.             else if (tag == "Attackable")
  24.                 OnAttackMouseClick.Invoke(raycastHit.collider.gameObject);
  25.         }
  26.     }
  27. }

3. 改变到 HitNothing 状态

现在只有石头击中角色时,才会变成 HitNothing 状态。如果角色通过走位躲掉石头,就会无法反击石头人。

没有击中角色状态下,我们判断石头是否达到静止,如果静止了则将状态置为 HitNothing。因为是物理信息,所以我们需要放在 FixedUpdate() 中进行实时检测,而不是 Update() 中。

如代码清单 5 所示,我们判断速度的平方值,如果小于 1,则切换状态为 HitNothing(第 15 到 18 行)。注意刚开始时石头的速度是零,我们需要将其设置为单位向量(长度为 1),否则会帧检测时直接变为 HitNothing 状态。

代码清单 5 物理检测
  1. public class RockController : MonoBehaviour
  2. {
  3.     // Start is called before the first frame update
  4.     void Start()
  5.     {
  6.         m_rigidbody = GetComponent<Rigidbody>();
  7.         m_rigidbody.velocity = Vector3.one;
  8.         m_rockState = RockState.HitPlayer;
  9.         FlyToTarget();
  10.     }
  11.  
  12.     private void FixedUpdate()
  13.     {
  14.         //Debug.Log("velocity = " + m_rigidbody.velocity.sqrMagnitude);
  15.         if (m_rigidbody.velocity.sqrMagnitude < 1)
  16.         {
  17.             m_rockState = RockState.HitNothing;
  18.         }
  19.     }
  20. }

不要忘了在对石头攻击加力的时候,也要先设置初始速度,否则也会切换到 HitNothing 状态。

  1. public class PlayerController : MonoBehaviour
  2. {
  3.     void HitAttackableObject()
  4.     {
  5.         RockController rock = m_enemyObj.GetComponent<RockController>();
  6.         if (rock != null &&
  7.             rock.m_rockState == RockController.RockState.HitNothing)
  8.         {
  9.             Rigidbody rigidbody = m_enemyObj.GetComponent<Rigidbody>();
  10.             rigidbody.velocity = Vector3.one;
  11.             rock.m_rockState = RockController.RockState.HitEnemy;
  12.             rigidbody.AddForce(transform.forward * 20, ForceMode.Impulse);
  13.         }
  14.     }
  15. }

石头达到静止状态的时候,角色会穿模石头。需要为角色物体也设置 Rigidbody 组件,并勾选 Is Kinematic 选项。

4. 实现石头破碎效果

我们使用粒子系统实现石头破碎的效果。在 Hierarchy 窗体中右击选择 Effects - Particle System 创建一个粒子系统对象。

图2 粒子系统属性

如图 2 所示,粒子系统可设置的属性非常多,我们来一点点设置以满意我们的要求:

1. 取消勾选 Looping,不需要循环播放。

2. Duration 可以设置播放时长,这边设置为 0.2。

3. Max ParticlesEmission - Rate over Time 决定粒子的数量。这边将 Rate over Time 设置为 40。

4. Gravity Modifier 设置为 1,指定重力大小,让粒子不往上飘。

5. 我们想产生的粒子也能发生碰撞效果,设置 Collision - Type 为 World;Collides With 保持为 Everything。

6. 设置 Start Lifetime 为 1,Start Speed 为 4,让粒子的消亡的快一点。

7. 需要指定粒子的形状是石头。指定 Renderer - Meshes 为 RockMesh。Renderer - Material 指定为 PolyartStandard。

8. 默认的石头太大了。我们将 Start Size 设置为 0.1 到 0.6 这个范围。

9. 想要石头炸裂时会旋转。设置 Rotation over Lifetime - Angular Velocity 为 90;Rotation by Speed - Angular Velocity 为 100。

以上我们的破碎粒子特效已经制作好了。我们将其拖拽到 Assets 窗体中设置为预制体,并删除原先 Hierarchy 窗体中的对象。

如代码清单 6 所示,我们创建一个 GameObject 用于拖拽指定上述创建好的粒子系统(第 3 行)。在石头击中石头人时实例化粒子系统(第 13 行)。因为默认勾选了 Play On Awake,所以实例化时会自动播放。

代码清单 6 设置粒子系统
  1. public class RockController : MonoBehaviour
  2. {
  3.     public GameObject m_breakEffect;
  4.  
  5.     private void OnCollisionEnter(Collision collision)
  6.     {
  7.         switch (m_rockState)
  8.         {
  9.             case RockState.HitEnemy:
  10.                 if (collision.gameObject.GetComponent<GolemController>())
  11.                 {
  12.                     TakeDamage(collision.gameObject.GetComponent<CharacterData>(), m_damage);
  13.                     Instantiate(m_breakEffect, transform.position, Quaternion.identity);
  14.                     Destroy(gameObject);
  15.                 }
  16.                 break;
  17.         }
  18.     }
  19. }

至此石头反击相关的内容就都实现完毕了。最终的效果如图 3 所示。

图3 最终效果