unity双击打不开脚本_游戏对象和脚本 (创建一个时钟)

3e979a321fd6287faa0d372c8266d71e.png

该文章是一篇译文,附上原文链接

Game Objects and Scripts​catlikecoding.com
1b3bcf5f35b7c1d8dd6adbfbfe473952.png
  1. 使用简单对象构建一个时钟
  2. 编写一个C#脚本
  3. 转动时钟的指针来显示时间
  4. 创建指针动画

在这个教程中,我们将创建一个简单的时钟并编写一个组件来显当前时间。你只需要对Unity编辑器有最低限度的了解。如果你已经使用了几分钟并知道如何在场景中导航,那么你就可以开始了。

这个教程假设你使用的是Unity编辑器版本最低为 2017.1.0。

16da5b2d5af6fd866e3e420a6187ff49.png
创建一个时钟
  1. 构建一个简单时钟

打开Unity并新建一个3D工程。你不需要任何额外的资源包也不需要任何分析。如果你还没有定制编辑器,你将得到它的默认窗口布局。

eedc7d6e0852e079ef9e8e4d173e8109.png
默认窗口布局

我使用的是另一种窗口布局,2x3的预设布局,你可以从编辑器右上角的下拉列表中选择。我通过将项目窗口切换到一个列布局来进一步定制该视图,这样更适合它的垂直方向。你可以通过工具栏上方窗口右上角的lock图标旁边的下拉列表更改它。我通常在场景窗口中通过Gizmos下拉菜单禁用Show Grid。

b8068d3cb4e87fc5fe7af5cf4d2c5651.png
自定义2x3窗口布局

Ps:为何我的游戏窗口中有黑边框?

当使用高分辨率显示时就会发生这种情况,要使其展开并填充整个游戏窗口,请打开游戏窗口上的aspect-ratio下拉菜单并禁用Low Resolution Aspect Ratios选项。

7ee036e4207376c3b1c6820bca76f307.png
禁用 Low resolution aspect ratios

1.1 创建一个游戏对象

默认场景包含两个游戏对象。它们陈列在Hierarchy窗口中,你还可以在Scene窗口中看到它们的图标。第一个是用于渲染场景的Main Camera。Game窗口就是用这个摄像机渲染的。第二个是照亮场景的Directional Light。

使用GameObject->Create Empty菜单创建你自己的游戏对象。你还可以通过Hierarchy窗口中上下文菜单来完成。这将向场景中添加一个新对象,你可以立即为其命名。由于我们将要创建一个时钟,所以命名为Clock。

9e926cddcc9a373c20f3acb12baaa933.png
拥有一个Clock对象的Hierarchy窗口

Inspector窗口显示游戏对象的细节。当我们选中clock对象时,它将包含一个有对象名称的标题以及一些配置选项。默认情况下,对象是可用的,非静态的,没有tag属于default 层。这些设置对我们是友好的。在下面的列表中,它显示了游戏对象的所有组件。总有一个Transform组件,这是我们的时钟对象当前拥有的所有组件。

4f09d19b10e3d8e044eefc697e067215.png
Clock对象选中时的Inspector窗口

Transform组件包含3D空间中的位置,旋转和缩放信息。确保clock对象的位置和旋转为0,缩放应当为1.

Ps:那么2D对象呢?

当在2D而不是3D中工作时,你可以忽略其中的一维坐标。专门针对2D对象(例如UI元素)通常有一个Rect Transform 组件,这是一个特殊的Transform组件。

1.2 创建时钟的表面

尽管我们已经有了时钟对象,但是我们还不能看见任何东西。我们将为它添加3D模型以便它们能够渲染。Unity包含一些基本对象,我们可以用它们来构建一个简单的时钟。让我们通过GameObject->3D Object->Cylinder菜单向场景中添加一个cylinder对象。确保它的Transform组件和Clock对象拥有相同的值。

9fa9d3df68aab2ec4dc71994cb81cae0.png
一个表示圆柱体的游戏对象

这个新的对象比一个空对象多了三个组件。首先,它有一个包含对内置圆柱体mesh引用的Mesh Filter。其次是用于3D物理的胶囊碰撞体。第三是Mesh Render。这是一个确保对象mesh能够被渲染的组件。它还控制用于渲染的材质,这是内置的默认材质,除非你改变它。该材质也显示在inspector组件列表的下面。

