《土豆荣耀》重构笔记(十一)实现发射导弹的功能

前言

  在实现了怪物攻击角色的功能之后,我们接下来需要实现玩家攻击怪物的功能。玩家攻击怪物的方式有发射导弹放置炸弹导弹在场景中会以恒定速率飞行,碰到物体时爆炸,若碰到怪物则对怪物造成伤害。接下来,我们开始制作能伤害怪物导弹并实现角色发射导弹的功能。


调整图片的Pixels Per Unit

  在Hierarchy创建一个名为MissileEmpty GameObject,然后将Assets\Sprites\Props下的part_rocketpart_flame拖拽到Missile成为其子物体(由于part_flame是图集,所以添加到场景后会创建一个名为frame1的GameObject)。可以看到,此时part_rocketflame1比角色和怪物大了很多,这是因为part_rocketpart_flame的像素太大了,我们需要对part_rocketpart_flame进行调整。

  我们可以在游戏场景中调整part_rocketflame1Scale将其缩小,但是这样的话,我们每次往场景里添加part_rocketpart_flame时,都必须手动将其缩小一遍。为了减少不必要的修改,我们直接修改part_rocketpart_flamePixels Per Unit。在Unity里,Pixels Per Unit表示一个Unity3D单位对应该图片多少个图片像素,我们增大图片的Pixels Per Unit,那么将该图片添加到游戏场景中时,其大小也会随之变小。

  在Project窗口分别选中part_rocketpart_flame,然后将它们的Pixels Per Unit放大5倍,也就是分别修改为500125。修改完成后,点击右下角的Apply,可以看到,游戏场景中的part_rocketpart_flame都缩小了。

修改Pixels Per Unit


创建导弹Prefab

  接下来,我们创建一个名为WeaponsSorting Layer,然后调整Weapons这一Sorting Layer的位置,让其处于ForegroundCharacter之间。

Sorting Layer

Missile子物体各属性的值:

  • part_rocket:
    • Position: (0, 0, 0)
    • Sorting Layer: Weapons, Order In Layer: 1
  • flame1:
    • Position: (-1.5, 0, 0)
    • Sorting Layer: Weapons, Order In Layer: 0

  修改完成之后,我们在Assets\Prefabs下创建一个名为Weapons的文件夹,并将MissileHierarchy窗口拖到Weapons文件夹下,将Missile做成Prefab。


让导弹飞起来

  为了导弹拥有速度这一物理属性,我们需要为Missile添加Rigidbody2D组件。为了避免导弹出现翻转的问题,我们需要勾选Rigidbody2D组件的Freeze Rotation Z。此外,因为导弹在飞行的过程中,不会受到重力影响下落,为了提高游戏性能,我们可以将其Rigidbody2D组件的Body Type设置为Kinematic。关于Rigidbody2D组件各种Body Type对应的作用,可以查阅Unity关于Rigidbody2D的说明

  接着,为了让导弹知道是否撞上场景内的其他物体,我们为Missile添加一个Capsule Collider2D组件。因为导弹和其他物体碰撞之后,不会产生一系列的物理碰撞效果,所以为了提高游戏性能,我们可以将勾选Capsule Collider 2D组件的Is Trigger属性。这样,当Missile和其他物体发生碰撞时,物理引擎将不对Missile和其他物体进行碰撞模拟。

Missile的组件属性

  添加完Rigidbody2DCapsule Collider2D后,我们在Assets\Scripts下创建一个名为Weapons的文件夹,然后在Weapons文件夹下创建一个名为Missile的C#脚本。最后我们将Missile.cs添加到Missile上,并打开Missile.cs进行编辑。

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

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(CapsuleCollider2D))]
public class Missile : MonoBehaviour {
[Tooltip("爆炸效果")]
public GameObject Explosion;
[Tooltip("导弹飞行的速度")]
public float Speed = 25f;

private Rigidbody2D m_Rigidbody2D;
private CapsuleCollider2D m_Trigger;

private void Awake() {
// 获取引用
m_Rigidbody2D = GetComponent<Rigidbody2D>();
m_Trigger = GetComponent<CapsuleCollider2D>();
}

private void Start() {
// 确保Body Type为Kinematic
m_Rigidbody2D.bodyType = RigidbodyType2D.Kinematic;
// 设置速度
m_Rigidbody2D.velocity = new Vector2(Speed, 0);

// 确保勾选了Trigger
m_Trigger.isTrigger = true;
}

// 导弹爆炸时调用的函数
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;
}

OnExplode();
}
}

