我最开始做编辑器的时候,确实也是用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
0
f;
}
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
0
f;
}
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;
|
![](http://img.manew.com/data/attachment/forum/201801/30/135649ohtg3w7t7wio9scg.jpg.thumb.jpg)
还有比较重要的属性中文化
[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;
|
![](http://img.manew.com/data/attachment/forum/201801/30/140220zhx69heeotfl6htm.jpg.thumb.jpg)
所以我们只需要写好数据类,然后适当加几个样式元标签,根据游戏内容自己实现一些特殊的元标签以便和游戏预览部分通信,以及针对布局需求用DecoratorDrawer绘制界面。然后外面再包一个EditorWindows,将游戏的数据用ScriptableObject整体序列化以及储存。
这样我们在游戏开发过程中,编辑器就可以自动完成了,数据部分也是高效的二进制序列化格式,读取即使用,也不需要重写一遍Parse。
要说缺点的话,也就是限死了必须用Unity的序列化格式。当然,如果你愿意的话,也可以写个反射脚本把它转换成JSON,XML等其他格式,但在“技能编辑器”这类应用环境内,由于只有客户端在使用,并不需要“通用性”(虽说这个格式C#也能内建读取就是了)
至于你说,策划和程序用的是不同的Unity工程,所以不能用一样的数据格式……
首先策划和美术起码得用一样的工程,否则同步资源太浪费时间了,不同步资源?是让策划瞎着眼睛配置数据吗?程序部分如果不想暴露代码,可以编译成DLL放到他们的工程目录内,这样用上去和使用同一工程是一样的。
你非要两边代码不共用,就意味着编辑器那边不仅要实现数据编辑,还要把部分游戏逻辑修改复制一份到另一边,很容易不一致,并导致委曲求全,编辑器使用非常困难。
关键是耗费巨大,又没有实际的好处。
只要编辑器和运行时使用同一套CS代码,就可以通过这套东西节约大量开发时间,以及需求变动时修改导致的等待时间。
然而,虽然有这套东西,但是Unity自己的原始属性面板确实比较难用,虽说都可以实现,但像Tab,数组之类的功能,一个个实现也很费时间
![](http://img.manew.com/data/attachment/forum/201801/30/140516l4lb1cnl3ylbzj3n.gif)
进入网站往下拉可以看到全部功能介绍的动图。
除了大量定义好的元标签之外,还提供了一个任意类型序列化的功能,便于容纳字典等其他复杂类型。
从源码看,它还重写了Unity的那套Attribute的底层,不再限制元标签必须在字段上,可以放到方法上实现诸如Button之类的功能。
[AppleScript]
纯文本查看
复制代码
1
2
3
4
|
[Button
(
"label"
)
]
public void TestMethod
(
)
{
Debug.Log
(
"test"
)
;
}
|
在它的基础上开始扩展,应该是更好的做法。