關(guān)于這些技巧
這些技巧不可能適用于每個(gè)項(xiàng)目。
這些是基于我的一些項(xiàng)目經(jīng)驗(yàn),項(xiàng)目團(tuán)隊(duì)的規(guī)模從3人到20人不等;
框架結(jié)構(gòu)的可重用性、清晰程度是有代價(jià)的——團(tuán)隊(duì)的規(guī)模和項(xiàng)目的規(guī)模決定你要在這個(gè)上面付出多少;
很多技巧是品味的問題(這里所列的所有技巧,可能有同樣好的技術(shù)替代方案);
一些技巧可能是對(duì)傳統(tǒng)的Unity開發(fā)的一個(gè)沖擊。例如,使用prefab替代對(duì)象實(shí)例并不是一個(gè)傳統(tǒng)的Unity風(fēng)格,并且這樣做的代價(jià)還挺高的(需要很多的preffab)。也許這些看起來有些瘋狂,但是在我看來是值得的。
流程
1、避免Assets分支
所有的Asset都應(yīng)該只有一個(gè)唯一的版本。如果你真的需要一個(gè)分支版本的Prefab、Scene或是Mesh,那你要制定一個(gè)非常清晰的流程, 來確定哪個(gè)是正確的版本。錯(cuò)誤的分支應(yīng)該起一個(gè)特別的名字,例如雙下劃線前綴:__MainScene_Backup。Prefab版本分支需要一個(gè)特別 的流程來保證安全(詳見Prefabs一節(jié))。
2、如果你在使用版本控制的話,每個(gè)團(tuán)隊(duì)成員都應(yīng)該保有一個(gè)項(xiàng)目的Second Copy用來測(cè)試
修改之后,Second Copy和Clean Copy都應(yīng)該被更新和測(cè)試。大家都不要修改自己的Clean Copy。這對(duì)于測(cè)試Asset丟失特別有用。
3、考慮使用外部的關(guān)卡編輯工具
Unity不是一個(gè)完美的關(guān)卡編輯器。例如,我們使用TuDee來創(chuàng)建3D Tile-Based的游戲,這使我們可以獲得對(duì)Tile友好的工具的益處(網(wǎng)格約束,90度倍數(shù)的旋轉(zhuǎn),2D視圖,快速Tile選擇等)。從一個(gè)XML文件來實(shí)例化Prefab也很簡(jiǎn)單。詳見Guerrilla Tool Development。
4、考慮把關(guān)卡保存為XML,而非scene
這是一種很奇妙的技術(shù):
它可以讓你不必每個(gè)場(chǎng)景都設(shè)置一遍;
他可以加載的更快(如果大多數(shù)對(duì)象都是在場(chǎng)景之間共享的)。
它讓場(chǎng)景的版本合并變的簡(jiǎn)單(就算是Unity的新的文本格式的Scene,也由于數(shù)據(jù)太多,而讓版本合并變的不切實(shí)際)。
它可以使得在關(guān)卡之間保持?jǐn)?shù)據(jù)更簡(jiǎn)便。
你仍就可以使用Unity作為關(guān)卡編輯器(盡管你用不著了)。你需要寫一些你的數(shù)據(jù)的序列化和反序列化的代碼,并實(shí)現(xiàn)在編輯器和游戲運(yùn)行時(shí)加載關(guān)卡、在編輯器中保存關(guān)卡。你可能需要模仿Unity的ID系統(tǒng)來維護(hù)對(duì)象之間的引用關(guān)系。
5、考慮編寫通用的自定義Inspector代碼
實(shí)現(xiàn)自定義的Inspector是很直截了當(dāng)?shù)模荱nity的系統(tǒng)有很多的缺點(diǎn):
它不支持從繼承中獲益;
它不允許定義字段級(jí)別的Inspector組件,而只能是class類型級(jí)別。舉個(gè)例子,如果沒有游戲?qū)ο蠖加幸粋€(gè)ScomeCoolType字段,而你想在Inspector中使用不同的渲染,那么你必須為你的所有class寫Inspector代碼。
你可以通過從根本上重新實(shí)現(xiàn)Inspector系統(tǒng)來處理這些問題。通過一些反射機(jī)制的小技巧,他并不像看上去那么看,文章底部(日后另作翻譯)將提供更多的實(shí)現(xiàn)細(xì)節(jié)。
場(chǎng)景組織
6、使用命名的空Game Object來做場(chǎng)景目錄
仔細(xì)的組織場(chǎng)景,就可以方便的找到任何對(duì)象。
7、把控制對(duì)象和場(chǎng)景目錄(空Game Objec)放在原點(diǎn)(0,0,0)
如果位置對(duì)于這個(gè)對(duì)象不重要,那么就把他放到原點(diǎn)。這樣你就不會(huì)遇到處理Local Space和World Space的麻煩,代碼也會(huì)更簡(jiǎn)潔。
8、盡量減少使用GUI組件的offset
通常應(yīng)該由控件的Layout父對(duì)象來控制Offset;它們不應(yīng)該依賴它們的爺爺節(jié)點(diǎn)的位置。位移不應(yīng)該互相抵消來達(dá)到正確顯示的目的。做基本上要防止了下列情況的發(fā)生:
父容器被放到了(100,-50),而字節(jié)點(diǎn)應(yīng)該在(10,10),所以把他放到(90,60)[父節(jié)點(diǎn)的相對(duì)位置]。
這種錯(cuò)誤通常放生在容器不可見時(shí)。
9、把世界的地面放在Y=0
這樣可以更方便的把對(duì)象放到地面上,并且在游戲邏輯中,可以把世界作為2D空間來處理(如果合適的話),例如AI和物理模擬。
10、使游戲可以從每個(gè)Scene啟動(dòng)
這將大大的降低測(cè)試的時(shí)間。為了達(dá)到所有場(chǎng)景可運(yùn)行,你需要做兩件事:
首先,如果需要前面場(chǎng)景運(yùn)行產(chǎn)生的一些數(shù)據(jù),那么要模擬出它們。
其次,生成在場(chǎng)景切換時(shí)必要保存的對(duì)象,可以是這樣:
C#
myObject = FindMyObjectInScene();
if (myObjet == null)
{
myObject = SpawnMyObject();
}
美術(shù)
11、把角色和地面物體的中心點(diǎn)(Pivot)放在底部,不要放在中間
這可以使你方便的把角色或者其他對(duì)象精確的放到地板上。如果合適的話,它也可能使得游戲邏輯、AI、甚至是物理使用2D邏輯來表現(xiàn)3D。
12、統(tǒng)一所有的模型的面朝向(Z軸正向或者反向)
對(duì)于所有具有面朝向的對(duì)象(例如角色)都應(yīng)該遵守這一條。在統(tǒng)一面朝向的前提下,很多算法可以簡(jiǎn)化。
13、在開始就把Scale搞正確
請(qǐng)美術(shù)把所有導(dǎo)入的縮放系數(shù)設(shè)置為1,并且把他們的Transform的Scale設(shè)置為1,1,1。可以使用一個(gè)參考對(duì)象(一個(gè)Unity的Cube)來做縮放比較。為你的游戲選擇一個(gè)世界的單位系數(shù),然后堅(jiān)持使用它。
14、為GUI組件或者手動(dòng)創(chuàng)建的粒子制作一個(gè)兩個(gè)面的平面模型
設(shè)置這個(gè)平面面朝向Z軸正向,可能簡(jiǎn)化Billboard和GUI創(chuàng)建。
15、制作并使用測(cè)試資源
為SkyBox創(chuàng)建帶文字的方形貼圖;
一個(gè)網(wǎng)格(Grid);
為Shader測(cè)試使用各種顏色的平面:白色,黑色,50%灰度,紅,綠,藍(lán),紫,黃,青;
為Shader測(cè)試使用漸進(jìn)色:黑到白,紅到綠,紅到藍(lán),綠到藍(lán);
黑白格子;
平滑的或者粗糙的法線貼圖;
一套用來快速搭建場(chǎng)景的燈光(使用Prefa);
Prefabs
16、所有東西都使用Prefab
只有場(chǎng)景中的“目錄”對(duì)象不使用Prefab。甚至是那些只使用一次的唯一對(duì)象也應(yīng)該使用Prefab。這樣可以在不動(dòng)用場(chǎng)景的情況下,輕松修改他們。(一個(gè)額外的好處是,當(dāng)你使用EZGUI時(shí),這可以用來創(chuàng)建穩(wěn)定的Sprite Atlases)
17、對(duì)于特例使用單獨(dú)的Prefab,而不要使用特殊的實(shí)例對(duì)象
如果你有兩種敵人的類型,并且只是屬性有區(qū)別,那么為不同的屬性分別創(chuàng)建Prefab,然后鏈接他們。這可以:
在同一個(gè)地方修改所有類型
在不動(dòng)用場(chǎng)景的情況下進(jìn)行修改
如果你有很多敵人的類型,那么也不要在編輯器中使用特殊的實(shí)例。一種可選的方案是程序化處理它們,或者為所有敵人使用一個(gè)核心的文件/Prefab。使用一個(gè)下拉列表來創(chuàng)建不同的敵人,或者根據(jù)敵人的位置、玩家的進(jìn)度來計(jì)算。
18、在Prefab之間鏈接,而不要鏈接實(shí)例對(duì)象
當(dāng)Prefab放置到場(chǎng)景中時(shí),它們的鏈接關(guān)系是被維護(hù)的,而實(shí)例的鏈接關(guān)系不被維護(hù)。盡可能的使用Prefab之間的鏈接可以減少場(chǎng)景創(chuàng)建的操作,并且減少場(chǎng)景的修改。
19、如果可能,自動(dòng)在實(shí)例對(duì)象之間產(chǎn)生鏈接關(guān)系
如果你確實(shí)需要在實(shí)例之間鏈接,那么應(yīng)該在程序代碼中去創(chuàng)建。例如,Player對(duì)象在Start時(shí)需要把自己注冊(cè)到GameManager,或者GameManager可以在Start時(shí)去查找Player對(duì)象。
對(duì)于需要添加腳本的Prefab,不要用Mesh作為根節(jié)點(diǎn)。當(dāng)你需要從Mesh創(chuàng)建一個(gè)Prefab時(shí),首先創(chuàng)建一個(gè)空的GameObject作為父對(duì)象,并用來做根節(jié)點(diǎn)。把腳本放到根節(jié)點(diǎn)上,而不要放到Mesh節(jié)點(diǎn)上。使用這種方法,當(dāng)你替換Mesh時(shí),就不會(huì)丟失所有你在Inspector中設(shè)置的值了。
使用互相鏈接的Prefab來實(shí)現(xiàn)Prefab嵌套。Unity并不支持Prefab的嵌套,在團(tuán)隊(duì)合作中第三方的實(shí)現(xiàn)方案可能是危險(xiǎn)的,因?yàn)榍短椎腜refab之間的關(guān)系是不明確的。
20、使用安全的流程來處理Prefab分支
我們用一個(gè)名為Player的Prefab來講解這個(gè)過程。
用下面這個(gè)流程來修改Player:
復(fù)制Player Prefab;
把復(fù)制出來的Prefab重命名為__Player_Backup;
修改Player Prefab;
測(cè)試一切工作正常,刪除__Player_Backup;
不要把新復(fù)制的命名為Player_New,然后修改它。
有些情況可能更復(fù)雜一些。例如,有些修改可能涉及到兩個(gè)人,上述過程有可能使得場(chǎng)景無(wú)法工作,而所有人必須停下來等他們修改完畢。如果修改能夠很快完成,那么還用上面這個(gè)流程就可以。如果修改需要花很長(zhǎng)時(shí)間,則可以使用下面的流程:
第一個(gè)人:復(fù)制Player Prefab;
把它重命名為__Player_WithNewFeature或者_(dá)_Player_ForPerson2;
在復(fù)制的對(duì)象上做修改,然后提交給第二個(gè)人;
第二個(gè)人:在新的Prefab上做修改;
復(fù)制Player Prefab,并命名為__Player_Backup;
把__Player_WithNewFeature拖放到場(chǎng)景中,創(chuàng)建它的實(shí)例;
把這個(gè)實(shí)例拖放到原始的Player Prefab中;
如果一切工作正常,則可使刪除__Player_Backup和__Player_WithNewFeature;
擴(kuò)展和MonoBehaviourBase
21、擴(kuò)展一個(gè)自己的Mono Behaviour基類,然后自己的所有組件都從它派生
這可以使你方便的實(shí)現(xiàn)一些通用函數(shù),例如類型安全的Invoke,或者是一些更復(fù)雜的調(diào)用(例如random等等)。
22、為Invoke, StartCoroutine and Instantiate 定義安全調(diào)用方法
定義一個(gè)委托任務(wù)(delegate Task),用它來定義需要調(diào)用的方法,而不要使用字符串屬性方法名稱,例如:
public void Invoke(Task task, float time)
{
Invoke(task.Method.Name, time);
}
23、為共享接口的組件擴(kuò)展
有些時(shí)候把獲得組件、查找對(duì)象實(shí)現(xiàn)在一個(gè)組件的接口中會(huì)很方便。
下面這種實(shí)現(xiàn)方案使用了typeof,而不是泛型版本的函數(shù)。泛型函數(shù)無(wú)法在接口上工作,而typeof可以。下面這種方法把泛型方法整潔的包裝起來。
C#
//Defined in the common base class for all mono behaviours
public I GetInterfaceComponent<I>() where I : class
{
return GetComponent(typeof(I)) as I;
}
public static List<I> FindObjectsOfInterface<I>() where I : class
{
MonoBehaviour[] monoBehaviours = FindObjectsOfType<MonoBehaviour>();
List<I> list = new List<I>();
foreach(MonoBehaviour behaviour in monoBehaviours)
{
I component = behaviour.GetComponent(typeof(I)) as I;
if(component != null)
{
list.Add(component);
}
}
return list;
}
24、使用擴(kuò)展來讓代碼書寫更便捷
例如:
C#
public static class CSTransform
{
public static void SetX(this Transform transform, float x)
{
Vector3 newPosition =
new Vector3(x, transform.position.y, transform.position.z);
transform.position = newPosition;
}
...
}
25、使用防御性的GetComponent()
有些時(shí)候強(qiáng)制性組件依賴(通過RequiredComponent)會(huì)讓人蛋疼。例如,很難在Inspector中修改組件(即使他們有同樣的基類)。下面是一種替代方案,當(dāng)一個(gè)必要的組件沒有找到時(shí),輸出一條錯(cuò)誤信息。
public static T GetSafeComponent<T>(this GameObject obj) where T : MonoBehaviour
{
T component = obj.GetComponent<T>();
if(component == null)
{
Debug.LogError("Expected to find component of type "
+ typeof(T) + " but found none", obj);
}
return component;
}<span class="alt">
</span>
風(fēng)格
26、避免對(duì)同一件事使用不同的處理風(fēng)格
在很多情況下,某件事并不只有一個(gè)慣用手法。在這種情況下,在項(xiàng)目中明確選擇其中的一個(gè)來使用。下面是原因:
一些做法并不能很好的一起協(xié)作。使用一個(gè),能強(qiáng)制統(tǒng)一設(shè)計(jì)方向,并明確指出不是其他做法所指的方向;
團(tuán)隊(duì)成員使用統(tǒng)一的風(fēng)格,可能方便大家互相的理解。他使得整體結(jié)構(gòu)和代碼都更容易理解。這也可以減少錯(cuò)誤;
幾組風(fēng)格的例子:
協(xié)程與狀態(tài)機(jī)(Coroutines vs. state machines);
嵌套的Prefab、互相鏈接的Prefab、超級(jí)Prefab(Nested prefabs vs. linked prefabs vs. God prefabs);
數(shù)據(jù)分離的策略;
在2D游戲的使用Sprite的方法;
Prefab的結(jié)構(gòu);
對(duì)象生成策略;
定位對(duì)象的方法:使用類型、名稱、層、引用關(guān)系;
對(duì)象分組的方法:使用類型、名稱、層、引用數(shù)組;
找到一組對(duì)象,還是讓它們自己來注冊(cè);
控制執(zhí)行次序(使用Unity的執(zhí)行次序設(shè)置,還是使用Awake/Start/Update/LateUpdate,還是使用純手動(dòng)的方法,或者是次序無(wú)關(guān)的架構(gòu));
在游戲中使用鼠標(biāo)選擇對(duì)象/位置/目標(biāo):SelectionManager或者是對(duì)象自主管理;
在場(chǎng)景變換時(shí)保存數(shù)據(jù):通過PlayerPrefs,或者是在新場(chǎng)景加載時(shí)不要銷毀的對(duì)象;
組合動(dòng)畫的方法:混合、疊加、分層;
時(shí)間
27、維護(hù)一個(gè)自己的Time類,可以使游戲暫停更容易實(shí)現(xiàn)
做一個(gè)“Time.DeltaTime”和””Time.TimeSinceLevelLoad”的包裝,用來實(shí)現(xiàn)暫停和游戲速度縮放。這使用起來略顯麻煩,但是當(dāng)對(duì)象運(yùn)行在不同的時(shí)鐘速率下的時(shí)候就方便多了(例如界面動(dòng)畫和游戲內(nèi)動(dòng)畫)。
生成對(duì)象
28、不要讓游戲運(yùn)行時(shí)生成的對(duì)象搞亂場(chǎng)景層次結(jié)構(gòu)
在游戲運(yùn)行時(shí),為動(dòng)態(tài)生成的對(duì)象設(shè)置好它們的父對(duì)象,可以讓你更方便的查找。你可以使用一個(gè)空的對(duì)象,或者一個(gè)沒有行為的單件來簡(jiǎn)化代碼中的訪問。可以給這個(gè)對(duì)象命名為“DynamicObjects”。
類設(shè)計(jì)
29、使用單件(Singleton)模式
從下面這個(gè)類派生的所有類,將自動(dòng)獲得單件功能:
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或者AudioManager亦或者GUIManager。
對(duì)于那些非唯一的prefab實(shí)例使用單件管理器(例如Player)。不要為了堅(jiān)持這條原則把類的層次關(guān)系復(fù)雜化,寧愿在你的GameManager(或其他合適的管理器中)中持有一個(gè)它們的引用。
對(duì)于外部經(jīng)常使用的共有變量和方法定義為static,這樣你可以這樣簡(jiǎn)便的書寫“GameManager.Player”,而不用寫成“GameManager.Instance.player”。
30、在組件中不要使用public成員變量,除非它需要在inspector中調(diào)節(jié)
除非需要設(shè)計(jì)師(策劃or美術(shù))去調(diào)節(jié)的變量,特別是它不能明確表明自己是做什么的變量,不要聲明為public。如果在這些特殊情況下,無(wú)法避免,則可使用兩個(gè)甚至四個(gè)下劃線來表明不要從外部調(diào)節(jié)它,例如:
public float __aVariable;
31、把界面和游戲邏輯分開
這一條本質(zhì)上就是指的MVC模式。
**
所有的輸入控制器,只負(fù)責(zé)向相應(yīng)的組件發(fā)送命令,讓它們知道控制器被調(diào)用了。舉 一個(gè)控制器邏輯的例子,一個(gè)控制器根據(jù)玩家的狀態(tài)來決定發(fā)送哪個(gè)命令。但是這樣并不好(例如,如果你添加了多個(gè)控制器,那將會(huì)導(dǎo)致邏輯重復(fù))。相反的,玩 家對(duì)象應(yīng)該根據(jù)當(dāng)前狀態(tài)(例如減速、驚恐)來設(shè)置當(dāng)前的速度,并根據(jù)當(dāng)前的面朝向來計(jì)算如何向前移動(dòng)。控制器只負(fù)責(zé)做他們自己狀態(tài)相關(guān)的事情,控制器不改 變玩家的狀態(tài),因此控制前甚至可以根本不知道玩家的狀態(tài)。另外一個(gè)例子,切換武器。正確的方法是,玩家有一個(gè)函 數(shù):“SwitchWeapon(Weapon newWeapon)”供GUI調(diào)用。GUI不應(yīng)該維護(hù)所有對(duì)象的Transform和他們之間的父子關(guān)系。
所有界面相關(guān)的組件,只負(fù)責(zé)維護(hù)和處理他們自己狀態(tài)相關(guān)的數(shù)據(jù)。例如,顯示一個(gè)地圖,GUI可以根據(jù)玩家的位移計(jì)算地圖的顯示。但是,這是游戲狀態(tài)數(shù)據(jù),它不屬于GUI。GUI只是顯示游戲狀態(tài)數(shù)據(jù),這些數(shù)據(jù)應(yīng)該在其他地方維護(hù)。地圖數(shù)據(jù)也應(yīng)該在其他地方維護(hù)(例如GameManager)。
游戲玩法對(duì)象不應(yīng)該關(guān)心GUI。有一個(gè)例外是處理游戲暫停(可能是通過控制Time.timeScale,其實(shí)這并不是個(gè)好主意)。游戲玩法對(duì)象應(yīng)該知道游戲是否暫停。但是,這就是全部了。另外,不要把GUI組件掛到游戲玩法對(duì)象上。
這么說吧,如果你把所有的GUI類都刪了,游戲應(yīng)該可以正確編譯。
你還應(yīng)該達(dá)到:在不需要重寫游戲邏輯的前提下,重寫GUI和輸入控制。
32、分離狀態(tài)控制和簿記變量
簿記變量只是為了使用起來方便或者提高查找速度,并且可以根據(jù)狀態(tài)控制來覆蓋。將兩者分離可以簡(jiǎn)化:
保存游戲狀態(tài)
調(diào)試游戲狀態(tài)
實(shí)現(xiàn)方法之一是為每個(gè)游戲邏輯定義一個(gè)”SaveData“類,例如:
[Serializable]
PlayerSaveData
{
public float health; //public for serialisation, not exposed in inspector
}
Player
{
//... bookkeeping variables
//Don’t expose state in inspector. State is not tweakable.
private PlayerSaveData playerSaveData;
}
33、分離特殊的配置
假設(shè)我們有兩個(gè)敵人,它們使用同一個(gè)Mesh,但是有 不同的屬性設(shè)置(例如不同的力量、不同的速度等等)。有很多方法來分離數(shù)據(jù)。下面是我比較喜歡的一種,特別是對(duì)于對(duì)象生成或者游戲存檔時(shí),會(huì)很好用。(屬 性設(shè)置不是狀態(tài)數(shù)據(jù),而是配置數(shù)據(jù),所以我們不需要存檔他們。當(dāng)對(duì)象加載或者生成是,屬性設(shè)置會(huì)自動(dòng)加載。)
為每一個(gè)游戲邏輯類定義一個(gè)模板類。例如,對(duì)于敵人,我們來一個(gè)“EnemyTemplate”,所有的屬性設(shè)置變量都保存在這個(gè)類中。
在游戲邏輯的類中,定義一個(gè)上述模板類型的變量。
制作一個(gè)敵人的Prefab,以及兩個(gè)模板的Prefab:“WeakEnemyTemplate”和”StrongEnemyTemplate”。
在加載或者生成對(duì)象是,把模板變量正確的復(fù)制。
這種方法可能有點(diǎn)復(fù)雜(在一些情況下,可能不需要這樣)。
舉個(gè)例子,最好使用泛型,我們可以這樣定義我們的類:
public class BaseTemplate
{
...
}
public class ActorTemplate : BaseTemplate
{
...
}
public class Entity<EntityTemplateType> where EntityTemplateType : BaseTemplate
{
EntityTemplateType template;
...
}
public class Actor : Entity <ActorTemplate>
{
...
}
34、除了顯示用的文本,不要使用字符串
特別是不要用字符串作為對(duì)象或者prefab等等的ID標(biāo)識(shí)。一個(gè)很遺憾的例外是動(dòng)畫系統(tǒng),需要使用字符串來訪問相應(yīng)的動(dòng)畫。
35、避免使用public的數(shù)組
舉例說明,不要定義一個(gè)武器的數(shù)組,一個(gè)子彈的數(shù)組,一個(gè)粒子的數(shù)組,這樣你的代碼看起來像這樣:
public void SelectWeapon(int index)
{
currentWeaponIndex = index;
Player.SwitchWeapon(weapons[currentWeapon]);
}
public void Shoot()
{
Fire(bullets[currentWeapon]);
FireParticles(particles[currentWeapon]);
}
這在代碼中還不是什么大問題,但是在Inspector中設(shè)置他們的值的時(shí)候,就很難不犯錯(cuò)了。
我們可以定義一個(gè)類,來封裝這三個(gè)變量,然后使用一個(gè)它的實(shí)例數(shù)組:
[Serializable]
public class Weapon
{
public GameObject prefab;
public ParticleSystem particles;
public Bullet bullet;
}
這樣代碼看起來很整潔,但是更重要的是,在Inspector中設(shè)置時(shí)就不容易犯錯(cuò)了。
36、在結(jié)構(gòu)中避免使用數(shù)組
舉個(gè)例子,一個(gè)玩家可以有三種攻擊形式,每種使用當(dāng)前的武器,并發(fā)射不同的子彈、產(chǎn)生不同的行為。
你可以把三個(gè)子彈作為一個(gè)數(shù)組,并像下面這樣組織邏輯:
public void FireAttack()
{
/// behaviour
Fire(bullets[0]);
}
public void IceAttack()
{
/// behaviour
Fire(bullets[1]);
}
public void WindAttack()
{
/// behaviour
Fire(bullets[2]);
}
使用枚舉值可以讓代碼看起來更好一點(diǎn):
public void WindAttack()
{
/// behaviour
Fire(bullets[WeaponType.Wind]);
}
但是這對(duì)Inspector一點(diǎn)也不好。
最好使用單獨(dú)的變量,并且起一個(gè)好的變量名,能夠代表他們的內(nèi)容的含義。使用下面這個(gè)類會(huì)更整潔。
[Serializable]
public class Bullets
{
public Bullet FireBullet;
public Bullet IceBullet;
public Bullet WindBullet;
}
這里假設(shè)沒有其他的Fire、Ice、Wind的數(shù)據(jù)。
37、把數(shù)據(jù)組織到可序列化的類中,可以讓inspector更整潔
有些對(duì)象有一大堆可調(diào)節(jié)的變量,這種情況下在Inspector中找到某個(gè)變量簡(jiǎn)直就成了噩夢(mèng)。為了簡(jiǎn)化這種情況,可以使用一下的步驟:
把這些變量分組定義到不同的類中,并讓它們聲明為public和serializable;
在一個(gè)主要的類中,把上述類的實(shí)例定義為public成員變量;
不用在Awake或者Start中初始化這些變量,因?yàn)閁nity會(huì)處理好它們;
你可以定義它們的默認(rèn)值;
這可以把變量分組到Inspector的分組頁(yè)簽中,方便管理。
[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;
}<span class="alt">
</span>
文本
38、如果你有很多的劇情文本,那么把他們放到一個(gè)文件里面。
不要把他們放到Inspector的字段中去編輯。這些需要做到不打開Unity,也不用保存Scene就可以方便的修改。
39、如果你計(jì)劃實(shí)現(xiàn)本地化,那么把你的字符串分離到一個(gè)統(tǒng)一的位置。
有很多種方法來實(shí)現(xiàn)這點(diǎn)。例如,定義一個(gè)文本Class,為每個(gè)字符串定義一個(gè)public的字符串字段,并把他們的默認(rèn)值設(shè)為英文。其他的語(yǔ)言定義為子類,然后重新初始化這些字段為相應(yīng)的語(yǔ)言的值。
另外一種更好的技術(shù)(適用于文本很大或者支持的語(yǔ)言數(shù)量眾多),可以讀取幾個(gè)單獨(dú)的表單,然后提供一些邏輯,根據(jù)所選擇的語(yǔ)言來選取正確的字符串。
測(cè)試與調(diào)試
40、實(shí)現(xiàn)一個(gè)圖形化的Log用來調(diào)試物理、動(dòng)畫和AI。
這可以顯著的加速調(diào)試工作。詳見這里。
41、實(shí)現(xiàn)一個(gè)HTML的Log。
在很多情況下,日志是非常有用的。擁有一個(gè)便于分析的Log(顏色編碼、有多個(gè)視圖、記錄屏幕截圖等)可以使基于Log的調(diào)試變動(dòng)愉悅。詳見這里。
42、實(shí)現(xiàn)一個(gè)你自己的幀速率計(jì)算器。
沒有人知道Unity的FPS計(jì)算器在做什么,但是肯定不是計(jì)算幀速率。實(shí)現(xiàn)一個(gè)你自己的,讓數(shù)字符合直覺并可視化。
43、實(shí)現(xiàn)一個(gè)截屏的快捷鍵。
很多BUG是圖形化的,如果你有一個(gè)截圖,就很容易報(bào)告它。一個(gè)理想的系統(tǒng),應(yīng)該在PlayerPrefes中保存一個(gè)計(jì)數(shù),并根據(jù)這個(gè)計(jì)數(shù),使得所有成功保存的截屏文件都不被覆蓋掉。截屏文件應(yīng)該保存在工程文件夾之外,這可以防止人們不小心把它提交到版本庫(kù)中。
44、實(shí)現(xiàn)一個(gè)打印玩家坐標(biāo)的快捷鍵。
這可以在匯報(bào)位置相關(guān)的BUG時(shí)明確它發(fā)生在世界中的什么位置,這可以讓Debug容易一些。
45、實(shí)現(xiàn)一些Debug選項(xiàng),用來方便測(cè)試。
一些例子:
解鎖所有道具;
關(guān)閉所有敵人;
關(guān)閉GUI;
讓玩家無(wú)敵;
關(guān)閉所有游戲邏輯;
46、為每一個(gè)足夠小的團(tuán)隊(duì),創(chuàng)建一個(gè)適合他們的Debug選項(xiàng)的Prefab。
設(shè)置一個(gè)用戶標(biāo)識(shí)文件,單不要提交到版本庫(kù),在游戲運(yùn)行時(shí)讀取它。下面是原因:
團(tuán)隊(duì)的成員不會(huì)因?yàn)橐馔獾奶峤涣俗约旱腄ebug設(shè)置而影響到其他人。
修改Debug設(shè)置不需要修改場(chǎng)景。
47、維護(hù)一個(gè)包含所有游戲元素的場(chǎng)景。
例如,一個(gè)場(chǎng)景,包括所有的敵人,所有可以交互的對(duì)象等等。這樣可以不用玩很久,而進(jìn)行全面的功能測(cè)試。
48、定義一些Debug快捷鍵常量,并把他們保存在統(tǒng)一的地方。
Debug鍵通常(方便起見)在一個(gè)地方來處理,就像其他的游戲輸入一樣。為了避免快捷鍵沖突,在一個(gè)中心位置定義所有常量。一種替代方案是,在一個(gè)地方處理所有按鍵輸入,不管他是否是Debug鍵。(負(fù)面作用是,這個(gè)類可能需要引用更多的其他對(duì)象)
文檔
49、為你的設(shè)置建立文檔。
代碼應(yīng)該擁有最多的文檔,但是一些代碼之外的東西也必須建立文檔。讓設(shè)計(jì)師們通過代碼去看如果進(jìn)行設(shè)置是浪費(fèi)時(shí)間。把設(shè)置寫入文檔,可以提高效率(如果文檔的版本能夠及時(shí)更新的話)。
用文檔記錄下面這些:
Layer的使用(碰撞、檢測(cè)、射線檢測(cè)——本質(zhì)上說,什么東西應(yīng)該在哪個(gè)Layer里);
Tag的使用;
GUI的depth層級(jí)(說什么應(yīng)該顯示在什么之上);
慣用的處理方式;
Prefab結(jié)構(gòu);
動(dòng)畫Layer。
命名規(guī)則和目錄結(jié)構(gòu)
50、遵從一個(gè)命名規(guī)范和目錄結(jié)構(gòu),并建立文檔
命名和目錄結(jié)構(gòu)的一致性,可以方便查找,并明確指出什么東西在哪里。
你很有可能需要?jiǎng)?chuàng)建自己的命名規(guī)則和目錄結(jié)構(gòu),下面的例子僅供參考。
普遍的命名規(guī)則
名字應(yīng)該代表它是什么,例如鳥就應(yīng)該叫做Bird。
選擇可以發(fā)音、方便記憶的名字。如果你在制作一個(gè)瑪雅文化相關(guān)的游戲,不要把關(guān)卡命名為QuetzalcoatisReturn。
保持唯一性。如果你選擇了一個(gè)名字,就堅(jiān)持用它。
使用Pascal風(fēng)格的大小寫,例如ComplicatedVerySpecificObject。不要使用空格,下劃線,或者連字符,除了一個(gè)例外(詳見為同一事物的不同方面命名一節(jié))。
不要使用版本數(shù)字,或者標(biāo)示他們進(jìn)度的名詞(WIP、final)。
不要使用縮寫:DVamp@W應(yīng)該寫成DarkVampire@Walk。
使用設(shè)計(jì)文檔中的術(shù)語(yǔ):如果文檔中稱呼一個(gè)動(dòng)畫為Die,那么使用DarkVampire@Die,而不要用DarkVampire@Death。
保持細(xì)節(jié)修飾詞在左側(cè):DarkVampire,而不是VampireDark;PauseButton,而不是ButtonPaused。舉例說明,在Inspector中查找PauseButton,比所有按鈕都以Button開頭方便。(很多人傾向于相反的次序,認(rèn)為那樣名字可以自然的分組。然而,名字不是用來分組的,目錄才是。名字是用來在同一類對(duì)象中可以快速辨識(shí)的。)
為一個(gè)序列使用同一個(gè)名字,并在這些名字中使用數(shù)字。例如PathNode0, PathNode1。永遠(yuǎn)從0開始,而不是1。
對(duì)于不是序列的情況,不要使用數(shù)字。例如 Bird0, Bird1, Bird2,本應(yīng)該是Flamingo, Eagle, Swallow。
為臨時(shí)對(duì)象添加雙下劃線前綴,例如__Player_Backup。
為同一事物的不同方面命名
在核心名稱后面添加下劃線,后面的部分代表哪個(gè)方面。例如
GUI中的按鈕狀態(tài):EnterButton_Active、EnterButton_Inactive
貼圖: DarkVampire_Diffuse, DarkVampire_Normalmap
天空盒:JungleSky_Top, JungleSky_North
LOD分組:DarkVampire_LOD0, DarkVampire_LOD1
結(jié)構(gòu)
場(chǎng)景組織、工程目錄、腳本目錄應(yīng)該使用相似的模式。
目錄結(jié)構(gòu)
Materials
GUI
Effects
Meshes
Actors
DarkVampire
LightVampire
...
Structures
Buildings
...
Props
Plants
...
...
Plugins
Prefabs
Actors
Items
...
Resources
Actors
Items
...
Scenes
GUI
Levels
TestScenes
Scripts
Textures
GUI
Effects
...
場(chǎng)景結(jié)構(gòu)
Cameras
Dynamic Objects
Gameplay
Actors
Items
...
GUI
HUD
PauseMenu
...
Management
Lights
World
Ground
Props
Structure
...
腳本目錄結(jié)構(gòu)
ThirdParty
...
MyGenericScripts
Debug
Extensions
Framework
Graphics
IO
Math
...
MyGameScripts
Debug
Gameplay
Actors
Items
...
Framework
Graphics
GUI
...