从创建项目开始

创建一个项目

打开unity,在Projects中可以查看当前的本地项目或者云端项目,点击New project或者右上角的New都可以新建项目。

创建一个项目
创建一个项目

然后在1处填写创建项目的名称,2处选择创建地址,3处选择Template(模板),可以选中3D或者2D。

创建一个项目
创建一个项目

稍等片刻即可进入unity的主页面。

保存Scene并管理Assets

进入unity之后,可以看到已经预设的SampleScene和其中的Main Camera(主摄像机)和Directional Light(平行光)组件,如果你不喜欢这些预设的名字,直接点击修改并使用Ctrl+S保存你的修改即可。

保存Scene
保存Scene

为了使我们的Assets文件夹管理地更加井井有条,推荐对不同的assets进行分文件夹管理,这时一些文件夹的命名可以自己定义,但通常可以遵循一定的不成文的规定,比如通常将场景存放在Scenes目录、将脚本存放在Scripts目录等等,要根据自己来管理好Assets目录,以后对于大量的assets可以很方便快捷。

创建游戏场景

创建Plane

可以使用unity的内置GameObject的Plane(平面)类型来作为游戏场景的“地面”。
可以在Hierarchy视窗下右键选择3D Object下找到Plane创建,也可以在顶部菜单栏的GameObject下的3D Object下找到Plane创建。当然也可以看到unity为我们创建了很多预置的Game Object,比如3DO bject、2D Object、Effects、Light、Audio、 Video、UI、Camera等等,当然每一个分类下还有更详细的分类,这些都可以直接拿来使用,非常方便,一些重要的Game Object以后还会慢慢使用。

新建GameObject的Plane
新建GameObject的Plane

这里只需要创建一个Plane并将其命名为Ground来作为我们游戏的“地面”,可以看到Plane出现在了MainScene中,同时我们可以注意到Hierarchy视窗中的MainScene右上角出现了一个*号,这就表示该Scene处于待保存的状态,可以通过菜单栏的File->Save或者快捷键Ctrl+S保存Scene。

待保存的Scene
待保存的Scene

选中刚刚创建的Ground,Inspector视窗中就会出现其所有的Components,这些都是预先被untiy设置给Plane的,点击Transform这一Component的右侧的齿轮状图标,可以选择Reset(重置)选项,这样,刚刚创建的Plane的Transform就会被重置为初始值,它的Position会被设置为(0,0,0),这是整个游戏世界的原点坐标,游戏中的所有GameObject的坐标都是基于此原点进行计算的。

初始化Transform
初始化Transform

选中任何一个GameObject,比如选中Ground,然后按F键,或者在菜单栏中点击Edit->Frame Select可以快速地调整Scene的角度,让我们有一个非常合适的角度来观察Ground的全貌。

改变Transform

改变Transform的三组值的方法有很多。

直接赋值

可以在Inspector面板中对Transform的九个值直接输入数值来设置

在Inspector面板中设置Transform的值
在Inspector面板中设置Transform的值

拖动输入框调节

当我们把鼠标指向每一个值的输入框的左侧边界时,就会发现鼠标成为了一个左右双箭头的形状,此时按下鼠标左键所有拖动,就会发现该输入框变成了蓝色,并可以随着拖动改变它的值。

拖动输入框
拖动输入框

在Scene窗口中改变

在左上方有六个按钮,分别表示对Scene中GameObject的操作。
这里提一句:不管选中六个按钮中的哪一个,只要按住Alt键在Scene中拖动鼠标就可以转动视角,只要滚动鼠标滚轮即可放大/缩小视角。
这六个按钮从左到右依次为:

六个改变Scene中GameObject的选项
六个改变Scene中GameObject的选项
  • 第一个:拖动Scene的视角
  • 第二个:选中后,再选中Scene中的任何一个GameObject,就可以通过拖动它的三个方向箭头(x、y、z)以及三个平面(xy平面、xz平面、yz平面)来改变Position的值。
    改变Position
    改变Position
  • 第三个:选中后可以在Scene中对选中的GameObject改变其Rotation的值
    改变Rotation
    改变Rotation
  • 第四个:选中后在Scene中可以对选中的GameObject进行三个方向的Scale的调整
    改变Scale
    改变Scale
  • 第五个:选中后可以在Scene中对选中的GameObject进行顶点的位置调整从而改变Scale的值
    改变Scale
    改变Scale
  • 第六个:选中后可以同时改变Position、Rotation、Scale的值,是第二三四个的结合。
    同时改变所有的Transform
    同时改变所有的Transform

