原文地址:http://gad.qq.com/program/translateview/7167991
英文版原文
翻譯:高磊(穩定心態) 校審:羅倩(蘑菇)
大約四年前,我發布了關于Unity開發的50個技巧的初始版本。
雖然最新版本與初始版本仍有許多關聯,但在初始版本之后,我修改了許多內容:
Unity更好用。例如,我現在信賴FPS計數器。使用property drawer的功能可以降低編寫customeditors的必要性。同時Prefab的工作方式也降低了顯式嵌套Prefab或替代件的需求。Scriptable objects更為友好。
VisualStudio集成度更佳,從而使調試操作更簡便,同時減少了對于大量Gorilla調試操作的需求。
第三方工具和庫更優化。AssetStore里現有許多可用的對于可視化調試和更佳日志記錄等事物的輔助工具。我們自帶(免費)的擴展插件有大量初始發布中所述的代碼(以及本次版本發布所述的許多代碼)。
版本控制更好。(或者可以說,我現在知道如何更有效地使用它)。比方說,無需對Prefab執行多個副本或者備份。
個人經驗的積累。在過去四年里,我參與了許多Unity項目;包括大量的游戲原型制作,Father.IO等的游戲制作,以及我們的旗艦工具Unity asset Grids。
本文是在考慮上述所有內容的基礎上對初始版本進行的修訂版本。
在繼續討論技巧前,本人現發布以下免責聲明(與初始版本基本相同):
這些技巧并不適用于每個Unity項目。
這些技巧是基于本人與3到20人的小型團隊協作參與項目所獲得的經驗。在結構性,可重用性,清晰度等方面存在費用——根據團隊規模,項目規模和項目目標來確定是否此費用。比方說,你可能不會在游戲制作環節使用所有內容。
許多技巧涉及個人喜好問題(雖然這里列出的技巧之間可能有競爭,但都是很好的技巧)。
此外,Unity也在其官網上發布了一些最佳操作實例(雖然它們中大多數是從效能角度出發):
1)http://unity3d.com/learn/tutorials/topics/best-practices
2)基于物理的內容創建的最佳操作實例:https://youtu.be/OeEYEUCa4tI
3)Unity中的最佳2D操作實例:https://youtu.be/HM17mAmLd7k
4)Unity內部技巧和技巧: https://youtu.be/Ozc_hXzp_KU
5)Unity提示和技巧:https://youtu.be/2S6Ygq58QF8
6)http://docs.unity3d.com/Manual/HOWTO-ArtAssetBestPracticeGuide.html
開發流程
1.確定開始的縮放比例,并以相同縮放比例構建所有原型。否則,你可能需要后續重做assets(例如,無法總是正確地縮放動畫)。對于3D游戲,采用1 Unity單位= 1m通常是最佳的。對于不使用照明或物理的2D游戲,采用1 Unity單位 = 1 像素(在“設計”分辨率階段)通常是較好的。對于UI(以及2D游戲),選擇設計分辨率(我們使用HD或2xHD,并將所有assets設計為以此分辨率縮放。
2. 使每個場景都可以運行。這樣可以避免為了運行游戲而必須轉換場景,從而加快了測試速度。如果要在所有場景中必需的場景加載之間持續存在對象,這可能需要技巧。一種方法是當持續對象不存在于場景中時,使它們作為可自行加載的單例模式。另一個技巧中將詳述單例模式。
3. 使用源代碼控制,并學習如何有效地使用它。
將assets序列化為文本。實際上,它并不會提高場景和Prefab的可合并性,但它會使變化更容易觀測。
采用場景和Prefab共享策略。一般來說,多個人不應在同一場景或Prefab工作。對于小型制作團隊,只要在開始工作前確保沒有人制作場景或Prefab即可。交換表示場景所有權的物理標記可能很有用(如果桌面上有場景標記,你僅可以在某一場景中工作)。
將標簽作為書簽。
確定并堅持采用分支策略。由于場景和Prefab不能平滑地合并,分支稍顯復雜。然而當你決定使用分支時,它應該結合場景和Prefab共享策略使用。
使用子模塊時要小心。子模型可能是維護可重用代碼的最佳途徑。但需注意幾個警告事項:
元數據文件通常在多個項目中不一致。對于非Monobehaviour或非Scriptable object代碼而言,這通常不是問題,但對于MonoBehaviours和Scriptable objects使用子模塊可能會導致代碼丟失。
如果你參與許多項目(包括一個或多個子模塊項目),倘若你必須對幾次迭代中的多個項目執行獲取—合并—提交—推送操作以穩定所有項目的代碼,有時會發生更新崩潰(并且如果其他人同時進行變更,它可能會轉變為持續崩潰)。一種最大程度上降低此效應的方法是在項目初始階段對子模塊進行更改。如此一來,總是需要推送僅使用子模塊的項目;它們從來無需推回。
4. 保持測試場景和代碼分離。向存儲庫提交臨時資源和腳本,并在完成后將它們移出項目。
5. 如果你要更新工具(尤其是Unity),必須同時進行。當你使用一個與先前不同的版本打開項目時,Unity能夠更好地保留鏈接,但倘若人們使用不同的版本,有時仍然會丟失鏈接。
6. 在一個干凈的項目中導入第三方assets,并從中導出一個可供自己使用的新的資源包。當你直接向項目導入這些資源,它們有時會導致問題:
可能存在沖突(文件或文件名),尤其對于在插件目錄根中存在文件或者在實例中使用StandardAssets中assets的資源。
這些資源可能被無序地放入到自有項目的文件中。如果你決定不使用或者想要移除這些assets,這可能成為一個重要問題。
請按照下述步驟使assets導入更安全:
1)創建一個新項目,然后導入asset。
2)運行實例并確保它們能夠工作。
3)將asset排列為一個更合適的目錄結構。(我通常不對一個資源強制排列自有的目錄結構。但是我確保所有文件均在一個目錄中,同時在重要位置不存在任何可能會覆蓋項目中現有文件的文件。
4)運行實例并確保它們仍可以工作。(有時,當我移動事物時會導致assets損壞,但這通常不應該是一個問題)。
5)現要移除所有無需的事物(如實例)。
6)確保asset仍可編譯,并且Prefab仍然擁有所有自身的鏈接。若留下任何需運行的事項,則對它進行測試。
7)現選定所有assets,并導出一個資源包。
8)導入到你的項目中。
7. 自動構建進程。甚至對于小型項目,這步很有用,但對于以下情況尤為適用:
你需要構建許多不同的游戲版本。
其他擁有不同程度技術知識的團隊成員需要進行構建,或者
你需要對項目進行小幅調整后才能進行構建。
詳見Unity構建編譯:對于如何執行的較好指導的基本和高級可能性。
8. 為你的設置建立文檔。大部分記錄應在代碼中,但是某些事項應記錄在代碼外。制作設計師通過耗時的設置來篩選代碼。文檔化的設置可以提高效率(若文檔是最新的)。
對下述內容建立文檔:
標簽使用。
圖層使用(對于碰撞,剔除和光線投射—從本質上來說,每個圖層對應的使用)。
圖層的GUI深度(每個圖層對應的顯示)
場景設置。
復雜Prefab的Prefab結構。
常用語偏好。
構建設置。
通用編碼
9. 將所有代碼放入一個命名空間中。這避免了自有庫和第三方代碼之間可能發生的代碼沖突。但不要依賴于命名空間以避免與重要類沖突。即使你會使用不同的命名空間,也不要將“對象”、“動作”或“事件”作為類名稱。
10. 使用斷言。斷言對于代碼中不變量的測試非常有用,它能夠輔助清除邏輯錯誤。Unity.Assertions.Assert類提供了可用的斷言。它們都可以測試一些條件,但如果不符合條件,則在控制臺中寫入錯誤信息。如果你不熟悉如何有效地使用斷言,請參考使用斷言編程的優點(a.k.a.斷言語句)。
11. 切勿對顯示文本以外的任何事項使用字符串。尤其應注意,不要使用字符串來標識對象或Prefab。但存在一些例外情形(仍然有一些內容只能通過Unity中的名稱訪問)。在這種情形下,將這些字符串定義為“AnimationNames”或 “AudioModuleNames”等文件中的常量。倘若這些類變為不可管理,使用嵌套類后便可類似命名AnimationNames.Player.Run。
12. 不要使用“Invoke”和“SendMessage”。這些MonoBehaviour方法通過名稱調用其他方法。通過名稱調用的方法難以在代碼中追蹤(無法找到“Usages”,而“發SendMessage”的范圍更寬,因此更難以追蹤)。
較簡便的方法是使用Coroutines和C#操作推出“Invoke”:
public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time)
{
return monoBehaviour.StartCoroutine(InvokeImpl(action, time));
}
private static IEnumerator InvokeImpl(Action action, float time)
{
yield return new WaitForSeconds(time);
action();
}
你可以參考monoBehaviour模式:
this.Invoke(ShootEnemy); //其中ShootEnemy是一個無參數的void法。
如果你實現自己的基礎MonoBehaviour,你可以向其中添加自己的“Invoke”。
另一種較安全的“SendMessage”方法更難以實施。與之相反,我通常使用“GetComponent”變量以獲取父對象,當前游戲對象或子對象的組件,并直接執行調用。
13.當游戲運行時,不要讓派生對象混亂層次結構。將它們的父對象設為場景對象,以便在游戲運行時更容易找到內容。你可以使用一個空游戲對象,或者甚至使用一個無行為的單例模式(詳見本文后面的部分),從而更容易地從代碼進行訪問。將此對象命名為“DynamicObjects”。
14. 明確是否要將空值(null)作為一個合法值,并盡量避免這么做
空值可輔助檢測錯誤代碼。但是,如果你使“if”默默地通過空值成為一種習慣,錯誤代碼將很快運行,同時你只能在很久之后才會注意到錯誤。此外,隨著每個圖層通過空變量,它可以在代碼深度暴露。我嘗試避免將空值整體作為一個合法值。
我優先采用的常用語不是進行任何空檢查,倘若它是一個問題,讓代碼失敗。有時,在“可重用”方法中,我將檢查出一個值為空的變量,并拋出一個異常,而不是將它傳遞至其它可能失敗的方法。
在某些情形下,值可以合法為空,并且需要采取不同的方式處理。在此類情況下,添加注釋來解釋什么時候某些內容可能為空,并說明為什么可能為空。
常見場景通常用于inspector配置的值。用戶可以指定一個值,但如果未指定任何值,則使用一個默認值。最好結合包含T值的可選類。(這有點像“可為空”)。你可以使用一個特殊的屬性渲染器來渲染一個勾選框,若勾選,則僅顯示數值框。
(但切勿直接使用泛型類,你必須擴展特定T值的類)。
[Serializable]
public class Optional<t>
{
public bool useCustomValue;
public T value;
}
在你的代碼中,你可以采取這種使用途徑:
health= healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;
15. 如果你使用“協程”,學習如何有效地使用它。
“協程”是解決許多問題的一種最有效的方法。但是難以對“協程”進行調式,同時你可以很容易地對它進行混亂的編碼,從而使其他人,甚至包括你自己也無法理解其意義。
你應該知道:
1)如何并發執行協程。
2)如何按序執行協程。
3)如何從現有程序中創建新的協程。
4)如何使用“CustomYieldInstruction”創建自定義協程。
//This is itself a coroutine
IEnumerator RunInSequence()
{
yield return StartCoroutine(Coroutine1());
yield return StartCoroutine(Coroutine2());
}
public void RunInParallel()
{
StartCoroutine(Coroutine1());
StartCoroutine(Coroutine1());
}
Coroutine WaitASecond()
{
return new WaitForSeconds(1);
}
16. 利用擴展法來協同共享接口的組件。有時可以方便地獲取實施某個接口的組件,或者找到這些組件相應的對象。
下述實例使用typeof而不是這些函數的通用版本。通用版本無法協同接口使用,但typeof卻可以。下面的方法將其整潔地套入通用方法之中。
public static TInterface GetInterfaceComponent<tinterface>(this Component thisComponent)
where TInterface : class
{
return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}
17. 利用擴展法使語法更簡潔。例如:
public static class TransformExtensions
{
public static void SetX(this Transform transform, float x)
{
Vector3 newPosition =
new Vector3(x, transform.position.y, transform.position.z);
transform.position = newPosition;
}
...
}
18. 使用另一種防御性GetComponent方法。有時通過RequiredComponent強制組件關系可能難以操作,但是這總是可能和可取的,特別是當你調用其它類上的GetComponent。作為一種替代方法,但需要某個組件打印找到的錯誤信息時,可以使用下述GameObject擴展。
public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour
{
T component = obj.GetComponent();
if(component == null)
{
Debug.LogError("Expected to find component of type "
+ typeof(T) + " but found none", obj);
}
return component;
}
19. 避免對相同的事項使用不同的常用語。在許多情況下,有多種常用法。此時,對整個項目選擇一種常用法。其原因在于:
1)某些常用語不能一起工作。在某個方向中使用一種常用語強行設 計可能不適合另一種常用語。
2)對于整個項目使用相同的常用語能夠使團隊成員更容易理解進展。它使結構和代碼更容易理解。這樣就更難犯錯。
常用語組示例:
協程與狀態機。
嵌套的Prefab,互相鏈接的Prefab和超級Prefab
數據分離策略。
對2D游戲中狀態使用sprites的方法。
Prefab結構。
派生策略。
定位對象的方法:按類型,按名稱,按標簽,按圖層和按引用關系(“鏈接”)。
分組對象的方法:按類型,按名稱,按標簽,按圖層和按引用數組(“鏈接”)。
調用其他組件方法的途徑。
查找對象組和自注冊。
控制執行次序(使用Unity的執行次序設置,還是使用yield邏輯,利用Awake / Start和Update / Late Update依賴,還是使用純手動的方法,或者采用次序無關的架構)。
在游戲中使用鼠標選擇對象/位置/目標:SelectionManager或者對象自主管理。
在場景變換時保存數據:通過PlayerPrefs,或者是在新場景加載時未毀損的對象。
組合(混合、添加和分層)動畫的方法。
輸入處理(中央和本地)
20. 維護一個自有的Time類,這可以更容易實現游戲暫停。包裝一個“Time.DeltaTime”和“Time.TimeSinceLevelLoad”來實現暫停和游戲速度的縮放。它使用時有點麻煩,但是當對象運行在不同的時鐘速率下就容易多了(例如界面動畫和游戲動畫)。
21.需要更新的自定義類不應該訪問全局靜態時間。相反,它們應將增量時間作為它們Update方法的一個參數。當你如上所述實施一個暫停系統,或者當你想要加快或減慢自定義類的行為時,這樣使這些類變為可用。
22. 使用常見結構進行WWW調用。在擁有很多服務器通信的游戲中,通常有幾十個WWW調用。無論你是使用Unity的原始WWW類還是使用某個插件,你可以從生成樣板文件的頂部寫入一個薄層獲益。
我通常定義一個Call方法(分別針對Get和Post),即CallImpl協程和MakeHandler。從本質上來說,Call方法通過采用MakeHandler法,從一個解析器,成功和失敗的處理器構建出一個super hander。此外,它也調用CallImpl協程,創建一個URL,進行調用,等待直至完成,然后調用super handler。
其大概形式如下:
public void Call(string call, Func parser, Action onSuccess, Action onFailure)
{
var handler = MakeHandler(parser, onSuccess, onFailure);
StartCoroutine(CallImpl(call, handler));
}
public IEnumerator CallImpl(string call, Func handler)
{
var www = new WWW(call);
yield return www; handler(www);
}
public Func MakeHandler(Func parser, Action onSuccess, Action onFailure)
{
if (NoError(www))
{
var parsedResult = parser(www.text);
onSuccess(parsedResult);
}
else { onFailure("error text");
}
}
它具有一些優點:
它允許你避免編寫大量樣板代碼。
它允許你在中央位置處理某些事項(例如顯示加載的UI組件或處理某些通用錯誤)。
23. 如果你有大量文本,將它們放在同一個文件中。不要將它們放入inspector將編輯的字段中。使其在無需打開Unity編輯器,尤其是無需保存場景的前提下易于更改。
24. 如果你想執行本地化,將所有字符串分離到同一個位置。有很多方法可以實現這一點。一種方法是針對每個字符串定義一個具有public字符串字段的Text類,例如默認設為英文。其他語言將其子類化,并使用同等語言重新初始化這些字段。
一些更復雜的技術(其適用情形是正文本較大和/或語言數量較多時)將讀取到一個電子表格中,并基于所選語言提供選擇正確字符串的邏輯。
類的設計
25.確定實現可檢查字段的方法,并將其確立為標準。有兩種方法:使字段public,或者使它們private并標記為[可序列化]。后者“更正確”但不太方便(當然不是Unity本身常用的方法)。無論你選擇哪種方式,將它確立為標準,以便于團隊中開發人員知道如何解釋一個public字段。
可檢查字段是public的。在這種情況下,public表示“設計師在 運行時更改此變量是安全的。避免在代碼中設置該值”。
可檢查字段是private,并被標記為“可序列化”。 在這種情 況下,public表示“在代碼中更改此變量是安全的”(因此,你不應該看到太多,并且在MonoBehaviours 和ScriptableObjects中不應該有任何public字段)。
26. 對于組件,切勿使不應在inspector中調整的變量成為public。否則,它們將被設計師調整,特別是當不清楚它是什么時。在某些罕見的情況下,這是無法避免的。此時,使用兩條,甚至四條下劃線對變量名添加前綴以警告調整人員:
public float __aVariable;
27. 使用Property Drawers使字段更加用戶友好。可以使用Property Drawers自定義inspector中的控制。這樣可以使你能夠創建更適合數據性質的控制,并實施某些安全保護(如限定變量范圍)。
28. 相較于Custom Editors,更偏好采用PropertyDrawers。Property Drawers是根據字段類型實現的,因此涉及的工作量要少得多。另外,它們的重用性更佳—一旦實現某一類型,它們可應用于包含此類型的任何類。而Custom Editors是根據MonoBehaviour實現的,因此重用性更少,涉及的工作量更多。
29.默認密封MonoBehaviours。一般來說,UnityMonoBehaviours的繼承友好不高:
類似于Start和Update,Unity調用信息的方式使得在子類中難以使用這些方法。你稍不注意就可能調用錯誤內容,或者忘記調用一個基本方法。當你使用custom editors時,通常需要對editors復制繼承層次結構。任何人在擴展某一類時,必須提供自己的editor,或者湊合著使用你提供的editor。
在調用繼承的情況下,如果你可以避免,不要提供任何Unity信息方法。如果你這樣做,切勿使他們虛擬化。如果需要,你可以定義一個從信息方法調用的空的虛擬函數,子類可以覆蓋此方法來執行其他工作。
public class MyBaseClass
{
public sealed void Update()
{
CustomUpdate();
... // This class's update
}
//Called before this class does its own update
//Override to hook in your own update code.
virtual public void CustomUpdate(){};
}
public class Child : MyBaseClass
{
override public void CustomUpdate()
{
//Do custom stuff
}
}
這樣可以防止某一類意外地覆蓋你的代碼,但是仍能夠賦予其掛鉤連接Unity信息的功能。我不喜歡這種模式的一個原因是事項次序發生問題。在上述示例中,子類可能想在此類自行更新后直接執行。
30.從游戲邏輯分離接口。一般來說,接口組件不應該知道任何關于所應用游戲的任何內容。向它們提供需要可視化的數據,并訂閱事件以查出用戶與它們交互的時間。接口組件不應該創建gamelogic。它們可以篩選輸入,從而確認其有效性,但是主規則處理不應在其他位置發生。在許多拼圖游戲中,拼圖塊是接口的擴展,同時不應該包含任何規則。
(例如,棋子不應該計算自身的合法移動)。
類似地,輸入應該從作用于此輸入的邏輯分離。使用一個通知你的actor移動意圖的輸入控制器;由actor處理是否實際移動。
這里是一個允許用戶從選項列表中選擇武器的UI組件的簡化示例。這些類知曉的唯一游戲內容是武器類(并且只是因為武器是這個容器需要顯示數據的有用源)。此外,游戲也對容器一無所知;它所要做的是注冊OnWeaponSelect事件。
public WeaponSelector : MonoBehaviour
{
public event Action OnWeaponSelect {add; remove; }
//the GameManager can register for this event
public void OnInit(List weapons)
{
foreach(var weapon in weapons)
{
var button = ... //Instantiates a child button and add it to the hierarchy
buttonOnInit(weapon, () => OnSelect(weapon));
// child button displays the option,
// and sends a click-back to this component
}
}
public void OnSelect(Weapon weapon)
{
if(OnWepaonSelect != null) OnWeponSelect(weapon);
}
}
public class WeaponButton : MonoBehaviour
{
private Action<> onClick;
public void OnInit(Weapon weapon, Action onClick)
{
... //set the sprite and text from weapon
this.onClick = onClick;
}
public void OnClick() //Link this method in as the OnClick of the UI Button component
{
Assert.IsTrue(onClick != null); //Should not happen
onClick();
}
}
31. 分離配置,狀態和簿記。
** ** 配置變量是指一類被inspector調整從而通過其屬性定義對象的變量。如maxHealth。
** ** 狀態變量是指一類可完全確定對象當前狀態的變量,以及如果你的游戲支持保存操作,你需要保存的一類變量。如currentHealth。
** ** 簿記變量是指用于速度、方便或過度狀態。它們總是完全可以通過狀態變量確定。如previousHealth。
通過分離這些變量類型,你可以更容易知道哪些是可以更改的,哪些是需要保存的,哪些是需要通過網絡發送/檢索的,并允許你在某種程度上強制執行此類操作。下面給出了一個關于此設置的簡單示例。
public class Player
{
[Serializable]
public class PlayerConfigurationData
{
public float maxHealth;
}
[Serializable]
public class PlayerStateData
{
public float health;
}
public PlayerConfigurationData configuration;
private PlayerState stateData;
//book keeping
private float previousHealth;
public float Health
{
public get { return stateData.health; }
private set { stateData.health = value; }
}
}
32. 避免使用public索引耦合數組。例如,不要定義任何武器數組,任何子彈數組,以及任何顆粒數組,從而使你的代碼類似于:
public void SelectWeapon(int index)
{
currentWeaponIndex = index;
Player.SwitchWeapon(weapons[currentWeapon]);
}
public void Shoot()
{
Fire(bullets[currentWeapon]);
FireParticles(particles[currentWeapon]);
}
這類問題不出在代碼中,而是在inspector進行設置時不發出錯誤。相反,定義封裝三個變量的類,并創建下述數組:
[Serializable]
public class Weapon
{
public GameObject prefab;
public ParticleSystem particles;
public Bullet bullet;
}
此代碼看起來更整潔,但最重要的一點是,在inspector中設置數據更難以出錯。
33. 避免使用除序列以外的結構數組。例如,玩家可能有三種攻擊類型。每種類型使用當前武器,但生成不同的子彈和不通過的行為。
你可能會嘗試將三個子彈轉儲到某個數組中,然后使用此類邏輯:
public void FireAttack()
{
/// behaviour
Fire(bullets[0]);
}
public void IceAttack()
{
/// behaviour
Fire(bullets[1]);
}
public void WindAttack()
{
/// behaviour
Fire(bullets[2]);
}
Enums can make things look better in code…
public void WindAttack()
{
/// behaviour
Fire(bullets[WeaponType.Wind]);
}
最好使用分離變量以便于名稱輔助顯示將放入的內容。使用一類使其整潔。
[Serializable]
public class Bullets
{
public Bullet fireBullet;
public Bullet iceBullet;
public Bullet windBullet;
}
它假設不存在其他火、冰和風的數據。
34. 將數據集中在可序列化類中,以使inspector中的事項更整潔。一些實體可能有幾十個可調分。對于在inspector尋找正確的變量,它可能成為一個噩夢。要使事項更簡便,請遵循以下步驟:
對于各變量組定義分離類。使它們公開化和可序列化。
在主類中,對上述每個類型的變量定義為公開。
切勿在Awake或Start中初始化這些變量;由于它們是可序列化的,Unity會對它進行處理。你可以通過在定義中分配值來指定先前的默認值;
這將變量集中到inspector中的可折疊單元,從而更容易進行管理。
[Serializable]
public class MovementProperties //Not a MonoBehaviour!
{
public float movementSpeed;
public float turnSpeed = 1; //default provided
}
public class HealthProperties //Not a MonoBehaviour!
{
public float maxHealth;
public float regenerationRate;
}
public class Player : MonoBehaviour
{
public MovementProperties movementProeprties;
public HealthPorperties healthProeprties;
}
35.使非MonoBehaviours的類可序列化,即使它們不用于public字段。當 Inspector處于Debug模式下,它允許你查看inspector中的類字段。這同樣適用于嵌套的類(私密或公開)。
36. 避免通過代碼修改那些在Inspector中可編輯的變量。Inspector中可調整的變量即為配置變量,且不應該視為運行期間的常量,更不能作為一個狀態變量。按照這種操作使得將組件狀態重置為初始狀態的編寫方法更加簡便,同時使變量動作更清楚。
public class Actor : MonoBehaviour
{
public float initialHealth = 100;
private float currentHealth;
public void Start()
{
ResetState();
}
private void Respawn()
{
ResetState();
}
private void ResetState()
{
currentHealth = initialHealth;
}
}
模式
模式是指一種按標準方法解決常見問題的途徑。Bob Nystrom著有的《游戲編程模式》(免費在線閱讀)為如何將模式應用于游戲編程中出現的問題提供了一種有效的觀察資源。Unity本身使用了許多模式:Instantiate是原型模式的一個示例;MonoBehaviours遵循樣板模式的一個版本,UI和動畫使用了觀察者模式,而新的動畫引擎利用了狀態機。
這些技巧均涉及到Unity模式的具體應用。
37.為了方便考慮,使用單例模式。下述類將從其自身繼承的任何類自動轉換為單例模式:
public class Singleton<t> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
//Returns the instance of this singleton.
public static T Instance
{
get
{
if(instance == null)
{
instance = (T) FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T) +
" is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
單例模式對于ParticleManager or AudioManager or GUIManager等管理器很有用。
(許多程序員對模糊命名為XManager的類報警,這是因為它指向一個命名不當,或者設計有太多不相關任務的類)。一般來說,我同意這種做法。但是,我們在每個游戲中只有少量的管理器,并且它們在每個游戲中都做同樣的事情,因此這些類實際上是常用語。)
避免對非管理器(如玩家)的Prefabs獨特示例使用單例模式。若不遵守這一原則會使繼承分層復雜化,并使某些變更類別更困難。而是保持引用你的GameManager(或者其他合適的超級類)。針對常在類外部使用的public變量和方法定義靜態屬性和方法。這允許你編寫GameManager.Player,而不是GameManager.Instance.player。
如其他技巧中所述,單例模式也可用于創建持續在追蹤全局數據的場景加載之間的默認派生點和對象。
38.使用狀態機獲取不同狀態下的不同行為或者執行狀態轉換時的代碼。一個輕量級狀態機具有多種狀態,并且每個狀態允許你指定進入或存在狀態的運行動作,以及更新動作。這可以使代碼更清潔,同時具有較少的錯誤傾向。如果你的Update方法代碼有一個改變其動作或者下面的變量的if-或者switch語句,那么你將從狀態機受益:
hasShownGameOverMessage.
public void Update()
{
if(health <= 0)
{
if(!hasShownGameOverMessage)
{
ShowGameOverMessage();
hasShownGameOverMessage = true; //Respawning resets this to false
}
}
else
{
HandleInput();
}
}
若存在更多狀態,這種類型的代碼可能變得非常混亂;狀態機可以使它變得非常清潔。
39.使用類型UnityEvent的字段在inspector中設置觀察者模式。UnityEvent類允許你將占用四個參數的方法鏈接到使用與Buttons上事件相同UI界面的inspector。
40.當一個字段值發生變化時,使用觀察者模式以檢測。只有當游戲中頻繁發生變量變化時才會發生執行代碼的問題。我們已經在一個通用類中創建一種關于此模式的通用解決方案,這樣允許你無論何時發生值變化時注冊事件。以下是一個health示例。其創建方式為:
/*ObservedValue*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };
你現在可以在任何位置更改它,而無需在每個檢查位置執行檢查,例如:
if(hit)health.Value -= 10;
無論何時health值低于0,調用Die方法。更多討論和實施,請參考此發布。
41.在prefabs上使用Actor模式。(這不是一個“標準”模式。其基本理念來自于本文所提及的Kieran Lord。)
Actor是Prefab中的主要組件;通常是提供prefabs“標識”的組件,較高級的代碼將與其經常交互。Actor使用同一對象上(有時在子類上)的其他組件—Helpers—執行工作。如果你通過Unity的菜單創建一個Button對象,它將使用Sprite和Button組件創建一個游戲對象(用Text組件創建一個子類)。在這種情況下,Button是一個actor組件。同樣,除了附連的Camera組件之外,主攝像機一般有多個組件(GUI圖層,Flare圖層,音頻監聽器)。Camera即為一個actor。
Actor可能需要結合其他組件才能正常工作。你可以通過使用下述在actor組件上屬性使prefab更穩健和有用:
1)使用RequiredComponent來指示actor對于相同游戲對象所需的所有組件。(然后你的actor總是安全地調用GetComponent,而無需檢查返回的值是否為空。)
2)使用DisallowMultipleComponent防止附加相同組件的多個實例。然后你的actor總是可以調用GetComponent,而無需擔心當有多個組件附加時應產生什么行為)。
3)若你的actor對象有子類時,使用SelectionBase。這會使你在場景試圖更容易選擇。
[RequiredComponent(typeof(HelperComponent))]
[DisallowMultipleComponent]
[SelectionBase]
public class Actor : MonoBehaviour
{
...//
}
42.對隨機和模式化數據流使用Generators。(雖然這不是一個標準模式,但我們發現它非常有用。)
Generator類似于隨機生成器:它是一種具有可以被調用獲取特定類型新項目的Next方法的對象。在構建期間可以操縱Generators生成各種模式或不同類型的隨機性。它們很有用,因為它們保持生成新道具的邏輯與你需要的項目分離,從而使代碼清潔多了。
這里有幾個實例:
var generator = Generator
.RamdomUniformInt(500)
.Select(x => 2*x); //Generates random even numbers between 0 and 998
var generator = Generator
.RandomUniformInt(1000)
.Where(n => n % 2 == 0); //Same as above
var generator = Generator
.Iterate(0, 0, (m, n) => m + n); //Fibonacci numbers
var generator = Generator
.RandomUniformInt(2)
.Select(n => 2*n - 1)
.Aggregate((m, n) => m + n); //Random walk using steps of 1 or -1 one randomly
var generator = Generator
.Iterate(0, Generator.RandomUniformInt(4), (m, n) => m + n - 1)
.Where(n >= 0); //A random sequence that increases on average
我們使用Generators派生障礙,改變背景色,程序性音樂,生成可能在文字游戲中生成字母的字母序列,等等。此外,Generators在控制以非恒定間隔重復的協程方面也有效,其構造如下:
while (true)
{
//Do stuff
yield return new WaitForSeconds(timeIntervalGenerator.Next());
}
更多關于Generators的討論,請參考此發布。
Prefabs和Scriptable object
43. 對任何事物使用prefabs。你的場景中唯一的游戲對象不應該是prefabs(或者prefabs的一部分),而應該是目錄。即使僅使用一次的唯一對象應該是prefabs。這使得更容易進行無需場景變換的變更。
44. 對prefabs之間互相鏈接;而不要對實例對象互相鏈接。當prefab放置到某個場景中時,維護prefabs鏈接;對于實例鏈接則無需保持。盡可能的使用Prefab之間的鏈接可以減少場景創建的操作,并且減少場景的修改。
如有可能,在實例對象之間自動創建鏈接。如果你需要在實例之間鏈接,則在程序代碼中創建鏈接。例如,玩家prefab在啟動時需要把自己注冊到GameManager,或者GameManager可以在啟動時去查找玩家prefab。
45. 若需要添加其他腳本,不要將Mesh放置在prefabs的根節點上。當你需要從Mesh創建一個prefab時,首先創建一個空的GameObject作為父對象,并用來做根節點。把腳本放到根節點上,而不要放到Mesh節點上。通過采用這種方法,更容易替換Mesh,而不會丟失所有你在Inspector中設置的值。
46. 對共享配置數據,而不是prefabs使用Scriptableobject
若是如此:
1)場景較小
2)你不能錯誤地對單個場景(prefab實例上)進行更改。
47. 對level數據使用scriptableobjects。關卡數據常存儲在XML或JSON中,但使用scriptable objects具有一些優點:
1)它可以在Editor中編輯。這樣更容易驗證數據,并且對非技術領域的設計師更友好。此外,你可以使用自定義編輯器使編輯更容易。
2)你不必操心讀取/編寫和解析數據。
3)它更容易分拆和嵌套,同時管理生成的assets,因此是從構建塊,而非大型配置組成關卡。
48. 使用scriptable objects配置inspector中的行為。Scriptableobjects通常與數據配置相關,但它們也支持將“方法”用作數據。
考慮一個場景,其中你有一個Enemy類型,并且每個敵人有一堆SuperPowers。如果它們在Enemy類中,你可以創建這些常規類,并生成一個列表……若沒有自定義編輯器,你便無法在inspector中設置一個包含不同superpowers的列表(每個具有自身屬性)。但如果你創建這些super powers assets(將它們實現為ScriptableObjects),你就可以進行上述設置!
其構造為:
public class Enemy : MonoBehaviour
{
public SuperPower superPowers;
public UseRandomPower()
{
superPowers.RandomItem().UsePower(this);
}
}
public class BasePower : ScriptableObject
{
virtual void UsePower(Enemy self)
{
}
}
[CreateAssetMenu("BlowFire", "Blow Fire")
public class BlowFire : SuperPower
{
public strength;
override public void UsePower(Enemy self)
{
///program blowing fire here
}
}
當遵循這一模式時,需注意以下幾點:
1)無法可靠地使Scriptable objects抽象化。相反,需要使用具體的基類,并使用抽象方法拋出NotImplementedExceptions。此外,你也可以定義Abstract屬性,并標記應為抽象的類和方法。
2)Scriptableobjects是指無法序列化的通用對象。然而,你可以使用通用基類,并且只對指定所有通用對象的子類抽象化。
49. 使用scriptable objects對prefabs特殊化。若兩個對象的配置僅在某些屬性上不同,則通常在場景中放置兩個實例,并調整這些實例上的屬性。通常較好的做法是創建一個單獨的屬性類,它可以區別兩種類型為一個單獨的scriptableobject類。
這可以提供更多的靈活性:
1)你可以利用從特殊類的繼承,向不同對象類型提供更具體的特定屬性。
2)場景設置更安全(你只要選擇正確的scriptable object,而無需調整所有屬性,便可以創建所需類型的對象)。
3)運行期間,通過代碼更容易操縱這些對象。
4)如果你有這兩種類型的多個實例,你就會知道當進行更改時,它們的屬性將總是保持一致。
5)你可以將配置變量集分拆為可以混合和匹配的集合。
下面舉出了一個關于此設置的簡要示例:
[CreateAssetMenu("HealthProperties.asset", "Health Properties")]
public class HealthProperties : ScriptableObject
{
public float maxHealth;
public float resotrationRate;
}
public class Actor : MonoBehaviour
{
public HealthProperties healthProperties;
}
如果特殊化類的數量較大,你可能要將特殊化類定義為普通類,并使用鏈接到一個包含某些特殊化類的列表,這些特殊化類是鏈接到你可以獲取的適當位置的scriptable object中。
public enum ActorType
{
Vampire, Wherewolf
}
[Serializable]
public class HealthProperties
{
public ActorType type;
public float maxHealth;
public float resotrationRate;
}
[CreateAssetMenu("ActorSpecialization.asset", "Actor Specialization")]
public class ActorSpecialization : ScriptableObject
{
public List healthProperties;
public this[ActorType]
{
get { return healthProperties.First(p => p.type == type); } //Unsafe version!
}
}
public class GameManager : Singleton
{
public ActorSpecialization actorSpecialization;
...
}
public class Actor : MonoBehaviour
{
public ActorType type;
public float health;
//Example usage
public Regenerate()
{
health
+= GameManager.Instance.actorSpecialization[type].resotrationRate;
}
}
50. 使用CreateAssetMenu屬性自動向Asset/Create菜單添加ScriptableObject創建。
調試
51. 學習如何有效地使用Unity的調試工具。
1)向Debug.Log語句添加上下文對象以查看它們的生成位置。
2)在編輯器中使用Debug.Break暫停游戲(例如,當你想產生錯誤條件,并且在該幀上檢查部件屬性時,它很有用)。
3)針對可視化調試使用Debug.DrawRay和Debug.DrawLine功能(例如,當調試為什么沒有光影投射時,DrawRay非常有效)。
4)針對可視化調試使用Gizmos。此外,你可以通過使用DrawGizmo屬性提供mono behaviours外部的gizmo渲染器。
5)使用debug inspector試圖(使用inspector查看運行中的私密字段的值)。
52. 學習如何有效地使用調試器。詳見Visual Studio中的“調試Unity游戲示例”。
53. 使用一個隨著時間的推移繪制數值圖形的可視化調試器。這對于調試物理,動畫和其他動態進程,尤其是偶然性錯誤非常有用。你將能夠從圖中找出錯誤,并能夠同時有哪些其他變量發生了變化。另外,可視化檢查也使某些異常行為變得更明顯,比如說數值變化太頻繁,或者不具明顯原因地發生偏移。我們使用的是Monitor Components,但也有幾種可用的方案。
54. 使用改進的控制臺記錄。使用一個可以根據類別進行顏色編碼輸出,同時可以根據這些類別篩選輸出的編輯器擴展。我們使用的是Editor Console Pro,但也有幾種可用的方案。
55. 使用Unity的測試工具,特別是測試算法和數學代碼。詳見Unity測試工具教程,或者使用Unity測試工具以光速進行事后單元測試。
56. 使用Unity的測試工具以運行“scratchpad”測試。
Unity的測試工具不僅適合正式測試,而且還可以便于進行可以在編輯器中運行,同時無需場景運行的scratch-pad測試。
57. 實現截屏快捷鍵。當你截屏拍照時,許多錯誤是可見的,并且更容易報告。理想化的系統應該在PlayerPrefs保持一個計數器,從而使連續截屏不會被覆蓋。截屏應保存在項目文件夾外,以避免人員將它們誤提交到存儲庫。
58. 實現打印重要變量快照的快捷方式。當你可以檢查的游戲期間發生未知錯誤,這樣更容易記錄一些信息。當然,記錄哪些變量是取決于你的游戲。實例是玩家和敵人的位置,或者AI演員的“思維狀態”(例如嘗試行走的路徑)。
59. 實現一些方便測試的調試選項。下面舉出了一些示例:
解鎖所有道具。
禁用敵人。
禁用GUI。
讓玩家無敵。
禁用所有游戲邏輯。
要注意,切勿不慎提交調試選項;更改調試選項可能會迷惑團隊中的其他開發人員。
60. 定義一些Debug快捷鍵常量,并將它們保存到同一個位置。通常(為方便起見)在一個位置處理Debug鍵,如同其它的游戲輸入一樣。為了避免快捷鍵沖突,在一個中心位置定義所有常量。另一種方法是在某個位置處理所有按鍵輸入,無論它是否是Debug鍵。(其負面效果在于,此類可能需要引用更多的其它對象)。
61. 在程序網格生成時,在頂點繪制或派生小球體。這將幫助你在使用三角形和UVs以顯示網格之前,確定頂點處在期預期的位置,并且網格是正確的尺寸。
性能
62. 請注意關于效能原因設計和構造的通用建議。
1)這些建議通常是基于虛構的,而不是由測試支持的。
2)即便有時建議是由測試支持的,但測試存在錯誤。
3)有時建議是由正確的測試支持,但它們處在不真實的或不同的環境之中。(例如,很容易展現如何比通用列表更快地使用數組。然而,在真實游戲環境中,這種差異幾乎總是可以忽略不計。同樣,若測試適用于除目標設備以外的不同硬件時,它們的結果可能對你無意義。)
4)有時建議是良好的,但卻過時。
5)有時,建議是適用的。然而,存在權衡關系。航運慢速游戲有時要好于非航運快速游戲。而高度優化的游戲更可能包含可以延遲航運的復雜代碼。
效能建議可能有助于記憶,幫助你通過下述進程更快地追蹤實際問題源。
63.從早期階段對目標設備進行定期測試。
不同的設備可能具有顯著不同的效能特性;不要對它們感到吃驚。越早知道問題,你就能越有效地解決問題。
64.知道如何更有效地使用效能評測器以追蹤導致效能問題的原因。
如果你剛接觸效能分析,請參閱效能評測器簡介。
學習如何針對精細度分析來定義你自己的框架(使用Profiler.BeginFrame 和Profiler.EndFrame)。
學習如何使用平臺特定的效能分析,如iOS系統的內置效能分析器。
學習分析內置玩家中的文件,并顯示效能分析器中的數據。
65. 在必要時,使用自定義分析器進行更準確的分析。有時,Unity的效能分析器無法清楚地展示發生的事物;它可能消耗完分析框架,否則深度分析可能減慢游戲速度,以致于測試沒有意義。我們對此使用自有的內部分析器,但應該可以在Asset Store中找到其他替代工具。
66. 衡量效能增強的影響。
當你作出更改提升效能時,衡量它確保該更改著實有效。如果這個更改是不可衡量或凌亂的,請撤銷更改。
67. 不要編寫可讀度減低的代碼,以保證更佳的效能。除非有下述任一情況:
你碰到了一個問題,使用效能分析器識別出問題源,同時相較于可維護性損失,獲得的增益足夠高。或者你清楚自己在做什么。
命名規范和目錄結構
68.遵循一個命名規范和目錄結構。保持命名和目錄結構的一致性可以方便查找,并明確指出具體內容。
你很有可能想要創建自己的命名規范和目錄結構。下面舉出了一個例子。
命名的一般原則
1.按事物本身命名。例如,鳥應該稱為Bird。
- 選擇可以發音,方便記憶的名字。如果你在制作一個與瑪雅文化相關的游戲,不要把關卡命名為QuetzalcoatisReturn。
- 保持一致性。如果你選擇了一個名字,就堅持用它。不要在一處命名buttonHolder,而在其它位置命名buttonContainer。
- 使用Pascal風格的大小寫,例如ComplicatedVerySpecificObject。
不要使用空格,下劃線,或者連字符,但有一個例外
(詳見為同一事物的不同方面命名一節)。 - 不要使用版本數字,或者表示其進度的名詞(WIP,final)。
- 不要使用縮寫:DVamp@W應該寫成DarkVampire@Walk。
- 使用設計文檔中的術語:如果文檔中將一個動畫命名為Die,則使用DarkVampire@Die,而不要用DarkVampire@Death。
- 保持細節修飾詞在左側:DarkVampire,而不是VampireDark;PauseButton,而不是ButtonPaused。舉個例子,在Inspector中查找PauseButton,這要比所有按鈕都以Button開頭更加方便。(很多人傾向于相反次序,認為這樣可以使名稱自然分組。然而,名稱不是用來分組的,目錄才是。名稱是用于在同一類對象中快速辨識的。)
9.某些名稱形成一個序列。在這些名稱中使用數字。例如PathNode0, PathNode1。永遠從0開始,而不是1。 - 對于非序列的情況,不要使用數字。例如 Bird0, Bird1, Bird2,本應該是Flamingo, Eagle,Swallow。
11.為臨時對象添加雙下劃線前綴,例如__Player_Backup
命名同一事物的不同方面
在核心名稱與描述“對象”的事物之間添加下劃線。例如:
GUIbuttons states EnterButton_Active,EnterButton_Inactive
Textures DarkVampire_Diffuse,DarkVampire_Normalmap
Skybox JungleSky_Top,JungleSky_North
LODGroups DarkVampire_LOD0, DarkVampire_LOD1
不要只是為了區分不同類型的項目而使用此類規范,例如Rock_Small, Rock_Large,本應該是SmallRock,LargeRock。
結構
場景,項目目錄和腳本目錄的結構應遵循一個類似的模式。下面列舉了一些精簡示例。
目錄結構
MyGame
Helper
Design
Scratchpad
Materials
Meshes
Actors
DarkVampire
LightVampire
...
Structures
Buildings
...
Props
Plants
...
...
Resources
Actors
Items
...
Prefabs
Actors
Items
...
Scenes
Menus
Levels
Scripts
Tests
Textures
UI
Effects
...
UI
MyLibray
...
Plugins
SomeOtherAsset1
SomeOtherAsset2
...
場景結構
Main
Debug
Managers
Cameras
Lights
UI
Canvas
HUD
PauseMenu
...
World
Ground
Props
Structures
...
Gameplay
Actors
Items
...
Dynamic Objects
腳本目錄結構
Debug
Gameplay
Actors
Items
...
Framework
Graphics
UI
...