如何讓編輯器運行你的代碼
Unity3D可以通過事件觸發來執行你的編輯器代碼,但是我們需要一些編譯器參數來告知編譯器何時需要觸發該段代碼。
[MenuItem(XXX)]聲明在一個函數上方,告知編譯器給Unity3D編輯器添加一個菜單項,並且當點擊該菜單項的時候調用該函數。觸發函數里可以編寫任何合法的代碼,可以是一個資源批處理程序,也可以彈出一個編輯器窗口。代碼里可以訪問到當前選中的內容(通過Selection類),並據此來確定顯示視圖。與此類似,[ContextMenu("XXX")]可以向你的上下文菜單中添加一個菜單項。
當你編寫了一些Component腳本,當它被附屬到某個GameObject時,想在編輯視圖即可在Scene視圖觀察到效果,那么你可以把[ExecuteInEditMode]寫在類上方來通知編譯器,該類的OnGUI和Update等函數在編輯模式也也會被調用。我們還可以使用[AddComponentMenu("XXX/XXX")]來把該腳本關聯到Component菜單中,點擊相應菜單項即可為GameObject添加該Component腳本。
開始編寫編輯器
為了避免不必要的包含,Unity3D的運行時和編輯器類分辨存儲在不同的Assemblies里(UnityEngine和UnityEditor)。當你准備開始編寫編輯器之前,你需要using UnityEditor來導入編輯器的名稱空間。
有些代碼可能是運行時和編輯器都需要執行的,如果你想在其中加以區分,那么可以使用#if UNITY_EDITOR ... #endif宏來對編輯器代碼做特殊處理。
在你開始真正編寫代碼之前,我認為你還需要知道所有放在命名為Editor目錄下的腳本會在其它腳本之后進行編譯,這方便了你去使用那些運行時的內容。而那些目錄下的腳本是不能訪問到Editor目錄下的內容的。所以,你最好把你的編輯器腳本寫在Editor目錄下。
如何創建自定義編輯器窗口
創建你的窗口
如果你想自定義一個可編輯的面板,那么你需要編寫一個繼承自EditorWIndow的類。通常情況下,你還需要寫一個[MenuItem]來告知編譯器何時打開這個面板。這個事件的回調應該是一個靜態方法,並且返回一個窗口的實例。
現在,當你點擊對應的菜單項時,會彈出一個空白的窗口。並且你可以像Unity3D編輯器預制的窗口一樣隨意拖動和停靠。下面來看看我們如何來在窗口內實現我們想要的功能吧。
擴展你的窗口
和運行時的GUI一樣,如果你需要在窗口中添加交互控件,那么必須重寫OnGUI方法。具體的重寫方式和運行時的GUI一樣,你甚至可以使用任何擴展自原生GUI系統的插件(例如iGUI和GUIX)來簡化你的插件開發流程(僅經過初步測試,更深層次的可用性尚待驗證)。同時UnityEditor名稱空間下的EditorGUILayout在原生GUI之上提供了一些更方便的接口和控件,讓你可以輕松的使用一些編輯器特有的UI控件。
除了OnGUI外,你可能還會需要如下一些回調來觸發某些具體的邏輯(完整的列表請參考官方文檔):
l OnSelectionChange,但你點選物品時觸發
l OnFocus /OnLostFocus,獲得和失去焦點時觸發
進一步擴展你的窗口
自定義控件
和運行時GUI的使用方式一樣,如果你打算自定義自己的控件,那么最簡單的方式就是實現一個靜態方法(也可以不是靜態的),並提供一些可選參數,在方法內部根據這些參數來完成對控件的布局(就像你在OnGUI中做的一樣)。
如果你打算把自定義控件實現在窗口類內部,你可以使用Partial類來更好的管理你的代碼。
繪制2D內容
繪制圖片
可以使用GUI.DrawTexture來完成對圖片資源的繪制。
繪制基礎圖元
GUI本身並沒有提供繪制基礎圖元的方法,但是可以通過一些方式來封裝出這些方法。
l 繪制線段:通過一個像素的貼圖資源配合GUI.DrawTexture和矩陣旋轉來完成線段的繪制。
l 繪制矩形框:通過GUI.Box和樣式設置來封裝出對矩形框和矩形填充框。
資源選擇器
EditorLayout.ObjectField控件提供一個資源選擇邏輯,生成時需要指定某種資源類型。然后你可以拖動該種資源到該控件或點擊控件旁邊的小圓圈進行列表進行選擇。
如何存儲編輯內容
你可能需要創建一個繼承自SerializedObject的類來保存編輯的數據。繼承自SerializedObject的對象能用於存儲數據而不參與渲染,並可以最終打包到AssetBundle。
針對當前的編輯選項等內容的存儲,可能需要另外一個SerializedObject類(和具體的系統設計相關)。
向導式的編輯窗口
在很多情況下可能你都會需要一個有很多參數的編輯面板,然后在編輯結束后有一個按鈕加以確認。這你不用自己來實現,UnityEditor提供了ScriptableWizard來幫助你快捷的進行開發。
他是繼承自EditorWindow的,所以他們的使用是很類似的。不過注意,當你點擊確認按鈕時,OnWizardCreate()會被調用。另外,ScriptableWizard.DisplayWizard可以幫助你生成並顯示出該窗口。
如何擴展Inspector面板
當你在Unity3D中點選一個對象時,Inspector面板會隨即顯示出此對象的屬性。我們可以針對某個類型的對象擴展該面板,這在為Unity3D開發插件時是非常有用的。
定義INSPECTOR何時被觸發
自定義的Inspector面板需要繼承Editor類。由於功能相對具體,所以你無需定義代碼何時被觸發,對應代碼會在你點擊它所對應的物體時自動執行。
那么如何定義它所對應的類型呢?只需要在你的類定義之前通過編譯器的命令[CustomEditor(typeof(XXX))]就可以完成這項工作了。
訪問被編輯的對象
在Inspector視圖中,我們經常需要訪問正在被編輯的對象。Editor類的成員變量target正是提供了這一關聯。
盡管如此,需要注意target是一個Object類型的對象,具體使用時可能需要類型轉換(可以使用C#的泛型來避免重復的類型轉換)。
實現你自己的Inspector界面
擴展Editor與擴展EditorWindow唯一的不同在於你需要重寫的是OnInspectorGUI而不是OnGUI。另外,如果你想繪制默認的可編輯項,只需調用DrawDefaultInspector即可。
在Scene界面定義編輯句柄
當選中一個物體的時候,可能我們希望在Scene視圖里也能定義一些編輯或展現。這一工作可以通過OnSceneGUI和Handle類來完成。OnSceneGUI用來處理來自Scene視圖的事件,而Handle類用來在Scene視圖實現一些3D的GUI控件(例如控制對象位置的Position控制器)。
具體的使用方式可以參考官方的參考文檔。
一些常用的功能說明
l AssetDatabase.CreateAsset可以幫住你從資源目錄中創建一個資源實例。
l Selection.activeObject返回當前選中的對象。
l EditorGUIUtility.PingObject用來實現在Project窗口中點擊某一項的操作。
l Editor.Repaint用來重繪界面所有的控件。
l XXXImporter用來設置某種資源的具體導入設置(例如在某些情況下你需要設置導入的貼圖為可讀的)。
l EditorUtility.UnloadUnusedAssets用於釋放沒有使用的資源,避免你的插件產生內存泄漏。
l Event.Use用來標記事件已經被處理結束了。
l EditorUtility.SetDirty用來通知編輯器數據已被修改,這樣在下次保存時新的數據將被存儲。
Unity3D的方便之處在於,它很容易地擴展編輯器套件。每款游戲都對加工有着不同的需求,可以快速地以完全集成的方法來構建這些內容並極大地提升開發速度。
目前有大量復雜的軟件包提供以基本Unity功能套件為基礎的復雜工具,從視覺腳本編輯器到編輯器內導航網格生成。但是,有關如何自行構建此類事物的程序說明卻很少。我將在下文列舉某些在自己的工作中總結的編輯器定制相關信息。
如何構建編輯器腳本
因為你不想在游戲中包含所有的編輯器定制,而且你也不想游戲對某些Unity編輯器內的東西有所依賴,所以Unity將運行時間和編輯器代碼放置在單獨的編譯中。
在編輯命令中,運行時間代碼在編輯器代碼之前執行,這樣編輯器類型就可以可靠地聯系至運行時間組件(游戲邦注:否則就會變得難以編輯),但是你的運行時間組件並不涉及任何編輯器代碼。
你必須維持嚴格的層次。Unity 3.4版本中這個方面做得更加具體,現在其產生的項目文件與其提供的4個編輯階段相對應,這樣就不會混淆文件的構建時間。
在某個點上的程序說明有些不太清楚。當我首次開始使用時,我認為需要在我的項目上創建單個“Editor”文件夾,然后把所有的編輯器類型放入其中。事實上,系統的靈活性要更高些,你可以在項目中創建任意數量的“Editor”文件夾,將其埋藏在“資產”文件夾的任何地方,所有這些都可以存放編輯器代碼。
所以,現在通常情況下我會以功能(游戲邦注:比如命名為“AI”)為單位來創建文件夾,然后納入所有功能相關組件,然后在旁邊放上Editor文件夾(游戲邦注:比如命名為“AI/Editor”),裝上所有運行這些組件的編輯器擴展。
只要Unity的內在類型能夠發揮作用,運行時間類型都會存在於UnityEngine命名空間中,而所有的編輯器類型都會存在於UnityEditor命名空間里。
UnityEditor.Editor類
到目前為止,我設立的最普遍的定制是一個自定義檢查器。Unity的Inspector面板提供看到組件狀態的窗口,但是這種基本設置只能理解有限的類型,而且只能展示公共區域。
自定義檢查器讓你可以完全控制用戶查看和編輯你的組件的方式。比如,它們可以讓你呈現只讀資產、強迫性價值限制或只改變選項呈現的方式。
Unity中的Inspector都是Editor類的子類別,所以你應該從這里開始。但是,我對編輯器類處理樣式的方法不是很喜歡。里面有個“Target”用來提及檢查器正在編輯的物體,但是只是基本的“Object”樣式,所以你要不斷將其轉變成更有用的樣式。為避開這個問題,我使用了一個非常簡單的類別,具體如下:
public class InspectorBase : Editor where T : UnityEngine.Object
{
protected T Target { get { return (T) target; } }
}
現在,如果我想要為MyCustomComponent創造檢查器,我就可以從InspectorBase得到檢查器,然后使用“Target”,這樣我就不用時常更改了。
應當注意的是,你還需要將CustomEditor屬性附到檢查器類中,Unity才能夠真正使用它們。
編輯器GUI
一旦你創造自定義檢查器后,你通常想要執行的方法就是OnInspectorGUI()。OnInspectorGUI()可用來指定在檢查器中展示的所有東西,使用的是Unity的GUI系統。
因為這是編輯器代碼,我們可以使用UnityEditor命名空間中的類型,這包括EditorGUILayout。EditorGUILayout使得了大量的簡單控制,可以在編輯器中使用,比Unity普通運行時間GUI系統提供的更好。比如,假如我想向用戶展示進入3D位置的領域,我可以使用EditorGUILayout.Vector3Field():Target.somePosition = EditorGUILayout.Vector3Field(“Some position”, Target.somePosition)。
在檢查器中產生的效果如下圖所示:
正因為GUI系統能夠發揮作用,所以如果我改變UI中的值,Vector3Field就會傳回新的值,Target.somePosition就會得到更新。在將其指派給目標之前,你可以自由改變值(游戲邦注:比如將值定義在某個范圍內),你也可以完全忽略傳回的值。
值並不一定來自於域,你可以曝光檢查器中的資產,可以采用調用一個功能來獲得當前值並使用另一個功能來保存。
當然,Unity會默認處理這個事情。如果你只是想要在Unity已經展示的為基礎來構建,你就不必要重新執行所有那些域。Editor有個DrawDefaultInspector()方法,告訴Unity調用所有通常調用的控制,但是在這個過程完成之后,你仍然有機會添加額外域和按鍵。
說到按鍵,EditorGUILayout的用途確實很廣泛,但是你或許已經注意到存在漏洞。比如,如果我想要在導航網格組件上添加“重新計算”按鍵,這又會怎么樣呢?技巧在於EditorGUILayout仍然構建於常規運行時間GUILayout之上,所以你還是可以使用GUILayout中的所有東西。
你對檢查器中的域做出改變並且為目標物體的域指派新值時,Unity會察覺到你正在改變物體,所以下次保存屏幕或項目時就會將其寫入磁盤。這種察覺是有限的,它只能識別公共資產的直接指派。如果你通過資產或調用方法來修改目標物體,你可能就需要自行調用EditorUtility.SetDirty了。
擴展組件背景菜單
在測試時,有時手動引發某些行為還是很有用的。你可以通過在自定義檢查器上安放按鍵來觸發行為:if(GUILayout.Button(“Explode now!”)) Target.ExplodeNow()。
但是還有個更加簡單的方法,這個方法完全不需要自定義檢查器。你可以使用的是UnityEngine.ContextMenu屬性:
/* In the target class… */
[ContextMenu("Explode now!")]
public void ExplodeNow() { … }
右鍵點擊組件的檢查器(游戲邦注:無論是否自定義化),你會看到背景菜單,其中有額外的功能。可以快速地進行測試。
擴展主菜單
到這里為止,我所說的所有東西都是圍繞某個特別組件為中心的定制。其他種類的擴展又會如何呢?
在我的游戲中,動畫系統將其資產存放在文件夾架構中,這樣每個文件夾都對應enum的一個入口。當我改變enum時,如果可以同步文件夾結構會起到很大作用,添加任何丟失的文件夾並刪除
任何多余的文件夾。所以我采用了以下較為簡單的方法:
public class AnimationSystem{
public static void SyncFolderStructure() { … }
}
但是我要何時以及如何調用呢?我采用的做法是將其連同到Assets菜單中的菜單項目中,使用MenuItem屬性:
[MenuItem("Assets/Sync folder structure")]
public static void SyncFolderStructure() { … }
點擊菜單項目就可以調用功能。應當注意的是,功能需要是靜態的,但是其中的類可以是多種類型的。
Wizards
Editor GUI元素並不一定要在Inspector中。它還可以創造主觀編輯器窗口,可以像任何Unity內置窗口那樣一動,而且可以像在檢查器中那樣使用GUI命令。最簡單的方法就是使用ScriptableWizard,這很像一個對話盒。你在呈現后設定某些值,然后點擊按鍵讓其施展“魔法”。
在默認情況下,ScriptableWizard的作用很像檢查器:類中的任何公共域都會自動呈現在wizard窗口中。你的wizard會像一大串公共域那樣簡單,而且還有個OnWizardCreate()方法,當用戶點擊“Create”按鍵時Unity就會調用這個方法。而且,你可以改變按鍵上的文字,“Apply”或“OK”之類的會顯得更加直觀。
wizard的另一個層面是決定用戶如何開啟,常用方法是使用有靜態功能的菜單選項,如上圖所示:
[MenuItem("GameObject/Create Other/Explosion")]
public static void CreateExplosion()
{
ScriptableWizard.DisplayWizard(“Create explosion”);
}
(本文為游戲邦/gamerboom.com編譯,如需轉載請聯系:游戲邦)
Opinion: Extending The Unity3D Editor
Richard Fine
One of the handy things about Unity3D is that it’s very easy to extend the editor suite. Every game has unique requirements for tooling, and being able to build those up quickly and in a fully integrated manner can make a world of difference to your development speed.
A number of pretty sophisticated packages exist that offer complex tools on top of the base Unity featureset, from visual script editors to in-editor navigation mesh generation. The documentation for how you might build such a thing yourself, however, is a bit thin. Here’s a whirlwind tour of some of the most useful bits of info about editor customization that I’ve found in the course of my work.
How Editor Scripts Are Built
Because you don’t want all your editor customization to be included in the game that you ship, and because you don’t want your shipping game to have any dependencies on things in the Unity editor, Unity keeps runtime and editor code in separate assemblies.
The compilation order is such that runtime code is compiled before editor code, so that your editor classes can safely refer to runtime components (otherwise it’d be difficult to edit them) – but it does mean that your runtime components can’t reference any of your editor code.
You have to maintain a strict layering. Unity’s gotten a bit more explicit about this in 3.4 – now the project files it generates (for VS/MonoDevelop) clearly correspond to the four compilation stages it provides, so there’s no confusion about which files will get build at which times.
The documentation is a little unclear on one particular point. When I first started, I thought that I had to have a single ‘Editor’ folder at the top of my project, and that all editor classes had to go inside it. The system’s actually a bit more flexible than that; you can have as many ‘Editor’ folders as you want in your project, buried wherever you like inside your Assets folder, and all of them can contain editor code.
So now it’s very common that I’ll have a folder for one particular feature (e.g. ‘AI’) that contains all the components for that feature, with an Editor folder alongside (e.g. ‘AI/Editor’) that contains the editor extensions for working with those components.
As far as Unity’s built-in types go, runtime types all live in the UnityEngine namespace (in the UnityEngine assembly), while editor types all live in the UnityEditor namespace (in the UnityEditor assembly).
The UnityEditor.Editor Class
By far the most common kind of customization I set up is a custom inspector. Unity’s Inspector panel provides your window into a component’s state, but in its base form it only understands a limited set of types, and will only expose public fields (no properties).
Custom inspectors give you the opportunity to completely control how users view and edit your components; for example, they let you display read-only properties, enforce value constraints, or just change the way an option is presented – for example, replacing a [0..1] float field with a percentage slider.
Inspectors in Unity are all subclasses of the Editor class, so that’s where you start. One thing I don’t like about the editor class, though, is the way it handles types: it has a ‘target’ member that refers to the object the inspector is editing, but it’s of the base ‘Object’ type, so you keep on having to cast it to a more useful type. To get around this I use a very simple generic class:
public class InspectorBase : Editor where T : UnityEngine.Object
{
protected T Target { get { return (T) target; } }
}
Now, if I want to create an inspector for MyCustomComponent, I can derive the inspector from InspectorBase, and use the ‘Target’ member instead of the untyped ‘target’ member, and I don’t have to keep casting everywhere.
Note that you also need to attach the [CustomEditor] attribute to your inspector classes for Unity to actually pick them up and use them.
Editor GUI
Once you’ve created your custom inspector, the method you usually want to implement is OnInspectorGUI(). OnInspectorGUI() is responsible for specifying everything shown in the inspector, using Unity’s immediate-mode GUI system.
Because this is editor code, we can use the types in the UnityEditor namespace, which includes EditorGUILayout. EditorGUILayout provides a bunch of really simple controls for use in the editor, over and above what Unity’s regular runtime GUI system offers. For example, say I want to show the user a field for entering a 3D position. I could use EditorGUILayout.Vector3Field():
Target.somePosition = EditorGUILayout.Vector3Field(“Some position”, Target.somePosition);
This results in a line in your inspector that looks like this:
The immediate-mode GUI system works such that if I change the values in the UI, Vector3Field will return the new values, and Target.somePosition gets updated. You’re free to manipulate the value – for example, clamping it to a range – before assigning it to the target; and you’re free to ignore the return value completely (which would effectively make the field read-only).
The values don’t have to be coming to/from fields, either – you can expose properties in the inspector by adding lines for them in this way, or call a function to get the current value and another function to save it, whatever you like.
Of course, Unity does this stuff by default for any public members. If you just want to build on top of what Unity’s already showing, you don’t have to reimplement all those fields – Editor has a DrawDefaultInspector() method that tells Unity to draw all the controls it would usually draw, but when it’s finished, you’ve still got the opportunity to add a few extra fields and buttons yourself.
Speaking of buttons… EditorGUILayout is pretty comprehensive, but you might notice that there are some things missing – what if I want to put a “recalculate” button on my navigation mesh component, for example? The trick is that EditorGUILayout is still built on top of the regular runtime GUILayout, so everything in GUILayout is available to you as well.
As you make changes to the fields in the inspector, and assign new values to the fields of your target object, Unity detects that you’re changing the object and flags it as ‘dirty’ so that it will be written out to disk the next time you save the scene or project. This detection is limited: it only picks up direct assignment to public properties. If you’re changing the target object through properties or through calling methods on it, you may need to call EditorUtility.SetDirty yourself.
Extending The Component Context Menu
It’s often useful to be able to manually trigger certain behavior when testing things out. You could do this by putting a button on the custom inspector that triggers the behavior:
if(GUILayout.Button(“Explode now!”)) Target.ExplodeNow();
but there’s an even easier way – one that doesn’t require a custom inspector at all. What you can do instead is use the UnityEngine.ContextMenu attribute:
/* In the target class… */
[ContextMenu("Explode now!")]
public void ExplodeNow() { … }
Right-clicking in the component’s inspector – whether you’ve customized it or not – will then show you a context menu, with the extra item in there. Very handy for quickly rigging things up for testing.
Extending the main menus
Everything I’ve talked about so far has been about customization centered around a particular component. What about other kinds of extension, like general utilities?
The animation system in my game stores its assets in a folder structure, such that each folder corresponds to one entry in an enum. When I change the enum, it’s useful to be able to synchronize that folder structure, adding any missing folders and deleting any obsolete ones. So I’ve got this class, with a simple method:
public class AnimationSystem{
public static void SyncFolderStructure() { … }
}
But how and when do I call it? What I’ve done is to wire it up to a menu item in the Assets menu, using the MenuItem attribute:
[MenuItem("Assets/Sync folder structure")]
public static void SyncFolderStructure() { … }
Clicking the menu item calls the function. Note that the function needs to be static – but the class it’s in can be anything, and can derive from anything (including nothing).
Wizards
Editor GUI elements don’t only have to live inside the Inspector. It’s also possible to create arbitrary editor windows, that can be moved and docked like any of
Unity’s built-in windows, and that are populated using GUI commands just like in an inspector. One of the simplest ways of doing this is to use a ScriptableWizard, which is like a dialogue box – you display it, set some values in it, then hit a button to make it work its magic.
ScriptableWizard, by default, works almost like an inspector: any public fields in your derived class will be automatically shown in the wizard window. Your wizard might be as simple as a bunch of public fields, plus an OnWizardCreate() method, which Unity will call when the user hits the ‘Create’ button. Worth noting that you can change the text on that button, too, if ‘Apply’ or ‘OK’ or similar would be more intuitive.
The only other aspect of the wizard is deciding how the user will launch it; the usual approach is to use a menu item bound to a static function, as shown above:
[MenuItem("GameObject/Create Other/Explosion")]
public static void CreateExplosion()
{
ScriptableWizard.DisplayWizard(“Create explosion”);
}
Conclusion
There’s loads more – I’ve not even touched on custom asset types yet, or asset import postprocessors, or scene view customization. They’ll wait for another day…