目录

写作背景

要点提要

1、创建一个时钟

1.1 创建一个 Game Object

1.2 创建钟盘

1.3 创建时钟的其他部分

1.4 创建指针

2、让时钟动起来

2.1 定义组件类型

2.2 拿到指针

2.3 设置 3 个指针

2.4 掌握时间

2.5 旋转指针

2.6 让指针动起来

2.7 连续旋转

总结


写作背景

这篇教程是一个基础的长教程,会和大家一起创建一个简单的时钟,并且给它加上一些组件脚本用来显示当前时间。这篇教程的意义并不在于怎么学会写出一个时钟,而是教你认识 Unity 的编辑器。当然如果你之前已经接触过或者用过 Unity 了并且能自己找到场景窗口,那么就算是有个很好的开始了。

推荐大家使用 Unity2017.1 及以上的 Unity 版本进行学习,太老的版本可能会不兼容。

要点提要

1、用简单的 Objects 创建一个时钟。

2、用 C# 实现。

3、通过旋转时钟的指针来显示时间

4、驱动指针运动

unity 合并两个场景_c#

是时候创建这个时钟了! 

1、创建一个时钟

打开你的 Unity,并且选择创建一个 3D 的项目,这个事例不需要任何其他的插件,也不需要准备任何其他的资源就用 Unity 的默认创建就够了。创建完成之后,如果你没有对编辑器进行过自定义设置的话,你会看到如下默认窗口布局:

unity 合并两个场景_c#_02

不过我更喜欢 Unity 自带的一种预设布局, 2 by 3:

2 代表的是场景窗口和实际游戏窗口,而 3 代表的是 Unity 的层次列表、工程列表以及属性的显示面板,就如下图显示的一样:

unity 合并两个场景_c#_03

你可以从编辑器右上角的下拉列表里进行选取。

除此以外,我还会对编辑器的布局做进一步的定制,以便让它能更好的展示垂直化的内容。比如我把 Project 这个窗口调整为 1 列,就点击这个 Project 窗口右上角,锁的图标旁边有一个很小的下拉列表,点开就能设置。同时我也会禁用网格显示,这个按钮在 Scene 窗口的右上角,点击 Gizmos 之后会出现一个下拉菜单。

Q&A:为什么我的 Game 窗口会有一些黑色的边距呢?

这种情况一般发生在高分辨的显示器上。如果想让 Game 显示的内容铺满真个窗口的话,你可以打开设置分辨率比例的下拉框,然后禁用 Low Resolution Aspect Ratiosoption 选项。

unity 合并两个场景_游戏引擎_04

1.1 创建一个 Game Object

默认创建的场景,包含两个 Game Object,你可以从 Hierarchy(层次)窗口看到这两个游戏对象。一个是 Main Camera, 一个是 Directional light。即一个是游戏的摄像机,一个是平行光。Main Camera 用来渲染场景,所有 Game 窗口看见的东西都是由这个摄像机绘制出来的。而 Directional light 用来给场景展示阴影。

你可以通过一些方式创建你自己的游戏对象。比如右键 Hierarchy 窗口,从打开的菜单中选择 GameObject Create Empty 选项,或者从 Unity 引擎的菜单栏 GameObject Create Empty。这样 Unity 就会帮我们在场景里加入一个空的游戏对象,新对象创建好之后,你可以立刻对它进行命名,我们把这个对象命名为 Clock。

unity 合并两个场景_c#_05

除了刚才提到的 Project 和 Hierarchy 窗口之外,还有一个叫 Inspector 窗口,用来展示游戏对象的详细信息。当我们选中刚才添加的 clock 之后,就可以看到它包含一个带有名字的头信息和一些配置选项。默认情况下,对象是启用的,非 static 并且没有包含 tag 信息,并且属于 default 的层。这些日后对我们会很有用。

再往下的话,它会展示一系列的组件信息,但是总是会有一个 Transform 的组件,当然我们刚才创建的时钟对象也有。

unity 合并两个场景_unity_06

Transform 组件上包含了一个游戏对象在 3D 空间里的位置、旋转和缩放。这里我们把 clock 的 position 和 rotation 设置为 0,scale 设置为 1。

Q&A:如果是一个 2D 的游戏对象应该怎么办?

