原文地址:http://gad.qq.com/program/translateview/7167991
英文版原文
翻譯:高磊(穩(wěn)定心態(tài)) 校審:羅倩(蘑菇)
大約四年前椎组,我發(fā)布了關(guān)于Unity開發(fā)的50個技巧的初始版本。
雖然最新版本與初始版本仍有許多關(guān)聯(lián),但在初始版本之后招盲,我修改了許多內(nèi)容:
Unity更好用效览。例如,我現(xiàn)在信賴FPS計數(shù)器。使用property drawer的功能可以降低編寫customeditors的必要性错沽。同時Prefab的工作方式也降低了顯式嵌套Prefab或替代件的需求谊娇。Scriptable objects更為友好肺孤。
VisualStudio集成度更佳,從而使調(diào)試操作更簡便济欢,同時減少了對于大量Gorilla調(diào)試操作的需求赠堵。
第三方工具和庫更優(yōu)化。AssetStore里現(xiàn)有許多可用的對于可視化調(diào)試和更佳日志記錄等事物的輔助工具法褥。我們自帶(免費(fèi))的擴(kuò)展插件有大量初始發(fā)布中所述的代碼(以及本次版本發(fā)布所述的許多代碼)茫叭。
版本控制更好。(或者可以說半等,我現(xiàn)在知道如何更有效地使用它)揍愁。比方說,無需對Prefab執(zhí)行多個副本或者備份杀饵。
個人經(jīng)驗(yàn)的積累莽囤。在過去四年里,我參與了許多Unity項(xiàng)目切距;包括大量的游戲原型制作朽缎,Father.IO等的游戲制作,以及我們的旗艦工具Unity asset Grids谜悟。
本文是在考慮上述所有內(nèi)容的基礎(chǔ)上對初始版本進(jìn)行的修訂版本话肖。
在繼續(xù)討論技巧前,本人現(xiàn)發(fā)布以下免責(zé)聲明(與初始版本基本相同):
這些技巧并不適用于每個Unity項(xiàng)目赌躺。
這些技巧是基于本人與3到20人的小型團(tuán)隊協(xié)作參與項(xiàng)目所獲得的經(jīng)驗(yàn)狼牺。在結(jié)構(gòu)性,可重用性礼患,清晰度等方面存在費(fèi)用——根據(jù)團(tuán)隊規(guī)模是钥,項(xiàng)目規(guī)模和項(xiàng)目目標(biāo)來確定是否此費(fèi)用掠归。比方說,你可能不會在游戲制作環(huán)節(jié)使用所有內(nèi)容悄泥。
許多技巧涉及個人喜好問題(雖然這里列出的技巧之間可能有競爭虏冻,但都是很好的技巧)。
此外弹囚,Unity也在其官網(wǎng)上發(fā)布了一些最佳操作實(shí)例(雖然它們中大多數(shù)是從效能角度出發(fā)):
1)http://unity3d.com/learn/tutorials/topics/best-practices
2)基于物理的內(nèi)容創(chuàng)建的最佳操作實(shí)例:https://youtu.be/OeEYEUCa4tI
3)Unity中的最佳2D操作實(shí)例:https://youtu.be/HM17mAmLd7k
4)Unity內(nèi)部技巧和技巧: https://youtu.be/Ozc_hXzp_KU
5)Unity提示和技巧:https://youtu.be/2S6Ygq58QF8
6)http://docs.unity3d.com/Manual/HOWTO-ArtAssetBestPracticeGuide.html
開發(fā)流程
1.確定開始的縮放比例厨相,并以相同縮放比例構(gòu)建所有原型。否則鸥鹉,你可能需要后續(xù)重做assets(例如蛮穿,無法總是正確地縮放動畫)。對于3D游戲毁渗,采用1 Unity單位= 1m通常是最佳的践磅。對于不使用照明或物理的2D游戲,采用1 Unity單位 = 1 像素(在“設(shè)計”分辨率階段)通常是較好的灸异。對于UI(以及2D游戲)府适,選擇設(shè)計分辨率(我們使用HD或2xHD,并將所有assets設(shè)計為以此分辨率縮放肺樟。
2. 使每個場景都可以運(yùn)行檐春。這樣可以避免為了運(yùn)行游戲而必須轉(zhuǎn)換場景,從而加快了測試速度么伯。如果要在所有場景中必需的場景加載之間持續(xù)存在對象疟暖,這可能需要技巧。一種方法是當(dāng)持續(xù)對象不存在于場景中時蹦狂,使它們作為可自行加載的單例模式誓篱。另一個技巧中將詳述單例模式朋贬。
3. 使用源代碼控制凯楔,并學(xué)習(xí)如何有效地使用它。
將assets序列化為文本锦募。實(shí)際上摆屯,它并不會提高場景和Prefab的可合并性,但它會使變化更容易觀測糠亩。
采用場景和Prefab共享策略虐骑。一般來說,多個人不應(yīng)在同一場景或Prefab工作赎线。對于小型制作團(tuán)隊廷没,只要在開始工作前確保沒有人制作場景或Prefab即可。交換表示場景所有權(quán)的物理標(biāo)記可能很有用(如果桌面上有場景標(biāo)記垂寥,你僅可以在某一場景中工作)颠黎。
將標(biāo)簽作為書簽另锋。
確定并堅持采用分支策略。由于場景和Prefab不能平滑地合并狭归,分支稍顯復(fù)雜夭坪。然而當(dāng)你決定使用分支時,它應(yīng)該結(jié)合場景和Prefab共享策略使用过椎。
使用子模塊時要小心室梅。子模型可能是維護(hù)可重用代碼的最佳途徑。但需注意幾個警告事項(xiàng):
元數(shù)據(jù)文件通常在多個項(xiàng)目中不一致疚宇。對于非Monobehaviour或非Scriptable object代碼而言亡鼠,這通常不是問題,但對于MonoBehaviours和Scriptable objects使用子模塊可能會導(dǎo)致代碼丟失敷待。
如果你參與許多項(xiàng)目(包括一個或多個子模塊項(xiàng)目)拆宛,倘若你必須對幾次迭代中的多個項(xiàng)目執(zhí)行獲取—合并—提交—推送操作以穩(wěn)定所有項(xiàng)目的代碼,有時會發(fā)生更新崩潰(并且如果其他人同時進(jìn)行變更讼撒,它可能會轉(zhuǎn)變?yōu)槌掷m(xù)崩潰)浑厚。一種最大程度上降低此效應(yīng)的方法是在項(xiàng)目初始階段對子模塊進(jìn)行更改。如此一來根盒,總是需要推送僅使用子模塊的項(xiàng)目钳幅;它們從來無需推回。
4. 保持測試場景和代碼分離炎滞。向存儲庫提交臨時資源和腳本敢艰,并在完成后將它們移出項(xiàng)目。
5. 如果你要更新工具(尤其是Unity)册赛,必須同時進(jìn)行钠导。當(dāng)你使用一個與先前不同的版本打開項(xiàng)目時,Unity能夠更好地保留鏈接森瘪,但倘若人們使用不同的版本牡属,有時仍然會丟失鏈接。
6. 在一個干凈的項(xiàng)目中導(dǎo)入第三方assets扼睬,并從中導(dǎo)出一個可供自己使用的新的資源包逮栅。當(dāng)你直接向項(xiàng)目導(dǎo)入這些資源,它們有時會導(dǎo)致問題:
可能存在沖突(文件或文件名)窗宇,尤其對于在插件目錄根中存在文件或者在實(shí)例中使用StandardAssets中assets的資源措伐。
這些資源可能被無序地放入到自有項(xiàng)目的文件中。如果你決定不使用或者想要移除這些assets军俊,這可能成為一個重要問題侥加。
請按照下述步驟使assets導(dǎo)入更安全:
1)創(chuàng)建一個新項(xiàng)目,然后導(dǎo)入asset粪躬。
2)運(yùn)行實(shí)例并確保它們能夠工作担败。
3)將asset排列為一個更合適的目錄結(jié)構(gòu)矗蕊。(我通常不對一個資源強(qiáng)制排列自有的目錄結(jié)構(gòu)。但是我確保所有文件均在一個目錄中氢架,同時在重要位置不存在任何可能會覆蓋項(xiàng)目中現(xiàn)有文件的文件傻咖。
4)運(yùn)行實(shí)例并確保它們?nèi)钥梢怨ぷ鳌#ㄓ袝r岖研,當(dāng)我移動事物時會導(dǎo)致assets損壞卿操,但這通常不應(yīng)該是一個問題)。
5)現(xiàn)要移除所有無需的事物(如實(shí)例)孙援。
6)確保asset仍可編譯害淤,并且Prefab仍然擁有所有自身的鏈接。若留下任何需運(yùn)行的事項(xiàng)拓售,則對它進(jìn)行測試窥摄。
7)現(xiàn)選定所有assets,并導(dǎo)出一個資源包础淤。
8)導(dǎo)入到你的項(xiàng)目中崭放。
7. 自動構(gòu)建進(jìn)程。甚至對于小型項(xiàng)目鸽凶,這步很有用币砂,但對于以下情況尤為適用:
你需要構(gòu)建許多不同的游戲版本。
其他擁有不同程度技術(shù)知識的團(tuán)隊成員需要進(jìn)行構(gòu)建玻侥,或者
你需要對項(xiàng)目進(jìn)行小幅調(diào)整后才能進(jìn)行構(gòu)建决摧。
詳見Unity構(gòu)建編譯:對于如何執(zhí)行的較好指導(dǎo)的基本和高級可能性。
8. 為你的設(shè)置建立文檔凑兰。大部分記錄應(yīng)在代碼中掌桩,但是某些事項(xiàng)應(yīng)記錄在代碼外。制作設(shè)計師通過耗時的設(shè)置來篩選代碼姑食。文檔化的設(shè)置可以提高效率(若文檔是最新的)波岛。
對下述內(nèi)容建立文檔:
標(biāo)簽使用。
圖層使用(對于碰撞矢门,剔除和光線投射—從本質(zhì)上來說盆色,每個圖層對應(yīng)的使用)灰蛙。
圖層的GUI深度(每個圖層對應(yīng)的顯示)
場景設(shè)置祟剔。
復(fù)雜Prefab的Prefab結(jié)構(gòu)。
常用語偏好摩梧。
構(gòu)建設(shè)置物延。
通用編碼
9. 將所有代碼放入一個命名空間中。這避免了自有庫和第三方代碼之間可能發(fā)生的代碼沖突仅父。但不要依賴于命名空間以避免與重要類沖突叛薯。即使你會使用不同的命名空間浑吟,也不要將“對象”、“動作”或“事件”作為類名稱耗溜。
10. 使用斷言组力。斷言對于代碼中不變量的測試非常有用,它能夠輔助清除邏輯錯誤抖拴。Unity.Assertions.Assert類提供了可用的斷言燎字。它們都可以測試一些條件,但如果不符合條件阿宅,則在控制臺中寫入錯誤信息候衍。如果你不熟悉如何有效地使用斷言,請參考使用斷言編程的優(yōu)點(diǎn)(a.k.a.斷言語句)洒放。
11. 切勿對顯示文本以外的任何事項(xiàng)使用字符串蛉鹿。尤其應(yīng)注意,不要使用字符串來標(biāo)識對象或Prefab往湿。但存在一些例外情形(仍然有一些內(nèi)容只能通過Unity中的名稱訪問)妖异。在這種情形下,將這些字符串定義為“AnimationNames”或 “AudioModuleNames”等文件中的常量领追。倘若這些類變?yōu)椴豢晒芾硭婀耄褂们短最惡蟊憧深愃泼鸄nimationNames.Player.Run。
12. 不要使用“Invoke”和“SendMessage”蔓腐。這些MonoBehaviour方法通過名稱調(diào)用其他方法掷伙。通過名稱調(diào)用的方法難以在代碼中追蹤(無法找到“Usages”杉适,而“發(fā)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是一個無參數(shù)的void法祷嘶。
如果你實(shí)現(xiàn)自己的基礎(chǔ)MonoBehaviour,你可以向其中添加自己的“Invoke”撩幽。
另一種較安全的“SendMessage”方法更難以實(shí)施兜挨。與之相反,我通常使用“GetComponent”變量以獲取父對象葬燎,當(dāng)前游戲?qū)ο蠡蜃訉ο蟮慕M件误甚,并直接執(zhí)行調(diào)用。
13.當(dāng)游戲運(yùn)行時谱净,不要讓派生對象混亂層次結(jié)構(gòu)窑邦。將它們的父對象設(shè)為場景對象,以便在游戲運(yùn)行時更容易找到內(nèi)容壕探。你可以使用一個空游戲?qū)ο蟾郧眨蛘呱踔潦褂靡粋€無行為的單例模式(詳見本文后面的部分),從而更容易地從代碼進(jìn)行訪問李请。將此對象命名為“DynamicObjects”瞧筛。
14. 明確是否要將空值(null)作為一個合法值厉熟,并盡量避免這么做
空值可輔助檢測錯誤代碼。但是较幌,如果你使“if”默默地通過空值成為一種習(xí)慣揍瑟,錯誤代碼將很快運(yùn)行,同時你只能在很久之后才會注意到錯誤乍炉。此外月培,隨著每個圖層通過空變量,它可以在代碼深度暴露恩急。我嘗試避免將空值整體作為一個合法值杉畜。
我優(yōu)先采用的常用語不是進(jìn)行任何空檢查,倘若它是一個問題衷恭,讓代碼失敗此叠。有時,在“可重用”方法中随珠,我將檢查出一個值為空的變量灭袁,并拋出一個異常,而不是將它傳遞至其它可能失敗的方法窗看。
在某些情形下茸歧,值可以合法為空,并且需要采取不同的方式處理显沈。在此類情況下软瞎,添加注釋來解釋什么時候某些內(nèi)容可能為空,并說明為什么可能為空拉讯。
常見場景通常用于inspector配置的值涤浇。用戶可以指定一個值,但如果未指定任何值魔慷,則使用一個默認(rèn)值只锭。最好結(jié)合包含T值的可選類。(這有點(diǎn)像“可為空”)院尔。你可以使用一個特殊的屬性渲染器來渲染一個勾選框蜻展,若勾選,則僅顯示數(shù)值框邀摆。
(但切勿直接使用泛型類纵顾,你必須擴(kuò)展特定T值的類)。
[Serializable]
public class Optional<t>
{
public bool useCustomValue;
public T value;
}
在你的代碼中隧熙,你可以采取這種使用途徑:
health= healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;
15. 如果你使用“協(xié)程”片挂,學(xué)習(xí)如何有效地使用它。
“協(xié)程”是解決許多問題的一種最有效的方法贞盯。但是難以對“協(xié)程”進(jìn)行調(diào)式音念,同時你可以很容易地對它進(jìn)行混亂的編碼,從而使其他人躏敢,甚至包括你自己也無法理解其意義闷愤。
你應(yīng)該知道:
1)如何并發(fā)執(zhí)行協(xié)程。
2)如何按序執(zhí)行協(xié)程件余。
3)如何從現(xiàn)有程序中創(chuàng)建新的協(xié)程讥脐。
4)如何使用“CustomYieldInstruction”創(chuàng)建自定義協(xié)程。
//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. 利用擴(kuò)展法來協(xié)同共享接口的組件啼器。有時可以方便地獲取實(shí)施某個接口的組件旬渠,或者找到這些組件相應(yīng)的對象。
下述實(shí)例使用typeof而不是這些函數(shù)的通用版本端壳。通用版本無法協(xié)同接口使用告丢,但typeof卻可以。下面的方法將其整潔地套入通用方法之中损谦。
public static TInterface GetInterfaceComponent<tinterface>(this Component thisComponent)
where TInterface : class
{
return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}
17. 利用擴(kuò)展法使語法更簡潔岖免。例如:
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強(qiáng)制組件關(guān)系可能難以操作照捡,但是這總是可能和可取的颅湘,特別是當(dāng)你調(diào)用其它類上的GetComponent。作為一種替代方法栗精,但需要某個組件打印找到的錯誤信息時闯参,可以使用下述GameObject擴(kuò)展。
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. 避免對相同的事項(xiàng)使用不同的常用語悲立。在許多情況下赢赊,有多種常用法。此時级历,對整個項(xiàng)目選擇一種常用法释移。其原因在于:
1)某些常用語不能一起工作。在某個方向中使用一種常用語強(qiáng)行設(shè) 計可能不適合另一種常用語寥殖。
2)對于整個項(xiàng)目使用相同的常用語能夠使團(tuán)隊成員更容易理解進(jìn)展玩讳。它使結(jié)構(gòu)和代碼更容易理解。這樣就更難犯錯嚼贡。
常用語組示例:
協(xié)程與狀態(tài)機(jī)熏纯。
嵌套的Prefab,互相鏈接的Prefab和超級Prefab
數(shù)據(jù)分離策略粤策。
對2D游戲中狀態(tài)使用sprites的方法樟澜。
Prefab結(jié)構(gòu)。
派生策略。
定位對象的方法:按類型秩贰,按名稱霹俺,按標(biāo)簽,按圖層和按引用關(guān)系(“鏈接”)毒费。
分組對象的方法:按類型丙唧,按名稱,按標(biāo)簽觅玻,按圖層和按引用數(shù)組(“鏈接”)想际。
調(diào)用其他組件方法的途徑。
查找對象組和自注冊溪厘。
控制執(zhí)行次序(使用Unity的執(zhí)行次序設(shè)置胡本,還是使用yield邏輯,利用Awake / Start和Update / Late Update依賴畸悬,還是使用純手動的方法侧甫,或者采用次序無關(guān)的架構(gòu))。
在游戲中使用鼠標(biāo)選擇對象/位置/目標(biāo):SelectionManager或者對象自主管理傻昙。
在場景變換時保存數(shù)據(jù):通過PlayerPrefs闺骚,或者是在新場景加載時未毀損的對象。
組合(混合妆档、添加和分層)動畫的方法僻爽。
輸入處理(中央和本地)
20. 維護(hù)一個自有的Time類,這可以更容易實(shí)現(xiàn)游戲暫停贾惦。包裝一個“Time.DeltaTime”和“Time.TimeSinceLevelLoad”來實(shí)現(xiàn)暫停和游戲速度的縮放胸梆。它使用時有點(diǎn)麻煩,但是當(dāng)對象運(yùn)行在不同的時鐘速率下就容易多了(例如界面動畫和游戲動畫)须板。
21.需要更新的自定義類不應(yīng)該訪問全局靜態(tài)時間碰镜。相反,它們應(yīng)將增量時間作為它們Update方法的一個參數(shù)习瑰。當(dāng)你如上所述實(shí)施一個暫停系統(tǒng)绪颖,或者當(dāng)你想要加快或減慢自定義類的行為時,這樣使這些類變?yōu)榭捎谩?/p>
22. 使用常見結(jié)構(gòu)進(jìn)行WWW調(diào)用甜奄。在擁有很多服務(wù)器通信的游戲中柠横,通常有幾十個WWW調(diào)用。無論你是使用Unity的原始WWW類還是使用某個插件课兄,你可以從生成樣板文件的頂部寫入一個薄層獲益牍氛。
我通常定義一個Call方法(分別針對Get和Post),即CallImpl協(xié)程和MakeHandler烟阐。從本質(zhì)上來說搬俊,Call方法通過采用MakeHandler法紊扬,從一個解析器,成功和失敗的處理器構(gòu)建出一個super hander唉擂。此外餐屎,它也調(diào)用CallImpl協(xié)程,創(chuàng)建一個URL楔敌,進(jìn)行調(diào)用啤挎,等待直至完成驻谆,然后調(diào)用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");
}
}
它具有一些優(yōu)點(diǎn):
它允許你避免編寫大量樣板代碼。
它允許你在中央位置處理某些事項(xiàng)(例如顯示加載的UI組件或處理某些通用錯誤)胜臊。
23. 如果你有大量文本勺卢,將它們放在同一個文件中。不要將它們放入inspector將編輯的字段中象对。使其在無需打開Unity編輯器黑忱,尤其是無需保存場景的前提下易于更改。
24. 如果你想執(zhí)行本地化勒魔,將所有字符串分離到同一個位置甫煞。有很多方法可以實(shí)現(xiàn)這一點(diǎn)。一種方法是針對每個字符串定義一個具有public字符串字段的Text類冠绢,例如默認(rèn)設(shè)為英文抚吠。其他語言將其子類化,并使用同等語言重新初始化這些字段弟胀。
一些更復(fù)雜的技術(shù)(其適用情形是正文本較大和/或語言數(shù)量較多時)將讀取到一個電子表格中楷力,并基于所選語言提供選擇正確字符串的邏輯。
類的設(shè)計
25.確定實(shí)現(xiàn)可檢查字段的方法孵户,并將其確立為標(biāo)準(zhǔn)萧朝。有兩種方法:使字段public,或者使它們private并標(biāo)記為[可序列化]夏哭。后者“更正確”但不太方便(當(dāng)然不是Unity本身常用的方法)检柬。無論你選擇哪種方式,將它確立為標(biāo)準(zhǔn)竖配,以便于團(tuán)隊中開發(fā)人員知道如何解釋一個public字段何址。
可檢查字段是public的。在這種情況下械念,public表示“設(shè)計師在 運(yùn)行時更改此變量是安全的头朱。避免在代碼中設(shè)置該值”。
可檢查字段是private龄减,并被標(biāo)記為“可序列化”项钮。 在這種情 況下,public表示“在代碼中更改此變量是安全的”(因此,你不應(yīng)該看到太多烁巫,并且在MonoBehaviours 和ScriptableObjects中不應(yīng)該有任何public字段)署隘。
26. 對于組件,切勿使不應(yīng)在inspector中調(diào)整的變量成為public亚隙。否則磁餐,它們將被設(shè)計師調(diào)整,特別是當(dāng)不清楚它是什么時阿弃。在某些罕見的情況下诊霹,這是無法避免的。此時渣淳,使用兩條脾还,甚至四條下劃線對變量名添加前綴以警告調(diào)整人員:
public float __aVariable;
27. 使用Property Drawers使字段更加用戶友好。可以使用Property Drawers自定義inspector中的控制入愧。這樣可以使你能夠創(chuàng)建更適合數(shù)據(jù)性質(zhì)的控制鄙漏,并實(shí)施某些安全保護(hù)(如限定變量范圍)。
28. 相較于Custom Editors棺蛛,更偏好采用PropertyDrawers怔蚌。Property Drawers是根據(jù)字段類型實(shí)現(xiàn)的,因此涉及的工作量要少得多旁赊。另外桦踊,它們的重用性更佳—一旦實(shí)現(xiàn)某一類型,它們可應(yīng)用于包含此類型的任何類彤恶。而Custom Editors是根據(jù)MonoBehaviour實(shí)現(xiàn)的钞钙,因此重用性更少,涉及的工作量更多声离。
29.默認(rèn)密封MonoBehaviours芒炼。一般來說,UnityMonoBehaviours的繼承友好不高:
類似于Start和Update术徊,Unity調(diào)用信息的方式使得在子類中難以使用這些方法本刽。你稍不注意就可能調(diào)用錯誤內(nèi)容,或者忘記調(diào)用一個基本方法赠涮。當(dāng)你使用custom editors時子寓,通常需要對editors復(fù)制繼承層次結(jié)構(gòu)。任何人在擴(kuò)展某一類時笋除,必須提供自己的editor斜友,或者湊合著使用你提供的editor。
在調(diào)用繼承的情況下垃它,如果你可以避免鲜屏,不要提供任何Unity信息方法烹看。如果你這樣做,切勿使他們虛擬化洛史。如果需要惯殊,你可以定義一個從信息方法調(diào)用的空的虛擬函數(shù),子類可以覆蓋此方法來執(zhí)行其他工作也殖。
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信息的功能。我不喜歡這種模式的一個原因是事項(xiàng)次序發(fā)生問題忆嗜。在上述示例中己儒,子類可能想在此類自行更新后直接執(zhí)行。
30.從游戲邏輯分離接口霎褐。一般來說址愿,接口組件不應(yīng)該知道任何關(guān)于所應(yīng)用游戲的任何內(nèi)容该镣。向它們提供需要可視化的數(shù)據(jù)冻璃,并訂閱事件以查出用戶與它們交互的時間。接口組件不應(yīng)該創(chuàng)建gamelogic损合。它們可以篩選輸入省艳,從而確認(rèn)其有效性,但是主規(guī)則處理不應(yīng)在其他位置發(fā)生嫁审。在許多拼圖游戲中跋炕,拼圖塊是接口的擴(kuò)展,同時不應(yīng)該包含任何規(guī)則律适。
(例如辐烂,棋子不應(yīng)該計算自身的合法移動)。
類似地捂贿,輸入應(yīng)該從作用于此輸入的邏輯分離纠修。使用一個通知你的actor移動意圖的輸入控制器;由actor處理是否實(shí)際移動厂僧。
這里是一個允許用戶從選項(xiàng)列表中選擇武器的UI組件的簡化示例扣草。這些類知曉的唯一游戲內(nèi)容是武器類(并且只是因?yàn)槲淦魇沁@個容器需要顯示數(shù)據(jù)的有用源)。此外颜屠,游戲也對容器一無所知辰妙;它所要做的是注冊O(shè)nWeaponSelect事件。
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. 分離配置甫窟,狀態(tài)和簿記密浑。
** ** 配置變量是指一類被inspector調(diào)整從而通過其屬性定義對象的變量。如maxHealth粗井。
** ** 狀態(tài)變量是指一類可完全確定對象當(dāng)前狀態(tài)的變量尔破,以及如果你的游戲支持保存操作敬锐,你需要保存的一類變量。如currentHealth呆瞻。
** ** 簿記變量是指用于速度台夺、方便或過度狀態(tài)。它們總是完全可以通過狀態(tài)變量確定痴脾。如previousHealth颤介。
通過分離這些變量類型,你可以更容易知道哪些是可以更改的赞赖,哪些是需要保存的滚朵,哪些是需要通過網(wǎng)絡(luò)發(fā)送/檢索的,并允許你在某種程度上強(qiáng)制執(zhí)行此類操作前域。下面給出了一個關(guān)于此設(shè)置的簡單示例辕近。
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索引耦合數(shù)組。例如匿垄,不要定義任何武器數(shù)組移宅,任何子彈數(shù)組,以及任何顆粒數(shù)組椿疗,從而使你的代碼類似于:
public void SelectWeapon(int index)
{
currentWeaponIndex = index;
Player.SwitchWeapon(weapons[currentWeapon]);
}
public void Shoot()
{
Fire(bullets[currentWeapon]);
FireParticles(particles[currentWeapon]);
}
這類問題不出在代碼中漏峰,而是在inspector進(jìn)行設(shè)置時不發(fā)出錯誤。相反届榄,定義封裝三個變量的類浅乔,并創(chuàng)建下述數(shù)組:
[Serializable]
public class Weapon
{
public GameObject prefab;
public ParticleSystem particles;
public Bullet bullet;
}
此代碼看起來更整潔,但最重要的一點(diǎn)是铝条,在inspector中設(shè)置數(shù)據(jù)更難以出錯靖苇。
33. 避免使用除序列以外的結(jié)構(gòu)數(shù)組。例如班缰,玩家可能有三種攻擊類型贤壁。每種類型使用當(dāng)前武器,但生成不同的子彈和不通過的行為鲁捏。
你可能會嘗試將三個子彈轉(zhuǎn)儲到某個數(shù)組中芯砸,然后使用此類邏輯:
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]);
}
最好使用分離變量以便于名稱輔助顯示將放入的內(nèi)容。使用一類使其整潔给梅。
[Serializable]
public class Bullets
{
public Bullet fireBullet;
public Bullet iceBullet;
public Bullet windBullet;
}
它假設(shè)不存在其他火假丧、冰和風(fēng)的數(shù)據(jù)。
34. 將數(shù)據(jù)集中在可序列化類中动羽,以使inspector中的事項(xiàng)更整潔包帚。一些實(shí)體可能有幾十個可調(diào)分。對于在inspector尋找正確的變量运吓,它可能成為一個噩夢渴邦。要使事項(xiàng)更簡便疯趟,請遵循以下步驟:
對于各變量組定義分離類。使它們公開化和可序列化谋梭。
在主類中信峻,對上述每個類型的變量定義為公開。
切勿在Awake或Start中初始化這些變量瓮床;由于它們是可序列化的盹舞,Unity會對它進(jìn)行處理。你可以通過在定義中分配值來指定先前的默認(rèn)值隘庄;
這將變量集中到inspector中的可折疊單元踢步,從而更容易進(jìn)行管理。
[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字段获印。當(dāng) Inspector處于Debug模式下,它允許你查看inspector中的類字段街州。這同樣適用于嵌套的類(私密或公開)兼丰。
36. 避免通過代碼修改那些在Inspector中可編輯的變量。Inspector中可調(diào)整的變量即為配置變量菇肃,且不應(yīng)該視為運(yùn)行期間的常量地粪,更不能作為一個狀態(tài)變量。按照這種操作使得將組件狀態(tài)重置為初始狀態(tài)的編寫方法更加簡便琐谤,同時使變量動作更清楚。
public class Actor : MonoBehaviour
{
public float initialHealth = 100;
private float currentHealth;
public void Start()
{
ResetState();
}
private void Respawn()
{
ResetState();
}
private void ResetState()
{
currentHealth = initialHealth;
}
}
模式
模式是指一種按標(biāo)準(zhǔn)方法解決常見問題的途徑玩敏。Bob Nystrom著有的《游戲編程模式》(免費(fèi)在線閱讀)為如何將模式應(yīng)用于游戲編程中出現(xiàn)的問題提供了一種有效的觀察資源斗忌。Unity本身使用了許多模式:Instantiate是原型模式的一個示例;MonoBehaviours遵循樣板模式的一個版本旺聚,UI和動畫使用了觀察者模式织阳,而新的動畫引擎利用了狀態(tài)機(jī)。
這些技巧均涉及到Unity模式的具體應(yīng)用砰粹。
37.為了方便考慮唧躲,使用單例模式。下述類將從其自身繼承的任何類自動轉(zhuǎn)換為單例模式:
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的類報警弄痹,這是因?yàn)樗赶蛞粋€命名不當(dāng),或者設(shè)計有太多不相關(guān)任務(wù)的類)嵌器。一般來說肛真,我同意這種做法。但是爽航,我們在每個游戲中只有少量的管理器蚓让,并且它們在每個游戲中都做同樣的事情乾忱,因此這些類實(shí)際上是常用語。)
避免對非管理器(如玩家)的Prefabs獨(dú)特示例使用單例模式历极。若不遵守這一原則會使繼承分層復(fù)雜化窄瘟,并使某些變更類別更困難。而是保持引用你的GameManager(或者其他合適的超級類)趟卸。針對常在類外部使用的public變量和方法定義靜態(tài)屬性和方法寞肖。這允許你編寫GameManager.Player,而不是GameManager.Instance.player衰腌。
如其他技巧中所述新蟆,單例模式也可用于創(chuàng)建持續(xù)在追蹤全局?jǐn)?shù)據(jù)的場景加載之間的默認(rèn)派生點(diǎn)和對象。
38.使用狀態(tài)機(jī)獲取不同狀態(tài)下的不同行為或者執(zhí)行狀態(tài)轉(zhuǎn)換時的代碼右蕊。一個輕量級狀態(tài)機(jī)具有多種狀態(tài)琼稻,并且每個狀態(tài)允許你指定進(jìn)入或存在狀態(tài)的運(yùn)行動作,以及更新動作饶囚。這可以使代碼更清潔帕翻,同時具有較少的錯誤傾向。如果你的Update方法代碼有一個改變其動作或者下面的變量的if-或者switch語句萝风,那么你將從狀態(tài)機(jī)受益:
hasShownGameOverMessage.
public void Update()
{
if(health <= 0)
{
if(!hasShownGameOverMessage)
{
ShowGameOverMessage();
hasShownGameOverMessage = true; //Respawning resets this to false
}
}
else
{
HandleInput();
}
}
若存在更多狀態(tài)嘀掸,這種類型的代碼可能變得非常混亂规惰;狀態(tài)機(jī)可以使它變得非常清潔睬塌。
39.使用類型UnityEvent的字段在inspector中設(shè)置觀察者模式。UnityEvent類允許你將占用四個參數(shù)的方法鏈接到使用與Buttons上事件相同UI界面的inspector歇万。
40.當(dāng)一個字段值發(fā)生變化時揩晴,使用觀察者模式以檢測。只有當(dāng)游戲中頻繁發(fā)生變量變化時才會發(fā)生執(zhí)行代碼的問題贪磺。我們已經(jīng)在一個通用類中創(chuàng)建一種關(guān)于此模式的通用解決方案硫兰,這樣允許你無論何時發(fā)生值變化時注冊事件。以下是一個health示例寒锚。其創(chuàng)建方式為:
/*ObservedValue*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };
你現(xiàn)在可以在任何位置更改它劫映,而無需在每個檢查位置執(zhí)行檢查,例如:
if(hit)health.Value -= 10;
無論何時health值低于0刹前,調(diào)用Die方法泳赋。更多討論和實(shí)施,請參考此發(fā)布腮郊。
41.在prefabs上使用Actor模式摹蘑。(這不是一個“標(biāo)準(zhǔn)”模式。其基本理念來自于本文所提及的Kieran Lord轧飞。)
Actor是Prefab中的主要組件衅鹿;通常是提供prefabs“標(biāo)識”的組件撒踪,較高級的代碼將與其經(jīng)常交互。Actor使用同一對象上(有時在子類上)的其他組件—Helpers—執(zhí)行工作大渤。如果你通過Unity的菜單創(chuàng)建一個Button對象制妄,它將使用Sprite和Button組件創(chuàng)建一個游戲?qū)ο螅ㄓ肨ext組件創(chuàng)建一個子類)。在這種情況下泵三,Button是一個actor組件耕捞。同樣,除了附連的Camera組件之外烫幕,主攝像機(jī)一般有多個組件(GUI圖層俺抽,F(xiàn)lare圖層,音頻監(jiān)聽器)较曼。Camera即為一個actor磷斧。
Actor可能需要結(jié)合其他組件才能正常工作。你可以通過使用下述在actor組件上屬性使prefab更穩(wěn)健和有用:
1)使用RequiredComponent來指示actor對于相同游戲?qū)ο笏璧乃薪M件捷犹。(然后你的actor總是安全地調(diào)用GetComponent弛饭,而無需檢查返回的值是否為空赎离。)
2)使用DisallowMultipleComponent防止附加相同組件的多個實(shí)例粟矿。然后你的actor總是可以調(diào)用GetComponent,而無需擔(dān)心當(dāng)有多個組件附加時應(yīng)產(chǎn)生什么行為)迹辐。
3)若你的actor對象有子類時枪孩,使用SelectionBase憔晒。這會使你在場景試圖更容易選擇。
[RequiredComponent(typeof(HelperComponent))]
[DisallowMultipleComponent]
[SelectionBase]
public class Actor : MonoBehaviour
{
...//
}
42.對隨機(jī)和模式化數(shù)據(jù)流使用Generators销凑。(雖然這不是一個標(biāo)準(zhǔn)模式丛晌,但我們發(fā)現(xiàn)它非常有用。)
Generator類似于隨機(jī)生成器:它是一種具有可以被調(diào)用獲取特定類型新項(xiàng)目的Next方法的對象斗幼。在構(gòu)建期間可以操縱Generators生成各種模式或不同類型的隨機(jī)性。它們很有用抚垄,因?yàn)樗鼈儽3稚尚碌谰叩倪壿嬇c你需要的項(xiàng)目分離蜕窿,從而使代碼清潔多了。
這里有幾個實(shí)例:
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在控制以非恒定間隔重復(fù)的協(xié)程方面也有效纺腊,其構(gòu)造如下:
while (true)
{
//Do stuff
yield return new WaitForSeconds(timeIntervalGenerator.Next());
}
更多關(guān)于Generators的討論畔咧,請參考此發(fā)布茎芭。
Prefabs和Scriptable object
43. 對任何事物使用prefabs。你的場景中唯一的游戲?qū)ο蟛粦?yīng)該是prefabs(或者prefabs的一部分)誓沸,而應(yīng)該是目錄梅桩。即使僅使用一次的唯一對象應(yīng)該是prefabs。這使得更容易進(jìn)行無需場景變換的變更拜隧。
44. 對prefabs之間互相鏈接宿百;而不要對實(shí)例對象互相鏈接。當(dāng)prefab放置到某個場景中時洪添,維護(hù)prefabs鏈接垦页;對于實(shí)例鏈接則無需保持。盡可能的使用Prefab之間的鏈接可以減少場景創(chuàng)建的操作干奢,并且減少場景的修改痊焊。
如有可能,在實(shí)例對象之間自動創(chuàng)建鏈接律胀。如果你需要在實(shí)例之間鏈接宋光,則在程序代碼中創(chuàng)建鏈接。例如炭菌,玩家prefab在啟動時需要把自己注冊到GameManager罪佳,或者GameManager可以在啟動時去查找玩家prefab。
45. 若需要添加其他腳本黑低,不要將Mesh放置在prefabs的根節(jié)點(diǎn)上赘艳。當(dāng)你需要從Mesh創(chuàng)建一個prefab時,首先創(chuàng)建一個空的GameObject作為父對象克握,并用來做根節(jié)點(diǎn)蕾管。把腳本放到根節(jié)點(diǎn)上,而不要放到Mesh節(jié)點(diǎn)上菩暗。通過采用這種方法掰曾,更容易替換Mesh,而不會丟失所有你在Inspector中設(shè)置的值停团。
46. 對共享配置數(shù)據(jù)旷坦,而不是prefabs使用Scriptableobject
若是如此:
1)場景較小
2)你不能錯誤地對單個場景(prefab實(shí)例上)進(jìn)行更改。
47. 對level數(shù)據(jù)使用scriptableobjects佑稠。關(guān)卡數(shù)據(jù)常存儲在XML或JSON中秒梅,但使用scriptable objects具有一些優(yōu)點(diǎn):
1)它可以在Editor中編輯。這樣更容易驗(yàn)證數(shù)據(jù)舌胶,并且對非技術(shù)領(lǐng)域的設(shè)計師更友好捆蜀。此外,你可以使用自定義編輯器使編輯更容易。
2)你不必操心讀取/編寫和解析數(shù)據(jù)辆它。
3)它更容易分拆和嵌套誊薄,同時管理生成的assets,因此是從構(gòu)建塊娩井,而非大型配置組成關(guān)卡暇屋。
48. 使用scriptable objects配置inspector中的行為。Scriptableobjects通常與數(shù)據(jù)配置相關(guān)洞辣,但它們也支持將“方法”用作數(shù)據(jù)咐刨。
考慮一個場景,其中你有一個Enemy類型扬霜,并且每個敵人有一堆SuperPowers定鸟。如果它們在Enemy類中,你可以創(chuàng)建這些常規(guī)類著瓶,并生成一個列表……若沒有自定義編輯器联予,你便無法在inspector中設(shè)置一個包含不同superpowers的列表(每個具有自身屬性)。但如果你創(chuàng)建這些super powers assets(將它們實(shí)現(xiàn)為ScriptableObjects)材原,你就可以進(jìn)行上述設(shè)置沸久!
其構(gòu)造為:
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
}
}
當(dāng)遵循這一模式時,需注意以下幾點(diǎn):
1)無法可靠地使Scriptable objects抽象化余蟹。相反卷胯,需要使用具體的基類,并使用抽象方法拋出NotImplementedExceptions威酒。此外窑睁,你也可以定義Abstract屬性,并標(biāo)記應(yīng)為抽象的類和方法葵孤。
2)Scriptableobjects是指無法序列化的通用對象担钮。然而,你可以使用通用基類尤仍,并且只對指定所有通用對象的子類抽象化箫津。
49. 使用scriptable objects對prefabs特殊化。若兩個對象的配置僅在某些屬性上不同宰啦,則通常在場景中放置兩個實(shí)例鲤嫡,并調(diào)整這些實(shí)例上的屬性。通常較好的做法是創(chuàng)建一個單獨(dú)的屬性類绑莺,它可以區(qū)別兩種類型為一個單獨(dú)的scriptableobject類。
這可以提供更多的靈活性:
1)你可以利用從特殊類的繼承惕耕,向不同對象類型提供更具體的特定屬性纺裁。
2)場景設(shè)置更安全(你只要選擇正確的scriptable object,而無需調(diào)整所有屬性,便可以創(chuàng)建所需類型的對象)欺缘。
3)運(yùn)行期間栋豫,通過代碼更容易操縱這些對象。
4)如果你有這兩種類型的多個實(shí)例谚殊,你就會知道當(dāng)進(jìn)行更改時丧鸯,它們的屬性將總是保持一致。
5)你可以將配置變量集分拆為可以混合和匹配的集合嫩絮。
下面舉出了一個關(guān)于此設(shè)置的簡要示例:
[CreateAssetMenu("HealthProperties.asset", "Health Properties")]
public class HealthProperties : ScriptableObject
{
public float maxHealth;
public float resotrationRate;
}
public class Actor : MonoBehaviour
{
public HealthProperties healthProperties;
}
如果特殊化類的數(shù)量較大丛肢,你可能要將特殊化類定義為普通類,并使用鏈接到一個包含某些特殊化類的列表剿干,這些特殊化類是鏈接到你可以獲取的適當(dāng)位置的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創(chuàng)建。
調(diào)試
51. 學(xué)習(xí)如何有效地使用Unity的調(diào)試工具置尔。
1)向Debug.Log語句添加上下文對象以查看它們的生成位置杠步。
2)在編輯器中使用Debug.Break暫停游戲(例如,當(dāng)你想產(chǎn)生錯誤條件榜轿,并且在該幀上檢查部件屬性時幽歼,它很有用)。
3)針對可視化調(diào)試使用Debug.DrawRay和Debug.DrawLine功能(例如谬盐,當(dāng)調(diào)試為什么沒有光影投射時甸私,DrawRay非常有效)。
4)針對可視化調(diào)試使用Gizmos设褐。此外颠蕴,你可以通過使用DrawGizmo屬性提供mono behaviours外部的gizmo渲染器。
5)使用debug inspector試圖(使用inspector查看運(yùn)行中的私密字段的值)助析。
52. 學(xué)習(xí)如何有效地使用調(diào)試器犀被。詳見Visual Studio中的“調(diào)試Unity游戲示例”。
53. 使用一個隨著時間的推移繪制數(shù)值圖形的可視化調(diào)試器外冀。這對于調(diào)試物理寡键,動畫和其他動態(tài)進(jìn)程,尤其是偶然性錯誤非常有用雪隧。你將能夠從圖中找出錯誤西轩,并能夠同時有哪些其他變量發(fā)生了變化。另外脑沿,可視化檢查也使某些異常行為變得更明顯藕畔,比如說數(shù)值變化太頻繁,或者不具明顯原因地發(fā)生偏移庄拇。我們使用的是Monitor Components注服,但也有幾種可用的方案韭邓。
54. 使用改進(jìn)的控制臺記錄。使用一個可以根據(jù)類別進(jìn)行顏色編碼輸出溶弟,同時可以根據(jù)這些類別篩選輸出的編輯器擴(kuò)展女淑。我們使用的是Editor Console Pro,但也有幾種可用的方案辜御。
55. 使用Unity的測試工具鸭你,特別是測試算法和數(shù)學(xué)代碼。詳見Unity測試工具教程擒权,或者使用Unity測試工具以光速進(jìn)行事后單元測試袱巨。
56. 使用Unity的測試工具以運(yùn)行“scratchpad”測試。
Unity的測試工具不僅適合正式測試菜拓,而且還可以便于進(jìn)行可以在編輯器中運(yùn)行瓣窄,同時無需場景運(yùn)行的scratch-pad測試。
57. 實(shí)現(xiàn)截屏快捷鍵纳鼎。當(dāng)你截屏拍照時俺夕,許多錯誤是可見的,并且更容易報告贱鄙。理想化的系統(tǒng)應(yīng)該在PlayerPrefs保持一個計數(shù)器劝贸,從而使連續(xù)截屏不會被覆蓋。截屏應(yīng)保存在項(xiàng)目文件夾外逗宁,以避免人員將它們誤提交到存儲庫映九。
58. 實(shí)現(xiàn)打印重要變量快照的快捷方式。當(dāng)你可以檢查的游戲期間發(fā)生未知錯誤瞎颗,這樣更容易記錄一些信息件甥。當(dāng)然,記錄哪些變量是取決于你的游戲哼拔。實(shí)例是玩家和敵人的位置引有,或者AI演員的“思維狀態(tài)”(例如嘗試行走的路徑)。
59. 實(shí)現(xiàn)一些方便測試的調(diào)試選項(xiàng)倦逐。下面舉出了一些示例:
解鎖所有道具譬正。
禁用敵人。
禁用GUI檬姥。
讓玩家無敵曾我。
禁用所有游戲邏輯。
要注意健民,切勿不慎提交調(diào)試選項(xiàng)抒巢;更改調(diào)試選項(xiàng)可能會迷惑團(tuán)隊中的其他開發(fā)人員。
60. 定義一些Debug快捷鍵常量秉犹,并將它們保存到同一個位置虐秦。通常(為方便起見)在一個位置處理Debug鍵平酿,如同其它的游戲輸入一樣。為了避免快捷鍵沖突悦陋,在一個中心位置定義所有常量。另一種方法是在某個位置處理所有按鍵輸入筑辨,無論它是否是Debug鍵俺驶。(其負(fù)面效果在于,此類可能需要引用更多的其它對象)棍辕。
61. 在程序網(wǎng)格生成時暮现,在頂點(diǎn)繪制或派生小球體。這將幫助你在使用三角形和UVs以顯示網(wǎng)格之前楚昭,確定頂點(diǎn)處在期預(yù)期的位置栖袋,并且網(wǎng)格是正確的尺寸。
性能
62. 請注意關(guān)于效能原因設(shè)計和構(gòu)造的通用建議抚太。
1)這些建議通常是基于虛構(gòu)的塘幅,而不是由測試支持的。
2)即便有時建議是由測試支持的尿贫,但測試存在錯誤电媳。
3)有時建議是由正確的測試支持,但它們處在不真實(shí)的或不同的環(huán)境之中庆亡。(例如匾乓,很容易展現(xiàn)如何比通用列表更快地使用數(shù)組。然而又谋,在真實(shí)游戲環(huán)境中拼缝,這種差異幾乎總是可以忽略不計。同樣彰亥,若測試適用于除目標(biāo)設(shè)備以外的不同硬件時咧七,它們的結(jié)果可能對你無意義。)
4)有時建議是良好的剩愧,但卻過時猪叙。
5)有時,建議是適用的仁卷。然而穴翩,存在權(quán)衡關(guān)系。航運(yùn)慢速游戲有時要好于非航運(yùn)快速游戲锦积。而高度優(yōu)化的游戲更可能包含可以延遲航運(yùn)的復(fù)雜代碼芒帕。
效能建議可能有助于記憶,幫助你通過下述進(jìn)程更快地追蹤實(shí)際問題源丰介。
63.從早期階段對目標(biāo)設(shè)備進(jìn)行定期測試背蟆。
不同的設(shè)備可能具有顯著不同的效能特性鉴分;不要對它們感到吃驚。越早知道問題带膀,你就能越有效地解決問題志珍。
64.知道如何更有效地使用效能評測器以追蹤導(dǎo)致效能問題的原因。
如果你剛接觸效能分析垛叨,請參閱效能評測器簡介伦糯。
學(xué)習(xí)如何針對精細(xì)度分析來定義你自己的框架(使用Profiler.BeginFrame 和Profiler.EndFrame)。
學(xué)習(xí)如何使用平臺特定的效能分析嗽元,如iOS系統(tǒng)的內(nèi)置效能分析器敛纲。
學(xué)習(xí)分析內(nèi)置玩家中的文件,并顯示效能分析器中的數(shù)據(jù)剂癌。
65. 在必要時淤翔,使用自定義分析器進(jìn)行更準(zhǔn)確的分析。有時佩谷,Unity的效能分析器無法清楚地展示發(fā)生的事物旁壮;它可能消耗完分析框架,否則深度分析可能減慢游戲速度琳要,以致于測試沒有意義寡具。我們對此使用自有的內(nèi)部分析器,但應(yīng)該可以在Asset Store中找到其他替代工具稚补。
66. 衡量效能增強(qiáng)的影響童叠。
當(dāng)你作出更改提升效能時,衡量它確保該更改著實(shí)有效课幕。如果這個更改是不可衡量或凌亂的厦坛,請撤銷更改。
67. 不要編寫可讀度減低的代碼乍惊,以保證更佳的效能杜秸。除非有下述任一情況:
你碰到了一個問題,使用效能分析器識別出問題源润绎,同時相較于可維護(hù)性損失撬碟,獲得的增益足夠高±蚱玻或者你清楚自己在做什么呢蛤。
命名規(guī)范和目錄結(jié)構(gòu)
68.遵循一個命名規(guī)范和目錄結(jié)構(gòu)。保持命名和目錄結(jié)構(gòu)的一致性可以方便查找棍郎,并明確指出具體內(nèi)容其障。
你很有可能想要創(chuàng)建自己的命名規(guī)范和目錄結(jié)構(gòu)。下面舉出了一個例子涂佃。
命名的一般原則
1.按事物本身命名励翼。例如蜈敢,鳥應(yīng)該稱為Bird。
- 選擇可以發(fā)音汽抚,方便記憶的名字抓狭。如果你在制作一個與瑪雅文化相關(guān)的游戲,不要把關(guān)卡命名為QuetzalcoatisReturn殊橙。
- 保持一致性辐宾。如果你選擇了一個名字,就堅持用它膨蛮。不要在一處命名buttonHolder,而在其它位置命名buttonContainer季研。
- 使用Pascal風(fēng)格的大小寫敞葛,例如ComplicatedVerySpecificObject。
不要使用空格与涡,下劃線惹谐,或者連字符,但有一個例外
(詳見為同一事物的不同方面命名一節(jié))驼卖。 - 不要使用版本數(shù)字氨肌,或者表示其進(jìn)度的名詞(WIP,final)酌畜。
- 不要使用縮寫:DVamp@W應(yīng)該寫成DarkVampire@Walk怎囚。
- 使用設(shè)計文檔中的術(shù)語:如果文檔中將一個動畫命名為Die,則使用DarkVampire@Die桥胞,而不要用DarkVampire@Death恳守。
- 保持細(xì)節(jié)修飾詞在左側(cè):DarkVampire,而不是VampireDark贩虾;PauseButton催烘,而不是ButtonPaused。舉個例子缎罢,在Inspector中查找PauseButton伊群,這要比所有按鈕都以Button開頭更加方便。(很多人傾向于相反次序策精,認(rèn)為這樣可以使名稱自然分組舰始。然而,名稱不是用來分組的蛮寂,目錄才是蔽午。名稱是用于在同一類對象中快速辨識的。)
9.某些名稱形成一個序列酬蹋。在這些名稱中使用數(shù)字及老。例如PathNode0, PathNode1抽莱。永遠(yuǎn)從0開始,而不是1骄恶。 - 對于非序列的情況食铐,不要使用數(shù)字。例如 Bird0, Bird1, Bird2僧鲁,本應(yīng)該是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
不要只是為了區(qū)分不同類型的項(xiàng)目而使用此類規(guī)范斟叼,例如Rock_Small, Rock_Large,本應(yīng)該是SmallRock,LargeRock春寿。
結(jié)構(gòu)
場景朗涩,項(xiàng)目目錄和腳本目錄的結(jié)構(gòu)應(yīng)遵循一個類似的模式。下面列舉了一些精簡示例绑改。
目錄結(jié)構(gòu)
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
...
場景結(jié)構(gòu)
Main
Debug
Managers
Cameras
Lights
UI
Canvas
HUD
PauseMenu
...
World
Ground
Props
Structures
...
Gameplay
Actors
Items
...
Dynamic Objects
腳本目錄結(jié)構(gòu)
Debug
Gameplay
Actors
Items
...
Framework
Graphics
UI
...