1.3 制作第一个游戏:3D滚球跑酷

本节利用前面的知识来实现第一个较为完整的小游戏,如图1-21所示。

55875-00-021-1

图1-21 3D滚球跑酷游戏完成效果

1.3.1 游戏设计

1. 功能点分析

游戏中的小球会以恒定速度向前移动,而玩家控制着小球左右移动来躲避跑道中的黄色障碍物。如果玩家能控制小球在跑道上移动一定距离则视为玩家通过关卡,触碰到障碍物或从跑道上掉落则视为失败。我们需要实现的功能点概括来说分为主角的运动、摄像机的移动和过关与失败的检测等。

2. 场景搭建

01 创建项目。打开Unity Hub或者单独的Unity,初始模板选择3D,如图1-22所示。建议使用Unity 2018.3以后的版本,这里使用的Unity版本为2019.2.13f1。

55875-00-021-2

图1-22 创建3D工程

02 创建场景内物体。在场景中新建一个Cube(立方体)作为跑道,将其长度改为1000,宽度改为8(即立方体z轴和x轴的scale),然后将其位置沿z轴前移480,如图1-23所示。

55875-00-021-3

图1-23 创建跑道

03 新建一个Sphere(球体)作为玩家,重置它的初始位置(选择Transform组件右上角菜单中的Reset选项),按住Ctrl键拖曳球体的y轴,使其刚好移动到跑道上。

04 新建若干个Cube(立方体)作为障碍物,用上面的方法铺在跑道上面。为了便于区分,新建两个材质分别作为跑道和障碍物的材质,调整好颜色后直接拖曳到物体上即可,如图1-24所示。

55875-00-022-1

图1-24 调整跑道和物体的颜色

小知识

按住Ctrl键拖曳物体的作用

按住Ctrl键拖曳物体会让其以一个固定值移动(可以在Edit→Snap Settings中修改这个固定值),调整物体的旋转和缩放时也是同理。这个功能在搭建场景时可以很方便地对齐物体的位置,特别是当物体为同一规格大小时。Unity中还有很多类似的很方便的快捷键,在用到时会介绍。

1.3.2 功能实现

1. 主角的移动

之前的实例中已经提到过如何控制小球的移动,因此此处不再赘述。与之前不同的是,在该案例的设计中,玩家只能控制小球左右移动,因此只需要获取横向的输入即可,纵向移动保持一个固定值。编辑好脚本后挂载在小球上,代码如下。

public class Player : MonoBehaviour
{
    public float speed;
    public float turnSpeed;

    void Update()
    {
         float x = Input.GetAxis("Horizontal");
         transform.Translate(x*turnSpeed*Time.deltaTime, 0, speed * Time.deltaTime);
    }
}

2. 摄像机的移动

摄像机移动的方法可以分为两种。一种是像控制小球一样为摄像机挂载控制脚本,使其与小球保持同步运动。另一种则更为简单直接,即将摄像机设置为小球的子物体,此时摄像机在没有其他代码控制的情况下会与小球保持相对静止,即随着小球移动。这里选择第二种方法,当设置好父子关系后调整摄像机到合适的高度和角度,如图1-25所示。

55875-00-023-1

图1-25 将摄像机作为球体的子物体

小提示

Unity中的父子关系

父子关系是Unity中相当重要的一个概念,此处可以不用深究,在第2章会详细说明。

1.3.3 游戏机制

1. 游戏失败

有两种情况会导致游戏失败,一种是碰到了障碍物,另一种是小球从跑道边缘掉落。

碰撞到障碍物与吃金币实例中的原理类似,将障碍物碰撞体上的Is Trigger选项勾选上,然后在障碍物脚本里的OnTriggerEnter()函数中检测碰撞。

不过游戏中的障碍物可能会有许多个,如果要一个个地分别做上述修改显然很麻烦,还容易遗漏。因此正确的做法是把其中一个障碍物设为Prefab(预制体),后面添加的障碍物都以这个Prefab为模板复制即可。

最后注意,要检测物理碰撞还需要在小球上添加Rigidbody组件。为了避免不必要的物理计算消耗,在这个游戏中完全用代码控制小球的移动,物理系统仅做检测碰撞使用,因此小球Rigidbody组件上的Is Kinematic选项要勾选上。障碍物脚本里的代码如下。

public class Barrier : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        // 防止其他物体与障碍物的碰撞被检测,我们只需要障碍物与小球的碰撞被检测到
        if(other.name=="Player")
        {
            Time.timeScale = 0;
        }
    }
}

小知识

什么是预制体?Time.timeScale是什么?

预制体简单来说就是一个事先定义好的游戏物体,之后可以在游戏中反复使用。最简单的创建预制体的方法是直接将场景内的物体拖曳到Project窗口中,这时在Hierarchy(层级)窗口中所有与预制体关联的物体名称都会以蓝色显示(普通物体的名称是黑色)。关于预制体的内容会在第2章详细说明。

Time.timeScale表示游戏的运行时间倍率,设置为0即表示游戏里的时间停滞,1即正常的时间流逝速度,2即两倍于正常的时间流逝速度,以此类推。

