實用妙招帶你輕松玩轉編輯器腳本Editor Scripting(第二部)

如果你還沒看過Unity編輯器編程實用妙招的第一部,可以在Unity社區(qū)專欄(https://developer.unity.cn/projects/637dceededbc2a3b99d518b6)中回顧。在這篇兩部分的文章中,Unity開發(fā)者Jordi Caballol將帶你了解高級的編輯器使用技巧、改善你的工作流程,讓你的下一個項目進展更順利。

每條技巧都建立在一個類RTS(即時戰(zhàn)略)的游戲原型上,游戲里的每個單位都會自動攻擊敵方建筑及其他單位。下方是最初版的原型:

上一篇里,我介紹了怎樣為項目導入并設置好美術資產(chǎn)?,F(xiàn)在我們再到游戲中使用這些資產(chǎn),同時盡可能地節(jié)省時間。
首先是游戲元素的拆包。在準備游戲里的各個元素時,我們通常會遇到以下情況:
一方面,我們從美術團隊那拿到預制件,它可以是FBX Importer生成的預制件,也可能是手動用材質和動畫小心建立的預制件,用于給層級結構添加道具等。要在游戲里使用預制件,合理的做法是創(chuàng)建這個預制體的預制件變體(Prefab Variant),再把所有游戲相關的組件加進去。這樣一來,當美術團隊修改或更新預制件時,所有改動可以立即應用到游戲中。如果對象的組件較少、設置較簡單,這種方法的確能奏效。但如果它非常復雜,每次都要從頭開始配置就非常的麻煩。
另一方面,許多的組件其實會有同樣的屬性,比如所有的“汽車”預制件或相似的“敵人”等。這時,用同一個基礎預制件來制作所有變體是可行的。也就是說,如果預制件的美術設置起來很方便(即模型網(wǎng)格與材質),那這種方法就很理想。
接著來看看怎樣簡化游戲玩法組件的設置過程,以便快速添加并直接使用。
黑科技7:提早設立好組件
對于游戲里較為復雜的元素,最常見的方法是用一個“主要”組件(比如“enemy”、“pickup”或“door”)作為與對象互動的接口,用一堆可重復使用的小組件來實現(xiàn)各種功能,比如“selectable”、“CharacterMovement”或“UnitHealth”,以及renderer、collider這些Unity內置組件。
有些組件依賴于其他組件工作。比如,角色可能要有一個NavMeshAgent(代理)才能移動。而Unity的[RequireComponent]特性正好能寫入這些依賴項。如果特定對象有一個“主要”組件,你可以用[RequireComponent]來添加對象所必須的組件。
例如,我的原型的單位有以下特性:

并且,我還能在AddComponentMenu輕松找到其他需要的額外組件。這里,Locomotion負責移動,AttackComponent負責攻擊其他單位。
另外,該類還會繼承基礎類(與建筑共享)的其他RequireComponent特性,比如Health(血量)組件。有了這些,我只需再手動加上Soldier組件,其他組件都會自動被添加。如果我再為某個組件添加一條新的RequireComponnet特性,Unity會將新組件更新到所有現(xiàn)存的游戲對象上,幫助擴展現(xiàn)有對象。
[RequireComponent]還有一個隱蔽的好處:如果“組件A”需要“組件B”,則添加A不僅能保證B會被添加,還能讓B先于A被添加。如果組件A調用了Reset方法,組件B依舊會存在并且對數(shù)據(jù)的訪問仍會保留。我們可以引用這個組件,記錄持續(xù)性的UnityEvents,再完成對象的設置。同時使用RequireComponent特性與Reset方法,我們可以只加一個組件就完成對象的配置。

黑科技8:與沒有關聯(lián)的預制件分享數(shù)據(jù)
上個方法最大的缺點在于,如果我們想修改某個值,就必須一個個手動更改。如果所有的設置都用代碼完成,設計師們要改起來會非常困難。
在前一篇文章里,我們討論了怎樣用AssetPostprocessor在導入期間添加依賴項、修改對象。我們同樣能用它在預制件上應用數(shù)值。
為了讓設計師們能輕松修改這些數(shù)值,我們需要從預制件上讀取它們,以便通過修改預制件來更改整個項目中的數(shù)值。
如果是編輯器代碼,你可以利用Preset類把一個組件的數(shù)值復制到另一個。
像這樣用原組件創(chuàng)建一個預設,再應用到另一個組件:

在生效時,它會覆蓋預制件的數(shù)值,不過這并不是我們的目的。我們只想復制部分數(shù)值,其他的不動。此時,我們可以用Preset.ApplyTo覆蓋方法,它接收一個一定要應用的屬性列表。雖然我們能硬寫出一份待覆蓋的屬性列表,這對大部分項目來說都沒問題,但如何讓這個流程更為通用呢?
我先是創(chuàng)建了一個帶有所有組件的基礎預制件,然后以其作為模板創(chuàng)建了一個變體。接著我再從變體的覆蓋列表里確定需要應用的數(shù)值。
你可以用PrefabUtility.GetPropertyModifications來獲取覆蓋值。該方法會抓取整個預制件上的所有覆蓋值,你需要篩選出與組件相關的那幾個。要注意的是我們修改的是基礎預制件的組件,而非變體,所以得用GetCorrespondingObjectFromSource來引用它。

這一段會將模板的所有覆蓋值應用到預制件上。另一個細節(jié)問題是模板可能是一個變體的變體,所以我們也得應用上一層變體的覆蓋值。
為此,我們得讓這段操作循環(huán)往復:

接著我們找到預制件的模板。理想情況下,不同類型的對象會有不同的模板。我們可以將模板及待修改的對象放在同一文件夾下來提高效率。
在放預制件的文件夾下查找名為Template.prefab的對象。如果沒有,就在上級文件夾里重復查找:

到這里,只要模板預制件被修改,所有改動都能自動應用到同一文件夾內的預制件上,即便它們并非模板的變體。在例中,我修改了默認的玩家顏色(當單位未被指派給任意玩家時)。注意看所有對象都更新了:

黑科技9:用ScriptableObject(可編程對象)和表格來平衡游戲數(shù)據(jù)
在平衡游戲時,需要調整的數(shù)據(jù)往往散布在各個組件上,存在每個角色的預制件或ScriptableObject里。這使得細節(jié)調整非常低效。
使用表格來簡化平衡過程是一種常見的做法。它可以搜集所有的數(shù)據(jù),還能用公式來計算一些額外數(shù)據(jù)。手動將數(shù)據(jù)輸入到Unity里可能會非常麻煩。
而表格此時就能發(fā)揮一定作用。表格可以被導出為CSV (.csv)或TSV (.tsv),再用ScriptedImporter導入。下方截圖展示了原型單位的統(tǒng)計數(shù)據(jù):

這段代碼非常簡單:用單位的所有統(tǒng)計數(shù)據(jù)創(chuàng)建一個ScriptableObject,再讀取文件。你可以根據(jù)表的每一行創(chuàng)建一個ScriptableObject的實例,填入行內的數(shù)據(jù)。
最后,利用上下文將ScriptableObject添加到導入后的資產(chǎn)上。另外,我們還得添加一個主資產(chǎn),這里我創(chuàng)建了一個空的TextAsset(我們不會真的用這個對象干什么)。
這個方法同時適用于建筑和單位,你只需要留心那些數(shù)據(jù)更多的單位。

這步完成后,你就有了包含所有表格數(shù)據(jù)的ScriptableObject。

生成的ScriptableObject可以隨時被用到游戲里。你也能借助之前編寫的PrefabPostprocessor來應用它們。
在OnPostprocessPrefab方法里,我們能加載該資產(chǎn),并自動在組件的參數(shù)上填入輸入。不僅如此,如果數(shù)據(jù)資產(chǎn)設有依賴項,預制件會在數(shù)據(jù)被修改時重新導入,自動更新所有內容。

黑科技10:加快編輯器內的迭代
在搭建關卡時,快速修改并測試、重復微調并實驗非常關鍵。因此,快速迭代、簡化測試步驟十分重要。
在討論Unity迭代時間時,我們最先想到的可能就是Domain Reload(域重載)。Domain Reload與兩種關鍵情形相關:代碼編譯完成、加載動態(tài)鏈接庫(DLL)時,以及進入與退出Play Mode(運行模式)時。編譯產(chǎn)生的域重載不可避免,但你可以在Project Settings > Editor > Enter Play Mode Settings里禁用Play Mode的重載。
如果你的代碼編寫得不合適,禁用這部分重載會產(chǎn)生一些問題,最常見的有靜態(tài)變量在運行后不會重置。如果你的代碼能適應,那就禁用它吧。在我的原型里,Domain Reload已被禁用,你可以瞬間進入Play Mode。
黑科技11:自動生成數(shù)據(jù)
迭代時間的另一個問題在于重新計算運行所需的數(shù)據(jù)。我們需要選中這些組件,點擊對應的按鈕來觸發(fā)重新計算。比如,在我的原型里,每支隊伍都有一個TeamController。這個控制程序以列表列出了所有敵方建筑,并會派出單位攻擊建筑。要想自動填寫這些數(shù)據(jù),我們可以用IProcessSceneWithReport接口。我在兩種情形下調用這個接口:游戲打包時、在Play Mode里加載場景時。這時我有機會創(chuàng)建、摧毀或修改任意對象。不過,這些改動只會影響運行版和Play Mode。
這次回調會創(chuàng)建控制器、設定建筑列表。而我就不必再手動調整任何東西。當游戲開始時,控制器會帶有一份更新后的建筑列表,任何對列表的修改也會被自動更新。
在原型里,我編寫了一個方法來獲取場景內某一組件的所有實例。你能用它來抓取所有的建筑:

剩下的就簡單了:抓取所有建筑,找到建筑的所有所屬隊伍,為每支隊伍創(chuàng)建一個帶有敵方建筑列表的控制器。


黑科技12:處理多個場景
除了編輯中的場景,游戲還會加載其他場景(每個場景都包含管理程序、UI等等)。編輯這些場景會占據(jù)一定的寶貴時間。在我的原型里,展示血條的Canvas被放在了另一個稱為InGameUI的場景中。
為了高效地利用好這個場景,我在場景里加了一個組件,其中以列表列出了需要一并加載的場景。如果你在Awake里同時加載這些場景,UI場景也會被加載,它所有的Awake方法都會被觸發(fā)。等到調用Start方法時,所有的場景就已經(jīng)完成了加載和初始化,讓你能訪問其中的數(shù)據(jù),比如管理器單例。
當然,在進入Play Mode時部分場景是已經(jīng)加載了的,所以有必要在加載前檢查個別場景是否已加載:

總結
在第一部和第二部兩篇文章里,我已經(jīng)展示了怎樣利用起那些鮮為人知的Unity特色功能。這里所列出的方法只是一個個小步驟,但我希望它們能在你的下一個項目里發(fā)揮作用,或至少成為候選。
原型所用到的資產(chǎn)都能在資源商店(https://assetstore.unity.com/)上免費下載:
Skeletons: Toon RTS Units – Undead Demo
Knights: Toon RTS Units – Demo
Towers: Awesome Stylized Mage Tower

如果你有興趣討論文章,或者分享自己的感想,請前往我們的Scripting論壇(https://forum.unity.com/forums/scripting.12/)。這里我先下線了,但你可以在Twitter(@CaballolD)聯(lián)系我。未來將有更多Unity開發(fā)者發(fā)布Tech from the Trenches系列技術博文,請持續(xù)關注Unity社區(qū)專欄(https://developer.unity.cn/articles)。