目录 | 1 创建一个时钟1.1 创建一个Game Object1.2 创建钟盘1.3 创建时钟的其他部分1.4 创建指针2 让时钟动起来2 定义组件类型2.1 定义组件类型2.2 拿到指针2.3 设置3个指针2.4 掌握时间2.5 旋转指针2.6 让指针动起来2.7 连续旋转3 总结 |
---|
本文要点: 1、用简单的Objects 创建一个时钟。 2、用C#实现。 3、通过旋转时钟的指针来显示时间 4、驱动指针运动
这篇教程是一个基础教程,会和大家一起创建一个简单的时钟,并且给它加上一些组件脚本用来显示当前时间。这篇教程的意义并不在于怎么学会写出一个时钟,而是教你认识Unity的编辑器。当然如果你之前已经接触过或者用过Unity了并且能自己找到场景窗口,那么就算是有个很好的开始了。
这篇教程推荐大家用Unity2017.1及以上的Unity版本进行学习,太老的版本可能会不兼容。
(是时候创建这个时钟了)
1 创建一个时钟
打开你的Unity,并且选择创建一个3D的项目,这个事例不需要任何其他的插件,也不需要准备任何其他的资源就用Unity的默认创建就够了。创建完成之后,如果你没有对编辑器进行过自定义设置的话,你会看到如下一个默认的窗口布局。
(默认的窗口布局)
不过我更喜欢Unity自带的一种预设布局, 2 by 3。2代表的是场景窗口和实际游戏窗口,而3代表的是Unity的层次列表、工程列表以及属性的显示面板,就如下图显示的一样。
(预设的 2-by-3 布局)
你可以从编辑器右上角的下拉列表里进行选取。
除此之外呢,我还会对编辑器的布局做进一步的定制,以便让它能更好的展示垂直化的内容。比如我把Project这个窗口调整为1列,就点击这个Project窗口右上角,锁的图标旁边有一个很小的下拉列表,点开就能设置。同时我也会禁用网格显示,这个按钮在Scene窗口的右上角,点击Gizmos 之后会出现一个下拉菜单。
小提问:为什么我的Game窗口会有一些黑色的边距呢? 这种情况一般发生在高分辨的显示器上。如果想让Game显示的内容铺满真个窗口的话,你可以打开设置分辨率比例的下拉框,然后禁用Low Resolution Aspect Ratiosoption选项。
(Low resolution aspect ratios 被禁用了)
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。
(场景里添加了Clock对象)
除了刚才提到的Project和Hierarchy窗口之外,还有一个叫Inspector 窗口,用来展示游戏对象的详细信息。当我们选中刚才添加的clock之后,就可以看到它包含一个带有名字的头信息和一些配置选项。默认情况下,对象是启用的,非static并且没有包含tag信息,并且属于default 的层。这些日后对我们会很有用。
再往下的话,它会展示一系列的组件信息,但是总是会有一个Transform 的组件,当然我们刚才创建的时钟对象也有。
(clock选中的时候,Inspector上的信息)
Transform 组件上包含了一个游戏对象在3D空间里的位置、旋转和缩放。这里我们把clock的position和rotation 设置为0,scale 设置为1。
小提问:如果是一个2D的游戏对象应该怎么办? 如果你是在2D的环境下工作的话,你可以忽略掉三维中的一维,有一些2D的游戏对象,比如UI这种,它会有一个叫做Rect Transform的组件。它其实是一种特殊的Transform 组件。
1.2 创建钟盘
现在我们已经有了一个clock的游戏对象了,但是它现在只是一个空的对象,所以场景上我们看不到任何东西。下一步我们添加一些3D的模型在上面,这样他们就可以被渲染出来了。Unity其实自身包含了一些初级的物体模型,我们可以用Unity自带的模型来创建这个时钟。
第一步,先添加一个圆柱体到场景里吧。步骤如下:GameObject → 3D Object → Cylinder,然后将它的Transform上的几个数值设置的和clock对象一样。
(一个圆柱体的游戏对象)
这个新的对象比刚才的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,这样我们就得到了一个很大的钟盘了。
(经过缩放的圆柱体)
修改这个圆柱体的名字叫Face,就好像它是这个大时钟的脸一样。因为它是clock对象的一部分,所以我们可以在Hierarchy 窗口拖拽它到clock对象下面。
(拖拽后的父子结构)
子物体的变换(transformation )会受父物体影响,这意味着如果clock 变换了位置、缩放、或者旋转信息,face都会跟着变化,就好比它们已经是一个完整的物体了。按照这个方式,我们可以在Hierarchy 窗口完成非常多的复杂设计。
1.3 创建时钟的其他部分
钟盘上一般都会有一些标识来帮助时钟展示时间,我们一般称之为外围。那么我们就把它的12个小时的显示做出来。
我们通过GameObject → 3D Object → Cube的方式,来创建一个立方体。把它的scale 设置为(0.5, 0.2, 1),这样我们就可以得到一个窄的、扁的长条。下一步把它放置在钟盘上,设置 position 到(0, 0.2, 4),这就是小时的指示器了。我们把它的名字改为 Hour Indicator。
(12小时制的时钟,小时指示器)
现在有了一个新问题,这个小时指示器颜色和背景太像了,很难区分。我们可以给它重新创建一个材质。点击Assets → Create → Material 或者右击Project窗口,然后我们会得到一个默认材质球的副本。把它的Albedo 的颜色修改的深一些,比如RGB值都设置为73。这就是一个深灰色的材质了,把它的名字修改成Clock Dark。
(深色的材质球,和弹出的颜色选择面板)
小提问:什么是Albedo? Albedo是拉丁语,意识是白的程度,你可以简单的理解为材质的颜色。
材质球创建好了之后,我们就可以通过拖拽这个材质球到hierarchy 窗口,或者拖拽它到inspector 窗口,或者直接改变Hour Indicator的mesh renderer组件下的Materials 数组中第一个元素。
(深色的小时指示器)
我们的钟盘上是有12个小时的,那么我该如果表示它是小时1呢?很简单,钟盘有360度,一共12小时,所以每个指示器旋转30度就可以了。试试看,旋转Y轴30度。
(Rotated 设置后的效果展示)
上面有了理论基础之后,我们看到指示器的位置还是在12点钟的位置,这是为什么呢?因为所有的rotation都是相对于它自己的原点的,也就是它的位置。
我们需要把指示器沿着表盘的的边布置,并且让它对齐到1点钟的位置上。有个取巧的办法就是让hierarchy 帮我们自动排布。
首先我们把刚才指示器设置的rotation归0,然后创建一个新的空的GameObject,它的position 和rotation 为0 ,scale 为1。然后把indicator拖拽到它下面,变成它的子节点。
(一个小时指示器的模板)
现在我们把父节点的rotation 的Y设置到30度。前面我们说过父节点的变化会影响子节点,所以自然的小时指示器也会被旋转了,这就是我们想要的效果了。
(正确的放置了hour indicator)
复制刚才的模板组,windows上的快捷键为Ctrl D(MAC上为 Command D),每一个新复制出来的都增加30度,这样我们就能制作完整的12个小时的了。
(12小时都有了)
这个时候,我们就不再需要这个临时的模板组了,选择所有的hour indicators,把它们全部拖拽到clock的对象下面,让它们成为clock的一部分。当我们拖拽的时候,hour indicators的父节点会发生改变,但是Unity会自动转换它们的变换信息来确保即使修改了父节点也不影响它们在场景里的世界坐标。并且自动帮你转化到当前的父节点下。
你可以通过按住Ctrl或者command键来一次选择多个物体进行操作。
(拖拽之后的clock 结构)
小提问:如果我看到一些像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点钟方向了。
(时针)
小提问:那个灯光的图标去哪了? 我把它移开了,省的每次截图的时候都会遮挡,因为它代表平行光,就类似于太阳光,所以位置无关紧要。
要让时针以钟盘中心为圆点进行旋转,我们需要给它创建一个父节点。就像我们刚才给指示器创建的那样,同样的记得要重置transform下面的 position 、rotation 和scale。它和指示器不一样,指示器只是一个固定位置,但是时针是需要旋转的,所以这个parent不能删除,我们把它重新命名一下改为Hours Arm,所以这个时候,原先的Arm 节点就变成了clock的孙节点了。
(Arm现在是孙子节点)
复制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换上。
(所有的三个指针)
现在,我们的时钟已经创建好了,如果你也完成了的话,这个时候去保存场景就再好不过了,这里我保存一下工程。
(保存场景)
如果你自己照着做了,或者想跳过刚才制作场景的步骤,那么没关系,你可以直接用我创建的,后面会给出下载方式。你可以把这些packages 直接导入到你的工程里去,方法为Assets → Import Package → Custom Package...,然后拖拽packages 到窗口上,或者双击文件浏览选中。
2 让时钟动起来
好现在为止,我们已经有了一个时钟。但是这个时钟只是静静的躺在那里,并不会动。那么Unity是不是有现成的组件可以让时钟动起来呢?遗憾的是也没有,我们必须自己写。
所有的组件都是由scripts(脚本)定义的,所有我们开始这个阶段的工作,先创建一个名叫Clock的脚本吧。和创建其他资源一样,脚本也可以通过Assets → Create → C# Script的方式创建。
(创建 clock 脚本)
当选中了脚本之后,inspector 就会显示它的内容,还有一个按钮可以让你用代码编辑器打开它。你也可以通过双击来调用Unity的默认的代码编辑器。每一个Unity创建的脚本都包含Unity脚本组件的默认代码,如下:
代码的语言是C#,Unity支持的脚本语言,为了搞清楚代码是如何工作的,我们先把它删除,然后从0写起。
小提问:JaveScript能不能用? Unity其实是支持JaveScript的,虽然名字叫JaveScript,但其实它是UnityScript,只是语法和JaveScript很像而已。截止2017.1.0,Unity 仍然可以通过菜单创建JavaScript 脚本,但是2017.2.0将会移除创建的入口,并且在未来会逐步停止支持。
2.1 定义组件类型
一个空的文件并不是一个有效的脚本。它其实需要包含我们想要的clock的组件才行。我们再用代码去实现组件的时候,并不是定一个组件的示例,而是定义一种类型或者类,比如 Clock,一旦这个类或者类型确定了之后,我们就可以通过Unity创建很多个组件出来,就像Unity其他的内置组件一样。
在C#语言里,我们定义一个Clock的类型是通过最前面的class,然后紧跟这个类的名字。在后面的代码展示中,我们会把新加的代码用黄色的底标识,因为是从一个空文件开始写的,所以下面的 classClock 会用黄色的底标识,如下:
小提问 class是什么意思? class中文叫类,你可以把它想象成一个对象,并且这个对象会有一些属性和方法供计算机识别和调用。是人指挥电脑做事情的基本类型。
那我们定了类之后,还没有给类加上权限约束,所以我们还要在前面加上 public 表示这是一个公开的类,大家都可以使用。
小提问:class的默认权限是什么? 如果没有加上权限修饰符的,类默认会被当做内部类,即 internalclassClock 。这样的话其他的代码就无法创建和使用这个类,Unity也不能像其他组件一样通过面板操作来给某个对象添加该组件。
到这里之后,我们还没符合一个有效的C#语法。一个完整的类,内容必须被一对大括号包裹{},类可以是允许是空的,所以这里我们先加上大括号,定一个Clock的空类。
现在我们的代码已经可以用了,保存这个文件,然后切回到Unity,Unity的编辑器将会在后台对代码进行编译,完成之后,选中这个脚本,inspector 窗口会提示我们尚未包含 MonoBehaviour 脚本。
小提问:MonoBehaviour是什么意思? MonoBehaviour代表我们自定的这个脚本是否能够像其他组件一样绑定到游戏对象上。如果像让自己写的代码能够绑定到游戏对象,那么就必须从这个类继承。
为了将Clock类转换为MonoBehaviour类的子类,我们需要对刚才的类申明做一些改造,加上冒号:和要继承的类名,如下:
这样写完之后,切回Unity你会发现编译器会报错,这是为什么呢?因为编译器不知道MonoBehaviour是什么东西,因为有些类是包含在一些命名空间里的,如果不申明命名空间的话,编译器找不到这个类,这个例子里MonoBehaviour是在UnityEngine的命名空间下的,所以你需要这么写:
小提问:什么是命名空间(namespace)? 命名空间就像是网站的域名一样,但是是代码层级的。想象一下,如果两个人都写了一个Clock的类,那么我在引用Clock的话,就不知道用的是哪个。所以我们可以在类前加上命名空间来区分不同的类,这样就能区分想用的是哪个类了。比如一年级2班的小明,和二年级1班的小明。
因为UnityEngine这个命名空间下的东西特别多,如果我们只是在类名前加上UnityEngine的话,后面每个用到的类都要这样写会很累,那么还有一个办法,就是提前申明。就好比你写文章里面会提到小明,你开头第一行就说,一下但凡我提到小明的地方都是指一年级2班的。代码可以这样改一下:
到现在为止,我们的Clock已经可以像其他组件一样通过拖拽或者Add Component 按钮进行添加了。
(自定义的Clock组件)
嗯,通过这样添加组件之后,我们的Clock的实例就会被创建了,就像其他的组件一样。
2.2 拿到指针
如果要旋转指针,那么Clock得知道它们在哪。我们就从时针开始,就像其他的游戏对象一样,它可以通过修改transform组件的rotation 值来完成。那么我们首先就要知道时针的transform在哪里。这很好办,在Clock的类里添加一个属性,命名为hoursTransform。当然你可以可以命名为任何你想要的名字,只要你自己知道什么干什么的就可以,代码如下:
这里还有问题,我们已经在代码里命名了,但是代码的编译器并不知道这个名字代表什么意思,所以你还需要给这个名字加一个类型的限定,比如我们想要改变的是Transfrom,那么就把Transfrom加载它的前面,变成这样:
OK,到这里之后代码的编译器已经能明白能要什么了,不会报错了,但是,记得刚才的访问权限吗?如果不加的话,表示这个数据或者属性只能类内部使用,如果想要外部使用的话,也一样需要加上权限修饰符。如下:
当我们的权限是public的时候,好玩的事情就发生了,我们切回Unity,然后选择clock对象,还记得我们之前已经把clock的脚本组件绑上去了吗?看看这个clock脚本里多出了一栏:
这个时候,你只要把前面创建的时针对象拖拽进来,就能完成对时针引用。把它们连接在一起,如下:
2.3 设置3个指针
按照刚才的方法,把3个指针的引用都拿到。
当然其实你也可以偷懒的写法,像下面这样,但是极度不建议这么写:
处理完之后,我们的时针、分针和秒针都有了,如下:
(处理完3个指针的引用)
2.4 掌握时间
现在我们已经能从代码层面找到时钟的指针了,下一步我们需要知道当前的时间是多少才行。那么我们就需要让代码做些事情了。
具体来说就是个类添加一些方法,比如我们先给Clock添加一个方法叫Awake,这个是Unity脚本所支持的类,只要物体被加到场景就会执行一次,代码如下:
所谓的方法或者函数的意思就相当于是数据里的概念,比如:f(x)=2x 3,X是一个变量,你输入任何参数给它,它都会返回这个数乘以2并 3的结果给你。程序里的函数功能类似,但是不一定是用来做算数的,你可以设计任何你想要的函数,调用它就能得到你想要的处理或者结果。每个函数前面需要也需要访问权限的限制,除此之外,还需要一个返回类型的限制。我们这里使用void标识该函数不需要返回。改造如下:
当然,我们这个例子也不需要输入任何参数,但是即使不需要参数,函数任然需要用一个空的小括号()来完成函数的语法。如下,表示一个空的Awake函数,并且参数也为空。
到这里之后,编译器已经不会报错了,但其实这个方法或者类目前为止还没做任何有意义的事情,所以接下来我们需要让它干点事情,首先就输出一个日志测试一下功能。
Debug是UnityEngine命名空间下的一个类,用来做调试用的,这个类有一个方法就是Log,可以在Unity的控制台打印日志,这样在按下Play的时候,我们就可以在Unity的控制台上看到一个Test的输出了。
现在类的功能测试了没有问题,那么我们再干点其他的事情,把刚才的测试代码替换一下变成下面的,猜猜看会发生什么事情?
Time也是UnityEngine命名空间下的类,用来标识时间,当我们点击Play的时候,你会发现,你控制台输出的总是0!这是为什么?因为这个Time.time记录的是Unity启动运行之后到现在时间,而不是真正的时间。所以这里永远只会输出0。
要访问真正的系统时间,我们需要使用DataTime结构,这个不是Unity内部的类型,它是C#语言的类型,在System的命名空间下。也是.NET framework的一部分。所以我们要想得到真正的时间需要这样修改代码:
Now就代表现在的时间。这时候你已经可以在控制台看到实时的正确的时间了。
2.5 旋转指针
现在万事俱备只欠东风了,只要能够正确旋转时钟的指针就大功告成了。梳理下我们现在有的,时钟的各个部件、代码能拿到三个指针,并且现在也能获得正确的时间。
还是有点不对,刚才我们打印的是Now,那么我们具体怎么拆分时、分、秒呢?没关系, DateTime 什么都支持,比如获得当前时间这样就可以:
那么现在我们就要把Log去掉,做真正的事情了。Unity本身支持任何物体的旋转Rotations 。所以我们只要使用Unity提供的方法就可以完成角度的旋转,先看下方法怎么用:
这里我们删除了日志,然后把时钟本身沿着Y轴旋转了一定的角度,角度为当前的小时数。
这里有两个问题,第一个问题,我们前面说过,Transform的这些属性都是用浮点表示的,所以我们应该把0改为0f,表示这是浮点类型。
第二个问题,钟盘之前处理的小时表示都有乘以30度来表示,所以这里也需要乘以30度才能对上。
这样写了之后,还是有问题,为什么呢?因为30这个值它可以提取出来当做一个变量参与结算,不然光30一个值,会搞不清楚到底代表什么意思。
一个小小的代码优化,增加可读性,就变成这样:
现在是不是完美了呢?还是没有,因为30这个角度值是不会变的,它完全可以当做一个常量来表示,所以可以在前面加上const 来表示这是一个常量,这样代码在编译的时候,编译器就会为这个值省去一些复制和存储,提高性能。如这样:
那倒这里是不是已经OK了呢?还没有!为什么?因为刚才说了,它可以旋转了,但是它旋转的是钟本身,并不是时针、分针或者秒针。要想让时针旋转,就需要给时针赋值才行:
启动运行看下4点钟的位置,
(4点钟位置)
你是不是惊讶的说,这根本不是4点钟的位置,别急还记得我们之前创建表盘的时候嘛,1点钟的位置其实在这里
所以结果名没有错哈。
那么时针的制作完了,下面把分针和秒针的也做完,代码如下:
显示效果如下
(4点29分06秒)
现在看看,其实我们刚才一共用了DateTime.Now 三次,分别是获取时、分、秒的时候。每一次都要消耗性能去获取对应类的属性。那么我们其实可以把它用变量缓存起来,这样我们直接去变量里拿就会节省很多性能开销。那么代码做如下改造:
2.6 让指针动起来
到现在为止,我们可以通过启动编辑器获取一个时钟的正确时间,以及时钟的三个指针都有真缺的位置,但它还是不能自己动。没关系,我们只需要把Awake改为Update就可以了。
这是为什么呢?前面说过Awake整个生命周期只执行一次,但是Update是每一帧都会执行一次,所以你现在可以看到动的钟表了。
(每帧更新正确的时间)
除此之外,你还可以看到Clock的组件前面有了一个toggle来控制是否启用这个组件脚本了。
2.7 连续旋转
到现在为止,我们已经做出了时钟,并且时间可以随着正确的时间而变化,但其实它还是有缺点。很多时候,我们会看到一个时钟,指针在走的时候不是每秒、每分才动一次,而是持续的以很小的间距不断变化。这里我们就看看如何优化这种情况。
首先我们加一个变量表示我们是否要启用这个功能,代码如下:
一个bool类型的变量只能由true和false两种类型,但是它可以从inspector面板下进行勾选赋值。比如:
如果选中,表示这个变量值为true,没选中表示为false。
现在我们需要把指针转动的逻辑分为两种情况了,一种是连续的,一种是不连续的。
然后在Update里,根据当前变量的值决定使用哪一个函数。
当然上面的其实只执行了连续的情况,不连续的需要用else来执行。
在 UpdateContinuous 函数里,我们需要做一个改变,之前使用的是DataTime只能告诉我们当前的时、分、秒的值,但它不能告诉我们这些极小的值是多少,比如4点半,应该是4.5小时。那么我们只能用另外一个结构来代替TimeSpan。再修改一下代码:
OK,完美实现,但是它编译会报错。因为数据类型不匹配,也就是浮点的精度不匹配,需要做手动的转换。如下:
这次真的是大功告成!看效果!
3 总结
这篇教程从两个部分介绍Unity。
第一部分教大家怎么认识和操作Unity的编辑器界面。以及一些基础的Unity概念和用法。
第二部分,教大家怎么写脚本和代码,并介绍一些代码的基础知识。认识和了解自定义组件和Unity代码的一些基础知识。
所以,教程完结之后,能否真的做出时钟demo不重要,重要的是,是否掌握了一些Unity的基础知识。之所以很多东西没有一步搞到位,也是要更多的展示一下涉及到的相关操作和改进思路。
当然如果能够自己跟着教程做一遍,完成demo制作的话,也是非常了不起的。