【Unity3D开发小游戏】太空射击游戏

博客分类: Unity3D-Game 阅读次数: comments

【Unity3D开发小游戏】太空射击游戏

@TOC

一、前言

我们的太空射击游戏受到古老的街机游戏的启发 我们将利用物理等各种不同的Unity特征(包括刚体和碰撞器)、动画(Mecanim),脚本(C#),预制体,阴影和 Sprite Editor。

效果图:

在这里插入图片描述

二、源码

UI资源和源代码请搜索QQ群:1040082875下载

三、正文

版本

Unity 5.0.1f1

1.相机调整

首先,我们将选择主照相机,将Background颜色改为黑色,并将Size调整为10:

在这里插入图片描述

2.创建背景

空间纹理 我们的背景应该包括一个恒星的图像,滚动向下滚动,所以它似乎是在太空飞行的球员。我们首先画一个256 x 512PX空间背景在我们的绘图工具中的选择: 在这里插入图片描述 注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置: 在这里插入图片描述 注意:通常我们会将2D游戏的纹理类型设置为Sprite。但是使用UV贴图滚动纹理只适用于Texture,不适用于Sprite。Sprite是完美的像素,但滚动它们需要一个着色器,这对本教程来说太复杂了。

3.添加Quad

好吧,让我们把背景添加到我们的游戏中。我们会选择GameObject->3D Object->Quad给我们的游戏添加一个四边形: 在这里插入图片描述 注意:我们使用Quad在2D游戏中,因为Texture (而不是Sprite)可以添加到它。

现在我们来看一下Inspector,比例尺它的高宽比和我们的背景纹理是一样的: 在这里插入图片描述

我们也会将它重命名为Background: Quad Renamed

并移除Mesh Collider因为我们不需要它(这是3D游戏): Remove Mesh Collider 之后,我们可以从项目区看到Background: Quad with Default Material

如果我们按下Play然后我们可以看到我们的空间背景,它仍然非常黑暗: Dark Background

4.Unlit Shader

背景是黑暗的,因为它当前使用Standard只有当场景中有灯光时,着色器才会使事情变得明亮。 我们不会使用任何类型的灯光或阴影,所以让我们选择Unlit>Texture着色器: Background with Unlit Texture Shader

如果我们按下Play再一次,我们可以在不太暗的情况下看到背景恒星: Unlit Background ingame

5.UV映射

如果我们仔细观察一下阴影的特性,我们就可以看到我们的紫外线了。Tiling和Offset在此: Background UV Properties 随意使用这些属性,看看它们如何改变背景。 Tiling会通过改变偏移量会改变纹理的位置。 如果我们设置Y偏移量对于像这样的值0.1, 0.2, 0.3等等,我们已经可以看到滚动发生了。

当然,为了达到滚动效果,我们不希望每秒手动更改偏移量60次。 我们会写一个Script处理好了。 让我们点击添加组件按钮,然后选择新脚本,命名为UVScroll.cs: 在这里插入图片描述 让我们创建一个新的Scripts文件夹并将脚本移动到其中: UVScroll Script in Project Area

现在,我们可以双击脚本以便打开它:

using UnityEngine;
using System.Collections;

public class UVScroll : MonoBehaviour {

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {

    }
}

我们不需要Start或者Update函数,让我们移除它们。 修改UV映射在LateUpdate函数。 我们还将添加一个公共变量,让我们可以在检查器中修改滚动速度:

using UnityEngine;
using System.Collections;

public class UVScroll : MonoBehaviour {
    public Vector2 speed;

    void LateUpdate() {
        GetComponent<Renderer>().material.mainTextureOffset = speed * Time.time;
    }
}

*注意:虽然看起来很简单,但是这里有很多事情发生在幕后。首先,我们使用了Vector2为了我们speed变量,以确保我们可以修改x(横向)和y(垂直)速度。我们使用GetComponent()访问renderer 组件。使用GetComponent().Matter确保UV偏移量不会被永久修改,就像手工修改它时的情况一样。相反,通过使用GetComponent().Matter统一创建材料的运行时脚本,并在游戏停止后删除它。此外,我们使用mainTextureOffset属性修改主纹理的UV偏移量。然后我们用Time.time实现滚动效果,请执行以下操作:Time.time是游戏开始后的时间。既然这段时间总是平稳地增长,我们不妨把它用于滚动。而且我们把时间乘以速度让它慢下来或者让它变快。因为我们速度变量是Vector2,在将它与之相乘后,它仍将是一个Time.Time..所以最后我们得到了一个新的Vector2将用于偏移两变化.*

让我们保存脚本,现在我们可以修改速度了。 我们将设置y速度(用于垂直滚动)以我们所希望的速度离开 x速度(水平滚动)在0 因此,它根本不水平滚动: UV Scroll in Inspector with Speed

6.滚动空间纹理

如果我们按下Play然后我们可以看到我们的背景向下滚动,就像我们在太空中飞行一样: 在这里插入图片描述

这个效果很好,但是我们会更进一步改善视差滚动,给它增加更多的深度。

7.视差滚动

我们将实现最简单形式的视差滚动,简单地覆盖两个背景纹理,并以不同的速度滚动他们。顶部的滚动速度总是快于底部的滚动。就像我们在太空中飞行一样,离我们很近的恒星也会飞过来。 滚动条比那些真的很远的星星走得更快。

让我们画另一个纹理,它大部分是透明的,里面有几颗白色的星星: 在这里插入图片描述 注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置 在这里插入图片描述 新建一个Quad作为Background的子物体: 在这里插入图片描述 将新的Quad重命名为Stars。Scale设置为(1,1,1): 在这里插入图片描述 注意:我们可以将scale设置为(1, 1, 1)因为父GameObject已经有一个坐标为为(8, 16, 1)。因为这些恒星是它的子对象,它们会自动地被这个比例放大。因此,我们的恒星已经有了与恒星纹理相同的长径比。

我们还将删除Mesh Collider: 在这里插入图片描述 我们接着改变stars的纹理,我们将选择Unlit->Transparent因为我们大部分的纹理都是透明的: 在这里插入图片描述 我们希望星星比背景更快地向下滚动,所以让我们选择 Add Component->Scripts->UVScroll然后设置Y速度到0.06: 在这里插入图片描述 如果我们按下Play然后我们可以看到一些美丽的视差滚动: 在这里插入图片描述 注意:视差滚动是2D游戏开发中最令人佩服的视觉效果之一。它所需要的只是两个纹理,以不同的速度滚动。

8.边界

边界碰撞器 我们将增加4个边框,以确保船只不能飞出它。 我们将添加四个碰撞器。然后我们将修改每个碰撞器, 使其中一个在左边,一个在右边,一个在顶部,另一个在底部: 在这里插入图片描述 下面是它在场景: 在这里插入图片描述 边界脚本 我们要确保子弹和敌舰在他们到达任何边界时立即摧毁他们。

让我们添加一个新脚本,名字叫做Border.cs,双击打开,删除Stat和Update函数,添加OnCollisionEnter2D函数,以便在与边界发生冲突时得到通知:

using UnityEngine;
using System.Collections;

public class Border : MonoBehaviour {

    void OnCollisionEnter2D(Collision2D coll) {
        // Do Stuff..
    }
}

现在,每当有东西与边界相撞时,我们都想摧毁它,除非它是玩家的飞船:

void OnCollisionEnter2D(Collision2D coll) {
    if (coll.gameObject.name != "PlayerShip")
        Destroy(coll.gameObject);
}

我们要做的就是保持现场整洁。现在,每当一艘船或一颗子弹到达界面外,它们就会自动被摧毁。

9.玩家飞船

飞船图片 让我们为玩家创造一艘漂亮的太空船。 我们将使用像Paint.NET这样的工具画一个32 x 32px飞船。 我们还将创建两个动画,一个空闲和一个飞行动画。(当用户按上箭头键时,会播放飞行动画).

我们需要一个96x64px图像保存我们所有的动画。第一行将保存Fly动画,第2行将保存空闲动画: 在这里插入图片描述 注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置 在这里插入图片描述 设置Sprite Mode为Multiple,然后点击Sprite Editor: 在这里插入图片描述 我们会切片图像为32x32px格网: 在这里插入图片描述 点击Apple,然后关闭Sprite Editor 在这里插入图片描述

10.制作飞船动画

好的,现在我们有3片fly动画和2用于idle动画。选择前3片,然后将其从项目区拖入到场景: 在这里插入图片描述 保存到ShipAnimation文件夹,命名为fly.anim.

对于最后两个切片,我们将重复相同的过程,并将其保存为idle.anim.

如果我们按下Play然后我们就可以看到我们刚才拖到场景中的两个动画: 在这里插入图片描述 清理 在场景中删除ship_3,然后在项目中删除ship_3动画状态机: 在这里插入图片描述 在这里插入图片描述

11.飞船动画状态机

让我们双击ship_0在我们的ShipAnimation文件夹,这样我们就可以看到Animator窗口: 在这里插入图片描述 我们添加一个bool类型参数Flying,来判断飞船是否在飞行:

在这里插入图片描述 添加idle状态 这个fly状态已经在动画器中,所以让我们添加idle状态,只需拖动idle.anim文件从ShipAnimation文件夹进入Animator: 在这里插入图片描述 然后将两个状态链接起来,我们将选择fly状态,然后右键单击它,选择Make Transition然后拖动Make Transition选择idle状态: 在这里插入图片描述 添加白色箭头,禁用Has Exit Time,然后添加切换条件Flying = false: 在这里插入图片描述 注意:我们希望从fly到idle的时候,Flying是false

之后,我们创建了另一个Make Transition从…idle到fly: 在这里插入图片描述 点击白色箭头,设置条件为Flying=true: 在这里插入图片描述

12.飞船移动

玩家应该能够通过按箭头键来控制飞船。但是不应该能穿过流星,敌舰或其他我们可能在太空遇到的东西。

为了使它成为物理世界的一部分,我们需要在我们的飞船上增加一个碰撞器和刚体。 在这里插入图片描述 在这里插入图片描述 注意:我们将Gravity Scale设为0,因为在游戏中不存在重力。我们还启用了Fixed Angle以确保物理引擎在发生碰撞时不会试图旋转飞船。

好了,既然飞船的无弹力都准备好了,接着就要去实现移动了,我们应该使用刚体组件的移动,我们可以使用AddForce和velocity

velocity 是运动方向 * 速度。以下图片显示了不同的运动方向: 在这里插入图片描述 我们新建一个脚本Move.cs,双击打开,然后删除Stat和Update函数,添加一个speed变量:

using UnityEngine;
using System.Collections;

public class Move : MonoBehaviour {
    // Ship Speed
    public float speed = 5;
}

添加FixedUpdate函数:

using UnityEngine;
using System.Collections;

public class Move : MonoBehaviour {
    // Ship Speed
    public float speed = 5;

    void FixedUpdate() {

    }
}

接着就检查用户的输入,比如WSAD,或者左右上下箭头。 我们可以使用GetAxis函数,来自动检查水平和垂直轴的输入,然后返回-1 (左)和1 (右)。或0如果什么都没有按的话:

void FixedUpdate() {
    // Get Input from Arrow Keys, WSAD, Gamepads, ...
    float h = Input.GetAxisRaw("Horizontal");
    float v = Input.GetAxisRaw("Vertical");
}

现在我们可以用我们的h和v值以创建dir用speed然后让它成为刚体的velocity:

void FixedUpdate() {
    // Get Input from Arrow Keys, WSAD, Gamepads, ...
    float h = Input.GetAxisRaw("Horizontal");
    float v = Input.GetAxisRaw("Vertical");

    // Set the Rigidbody's Velocity
    Vector2 dir = new Vector2(h, v);
    GetComponent<Rigidbody2D>().velocity = dir.normalized * speed;
}

注意:我们使用了normalized方向,以确保它的长度总是准确的。这是必要的,以防止船太快地移动到对角线方向(例如,当向上和右箭头键同时按下)。这个问题是基于这样一个数学事实:(1, 0)有长度1,但是向量(1, 1)实际上有长度1.41。我们还把方向乘以速度让它变得更长。这就是让飞船移动得更快的原因。

运动本身已经完成,但我们仍然必须设置动画状态机的Flying参数,以使动画能够正确地查看。 这很容易,我们所要做的就是访问Animator组件,然后使用其SetBool函数将我们的参数设置为true或者false 取决于垂直方向(Y)方向:

void FixedUpdate() {
    // Get Input from Arrow Keys, WSAD, Gamepads, ...
    float h = Input.GetAxisRaw("Horizontal");
    float v = Input.GetAxisRaw("Vertical");

    // Set the Rigidbody's Velocity
    Vector2 dir = new Vector2(h, v);
    GetComponent<Rigidbody2D>().velocity = dir.normalized * speed;

    // Set Animation Parameter
    GetComponent<Animator>().SetBool("Flying", (v > 0));
}

注:表达式(v>0)结果为bool值。如果v大于0它是true不然的话flase。

如果我们按下Play然后我们可以用箭头键移动飞船: 在这里插入图片描述

13.发射子弹

玩家应该能够用空格键发射子弹。让我们创建一个新的C#脚本来实施射击。 新建脚本FirePlayer.cs,双击打开,然后Stat函数,添加一个public的GameObject对象:

using UnityEngine;
using System.Collections;

public class FirePlayer : MonoBehaviour {
    // Bullet Prefab
    public GameObject bullet;

    // Update is called once per frame
    void Update () {

    }
}

按空格生成子弹:

using UnityEngine;
using System.Collections;

public class FirePlayer : MonoBehaviour {
    // Bullet Prefab
    public GameObject bullet;

    // Update is called once per frame
    void Update () {
        if (Input.GetKeyDown(KeyCode.Space))
            Instantiate(bullet,
                        transform.position,
                        Quaternion.identity);
    }
}

注意:transform.position是当前的位置和Quaternion.identity是默认的旋转。

我们希望确保玩家自己的子弹不会与玩家的飞船相撞:

// Update is called once per frame
void Update () {
    if (Input.GetKeyDown(KeyCode.Space)) {
        // Spawn the Bullet
        GameObject g = (GameObject) Instantiate(bullet,
                                                transform.position,
                                                Quaternion.identity);
        // Ignore Bullet<->Player collisions
        Physics2D.IgnoreCollision(g.GetComponent<Collider2D>(),
                                  transform.parent.GetComponent<Collider2D>());
    }
}

注意:使用transform.parent(在层次结构中引用GameObject的父级)以后会用到

现在,如果我们将脚本添加到游戏中,然后按空格键,那么子弹就会被实例化在飞船的内部,这看起来很奇怪。为了防止这种情况发生,我们将创建两个BulletSpawn略高于船的位置。

让我们创建两个空对象,并且设置PlayerShip为父对象,命名为BulletSpawnLeft和BulletSpawnRight: 在这里插入图片描述 BulletSpawnLeft设置到略高于船左翼的位置: 在这里插入图片描述 BulletSpawnRight设置到略高于船右翼的位置: 在这里插入图片描述 我们不能真正看到两个生成点在场景中的位置,因为它们只是空的游戏对象。你可以给他们每个人一个Gizmo然后就可以看到他们在场景中的位置: 在这里插入图片描述 注意:Gizmo只是一个视觉助手,它只显示在场景中,而不是在最后的游戏中。

以下是与Gizmo一起出现的两个生成位置: 在这里插入图片描述 然后我们给飞船添加组件FirePlayer.cs: 在这里插入图片描述 这样就会对按空格键做出反应。我们只需要创建子弹,这样两个生成点就可以发射它。

飞船Tag 我们需要一种方法来确定某个游戏对象是否是一艘飞船。现在只有PlayerShip在我们的场景中,我们只需检查一下名字。但一旦有不同名称的多艘船,我们就需要一个更好的解决方案。 在这里插入图片描述 让我们选择加上标签..。从标记列表中添加一个Ship标签: 在这里插入图片描述 之后,我们可以选择PlayerShip然后再分配Ship贴在上面: 在这里插入图片描述 注意:我们可以在脚本里使用gameObject.tag

14.制作子弹

子弹纹理 让我们创造一个子弹纹理,这样飞船就可以拍摄了。我们将使用16 x 16PX图像: 在这里插入图片描述 注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置 在这里插入图片描述 拖入到场景中: 在这里插入图片描述

15.子弹物理

添加碰撞器和刚体 在这里插入图片描述

16.让子弹飞

接下来,我们会让子弹飞向某个方向。如果我们再想一想,玩家的子弹,敌人的子弹,以及敌人的飞船本身,在默认情况下都必须飞向某个特定的方向。因此,让我们为每个脚本创建一个脚本。

新建脚本ContinuousVelocity.cs,双击打开,删除Start函数和Update函数,添加FixedUpdate函数:

using UnityEngine;
using System.Collections;

public class ContinuousVelocity : MonoBehaviour {
    // The Velocity
    public Vector2 velocity;

    void FixedUpdate() {
        GetComponent<Rigidbody2D>().velocity = velocity;
    }
}

注:从理论上讲,只要将速度设为一次就足够了。然而,我们要为飞船与其他船只相撞的情况做准备。要保证船在碰撞后始终保持运动,唯一的办法就是一次又一次地设定速度。

我们还将指定一个速度,使它飞得相当快: 在这里插入图片描述

17.子弹造成伤害

新建脚本BulletDamage.cs,双击打开,删除Stat和Update函数,添加OnCollisionEnter2D函数:

using UnityEngine;
using System.Collections;

public class BulletDamage : MonoBehaviour {

    void OnCollisionEnter2D(Collision2D coll) {
        // Do Stuff...
    }
}

在这里,我们可以检查子弹撞上的是否是一艘船,在这种情况下,我们可以摧毁它:

void OnCollisionEnter2D(Collision2D coll) {
    // Collided with a Ship? Then destroy it.
    if (coll.gameObject.tag == "Ship")
        Destroy(coll.gameObject);
}

注意:我们将给我们所有的船贴上标签,所以我们可以在这里检查,不用担心。我们用Destroy函数从场景中删除游戏对象。

我们不希望子弹与某物相撞后留在现场,所以让我们销毁它:

using UnityEngine;
using System.Collections;

public class BulletDamage : MonoBehaviour {

    void OnCollisionEnter2D(Collision2D coll) {
        // Collided with a Ship? Then destroy it.
        if (coll.gameObject.tag == "Ship")
            Destroy(coll.gameObject);

        // Destroy Bullet in any case
        Destroy(gameObject);
    }
}

18.子弹预制体

我们不希望子弹一直在场景中,我们只想在玩家真正开火的时候拥有它。

创建预制体,将子弹从Hierarchy中拖到我们的项目区Prefab文件夹中的重命名为BulletPlayer: 在这里插入图片描述 现在子弹被保存在我们的项目区我们可以随时把它加载到游戏里。 让我们从Hierarchy删除子弹因为我们不需要它从一开始就在那里。

玩家发射子弹 创建了子弹后,我们现在可以使用BulletPlayer预制件让飞船开枪。

让我们选择PlayerShip对象的两个子对象BulletSpawn。然后拖动BulletPlayer从项目区进入FirePlayer脚本子弹插槽: 在这里插入图片描述 如果我们按下Play然后我们可以在按空格键上发射两颗子弹: 在这里插入图片描述

19.敌人的子弹

我们将为敌舰再制造一颗子弹。这一次,它将是一个黄色Sprite: 在这里插入图片描述 注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

现在,我们可以重复我们用于红色子弹的完全相同的工作流程。然后,我们将创建一个BulletEnemy预制体并设定速度向下飞行。

最后的结果如下: 在这里插入图片描述

20.敌人

创建纹理 让我们在游戏中加入一些敌人。我们首先画一个32 x 32 Px敌舰形象: 在这里插入图片描述 注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置 在这里插入图片描述 创建游戏对象

现在我们可以把敌人Sprites从项目区拖入到场景来创建一个游戏对象。

添加碰撞器和刚体 选中敌人然后添加组件: Circle Collider 2D 在这里插入图片描述 在这里插入图片描述 敌人运动 选中敌人,然后添加组件Continuous Velocity.cs,然后设定一个较小的下行速度: 在这里插入图片描述 敌人Tag 让我们把我们的敌舰分类,就像我们对待玩家的飞船一样。我们将Tag设置为Ship然后子弹就能摧毁它: 在这里插入图片描述 发射子弹 就像我们对玩家飞船所做的一样,我们也会在敌人的前面设一个子弹生成点,重命名为BulletSpawn:

在这里插入图片描述 我们将把点定位在船底。也可以指定一个Gizmo以便于识别: 在这里插入图片描述 新建一个脚本FireEnemy.cs,双击打开,删除Update函数,添加两个公有变量,一个用于子弹预制件,一个用于射击间隔:

using UnityEngine;
using System.Collections;

public class FireEnemy : MonoBehaviour {
    // The Bullet Prefab
    public GameObject bullet;

    // The firing Interval
    public float interval = 2;

    // Use this for initialization
    void Start () {

    }
}

现在我们可以用Unity的InvokeRepeting函数每隔几秒钟实例化一个新的子弹:

using UnityEngine;
using System.Collections;

public class FireEnemy : MonoBehaviour {
    // The Bullet Prefab
    public GameObject bullet;

    // The firing Interval
    public float interval = 2;

    // Use this for initialization
    void Start () {
        // Call Fire every few seconds
        InvokeRepeating("Fire", interval, interval);
    }

    void Fire() {
        // Spawn the Bullet
        GameObject g = (GameObject)Instantiate(bullet,
                                               transform.position,
                                               Quaternion.identity);

        // Ignore Bullet<->Enemy Ship collisions
        Physics2D.IgnoreCollision(g.GetComponent<Collider2D>(),
                                  transform.parent.GetComponent<Collider2D>());
    }
}

保存脚本并查看Inspector,然后拖动BulletEnemy预置体到脚本中子弹插槽: 在这里插入图片描述 如果我们按下Play现在我们可以看到敌舰每隔几秒钟就发射一颗子弹: 在这里插入图片描述 敌人不必从一开始就出现在现场,相反,我们会在稍后进行。所以让我们把它重命名为EnemyShip,然后拖入到Prefabs文件夹: 在这里插入图片描述 让我们在Hierarchy中删除这个对象

21.生成敌人

让我们创建5个空游戏对象,命名为Spawner,然后设置Gizmo,我们将它们放到场景的顶端:

下面是它在场景: 在这里插入图片描述 选中所有的Spawner,添加脚本Spawn.cs,双击打开:

using UnityEngine;
using System.Collections;

public class Spawn : MonoBehaviour {

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {

    }
}

我们已经知道如何实例化每隔几秒钟就生成一次:

using UnityEngine;
using System.Collections;

public class Spawn : MonoBehaviour {
    // The Ship
    public GameObject ship;

    // The Interval
    public float interval = 1;

    // Use this for initialization
    void Start () {
        InvokeRepeating("SpawnNext", interval, interval);
    }

    void SpawnNext () {
        Instantiate(ship, transform.position, Quaternion.identity);
    }
}

保存脚本后,我们可以将EnemyShip从项目区拖入到Ship插槽: 在这里插入图片描述 然后给每个Spawner设置一个不同的间隔Interval。

如果我们按下Play现在我们可以看到几艘敌舰在游戏的顶端生成: 在这里插入图片描述 现在我们就可以开始太空射击游戏了: 在这里插入图片描述

22.摘要

在本教程中,我们创建了一个天空射击游戏Unity版本的框架,我们学习各种技巧完成了这件事,现在该由读者来使游戏尽可能的有趣了。

可以实施各种改进,例如: