时钟

2022-05-11 16:25:07 浏览数 (1)

(前记:网上无意间翻到了这一系列文章,真心觉得不错,对于Unity初学者应该是非常有助益的(譬如我:)),顺手翻译了第一篇,也算是一次小小的整理和复习,虽然原文中的有些描述略显琐碎,但就总体而言也可谓细致入微,文后的“QA”也很不错,有兴趣的朋友可以仔细看看 :)PS 第一次做翻译,生硬不当之处甚多,见谅,如能不吝纠正指出,大好,拜谢先~ :))

Clock 时钟

a simple time display 一个简单的时间显示

Introduction 简介

In this tutorial we'll write a small C# script to animate the arms of a very simple clock. You'll learn to 

在本篇教程中,我们会编写一个“小巧”的C#脚本来让一个简易时钟的指针运动起来。你将会从中学到:

· create an object hierarchy;

· 创建对象层次

· create a script and attach it to an object;

· 创建一个脚本并将它依附到一个对象上

· access namespaces;

· 访问命名空间

· use the Update method;

· 使用Update方法

· rotate stuff based on time.

· 基于时间来旋转物体

You're assumed to already have a basic understanding of Unity's editor. If you've played with it for a few minutes you're good to go.

在教程开始之前,我们假定你对Unity编辑器已经有了基本了解,如果你已经使用了一段时间的编辑器,那么就再好不过了 :)

Creating the clock 创建时钟

We start by creating a new Unity project without any packages. The default scene contains a camera positioned at (0, 1, -10) looking down the Z axis. To get a similar perspective as the camera in the scene view, select the camera and perform GameObject / Align View to Selected from the menu. 

首先,我们新建一个不带有任何包(package)的Unity工程。接着在初始的场景中我们放置一个位于(0,1,-10)并且面向z轴的摄像机。为了使场景拥有一个类似于摄像机镜头的透视视图,我们选定摄像机(物体),然后执行菜单选项 GameObject / Align View to Selected。

We need an object structure to represent the clock. Create a new empty GameObject via GameObject / Create Empty and call it Clock. Create three empty child objects for it and call them Hours, Minutes, and Seconds. Make sure they are all positioned at (0, 0, 0).

我们还需要一个用来代表时钟的对象结构。通过菜单栏中的GameObject / Create Empty 功能创建一个空的GameObject ,并且将其命名为Clock。接着为这个Clock创建三个空的子物体,分别命名为Hours, Minutes, 和 Seconds。确保他们都位于(0,0,0)的位置。

We'll use simple boxes to visualize the arms of the clock. Create a child cube for each arm via GameObject / Create Other / Cube. Give the cube for Hours position (0, 1, 0) and scale (0.5, 2, 0.5). For the minutes cube it's position (0, 1.5, 0) and scale (0.25, 3, 0.25). For seconds cube it's (0, 2, 0) and (0.1, 4, 0.1).

我们将使用简单的箱体(box)来表现时钟的指针。通过菜单栏中的GameObject / Create Other / Cube功能,我们为每一个钟表指针(也就是上面所创建的Hours, Minutes, 和 Seconds)分别创建一个子立方体(cube)。Hours cube的位置设为(0,1,0),比例设为(0.5,2,0.5);minutes cube的位置设为(0,1.5,0),比例设为(0.25,3,0.25);seconds cube的位置设为(0,2,0),比例设为(0.1,4,0.1)

Animating the clock 让时钟动起来

We need a script to animate the clock. Create a new C# script via Create / C# Script in the Project view and name it ClockAnimator. Open the script and empty it so we can start fresh. 

我们仍然需要一个脚本来让时钟动起来。通过工程视图里的Create / C# Script功能,我们新建一个C#脚本文件。为了能够从头开始编写脚本,我们打开新建的脚本并清空其中的内容。

First, we indicate that we want to use stuff from the UnityEngine namespace. Then we declare the existence of the ClockAnimator. We describe it as a publicly available class that inherits from MonoBehaviour.

首先,我们声明我们需要使用命名空间UnityEngine 中的内容,接着我们声明类ClockAnimator.我们将其定义为一个继承于MonoBehaviour的公有类型。

This gives us a minimal class that can be used to create components. Save it, then attach it to the the Clock object by dragging from the Project to the Hierarchy view.

这样我们就有了一个可以用于创建组件(component)的最小类。保存脚本,并将其从工程视图拖拽至层级视图(Hierarchy view)的Clock对象之上,以使其依附于Clock对象之上。