尽管对象表示一个圆柱体,但是他拥有一个胶囊碰撞体,因为Unity没有一个原始的圆柱体碰撞器。我们不需要它,所以我们可以移除这个组件。如果你想在你的时钟上使用物理属性,你最好使用Mesh Collider组件。组件可以通过右上角的齿轮图标的下拉菜单移除。

226f9947cbd76de8160e795a38321928.png
没有碰撞体

为了把圆筒变成时钟的表面,我们需要压扁它。这是通过减小Y分量的缩放来实现的。将Y分量的值减小到0.1。由于cylinder mesh的高度是两个单元,所以它现在的有效高度是0.2个单元。我们需要做一个大时钟,所以我们把X和Z分量的缩放值增加到10。

4eed0c728e4c9bd71c98ab0af453c973.png
一个缩放过的圆柱体

由于圆柱体代表时钟的表面,修改它名字为Face。它是时钟的一部分,所以让它成为时钟对象的子节点。你可以通过将Face拖到Hierarchy窗口中的时钟对象之上来实现这一点。

790b5e8b9655ef03f557aa95dde23db0.png
Face成为子节点

子对象受制于其父对象的变换。这意味着当Clock对象位置改变时,Face对象也会跟着一起改变。就好像它们是一个单一的实体。缩放和旋转也是如此。你可以通过这个特性来创建复杂的对象层次结构。

1.3 创建时钟的外围

时钟表面的外圈通常有标记用来帮助指示时钟显示的时间。这就是时钟的外围。让我们用块来表示时钟的12小时。

通过GameObject->3D Object->Cube向场景中添加一个立方体。将它的缩放设为(0.5,0.2,1),这样它就变成了一个狭长的扁平块。它现在位于时钟表面的内部。将它的位置设为(0,0.2,4)。将它放在Face的上面和12小时对应的一边。将它命名为Hour Indicator。

f44147c4d2c8f36af1a5e60081b77f31.png
12小时指示器

指示器很难看见,因为它和时钟表面有相同的颜色。让我们通过Assets->Create->Material或Project窗口中的上下文菜单为它创建一个其他的材质。这为我们提供了一个默认材质副本的材质资源。把它的Albedo值改为深一点的,例如rgb设为73。这就得到了一个深灰色的材质。给它一个适当的名字,例如Clock Dark

d1a52944382e49837bea7dd7619593d2.png
Dark材质资源和颜色值

Ps:什么是albedo?

Albedo是一个拉丁词,意思为白色。它仅仅是材质的颜色。

让Hour Indicator 对象使用这个材质。你可以通过将这个材质拖放到场景或者Hierarchy窗口中的对象上来实现这一点。你也可以把它拖拽到Inspector窗口的底部,或者改变Mesh Render材质数组的第一个元素。

1da61129fa649ee6bd8a049d0d353f1a.png
使用Dark材质的小时指示器

我们的指示器正确定位到12点,但是如果我们想要表示1点呢?因为有12个小时,一个完整的圆是360°,所以我们必须把指示器绕Y轴旋转30°。

e92998e34db0138679d5479c10be8234.png
旋转小时指示器,位置不正确

虽然这给出了正确的方向,但是指示器仍然在12小时的位置。这是因为一个对象的旋转是相对于它自己的局部原点,也就是它自己的局部坐标。

我们必须沿着时钟表面的边缘移动指示器使其与1小时对齐。我们可以利用对象层次结构来代替自己计算这个位置。首先把指示器的旋转重置为0.然后创建一个位置和旋转为0,缩放为1的新空对象。让指示器成为该对象的子对象。

0284e38648f771a8ad8d5c01f138a9de.png
临时父节点

现在设置这个父节点Y轴旋转为30°。指示器也会有效的围绕父节点的原点旋转,从而精确的达到我们想要的位置。

1cd5831a80811314032bcb1ef927f014.png
正确定位的小时指示器

使用Ctrl或Cmd D或通过hierarchy窗口中的上下文菜单复制临时父节点。将复制父节点的Y轴旋转值在增加30°。持续这样的操作直到每个小时得到一个指示器。

6a9c554ee66f2e4c6f65d4c44245159d.png
十二小时指示器

