博主最近在学习Unity,发现一个英文教程很好。这个教程通过一个个小项目讲解了Unity方方面面,包括编辑器的使用,脚本的开发,网格基础, 渲染和Shader等等,而且是由浅入深介绍的。这个教程是荷兰的独立软件开发工程师Jasper Flick写的,发表在了他自己的网站catlike coding。你可以在这个网站上看到这个教程和他的其他作品。
因为这个教程很不错,所以我计划把它翻译下来,这样对我的Unity技术和翻译水平都会是一个提高的机会。 我会不定期更新,但是尽量一个月保证一篇到两篇。我的Unity和英文翻译水平都很有限,如果阅读时发现什么错误,敬请指出。如果有什么关于文章的问题,也欢迎提问,大家一起讨论。 另外需要说明的是,我已经得到Jasper本人的许可翻译和发表。
这个教程的第一部分名为《游戏对象和脚本》,是属于第一章《基础篇》的,它介绍如何用Unity来做一个三维的钟表。前面先介绍搭建钟表的模型如何制作,后面介绍脚本的开发。如果你按照他的教程做下来,你会得到如下的一个钟表:
虽然这个项目看起来很简单,但是对于初学者来说是一个很好的项目-- 因为你很快就可以做出一个确实能用的东西了。 而且教程内一些小技巧我相信大牛也不一定都熟悉,比如表盘刻度的自动放置。所以好好读读这篇文章我相信会有所收获。
下面是这篇文章的正文翻译:
游戏对象和脚本 – 创建一个时钟
原作者:Jasper Flick
原链接:http://catlikecoding.com/unity/tutorials/basics/game-objects-and-scripts/
翻译者: York Zhang
翻译者邮箱: york.zhang[at]outlook.com
用简单的游戏对象创建一个时钟,
旋转时钟的指针来显示时间,
让指针动起来!
在这个教程里我们将创建一个简单的时钟,并编写一个组件用来显示当前时间。你并不需要了解太多Unity编辑器。 如果你已经用几分钟稍微熟悉了点Unity编辑器,知道了怎么在Scene(场景)中切换,就可以了。
这个教程是基于Unity 2017.1.0和以上版本的。
是时候创建一个时钟了。
- 创建一个简单的时钟
打开Unity,然后创建一个新的3D工程。你不需要添加额外的Asset Packages,也不需要开启Unity Analytics。如果你还没有对Unity editor进行界面的修改,你会得到如下的窗口布局:
默认窗口布局
我用了一个不太一样的布局,也就是”2 by 3”, 你可以从编辑器右上角的下拉菜单中选择。我又进一步对这个布局改了一下,将Project窗口修改成了”One Column Layout”,这样就更好的适应了垂直方向。你可以通过修改工具条(toolbar)右上角的锁图标附近的下拉菜单进行修改。 另外,我关闭了位于Scene窗口的Gizmos下拉菜单里的Show Grid选项。
定制的“2 by 3”布局
为什么我的Game窗口有显示有黑色的边? 如果你用的是高分辨率显示的话,会出现这个问题。 为了显示整个Game窗口, 打开aspect-ratio下拉菜单,然后关闭Low Resolution Aspect Ratios 选项。
关闭状态下的Low Resolution Aspect Ratios 选项。
1.1 创建一个Game Object(游戏对象)
在默认的场景里, 你能看到有两个game objects。 他们是在Hierarchy窗口,你也能看到他们也在Scene窗口里。 第一个Game Object是Main Camera(主镜头),用来渲染场景。而Game窗口用这个镜头来渲染。第二个game object是Directional Light,用来照亮整个场景。
用GameObject-->Create Empty 选项来创建你的第一个game object。 你也可以通过右键菜单来在Hierachy里面创建。 这样,在场景里就添加了一个新对象,然后你马上就可以命名它。 因为我们要创建的是一个时钟,我们就给它取名为Clock。
Hierarchy里的clock对象
Insepctor窗口含有game object的细节。 当我们选择clock这个game object的额时候, 它会显示对象名字和一些设置在顶部。默认情况下,这个game object是启用状态的,非静态的,不带标签(tag)的,而且它归属于一个默认的层(layer)。 这些设置是对我们来说是可以的。 在这些下面,你会看到这个game object的所有组件。一般来说总会有一个Transform组件,而我们clock这个game object 唯一有的组件就是它。
选择clock之后的Inspector窗口
Transform组件包含game objec在三位空间中的位置(position),旋转(rotation)和大小(scale)。你需要确保前两者的值都是0,而第三者的值都是1。
二维的game object是什么情况呢? 在二维而不是三维的情况下, 你可以忽略其中一个维度。一般来说,像UI元素这样的2维对象,都会有一个Rect Transform,这是一个比较特殊的Transform组件。
1.2 创建表盘
虽然我们有了一个clock对象,但是我们在场景中什么都没有看到。 我们需要添加三维模型,它才能渲染出来。 Unity自带一些基础的对象,我们可以用它们来做简单的时钟。我们先通过GameObject –> 3D object--> Cylinder 来添加一个圆柱体。 让它和我们的clock对象有一样的Transform值
一个圆柱体的Game Object
和空的game object不同的是,这个新创建的game object多出三个组件。首先,它有一个Mesh Filder。它只是包含了对内置的圆柱体网格的参照。第二个是一个Capsule Collider(胶囊碰撞体), 这是一个描述三维物理的组件。第三个是Mesh Renderer(网格渲染器)。这个组件用来确保物体的网格会被渲染,他也控制了用来渲染的材质。你如果不对它进行更改,它会使用Default-Material(默认材质)。你在inspector里的组件列表里也能看到这个材质。
虽然说这个对象用来表示一个圆柱体,但是它还有一个胶囊碰撞体组件,因为Unity并不含有原始的圆柱体碰撞体。我们不需要这个胶囊碰撞提组件,所以我们可以删除它。 如果你想在你的表上用到物理特性,你最好不要用Mesh Collider组件。 你可通过上方右边的齿轮图标旁边的下拉菜单来删除组件。
不再有碰撞体
为了将这个圆柱体变成表盘,我们得压扁它。你需要减少Scale里Y的值到0.1. 因为圆柱体网格是两个单元高,它的有效高度变成了0.2个单元。 让我们做一个大时钟,将Scale里X和Z的值改为10。
大小变化后的圆柱体
因为这个圆柱体表示表盘,你需要将圆柱体的名字更改为Face。它是时钟的一部分,所以让其成为Clock的子对象。要做到这点,你需要在Hierarchy里将它拖到clock上面。
表盘子对象
子对象是受制于它的父对象的,意思是说当Clock改变了位置,表盘也会变。就好像他们是一体的。 旋转和大小也一样。 你可以用这个方式去制作更复杂的关系。
1.3 创建时钟的外圈
时钟的表盘上一般会有标记来帮助指出具体什么时间。也就是说表的外围。 让我们用方块来指一个12小时的时钟的时间。
通过GameObject-->3D Object-->Cube来添加一个立方体(Cube)。将它的大小调整为(0.5,0.2,1),然后他就变成了一个又细又扁的长形方块。 现在它就在表盘上,那么我们需要将其位置(position)修改为(0,0.2,4)。这样他就会位于表盘的最顶端,表示12点的刻度。让我们将它命名为Hour Indicator(小时刻度)。
12小时的刻度
因为这个小时刻度的颜色和表盘一样,所以不太容易看得见它。让我们通过Assets-->Create-->Material, 或者在Project窗口点击右键菜单来让我创建另外一个材质。你会发现这个材质和之前的默认材质是一样的。将Albedo值改成深一点的颜色,比如Red红,Green绿和Blue蓝都为73。这样我们就得到一个深灰色的材质。给它一个合适的名字,比如Clock Dark。
黑色材质asset和颜色的弹出窗口。
什么是Albedo? Albedo是一个拉丁字,用来表示白色。Unity用其来表示材质的颜色。
让我们来将这个材质添加到小时刻度上。你可以将材质拽到场景里或者Hierachy窗口里的对象上, 也可以将其拽到inspector窗口的底部或者改变mesh render的材质数组,将其设为第0个元素。
黑色小时刻度
我们的刻度已经正确的放在了12点的位置,但是1点位置怎么办呢?因为一天有12个小时,一个表盘的一圈是360都,所以我们需要将刻度沿着y轴旋转30。 来让我们试试。
位置错误的旋转的小时刻度
尽管我们得到了正确的角度,刻度依然是12点的位置。 这是因为一个对象的旋转是相对于它本身所在位置的。
我们需要将刻度沿着表盘的边缘移动,将其调整为1点。我们可以不自己去搞清楚这个位置,而用对象的Hierarchy来帮我们做。首先将刻度的旋转都设为0然后新建一个空对象,这个对象的位置和旋转都是0,大小都是1. 将刻度拖到它下面。
临时父对象
现在,将这个父对象的旋转改为30度。 这样刻度也会转了,绕着其父对象的原点,最后落在了我们想让它在的位置。
位置正确的小时刻度
用Ctrl/Command+D键,或者你也可以用这个对象的右键菜单来反复复制这个临时的父对象。每个父对象在Y的旋转值上增加30度。不断重复这个动作知道你得到每个小时的刻度。
12小时的刻度
现在我们不再需要这些临时的父对象了。 在Hierarchy里选择其中一个小时刻度, 将其拽到clock对象里。之后它就成为了clock的一个子对象。 这时, Unity改变了刻度的transformation,所以它的位置和旋转并没有在Word Space(世界空间)里改变。将所有的12个刻度都拖进clock对象里,然后删除所有的临时父对象。如果你想做的快点,你可以用ctrl或者commend按键来进行对对象的多重选择。
外圈子对象
我看到一些值是90.00001.这是怎么回事? 当位置(position), 旋转(rotation)和大小(scale)的值是浮点数的时候,这个问题可能会出现。 这些数字的不是非常非常准确,所以导致了微小的误差。你不需要担心这个0.00001的误差因为这几乎无法察觉到。
1.4 创建指针
我们可以用相同的方法来构建指针。 创建另一个立方体(cube)并将其命名为Arm,然后将同样的黑色材质给他。 将它的大小设置为(0.3,0.2,2.5),这样它会比刻度更长更细一些。 将位置(position)设为(0,0.2,0.75),这样它就会位于表盘之上,并且指向12点的位置你会看到有一部分会在反向一点点,这让这个指针旋转时看起来会比较平衡一点。
时针
光照的图标去哪了? 我把光挪开了,这样它就不会弄乱场景。 因为其是一个平行光(directional light),它的位置其实并不重要。
为了让指针绕着时钟的中心旋转, 就像我们处理小时刻度那样, 需要创建一个父对象给它。 我们依然需要将这个父对象的位置默认值和旋转默认值设为0,大小设为1. 因为我们之后需要旋转指针,所以将这个父对象作为clock的子对象,然后命名为Hours Arm。这样, Arm就成为clock了一个的“孙对象”。
Clock 的hierarchy里面的三个表针
反复复制Hours Arm两次来创建分针(Minutes Arm)和秒针(Seconds Arm)。 分针应该逼时针更长更细,因此将Arm子对象的大小设为(0.2,0.15,4),位置设为(0,0.375,1)。这样分针就会在时针上面了。
对于秒针,将其大小设为(0.1,0.1,5),位置设为(0,0.5,1.25).为了进一步区分,我创建了一个名为Clock Red的材质,这个材质的albedo的RGB值为(197,0,0)。之后将这个材质添加到了Arm子对象。
所有三个指针
我们的时钟现在做好了。如果你还没保存场景,现在保存一下吧。它会作为一个asset保存在工程里。
保存的场景
如果你卡在哪里,或者需要比较你做的项目和我做的项目,或者想完全跳过构建这个clock的过程,你可以下载一个包含上述所有工作的包。通过Asset-->Import Package-->Custom Package, 你可以将这些包导入到一个Unity工程里。你也可以将其拽入Unity窗口或者在文件浏览器里双击这个包来导入。
2 钟表动画的制作
我们的钟表目前还没有时间的显示。它只是Hierarchy的一个对象,是Unity渲染的一堆网格 -- 仅此而已。假如有个默认的钟表组件,我们就可以让它表示时间了。但是并没有,所以我们需要自己做一个了。 组件是通过脚本(script)来实现的。 通过Assets-->Create-->C# Script来新建一个新的脚本资源(script asset)并将其命名为Clock。
Clock 脚本asset
脚本被选择的时候,Inspector将显示它的内容和一个用来在代码编辑器里打开这个文件的按键。你也可以通过双击这个脚本来打开编辑器。 脚本文件将包含默认的代码模板,看起来是这个样子的:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Clock : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } }
这就是C#代码。这是一种用来写Unity脚本的编程语言。 为了了解这些代码是如何工作的,先让我们全把这些代码删掉,从头开始。
Javascript语言怎么样? Unity也支持另一种编程语言,叫做Javascript,而实际上它应该叫做UnityScript。 Unity 2017.1.0 仍然支持它,但是用来创建JavaScript的菜单项会在2017.2.0版本里移除,对这个语言的支持可能会在这之后彻底结束。
2.1 定义一个组件类型
一个空文件不是一个有效的脚本。 它必须包含我们时钟的定义。 我们不会给一个组件只定义一个实例。然而,我们会给它定义一个类(class)或者类型(type),也就是Clock。 一旦做好了,我们就可以通过它建立很多这样的组件。
在C#里来定义Clock,我们首先我们先定义一个Class,然后是它的名字。 在下面的代码片段里,修改的代码会有黄色的背景。因为我们最先开始是创建一个空文件,所以它应该正确的写上class Clock,其他都没有。 当然,如果你在中间添加空格或者添加新的一行都可以的。
class Clock
一个Class(类)到底是什么呢? 你可以将一个Class想象成一个蓝图(blueprint),它可以用来在计算机的内存里创造对象。 这个蓝图定义了这些对象应该含有的数据和功能。 Class也可以定义它自己的数据和功能,而不是其对象的。一般来说,全局可用的功能会用这些数据和功能。
因为我们不想限制代码来访问我们的Clock,所以我们需要给它添加Public 访问修饰符。
什么是类的默认的访问修饰符? 当没有访问修饰符的时候,就相当于internal class Clock。 这样就将访问区域限制在同一个Assembly里,一旦你将代码打包成多个DLL文件的时候就会有关系了。为了确保这个class总好使,一般状况下将其标记为public
现在这样我们并没有一个有效的C#语法。 我们指出来我们要定义一个类型,所以我们必须实际定义它是什么样子的。 这要通过在上述声明之后一些代码段来实现。 这些代码段的是由大括号括起来的。我们先不在这个括号里什么都不写,只用{}。
public class Clock {}
现在我们的代码是有效的了。 保存文件,然后回到Unity编辑器。这时候Unity编辑器就会检测到我们的脚本已经改变并再次编译了这段代码。 然后,选择我们的代码,Inspector会通知我们这个asset并没有包含MonoBehaviour脚本。
非组件的脚本
这个信息的意思是说我们不能用这个脚本去创建一个Unity组件。目前,我们的Clock只是定义了一个通用的C#对象类型。 Unity只能用MonoBehaviour的子类型来创建组件。
Mono-behivour是什么意思呢? Behavour的意思是说我们可以编写我们自己的组件来添加定制化的行为给Game Object。 比较奇怪的一点是它实际上是英式的拼写方式。(美式单词为Behavior)。 Mono的意思是定制化代码添加给Unity的方式。 这个次曾经被用在Mono项目,Mono项目实际上是.NET Framework在多平台上的部署。 因此,MonoBehaviour实际上是一个老名字, 我们用它是为了向前兼容。
为了将Clock转换成Monobehavour的子类型, 我们改变我们的类型声明,这样就扩展了那个类型。 我们用冒号来表示。 这让Clock继承了MonoBehaviour的所有功能。
public class Clock : MonoBehaviour {}
然而,编译之后,这会导致一个错误。 编译器会抱怨说它不能找到MonoBehaviour类型。 这是因为MonoBehaviour类型是包含在一个namespace下面的,这个namespace就是UnityEngine。 要访问它,我们需要用它的全名,即Unity.MonoBehaviour.
public class Clock : UnityEngine.MonoBehaviour {}
什么是namespace(命名空间)? Namespace就像一个网站的域,但是是用在代码里的。 就像域可以有子域, namespace也可以有subnamespaces(子命名空间)。 一个很大的区别是命名空间是反着写的。所以不会写成forum.unity3d.com,而是写成com.unity3d.forum.因为代码来自于Unity,你不需要去线上单独拿到它。Namespace是用来组织代码和防止命名冲突的。
因为每次都加上UnityEngine的前缀实在是不方便,当我们完全不提它的时候,我们可以让编译器去查找这个namespace。这是通过在文件顶部添加using UnityEngine; 来实现的。 注意分号是命令最后一定要添加的。
using UnityEngine; public class Clock : MonoBehaviour {}
现在我们能够添加到我们的clock game object到Unity了。 你可以将这个script直接拽到对象上,也可以通过对象的inspector下面的Add Component按钮来添加。
Clock对象和我们创建的组件
现在用我们的Clock类作为模板的一个C#对象实例已经创建了。 它被添加在了Clock game object的组件列表里。
2.2 处理指针
为了旋转这些指针,Clock对象需要知道他们。让我们从时针开始 就像所有的game object一样, 它能够通过改变transform里旋转的值来旋转。 所以我们需要将是真的信息传给Clock。 这可以通过在代码段里添加数据字段来实现,这些数据字段是由字段名和之后的分号定义的。
Hours transform是一个合适的名字。但是名字必须是单字。最方便的方式是让第一个词的字母小写其他的词的首字母大写,然后将他们写在一起。所以就成了hoursTransform。
public class Clock : MonoBehaviour { hoursTransform; }
Using那行去哪了? 它还在那,只是我们没写出来它。 代码片段值包含足够的已经存
在的代码,这样你就知道改变的上下文环境了
我们还需要定义字段的类型,现在这种情况下就是UnityEngine.Transform。 要把它放在名字的前面。
Transform hoursTransform;
我们的class现在定义了一个字段,它能包含另一个Transform对象的引用。 我们需要保证它包含时针上transform组件的引用。
字段默认是private的,也就是说他们只能被Clock内部的代码访问。但是clock类不能被我们的Unity场景访问。 为了让任何人都能访问修改,我们可以将这个字段标记为public。
public Transform hoursTransform;
Public字段不是不好的形式么? 一般来说,编程者的大多数的共识是避免创建public字段。但是,把代码和Unity联系起来,public字段又是需要的。当然你可以通过某种方式绕过它,但是这样就让代码不是那么直观了。
当字段是public的时候,inspector窗口就会显示它。这是因为inspector会自动让所有的public字段组件可以编辑。
时针的transorm字段
为了建立它们的联系,将Hours Arm从Hierarchy拽到Hours Transform 字段。 或者,点右侧的小圆点来搜索Hours Arm。
已经连接上的Hours Transform
当Hours Arm被放置在里面后,Unity编辑器就会抓取它的transform组件,并在我们的field里引用它。
2.3 关于时针,分针和秒针
我们需要对时针和秒针做一样的操作。 所以要添加两个不同名字的字段到Clock。
public Transform hoursTransform; public Transform minutesTransform; public Transform secondsTransform;
因为他们是有相同的访问修饰符, 你可以考虑让这些字段的声明更加简洁。他们可以被合并成一个前面是访问修饰符和类型的用逗号分隔开的列表:
public Transform hoursTransform, minutesTransform, secondsTransform; // public Transform minutesTransform; // public Transform secondsTransform;
“//”符号是干什么的? 双斜线(//)用来表示注释。直到这行结束,他们后面的所有文字都会被编译器忽略。 他们后面的文字一般用来在需要的时候解释代码。 我也会用来标注哪些代码被删除了。
将另外两个指针也挂在编辑器上:
所有的指针都连接上了
2.4 关于时间
现在我们在Clock里完成了指针,下一步就是搞清楚现在的时间。为此,我们需要让Clock去执行一些代码。这需要通过添加代码段到Class,也就是方法(Method)。这些代码段的前缀是它的名字,取名字的惯例是首字母大写。让我们给其取名为Awake,就是说这些代码会在component刚苏醒的时候被执行。
public class Clock : MonoBehaviour { public Transform hoursTransform, minutesTransform, secondsTransform; Awake {} }
Method有些像数学的函数。比如f(x) = 2x +3。 这个函数拿来一个数,乘以2,加上3. 它拿来的是一个数,结果也是一个数。在方法里,更像f(p) = c, 这里p表示输入的参数,c表示代码执行。 那么你自然会问,那么这个功能执行的结果是什么呢? 这个我们之后会细讲。现在这种情况下,我们只是想执行一些代码,而不提供一个结果的值。 换句话说,这个方法的结果是空(void)的。因此我们通过用void前缀来指出结果是空的。
void Awake {}
我们也不需要任何输入参数。但是我们还是需要定义方法的参数,这些参数要在圆括号里用都好分隔开。不过我们现在的情况下,它就是空的而已。
void Awake () {}
现在,我们有了一个有效的方法,只不过它现在还没做任何事。 就像Unity检测了到我们的字段, 它也检测到了Awake方法。 当一个组件含有Awake,当这个组件苏醒时,Unity会调用哪个方法,一般发生在它被创建或者加载时。
Awake方法难道不应该是public的么? Awake和一系列其的方法在Unity是特殊的。 Unity会在合适的时候寻找它们和调用它们,不管我们如何声明它们。 我们不应该让这些方法public, 因为它们不该被除了Unity引擎意外其他的任何类调用
为了检测这个方法是否工作,让我们来创建一个调试信息。UnityEngine.Debug类包含了一个公众可用的变量叫做Log,它可以用来做这件事。 我们传递给它一个简单的字符串文本来打印。字符串要卸载两个引号中间。再次提醒,分号是结束这个表达式的必要符号。
void Awake () { Debug.Log("Test"); }
在Unity编辑器里运行它,你将会在编辑器状态栏上会显示这个测试的字符串。 如果你在Window-->Console里打开Console(控制台)窗口,你也会看到这些字符串。 Console还会提供一些额外的信息,比如当你选择一段文字就会看到哪个代码生成的这些消息。
现在我们知道我们的方法好使,然后我们就要搞清楚当它运行的时候的时间。 UnityEngine命名空间包含了一个Time类,这个类包含了一个time属性。好像当然我们应该用它,所以让我们用log来显示它。
void Awake () { Debug.Log(Time.time); }
什么是属性(property)? 属性是一个伪装成字段的方法。他可能是只读或者只写的。他的名字按照惯例一般是首字母大写, 但是Unity往往不遵守这个惯例。
我们发现log的值总是0.这是因为Time.time实际上是游戏运行后的时间。 因为我们的Awake方法是立即被调用的,没有时间流逝,所以这个Time.time并没有告诉我们真实的时间。
为了访问我们电脑的系统时间,我们需要用到DateTime结构体(struct)。这并不是Unity类型。他存在于System命名空间里。 他是.NET Framework里的核心功能的一部分,Unity可以用它来支持脚本的开发。
什么是结构体(struct)? 结构体就像类,也是一个蓝图。 区别是,他创建的都是简单的数值,比如整形数字或者颜色,而不是一个对象。 你可以像定义类那样定义结构体,知识要用struct而非class关键字。
DateTime有一个公众可访问的属性Now。他产生了DateTime的一个值,这个值就是现在的系统日期和时间。 让我们输出它看看。
using System; using UnityEngine; public class Clock : MonoBehaviour { public Transform hoursTransform, minutesTransform, secondsTransform; void Awake () { Debug.Log(DateTime.Now); } }
现在我们每次进入运行模式之后都会得到一个时间间隔。
2.5 旋转指针
下一步就是根据当前时间来旋转指针了。 让我们先从小时开始。 DateTime有一个Hour的属性表示小时。在目前的时间段调用它你就会得到一天中的小时时间。
void Awake () { Debug.Log(DateTime.Now.Hour); }
我们可以用他来建立一个旋转机制。旋转在Unity被存储为四元数(quaternions)。我们可以通过一个公众可访问的Quaternion.Eular方法来创建一个旋转。 他包含X,Y,Z三个轴的角度作为参数,然后输出为一个合适的四元数。
void Awake () { // Debug.Log(DateTime.Now.Hour); Quaternion.Euler(0, DateTime.Now.Hour, 0); }
什么是四元数(quaternion)? 四元数是复杂的数学概念,用来表示三维空间的旋转。虽然比起三维空间的维度它更难以理解,但是它有一些很有用的特性。比如,它不会导致万向节死锁(gimbal lock)。 UnityEngine.Quaternion是被用作简单的数值。它是结构体,而不是类。
所有的三个参数都是真实的数值,在C#里都被表示为浮点数值。 为了明确的声明我们提供给方法给这些数字,让我们在所有的0后面加上f后缀。
Quaternion.Euler(0f, DateTime.Now.Hour, 0f);
我们的钟表有12个小时的刻度,所以每个刻度之间是30度。 为了让旋转匹配上,我们要将小时乘以30。 乘法是用星号(*)来运算的。
Quaternion.Euler(0f, DateTime.Now.Hour * 30f, 0f);
为了说清楚我们是将小时转换成度数的,也为了以后方便,我们可以定义一个合适名字的字段。 因为它是浮点类型的,它的类型就是float。因为我们已经知道数字了,所以我们可以直接在声明的时候就立即赋值。
float degreesPerHour = 30f; public Transform hoursTransform, minutesTransform, secondsTransform; void Awake () { Quaternion.Euler(0f, DateTime.Now.Hour * degreesPerHour, 0f); }
每小时旋转的角度应该是不变的。我们可以通过在声明的时候添加const前缀来强调这点。这样degreesPerHour就变成了一个常量。
const float degreesPerHour = 30f;
常量(constant)有什么特别的? Const关键字指出这个值永远都不会变,也不需要成为一个字段。 他的值会被编译的时候计算,也会被常量取代。 常量只可以用在原始类型,比如数字。
目前为止,我们经由个旋转,但是我们还没对它做什么,他这样就不起作用。 为了将其应用到时针上, 我们要将其赋值时针的Transform里的localRotation属性。
void Awake () { hoursTransform.localRotation = Quaternion.Euler(0f, DateTime.Now.Hour * degreesPerHour, 0f); }
4点时的时针指向
“local”在旋转里是什么意思? localRotation用来表示transform组件的实际旋转,独立于他的父对象。换句话说, 这是对象本地空间的旋转。 这就是inspector里transform组件里显示的值。如果我们旋转clock的根对象,你能想到,他的指针会绕着这个根对象旋转。 其实还有一个rotation属性。 他用来表示transform组件在世界空间的的最终旋转,而且要把它父对象的旋转考虑进去。 假如我们用这个属性,当我们旋转时钟的时候,指针的位置不会调整, 因为他的旋转会被补偿。
const float degreesPerHour = 30f, degreesPerMinute = 6f, degreesPerSecond = 6f; public Transform hoursTransform, minutesTransform, secondsTransform; void Awake () { hoursTransform.localRotation = Quaternion.Euler(0f, DateTime.Now.Hour * degreesPerHour, 0f); minutesTransform.localRotation = Quaternion.Euler(0f, DateTime.Now.Minute * degreesPerMinute, 0f); secondsTransform.localRotation = Quaternion.Euler(0f, DateTime.Now.Second * degreesPerSecond, 0f); }
钟表显示为16:29:06
我们用DateTime.Now三次来获取小时,分钟和秒。每次我们都进入到属性里,做一些工作,这理论上会导致不同的时间。 为了防止这种情况的发生,我们需要确保时间只获取一次。 我们可以先在方法里声明一个变量,把整个时间都赋值给它,然后使用这个变量。
什么是变量? 变量想字段,只不过它只存在方法被执行的时候。 他属于方法而不是类。
void Awake () { DateTime time = DateTime.Now; hoursTransform.localRotation = Quaternion.Euler(0f, time.Hour * degreesPerHour, 0f); minutesTransform.localRotation = Quaternion.Euler(0f, time.Minute * degreesPerMinute, 0f); secondsTransform.localRotation = Quaternion.Euler(0f, time.Second * degreesPerSecond, 0f); }
2.6 表针动画
当你进入运行模式的时候,你会看到当前时间。 但是时钟是静止的。 为了保持时钟的时间和我们的目前时间是一致的,修改Awake方法为Update。 在运行模式中, 这个方法会被Unity在每帧都调用。
void Update () { DateTime time = DateTime.Now; hoursTransform.localRotation = Quaternion.Euler(0f, time.Hour * degreesPerHour, 0f); minutesTransform.localRotation = Quaternion.Euler(0f, time.Minute * degreesPerMinute, 0f); secondsTransform.localRotation = Quaternion.Euler(0f, time.Second * degreesPerSecond, 0f); }
在组件名字的前面,现在我们的组件还获得了一个开关。这个开关允许我们关闭这个组件,如果关闭了Unity就不会调用Update方法了。
2.7 连续滚动
我们时钟的指针准确的指向了小时,分钟和秒,但是它更像一个数字时钟,这个数字时钟是不连续的,却是有指针的。 很多时钟都会有慢慢移动的指针来模拟表示时间。 这两种模式都是可以的,所以让我们通过添加一个开关来让两种模式都可以设置。
添加另外一个public 字段到clock,命名为continuous。他可以开关,因此我们可以使用布尔(boolean)类型,用bool来声明它。
public Transform hoursTransform, minutesTransform, secondsTransform; public bool continuous;
布尔类型的值要么是true(真),要么是false(假),也就对应我们所说的了开和关。默认情况下它是false的,所以一旦它出现在inspector里,让我们打开它。
使用Continuous选项
现在我们有两个模式了。 下一步,复制我们Update方法,将他们重命名为”UpdateContinuous”和”UpdateDiscrete”。
void UpdateContinuous () {} DateTime time = DateTime.Now; hoursTransform.localRotation = Quaternion.Euler(0f, time.Hour * degreesPerHour, 0f); minutesTransform.localRotation = Quaternion.Euler(0f, time.Minute * degreesPerMinute, 0f); secondsTransform.localRotation = Quaternion.Euler(0f, time.Second * degreesPerSecond, 0f); } void UpdateDiscrete () { DateTime time = DateTime.Now; hoursTransform.localRotation = Quaternion.Euler(0f, time.Hour * degreesPerHour, 0f); minutesTransform.localRotation = Quaternion.Euler(0f, time.Minute * degreesPerMinute, 0f); secondsTransform.localRotation = Quaternion.Euler(0f, time.Second * degreesPerSecond, 0f); }
创建一个新的Update方法。 如果Continuous是true,就应该调用UpdateContinuous方法。你可以用if语句来实现。If关键字后面是一个圆括号内的表达式。如果那个表达式为真(true),那么内部代码段就会被执行。否则,代码段就会被跳过。
void Update () { if (continuous) { UpdateContinuous(); } }
Update方法应该在什么地方被定义呢? 要在Clock类里面。 相对于其他方法的位置无所谓。既可以在其他方法上面也可以在下面。
也可以添加一个替代的代码段,当表达式为假(false)的时候它会被执行。 这是通过else关键字来实现的。 我们也能用这个来调用我们的UpdateDiscrete方法。
void Update () { if (continuous) { UpdateContinuous(); } else { UpdateDiscrete(); } }
现在我们可以在两种模式中切换了,但是他们做的事情都是一样的。 我们需要调整UpdateContinuous,这样他就显示细微的小时,分钟,秒的变化。 不幸的是,DateTime不包含这种方便的细微的数据。幸运的是,它确实有个TimeOfDay属性。它给我们了一个TimeSpan值,这个值包含我们需要格式的数据,也就是TotalHours, TotalMinutes和TotalSeconds。
void UpdateContinuous () { TimeSpan time = DateTime.Now.TimeOfDay; hoursTransform.localRotation = Quaternion.Euler(0f, time.TotalHours * degreesPerHour, 0f); minutesTransform.localRotation = Quaternion.Euler(0f, time.TotalMinutes * degreesPerMinute, 0f); secondsTransform.localRotation = Quaternion.Euler(0f, time.TotalSeconds * degreesPerSecond, 0f); }
但是这样会导致编译错误,因为新的数值被定义错了类型。 他们被定义为双精度浮点数值,也就是double类型。 这些数值提供了比float更高的精度。但是Unity的代码只能支持单精度浮点类型。
单精度足够准确么? 对于大多数游戏来说,足够了。 但是如果是非常远的距离或者大小区别的话,就会碰到问题。 这是你就要用一些小技巧,比如远距传物来保持本地的游玩地区接近世界中间。当用双精度来解决这个问题的时候,会导致数值的大小也会改变,这样就会导致性能问题。所以,大多数游戏引擎都用单精度。
通过转换double到float,可以解决这个问题。 这回抛弃我们不需要的那部分数值精度。 这个过程被称作数值转换。将新类型用圆括号括起来放在数值前面,它就会被转换了。
hoursTransform.localRotation = Quaternion.Euler(0f, (float)time.TotalHours * degreesPerHour, 0f); minutesTransform.localRotation = Quaternion.Euler(0f, (float)time.TotalMinutes * degreesPerMinute, 0f); secondsTransform.localRotation = Quaternion.Euler(0f, (float)time.TotalSeconds * degreesPerSecond, 0f);
现在你应该大致了解了Unity创建对象和脚本的基础知识。