代码语言:javascript复制
using UnityEngine;
public class ClockAnimator : MonoBehaviour {
}

To animate the arms, we need access to their Transform components first. Add a public Transform variable for each arm to the script, then save it. These public variables will become component properties which you can assign object to in the editor. The editor will then grab the Transform components of these objects and assign them to our variables. Select the Clock object, then drag the corresponding objects to the new properties.

为了能让钟表指针运动,我们首先需要取得他们的Transform 组件。在脚本中,我们为每一个钟表指针添加一个Transform 类型的公有变量然后保存。这些公有变量将会成为组件中的属性,这样你就可以在编辑器中使用对象对他们进行赋值,编辑器将会自动获取这些赋值对象的Transform 组件并将他们赋给这些变量。在此我们选定Clock对象,然后将相应的对象(指之前的Hours, Minutes, 和 Seconds)物体拖拽到新增加的属性之上。

代码语言:javascript复制
using UnityEngine;
public class ClockAnimator : MonoBehaviour {
 public Transform hours, minutes, seconds;
}

Next, we'll add an update method to the script. This is a special method that will be called once every frame. We'll use it to set the rotation of the clock arms. 

接着,我们在脚本中添加一个名为Update成员方法,这个方法比较特殊,其每帧都会被调用一次。我们使用他来设置时钟指针的旋转。

After saving the script, the editor will notice that our component has an update method and will show a checkbox that allows us to disable it. Of course we keep it enabled.

保存脚本,编辑器会自动检测到我们的组件(指ClockAnimator)新增了一个Update方法,并且会加显一个可以让我们关闭组件的复选框。当然,我们现在不会关闭组件。

代码语言:javascript复制
using UnityEngine;
public class ClockAnimator : MonoBehaviour {
 public Transform hours, minutes, seconds;
 void Update() {
        // currently do nothing
    }
}

Each hour, the Hours arm has to rotate 360/12 degrees. The Minutes arm has to rotate 360/60 degrees per minute. Finally, the Seconds arm has to rotate 360/60 degrees every second. Let's define these values as private constant floating-point values for convenience.

每一小时,Hours 指针需要旋转 360/12 角度;每一分钟,Minutes 指针需要旋转360/60 角度;最后,Seconds 指针每一秒都需要旋转 360/60 角度。为了方便,我们将这些数值定义为私有的浮点数常量。

代码语言:javascript复制
using UnityEngine;
public class ClockAnimator : MonoBehaviour {
 private const float
        hoursToDegrees = 360f / 12f,
        minutesToDegrees = 360f / 60f,
        secondsToDegrees = 360f / 60f;
 public Transform hours, minutes, seconds;
 void Update() {
        // currently do nothing
    }
}

Each update we need to know the current time to get this thing to work. The System namespace contains the DateTime struct, which is suited for this job. It has a property called Now that always corresponds with the current time. Each update we need to grab it and store it in a temporary variable.

每一次Update,我们都需要知道当前的时间。System 命名空间中包含有名为DateTime 的结构体,使用它我们就可以获取时间。该结构体有一个叫做Now 的属性,这个属性便对应于当前程序时间。每一帧,我们都需要获取这个属性并将它暂存起来。

代码语言:javascript复制
using UnityEngine;
using System;
public class ClockAnimator : MonoBehaviour {
 private const float
        hoursToDegrees = 360f / 12f,
        minutesToDegrees = 360f / 60f,
        secondsToDegrees = 360f / 60f;
 public Transform hours, minutes, seconds;
 void Update() {
 DateTime time = DateTime.Now;
    }
}

To get the arms to rotate, we need to change their local rotation. We do this by directly setting the localRotation of the arms, using quaternions. Quaternion has a nice method we can use to define an arbitrary rotation. 

为了能使指针旋转,我们需要改变他们的局部旋转数值。我们可以通过使用四元数(quaternion)来设置指针的localRotation 变量来达到这个目的。Quaternion 有一个非常好用的方法,通过他我们可以定义出任意的一个旋转。

Because we're looking down the Z axis and Unity uses a lefthanded coordinate system, the rotation must be negative.

由于我们现在摄像机面向Z轴,而Unity本身又使用左手坐标系,所以旋转值必须为负数(才是顺时针旋转方向)。