代码说明:

  • Rigidbody2D.bodyType:我们可以通过修改Rigidbody2D.bodyType的值来动态修改Rigidbody2D的类型
  • Collider2D.isTrigger:我们可以通过修改Collider2D.isTrigger的值来动态设置Collider2D是否开启Trigger属性
  • OnTriggerEnter2D:当我们开启Collider2D的Trigger属性时,我们需要使用OnTriggerEnter来知道Collider2D正和哪些物体发生接触。类似的可用于Trigger检测的函数还有OnTriggerStayOnTriggerExit

&emsp;&emsp;编辑完Missile.cs之后,运行游戏,可以看到导弹在场景内以恒定速率飞行,在接触到其他物体之后消失。


制作导弹爆炸动画

&emsp;&emsp;导弹在接触到其他物体之后直接消失游戏体验不佳,因此我们还需要一个爆炸的动画。

&emsp;&emsp;首先,在Assets\AnimationAssets\Animator下创建一个名为Weapons的文件夹。接着,打开Assets\Sprites\FX文件夹,可以看到part_explosionSprite ModeMultiple,被切割为四张图片。因为导弹爆炸动画是更换当前显示的图片实现的,四张图片分别代表爆炸动画的四个关键帧,这里,我们采用一种新的制作帧动画的方式来快速制作帧动画

&emsp;&emsp;在场景中新建一个名为MissileExplosion的Empty GameObject,然后同时选中part_explosion切割出来的四张图片,然后将它们拖拽到Hierarchy窗口中MissileExplosion上。因为我们同时给游戏场景里面添加了多个Sprite,Unity会认为我们想使用这些Sprite为MissileExplosion制作帧动画,所以会询问我们新建的帧动画保存的位置。这里,我们将该帧动画命名为MissileExplosion.anim并将其保存至Assets\Animation\Weapons文件夹下,Unity会自动利用我们选择的四张图片帮我们创建好帧动画

制作导弹爆炸动画

&emsp;&emsp;接着,我们在Project窗口将Assets\Animation\Weapons文件夹下的MissileExplosion.controller移动至Assets\Animator\Weapons文件夹下。因为我们是为空物体创建的动画,因此我们看不到动画的位置。为了便于观察,我们需要为MissileExplosion添加一个初始Sprite,这里我们选择Assets\Sprites\FXpart_explosion切割出来的part_explosion_0作为MissileExplosion的初始Sprite。

设置初始Sprite

&emsp;&emsp;最后,我们还需要在导弹爆炸时播放爆炸音效。在MissileExplosion下添加AudioSource组件,然后将Assets\Audio\FX下的rocketExplode拖拽到AudioClip的赋值框处。

添加爆炸音效

&emsp;&emsp;添加完音效之后,我们将MissileExplosion拖拽到Assets\Prefabs\Weapons文件夹下做成Prefab。运行游戏,可以听到播放了一次导弹爆炸音效,且导弹爆炸动画在不断循环播放。


添加Animation Event

&emsp;&emsp;我们不希望导弹爆炸动画一直在游戏场景中循环播放,我们希望导弹爆炸动画播放完之后,动画能自动被销毁。也就是说,我们希望导弹爆炸动画在播放至最后一帧时,能调用一个销毁自己的函数,我们可以使用Animation Event来完成这件事。

&emsp;&emsp;首先,我们在Assets\Scripts\Utility文件夹下创建一个名为Destroyer的C#脚本后,编辑Destroyer.cs

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

public class Destroyer : MonoBehaviour {

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

&emsp;&emsp;接着,把Destroyer.cs添加到Hierarchy窗口的MissileExplosion上。我们打开Animation窗口并选择MissileExplosion,在最后一帧处右击鼠标选择Add Animation Event并设置调用的函数为DestroyGameObject

Animation Event

&emsp;&emsp;运行游戏,可以看到爆炸动画播放完之后,被自动销毁。最后,我们将MissileExplosion产生的修改Apply至Prefab上,删除Hierarchy窗口中的MissileExplosion物体,并选中Assets\Prefabs\Weapons下的Missile,将MissileExplosion拖拽到Explosion赋值框。因为我们是在Prefab做修改,所以场景中所有Prefab的实例都会同步修改。再次运行游戏,可以看到导弹接触到物体之后,已经能正常产生爆炸效果了。

修改导弹Prefab


制作导弹飞行特效

&emsp;&emsp;为了避免让玩家决定导弹是在平移而不是飞行,我们需要在导弹飞行过程中加入火焰喷射动画烟雾拖尾粒子

&emsp;&emsp;首先,我们使用跟创建导弹爆炸动画一样的方式创建导弹的飞行过程中的火焰喷射动画。在Project窗口中同时选中flame1flame2两张图片,并将它们拖动至Missile的子物体flame1上。接着,我们将新创建的动画命名为Flame.anim并将其保存至Assets\Animation\Weapons下,并将Assets\Animation\Weapons下的flame1.controller移动至Assets\Animator\Weapons文件夹即可。此时,将导弹的飞行速度调小至2,然后运行游戏,就可以看到导弹飞行的过程中在播放火焰喷射动画。

&emsp;&emsp;除了创建火焰喷射动画,我们还需要使用粒子系统(Particle System)来制作导弹飞行时的烟雾拖尾效果。

&emsp;&emsp;首先,在Missile下创建一个名为trail的Empty GameObject,然后设置其Position(-0.5, 0, 0)。接着,我们为trail添加Particle System组件,因为Particle System组件里面还内嵌了很多包括EmissionRender等模块,我们不在这里一一细讲每个模块,感兴趣的同学可以参阅Particle System modules来了解例子系统各个模块的功能。

trail物体的Particle System组件设置:

  • Main Module:
    • Start Lifetime: 0.3
    • Start Size(Random Between Two Constants): (0.3, 0.8)
    • Start Rotation(Random Between Two Constants): (-30, 30)
    • Start Color(Random Between Two Colors) :((255, 255, 255, 255), (147, 142, 138, 255))
    • Gravity Modefier: 0.1
    • Simulation Space: World
    • Scaling Mode: Shape
  • Emission:
    • Rate Over Time: 30
  • Limit Velocity Over Lifetime:
    • Dampen: 0.7
  • Size over Lifetime:
    • Size(Random Between Two Constants): (1, 2)
  • Rotation over Lifetime:
    • Angular Velocity(Random Between Two Constants): (-10, 10)
  • Texture Sheet Animation
    • Animation: Single Row
    • Randow Row: false
    • Frame over Time(Random Between Two Constants): (1, 3.9996)
  • Render
    • Material: Smoke
    • Max Particle Size: 5
    • Sorting Layer: Weapons

&emsp;&emsp;修改完成后,将导弹的速度调回25,运行游戏,可以看到导弹飞行的过程中出现了烟雾拖尾的效果。此时,我们将Missile的修改Apply至它的Prefab上,保存修改,然后在Hierarchy窗口中删除Missile物体。需要注意的是,为了方便阐述,上面大部分属性都没有使用Curve,为了做出效果更好的粒子系统,大家可以根据自己的喜好来调整各个Particle System modules的属性。

Particle System modules


发射导弹

&emsp;&emsp;接下来,我们可以实现发射导弹的功能。在Assets\Scripts\Player下创建一个名为PlayerAttack的C#脚本。然后编辑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
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;

// private Animator m_Animator;
private PlayerController m_PlayerCtrl;

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

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

private void Fire() {
// // 播放射击动画
// m_Animator.SetTrigger("Shoot");
// 播放射击音效
AudioSource.PlayClipAtPoint(ShootEffect, ShootingPoint.position);

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

// 如果角色跟导弹的朝向不一致,就翻转导弹
if(m_PlayerCtrl.FacingRight ^ instance.FacingRight) {
instance.Flip();
}
} else {
Debug.LogError("请设置ShootingPoint");
return;
}
}
}

此外,我们还需要编辑Missile.cs,加入Flip函数,并且添加根据朝向来设置速度的代码。

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
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
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 int DamageAmount = 10;

private Rigidbody2D m_Rigidbody2D;
private CapsuleCollider2D m_Trigger;

private void Awake() {
// 获取引用
m_Rigidbody2D = GetComponent<Rigidbody2D>();
m_Trigger = GetComponent<CapsuleCollider2D>();
}

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

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

// 确保勾选了Trigger
m_Trigger.isTrigger = true;
}

public void Flip() {
// 更新朝向
FacingRight = !FacingRight;

// 修改scale的x分量实现转向
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;
}

OnExplode();
}
}

&emsp;&emsp;编辑完成之后,我们在Player下新建一个名为ShootingPoint的Empty GameObject,然后将其Position设置为(1.2, 0.25, 0)。接着,我们将Missile的Prefab拖拽至MissilePrefab的赋值框处,将Player的子物体ShootingPoint拖拽至ShootingPoint的赋值框处,将Assets\Audio\FX下的bazooka拖拽到ShootEffect的赋值框处,最后点击Apply将修改应用至Player的Prefab,保存场景产生的修改。运行游戏,我们已经可以通过点击鼠标左键来发射导弹了。

设置PlayerAttack的属性


后言

&emsp;&emsp;至此,我们已经完成了角色发射导弹的功能。由于篇幅限制,我们将会在下一篇文章里实现利用导弹对怪物造成伤害的功能。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay9分支下看到,读者可以clone这个仓库到本地进行查看。


参考链接

  1. Importing and preparing Sprites
  2. Unity的Rigidbody2D
  3. Using Animation Events
  4. Particle System modules

《土豆荣耀》重构笔记(十一)实现发射导弹的功能
https://asancai.github.io/posts/29b00938/
作者
RainbowCyan
发布于
2019年1月8日
许可协议