如果你是在 2D 的环境下工作的话,你可以忽略掉三维中的一维,有一些 2D 的游戏对象,比如 UI 这种,它会有一个叫做 Rect Transform 的组件。它其实是一种特殊的 Transform 组件。

1.2 创建钟盘

现在我们已经有了一个 clock 的游戏对象了,但是它现在只是一个空的对象,所以场景上我们看不到任何东西。下一步我们添加一些 3D 的模型在上面,这样他们就可以被渲染出来了。Unity 其实自身包含了一些初级的物体模型,我们可以用 Unity 自带的模型来创建这个时钟。

第一步,先添加一个圆柱体到场景里吧。步骤如下:GameObject → 3D Object → Cylinder,然后将它的 Transform 上的几个数值设置的和 clock 对象一样。

unity 合并两个场景_游戏引擎_07

这个新的对象比刚才的 clock 对象多 3 个组件:

  • Mesh Filter, 主要是引用了 Unity 内置的圆柱体网格。
  • Capsule Collider,主要是用来做 3D 的物理。
  • Mesh Renderer 这个组件是用来告诉 Unity 怎么把 Mesh Filter 画出来的。它同时包含了一个内置的 material(材质球), 材质球可以根据需要自己创建或者更换,同时材质球也在 Mesh Renderer 的组件下面被展示出来了。

虽然这个物体代表的是圆柱体,但是它却有一个胶囊体的碰撞表示,那是因为 Unity 没有内置圆柱图的碰撞。在本例子里面,我们并不需要它,所以我们可以把它移除。不过如果你想把物理加入到时钟里的话,建议使用 Mesh Collider 组件。

每个组件都可以通过它右上角的齿轮图标,点击之后弹出的下拉列表进行移除。

如果要把一个圆柱体做成一个钟盘的话,我们要把它压扁。这很简单,只需要修改 Transfrom 的 Scale 就可以了。把 Scale 的 Y 改为 0.1f,然后把 X 和 Z 的值改为 10,这样我们就得到了一个很大的钟盘了。

unity 合并两个场景_游戏_08

修改这个圆柱体的名字叫 Face,就好像它是这个大时钟的脸一样。因为它是 clock 对象的一部分,所以我们可以在 Hierarchy 窗口拖拽它到 clock 对象下面。

unity 合并两个场景_unity 合并两个场景_09

子物体的变换(transformation )会受父物体影响,这意味着如果 clock 变换了位置、缩放、或者旋转信息,face 都会跟着变化,就好比它们已经是一个完整的物体了。按照这个方式,我们可以在 Hierarchy 窗口完成非常多的复杂设计。

1.3 创建时钟的其他部分

钟盘上一般都会有一些标识来帮助时钟展示时间,我们一般称之为外围。那么我们就把它的 12 个小时的显示做出来。

我们通过 GameObject → 3D Object → Cube 的方式,来创建一个立方体。把它的 scale 设置为 (0.5, 0.2, 1),这样我们就可以得到一个窄的、扁的长条。下一步把它放置在钟盘上,设置 position 到 (0, 0.2, 4),这就是小时的指示器了。我们把它的名字改为 Hour Indicator。

unity 合并两个场景_游戏引擎_10

现在有了一个新问题,这个小时指示器颜色和背景太像了,很难区分。我们可以给它重新创建一个材质。点击 Assets → Create → Material 或者右击 Project 窗口,然后我们会得到一个默认材质球的副本。把它的 Albedo 的颜色修改的深一些,比如 RGB 值都设置为 73。这就是一个深灰色的材质了,把它的名字修改成 Clock Dark。

unity 合并两个场景_游戏_11

Q&A:什么是 Albedo?

Albedo 是拉丁语,意识是白的程度,你可以简单的理解为材质的颜色。

材质球创建好了之后,我们就可以通过拖拽这个材质球到 hierarchy 窗口,或者拖拽它到 inspector 窗口,或者直接改变 Hour Indicator 的 mesh renderer 组件下的 Materials 数组中第一个元素。

unity 合并两个场景_游戏_12

我们的钟盘上是有 12 个小时的,那么我该如果表示它是小时 1 呢?很简单,钟盘有 360 度,一共 12 小时,所以每个指示器旋转 30 度就可以了。试试看,旋转 Y 轴 30 度。

