《土豆荣耀》重构笔记(十三)实现放置炸弹的功能

前言

  在上篇文章,我们已经实现了对怪物造成伤害的功能,但此时我们只有发射导弹一种攻击方式。为了增加游戏的可玩性,我们将制作炸弹玩家可以通过放置炸弹来对范围内的怪物造成伤害。在开始制作炸弹之前,我们先梳理一下和炸弹有关的需求。

炸弹有关的需求:

  1. 炸弹会对爆炸半径内的怪物角色造成一定的伤害
  2. 炸弹会对爆炸半径内的怪物角色产生一个冲击力
  3. 炸弹被释放后,会先燃烧一段时间引信,然后再爆炸
  4. 角色可以在原地释放炸弹、也可以通过火箭筒向前发射炸弹
  5. 炸弹属于大威力道具,因此炸弹的放置有冷却时间限制,且炸弹的数量是有限的

制作炸弹Prefab

  清楚了炸弹的需求之后,我们先来制作炸弹并实现炸弹爆炸对怪物角色造成伤害的功能。首先,我们将Assets\Sprites\Props下的prop_bomb拖拽到Hierarchy窗口中,然后将其重命名为Bomb。此时,可以看到场景里面出现了一个炸弹,但尺寸比较大。这里,我们需要先将Assets\Sprites\Props下的prop_bombPixel Per Unit改为500

设置Pixel Per Unit

  接着,因为炸弹能对角色造成伤害,为了在检测时提高效率,我们不能将PlayerLayer设置为Default,因为LayerDefault的物体很多,这会让我们增加很多不必要的判断。我们首先新建一个名为PlayerLayer,然后在Layer Collision Matrix中取消Player-Setting项的勾选,不让PlayerSetting这两个Layer的物体产生任何物理交互。最后,我们将物体PlayerLayer设置为Player,并将其修改ApplyPlayer的Prefab上。

设置Layer Collision Matrix

  接着,我们在Assets\Scripts\Weapons新建一个名为Bomb的C#脚本,然后编辑Bomb.cs如下:

Bomb.cs
1
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class Bomb : MonoBehaviour {
[Tooltip("炸弹产生的伤害值")]
public float DamageAmount = 50f;
[Tooltip("爆炸半径")]
public float BombRadius = 10f;
[Tooltip("爆炸时产生的冲击力")]
public float BombForce = 800f;
[Tooltip("炸弹爆炸时的音效")]
public AudioClip BoomClip;
[Tooltip("引信燃烧的时间")]
public float FuseTime = 1.5f;
[Tooltip("燃烧引信的音效")]
public AudioClip FuseClip;
[Tooltip("炸弹爆炸时的特效")]
public GameObject BombExplosion;

private LayerMask m_LayerMask;
private AudioSource m_AudioSource;

private void Awake() {
// 获取引用
m_AudioSource = GetComponent<AudioSource>();
// 取消默认播放
m_AudioSource.playOnAwake = false;
}

private void Start() {
// 设置LayerMask检测Enemy和Player这两个Layer
m_LayerMask = 1 << LayerMask.NameToLayer("Enemy") | 1 << LayerMask.NameToLayer("Player");

if (transform.root == transform){
// 如果不是附着在其他物体上,就开始执行燃烧引信的协程
StartCoroutine(BombDetonation());
}
}

// 燃烧引信的协程
private IEnumerator BombDetonation() {
// 设置燃烧引信的音效并播放
if(FuseClip != null) {
m_AudioSource.clip = FuseClip;
m_AudioSource.Play();
} else {
Debug.LogWarning("请设置FuseClip");
}

// 等待FuseTime时间
yield return new WaitForSeconds(FuseTime);

// 等待FuseTime时间之后,执行爆炸函数
Explode();
}

// 爆炸函数
public void Explode() {
// 获取一定范围内的所有Layer为Enemy或者Player物体
Collider2D[] objects = Physics2D.OverlapCircleAll(transform.position, BombRadius, m_LayerMask);

foreach(Collider2D obj in objects) {
// 对怪物造成伤害
if(obj.tag == "Enemy") {
obj.GetComponent<Enemy>().TakeDamage(this.transform, BombForce, DamageAmount);
continue;
}

// 对角色造成伤害
if(obj.CompareTag("Player")) {
obj.GetComponent<PlayerHealth>().TakeDamage(this.transform, BombForce, DamageAmount);
}
}

// 实例化爆炸特效
if(BombExplosion != null) {
Instantiate(BombExplosion, this.transform.position, Quaternion.identity);
} else {
Debug.LogWarning("请设置BombExplosion");
}

// 播放爆炸音效
if(BoomClip != null) {
AudioSource.PlayClipAtPoint(BoomClip, transform.position);
} else {
Debug.LogWarning("请设置BoomClip");
}

Destroy(gameObject);
}
}

