
前言 很多童鞋沒有系統的Unity3D遊戲開發基礎,也不知道從何開始學。 為此我們精選了一套國外優秀的Unity3D遊戲開發教程,翻譯整理後放送給大家,教您從零開始一步一步掌握Unity3D遊戲開發。 本文不是廣告,不是推廣,是免費的純幹貨! 本文全名:喵的Unity遊戲開發之路 - 對象管理 - 各色對象各色對象制作形狀
- 建立形狀的工廠。
- 儲存和加載形狀辨別符。
- 支援多種材質和随機顔色。
- 啟用GPU執行個體化。
這是有關對象管理的系列教程中的第二篇。在這一部分中,我們将添加對具有不同材質和顔色的多種形狀的支援,同時保持與遊戲先前版本的向後相容性。
本教程使用Unity 2017.4.1f1制作。
這些多元資料集在遊戲終止後幸存下來。
形狀工廠
本教程的目的是通過允許建立除白色立方體之外的其他形狀,使我們的遊戲更加有趣。就像位置,旋轉和比例一樣,每次播放器生成新形狀時,我們都會随機選擇建立的形狀。
形狀等級
我們将具體說明我們的遊戲産生什麼樣的東西。它生成形狀,而不是通用的可持久對象。是以,建立一個新
Shape
類,該類代表3D幾何形狀。它隻是擴充了
PersistableObject
而沒有添加任何新東西,至少目前是這樣。
從多元資料集預制件上删除元件PersistableObject,然後給它一個
Shape
元件。它不能同時具有兩者,因為我們賦予
PersistableObject
了
DisallowMultipleComponent
屬性,該屬性也适用于
Shape
。
具有Shape元件的多元資料集。
這打破了Game對象對預制件的引用。但是因為
Shape
也是PersistableObject,我們可以再次配置設定它。
再次配置設定了預制遊戲。
多種不同形狀
建立一個預設的球體和膠囊對象,為每個對象賦予一個
Shape
元件,并将它們也轉換為預制件。這些是我們的遊戲将支援的其他形狀。
球形和膠囊形預制件。
圓柱呢?
您也可以添加一個圓柱對象,但是我省略了它,因為圓柱沒有自己的碰撞類型。取而代之的是,他們使用了一個不太适合的膠囊對撞機。現在這不是問題,但可能稍後。
工廠資産
目前,
Game
隻能生成一件東西,因為它隻有一個對預制件的引用。要支援所有三種形狀,将需要三個預制參考。這将需要三個字段,但這并不靈活。更好的方法是使用數組。但是也許以後我們會想出另一種方式來建立形狀。這可能會變得
Game
相當複雜,因為它還負責使用者輸入,跟蹤對象以及觸發儲存和加載。
為
Game
簡單起見,我們将負責在自己的類中支援哪些形狀。此類将像工廠一樣,按需建立形狀,而其客戶不必知道如何制作這些形狀,甚至不必知道有多少種不同的選擇。我們将為此類命名ShapeFactory。
工廠的唯一責任是傳遞形狀執行個體。它不需要位置,旋轉或縮放,也不需要
Update
更改狀态的方法。是以,它不必是元件,而必須将其附加到遊戲對象上。相反,它可以單獨存在,而不是作為特定場景的一部分,而是作為項目的一部分。換句話說,它是一種資産。通過使其擴充
ScriptableObject
而不是可以實作
MonoBehaviour
。
現在,我們有了一個自定義資産類型。要将這樣的資産添加到我們的項目中,我們必須在Unity菜單中為其添加一個條目。最簡單的方法是将
CreateAssetMenu
屬性添加到我們的類中。
您現在可以通過Assets› Create› Shape Factory來建立我們的工廠。我們隻需要一個。
塑造工廠資産。
為了讓我們的工廠知道形狀預制件,請給它一個
Shape[] prefabs
數組字段。我們不希望這個領域公開,因為它的内部運作不應暴露于其他類别。是以,請保密。要使數組顯示在檢查器中并由Unity儲存,請向其中添加
SerializeField
屬性。
字段出現在檢查器中之後,将所有三個形狀的預制件拖到其上,以便将對它們的引用添加到陣列中。確定多元資料集是第一個元素。将球體用于第二個元素,将膠囊用于第三個元素。
引用預制件的工廠。
擷取形狀
為了使工廠有用,必須有一種方法可以使形狀執行個體脫離工廠。是以,給它一個公共
Get
方法。用戶端可以通過形狀辨別符參數訓示所需的形狀。為此,我們将簡單地使用整數。
為什麼不使用枚舉?
當然可以,是以您可以這樣做。但是我們實際上并不關心在代碼中辨別确切的形狀類型,是以整數可以正常工作。這樣就可以通過更改工廠的數組内容來控制純粹支援哪些形狀,而無需更改任何代碼。
我們可以直接使用辨別符作為索引來找到合适的形狀預制件,将其執行個體化并傳回。這意味着0表示一個立方體,1表示一個球體,而2表示一個膠囊。即使我們稍後更改工廠的工作方式,也必須確定此辨別保持不變,以保持向後相容。
除了請求特定形狀外,我們還可以通過一種
GetRandom
方法從工廠中擷取随機形狀執行個體。我們可以使用該
Random.Range
方法随機選擇一個索引。
不是應該用Random.Range(0, prefab.Length - 1)嗎?
Unity的Random.Range帶有整數參數的方法使用一個互斥最大值。輸出範圍是從最小到最大負1。之是以這樣做,是因為通常的用例都希望獲得随機數組索引,而這正是我們在這裡所做的。
請注意,Random.Range對于float參數,請使用包含性最大值。
擷取形狀
由于我們現在要在中建立形狀,是以我們
Game
将其顯式并将其清單重命名為
shapes
。是以,無論到哪裡編寫objects,都将其替換為
shapes
。最簡單的方法是使用代碼編輯器的重構功能來更改該字段的名稱,并且它将在使用該字段的所有位置進行重命名。
還将清單的項目類型更改為
Shape
,這更加具體。
接下來,删除預制字段,并添加一個
shapeFactory
字段以保留對形狀工廠的引用。
在中
CreateObject
,我們現在将通過調用
shapeFactory.GetRandom
而不是執行個體化顯式的預制來建立任意形狀。
我們還要重命名執行個體的變量,以便非常明确地表明我們正在處理形狀執行個體,而不是仍需要執行個體化的預制引用。再一次,您可以使用重構來快速一緻地重命名該變量。
加載時,我們現在還必須使用形狀工廠。在這種情況下,我們不需要随機形狀。我們以前隻使用過多元資料集,是以我們應該通過調用來獲得多元資料集
shapeFactory.Get(0)
。
我們還要在這裡明确說明我們正在處理執行個體。
與工廠而不是預制的遊戲。
在給遊戲提供我們工廠的參考之後,它現在将在每次玩家生成新的形狀時建立随機形狀,而不是總是獲得立方體。
建立随機形狀。
記住形狀
雖然現在可以建立三個不同的形狀,但是此資訊尚未儲存。是以,每次加載儲存的遊戲時,我們最終隻能得到多元資料集。這對于以前儲存的遊戲是正确的,但對于我們添加了對多種形狀的支援後儲存的遊戲卻不正确。我們還必須添加對儲存不同形狀的支援,理想情況下,同時仍然能夠加載舊的儲存檔案。
形狀辨別符屬性
為了能夠儲存對象的形狀,對象必須記住此資訊。最簡單的方法是在Shape中添加形狀辨別符字段。
理想情況下,此字段是隻讀的,因為形狀執行個體始終是一種類型,并且不會更改。但是必須以某種方式為它配置設定一個值。我們可以将私有字段标記為可序列化,并通過每個預制件的檢查器為其配置設定一個值。但是,這不能保證辨別符與工廠使用的數組索引比對。我們也有可能在其他地方使用形狀預制件,這與工廠無關,或者甚至在某個時候将其添加到另一個工廠。是以,形狀辨別取決于工廠,而不取決于預制件。是以,這是每個執行個體而不是每個預制件要跟蹤的東西。
預設情況下,私有字段不會序列化,是以預制與它無關。一個新執行個體将簡單地擷取該字段的預設值,在這種情況下為0,因為我們沒有給它另一個預設值。為了使辨別符可公開通路,我們将在中添加一個
ShapeId
屬性
Shape
。除了第一個字母是大寫字母外,我們使用相同的名稱。屬性是僞裝成字段的方法,是以它們需要一個代碼塊。
屬性實際上需要兩個單獨的代碼塊。一種擷取它表示的值,另一種進行設定。這些通過
get
和
set
關鍵字辨別。可以僅使用其中之一,但是在這種情況下,我們需要兩者。
getter部分僅傳回私有字段。設定者僅配置設定給私有字段。設定器具有
value
為此目的命名的适當類型的隐式參數。
通過使用屬性,可以在看起來很簡單的檢索或配置設定中添加其他邏輯。在我們的情況下,形狀辨別符必須在出廠時執行個體化時每次執行個體設定一次。在那之後再次設定它将是一個錯誤。
我們可以通過驗證辨別符在配置設定時是否仍具有其預設值來檢查配置設定是否正确。如果是這樣,則該配置設定有效。如果沒有,我們将記錄一個錯誤。
但是,0是有效的辨別符。是以,我們必須使用其他值作為預設值。讓我們使用最小的可能整數
int.MinValue
,即-2147483648。另外,我們應確定辨別符不能重置為其預設值。
為什麼不隻是使用readonly财産?
readonly隻能為字段或屬性配置設定預設值,或者在構造函數方法中将其配置設定給該字段或屬性。不幸的是,我們在執行個體化Unity對象時不能使用構造函數方法。是以,我們必須使用這樣的方法。
調整
ShapeFactory.Get
以便它在傳回執行個體之前設定執行個體的辨別符。
識别檔案版本
我們之前沒有形狀辨別符,是以我們沒有儲存它們。如果我們從現在開始儲存它們,我們将使用其他儲存檔案格式。如果我們的遊戲的舊版本(來自上一教程)不能讀取此格式,則很好,但是我們應確定新遊戲仍然可以使用舊格式。
我們将使用儲存版本号來辨別儲存檔案使用的格式。現在,我們開始介紹此概念時,從版本1開始。将其作為常量整數添加到中
Game
。
const是什麼意思
它聲明一個簡單的值是一個常量,而不是一個字段。它無法更改,并且不存在于記憶體中。相反,它隻是代碼的一部分,其顯式值在引用的任何地方都會使用,并在編譯期間替換。
儲存遊戲時,請先編寫儲存版本号。加載時,請先閱讀存儲的版本。這告訴我們正在處理什麼版本。
但是,這僅适用于包含儲存版本的檔案。上一教程中的舊儲存檔案沒有此資訊。相反,寫入這些檔案的第一件事是對象計數。是以,我們最終将計數解釋為版本。
存儲在舊的儲存檔案中的對象計數可以是任何數字,但始終至少為零。我們可以使用它來區分儲存版本和對象計數。通過不逐字寫儲存版本來完成。而是在編寫版本時翻轉版本的符号。從1開始,這意味着存儲的儲存版本始終小于零。
閱讀版本時,請再次翻轉其符号以擷取原始編号。如果我們正在讀取舊的儲存檔案,則最終會翻轉計數的符号,是以它變為零或負數。是以,當我們最終得到的版本小于或等于零時,我們知道我們正在處理一個舊檔案。在那種情況下,我們已經有了計數,隻有一個翻轉的符号。否則,我們仍然必須讀取計數。
問号是什麼意思?
它是三元運算符,condition ? trueResult : falseResult它是if-else表達式的簡寫形式。在這種情況下,代碼等效于以下代碼:
int version = -reader.ReadInt(); int count; if (version <= 0) { count = -version; } else { count = reader.ReadInt(); }
這使得新代碼可以處理舊的儲存檔案格式。但是舊代碼無法處理新格式。我們對此無能為力,因為舊的代碼已經編寫好了。我們能做的是確定從現在開始遊戲将拒絕加載它不知道如何處理的将來的儲存檔案格式。如果加載的版本高于我們目前的儲存版本,請記錄錯誤并立即傳回。
儲存形狀辨別符
形狀不應該寫自己的辨別符,因為必須讀取它以确定要執行個體化的形狀,并且隻有在此之後才能加載自身。是以
Game
,編寫辨別符是責任。因為我們将所有形狀存儲在一個清單中,是以我們必須在形狀儲存之前寫出每個形狀的辨別符。
請注意,這不是儲存形狀辨別符的唯一方法。例如,也可以對每種形狀類型使用單獨的清單。在這種情況下,每個清單隻需寫入一次每個形狀辨別符。
加載形狀辨別符
對于清單中的每個形狀,首先加載其形狀辨別符,然後使用該辨別符從工廠擷取正确的形狀。
但這僅對新的儲存版本1有效。如果我們正在從舊的儲存檔案中讀取,則隻需擷取多元資料集即可。
材質變體
除了改變生成的對象的形狀外,我們還可以改變它們的組成。目前,所有形狀都使用相同的材質,這是Unity的預設材質。讓我們将其更改為随機選擇的材質。
三種材質
建立三種新材質。命名第一個Standard,使其保持不變,以便與Unity的預設材質比對。将第二個命名為Shiny并将其“ 平滑度”增加到0.9。将第三個命名為Metallic,并将其Metallic和Smoothness都設定為0.9。
标準,有光澤和金屬感。
當從工廠獲得形狀時,現在還應該可以指定必須使用哪種材質。這需要
ShapeFactory
知道允許的材質。是以,為其提供一個材質陣列(就像其預制陣列一樣),并為其配置設定三種材質。確定标準材質是第一個元素。第二種是有光澤的材質,第三種是金屬。
工廠用料。
設定形狀的材質
為了儲存形狀具有的材質,我們現在還必須跟蹤材質辨別符。為此添加一個屬性
Shape
。但是,除了顯式地編碼屬性的工作方式之外,請省略getter和setter的代碼塊。請以分号結尾。這将生成一個預設屬性,并帶有一個隐式的隐藏私有字段。
設定形狀的材質時,我們必須同時為其提供實際的材質及其辨別符。這表明我們必須一次使用兩個參數,但是對于屬性來說這是不可能的。是以,我們不會依賴該屬性的設定者。要禁止在
Shape
類本身之外使用它,請将設定器标記為私有。
相反,我們添加了
SetMaterial
帶有必需參數的公共方法。
該方法可以通過調用該GetComponent方法來擷取形狀的MeshRenderer分量。請注意,這是一個通用方法,就像
List
通用類一樣。設定渲染器的材質以及材質辨別符屬性。確定将參數配置設定給屬性,差別在于M是否為大寫字母。
使用材質擷取形狀
現在我們可以進行調整
ShapeFactory.Get
以處理材質。給它第二個參數來訓示應該使用哪種材質。然後使用它來設定形狀的材質及其材質辨別符。
調用Get的任何人可能都不在乎材質,而對标準材質感到滿意。我們可以使用單個形狀辨別符參數來支援Get的變體。為此,我們可以使用0 為其materialId參數配置設定預設值。這樣做可以在調用Get時省略該materialId參數。結果,現有代碼在此時編譯時沒有錯誤。
我們可以對
shapeId
參數執行相同的操作,也将其預設值設定為0。
您如何訓示所需的預設值?
要省略materialId,隻需将其忽略即可,是以您可以調用Get(0)。您也可以通過調用Get()忽略兩個參數。但是,如果要省略shapeId而不是materialId,則必須明确說明要提供的參數。您可以通過标記參數,在參數值之前寫入參數名稱以及冒号來做到這一點。例如,Get(materialId: 0)。
該
GetRandom
方法現在應該同時選擇随機形狀和随機材質。是以,它也可以用Random.Range來選擇随機的材質辨別符。
具有随機材質的形狀。
儲存和加載物料辨別符
儲存材質辨別符與儲存形狀辨別符相同。将其寫在每個形狀的形狀辨別符之後。
加載也一樣。我們不會費心增加此更改的儲存版本,因為我們仍在同一教程中,該教程象征着一個公共發行版。是以,對于存儲形狀辨別符但不存儲材質辨別符的儲存檔案,加載将失敗。
随機顔色
除了整個材質,我們還可以改變形狀的顔色。我們通過調整每個形狀執行個體材質的顔色屬性來實作。
我們可以定義一組有效的顔色并将其添加到形狀工廠中,但是在這種情況下,我們将使用不受限制的顔色。這意味着工廠不必了解形狀顔色。而是設定形狀的顔色,就像其位置,旋轉和比例一樣。
形狀顔色
添加一種
SetColor
方法,使其Shape可以調整其顔色。它必須調整使用的任何材質的顔色屬性。
為了儲存和加載形狀的顔色,必須跟蹤它。我們不需要提供對顔色的公共通路權限,是以可以通過設定一個私有字段
SetColor
。
儲存和加載顔色是通過覆寫PersistableObject的
Save
和
Load
方法來完成的。首先要照顧好基礎,然後是色彩資料。
但是,這假設存在用于寫入和讀取顔色的方法,目前情況并非如此。是以,讓我們添加它們。首先是GameDataWriter的一種新的
Write
方法。
然後也是一種GameDataReader到底
ReadColor
方法。
我們是否需要将顔色通道存儲為浮點數?
您也可以決定将它們存儲為位元組,但是如果這樣做,最好始終Color32在任何地方使用。這樣可以確定儲存和加載的資料始終相同。您不必為此煩惱,隻需為每個形狀節省十二個位元組,除非您确實需要最小化儲存檔案的大小。同樣,您可以決定跳過Alpha通道,因為不透明的材質不需要它,但通常也不值得擔心。
其餘向後相容
盡管此方法可以存儲形狀顔色,但現在假定顔色存儲在儲存檔案中。舊的儲存格式不是這種情況。為了仍然支援舊格式,我們必須跳過加載顔色。在Game中,我們使用讀取版本來決定要做什麼。但是,
Shape
不知道版本。是以,
Shape
在加載資料時,我們必須以某種方式将正在讀取的資料的版本進行通信。将版本定義為GameDataReader的屬性是有意義的。
由于讀取檔案的版本在讀取時不會更改,是以該屬性應僅設定一次。與
GameDataReader
不是Unity對象類一樣,我們可以通過僅提供
get
一部分屬性來使用隻讀屬性。可以通過構造函數方法初始化此類屬性。為此,我們必須将版本添加為構造函數參數。
我們不能滿足Version = version嗎?
是的,但是我為清楚起見添加this了。
現在,編寫和閱讀版本号已成為PersistentStorage的責任。該版本必須作為其
Save
方法的參數添加,該方法必須在其他任何方法之前将其寫入。并且該
Load
方法在GameDataReader構造時讀取它。同樣在這裡,我們将執行符号更改技巧,以支援讀取零版本檔案。
這意味着
Game
不再需要編寫儲存版本。
而是在調用PersistentStorage.Save時必須将其作為參數提供。
現在,在其Load方法中,可以通過reader.Version檢索版本。
現在我們還可以檢查Shape.Load中的版本。如果我們至少有版本1,請閱讀顔色。否則,請使用白色。
選擇形狀顔色
要建立具有任意顔色的形狀,隻需在Game.CreateShape中調用
SetColor
新執行個體。我們可以使用該
Random.ColorHVS
方法生成随機顔色。沒有參數,該方法可以建立任何有效的顔色,這可能會有點混亂。通過将飽和度範圍限制為0.5-1,将值範圍限制為0.25-1,讓我們将自己局限于彩色調色闆。由于我們此時尚未使用alpha,是以我們始終将其設定為1。
使用ColorHVS的所有8個參數很難了解,因為尚不清楚哪個值控制什麼。通過顯式命名參數,可以使代碼更易于閱讀。
具有随機顔色的形狀。
記住渲染器
現在,我們在設定材質和顔色時都需要通路它們Shape的
MeshRenderer
元件。使用
GetComponent
兩次是不理想的,尤其是如果我們決定将來多次更改形狀的顔色時。是以,讓我們将引用存儲在私有字段中,并使用Shape的新方法Awake對其進行初始化。
MeshRenderer meshRenderer; void Awake () { meshRenderer = GetComponent(); }
現在我們可以在SetColor和中使用該字段SetMaterial。
public void SetColor (Color color) { this.color = color;// GetComponent().material.color = color; meshRenderer.material.color = color; } public void SetMaterial (Material material, int materialId) {// GetComponent().material = material; meshRenderer.material = material; MaterialId = materialId; }
使用屬性塊
設定材質顔色的一個缺點是,這會導緻建立新的形狀獨特的材質。每次設定顔色時都會發生這種情況。我們可以通過使用一個
Material屬性塊
來避免這種情況。建立一個新的屬性塊,設定一個名為_Color的顔色屬性,然後通過調用MeshRenderer.SetPropertyBlock将其用作渲染器的屬性塊。
除了使用字元串來命名color屬性之外,還可以使用辨別符。這些辨別符由Unity設定。它們可以更改,但每個會話保持不變。是以,我們隻需擷取一次color屬性的辨別符,并将其存儲在靜态字段中就足夠了。通過
Shader.PropertyToID
使用名稱調用方法來找到辨別符。
也可以重用整個屬性塊。設定渲染器的屬性時,将複制塊的内容。是以,我們不必為每個形狀建立新的塊,我們可以為所有形狀不斷更改同一塊的顔色。
我們可以再次使用靜态字段來跟蹤該塊,但是不可能通過靜态初始化來建立一個塊執行個體。Unity不允許這樣做。相反,我們可以在使用該塊之前檢查該塊是否存在。如果沒有,我們将在那時建立它。
現在,我們不再獲得重複的材質,您可以通過在播放模式中使用形狀時調整其中一種材質來進行驗證。這些形狀将根據更改來調整外觀,如果使用重複的材質則不會發生這種情況。當然,當您調整材質的顔色時,這是行不通的,因為每種形狀都使用自己的color屬性,該屬性會覆寫材質的顔色。
GPU執行個體化
當我們使用屬性塊時,可以使用GPU執行個體化在單個繪制調用中組合使用相同材質的形狀,即使它們具有不同的顔色。但是,這需要支援執行個體顔色的着色器。這是一個着色器,您可以在Unity GPU Instancing手冊頁上找到它。唯一的差別是我删除了注釋并添加了
#pragma instancing_options assumeuniformscaling
指令。假設統一縮放比例可以使執行個體化效率更高,因為它需要較少的資料,并且可以工作,因為我們所有的形狀都使用統一比例尺。
Shader "Custom/InstancedColors" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma instancing_options assumeuniformscaling #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) UNITY_INSTANCING_BUFFER_END(Props) void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } FallBack "Diffuse"}
更改我們的三種材質,以便他們使用此新着色器代替标準着色器。它支援較少的功能,并具有不同的檢查器界面,但足以滿足我們的需求。然後,確定對所有材質都選中了“ 啟用GPU執行個體化”。
具有執行個體顔色的标準材質。
您可以通過“ 遊戲”視窗的“ 統計資料”疊加層來驗證差異。
有無GPU執行個體化。
下一個教程是 對象複用。
資源庫(Repository)
https://bitbucket.org/catlikecodingunitytutorials/object-management-02-object-variety/
往期精選
Unity3D遊戲開發中100+效果的實作和源碼大全 - 收藏起來肯定用得着
Shader學習應該如何切入?
喵的Unity遊戲開發之路 - 從入門到精通的學習線路和全教程
聲明:釋出此文是出于傳遞更多知識以供交流學習之目的。若有來源标注錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯系,我們将及時更正、删除,謝謝。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/object-management/object-variety/
翻譯、編輯、整理:MarsZhou
More:【微信公衆号】 u3dnotes