快速實現資料編輯器——不要再傻傻地用程式碼一行行繪製介面了

Zerone羽發表於2018-01-31
我最開始做編輯器的時候,確實也是用EditorGUILayout一行一行寫的。


Unity的EditorGUI這套東西,在實現介面上確實上已經比傳統的“拖控制元件+設屬性+加監聽”要快多了,確實容易就此滿足。尤其是以前回合制遊戲的編輯器,其實也就是個單層陣列,工作量並不大。


而從客戶端過來的人,因為以前引擎稀爛,本來就要設專人用大量精力做編輯器,他們認為在這種地方浪費時間是理所應當的。反正工期也長,既然有專門做編輯器的人,讓他們閒著也不好。


直到——之前專案的策劃非要我在專案原型階段拿一個編輯器出來,而且還是照抄別的遊戲的整套功能的那種,大概是個包含各種定製功能的彈幕遊戲,內建Action,Trigger和其他奇怪的玩意兒。如果用普通的方式,幾十個類,沒有一萬行程式碼怕是搞不定。


而我最多也就一週時間。


雖然明明可以用編輯xml檔案等序列化資料檔案的方式來代替,但他們硬要有編輯器才開始工作,似乎覺得編輯器是彈個響指一夜之間變出來的玩意兒。不考慮工具價效比也是中國策劃的通病了。


我當時也沒有別的辦法。由於之前的資料格式是XML(直接從競品偷的),我便根據檔案內容整理出了一個表述資料結構的XML,然後寫編輯器程式碼讀取這個XML並生成整個樹狀介面,這樣就不用一個一個類去實現了,再加上一些拖拽Asset,預覽等需求,大概用了三天便完成了這個功能。
由於後期的編輯器修改需求無非就是增減屬性,增刪Class。直接改下那個作為配置的XML檔案就可以了。所以編輯器方面就無需再花費精力,後來那個XML檔案都直接交給策劃自己改了。畢竟等我改需要時間,他們即改即用。


反正是非常的好用。之前老老實實一條一條寫程式碼的我就和傻逼一樣。
而且很顯然,這東西是通用的,放任何專案裡只要花個個把小時改下XML檔案就能把功能實現出來,除了略醜之外,和其他專案用專人跟進整個專案搞出來的東向並沒有啥區別。


但是這個東西畢竟也算專案程式碼,不太合適直接放出來,所以這裡我說的其實是和它無關的後續事項。

——雖然之前的方案用起來是挺方便,但它是“完全”的嗎?

其實並不是。


因為這個配置檔案和實際用的資料類是分開編寫的。每增加一個屬性,雖然編輯器那邊不需要我插手,但我還是需要修改實際的資料類,並修改對應部分的Parse程式碼才可以完成這個更新流程。編輯器那邊可以偷懶用字串,程式裡卻還是隻能用列舉。


也就是說,編輯器端的配置檔案和我這邊的資料類以及Parse程式碼依然是完全重複的勞動。

在我的既有“世界觀”裡,對資料檔案寫Parse程式碼轉換成資料類是一件理所應當的工作,花的時間也不長,便止步於此。但現在看來,我這樣的想法,又和認為“反正編輯器需要有一個人專門跟進,便連顯而易見的效率改進都不做”的人有什麼區別呢?


[AppleScript] 純文字檢視 複製程式碼
?
 
[CustomEditor(typeof(Type))]



這是所有寫過編輯器的人非常熟悉的一行程式碼,因為它是編輯器的入口。

但是:

[AppleScript] 純文字檢視 複製程式碼
?
 
[CustomPropertyDrawer(typeof(Type))]


恐怕就沒幾個人知道了。

它和CustomEditor功能類似,都是自定義特定型別的編輯器介面,但它的物件不是MonoBehaviour,而是一個欄位上的資料。

[AppleScript] 純文字檢視 複製程式碼
?
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
[CustomPropertyDrawer(typeof(UserStruct))]
public class UserStrutDraw : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return 0f;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.PropertyField(property.FindPropertyRelative("name"),new GUIContent("姓名:"));
        EditorGUILayout.PropertyField(property.FindPropertyRelative("sex"),new GUIContent("性別:"));
        EditorGUILayout.EndHorizontal();
        EditorGUI.EndProperty();
    }
}


建立這樣一個類後,用到UserStruct這個資料的編輯器介面都會發生變化(或者是公開屬性直接在屬性皮膚顯示,又或者是用EditorGUILayout.PropertyField呈現)

但這樣並不方便,因為同一段編輯器程式碼會用在多個型別上,所以通常的做法是:[CustomPropertyDrawer(typeof(Type))]中的Type不指定具體型別,而是指定一個PropertyAttribute元標籤物件。

[AppleScript] 純文字檢視 複製程式碼
?
 
1
2
public class UserDisplayAttribute : PropertyAttribute
{
}
然後在需要應用應用這個編輯器的地方打上UserDisplayAttribute這個元標籤。
[AppleScript] 純文字檢視 複製程式碼
?
 
1
2
3
4
5
[System.Serializable]
public class Profile
{
      [UserDisplayAttribute]
      public UserStruct user;
}

便能夠有和之前相同的效果。

此外,編輯器類的基類PropertyDrawer是用來定義某個屬性的,它具有獨佔性。但你也可以繼承自DecoratorDrawer,它是“裝飾”的意思,是可以疊加的,可以用它來做一些介面繪製工作。

