博主最近在學習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創建對象和腳本的基礎知識。