创建游戏对象

创建Sphere

接下来创建小球,同样地,在MainScene下右键->3D Object->Sphere来创建一个unity预置的Sphere(球体),命名为Player,并通过reset其Transform来使其位置重置到原点。

新建一个Sphere
新建一个Sphere

这样我们看到小球的中心已经被定位到了(0,0,0)处,为了让小球能在平面上滚动,我们需要将小球放到平面上。
观察小球的Transform我们可以得到,它的Scale的值为(1,1,1),也就是说它的三个方向的大小都为1单位,为了让小球放到平面上边,显然我们需要将其向上移动半个球的距离,即将Position的Y值设置为0.5,小球就刚好在平面上了。

将小球放到平面上
将小球放到平面上

关于光源

其实我们可以看到小球是有影子的,这是最开始unity为我们准备的Directional Light作用的结果,我们可以看到Scene中的一个小太阳的标志,这个就是我们的光源,使用Directional Light来模拟太阳的平行光。它的Transform则显示了光源的位置、角度(也就是平行光的照射方向),如果我们将这个GameObject去掉的话就没有了光的效果。
当然通过改变Rotation的值就可以调节光源的方向,比如为了效果我将Rotation的Y值改为60。

关闭光源后的效果
关闭光源后的效果

创建Material

为了使GameObject美观,我们通常会对其表面进行一系列装饰,而其表面的表现是通过为这个GameObject添加Material(材料)来实现的。
接下来为我们的Ground和Player添加最简单材料:纯颜色。
在Assets下新建Materials目录用于管理各种材料,然后右键该目录选择Create->Material新建一个材料命名为Background。

新建一个Material
新建一个Material

选中Background,就可以看到它的Inspector面板了,我们在Albedo(反射率)一栏中可以选择一种颜色,在下方的预览中就可以看到效果了,这里我们选择RGB色(0,32,64)作为我们的Background的颜色。

为Material选择颜色
为Material选择颜色

想要将创建的Material运用在某个GameObject上,很简单,只需要拖动该Material到Scene中的目标GameObject上或者拖动到Hierarchy的该GameObject上即可。

将Material运用到Scene中
将Material运用到Scene中

可以看到我们的Ground已经变成了蓝色

成功改变Plane的颜色
成功改变Plane的颜色

让小球滚动起来

让小球拥有成为刚体

为了让小球有滚动的效果,我们需要小球拥有一系列的物理属性,物理属性已经由unity内置,我们只需为需要增加物理属性的GameObject添加一个Rigidbody的Component即可。如上一节中所示,选中Player,在Inspector面板中通过Add Component中找到Physics下的Rigidbody即可。

添加Rigidbody属性
添加Rigidbody属性

为小球添加控制脚本

有了刚体属性的小球需要在我们的控制下滚动,比如我们规定使用W,S,A,D四个按键来控制小球的方向,那么对于一个有物理属性的刚体来说,为了能够动起来,当然需要力(Force)作用在物体上,这些有关于如何控制GameObject的方法需要我们使用脚本(Scripts)来完成,假如你已经拥有了一定的C#编程基础。
同样我们在Assets下创建Scripts目录来管理脚本,在该目录处右键->Create->C# Script创建一个脚本,这里我们命名为PlayerController。

新建脚本
新建脚本

为了让我们创建的脚本与Player联系起来,可以在Player的Inspector面板下选择Add Component,在其中搜索我们的脚本名字就可以找到该脚本,根据unity的命名规范,喜欢将脚本各个单词使用“驼峰法”并且首字母同样大写的方式,有趣的是,unity对于这些脚本通常都会在每个大写字母处将这些单词分开,我也不知道为什么。简单点儿的话可以直接将Assets中的脚本拖到Inspector面板下,就可以添加成功。

将脚本添加给GameObject
将脚本添加给GameObject

打开脚本

编辑脚本需要编辑器,Visual Studio是较好的选择,它和unity之间有很好的合作关系,使用起来也很方便。双击脚本文件或者在Inspector面板中点击脚本的右上角的齿轮图标选择Edit Scrpit都可以打开编辑器对其进行编辑。