我们不再需要临时父节点。在hierarchy窗口中选择一个小时指示器并将其拖到时钟对象上。它现在变为时钟对象的子节点。当这种情况发生时,Unity改变了指示器的变换,所以它的位置和旋转在世界空间中并没有发生变化。为十二个指示器重复这个操作然后删除所有的临时父对象。你可以通过crl 或 cmd同时选择多个对象来加快这一过程。

b03a353291a60d9c6bb5353be7fe9215.png
时钟外围子节点

Ps:我看到的值是90.00001.这有问题吗?

发生这种情况是由于位置、旋转和缩放组件是使用的浮点数。这些数字的精度有限,有可能与期望的数值有微小的偏差。不用担心这0.00001的偏差,因为它们是不可感知的。

1.4 创建时针

我们可以用同样的方法来创建时钟的时针。创建另一个名为Arm的对象,并给它赋予指示器使用的相同材质。设置它的缩放值为(0.3,0.2,2.5),这样它就比指示器更窄、更长。将它的位置设置为(0,0.2,0.75)这样它就会位于表盘顶部并指向12点,但是它也会指向相反的方向。这就使当它旋转时看起来像一个有小平衡重量的时针。

387beb6623f85ecb832a7fcac6b3e49e.png
时针

Ps:光照的图标去哪儿了?

我把光线移开,这样就不会使得场景变得杂乱。因为它是定向光,所以它的位置并不重要。

要使Arm围绕时钟的中心旋转,我们为它创建一个父对象。确保它的位置和旋转值为0并且缩放值为1.因为稍后我们会旋转这个时针,让这个父节点成为时钟的子对象并命名为Hours Arm。Arm对象就成为了Clock对象的孙子节点。

fde7a559aee808329626090e52c42a56.png
拥有时针的时钟层次

复制时针对象来创建分针和秒针。分针对象应当比时针对象更窄、更长,所以设置他的缩放为(0.2,0.15,4),设置位置为(0,0.375,1)。这样它就在时针的上面.

对于Seconds Arms节点,设置缩放为(0.1,0.1,5),位置为(0,0.5,1.25)。为了使它看起来不一样,我为它创建了一个Clock Red材质并设置它的颜色值为(197,0,0)。

47d550649d3735e8140ce7dfb0adab73.png
三根指针

现在时钟已经被构建好了。如果你还没有完成,这是一个很好的保存场景的时机。它将作为资源存储在项目中。

0becc4e19da26e4670f1af4ec35418c0.png
保存的场景

2.让时钟动起来

我们的时钟现在还不能告诉我们时间。它现在仅仅是Unity渲染的一堆Mesh的对象层次结构。仅此而已。如果有一个默认时钟组件,我们就可以用它来计时。由于没有现成的,所以我们需要自己创建。组件通过脚本定义。通过Assets->Create->C# Script创建一个新的脚本资源并命名为Clock。

3ae50f660eadfdc2af33fe6ae44b1829.png
时钟脚本资源

当选中脚本时,检查器会显示它的内容,以及一个在代码编辑器中打开脚本的按钮。你也可以双击脚本资源来打开它。脚本文件会包含一些默认模板代码,像以下显示的一样。

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中使用的脚本编程代码。为了理解代码是如何工作的,我们将全部删除从头开始。

Ps:那么JavaScript呢?

Unity还支持另一种编程语言,通常被称为JavaScript,但它的实际名称是UnityScript。Unity2017.1.0仍然支持它,但是创建JavaScript的菜单将在Unity 2017.2.0中移除。预计在那以后将完全取消支持。

2.1 定义一个组建类型

一个空文件不是一个合法的脚本。它必须包含时钟组件的定义。我们不定义组件的单个实例。相反,我们定义一般的类或类型称为Clock。一旦定义完成,我们就可以在Unity中创建多个这样的组件。

在C#中,我们声明要定义一个类,后面是类名称,以此来定义时钟类型。在下面的代码片段中,改变代码的背景颜色为黄色。当我们从一个空文件开始,它的内容应该变成class Clock而不是其他的,尽管你可以在单词之间随意添加空格和换行。

class Clock

Ps:从技术上讲,什么是类?