代码语言:javascript复制
using UnityEngine;
using System;
public class ClockAnimator : MonoBehaviour {
 private const float
        hoursToDegrees = 360f / 12f,
        minutesToDegrees = 360f / 60f,
        secondsToDegrees = 360f / 60f;
 public Transform hours, minutes, seconds;
 void Update() {
 DateTime time = DateTime.Now;
        hours.localRotation = Quaternion.Euler(0f, 0f, time.Hour * -hoursToDegrees);
        minutes.localRotation = Quaternion.Euler(0f, 0f, time.Minute * -minutesToDegrees);
        seconds.localRotation = Quaternion.Euler(0f, 0f, time.Second * -secondsToDegrees);
    }
}

Improving the clock 改善现有的时钟

This works! When in play mode, our clock shows the current time. However, it behaves much like a digital clock as it only shows discrete steps. Let's include an option to show analog time as well. Add a public boolean variable analog to the script and use it to determine what to do in the update method. We can toggle this value in the editor, even when in play mode.

上面的示例是可行的,在(编辑器的)运行模式下,我们的时钟可以显示当前时间,但是,由于只能显示不连续的走步,他看上去就好像一个数字时钟。让我们添加一个选项以支持模拟时间显示:在脚本中新增一个analog 布尔变量以标记在Update方法中到底运行哪些逻辑。我们可以在编辑器中甚至于运行模式下动态的开关这个数值。

代码语言:javascript复制
using UnityEngine;
using System;
public class ClockAnimator : MonoBehaviour {
 private const float
        hoursToDegrees = 360f / 12f,
        minutesToDegrees = 360f / 60f,
        secondsToDegrees = 360f / 60f;
 public Transform hours, minutes, seconds;
 public bool analog;
 void Update() {
 if (analog) {
            // currently do nothing
        }
 else {
 DateTime time = DateTime.Now;
            hours.localRotation = Quaternion.Euler(0f, 0f, time.Hour * -hoursToDegrees);
            minutes.localRotation = Quaternion.Euler(0f, 0f, time.Minute * -minutesToDegrees);
            seconds.localRotation = Quaternion.Euler(0f, 0f, time.Second * -secondsToDegrees);
        }
    }
}

For the analog option we need a slightly different approach. Instead of DateTime.Now we'll use DateTime.Now.TimeOfDay, which is a TimeSpan. This allows us easy access to the fractional elapsed hours, minutes, and seconds. Because these values are provided as doubles – double precision floating-point values – we need to cast them to floats.

为了支持analog选项,我们使用了一个与之前稍有不同的方法。我们使用类型为TimeSpan的DateTime.Now.TimeOfDay来代替之前使用的DateTime.Now。通过它我们可以简单的获取到小数形式的小时、分钟以及秒数。由于这些数值都是双精度类型(双倍精度浮点数),我们需要首先将他们转化为浮点数类型。

代码语言:javascript复制
using UnityEngine;
using System;
public class ClockAnimator : MonoBehaviour {
 private const float
        hoursToDegrees = 360f / 12f,
        minutesToDegrees = 360f / 60f,
        secondsToDegrees = 360f / 60f;
 public Transform hours, minutes, seconds;
 public bool analog;
 void Update() {
 if (analog) {
 TimeSpan timespan = DateTime.Now.TimeOfDay;
            hours.localRotation =
 Quaternion.Euler(0f,0f,(float)timespan.TotalHours * -hoursToDegrees);
            minutes.localRotation =
 Quaternion.Euler(0f,0f,(float)timespan.TotalMinutes * -minutesToDegrees);
            seconds.localRotation =
 Quaternion.Euler(0f,0f,(float)timespan.TotalSeconds * -secondsToDegrees);
        }
 else {
 DateTime time = DateTime.Now;
            hours.localRotation = Quaternion.Euler(0f, 0f, time.Hour * -hoursToDegrees);
            minutes.localRotation = Quaternion.Euler(0f, 0f, time.Minute * -minutesToDegrees);
            seconds.localRotation = Quaternion.Euler(0f, 0f, time.Second * -secondsToDegrees);
        }
    }
}
Now our clock works analog too!

现在,我们的时钟也可以显示模拟时间了 :)

Downloads 下载

clock.zip

The finished project. 最终的工程文件

Questions & Answers 问题&解答

What's a GameObject? 什么是GameObject