Unity的预置脚本内容
Unity的预置脚本内容

unity已经为我们预置好了脚本的最基本结构,最基本的,我们可以看到所有的unity脚本都继承自MonoBehaviour类,然后有两个预设函数,Start函数是在第一帧开始渲染前调用,Update函数在每一帧刷新前调用,都是非常常用的函数。
接下来思考我们要做的事情,我们需要检测用户的输入,并且通过输入的按键来控制小球的滚动方向,检测用户的输入同时也需要识别输入的是哪一个按键,除此之外我们还需要一些物理学有关的逻辑,比如我们需要添加一个力来控制小球的移动,这就是物理学逻辑,这些逻辑当然是每一帧都要进行一次,所以我们需要将这些逻辑写在每一帧更新都要执行的函数中。
显然我们可以写到Update函数下,因为Update函数是每一帧刷新前都会调用的,同时我们还有另外的选择,即使用FixedUpdate函数,它在每一次进行物理学运算的时候调用,每次检测到用户输入都需要进行物理学运算,所以我们可以将逻辑写到FixedUpdate函数下。

开始编写脚本

首先我们需要创建一个对于这个Player小球的引用,这样才能知道我们控制的是哪个小球,这里小球是刚体,所以我们创建一个刚体(Rigidbody)的引用,并且需要在第一帧开始渲染之前通过GetComponent方法来找到小球创建刚体的引用,这一逻辑自然就需要写到Start函数中了。

1
2
3
4
5
6
private Rigidbody rigidbodyPlayer;

void Start()
{
rigidbodyPlayer = GetComponent<Rigidbody>();
}

接下来对用户输入的读取就需要写到FiexedUpdate函数中了,我们使用Input类的GetAxis方法来获取水平或者垂直的运动轴,这个方法会返回一个float值作为该轴的移动距离。

1
2
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");

通过以上两行代码我们就可以将用户的W,S,A,D输入转化为水平轴和垂直轴的移动距离,分别存储在moveHorizontal和moveVertical两个float类型的变量中。

接下来通过Rigidbody类的AddForce方法可以为刚体添加作用力,AddForce方法接受一个三维向量(Vector3)参数,这个三维向量就可以表示力,显然我们的三维向量可以用刚才的moveHorizontal和moveVertical两个变量作为X值和Z值,同时我们是不需要小球在Y方向上移动的,也就是将Y方向的力作用设置为0.0f即可,经过调试我们会发现小球的移动速度过慢,为了方便调节小球的速度,只需要在表示力的三维向量前乘以一个倍数即可,为了方便调整,我们设置一个public的float类型的变量speed来调节这个乘积。
这里必须说明的是,凡在unity的脚本中被声明为public类型的变量,在unity的Inspector界面中的该脚本的Component下都会出现一个可以设置的值的方框。

1
2
3
4
5
6
public float speed;
void FixedUpdate()
{
Vector3 movement = new Vector3(moveHorizontal,0.0f,moveVertical);
rigidbodyPlayer.AddForce(movement*speed);
}
更改public的Speed的值
更改public的Speed的值

此处我们将speed的值设为10比较合理,运行游戏就会发现通过W,S,A,D的控制,小球动了起来。

让小球滚动起来
让小球滚动起来

完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
private Rigidbody rigidbodyPlayer;
public float speed;

void Start()
{
rigidbodyPlayer = GetComponent<Rigidbody>();
}

void FixedUpdate()
{
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");

Vector3 movement = new Vector3(moveHorizontal,0.0f,moveVertical);

rigidbodyPlayer.AddForce(movement*speed);
}
}

当然你也会发现,小球超出了Plane的边界居然掉了下去,其实这是合理的,因为小球为刚体,也就拥有物理引擎,当然受到重力的影响,在没有Plane的向上的作用力的情况下自然会下落。

设置摄像机

可以发现,我们的Camera的角度和位置都比较刁钻,这导致我们的游戏看到的画面并不完整,接下来我们对Camera进行设置,使其能够跟随我们的小球滚动来同时移动。
首先调节Main Camera的Position和Rotation使得画面和角度比较合适,比如这里将Position的Y值设为6,Z值设为-6,将Rotation的X值设为45得到了一个较为合适的位置。