你可以将类看做一个蓝图,它可用于创建存储在计算机内存中的对象。蓝图定义了它包含哪些数据以及它所拥有的函数。

类还可以用于定义不属于对象而是属于类本身的数据和功能。这通常用于提供全局可用的功能。

因为我们不想限制哪些代码可以访问时钟,所以最好在前面加上public访问修饰符。

public class Clock

Ps:什么是类的默认访问修饰符?

如果没有访问修饰符,将会默认我们编写的是internal class Clock。这将限制对来自同一程序集的访问,当使用打包在多个dll文件中的代码时,这一点就变得很重要。为了确保在任何时候都能正常工作,修改类默认访问修饰符为public。

目前我们还没有合法的C#语法。我们指出我们正在定义一个类型,所以我们必须实际定义它是什么样的。这是通过声明后面的代码块来完成。代码块的边界用大括号表示,我们现在使用{}让它空着。

public class Clock { }

我们的代码现在是有效的。保存文件并切回到Unity。Unity编辑器将检测资源是否发生了变化,并触发重新编译。完成之后,选择我们的脚本,检查器将给我们指出资源没有包含MonoBehavior脚本。

5e1c8ec32fede40726d5a20350bc4a07.png
没有组件的脚本

这意味着我们不能再Unity中使用这个脚本创建组件。此时,我们只是定义了一个通用的C#对象类型。Unity只能使用Monobehaviour的子类来创建组件。

Ps:mono-behavior是什么?

我们可以编写自己的组件来为游戏对象添加自定义行为。这就是行为所指的意义。它只是碰巧使用了奇怪的英式拼写。Mono部分指的是将对自定义代码的支持添加到Unity中的方式。它用到了mono项目,这是一个.net框架实现的多平台项目。因此,MonoBehaviour。这是一个向后兼容的老名字。

为了将Clock转换为MonoBehaviour的子类型,我们必须更改类型声明,以便它继承该类型,这是通过冒号实现的。这使得Clock继承了MonoBehaviour的所有功能。

public class Clock : MonoBehaviour { }

然而,这将导致编译后出现错误。编译器说无法找到MonoBehaviour类型。这是因为该类型包含在UnityEngine命名空间中。要访问它,我们必须使用它的完全限定名UnityEngine.MonoBehaviour。

public class Clock : UnityEngine.MonoBehaviour { }

Ps:什么是命名空间?

命名空间就类似于网站域名,但是它是作用于代码的。正如域可以有子域一样,命名空间也可以有子命名空间。最大的不同是它是反过来写的。所以http://forum.Unity3d.com将被写成com.unity3d.forum。代码是Unity自带的,不需要上网单独获取。命名空间用于组织代码并防止名称冲突。

因为在访问Unity类型的时候总是使用UnityEngine前缀是不方便的,所以我们没有显示的提到任何命名空间时,我们可以告诉编译器搜索这个命名空间。这是通过添加using UnityEngine在文件顶部完成的。命令需要以分号结束。

using UnityEngine;

public class Clock : MonoBehaviour { }

现在我们可以在Unity中为我们的时钟游戏对象添加我们的组件了。这是通过拖拽脚本资源到对象上或者通过对象检查器下方的Add Component按钮完成的。

225107a998c35d618234988101fe5c46.png
拥有组件的时钟对象

现在已经创建了一个C#对象实例,通过使用我们的Clock类作为模板。它已经被添加到时钟游戏对象的组件列表中。

2.2 获取指针的引用

为了旋转这些时钟指针,Clock对象需要获取它们。让我们以时针为例。像所有的游戏对象一样,它可以通过改变变换组件的旋转值,来达到旋转目的。所以我们必须把时针的变换组件加入到Clock中。这可以通过添加一个数据字段在代码块中来实现。

Hours transform是一个合适的名称。然而,名称必须是单个单词。通常是将名称字段的第一个单词小写并将所有其他单词大写。然后将它们组合在一起。所以它变成了hoursTransform.

public class Clock : MonoBehaviour
{
        hoursTransform;
}

Ps:using语句去哪里了?

它仍然在哪里,我仅仅是没有展示它。代码片段将包含足够的现有代码,以便您了解更改的上下文。

我们还必须定义字段的类型,在本例中是UnityEngine.Transform。它必须放在名称字段的前面。

 Transform hoursTransform;