unity 合并两个场景_游戏_13

上面有了理论基础之后,我们看到指示器的位置还是在 12 点钟的位置,这是为什么呢?因为所有的 rotation 都是相对于它自己的原点的,也就是它的位置。

我们需要把指示器沿着表盘的的边布置,并且让它对齐到 1 点钟的位置上。有个取巧的办法就是让 hierarchy 帮我们自动排布。

首先我们把刚才指示器设置的 rotation 归 0,然后创建一个新的空的 GameObject,它的 position 和 rotation 为 0 ,scale 为 1。然后把 indicator 拖拽到它下面,变成它的子节点。

现在我们把父节点的 rotation 的 Y 设置到 30 度。前面我们说过父节点的变化会影响子节点,所以自然的小时指示器也会被旋转了,这就是我们想要的效果了。

复制刚才的模板组,windows 上的快捷键为 Ctrl+D(MAC 上为 Command+D),每一个新复制出来的都增加 30 度,这样我们就能制作完整的 12 个小时的了。

unity 合并两个场景_c#_14

这个时候,我们就不再需要这个临时的模板组了,选择所有的 hour indicators,把它们全部拖拽到 clock 的对象下面,让它们成为 clock 的一部分。当我们拖拽的时候,hour indicators 的父节点会发生改变,但是 Unity 会自动转换它们的变换信息来确保即使修改了父节点也不影响它们在场景里的世界坐标。并且自动帮你转化到当前的父节点下。

你可以通过按住 Ctrl 或者 command 键来一次选择多个物体进行操作。

unity 合并两个场景_c#_15

Q&A:如果我看到一些像 90.00001 这样的值,有问题吗?

这个是因为所有的 position, rotation, 和 scale 都是用浮点型的数据表示的。每个浮点型都会有一些精度限制,有可能你看到的值会超出你的预期,但是就事实而言,你不用担心 0.00001 这样的精度问题。

1.4 创建指针

我们可以用相同的方式来创建时钟的指针。创建另外一个 cube,然后把名字改为 Arm,给它们换上和小时指示器相同的深色的材质球。把它的 scale 设置为 (0.3, 0.2, 2.5),变成一个比指示器还长的长条,把 position 设置为 (0, 0.2, 0.75),这样它就显示在表盘上面,并且指向 12 点钟方向了。

unity 合并两个场景_游戏_16

要让时针以钟盘中心为圆点进行旋转,我们需要给它创建一个父节点。就像我们刚才给指示器创建的那样,同样的记得要重置 transform 下面的 position 、rotation 和 scale。它和指示器不一样,指示器只是一个固定位置,但是时针是需要旋转的,所以这个 parent 不能删除,我们把它重新命名一下改为 Hours Arm,所以这个时候,原先的 Arm 节点就变成了 clock 的孙节点了。

复制 Hours Arm 两次,来创建 Minutes Arm 和 Seconds Arm。分针要比时针更长更细,所以我们设置一下 它的 scale 到 (0.2, 0.15, 4) 并且 position 为 (0, 0.375, 1)。

同样的,设置一下 Seconds Arms,scale (0.1, 0.1, 5) ,position (0, 0.5, 1.25)。为了更明显的区分秒针,重新建了一个红色的材质球,RGB 值为(197, 0, 0),然后给 Arm 换上。

unity 合并两个场景_unity 合并两个场景_17

现在,我们的时钟已经创建好了,如果你也完成了的话,这个时候去保存场景就再好不过了,这里我保存一下工程。

如果你自己照着做了,或者想跳过刚才制作场景的步骤,那么没关系,你可以直接用我创建的,后面会给出下载方式。你可以把这些 packages 直接导入到你的工程里去,方法为 Assets → Import Package → Custom Package..., 然后拖拽 packages 到窗口上,或者双击文件浏览选中。

2、让时钟动起来

现在为止,我们已经有了一个时钟。但是这个时钟只是静静的躺在那里,并不会动。那么 Unity 是不是有现成的组件可以让时钟动起来呢?遗憾的是也没有,我们必须自己写。