小球从跑道边缘掉落时也视为游戏结束。但是由于是直接用代码控制小球移动,与刚体有一点冲突,因此掉落部分的功能同样用代码来实现。在Player脚本里添加如下代码。

void Update()
    {
       ……
        // 一旦小球位置超出了跑道的范围则直接下落
        if(transform.position.x<-4||transform.position.x>4)
        {
            transform.Translate(0, -10 * Time.deltaTime, 0);
        }

        // 下落一定距离之后游戏结束
        if(transform.position.y<-20)
        {
            Time.timeScale = 0;
        }
    }

当游戏失败结束时应该允许玩家重新开始游戏,这里设置键盘上的R键为重置游戏的按键,在按R键后即可重新加载当前场景。在Player脚本里添加如下代码。

……
using UnityEngine.SceneManagement;
public class Player : MonoBehaviour
{
……
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.R))
        {
            SceneManager.LoadScene(0);
            Time.timeScale = 1;
            return;
        }
……
    }
}

小提示

注意添加头部引用

SceneManager这个类型是属于UnityEngine.SceneManagement的,因此要添加头部引用后才能调用SceneManager.LoadScene(0)方法。这里参数0表示场景的序号,由于游戏现在只有一个场景,因此表示加载当前场景。

2. 游戏胜利

一般来说游戏都应该有一个最终目标,达成这个目标则视为过关或者胜利。不过也不绝对,类似Flappy Bird这样的游戏就没有最终目标。这里还是设置一个完成目标,即玩家跑了一定距离就视为过关。

这里使用一个看不见的触发器作为决定距离的终点,确保其范围能够覆盖跑道的宽度,当小球进入范围就表示游戏过关,如图1-26所示。终点物体脚本的代码如下。

55875-00-025-1

图1-26 在终点创建触发器

public class End : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        if (other.name == "Player")
        {
            Debug.Log(" 过关 ");
            Time.timeScale = 0;
        }
    }
}

至此,这个小游戏的基本代码就完成了,之后会对其进行适当的修改,使其更完整。

1.3.4 完成和完善游戏

1. 测试自己的游戏

这时候可以开始测试自己设置的关卡难度了,一个好的游戏应当有一个合理的难度曲线。有一个小技巧可以提高这一步的效率,即单击场景视窗右上角的坐标轴图标,让场景摄像机迅速切换为对应轴方向的视角,而单击下面的Persp或Iso则分别代表切换摄像机为透视模式或正交模式,如图1-27所示。

55875-00-025-2

图1-27 场景可切换为正交模式显示

小提示

场景摄像机不会影响实际游戏画面

场景摄像机指的是在Scene(场景视窗)里的、仅在编辑模式可用的摄像机。Hierarchy窗口中的摄像机决定在Game窗口里看到的实际游戏画面。注意不要将两者混淆。

这里可以切换场景摄像机为y轴方向正交,善用复制与按住Ctrl键拖曳功能搭建关卡。

2. 加入通关UI

在Hierarchy窗口中单击鼠标右键,通过选择UI→Panel选项创建一个UI面板,并以Panel为父节点创建一个Text组件,在Text组件中输入过关的信息,同时调整字体大小、位置等设置,如图1-28和图1-29所示。

55875-00-026-1

图1-28 创建Text组件

55875-00-026-2

图1-29 通关界面

小知识

只是创建了Panel,为什么自动添加了其他东西?

当直接创建任何UI下的组件时都会自动生成Canvas与EventSystem组件,这两个组件分别与UI的布局和交互相关,暂时不做深究。完整的UI系统会在后续章节介绍。

接下来要做的是在游戏开始时隐藏UI,在小球触发终点物体时再显示。终点物体的End脚本代码如下,同时注意修改Panel的名字为EndUI。

public class End : MonoBehaviour
{
    // 声明一个物体变量
    GameObject endUI;
    private void Start()
    {
        // 通过物体在场景中的名字来找到这个物体
        endUI = GameObject.Find("EndUI");
        // 在场景中隐藏这个物体
        endUI.SetActive(false);
    }
   private void OnTriggerEnter(Collider other)
   {
        if (other.name == "Player")
        {
            Debug.Log(" 过关 ");
            //在场景中显示这个物体
            endUI.SetActive(true);
            Time.timeScale = 0;
        }
    }
}

如此一来,当小球触碰到终点后,UI就会显示出来,如图1-30所示。

55875-00-027-1

图1-30 到达终点时的显示效果

3. 加入摄像机运动效果

最后添加一个好玩的扩展功能:当控制小球左右移动时让摄像机往对应方向倾斜。具体的做法会涉及一些3D数学知识,会在后续章节中介绍。简单思路为:在Player脚本中使用获取的横向输入,以此控制摄像机的倾斜角度。在Player中添加如下代码,效果如图1-31所示。

55875-00-027-2

图1-31 摄像机随输入旋转的效果

void Update()
{
    ……
    Transform c = Camera.main.transform;
    Quaternion cur = c.rotation;
    Quaternion target = cur * Quaternion.Euler(0, 0, x * 1.5f);
    Camera.main.transform.rotation = Quaternion.Slerp(cur, target, 0.5f);
}

可以在此基础上加入更多细节,如音乐、音效和特效等。合适的音乐和特效可以让简单的游戏更吸引人。