改变摄像机的Transform
改变摄像机的Transform

接下来通过脚本控制Main Camera跟随小球Player一同移动,即在Position上保持相对静止。
可能你会想到,只需要将Main Camera拖动给Player使其成为Player的子物体不就可以保持两者相对位置不变化了吗?但是问题在于球体Player是滚动的,如果两者的位置完全相对静止,就会导致球滚动时Main Cmaera也会跟着球滚动,有一种天旋地转的感觉。感兴趣的话可以尝试一下。
新建脚本CameraController并添加给Main Camera做一个Component。为了使Main Camera的Transform的Position和Player的保持相对静止,Rotation并不和其保持一致,可以想到一个办法:设置一个偏移量,这个值初始化为游戏开始时Main Camera和Player之间的Position的差值,然后在球滚动时,每一次滚动都改变Main Cmaera的Position,使其新的Position等于现在球的Position的值加上刚才的偏移量,这样就会在每次球的位置改变时Main Camera都会跟上它的步骤。
显然,偏移量的设置需要在Start函数中完成,每一次球的位置发生变化时的逻辑可以在Update函数中完成,但还有一个更好的选择,就是LateUpdate函数,该函数在每次有GameObject发生变动时才会调用。
同时,我们的脚本使加在Main Camera上的,所以Main Camera的Transform可以直接调用,但是球的Transform则需要单独获取,这里我们设置一个public的GameObject量,然后在unity中将球Player拖动到这个量处作为参数即可。

1
2
3
4
5
6
7
8
9
10
11
12
private Vector3 offset;
public GameObject player;

void Start()
{
offset = transform.position - player.transform.position;
}

void LateUpdate()
{
transform.position = player.transform.position + offset;
}
添加Player实例
添加Player实例

这时候运行游戏就可以发现Main Camera的位置随着球的改变而发生了改变。

MainCamera成功跟随小球变化位置
MainCamera成功跟随小球变化位置

完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
private Vector3 offset;
public GameObject player;

void Start()
{
offset = transform.position - player.transform.position;
}

void LateUpdate()
{
transform.position = player.transform.position + offset;
}
}

建立围墙

为了不让小球总是滚落到Plane之外,可以在其四周围建立起一圈儿围墙,很简单,使用预设的Cube就可以了。
为了更好的管理四个围墙,我们可以先创建一个空的GameObject,将其命名为Walls,Reset使其重置位置,然后在其上右键新建一个Cube,并命名为WestWall,这样这个Cube就成为了Walls的子物体。

新建Cube
新建Cube

接下来如何调整这面墙的大小、位置就很简单了,可以在Inspector中直接输入具体数值,也可以直接在Scene中拖动和缩放,最好我们可以得到这个墙体结构。

改变Cube的Transform
改变Cube的Transform

为了简单方便,只要选中WestWall,在菜单栏的Edit中选择Duplicate(复制)(快捷键:Ctrl+D)即可,然后将复制好的墙拖动到适合的地点,在复制,最后可以得到四面墙。

复制Cube建立围墙
复制Cube建立围墙

接下来测试游戏,墙起作用了!

围墙正常运行
围墙正常运行

创建碰撞小立方体

新建Cube

我们在游戏场地中加入一些旋转的小方块儿,然后由球来碰撞这些小方块儿,碰撞之后就加一定的分数。
很简单地,创建一个Cube,并命名为PickUp,使其Reset,然后我们调整一下它的Rotation和Scale,使得其变成一个斜放的小立方体,(为了方便看,这里暂时隐藏了Player,只要在Player的Inspector面板中将其前边的对勾打掉它就会隐藏)。

建立小立方体并改变其角度
建立小立方体并改变其角度

让小立方体旋转起来

为了让小方块儿更有趣,我们来添加一个脚本让他旋转起来,新建脚本Rotator,并添加给PickUp,编辑此脚本。
这个逻辑中是不需要Start函数的,我们只需要每一帧都让小方块儿转动一下,所以将逻辑写到Update函数中,这里使用Ratate方法,需要一个Vector3作为参数,为了不让旋转过快,我们给每一帧都乘以一个小的时间量。

1
2
3
4
5
6
private Vector3 rotation = new Vector3(15,30,45);

void Update()
{
transform.Rotate(rotation * Time.deltaTime);
}

