《土豆荣耀》重构笔记(十七)随机生成可拾取道具

前言

  我们在前面的文章中,已经实现了随机生成足够多的怪物的功能。为了能延长游戏时间,增加游戏的趣味性,我们需要随机生成一些可拾取的道具,来恢复角色的血量或者增加角色可释放的炸弹数可拾取道具的需求如下:

可拾取道具的需求:

  1. 可拾取道具在设定上是空投补给的,所以在落地之前,可拾取道具将会在降落伞的作用下缓慢下降,在落地之后,可拾取道具上的降落伞缓慢消失
  2. 可拾取道具一共有两种,分别是能恢复角色血量医疗箱增加角色可释放炸弹数装备箱
  3. 可拾取道具降落的过程中也能被角色拾取
  4. 装备箱导弹击中会被直接引爆

制作降落伞动画

  根据可拾取道具的需求,我们知道降落伞应该有两个动画,一个是降落时在空中飘动的动画,另一个则是落地时缓慢消失的动画。首先,我们在Assets\AnimationAssets\Animator文件夹下新建一个名为Prop的文件夹,分别用来保存降落伞的动画文件动画状态机文件。创建完毕之后,我们先来制作降落伞降落时在空中飘动的动画

制作降落伞降落时在空中飘动的动画的步骤:

  1. 在场景中创建一个名为ParachuteEmpty GameObject,然后将Assets\Sprites\Props文件夹下的prop_parachute图片拖拽至Parachute物体,使其成为Parachute物体的子物体
  2. prop_parachute物体上SpriteRenderer组件的Sorting Layer设置为ForegroundOrder In Layer设置为1
  3. 打开Animation窗口,在Hierarchy窗口中选中Parachute物体,然后点击Create创建一个名为FloatDown.anim的动画,并将其保存在Assets\Animation\Prop文件夹下
  4. Assets\Animation\Prop文件夹下的Parachute.controller文件移动至Assets\Animator\Prop文件夹下
  5. 点击Animation窗口的红点按钮,开始为FloatDown.anim添加关键帧,添加的关键帧信息如下:

    FloatDown.anim的关键帧:

    1. 第一帧
      • frame: 0
      • Parachute:Rotaition: (0, 0, -12)
    2. 第二帧
      • frame: 30
      • Parachute:Rotaition: (0, 0, 12)
    3. 第三帧
      • frame: 60
      • Parachute:Rotaition: (0, 0, 12)

  制作完成之后,我们接着制作降落伞落地时缓慢消失的动画

制作降落伞落地时缓慢消失的动画的步骤:

  1. Parachute物体添加Destroyer.cs脚本
  2. Animation窗口选中FloatDown下拉框,然后点击Create New Clip创建一个名为Landing.anim的动画,并将其保存在Assets\Animation\Prop文件夹下
  3. 点击Animation窗口的红点按钮,开始为Landing.anim添加关键帧,添加的关键帧信息如下:

    Landing.anim的关键帧:

    1. 第一帧
      • frame: 0
      • prop_parachute:Scale: (1, 1, 1)
      • prop_parachute:GameObject.IsActive: true
      • prop_parachute:Sprite Renderer.Color: (1, 1, 1, 1)
    2. 第二帧
      • frame: 45
      • prop_parachute:Sprite Renderer.Color: (1, 1, 1, 0)
    3. 第三帧
      • frame: 60
      • prop_parachute:Scale: (0, 0, 1)
      • prop_parachute:GameObject.IsActive: false
  4. Landing.anim的最后一帧处添加一个Animation Event,选择DestroyGameObject方法作为这个Animation Event的调用方法

制作降落伞的其他工作

  制作完成降落伞的所有动画之后,我们接下来继续编辑控制降落伞动画动画状态机,也就是Unity自动为我们创建的Parachute.controller文件。