所有的组件都是由 scripts(脚本)定义的,所有我们开始这个阶段的工作,先创建一个名叫 Clock 的脚本吧。和创建其他资源一样,脚本也可以通过 Assets → Create → C# Script 的方式创建。

当选中了脚本之后,inspector 就会显示它的内容,还有一个按钮可以让你用代码编辑器打开它。你也可以通过双击来调用 Unity 的默认的代码编辑器。每一个 Unity 创建的脚本都包含 Unity 脚本组件的默认代码,如下:

unity 合并两个场景_unity 合并两个场景_18

代码的语言是 C#,Unity 支持的脚本语言,为了搞清楚代码是如何工作的,我们先把它删除,然后从 0 写起。

Q&A:JaveScript 能不能用?

Unity 其实是支持 JaveScript 的,虽然名字叫 JaveScript,但其实它是 UnityScript,只是语法和 JaveScript 很像而已。截止 2017.1.0,Unity 仍然可以通过菜单创建 JavaScript 脚本,但是 2017.2.0 将会移除创建的入口,并且在未来会逐步停止支持。

2.1 定义组件类型

一个空的文件并不是一个有效的脚本。它其实需要包含我们想要的 clock 的组件才行。我们再用代码去实现组件的时候,并不是定一个组件的示例,而是定义一种类型或者类,比如 Clock,一旦这个类或者类型确定了之后,我们就可以通过 Unity 创建很多个组件出来,就像 Unity 其他的内置组件一样。

在 C# 语言里,我们定义一个 Clock 的类型是通过最前面的 class,然后紧跟这个类的名字。在后面的代码展示中,我们会把新加的代码用黄色的底标识,因为是从一个空文件开始写的,所以下面的 classClock 会用黄色的底标识。

在我们定了类之后,还没有给类加上权限约束,所以我们还要在前面加上 public 表示这是一个公开的类,大家都可以使用。

到这里之后,我们还没符合一个有效的 C# 语法。一个完整的类,内容必须被一对大括号包裹 {},类可以是允许是空的,所以这里我们先加上大括号,定一个 Clock 的空类。

现在我们的代码已经可以用了,保存这个文件,然后切回到 Unity,Unity 的编辑器将会在后台对代码进行编译,完成之后,选中这个脚本,inspector 窗口会提示我们尚未包含 MonoBehaviour 脚本。

为了将 Clock 类转换为 MonoBehaviour 类的子类,我们需要对刚才的类申明做一些改造,加上冒号:和要继承的类名,内容如下:

public class Clock :UnityEngine.MonoBehaviour{}

也可以改成:

unity 合并两个场景_unity 合并两个场景_19

到现在为止,我们的 Clock 已经可以像其他组件一样通过拖拽或者 Add Component 按钮进行添加了。

2.2 拿到指针

如果要旋转指针,那么 Clock 得知道它们在哪。我们就从时针开始,就像其他的游戏对象一样,它可以通过修改 transform 组件的 rotation 值来完成。那么我们首先就要知道时针的 transform 在哪里。这很好办,在 Clock 的类里添加一个属性,命名为 hoursTransform。当然你可以可以命名为任何你想要的名字,只要你自己知道什么干什么的就可以,代码如下:

unity 合并两个场景_unity_20

这里还有问题,我们已经在代码里命名了,但是代码的编译器并不知道这个名字代表什么意思,所以你还需要给这个名字加一个类型的限定,比如我们想要改变的是 Transfrom,那么就把 Transfrom 加载它的前面,变成这样:

unity 合并两个场景_c#_21

OK,到这里之后代码的编译器已经能明白能要什么了,不会报错了,但是,记得刚才的访问权限吗?如果不加的话,表示这个数据或者属性只能类内部使用,如果想要外部使用的话,也一样需要加上权限修饰符。

当我们的权限是 public 的时候,好玩的事情就发生了,我们切回 Unity,然后选择 clock 对象,还记得我们之前已经把 clock 的脚本组件绑上去了吗?看看这个 clock 脚本里多出了一栏:

unity 合并两个场景_游戏引擎_22

2.3 设置 3 个指针

按照刚才的方法,把 3 个指针的引用都拿到:

unity 合并两个场景_unity 合并两个场景_23

不建议偷懒合并成一行