代码说明:

  • m_LayerMask: 为了让大家能理解LayerMask的本质,我这里没有使用LayerMask.GetMask这个静态方法来获取Layer Mask,而是直接使用了位运算
  • StartCoroutine:用于开始执行一个Coroutines(协程)Unity中的协程是基于C#提供的IEnumerator(迭代器)实现的。Unity的协程功能非常强大,使用起来也比较简单。我们只需要先定义一个返回类型为IEnumerator的方法,然后在这个方法里面使用yield语句来设置协程等待的条件,并使用StartCoroutine来执行该方法。那么当程序执行到yield语句这里的时候,就会停止执行后面的代码,然后每帧检查是否满足条件,一旦满足条件就继续往下执行。
  • Physics2D.OverlapCircleAll:Unity提供的api,用于获取在某个圆形范围内的所有Collider2D

&emsp;&emsp;编辑完毕之后,我们将Bomb.cs添加到Hierarchy窗口的Bomb物体上,可以看到Unity自动帮我们在Bomb物体上添加了AudioSource组件。为了让炸弹具有物理属性,能和场景里其他物体发生物理碰撞,我们还需要为Bomb物体添加Rigidbody2DCircle Collider2D组件。然后,我们对每个组件进行以下的设置:

Bomb物体的组件设置:

  1. Sprite Renderer:
    • Sorting Layer: Weapons
    • Order In Layer: 0
  2. Bomb:
    • DamageAmount: 50
    • BombRadius: 10
    • BombForce: 800
    • BoomClip: Assets\Audio\FX下的bigboom
    • FuseTime: 1.5f
    • FuseClip: Assets\Audio\FX下的bombfuse
  3. Circle Collider2D:
    • Is Trigger: false
    • Offset: (0, 0)
    • Radius: 0.5
  4. Rigidbody2D
    • Gravity Scale: 3.1

&emsp;&emsp;设置完毕之后,保存修改,然后将Bomb物体从Hierarchy窗口拖拽至Assets\Prefabs\Weapons下将其做成Pfefab。接着,我们运行游戏,可以听到炸弹燃烧引信和爆炸的音效,也能看到角色和怪物被炸弹炸飞并受到伤害的效果。但炸弹燃烧引信爆炸的时候,没有任何的特效,无法让玩家直观地意识到炸弹在燃烧引信炸弹爆炸了。因此,我们还需要为炸弹添加引信燃烧特效爆炸特效


为炸弹添加爆炸特效

&emsp;&emsp;接下来,我们首先为炸弹添加爆炸特效。首先,我们将Assets\Sprites\FX下的Circle拖拽到Hierarchy窗口中,并将Circle物体重命名为BombExplosion。接着,我们将Assets\Scripts\Utility下的Destroyer.cs添加到BombExplosion物体上,并修改Destroyer.cs如下:

Destroyer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Destroyer : MonoBehaviour {
[Tooltip("是否在Awake时执行销毁操作")]
public bool DestroyOnAwake = false;
[Tooltip("销毁延迟时间")]
public float AwakeDestroyDelay = 0f;

private void Awake() {
if(DestroyOnAwake) {
Destroy(this.gameObject, AwakeDestroyDelay);
}
}

// 销毁自身
private void DestroyGameObject() {
Destroy(gameObject);
}
}

&emsp;&emsp;接着,我们设置BombExplosion物体的组件如下:

BombExplosion物体的组件设置:

  1. Sprite Renderer:
    • Sorting Layer: Weapons
    • Order In Layer: 3
  2. Destroyer:
    • Destroy On Awake: true
    • Awake Destroy Delay: 0.1

&emsp;&emsp;修改完成之后,我们将BombExplosion物体拖拽至Assets\Prefabs\Weapons文件夹下将其制作为Prefab。然后我们删除场景中的BombExplosion物体,选中Bomb的Prefab,将其Bomb.cs下的Bomb Explosion属性设置为BombExplosion的Prefab。最后保存修改,运行游戏,可以看到炸弹爆炸时产生了爆炸特效。

设置爆炸特效


为炸弹添加引信燃烧特效

&emsp;&emsp;为炸弹添加完爆炸特效之后,我们为炸弹添加引信燃烧特效。首先,我们在Bomb物体下创建一个名为SparksEmpty Gameobject,然后我们为其添加Particle System组件。