保存脚本,运行游戏可以看到小方块旋转起来了。

让小立方体旋转起来
让小立方体旋转起来

创建Prefab

接下来我们需要多放置一些立方体在游戏的平面上,在此之前,我们需要把这个设计好的立方体生成一个Prefab(预设体),让它变成一个asset,这样我们可以从prefab创建这个立方体的实例,这样我们才能分别控制每一个由prefab生成的实例或者控制prefab本身,在这个游戏中,被小球碰撞到的小立方体会消失,这也就是我们需要更新每一个小立方体的状态,所以我们需要创建一个Prefab。

a prefab is an asset that contains a template, or blueprint of a game object or game object family.
(预设体是包含游戏对象或游戏对象族的模板或设计大纲的一种资源。)

在Assets下新建Prefabs目录来管理预设体,只需要将MainScene中的PickUp拖拽到新建的目录下就可以生成对应的asset了,这时也会看到MainScene中的PickUp变成了一个蓝色的图标。

创建一个Prefab
创建一个Prefab

此时在MainScene中新建一个空的GameObject来管理所有的小立方体,命名为PickUps,将刚刚的第一个PickUp拖拽到它的上边使其成为其子物体。

调整父子GameObject的关系
调整父子GameObject的关系

放置更多的立方体

接下来放置小立方体,为了调整一个好的视角,我们可以点击Scene中右上角的方位标志的Y轴的小锥体,就会调整视角到俯视的角度,这是一个便捷的小技巧。

调整视角成俯视
调整视角成俯视

由于我们的小立方体是一个斜放的立方体,在原本的坐标轴下很难调整立方体在水平面上的位置,这里还有一个小技巧,我们点击右上角的Local变成Global,这样就会在Global的坐标中对小立方体进行调整了,可以看到,小立方体的三个坐标轴变成了只有两个方向的轴,非常方便就可以调整了。

将坐标改为Global
将坐标改为Global

接下来只需要Duplicate几个或者使用Ctrl+D来重复放置一些小立方体即可。这里粗略地摆放了八个小立方体。

摆放8个小立方体
摆放8个小立方体

为小立方体涂色

让后再通过添加一种Material的方式将他们涂成黄色。
这里可以通过两种方式完成。
第一种,将Material拖拽给任意一个PickUp立方体,然后再该PickUp的Inspector的Prefab的Overrides(重载)下选择Apply All来让所有PickUp都被涂成黄色。

为小立方体涂色
为小立方体涂色

第二种方法会更加简单,直接将Material用于Assets中的小立方体的Prefab就可以啦,找到Assets中的PickUp的Prefab,点击Inspector面板中的Open Prefab,就会在Scene窗口中预览该Prefab,这是再拖动黄色的Material到他上边,这个Prefab就会被染成黄色了。

为小立方体涂色
为小立方体涂色
为小立方体涂色
为小立方体涂色

完成碰撞

为PickUp添加标签

如果小球碰撞了旋转的立方体,那么小立方体就会消失。那么如何识别我们的小球Player碰撞的是小立方体呢?我们通过为PickUp添加特定的标签来识别。添加标签再Prefab上,在Assets中找到我们的Prefab并打开它,可以在它的Inspector面板中发现它还没有添加标签(Untagged),点击Tag后的按钮选择Add Tag…,来添加一个名为PickUp的标签,然后再回到最开始的面板处选择新添加的这个标签,这样PickUp的标签就添加好了。

新建并添加PickUp标签
新建并添加PickUp标签

完成碰撞逻辑

重新编辑PlayerController的脚本,这里我们将碰撞发生的逻辑写到函数OnTriggerEnter中,这个函数在发生碰撞事件的时候调用,而且用碰到的Collider(碰撞机)作为参数,也就是我们的Player碰到的Collider。可见,如果要检测到的碰撞,被碰撞的物体需要由碰撞引擎,也就是要有Collider的属性,其实我们会发现,作为Cube保存的一个Prefab在创建之初就已经有Box Collider的这一Component了,所以不再需要我们自己添加。

Box Collider属性
Box Collider属性