编辑Parachute.controller的步骤:

  1. 打开Animator窗口,然后选择Hierarchy窗口中的Parachute物体
  2. 新建一个名为LandingTrigger变量
  3. 新建一个从FloatDownLandingTransition,然后设置转移条件Landing这一Trigger变量,并取消Has Exit Time的勾选

    Parachute.controller

  降落伞的动画状态机编辑完成之后,我们还需要让降落伞能在重力的作用下缓慢下降。因此,我们需要为Parachute物体添加Rigidbody2D组件,并将Parachute物体上Rigidbody2D组件的Linear Grag属性设置为6,用来模拟降落伞下落时受到的空气阻力


编写控制可拾取道具的脚本

  降落伞制作完毕之后,我们先根据可拾取道具的需求来编写控制可拾取道具的脚本。首先,我们在Assets\Scripts文件夹下创建一个名为Prop的文件夹,并在Assets\Scripts\Prop文件夹下分别创建一个名为MedicalBoxPickup和一个名为AmmunitionBoxPickup的C#脚本。创建完毕之后,我们先来编写MedicalBoxPickup.cs,用于控制医疗箱

MedicalBoxPickup.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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CircleCollider2D))]
[RequireComponent(typeof(BoxCollider2D))]
public class MedicalBoxPickup : MonoBehaviour {
[Tooltip("医疗箱的治疗量")]
public float HealAmount;
[Tooltip("被拾取时播放的音效")]
public AudioClip PickupEffect;

private Animator m_Animator;
private bool m_Landed;

private void Awake() {
m_Animator = transform.root.GetComponent<Animator>();

GetComponent<CircleCollider2D>().isTrigger = true;
}

private void Start() {
m_Landed = false;
}

private void OnTriggerEnter2D(Collider2D collision) {
// 接触到地面
if (collision.tag == "Ground" && !m_Landed) {
m_Landed = true;

// 脱离降落伞
transform.parent = null;
gameObject.AddComponent<Rigidbody2D>();

// 播放降落伞的落地动画
m_Animator.SetTrigger("Landing");

return;
}

// 被角色拾取
if(collision.CompareTag("Player")) {
// 恢复角色血量
collision.GetComponent<PlayerHealth>().Heal(HealAmount);

// 播放拾取音效
AudioSource.PlayClipAtPoint(PickupEffect, transform.position);

// 销毁整个物体
Destroy(transform.root.gameObject);
}
}
}

代码说明:
&emsp;&emsp;这里,我们使用Trigger来识别当前降落伞触碰到哪些物体,因此我们需要新建一个名为GroundTag,并将Foreground物体下的子物体env_PlatformBridgeenv_PlatformBridge (1)env_PlatformTopenv_PlatformTop (1)以及env_PlatformUfoTag设置为Ground

&emsp;&emsp;接着,我们需要改写PlayerHealth.cs,编辑PlayerHealth.cs如下:

PlayerHealth.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
115
116
117
118
119
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 bool m_IsDead;

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;
m_IsDead = false;
}

// 受伤函数
public void TakeDamage(Transform enemy, float hurtForce, float damage) {
if(m_IsDead) {
return;
}

// 处于免伤状态,不执行任何操作
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();
}
}

// 恢复血量
public void Heal(float healAmount) {
if(m_IsDead) {
return;
}

m_CurrentHP += healAmount;

UpdateHealthBar();
}