我们的类现在定义了一个字段,该字段可以保存对另一个对象的引用。该对象的类型必须是Transform。我们必须确保它持有的是时针转换组件的引用。

字段在默认情况下是私有的,这意味着它们只能由Clock类的代码访问。但是类并不知道我们的Unity场景。通过修改字段为public,以便任何地方都能修改。

public Transform hoursTransform;

Ps:公共字段不是很糟吗?

一般来说,在编程时避免创建公共字段是一种共识。但是,在Unity编辑器中需要使用公共字段来连接对象。虽然你可以解决这个问题,但这会使代码变得不那么直观。

一旦字段变为public,它将显示在inspector窗口中。这是因为inspector自动使组件的所有公共字段变为可编辑。

d37c01952f5172229d33059c2b2f2cf8.png
时针域

要建立正确的连接,请将Hours Arm从hierarchy中拖拽到Hours Transform字段。或者,使用该字段右侧的圆形按钮搜索Hours Arm.

5edc31dec0bf7c544ab3b0d9aee11921.png
建立连接的Hours Transform

在拖动或者选择Hours Arm对象之后,Unity编辑器获取到它的transform组件,并在我们的字段中放入对它的引用。

2.3获取三个指针的引用

我们对分针和秒针做同样的操作。所以给Clock添加另外两个合适名字的字段。

public Transform hoursTransform;
public Transform minutesTransform;
public Transform secondsTransform;

可以使这些字段声明更简洁,因为它们共享相同的访问修饰符和类型。它们可以合并到一个逗号分隔的字段名称列表中,该列表位于访问修饰符和类型之后。

public Transform hoursTransform, minutesTransform, secondsTransform;
//public Transform minutesTransform;
//public Transform secondsTransform;

Ps:// 是做什么的?

//表示注释。它们后面的所有文本直到行尾都被编译器忽略。如果需要,它用于添加文本以解释代码。我还使用它来表示已删除的代码。被删除的代码也有一行代码贯穿其中。

将编辑器中的其它两个指针也连接起来。

5d5f96dfe8ace829d702eee8f3fc164c.png
连接完成的指针

2.4 获取时间

现在我们能够在clock中获取到指针,下一步就是指示出当前的时间。 因此,我们需要告诉Clock执行一些代码。这是通过向类添加一个代码块(称为方法)来实现的。块必须有一个通常大写的名称作为前缀。我们将它命名为Awake,这意味着应该在组件唤醒的时候执行代码。

public class Clock : MonoBehaviour
{
        public Transform hoursTransform, minutesTransform, secondsTransform;

        Awake {}
}

方法有点类似数学函数。例如f(x)=2x+3.这个函数接受一个数字,将其乘以2,然后再加上3。它操作单一的数字,所以它的结果也是单一的数字。对于一个方法,它更像是f(p)=c,p表示输入参数,c表示代码执行。由于这是相当普遍的,那么这样一个函数的返回结果是什么呢?这一点必须明确提到。在我们的示例中,我们只想执行一些代码,而不提供返回值。换句话说,该方法的返回结果是空的,我们用void前缀表示。

void Awake {}

我们也不需要任何输入数据。然而,我们仍然需要定义方法的参数,在圆括号之间用逗号分隔。只是在我们的例子中它是空的。

void Awake() {}

现在我们有了一个合法的方法,尽管它还没有做任何事情。像Unity检测我们的字段一样,它也检测这个Awake方法。当一个组件拥有一个Awake方法时,Unity将在组件唤醒时调用该方法。这发生在创建或加载之后。

Ps:Awake不需要是公开的吗?

Awake和其它的一系列方法被Unity认为是特殊的。它将找到它们并在适当的时候调用。无论我们如何声明它们,我们不应该将这些方法公开。因为它们不打算被除Unity引擎以外的任何东西调用。

为了测试这是否有效,让Awake创建一条调试信息。UnityEngine.Debug类包含了公开可用日志方法用于此目的。我们将传递给它一个要打印的简单文本字符串。字符串写在双引号之间。同样,需要分号来标记表达式的末尾。

void Awake()
{
    Debug.Log("Test");
}

