游戏对象和脚本
原文地址:https://catlikecoding.com/unity/tutorials/basics/game-objects-and-scripts/
本次教程的主要内容:
用简单的对象构建一个时钟。
编写一个c#脚本。转动时钟的指针来显示时间。
让指针运动。
这是关于学习操作Unity的基础知识系列的第一个教程,在这个教程中我们将会创建一个简单的时钟,然后编写一个组件让它显示当前的时间。你还不需要有任何关于使用Unity编辑器的任何经验,但是一般情况下,你应该对多窗口编辑器有一定的经验。
本教程使用Unity 2020.3.6f1.
1. 创建一个工程
在我们开始使用Unity编辑器工作必须创建一个工程。
1.1 新工程
当你打开Unity时,你会看到Unity Hub。这是一个启动器和安装程序,在这里你能创建或打开工程,安装不同的Unity版本或者做一些其他的事情。如果你没有安装Unity 2020.3或更高版本,现在就安装它。
哪个Unity版本是合适的? Unity每年都会发布多个新版本。有两个平行的发布版本。最稳定和安全的是LTS版本。LTS代表长期支持,在Unity中表示支持2年。我坚持使用LTS版本作为我的教程,其中2020.3是最新的。本教程使用2020.3.6版本。版本号的第三个部分表示补丁发布版本。补丁版本包含bug修复,只有很少的新功能。最后的f1后缀表示正式的最终版本。任何2020.3版本都适用于本教程。
最高的Unity版本通常是开发分支,会引入新功能,可能删除旧功能。这些版本不会像LTS版本那样稳定,每个版本通常只支持几个月。
当你创建一个新项目时,你需要选择Unity版本和模板。我们将使用标准的3D模板。一旦它被创建,它将被添加到项目列表中,并在Unity编辑器的适当版本中打开。
我可以用不同的渲染管道创建一个项目吗? 是的,唯一的区别是项目在它的默认场景将有更多的东西,并且你的材质将看起来不同。您的项目还将包含对应的包。
1.2 编辑器布局
如果您还没有自定义过编辑器,那么你最终将得到它的默认窗口布局。
默认布局包含我们需要的所有窗口,但您可以根据自己的喜好对窗口进行重新排序和分组。您还可以打开和关闭窗口,就像资产商店一样。每个窗口都有自己的配置选项,可以通过在它们右上角的三个点按钮访问。除此之外,大多数软件还有一个工具栏,上面有更多的选项。如果你的窗口看起来与教程中的不一样——例如场景窗口有一个均匀的背景而不是天空框——那么它的一个选项就是不同的。
你可以通过Unity编辑器右上角的下拉菜单切换到一个预先配置的布局。您还可以将当前布局保存在那里,以便稍后恢复到它。
1.3 包
Unity的功能是模块化的。除了核心功能之外,还有一些额外的包可以下载并包含在您的项目中。默认的3D项目目前包含几个默认的包,可以在packages下的项目窗口中看到。
通过点击项目窗口右上角的按钮(这个按钮看起来像一只眼睛,里面有一条横线),这些包可以被隐藏起来。这纯粹是为了减少编辑器的视觉混乱,包仍然是项目的一部分。该按钮还显示有多少这样的包。
你可以通过包管理器控制哪些包被包含在您的项目中,包管理器可以通过 Window / Package Manager 菜单项打开。
这些包为Unity添加了额外的功能。例如,Visual Studio Editor为用于编写代码的Visual Studio编辑器添加集成。本教程不使用所包含的包的功能,所以我将它们全部删除。唯一的例外是Visual Studio Editor,因为我使用它来编写代码。
难道你不需要Visual Studio代码编辑器包吗? 尽管名称相似,但Visual Studio和Visual Studio Code是两个不同的编辑器。您只需要其中一个包,这取决于您使用的编辑器。
删除包最简单的方法是首先使用工具栏将包列表限制为 In Project only 。然后每次选择一个包,并使用窗口右下角的Remove按钮。Unity将在每次删除后重新编译,所以在这个过程结束前需要几秒钟。
在移除除了Visual Studio Editor之外的所有东西后,我在项目窗口中看到了三个包: Custom NUnit、Test Framework 和 Visual Studio Editor 。另外两个仍然存在,因为Visual Studio Editor依赖于它们。
你可以通过项目设置窗口让依赖项和隐式导入的包在包管理器中可见,打开 Edit / project settings,选择 Package Manager ,然后在 Advanced Settings 下启用 Show Dependencies 。
1.4 色彩空间
现在的渲染通常是在线性颜色空间中完成的,但Unity仍然默认使用伽马颜色空间。为了获得最佳的视觉效果,在 Project Settings 中选择 Player ,打开 Other Settings ,向下滚动到 Rendering 部分。确保颜色空间设置为 Linear 。Unity将显示警告,这可能需要很长时间,但对于一个几乎是空的项目来说并不算一个麻烦的事情。确认切换。
有理由使用伽马颜色空间吗? 只有当你使用旧硬件或旧图形api时才会如此。OpenGL ES 2.0和WebGL 1.0不支持线性空间,此外,在旧的移动设备上伽马可以比线性更快。
1.5 示例场景
这个新项目包含一个名为SamplesScene的示例场景,它是默认打开的。你可以在项目窗口的 Assets / Scenes 下找到它的asset。
默认情况下,项目窗口使用两列布局。你可以通过它的三点配置菜单选项切换到一列布局。
示例场景包含一个主摄像机和一个带有方向的光。这些是游戏对象。它们列在场景下面的层级窗口中。
你可以通过层级窗口或场景窗口选择游戏对象。相机有一个场景图标,看起来像一个老式的胶片相机,而方向光的图标看起来像太阳。
如何导航场景窗口? 您可以结合使用alt或option键和光标来旋转视图。您还可以使用方向键移动视角,并通过滚动进行缩放。此外,按下F键会使视图聚焦于当前选中的>游戏对象。还有更多的可能性,但这些已经足够找到你在场景中移动的方式了。
当一个对象被选中时,它的详细信息将显示在检查器窗口中,但我们将在需要时覆盖它们。我们不需要修改摄像机或灯光,所以我们可以通过点击层级窗口中它们左边的眼睛图标来隐藏它们。这个图标在默认情况下是不可见的,但是当我们将光标悬停在那里时,它就会出现。这纯粹是为了减少场景窗口中的视觉混乱。
眼睛旁边的像手一样的图标有什么作用? 在包含眼睛图标的列的旁边是另一个包含类似手的图标的列。默认情况下,这些图标也是不可见的。当游戏对象的手图标被点中时,不可能通过场景窗口选择该对象。这样你就可以通过场景窗口控制哪些对象响应选择。
2. 制作一个简单的时钟
现在我们的项目已经正确设置好了,我们可以开始创建我们的时钟了。
2.1 创建游戏对象
我们需要一个游戏对象来代表时钟。我们将从最简单的游戏对象开始,一个空的对象。它可以通过 GameObject / Create Empty 菜单选项创建。或者,您可以使用层次结构窗口的上下文菜单中的 Create Empty 选项。这将把游戏对象添加到场景中。在 SampleScene 下的层级窗口中,它是可见的,并立即被选中,现在它被标记为一个星号,表明它有未保存的更改。您也可以立即更改它的名称,或者稍后再修改。
一旦游戏对象被选中,检查器窗口就显示它的细节。在它的顶部是一个带有对象名称和一些配置选项的标题。默认情况下,该对象是启用的,不是 static 的,是 untagged 的,位于默认层。这些设置都很好,除了名称。重命名为 Clock 。
标题下方是游戏对象所有组件的列表。列表的顶部总是有一个 Transform 组件,这是我们的时钟当前拥有的全部。它控制游戏对象的位置、旋转和缩放。确保所有时钟的位置和旋转值都设置为0。它的缩放应该全部是1。
那么2D对象呢? 当在2D而不是3D中工作时,你可以忽略三个维度中的一个。专门用于2D类UI元素的对象—通常有一个RectTransform,这是一个专门的Transform组>件。
因为游戏对象是空的,所以在场景窗口本身是不可见的。然而,操作工具在游戏对象的位置是可见的,也就是世界的中心。
为什么我在选择时钟后没有看到操作工具? 操作工具存在于场景窗口中。确保你看到的是场景窗口,而不是游戏窗口。
可以通过编辑器工具栏左上角的按钮控制激活的操作工具。这些模式也可以通过Q、W、E、R、T和Y键来激活。组中最右边的按钮用于启用自定义编辑器工具,这是我们没有的。默认情况下,移动工具处于激活状态。
在模式按钮旁边还有三个按钮,用于控制操作工具的放置、方向和捕获。
2.2 创造时钟的表面
虽然我们有一个时钟对象,但我们还没有看到任何东西。我们要在它身上添加3D模型,这样一些东西会被渲染。Unity包含一些原始对象,我们可以用它们来构建一个简单的时钟。让我们通过 GameObject / 3D Object / cylinder 向场景添加一个圆柱体。确保它具有与我们的时钟相同的 Transform。
新对象比空的游戏对象多三个组件。首先,它有一个 MeshFilter ,它包含一个对内置圆柱体网格的引用。
第二个是 MeshRenderer 。这个组件的目的是确保对象的网格得到渲染。它还决定了渲染使用什么材质,现在是默认材质。该材质也显示在检查器中,组件列表下面。
第三个是CapsuleCollider ,它是用于处理3D物理的。对象代表一个圆柱体,但它有一个胶囊碰撞器,因为Unity没有原始的圆柱体碰撞器。我们不需要它,所以可以删除这个组件。如果你想在你的时钟上使用物理,你最好使用 MeshCollider 组件。组件可以通过右上角的三点下拉菜单删除。
我们将把圆柱体压扁,变成钟面。这是通过减少该物体缩放上的Y分量来实现的。把它减少到0.2。圆柱体网格为两个单位高,其有效高度为0.4单位。我们再做一个大钟,把它的X和Z分量增加到10。
我们的时钟应该是立着或挂在墙上的,但它现在是水平放置的。我们可以把圆筒旋转四分之一来解决这个问题。在Unity中,X轴向右,Y轴向上,Z轴向前。所以让我们以同样的方向来设计我们的时钟,这意味着当我们沿着Z轴看它时,我们可以看到它的正面。将圆柱体的X轴旋转设置为90,并调整场景视图,使时钟的正面可见,使移动工具的蓝色Z箭头指向远离你的地方,即屏幕里面。
将圆柱体对象的名称更改为 Face ,因为它代表时钟的面。它只是时钟的一部分,所以我们将它作为时钟对象的一个子对象。我们通过在层级窗口中拖拽表盘到时钟上来实现这一点。
子对象受制于父对象的转换。这意味着当 Clock 改变位置时,Face 也会改变位置。就好像它们是一个单一的实体。旋转和缩放也是如此。您可以因此来创建复杂的对象层次结构。
2.3 创建时钟外围
时钟表面的外圈通常有标记,帮助显示它正在显示的时间。这被称为时钟外围。让我们用方块来表示12小时钟的小时数。
通过 GameObject / 3D object / cube 添加一个立方体对象到场景中,将其命名为 Hour Indicator 12 ,并且也将其设置为Clock的子元素。层次结构中子对象的顺序并不重要,你可以把它放在 Face 的上面或下面。
设置它的 Scale X为0.5,Y为1,Z为0.1,因此它成为一个狭窄的扁平长块。然后设置 Position 其X为0,Y为4,Z为−0.25。把它放在 Face 的上面,表示12小时。同时删除它的 BoxCollider 组件。
指示剂很难看到,因为它时钟表面的颜色一样。让我们为它单独创建一个的材质,通过 Assets / create / material ,或者通过项目窗口的加号按钮或上下文菜单。这给了我们一个材质的资产,它是默认材料的副本。更改其名称为 Hour Indicator。
通过点击它的颜色域选择材质并改变它的反照率。这将打开一个颜色弹出窗口,提供各种选择颜色的方法。我选择深灰色,对应十六进制494949,与RGB 0-255模式下的73,73,73相同。我们不使用alpha通道,所以它的值是不相关的。我们也可以让所有其他的物质属性保持原样。
反照率是什么? 反照率是一个拉丁词,意思是白色。它是物体在白光照射下的颜色。
在 Hour Indicator 上使用这种材料。你可以通过在场景或层次窗口中拖动材质到物体上来实现这一点。当指示标志对象被选中时,你也可以把它拖到检查器窗口的底部,或者改变MeshRenderer的Materials数组的元素0。
2.4 12个Hour Indicator
我们可以用一个单一的指示标志来表示第12个小时,但让我们每个小时都包括一个指示标志。首先调整场景视角摄像机的方向,让我们沿着Z轴看。你可以通过点击场景视图右上角的视图相机小装置的圆锥来实现。你也可以通过网格工具栏按钮将场景网格的轴改为Z。
复制 Hour Indicator 12 游戏对象。您可以通过 Edit / Duplicate ,通过指示的键盘快捷方式,或通过其上下文菜单在层次结构窗口。副本将出现层次窗口中的原来的对象的下面,也是时钟的一个子对象。其名称为 Hour Indicator 12 (1) 。将其重命名为 Hour Indicator 6 ,并将其位置的Y分量取反,使其指示6小时。
用同样的方法创建第3小时和第9小时的标志。在这种情况下,它们的X应该是4和−4,而它们的Y应该是零。同时,将它们的Z轴旋转90°,这样它们就旋转了四分之一圈。
然后创建另一个 Hour Indicator 12 的副本,这次是第1小时。设置它的X位置为2,Y位置为3.464,Z旋转为−30°。然后重复第2小时,交换它的X和Y位置,并将Z旋转翻倍至−60°。
这些数字从何而来? 每小时沿Z轴顺时针旋转覆盖了30°。在这种情况下,我们使用负方向的旋转,因为Unity的旋转是逆时针的。我们可以通过三角函数找到1小时的位置。30°的正弦是1/2,余弦是√3/2, 我们用小时指示器离中心的距离,也就是4,来乘以那些三角函数值。所以最后得到x=2, y=2√3≈3.464。第二小时的旋转角度是60°,我们可以简单地交换正弦和余弦。
复制这两个指示器,并对它们的Y位置和旋转取相反数,创建第4和第5小时的指示标志。然后在第1、2、4和5小时使用相同的技巧来创建剩余的指示标志,这次取反它们的X位置和z轴的旋转角度。
2.3 创建时钟指针
下一步是制作时钟指针。我们从时针开始。再次复制 Hour Indicator 12,并命名为 Hours Arm 。然后创建一个指针的材质,并让指针使用它。在本例中,我使用纯黑色,十六进制的000000。减小手臂的X缩放为0.3,增加它的Y缩放为2.5。然后把它的Y位置改为0.75,这样它就指向12小时,但也有点反方向。这使得指针在旋转时看起来好像有一个小的配重。
指针必须绕着时钟的中心旋转,但是改变它的Z轴旋转,它就会绕着自己的中心旋转。
发生这种情况是因为旋转是相对于游戏对象的局部位置。为了创建正确的旋转,我们必须引入一个枢轴对象,并旋转该对象。因此,创建一个新的对象,并将其作为 Clock 的子对象。你可以通过层级窗口中的 Clock 上下文菜单直接创建这个对象。命名为 Hours Arm Pivot ,并确保其位置和旋转为零,其比例一致为1。然后让 Hours Arm 成为 pivot 的子节点。
现在试着旋转枢轴。如果你在场景视图中这样做,请确保工具手柄位置模式设置为 Pivot 而不是 Center 。
复制两次 Hours Arm Pivot 来创建 Minutes Arm Pivot 和 Seconds Arm Pivot 。相应地重命名它们,包括复制的指针子对象。
分针应该比时针更窄更长,所以将其X缩放设置为0.2,Y缩放设置为4,然后将其Y位置增加为1。也改变它的Z位置为-0.35,使它位于时针的顶部。注意,这适用于指针,而不是它的枢轴。
调整秒针。这次使用0.1和5作为XY缩放,使用1.25和−0.45作为YZ位置。
让我们通过单独创建一个的材料使秒针显眼。我给了它一个深红色,十六进制的B30000。此外,当制作完时钟时,我关掉了场景窗口中的网格。
如果你还没有这样做,这是一个保存场景的好时机,通过 File / Save 或指定的键盘快捷键。
保持项目资产的有序也是一个好主意。因为我们有三个材质,让我们把它们放在我们通过 Assets / create / folder 或通过项目窗口创建的 Materials 文件夹中,然后你可以把材质拖到那里。
3. 让时钟动起来
我们的钟现在是不报时的,总是停在12点。要使它动起来,我们必须给它添加一个自定义的行为。我们通过创建一个自定义组件类型来实现这一点,它是通过脚本定义的。
3.1 c#脚本
通过 Assets / Create / c# script 为项目添加一个新的脚本资产,并将其命名为Clock。C#是用于Unity脚本的编程语言,它的发音是 C-sharp 。让我们也立即将它放入一个新的Scripts文件夹,以保持项目的整洁。
当脚本被选中时,检查器将显示它的内容。但是要编辑代码,我们必须使用代码编辑器。您可以按 Open… 打开脚本进行编辑。按钮在其检查器或通过在层次结构窗口中双击它。打开哪个程序可以通过Unity的首选项来配置。
3.2 定义组件类型
在代码编辑器中加载脚本后,首先删除标准模板代码,因为我们将从头创建组件类型。
空文件不定义任何东西。它必须包含时钟组件的定义。我们将要定义的不是组件的单个实例。相反,我们定义了称为 Clock 的通用类或类型。一旦建立,我们可以在Unity中创建多个这样的组件,即使我们将在本教程中限制自己只使用单个时钟。
在C#中,我们定义 Clock 类型的方法是,首先声明我们正在定义一个类,然后是它的名称。在下面的代码片段中,更改后的代码有一个黄色的背景,如果你使用深色网页主题来查看本教程,则为暗红色。当我们从一个空文件开始时,它的内容实际上应该成为类Clock,而不是其他,尽管您可以根据自己的喜好在单词之间添加空格和换行。
class Clock
严格来说,什么是类? 您可以把类看作是一个蓝图,它可以用来创建驻留在计算机内存中的对象。蓝图定义了这些对象包含哪些数据以及它们具有哪些功能。
类还可以定义不属于对象实例,而是属于类本身的数据和功能。这通常用于提供全局可用的功能。我们会用一些,但 Clock 不会用。
因为我们不想限制哪些代码可以访问我们的 Clock 类型,所以最好在它前面加上访问修饰符public。
public class Clock
类的默认访问修饰符是什么? 如果没有访问修饰符,就好像我们编写了 internal class Clock 。这将限制对来自同一程序集的代码的访问,当您使用打包在不同程序集中的代码时,这将变得相关。为了确保它总是有效,在默认情况下将类设置为public。
此时,我们还没有有效的c#语法。如果你保存文件并回到Unity编辑器,那么编译错误将会记录在控制台窗口中。
我们指出我们正在定义一个类型,所以我们必须实际定义它是什么样子的。这是通过声明后面的代码块完成的。代码块的边界用花括号表示。我们现在让它为空,所以只写{}。
public class Clock {}
我们的代码现在是有效的。保存文件并切换回Unity。Unity编辑器将检测到脚本资产已经改变并触发重新编译。完成之后,选择我们的脚本。检查器会告诉我们资产不包含 MonoBehaviour 脚本。
这意味着我们不能使用这个脚本在Unity中创建组件。此时,我们的Clock定义了一个基本的C#对象类型。我们的自定义组件类型必须扩展Unity的 MonoBehaviour 类型,继承它的数据和功能。
MonoBehaviour是什么意思? 我们可以编写自己的组件,为游戏对象添加自定义行为,这就是 Behaviour 部分所指代的。它只是碰巧使用了英式拼写,这很奇怪。Mono部分的是在Unity中添加自定义代码的方式。它使用了Mono项目,这是一个.NET框架的多平台实现。因此,MonoBehaviour 这是一个古老的名字,我们因为向后兼容性而沿用它。
要将 Clock 转换为 MonoBehaviour 的子类型,我们必须更改类型声明,以便它扩展该类型,这需要在类型名称后加上冒号,然后再加上它扩展的对象。这使得 Clock 继承 MonoBehaviour 类类型的所有内容。
但是,这将导致编译后出现错误。编译器抱怨它找不到MonoBehaviour类型。这是因为类型包含在一个命名空间中,UnityEngine。要访问它,我们必须使用它的完全限定名称UnityEngine.MonoBehaviour。
public class Clock : UnityEngine.MonoBehaviour {}
名称空间是什么? 命名空间类似于网站域,但它用于代码。就像域名可以有子域名一样,命名空间也可以有子命名空间。最大的区别是它是反过来写的。所以不是forum.unity.com而是com.unity.forum。名称空间用于组织代码和防止名称冲突。
包含UnityEngine代码的程序集是Unity自带的,你不需要在线单独获取它。如果导入了适当的编辑器集成包,代码编辑器使用的项目文件应该自动设置以识别它。
当访问Unity类型时,总是必须包含UnityEngine前缀是不方便的。幸运的是,我们可以声明应该自动搜索命名空间,以完成C#文件中的类型名称。这是通过添加 using UnityEngine; 在文件的最上面。分号用于标记语句的结束。
using UnityEngine;
public class Clock : MonoBehaviour {}
现在我们可以将自定义组件添加到Unity中的 Clock 游戏对象中。这可以通过将脚本资产拖动到对象上,或者通过对象检查器底部的 Add Component 按钮来完成。
3.3 控制指针
为了旋转指针,Clock 对象需要了解它们。让我们从时针开始。像所有的游戏物体一样,它可以通过调整 Transform 组件来旋转。因此,我们必须将指针轴的 Transform 分量的信息添加到 Clock 中。这可以通过在其代码块中添加一个数据字段来实现,该数据字段定义为后跟一个分号的名称。
hours pivot 这个名字对这个字段来说是合适的。但是,名称必须是单个单词。约定是使字段名的第一个单词小写,其他所有单词大写,然后将它们粘在一起。所以我们把它命名为 hoursPivot。
public class Clock : MonoBehaviour {
hoursPivot;
}
using语句到哪里去了? 它还在,我只是没有展示出来。代码片段将包含足够的现有代码,以便你了解发生改变的地方的上下文。
我们还必须声明字段的类型,在本例中是UnityEngine.Transform。它必须写在字段名称的前面
Transform hoursPivot;
我们的类现在定义了一个字段,它可以持有对另一个对象的引用,该对象的类型必须为Transform。我们必须确保它持有时针轴的Transform分量的引用。
字段在默认情况下是私有的,这意味着它们只能由属于Clock的代码访问。但是类不知道我们的Unity场景,所以没有直接的方法将字段与正确的对象关联。我们可以通过将字段声明为可序列化来改变这一点。这意味着当Unity保存场景时,它应该包含在场景数据中,这是通过将所有数据放在一个序列中—序列化—并将其写入一个文件中来完成的。
将字段标记为可序列化是通过向其附加属性来完成的,在本例中是SerializeField。它写在字段声明的前面方括号之间,通常写在上面一行,但也可以放在同一行。
[SerializeField]
Transform hoursPivot;
我们不能用public修饰吗? 是的,但是让类字段可公开访问通常是一种糟糕的形式。经验法则是,只有当其他类型的c#代码需要访问时,才将类内容设置为公共的,并且选择的是方法或属性而不是字段。越不容易访问的东西越容易维护,因为直接依赖它的代码越少。在本教程中,我们唯一的c#代码是Clock,所以没有理由将其内容公开。
一旦这个字段是可序列化的,Unity将检测到它,并将其显示在我们的 Clock 对象的 Clock 组件的检查器窗口中。
要进行正确的连接,请从层次结构中拖动 Hours Arm Pivot 到 Hours Pivot 字段。或者,使用该字段右侧的圆形按钮,并在弹出的列表中搜索枢轴。在这两种情况下,Unity编辑器都抓取了 Hours Arm Pivot 的 Transform 组件,并在我们的字段中添加了一个引用。
3.4 了解所有三个指针
我们也要对分秒指针枢纽的转动做同样的计算。因此,将两个可序列化的Transform字段以适当的名称添加到Clock中。
[SerializeField]
Transform hoursPivot;
[SerializeField]
Transform minutesPivot;
[SerializeField]
Transform secondsPivot;
可以使这些字段声明更简洁,因为它们共享相同的属性、访问修饰符和类型。它们可以合并为一个逗号分隔的字段名称列表,位于属性和类型声明之后。
[SerializeField]
Transform hoursPivot, minutesPivot, secondsPivot;
//[SerializeField]
//Transform minutesPivot;
//[SerializeField]
//Transform secondsPivot;
//是做什么的? 双斜杠表示注释。它们之后的所有文本直到行尾都被编译器忽略。如果需要,它用于添加文本以解释代码。我还用它来表示已删除的代码。
将编辑器中的其他两个指针也连接起来。
3.5 唤醒时钟
现在我们能访问指针枢纽,下一步是旋转它们。为此,我们需要告诉 Clock 执行一些代码。这是通过向类添加一个代码块(称为方法)来实现的。块必须以名称作为前缀,名称按惯例大写。我们将其命名为 Awake ,表示当组件被唤醒时应该执行代码。
public class Clock : MonoBehaviour {
[SerializeField]
Transform hoursPivot, minutesPivot, secondsPivot;
Awake {}
}
方法有点像数学函数,例如f(x) = 2x + 3, 该函数接受一个由变量参数x表示的数字,加倍,然后加3。它只作用于一个数字,其结果也是一个数字。对于方法来说,,它更像是f ( p ) = c,p表示输入参数,c表示它执行的任何代码。
就像数学函数一样,方法可以产生结果,但这不是必需的。我们必须声明结果的类型—就像它是一个字段一样—或者写void来表示没有结果。在本例中,我们只想执行一些代码,而不提供结果值,因此使用void。
void Awake {}
我们也不需要任何输入数据。但是,我们仍然需要定义方法的参数,即圆括号之间用逗号分隔的列表。在我们的例子中,它只是一个空列表。
void Awake () {}
我们现在有了一个有效的方法,尽管它还没有做任何事情。就像Unity检测我们的字段一样,它也检测这个Awake方法。当一个组件有一个Awake方法时,Unity将在组件被唤醒时调用该方法。这发生在它在播放模式中被创建或加载之后。我们目前处于编辑模式,所以这还没有发生。
"Awake"不是必须公开吗? Awake和其他方法的集合被认为是特殊的Unity事件方法。Unity引擎会找到它们并在适当的时候调用它们,无论我们如何声明它们。这在受管理的. NET环境外部发生。
3.6 通过代码旋转
为了旋转指针,我们必须创造一个新的旋转。我们可以通过给Transform的localRotation属性赋值来改变Transform的旋转。
属性是什么? 属性是一种伪装为字段的方法。它可能是只读的,也可能是只写的。c#的惯例是将属性大写,但Unity的代码并没有这样做。
尽管在检查器中,Transform组件的旋转是用欧拉角(每个轴的角度)来定义的,但在代码中,我们必须用四元数来做。
四元数是什么? 四元数基于复数,用于表示3D旋转。虽然它们比X、Y和Z旋转角度的组合更难理解,但它们有一些有用的特性。例如,它们不受万向节锁的影响。
我们可以通过调用 Quaternion.Euler 方法来创建基于欧拉角的四元数。欧拉方法。在Awake中写入它,然后用分号结束语句来完成此操作。
void Awake () {
Quaternion.Euler;
}
该方法具有用于描述所需旋转的参数。在本例中,我们将提供一个逗号分隔的列表,其中包含三个参数,所有参数都在圆括号中,位于方法名之后。我们为X, Y和Z旋转提供三个数字。前两个使用0,Z轴旋转使用−30。
Quaternion.Euler(0, 0, -30);
这个调用的结果是一个四元数结构值,包含绕Z轴顺时针旋转30°,与时钟上的1小时匹配。
struct是什么? struct(structure的缩写)是一种蓝图,就像类一样。不同之处在于,无论它创建的是什么都被当作一个简单的值,如整数或颜色,而不是一个对象。定义自己的结构与定义类的工作原理相同,只是写的是struct而不是class。
要将这种旋转应用到时针,把Quaternion.Euler的结果复制给hoursPivots.localRotation,使用=复制语句。
hoursPivot.localRotation = Quaternion.Euler(0, 0, -30);
localRotation和rotation的区别是什么? localRotation属性表示Transform组件单独描述的旋转,因此它是相对于其父组件的旋转。这是你在检查器中看到的旋转。相反,考虑到整个对象层次结构,rotation属性表示世界空间的最终旋转。如果我们将时钟作为一个整体旋转,设置该属性将产生奇怪的结果,因为指针会忽略了对时钟的旋转的属性补偿。
难道不应该有hoursPivot从未初始化的警告吗? 编译器可以检测到没有任何代码给字段赋值,并且确实可以发出这样的警告,因为它不知道我们通过Unity的检查器设置了它。但是,这个警告在默认情况下是被抑制的。抑制可以通过项目设置来控制。在 Player / Other Settings / Script 下有一个 Suppress Common Warnings 选项。它抑制了关于未初始化和未使用私有字段的警告。
现在在编辑器中进入播放模式。你可以通过 Edit / Play,键盘快捷键,或者按下编辑器窗口顶部中心的播放按钮。Unity将焦点转移到游戏窗口,这将呈现场景中的 Main Camera 所看到的内容。时钟组件将被唤醒,时钟将被设置为1点钟。
如果镜头没有聚焦在时钟上,你可以移动它,让时钟变得可见,但要记住,当退出游戏模式时场景会重置,所以你在游戏模式下对场景所做的任何更改都不会持续。但是对于资产却不是这样,对它们的更改总是会持续存在。你也可以在游戏模式中打开场景窗口,甚至多个场景和游戏窗口。在继续之前退出播放模式。
3.7 获取当前时间
下一步是计算出我们被唤醒的当前时间。我们可以使用DateTime结构体访问正在运行的设备的系统时间。DateTime不是一个Unity类型,它在System命名空间中找到。它是.NET框架核心功能的一部分,Unity就是用它来支持脚本的。
DateTime具有一个Now属性,该属性生成一个DateTime值,其中包含当前系统日期和时间。为了检查它是否正确,我们将在Awake开始时将其记录到控制台。我们可以通过将它传递给Debug.Log方法来做到这一点。
using System;
using UnityEngine;
public class Clock : MonoBehaviour {
[SerializeField]
Transform hoursPivot, minutesPivot, secondsPivot;
void Awake () {
Debug.Log(DateTime.Now);
hoursPivot.localRotation = Quaternion.Euler(0, 0, -30);
}
}
现在,每当我们进入游戏模式时,我们都会得到一个时间戳记录。您可以在控制台窗口和编辑器窗口底部的状态栏中看到它。
3.8 旋转指针
我们离一个工作的时钟很接近了。让我们再次从hour开始。DateTime有一个Hour属性,用于获取DateTime值的小时部分。在当前时间戳上调用它将得到一天中的小时。
Debug.Log(DateTime.Now.Hour);
为了让时针显示当前的小时我们需要将-30°旋转乘以当前的小时。乘法是用星号*字符完成的。我们也不再需要记录当前时间,因此可以摆脱该语句。
//Debug.Log(DateTime.Now.Hour);
hoursPivot.localRotation = Quaternion.Euler(0, 0, -30 * DateTime.Now.Hour);
为了清楚地说明从小时到度的转换,可以定义包含转换因子的hoursToDegrees字段。Quaternion.Euler的角度被定义为浮点值,所以我们将使用float类型。因为我们已经知道了这个数字,所以可以将它作为字段声明的一部分立即赋值。然后在Awake中代替-30字面值与字段相乘。
float hoursToDegrees = -30;
[SerializeField]
Transform hoursPivot, minutesPivot, secondsPivot;
void Awake () {
hoursPivot.localRotation =
Quaternion.Euler(0, 0, hoursToDegrees * DateTime.Now.Hour);
}
浮点数是什么? 计算机不能存储所有的数字,它们必须能在二进制存储器中表示,二进制存储器由0或1组成。这使得无法在有限的内存大小中精确地存储许多数字,例如1/3,就像我们无法将该数字精确地写入十进制表示法一样。我们能做的最好的是写入0.3333333,然后在某个点停止。
假设我们决定在点后面最多写3位数字,在点前面只写1位。那么1/3约等于0.333。如果我们用1/3除以100,那么我们就不得不写成0.003,这意味着
我们失去了两位数的精确度.为了提高小值的精度,让我们添加一个单独的指数,表示我们的数字的量级。那么0.333×10(−2)可以表示1/3除以100,并且不会丢失有意义的数字。我们可以用0.333×102也可以表示与100的乘法,同时在点的前面只保留一个数字。因此,点可以被认为是浮动的,因为它不指定一个固定的数量级。这使得我们可以只用几个数字来表示大量的数字。浮点数与计算机的工作方式相同,只是它们使用二进制而不是十进制数字,而且还必须表示特殊的值,如无穷大和非数字。浮点数是一个存储在4个字节中的值,这意味着它有32位。
如果我们声明一个没有后缀的整数,那么它就被认为是一个整数,这是一种不同的值类型。尽管编译器会自动转换它们,但让我们通过添加f后缀来显式地将所有数字都指定为float类型。
float hoursToDegrees = -30f;
[SerializeField]
Transform hoursPivot, minutesPivot, secondsPivot;
void Awake () {
hoursPivot.localRotation =
Quaternion.Euler(0f, 0f, hoursToDegrees * DateTime.Now.Hour);
}
每小时旋转的角度总是相同的。我们可以通过在hoursToDegrees的声明中添加const前缀来实现这一点。这就把它变成了常数,而不是一个字段。
const值有什么特别之处?
const关键字表示值永远不会改变,不需要是字段。相反,它的值将在编译期间计算,并替换该常量的所有使用。这只适用于像数字这样的基本类型。
让我们使用DateTime相应的属性对其他两个指针进行相同的处理。一分和一秒都用旋转负六度来表示。
const float hoursToDegrees = -30f, minutesToDegrees = -6f, secondsToDegrees = -6f;
[SerializeField]
Transform hoursPivot, minutesPivot, secondsPivot;
void Awake () {
hoursPivot.localRotation =
Quaternion.Euler(0f, 0f, hoursToDegrees * DateTime.Now.Hour);
minutesPivot.localRotation =
Quaternion.Euler(0f, 0f, minutesToDegrees * DateTime.Now.Minute);
secondsPivot.localRotation =
Quaternion.Euler(0f, 0f, secondsToDegrees * DateTime.Now.Second);
}
我们使用DateTime.Now三次来检索小时,分钟和秒。每次我们再查找这个属性,就需要一些工作量,而且理论上可能会得到不同的时间值。为了确保不会发生这种情况,我们应该只检索一次时间。我们可以这样做:在方法内部声明一个变量,并为它分配时间,然后使用这个值。我们就叫它 Time 吧。
变量是什么? 变量的作用类似于字段,只是它只在方法执行时才存在。它属于方法,而不是类。
void Awake () {
DateTime time = DateTime.Now;
hoursPivot.localRotation =
Quaternion.Euler(0f, 0f, hoursToDegrees * time.Hour);
minutesPivot.localRotation =
Quaternion.Euler(0f, 0f, minutesToDegrees * time.Minute);
secondsPivot.localRotation =
Quaternion.Euler(0f, 0f, secondsToDegrees * time.Second);
}
对于变量,可以省略类型声明,用var关键字替换它。这可以缩短代码,但只有当变量的类型可以从声明时赋给它的值推断出来时才有可能。另外,我更喜欢只在语句中显式提到类型时才这样做,这里就是这种情况。
var time = DateTime.Now;
3.9 让指针动起来
我们在进入游戏模式时获得当前时间,但之后时钟保持静止。为了保持时钟与当前时间同步,将Awake方法的名称更改为Update。这是Unity每一帧调用的另一个特殊事件方法,而不是只调用一次,只要我们停留在游戏模式中。
void Update () {
var time = DateTime.Now;
hoursPivot.localRotation =
Quaternion.Euler(0f, 0f, hoursToDegrees * time.Hour);
minutesPivot.localRotation =
Quaternion.Euler(0f, 0f, minutesToDegrees * time.Minute);
secondsPivot.localRotation =
Quaternion.Euler(0f, 0f, secondsToDegrees * time.Second);
}
什么是帧? 在游戏模式中,Unity不断从主摄像机的角度渲染场景。渲染完成后,结果将显示在显示器上。然后显示器将显示这一帧,直到它得到下一帧。在渲染新帧之前,所有内容都会被更新。所以Unity经历了一系列的更新,渲染,更新,渲染等等。在渲染场景之后的一个更新步骤通常被认为是一个单独的帧,尽管在现实中,时间是更复杂的。
请注意,我们的Clock组件在检查器中它的名字前面获得了一个切换。这允许我们禁用它,这可以防止Unity调用它的Update方法。
3.10 连续旋转
我们的时钟指针准确地显示当前的小时、分钟或秒。它的行为就像一个数字时钟,离散但有指针。典型的时钟有缓慢旋转的指针,提供时间的模拟表示。让我们改变一下方法,让我们的时钟变成模拟时钟。
DateTime不包含小数数据。幸运的是,它有一个TimeOfDay属性。这为我们提供了一个TimeSpan值,该值通过其TotalHours、TotalMinutes和TotalSeconds属性,以我们需要的格式包含数据。
首先从DateTime.Now获取TimeOfDay结构体的值。现在把它存储在变量中。由于TimeSpan类型在这条语句中没有提到,我将显式地设置变量的类型。然后调整我们用来旋转指针的属性
void Update () {
TimeSpan time = DateTime.Now.TimeOfDay;
hoursPivot.localRotation =
Quaternion.Euler(0f, 0f, hoursToDegrees * time.TotalHours);
minutesPivot.localRotation =
Quaternion.Euler(0f, 0f, minutesToDegrees * time.TotalMinutes);
secondsPivot.localRotation =
Quaternion.Euler(0f, 0f, secondsToDegrees * time.TotalSeconds);
}
这将导致编译器错误,编译器会解释不能将double转换为float。这是因为TimeSpan属性产生的值具有双精度浮点类型,即double。这些值提供了比浮点值更高的精度,但Unity的代码只适用于单精度浮点值。
单精度够吗? 对于大多数游戏来说,是的。当处理非常大的距离或尺度差异时,这就会成为一个问题。然后你将不得不应用像传送或相机相关渲染的技巧来保持世界原点附近的活跃区域。虽然使用双精度可以解决这个问题,但它也会使所涉及的数字的内存大小增加一倍,从而导致其他性能问题。游戏引擎通常使用单精度浮点值,gpu也是如此。
我们可以通过显式地将double转换为float来解决这个问题。这个过程称为类型转换,通过在要转换的值前的圆括号中写入新类型来完成。
hoursPivot.localRotation =
Quaternion.Euler(0f, 0f, hoursToDegrees * (float)time.TotalHours);
minutesPivot.localRotation =
Quaternion.Euler(0f, 0f, minutesToDegrees * (float)time.TotalMinutes);
secondsPivot.localRotation =
Quaternion.Euler(0f, 0f, secondsToDegrees * (float)time.TotalSeconds);
现在你知道了在Unity中创建对象和编写代码的基本原理。下一个教程是构建图形。