// 更新血量条的函数
private void UpdateHealthBar() {
// 限制当前血量的值
m_CurrentHP = Mathf.Clamp(m_CurrentHP, 0, MaxHP);

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() {
m_IsDead = true;

// 将碰撞体设置为Trigger,避免和其他物体产生碰撞效果
Collider2D[] cols = GetComponents<Collider2D>();
foreach(Collider2D c in cols) {
c.isTrigger = true;
}

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

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

代码说明:
&emsp;&emsp;这里,我们新增了一个变量m_IsDead来检测角色是否死亡,然后增加一个用于恢复角色血量的Heal函数。最后,我们在UpdateHealthBar内限制了角色的血量。

&emsp;&emsp;编写MedicalBoxPickup.cs之后,我们继续编写AmmunitionBoxPickup.cs来控制弹药箱

AmmunitionBoxPickup.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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CircleCollider2D))]
[RequireComponent(typeof(BoxCollider2D))]
public class AmmunitionBoxPickup : MonoBehaviour {
[Tooltip("增加的炸弹数")]
public int BombAmount = 1;

[Tooltip("被拾取时播放的音效")]
public AudioClip PickupEffect;

private Animator m_Animator;
private bool m_Landed;

private void Awake() {
m_Animator = transform.root.GetComponent<Animator>();

GetComponent<CircleCollider2D>().isTrigger = true;
}

private void Start() {
m_Landed = false;
}

private void OnTriggerEnter2D(Collider2D collision) {
// 接触到地面
if (collision.tag == "Ground" && !m_Landed) {
m_Landed = true;

// 脱离降落伞
transform.parent = null;
gameObject.AddComponent<Rigidbody2D>();

// 播放降落伞的落地动画
m_Animator.SetTrigger("Landing");

return;
}

// 被角色拾取
if(collision.tag == "Player") {
// 增加炸弹数
collision.GetComponent<PlayerAttack>().AddBomb(BombAmount);

// 播放拾取音效
AudioSource.PlayClipAtPoint(PickupEffect, transform.position);

// 销毁整个物体
Destroy(transform.root.gameObject);
}
}
}

&emsp;&emsp;最后,我们在PlayerAttack.cs中增加AddBomb函数:

PlayerAttack.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// [RequireComponent(typeof(Animator))]
[RequireComponent(typeof(PlayerController))]
public class PlayerAttack : MonoBehaviour {
...

public void AddBomb(int bombNum) {
m_CurrentBombNumber += 1;
}
}

制作可拾取道具

&emsp;&emsp;编写完控制可拾取道具的脚本之后,我们开始制作可拾取道具。首先,我们需要将Assets\Sprites\Props文件夹下的prop_crate_ammoprop_crate_health图片的Pixel Per Unit都调整为200创建完之后,我们复制场景中的Parachute物体,并将Parachute物体重命名为MedicalBoxPickup,将复制得到的Parachute(1)物体重名为AmmunitionBoxPickup。接着,我们在Assets\Prefabs文件夹下新建一个名为Props的文件夹,用于保存可拾取道具的Prefab。最后,可拾取道具不应该跟怪物产生交互,因此我们还需要新建一个名为PickupLayer,并在Layer Collision Matrix中取消Enemy-PickupSetting-Pickup这两项的勾选:

Layer Collision Matrix

&emsp;&emsp;做完这些准备工作之后,我们先来制作医疗箱这一可拾取道具:

医疗箱制作步骤如下:

  1. Assets\Sprites\Props文件夹下的prop_crate_health拖拽至MedicalBoxPickup物体成为其子物体
  2. 修改prop_crate_health物体的Position(0.2, -0.7, 0)
  3. prop_crate_health物体上SpriteRenderer组件的Sorting Layer设置为ForegroundOrder In Layer设置为0
  4. prop_crate_health物体添加MedicalBoxPickup.cs,设置Heal Amount50Pickup EffectAssets\Audio\FX下的healthPickup
  5. prop_crate_health物体上BoxCollider2D组件的Offset设置为(-0.2, 0)Size设置为(1.8, 1.4)
  6. prop_crate_health物体上CircleCollider2D组件的Offset设置为(-0.2, 0)Radius设置为1.4,并勾选Is Trigger
  7. MedicalBoxPickup物体的Layer设置为Pickup,并选择Yes, change children将子物体的Layer也设置为Pickup
  8. MedicalBoxPickup物体拖拽至Assets\Prefabs\Props文件夹下,将其制作为Prefab,并删除场景中的MedicalBoxPickup物体

&emsp;&emsp;医疗箱制作完毕之后,我们继续制作弹药箱

