Unity3D学习笔记(8)——Roll A BALL样例学习

这一章将学习一个简单的样例,根据实际的程序来学习U3D。

本文仅供个人记录和复习,不用于其他用途

如何查阅文档

在正式开始之前,先介绍一下如何查阅官方文档。首先就是按照 Help——Scripting Reference 的顺序打开文档:

如果你需要查找某一个类或者是方法,可以直接在搜索框中搜寻。

另外,每一个组件的右上方都有一个书本模样的图标,点击之后可以直接跳转到对应的文档中:

这里要说明一点,API 文档一般是包含了各种类的说明,而组件打开 Manual 文档则更多是介绍 U3D 中的各项功能:

我们无需通读整篇文档,可以根据需要一个个地学习,循序渐进。

初始化游戏环境

首先创建一个工程,保存当前场景。这里我是在 Assets 目录下创建了一个 Scenes 文件夹用于保存场景。由于这次只会用到一个场景,那么就将它取名为 main。

接下来创建一个地面,在 Hierarchy 窗口库中右键选择 3D Project —> Plane:

我们可以调整一下地面的坐标,把它重设为坐标系原点。

做完了地形,那么接下来就要导入游戏角色——球。创建一个球体,把它稍微往上移动一些,避免与地面重叠。由于地面默认的尺寸为 10*10,我们可以将它的 X 轴和 Z 轴放大两倍,这样地面就看起来大了不少。

在上图中,地面是深蓝色的。如果我们要为某一个物体添加颜色,那么就需要制作材质。创建一个新的文件夹,命名为 Materials,并在里面创建一个空材质。点击空材质,在属性栏中找到 Albedo 一项,选择你喜欢的颜色。

将材质与物体相连接,这样地面的颜色就改变了。连接的方法有很多种,比如直接拖动材质到场景视图中的地形上,或者是拖到 Hierarchy 窗口中的对象上,这两种方法是最简单的。

添加刚体组件

游戏的设定是用键盘操控一个小球,并用它来吃掉地面上的食物,从而获取分数。为了让小球具备物理属性,例如重力、碰撞体积、受力运动等等,我们就需要为其添加刚体组件。

添加的方法就是点击 Add Component,输入 Rigidbody 并添加。这时如果你再运行游戏,你就会发现小球从半空中掉落到了地面上,这说明小球已经受到了重力,同时还具备了碰撞体积。