Basically, everything that's placed in a scene is a GameObject. It has a name, tag, layer, and Transform component, which you can modify in the editor or in a script. By itself it doesn't do anything, it's just an empty container. You can turn it into something useful by attaching components to it and by putting other objects inside it.

基本上,任何放置于场景中的物体都是GameObject。他拥有一个name、一个tag、一个layer以及一个Transform组件,这些你都可以在编辑器或是脚本中进行修改。GameObject本身并不做任何事情,仅是一个空的容器而已。你可以在其上依附组件或者添加其他物体来使他变得“有些用处”。

What's a child object? 什么是子物体

If you put an object inside another (by dragging in the Hierarchy view) then it's considered the child of the object that now contains it. The transformation of the parent is inherited by its children and is applied first. So it the child's transform's position is set to (10, 0, 0) while the parent's is (2, 1, 0), then the child will end up at (12, 1, 0). But if the parent's transform's rotation is set to (0, 0, 90) as well, the child effectively orbits its parent and ends up rotated (0, 0, 90) at position (2, 11, 0). Scale is inherited in the same fashion.

如果你将一个物体置于另外一个物体之中(通过层次视图中的拖拽),那么这个物体也就变为了另一个物体的子物体。子物体会继承父物体的变换(transformation)并受其影响。所以,如果子物体的变换位置是(10,0,0),而父物体的位置是(2,1,0),那么,子物体的最终位置将会是(12,1,0);但是如果将父物体的旋转设置为(0,0,90),那么子物体首先会围绕父物体旋转(绕Z轴正转90度,从(10,0,0)旋转为(0,10,0)),并且最终位置将为(2,11,0);比例变换类似。

What's a namespace? 什么是命名空间

It's like a website domain, but for code stuff. For example, MonoBehaviour is defined inside the UnityEngine namespace, so it can be addressed with UnityEngine.MonoBehaviour. 

Just like domains, namespaces can be nested. The big difference is that it's written the other way around. So instead of forum.unity3d.com it would be com.unity3d.forum. For example, the ArrayList type exists inside the Collections namespace, which in turn exists inside the System namespace. So you need to write System.Collections.ArrayList to get hold of it.

命名空间好似网站域名,但他是用于服务代码的。例如,MonoBehaviour 定义于UnityEngine 命名空间之下,所以我们可以使用UnityEngine.MonoBehaviour这种方式来对其进行定位。就像域名一样,命名空间也可以嵌套,但他与域名最大的不同在于相反的书写方式:如果域名为forum.unity3d.com的话,那么相应的命名空间就应该为com.unity3d.forum。例如,ArrayList 类型位于Collections 命名空间之下,而Collections 又位于System 命名空间之下,那么我们便需要使用System.Collections.ArrayList这种方式来定位它。

What does using do? using用来做什么

By declaring that we're using a namespace, we don't need to write it again each time we want to use something from it. So, when using UnityEngine, instead of having to write UnityEngine.MonoBehaviour we can suffice with MonoBehaviour.

用以声明我们正在使用某个命名空间,声明之后,每次在使用该命名空间时,我们就不需要再次编写该命名空间的名字。譬如我们声明了 using UnityEngine,那么仅仅通过MonoBehaviour这个名字便可以使用UnityEngine.MonoBehaviour这个类型。

What's a class? 什么是类

A class is a blueprint that can be used to create objects that reside in your computer's memory. The blueprint defines what data these objects contain and how they behave.

一个类就是一幅蓝图,依此我们可以在电脑内存中创建对象。这幅蓝图定义了这些对象所包含的数据以及所表现的行为。

What's aMonoBehaviour? 什么是MonoBehaviour

MonoBehaviouris a class from the UnityEngine namespace. If you create a class that you want to use as a Unity component, then it should inherit from MonoBehaviour. It contains a lot of useful stuff and makes things like Update work.

MonoBehaviouri是UnityEngine 命名空间中的一个类型。如果你想创建一个Unity组件功能的类型,那么你就应该让你的类型继承于他。这个类型包含了非常多有用的内容并且可以使Update这类的方法正常工作。

What's a variable? 什么是变量

A variable is value that can be changed. This can be either a reference to an object or something simple like an integer. A variable must be of a specific type, which is written before its name when defined. 

一个变量就是一个可以改变的量。他可以是一个对象的引用或者是简单的一个整数。变量必须是一个确定的类型,在变量定义时,变量类型编写在变量名之前。