OnTriggerEnter函数用其碰撞到的Collider作为参数,现在就需要我们识别这个碰到的Collider是不是一个PickUp了,显然可以使用Tag来鉴别,这里使用gameObject的CompareTag方法来完成,该方法以一个字符串作为参数,返回一个布尔值,如果gameObject的Tag和字符串一致,就返回true。
接下来完成控制小立方体消失的逻辑,很简单,使用gameObject的方法SetActive来完成,这个方法控制的就是我们在Inspector面板中看到的每一个GameObject前的小对勾,该方法接受一个布尔值作为参数来控制GameObject的消失与显现。
完整的代码如下:

1
2
3
4
5
6
7
void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("PickUp"))
{
other.gameObject.SetActive(false);
}
}

关于碰撞的简单原理

此时运行游戏,在小球碰到小立方体的时候小立方体并没有像我们像的那样消失,这里的原因需要我们了解一定的Unity对与碰撞到原理。Unity将Collider(碰撞机)分为静态碰撞机(Static Collider)和动态碰撞机(Dynamic Collider)两种。静态的碰撞机比如墙面、地板等一些列静态的物体,与他们碰撞时,不允许两个Collider相互重合,也就是说会发生反弹,而为了让我们的碰撞能够实现,我们需要将立方体设置为动态的(Trigger)触发器,成为Trigger的Collider是可以被穿过的,这样才会引发我们的“碰撞事件”的逻辑。这里我们找到PickUp的Prefab,在它的Box Collider的Component中,勾选Is Trigger选项,这样它就成为了一个触发器。

选择Trigger属性
选择Trigger属性

此时运行游戏可以发现,小立方体在受到碰撞后完美消失。

小立方体被碰撞后消失
小立方体被碰撞后消失

优化

这里有一个有关物理引擎的优化,首先我们区分了静态碰撞机和动态碰撞机,Untiy对于静态碰撞机,会将它们的体积记录在一个缓存中,这是很合理的,因为静态即不动,所以并不需要在每一帧都计算器和重新绘制它的体积,而动态的则不同,因为它是运动的,所以必须在每一帧处都计算和绘制,这个过程是需要耗费资源的。

Any game object with a collider and a rigid body is considered dynamic.
Any game object with a collider attached but no physics rigid body is expected to be static.
(任何同时带有Collider和Rigidbody属性的GameObject称为动态的,任何只有Collider而没有Rigidbody属性的GameObject称为静态的)

为了让PickUp成为动态的,这样不需要每一次都计算它的体积,我们为PickUp的Prefab添加Rigidbody属性,运行游戏,发现神奇的事情发生了,所有的PickUp居然掉了下去,这当然是由于所有的刚体都受到了重力的影响,所以PickUp会下落。

小立方体受到重力下落
小立方体受到重力下落

为了解决这一问题,移步到PickUp的Prefab下刚刚添加的Rigidbody属性处,会发现有两个选项,分别是Use Gravity和Is Kinematic。

更改其成为Kinematic Rigid body
更改其成为Kinematic Rigid body

显然,我们可以直接将Use Gravity的对勾打掉,这样它不再受重力,自然不会下落,这是一个解决办法,但是并不完美,因为这里我们只取消了重力,但对于一个刚体来说,还会受到其他的力从而使其改变Transform,所以更好的做法是选中下方的Is Kinematic选项,接下来解释Kinematic:
Kinematic Rigid body(运动刚体)的含义是它的Transform也就是位置、角度、大小不会随着力的作用而改变而是单纯地根据Transform设置的值来改变,也就是说我们不能通过对其作用力来改变它的Transfrom,改变的唯一方法是直接调节其Transform下的各个值。
运行游戏,完美!

添加计分机制

添加计数器

接下来添加一个计数器,每撞到一个小球就使计数器数值增加一,自然需要编辑PlayerController脚本,声明一个int类型的变量,在Start函数中初始化为0,在每一次碰撞到小立方体后都增加一,这似乎是一个很简单事情,这里还特地将count每次更新后的值都打印到控制台来验证其正确性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private int count;
void Start()
{
rigidbodyPlayer = GetComponent<Rigidbody>();
count = 0;
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("PickUp"))
{
other.gameObject.SetActive(false);
count++;
Debug.Log(count);
}
}
控制台显示计分结果
控制台显示计分结果

添加UI在游戏界面中显示分数