弹药箱制作步骤如下:

  1. Assets\Sprites\Props文件夹下的prop_crate_ammo拖拽至AmmunitionBoxPickup物体成为其子物体
  2. 修改prop_crate_ammo物体的Position(-0.1, -0.65, 0)
  3. prop_crate_ammo物体上SpriteRenderer组件的Sorting Layer设置为ForegroundOrder In Layer设置为0
  4. prop_crate_ammo物体添加AmmunitionBoxPickup.cs,设置Bomb Amount2Pickup EffectAssets\Audio\FX下的bombCollect
  5. prop_crate_ammo物体上BoxCollider2D组件的Offset设置为(0, -0.05)Size设置为(1.84, 1.25)
  6. prop_crate_ammo物体上CircleCollider2D组件的Offset设置为(0, 0)Radius设置为1.4,并勾选Is Trigger
  7. AmmunitionBoxPickup物体的Layer设置为Pickup,并选择Yes, change children将子物体的Layer也设置为Pickup
  8. 新建一个名为AmmunitionBoxTag,将prop_crate_ammo物体的Tag设置为AmmunitionBox
  9. prop_crate_ammo物体添加Bomb.cs,并修改其属性如下:

    Bomb.cs的属性

    • DamageAmount: 50
    • BombRadius: 10
    • BombForce: 800
    • BoomClip: Assets\Audio\FX下的bigboom
    • FuseTime: 1.5f
    • FuseClip: Assets\Audio\FX下的bombfuse
  10. AmmunitionBoxPickup物体拖拽至Assets\Prefabs\Props文件夹下,将其制作为Prefab,并删除场景中的AmmunitionBoxPickup物体

&emsp;&emsp;弹药箱制作完毕之后,为了能让弹药箱导弹击中之后直接引爆,我们需要修改Missile.csOnTriggerEnter2D函数:

Missile.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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(CapsuleCollider2D))]
public class Missile : MonoBehaviour {
...

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

// 引爆弹药箱
if(collider.tag == "AmmunitionBox") {
collider.GetComponent<Bomb>().Explode();
return;
}

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

OnExplode();
}
}

&emsp;&emsp;最后,如果弹药箱在降落过程中被引爆,那么我们需要直接销毁整个降落伞,所以我们还需要修改Bomb.csExplode函数

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

[RequireComponent(typeof(AudioSource))]
public class Bomb : MonoBehaviour {
...

// 爆炸函数
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(transform.root.gameObject);
}
}

生成可拾取道具

&emsp;&emsp;最后,我们还需要使用Generator来生成我们制作好的可交互道具。首先,我们在Hierarchy窗口的Generator物体下创建一个名为PickupGeneratorsEmpty GameObject。接着,我们在PickupGenerators物体下创建一个名为PickupGeneratorEmpty GameObject,并为PickupGenerator物体添加Generator.cs组件。

PickupGenerator的设置如下:

  • Position: (-10, 20, 0)
  • Generate Delay: 5
  • Generate Interval: 10
  • Prefab Orientation: None
  • Prefabs:
    • Element0: Assets\Prefabs\Props文件夹下的AmmunitionBoxPickup物体的Prefab
    • Element1: Assets\Prefabs\Props文件夹下的MedicalBoxPickup物体的Prefab

&emsp;&emsp;编辑完之后,我们将PickupGenerator物体拖拽至Assets\Prefabs\Generators文件夹下将其制作为Prefab。然后复制场景中已经成为实例对象的PickupGenerator物体得到PickupGenerator (1)物体,并将PickupGenerator (1)物体的Position修改为(10, 20, 0)。运行游戏,我们可以看到此时游戏中已经能不断随机生成弹药箱医疗箱


后言

&emsp;&emsp;至此,我们已经完成了随机生成可交互道具的功能。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay15分支下看到,读者可以clone这个仓库到本地进行查看。


《土豆荣耀》重构笔记(十七)随机生成可拾取道具
https://asancai.github.io/posts/7abcd8c8/
作者
RainbowCyan
发布于
2019年1月26日
许可协议