Variables are accessible only inside the scope that their are defined. By default, if a variable is defined inside a class, every object instance of that class has its own version of that variable. However, it can be marked as static, in which case it exists once for that class, independent of any objects. If a variable is defined inside a method, it only exists while that method is being invoked.

变量只在定义域之内可以访问。一般的,如果一个变量定义在类中,那么每个该类的对象实例都有一份该变量的副本。但是,如果变量被标记为静态的,那么该变量在类中便仅存在一份拷贝,并且独立于其他任何对象实例。如果变量定义在方法中,那么只有在方法被调用时该变量才会存在。

What's a method? 什么是方法

A method is a chunk of behavior, which is defined in a class. It can accept input and produce an output. Input is defined and provided after the method name, in parentheses, even if there is none. The method's type is that of its output. If there's no output this is indicated with void. 

一个方法就是定义于类中的一些行为。他可以接受输入并产生输出。输入在方法名之后的括号中进行定义和提供,即便没有输入也是如此。方法的类型就是他的输出,没有输出则代表方法类型为void。

By default, methods define behavior of objects, but it's possible to define behavior that doesn't need an object to function. Such methods are marked as static.

一般情况下,方法都用于定义对象的行为,但是我们也可以定义出不依赖于某个对象的方法。这些方法都标记为static。

What's special about const? const有什么特别

The const keyword indicates that a value will never change and needn't be variable. Its value will be computed during compilation and is directly inserted wherever it's referenced. 

const 关键字用于指示某个数值不会改变并且也不需要作为变量。这些数值会在编译时进行计算并且直接内嵌到其被引用的位置。

By the way, the compiler will precompute any constant expression, so writing (1   1) and simply writing 2 will both result in the same program.

另外值得一提的是,编译器会预编译任何常量表达式,所以编写(1   1)这种表达式和编写2这个简单数值,两者的编译结果是一致的。

What's a struct? 什么是结构体

A struct is a blueprint, just like a class. The difference is that whatever it creates is treated as a simple value, like an integer, instead of an object. It doesn't have the same sense of identity as an object.

结构体和类一致,也可以认为是一幅蓝图。区别在于,结构体被看做是一种类似整数的简单数值,而不是一个对象。结构并没有如对象一般的各类特性。

What's a property? 什么是属性

A property is a method that pretends to be a variable. It might be readonly or writeonly.

属性就是一个代表变量的方法。他可以是readonly 或者readonly 。

What's a quaternion? 什么是四元数

Quaternions are based on complex numbers and are used to represent 3D rotations. While harder to understand than simple 3D vectors, they have some useful characteristics. The UnityEngine namespace contains the Quaternion struct.

四元数基于复数理论并且常常用来表示3D旋转。虽然相较简单的3D向量而言,四元数比较难于理解,但是同时四元数也有很多非常有用的特性。UnityEngine 命名空间中包含有代表四元数的Quaternion 结构体。

Why not use rotation? 为什么不使用rotation(相较localRotation而言)

localRotationrefers to the actual rotation of aTransform, independent of the rotation of its parent. So if we were to rotate Clock, its arms would rotate along with it, as expected. 

localRotation表示一个Transform的局部旋转(独立于父对象)。所以如果我们旋转时钟本身,那么时钟指针也会如预想一样跟着一起旋转。

rotation refers to the final rotation of a Transform as it is observed, taking the rotation of its parent into account. The arms would not adjust when we rotate Clock, as its rotation will be compensated.

而rotation 则代表一个Transform 的最终旋转(就是最终被看到的样子),他会将父对象的旋转也纳入考虑。如果我们使用rotation来设置指针旋转,那么当我们旋转时钟本身的时候,时钟指针会因为父子变换之间的补偿而不会产生相应的旋转。

What's casting? 什么是强制转化

Casting changes the type of a value. You indicate this by writing the type within parentheses before the value. Casting simple values results in a convertion, like converting a floating-point value to an integer by discarding the fractional part. 

强制转换会改变一个数值的类型。其书写的方式就是在数值前面加上写有变换类型的括号。强制转化简单类型(意思好像是值类型)其实就是进行数值转化,譬如将一个浮点数转换为一个整数就是将其小数部分直接去除。

Casting an object reference to another type doesn't convert the object, it only changes how that reference is treated. You'd only do this when you're sure about the object's type.

强制转换对象引用类型并不会改变对象,而只会改变引用的解释方式。你应当只有在确认对象类型正确时才进行这种转换。

0 人点赞