[AppleScript] 純文字檢視 複製程式碼
?
 
1
2
3
4
5
6
[System.Serializable]
public class Profile
{
      [DrawLine]
      [UserDisplayAttribute]
      public UserStruct user;
}

另外,Attribute物件也是可以有內部屬性的
[AppleScript] 純文字檢視 複製程式碼
?
 
1
2
3
public class UserDisplayAttribute : PropertyAttribute
{
     public Color color;
}


直接寫在括號內就可以為這些屬性賦值,然後就可以在相應的PropertyDrawer類裡讀取到這個值,並處理。

[AppleScript] 純文字檢視 複製程式碼
?
 
1
2
3
4
5
6
[System.Serializable]
public class Profile
{
      [DrawLine]
      [UserDisplayAttribute(color = Color.red)]
      public UserStruct user;
}

這就為我們開通了另一條,不通過CustomEditor做介面的方法。而這種方法程式碼量更少,也更容易重用。我們可以在寫資料類的時候順便加上這些元標籤,然後用EditorGUILayout.PropertyField呈現整個資料類的根結點,然後用Unity自己的物件層級功能一層層展開,不需要為每條屬性書寫編輯器程式碼。對Unity自帶呈現不滿的地方,用PropertyDrawer類重新定義就可以。

陣列也是可以重定義的。

而且用這種方法,以前一些比較麻煩的元件功能也變得容易實現了,諸如Tab

[AppleScript] 純文字檢視 複製程式碼
?
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[CustomPropertyDrawer(typeof(TabAttribute))]
public class TabDraw : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return 0f;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        GUIStyle buttonActive = new GUIStyle(GUI.skin.button) { normal = GUI.skin.button.active };
        string[] tabNames = (attribute as TabAttribute).tabNames;
        EditorGUILayout.BeginHorizontal();
        int count = tabNames.Length;
        for (int i = 0; i < count; i++)
        {
            if (GUILayout.Button(tabNames, i == property.intValue ? buttonActive : GUI.skin.button))
            {
                property.intValue = i;
            }
        }
        EditorGUILayout.EndHorizontal();
    }
}
public class TabAttribute: PropertyAttribute
{
    public string[] tabNames;
}
 
//使用示例
[Tab(tabNames = new string[] { "tab1","tab2"})]
public int tabIndex;


 



還有比較重要的屬性中文化

[AppleScript] 純文字檢視 複製程式碼
?
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
[CustomPropertyDrawer(typeof(LabelAttribute),false)]
public class LabelDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        label.text = (attribute as LabelAttribute).label;
        EditorGUI.PropertyField(position, property, label);
    }
}
public class LabelAttribute : PropertyAttribute
{
    public string label;
    public LabelAttribute(string label)
    {
        this.label = label;
    }
}[/p][p=30, 2, left][Label("中文屬性名")]
public int testInt;

 



所以我們只需要寫好資料類,然後適當加幾個樣式元標籤,根據遊戲內容自己實現一些特殊的元標籤以便和遊戲預覽部分通訊,以及針對佈局需求用DecoratorDrawer繪製介面。然後外面再包一個EditorWindows,將遊戲的資料用ScriptableObject整體序列化以及儲存。


這樣我們在遊戲開發過程中,編輯器就可以自動完成了,資料部分也是高效的二進位制序列化格式,讀取即使用,也不需要重寫一遍Parse。


要說缺點的話,也就是限死了必須用Unity的序列化格式。當然,如果你願意的話,也可以寫個反射指令碼把它轉換成JSON,XML等其他格式,但在“技能編輯器”這類應用環境內,由於只有客戶端在使用,並不需要“通用性”(雖說這個格式C#也能內建讀取就是了)


至於你說,策劃和程式用的是不同的Unity工程,所以不能用一樣的資料格式……


首先策劃和美術起碼得用一樣的工程,否則同步資源太浪費時間了,不同步資源?是讓策劃瞎著眼睛配置資料嗎?程式部分如果不想暴露程式碼,可以編譯成DLL放到他們的工程目錄內,這樣用上去和使用同一工程是一樣的。


你非要兩邊程式碼不共用,就意味著編輯器那邊不僅要實現資料編輯,還要把部分遊戲邏輯修改複製一份到另一邊,很容易不一致,並導致委曲求全,編輯器使用非常困難。


關鍵是耗費巨大,又沒有實際的好處。

只要編輯器和執行時使用同一套CS程式碼,就可以通過這套東西節約大量開發時間,以及需求變動時修改導致的等待時間。


然而,雖然有這套東西,但是Unity自己的原始屬性皮膚確實比較難用,雖說都可以實現,但像Tab,陣列之類的功能,一個個實現也很費時間


1.1.gif (155.98 KB, 下載次數: 0)

下載附件  儲存到相簿

昨天 14:05 上傳


進入網站往下拉可以看到全部功能介紹的動圖。

除了大量定義好的元標籤之外,還提供了一個任意型別序列化的功能,便於容納字典等其他複雜型別。
從原始碼看,它還重寫了Unity的那套Attribute的底層,不再限制元標籤必須在欄位上,可以放到方法上實現諸如Button之類的功能。
[AppleScript] 純文字檢視 複製程式碼
?
 
1
2
3
4
[Button("label")]
public void TestMethod()
{
    Debug.Log("test");
}

在它的基礎上開始擴充套件,應該是更好的做法。

相關文章