在Unity编辑器中进入运行模式。将在编辑器底部的状态栏中看到显示的测试字符串。还可以在控制台窗口中看到它。通过Window->Console打开控制台窗口。当选中某条log文本时,控制台提供了一些附加信息,比如生成消息的代码。

现在我们知道我们的方法是有效的。让我们来指出调用它的当前时间。UnityEngine命名空间包含一个Time类,这个类有一个Time属性。我们把它打印出来。

void Awake()
{
    Debug.Log(Time.time);
}

Ps:什么是属性?

属性是伪装为字段的方法。它可能是只读的,也可能是只写的。通常是大写属性,但是Unity没有这样做。

Log显示的结果总是为0。那是因为Time.time是高告诉我们从我们进入Play模式后所流失的时间。由于Awake方法立即被执行,这时还没有时间流失。所以这不能告诉我们真实的时间。

为了获取我们所使用的计算机的系统时间,我们可以使用DateTime结构体。这不是Unity的类型,我们可以在System命名空间中找到它。它是.net框架的核心功能的一部分,Unity使用它来支持脚本。

Ps:什么是结构体?

结构体是一个蓝图,跟类类似。不同之处在于它创建的任何对象都被视为一个简单的值,比如整数、颜色值,而不是对象。定义自己的结构与定义类的工作原理是一样的,只是使用struct而不是class。

DateTime有一个公开可访问的Now属性。它提供了一个包含当前系统日期和时间DateTime值。让我们用Log来打印它。

using System;
using UnityEngine;

public class Clock : MonoBehaviour
{
    public Transform hoursTransform, minutesTransform, secondsTransform;

    void Awake()
    {
         Debug.Log(DateTime.Now);
    }
}

现在,每次进入播放模式时,我们都会得到一个时间戳。

2.5 旋转指针

下一步是基于当前时间旋转时钟指针。让我们再次从时针开始。DateTime有一个Hour属性,它为我们获取到DateTime的Hours值。在当前时间戳上调用它将得到当前的小时值。

void Awake() 
{
    Debug.Log(DateTime.Now.Hour);
}

我们可以使用这个小时值来创建旋转。旋转以四元素的形式存储在Unity中。我们可以使用公共可用的Quaternion.Euler方法来创建一个。它以X、Y、Z的标准正则角为参数生成一个合适的四元素。

void Awake()
{
    // Debug.Log(DateTime.Now.Hour);
    Quaternion.Euler(0, DateTime.Now.Hour, 0);
}

Ps:什么是四元素?

四元素是基于复数用于表示3D旋转的。虽然它比简单的三维向量更难理解,但它们有一些有用的特征。例如,它们不会受到万向锁的影响。

UnityEngine.Quaternion被用作一个简单的值。它是一个结构体而不是类。

三个参数都是实数,在C#用浮点数表示。为了显示的声明我们提供的方法支持哪种类型的数字,我们为所有的0加上f后缀。

Quaternion.Euler(0f, DateTime.Now.Hour, 0f);

我们的时钟有十二个小时,每个小时指示器间隔30°。为了与旋转匹配,我们必须把小时乘以30.

Quaternion.Euler(0f, DateTime.Now.Hour * 30, 0f);

为了明确的表示我们是将小时转换为度数,我们可以为转换因子定义一个适当的字段。由于它是浮点数所以它的类型是float。由于我们已经知道它的值,所以我们可以在字段定义的时候立即给它赋值。

private float degreesPerHour = 30f;

public Transform hoursTransform, minutesTransform, secondsTransform;

void Awake()
{
    Debug.Log(DateTime.Now.Hour);
    Quaternion.Euler(0f, DateTime.Now.Hour * degreesPerHour, 0f);
}

每小时的度数是不会改变的。我们可以在声明中添加const前缀来实现这一点。这就把每小时的度数变成了常数。

private const float degreesPerHour = 30f;

Ps:常量有什么特殊的地方?

const关键字表示值永远不会改变,并且不需要成为一个字段。相反,它的值将在编译期间计算,并替换所有使用到整个常量的地方。这只适用于像数字这样的基本类型。

现在我们有一个旋转,但是没有对它做任何操作,所以它只是被丢弃了。为了让它作用到时针上,我们需要把它赋值给时针transform组件的localRotation属性。

void Awake()
{
    hoursTransform.localRotation =
        Quaternion.Euler(0f, DateTime.Now.Hour * degreesPerHour, 0f);
}