处理完之后,我们的时针、分针和秒针都有了,如下:

unity 合并两个场景_游戏_24

2.4 掌握时间

现在我们已经能从代码层面找到时钟的指针了,下一步我们需要知道当前的时间是多少才行。那么我们就需要让代码做些事情了。

具体来说就是个类添加一些方法,比如我们先给 Clock 添加一个方法叫 Awake,这个是 Unity 脚本所支持的类,只要物体被加到场景就会执行一次,代码如下:

unity 合并两个场景_unity_25

要访问真正的系统时间,我们需要使用 DataTime 结构,这个不是 Unity 内部的类型,它是 C# 语言的类型,在 System 的命名空间下。也是.NET framework 的一部分。所以我们要想得到真正的时间需要这样修改代码:

unity 合并两个场景_游戏_26

2.5 旋转指针

那么现在我们就要把 Log 去掉,做真正的事情了。Unity 本身支持任何物体的旋转 Rotations 。所以我们只要使用 Unity 提供的方法就可以完成角度的旋转,先看下方法怎么用:

unity 合并两个场景_unity 合并两个场景_27

那么时针的制作完了,下面把分针和秒针的也做完,效果如下:

unity 合并两个场景_unity_28

现在看看,其实我们刚才一共用了 DateTime.Now 三次,分别是获取时、分、秒的时候。每一次都要消耗性能去获取对应类的属性。那么我们其实可以把它用变量缓存起来,这样我们直接去变量里拿就会节省很多性能开销。那么代码做如下改造:

unity 合并两个场景_游戏_29

2.6 让指针动起来

到现在为止,我们可以通过启动编辑器获取一个时钟的正确时间,以及时钟的三个指针都有真缺的位置,但它还是不能自己动。没关系,我们只需要把 Awake 改为 Update 就可以了。

unity 合并两个场景_游戏引擎_30

这是为什么呢?前面说过 Awake 整个生命周期只执行一次,但是 Update 是每一帧都会执行一次,所以你现在可以看到动的钟表了。

2.7 连续旋转

到现在为止,我们已经做出了时钟,并且时间可以随着正确的时间而变化,但其实它还是有缺点。很多时候,我们会看到一个时钟,指针在走的时候不是每秒、每分才动一次,而是持续的以很小的间距不断变化。这里我们就看看如何优化这种情况。

首先我们加一个变量表示我们是否要启用这个功能,代码如下:

unity 合并两个场景_unity_31

一个 bool 类型的变量只能由 true 和 false 两种类型,但是它可以从 inspector 面板下进行勾选赋值。

如果选中,表示这个变量值为 true, 没选中表示为 false。

现在我们需要把指针转动的逻辑分为两种情况了,一种是连续的,一种是不连续的。

unity 合并两个场景_unity_32

然后在 Update 里,根据当前变量的值决定使用哪一个函数:

unity 合并两个场景_unity_33

当然上面的其实只执行了连续的情况,不连续的需要用 else 来执行:

unity 合并两个场景_c#_34

在 UpdateContinuous 函数里,我们需要做一个改变,之前使用的是 DataTime 只能告诉我们当前的时、分、秒的值,但它不能告诉我们这些极小的值是多少,比如 4 点半,应该是 4.5 小时。那么我们只能用另外一个结构来代替 TimeSpan。再修改一下代码:

unity 合并两个场景_c#_35

OK,完美实现,但是它编译会报错。因为数据类型不匹配,也就是浮点的精度不匹配,需要做手动的转换,即在time.TotalSeconds前加上(float)作数据类型转换。

总结

这篇教程比较长,一共从两个部分介绍了 Unity:

第一部分教大家怎么认识和操作 Unity 的编辑器界面。以及一些基础的 Unity 概念和用法。

第二部分教大家怎么写脚本和代码,并介绍一些代码的基础知识。认识和了解自定义组件和 Unity 代码的一些基础知识。

所以,教程完结之后,能否真的做出时钟 demo 不重要,重要的是,是否掌握了一些 Unity 的基础知识。之所以很多东西没有一步搞到位,也是要更多的展示一下涉及到的相关操作和改进思路。

当然如果能够自己跟着教程做一遍,完成 demo 制作的话,也是非常了不起的!