既然小球已经落到了地面上,那么是时候让它滚动起来了。我们需要为小球创建一个脚本,并编写移动的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Player : MonoBehaviour {
private Rigidbody rd;
public int force = 5;
// Use this for initialization
void Start() {
// 获取小球的刚体
rd = GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update() {
// Input.GetAxis()用于控制器脚本,这里可以简单地理解为读取水平和垂直两个方向的操作:WASD或者上下左右
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// AddForce()为刚体施加一个力,Vector3表示三维向量,标识力的方向;force用于放大力的效果,整数
rd.AddForce(new Vector3(h, 0, v) * force);
}
}

代码不需要现在就理解,看看注释就好了。这里我简单的讲一下,该脚本是通过读取键盘的输入来控制小球的移动,像是WASD和方向键都是支持的。另外关于 force 变量,之所以要把它声明为 Public,是因为我想要在 Unity 的编辑器中调整这个力的倍数:

如上图所示,被声明为公共的成员属性都会标识在图像编辑器中,我们可以轻松地修改它的值。

运行游戏,小球已经能够自由地进行移动了。但问题又来了,小球在移动的时候,视角是不会动的,这样多少显得有些不好看。为了让视角运动起来,我们得设置摄像机的位置。

摄像机追随视角

我在上一章中的讲过,把一个物体设置为子对象,其位置会受到父对象的影响。那么,如果我们把主摄像机添加为小球的子对象,会发生什么呢?

如果你真的这么做了,并且运行了游戏,想必你的脑袋现在应该是晕乎乎的。没错,在小球滚动时,视角也会跟着它一起旋转。游戏在运行时,你可以观察一下场景视图,代表小球的坐标系是在不停地旋转的,所以这种方法是不可行的。

为了解决这个问题,我们需要创建一个脚本,用于控制摄像机与小球的距离,以达到跟随视角的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FollowTarget : MonoBehaviour {
public Transform playerTransform;
private Vector3 offset;
// Use this for initialization
void Start() {
offset = transform.position - playerTransform.position;
}
// Update is called once per frame
void Update() {
transform.position = playerTransform.position + offset;
}
}

代码很好理解,offset 代表了小球与摄像机的偏移量,根据小球的位置与偏移量便能算出摄像机应在的位置。你可能注意到了,我并没有对 playerTransform 进行初始化,所以我们要在编辑器中完成这一步:

将 Player 拖拽至 playerTransform 一栏,小球的位置便被赋给了 playerTransform,且会随着小球的位置进行改变(有点像指针)。做完这一步,摄像机的视角追随便完成了。

添加围墙

为了防止小球掉下去,我们得创建四面围墙,把小球挡在里面。具体步骤我就不说了,这里贴出围墙的部分参数:

添加食物

创建一个立方体,将其缩小为原来的两倍,并且设置偏转角度为(45, 45, 45):

为了方便我们移动立方体,我们可以在工具栏将 Local 设置为 Global,这样移动时物体的坐标系将会改为世界坐标系,我们在拖动时就很方便了:

创建预制体

由于我们需要很多个食物,且还要为它们添加脚本,一个个地创建显然是不明智的。Unity 提供了预制体,可以让我们快速地创建多个相同的对象:

创建步骤很简单,将你想要的物体从 Hierarchy 窗口中拖到 Project 窗口即可。此时,PickUp 就成为了一个预制体,要使用的话可以直接把它拖到场景视图中:

这样做有什么好处呢?好处当然有,比如你随意选择一个方块,修改它的缩放比:

你会发现你的场景变成了下面这样:

预制体的作用不仅仅是复制物体,如果某一物体的参数发生了改变,那么其他复制体也将受到影响,这一点对于数量多的物体非常适用。

最终结果如下:

另外,为了管理方便,你可以创建一个空的对象,将所有的复制体放到它的目录下,方便进行整体管理。

碰撞检测

为了让游戏展示出小球吃掉食物的效果,我们就得在小球与食物碰撞时销毁掉食物。那么问题来了,如何在 Untiy3D 中执行碰撞检测呢?我们要用到下面这个组件:

所有的实体都会附带一个 Collider 组件,这个组件意味碰撞器,用于管理物体的碰撞事件。让我们在 Player 脚本里编写一个新的方法:

1
2
3
4
5
void OnCollisionEnter(Collision collision) {
// 获取碰撞到的物体身上的Collider组件
string name = collision.collider.name;
print(name);
}

OnCollisionEnter()方法属于刚体类,当物体与其他实体发生碰撞时便会调用这个方法。传入的参数 collision 代表了与当前物体发生碰撞的实体。

打开控制台,运行游戏,控制台打印出了一条信息:

显然,当小球落下时,地面是最先与小球发生碰撞的,所以控制台打印出了这条信息。接着,我们控制小球与食物进行碰撞:

看到了吗,只要是与小球产生了碰撞的物体,控制台都会打印出它的名字。懂得了这个方法,我们就可以进行下一个步骤了:

如上图,物体的属性栏中会有一个 Tag 属性,我们点击方框,选择 Add Tag:

然后再添加新的标签名PickUp

最后,我们修改预制体的标签,这样所有的食物都有了一个共同的标签。

编写如下这段代码:

1
2
3
4
5
6
void OnCollisionEnter(Collision collision) {
if (collision.collider.tag == "PickUp") {
// 销毁小球碰到的食物
Destroy(collision.collider.gameObject);
}
}

由于食物有了共同标签,那么我们就可以根据碰撞物体的名字来执行操作。对于被小球碰到食物,我们将其销毁:

触发检测

虽然我们已经做完了碰撞检测,但在游戏的过程中,每当小球碰到食物时,总是会被阻挡一下。按照我们预想的效果,食物是不应该有碰撞体积的,所以我们得换另外一种检测方法——触发检测。

说到触发检测,我就得先解释一下触发器。举个例子,当游戏人物走到大门前时,我们要让大门自动打开,那么大门前的这块区域便是触发器,而打开大门这一事件则是触发事件。触发器一般是没有实体的,但当游戏人物与之接触时,仍然可以触发预设的事件,而这个概念正是我们现在所需要的。

设置触发器很简单,找到 PickUp,将属性栏中的 Is Trigger 打上勾:

这样一来,碰撞器就转变成了触发器,而此时的食物也没有了碰撞体积:

修改代码,触发检测需要新添加一个方法:

1
2
3
4
5
void OnTriggerEnter(Collider collider) {
if (collider.tag == "PickUp") {
Destroy(collider.gameObject);
}
}

与碰撞检测的代码类似,获取触发的物体标签,根据标签进行销毁。

分数显示与游戏胜利

要在游戏中显示得分的话,就得用到 uGUI 了。这里只是一个简单的应用,之后我会更新 uGUI 的使用教程。

在 Hierarchy 窗口中右键选择 UI -> Text,Unity 会自动创建一个 Canvas。将场景视图切换到 2D,双击 Canvas 下的 Text:

在属性栏中点击方框,按住 Alt 并选择左上角显示:

稍微拖动一下 Text,让它与边框隔开一些。这样,Text 便被调整到了画面的左上角:

为了显示分数,我们得在脚本中获取 Text 对象。新建成员变量 Score 和 text:

1
2
3
public Text text;
private int score = 0;

记得引入 UI 的头文件:

1
using UnityEngine.UI;

当小球每吃掉一个食物时,就要增加分数并修改文本:

1
2
score++;
text.text = score.ToString();

另外,在游戏结束时也应该出现胜利的字样。让我们在 Canvas 中新建一个文本 WinText,并在属性框中修改其内容、文字大小、居中、颜色:

一般来说,胜利画面是最后才显示的,最开始时应该将其隐藏起来。Unity 中的每个物体名字旁都有个小框,取消掉勾之后物体就不会显示:

我们要在脚本中加上判断,当分数达到 9 时视为胜利。为了控制 WinText 的显示,我们需要创建一个空的游戏对象,并用 WinText 为其赋值:

1
public GameObject winText;

胜利条件的判断如下:

1
2
3
if (score == 9) {
winText.SetActive(true);
}

最终代码如下:

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;
using UnityEngine.UI;
public class Player : MonoBehaviour {
private Rigidbody rd;
public int force = 5;
public Text text;
private int score = 0;
public GameObject winText;
// Use this for initialization
void Start() {
// 获取小球的刚体
rd = GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update() {
// Input.GetAxis()用于控制器脚本,这里可以简单地理解为读取水平和垂直两个方向的操作:WASD或者上下左右
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// AddForce()为刚体施加一个力,这个力是三维向量;force用于放大力的效果,整数
rd.AddForce(new Vector3(h, 0, v) * force);
}
// 触发检测
void OnTriggerEnter(Collider collider) {
if (collider.tag == "PickUp") {
score++;
text.text = score.ToString();
if (score == 9) {
winText.SetActive(true);
}
Destroy(collider.gameObject);
}
}
}

别忘了,我们创建的 text 和 winText 都是没有初始化的,记得在编辑器中将两个 UI 对象拖过去赋值:

游戏最终效果如下:

至此,游戏的基本逻辑就完成了。