f351212b80171f5416131cd7d11928a7.png
时针指向4点

Ps:什么是局部旋转?

localRotation是指变换组件的实际旋转,独立于其父节点的旋转。换句话说,它是对象在局部坐标系中的旋转。它显示在检查器中的变换组件中。所以如果我们要旋转时钟根对象,它的子对象也会向我们期望的那样跟着旋转。

还有一个rotation属性。它指的是转换组件在世界坐标系中的最终旋转,会计算上父节点的变换值。如果我们使用这种方法,当我们旋转时钟时,子节点就不会跟着调整,因为时钟的旋转会被补偿。

当进入play模式时,时针现在只想正确的方向。我们用同样的方法来处理另外两个指针。一分和一秒都占6度。

private const float degreesPerHour = 30f;
private const float degreesPerMinute = 6f;
private const float 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);
}

16da5b2d5af6fd866e3e420a6187ff49.png
时钟显示16:29:06

我们使用DateTime.Now三次来获取时、分、秒。每次我们都要访问属性,这需要花费一些时间,理论上会得到不同的时间值。为了确保不会发生这样的情况,我们应该只访问一次时间。我们可以通过在方法中声明一个变量并把当前时间赋值给它,然后用这个时间值来实现这一点。

Ps:什么是变量?

变量的作用类似于字段,只是它只在方法执行时存在。它属于方法,而不属于类。

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);
}

注意,我们的组件在检查器中在它的名称前多了一个check box。这允许我们禁用组件,从而防止Unity调用它的Update方法。

5758136968b1bdb7fcf61f3a39d7d627.png
拥有check box的组件

2.7 持续旋转

我们时钟的指针精确的指示当前的小时、分、秒。它的行为像一个数字时钟,离散但有指针。许多时钟都有缓慢旋转的指针来模拟时间的流逝。这两种方法都可行。所以让我们通过向组件中添加一个toggle来对其进行配置。

我们为Clock添加另一个名为continuous的公共字段。它可以是on或off,我们可以声明为bool类型。

public Transform hoursTransform, minutesTransform, secondsTransform;

public bool continuous;

Bool值可以是true或false,对应于本例中的on和off。默认情况下是false,所以一旦字段出现在检查器中,就打开它。

5e122f3d49c3ffd7ea9bc324dcae4d76.png
使用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 UpdateDiscrect()
{
    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,则执行它后面的代码块。否则,代码块将被跳过。

private void Update()
{
    if (continuous)
    {
        UpdateContinuous();
    }
}

Ps:新的Update方法必须在哪儿定义?

在Clock类内部。它相对于另外两个方法的位置无关紧要。可以在它们后面或前面。

还可以添加另一个代码块,在表达式为false时执行。这是通过关键字else完成的。我们可以使用它来调用UpdateDiscrete方法。

private void Update()
{
    if (continuous)
    {
        UpdateContinuous();
    }
    else
    {
        UpdateDiscrect();
    }
}

现在我们可以在两种方法之间切换,但是它们仍然做同样的事情。我们必须调整UpdateContinuous,这样它就会显示小数的小时、分钟和秒。不幸的是DateTime不包含小数数据。幸运的是,它却又一个TimeOfDay属性。这将为我们提供一个TimeSpan值,其中包含我们所需要的格式的数据。特别是TotalHours,TotalMinutes,TotalSeconds。

private 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。这些值比浮点值拥有更高的精度, 但是Unity代码只适用于单精度浮点值。

Ps:单精度值是否够用?

对于大多数游戏来说已经够用。当处理非常大的距离或尺度差异时,这将导致一些其他问题。然后你将不得不使用类似传送这样的技巧来保持当前游戏区域接近世界起点。虽然使用双精度可以解决这个问题,但是它也会使所涉及的数字大小加倍,从而导致一些其他的性能问题。因此,大多数游戏引擎都使用浮点数。

我们可以通过转换double为float解决这个问题。这只是简单的丢弃了我们不需要的精确数据。这个过程被称为强制转换。通过在要转换的值前面的圆括号内写入新类型完成。

private void UpdateContinuous()
{
    TimeSpan time = DateTime.Now.TimeOfDay;
        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中创建对象和编写脚本的基本原理。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值