显然,将分数显示在游戏界面中才是更好的选择,这里需要用到UI组件。
在Hierarchy面板中右键,选择UI->Text,就会创建一个UI组件,但是我们会发现其实Text外还有一层Canvas(画布),下方还出现了一个EventSystem,这些都是Unity为我们自动创建的,这是因为:

The single most important thing to know about these additional items is that all UI elements must be the child of a canvas to behave correctly.
(所有的UI元素都必须是一个Canvas(画布)的子元素才能正常工作)

这里将Text命名为CountText,这时候的CountText处于一个非常奇怪的位置,接下来我们来调整Text的位置。

新建Text和Canvas
新建Text和Canvas

在Inspector面板中,我们可以调整Text显示的内容,字体的样式,字体颜色以及这个UI组件的位置,先把文字显示改为Count Text,接下来改变其字体颜色为白色。

改变字体颜色
改变字体颜色

接下来调整Text的位置,找到Inspecctor的Rect Transform,可以看到UI的Transform和一般GameObject的定位方式不同,这个位置的确定是相对于游戏屏幕的,点击左边的这个方框,展开位置选择的面板,根据提示:按住Shift键选择中心轴(pivot),按住Alt键选择位置(position)。我们选择同时按住Shift和Alt键,将Text放到Canvas的左上角:

更改Text的位置
更改Text的位置

这样就可以看到Count Text放置到了Game窗口的左上角。接下来可以改变Rect Transform的Pos X的值和Pos Y的值让Count Text稍微离左边缘和顶部边缘一段距离:

加入偏移量
加入偏移量

编辑脚本来显示分数

接下来当然是将分数显示到Text中,继续编辑PlayerController脚本,首先需要添加新的Namespace:

1
using UnityEngine.UI;

然后自然需要创建一个Text变量来表示CountText的一个引用,我们将此Text变量声明为public的类型,为了可以在Inspector中对其进行直接的赋值。接下来要初始化CountText中所显示的文字,使用Text的text变量即可,在Start函数中进行初始化,然后再每一次碰撞到小立方体的时候再重新更新Text的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Text countText;
void Start()
{
rigidbodyPlayer = GetComponent<Rigidbody>();
count = 0;
countText.text = "Count: " + count.ToString();
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("PickUp"))
{
other.gameObject.SetActive(false);
count++;
countText.text = "Count: " + count.ToString();
}
}

保存脚本回到Unity,将CountText拖动到Inspector中新产生的这个Text变量处来实例化它即可。

实例化Text
实例化Text

运行游戏,完美!

游戏正常运行
游戏正常运行

添加完成提示

最后,当所有的小立方体都被收集后,再屏幕中心显示“You Win!”的字样,这看起来就很简单了,同样是:添加一个新的Text组件、命名、改变字体颜色大小和位置、在脚本中新建一个Text变量、在Start函数中将其初始值设为空、满足count值大于等于8的时候即显示“You Win!”、将新的Text拖动到Inspector面板中的变量处。
主要的代码:

1
2
3
4
5
6
public Text winText;
winText.text = "";
if (count >= 8)
{
winText.text = "You Win!";
}

运行游戏,完美!

添加You Win!的信息
添加You Win!的信息

Build游戏

最后就是创建我们的游戏了!Unity创建游戏的平台非常广泛,我们可以无需任何其他插件的前提下创建Windows、Mac和Linux平台下的游戏,但是如果创建诸如Android、IOS端的游戏需要额外的工作,比较复杂,以后会单独提到,这里先创建一个Windows平台的.exe文件。
先保存Scene,然后选择File下的Build Settings…。

进入Build Settings
进入Build Settings

默认即PC,Mac & Linux Standalone,这里还需要我们通过Add Open Scene来选择我们需要创建的Scene,不过本游戏只有一个Scene,不进行选择也是可以的。点击Build,选择要保存到目标目录,然后稍等片刻,就会得到了最终的.exe文件。

Build游戏
Build游戏

打开文件即可直接运行,整个游戏的所有工程也就全部完成了!

打开游戏程序
打开游戏程序
游戏完美运行!
游戏完美运行!

参考资料:https://unity3d.com/learn/tutorials/s/roll-ball-tutorial
本节内容的完整官方教程视频分享(英文无字幕):https://pan.baidu.com/s/1TNMMFjs7pZqLPhGQZ1tPqQ 提取码:1u70