Sparks物体的Particle System组件设置:

  • Main Module:
    • Start Lifetime: 0.5
    • Start Size(Random Between Two Constants): (0.2, 1)
    • Start Rotation(Random Between Two Constants): (0, 360)
    • Gravity Modefier: 1
    • Simulation Space: World
    • Scaling Mode: Shape
    • Max Particles: 100
  • Emission:
    • Rate Over Time: 40
  • Shape:
    • Angle: 36
    • Radius: 0.01
    • Arc: 0.01
    • Randomize Direction: 1
  • Size over Lifetime:
    • Size(Random Between Two Constants): (0.4, 0.6)
  • Rotation over Lifetime:
    • Angular Velocity(Random Between Two Constants): (0, 180)
  • Texture Sheet Animation
    • Tiles: (2, 2)
  • Render
    • Material: Assets\Materials下的ExplosionParticle
    • Sorting Layer: Weapons
    • Order In Layer: 4

&emsp;&emsp;修改完成之后,我们还需要调整SparksTransform组件。

Sparks物体的Transform组件设置:

  • Position: (0.47, 0.53, 0)
  • Rotation: (0, 90, 0)

&emsp;&emsp;最后,我们将Bomb物体上的修改Apply至其Prefab上,然后运行游戏,可以看到炸弹已经有了引信燃烧特效了。


释放炸弹

&emsp;&emsp;为炸弹添加好引信燃烧炸弹爆炸特效之后,我们需要让角色能够释放炸弹。首先,我们删除场景中的Bomb物体。接着,我们修改PlayerAttack.cs如下:

PlayerAttack.cs
1
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// [RequireComponent(typeof(Animator))]
[RequireComponent(typeof(PlayerController))]
public class PlayerAttack : MonoBehaviour {
[Tooltip("导弹Prefab")]
public Missile MissilePrefab;
[Tooltip("导弹发射点")]
public Transform ShootingPoint;
[Tooltip("发射导弹的音效")]
public AudioClip ShootEffect;
[Tooltip("炸弹Prefab")]
public Rigidbody2D BombPrefab;
[Tooltip("炸弹的初始数量")]
public int InitBombNumber = 4;
[Tooltip("使用火箭筒抛射炸弹的力")]
public float ProjectileBombForce = 1000f;


private int m_CurrentBombNumber;

// private Animator m_Animator;
private PlayerController m_PlayerCtrl;

private void Awake() {
// 获取引用
// m_Animator = GetComponent<Animator>();
m_PlayerCtrl = GetComponent<PlayerController>();

// 检查关键属性是否赋值
if(MissilePrefab == null) {
Debug.LogError("请设置MissilePrefab");
}

if(ShootingPoint == null) {
Debug.LogError("请设置ShootingPoint");
}

if(BombPrefab == null) {
Debug.LogError("请设置BombPrefab");
}
}

private void Start() {
m_CurrentBombNumber = InitBombNumber;
}

private void Update() {
if (Input.GetButtonDown("Fire1")) {
// 发射导弹
Fire();
}

if (Input.GetButtonDown("Fire2")) {
// 放置炸弹
LayBomb();
}

if (Input.GetButtonDown("Fire3")) {
// 抛射炸弹
ProjectileBomb();
}
}

// 发射导弹
private void Fire() {
// // 播放射击动画
// m_Animator.SetTrigger("Shoot");

// 播放射击音效
AudioSource.PlayClipAtPoint(ShootEffect, ShootingPoint.position);

// 创建导弹
Missile instance = Instantiate(MissilePrefab, ShootingPoint.position, Quaternion.identity) as Missile;

// 如果角色跟导弹的朝向不一致,就翻转导弹
if(m_PlayerCtrl.FacingRight ^ instance.FacingRight) {
instance.Flip();
}
}

// 放置炸弹
private void LayBomb() {
if(m_CurrentBombNumber <= 0) {
return;
}

// 放置炸弹
Instantiate(BombPrefab, this.transform.position, Quaternion.identity);

// 减少炸弹数量
m_CurrentBombNumber --;
}

// 抛射炸弹
private void ProjectileBomb() {
if(m_CurrentBombNumber <= 0) {
return;
}

// 抛射炸弹
Rigidbody2D body = Instantiate(BombPrefab, ShootingPoint.position, Quaternion.identity) as Rigidbody2D;
if(m_PlayerCtrl.FacingRight) {
body.AddForce(Vector2.right * ProjectileBombForce);
} else {
body.AddForce(Vector2.left * ProjectileBombForce);
}

// 减少炸弹数量
m_CurrentBombNumber --;
}
}

&emsp;&emsp;修改完成之后,我们将BombPrefab设置为Bomb的Prefab,运行游戏,此时我们可以通过单击鼠标右键来在原地释放炸弹以及通过单击鼠标滚轮向前抛射炸弹。最后,我们将Player的修改Apply至其Prefab上,保存场景的修改。


后言

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


参考链接

  1. Unity的Coroutines
  2. yield语句的检查时机
  3. C#的IEnumerator

《土豆荣耀》重构笔记(十三)实现放置炸弹的功能
https://asancai.github.io/posts/7a9e2f41/
作者
RainbowCyan
发布于
2019年1月13日
许可协议