Lesson22-23 - Inspector 窗口拓展

Inspector 拓展概述

通过 CustomEditor 特性,可以完全自定义脚本在 Inspector 窗口中的显示方式。这提供了强大的编辑器自定义能力。

基础框架

创建自定义编辑器脚本

命名约定:在目标脚本名后加 Editor,并放在 Editor 文件夹中。

1
2
3
4
5
6
7
8
9
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(MyScript))]
public class MyScriptEditor : Editor {
public override void OnInspectorGUI() {
EditorGUILayout.LabelField("这是自定义 Inspector");
}
}

CustomEditor 特性

1
2
3
4
5
[CustomEditor(typeof(要自定义的脚本类))]
[CustomEditor(typeof(MyScript))]
public class MyScriptEditor : Editor {
// ...
}

参数 typeof(MyScript) 指定要自定义的目标脚本。

SerializedObject 和 SerializedProperty

概念

  • SerializedObject:代表整个脚本对象,用于序列化和反序列化属性
  • SerializedProperty:代表脚本中的单个属性,用于获取和修改属性值

初始化

在 OnEnable 中初始化 SerializedProperty:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[CustomEditor(typeof(MyScript))]
public class MyScriptEditor : Editor {
private SerializedProperty healthProperty;
private SerializedProperty damageProperty;

private void OnEnable() {
healthProperty = serializedObject.FindProperty("health");
damageProperty = serializedObject.FindProperty("damage");
}

public override void OnInspectorGUI() {
serializedObject.Update();

// 使用属性...

serializedObject.ApplyModifiedProperties();
}
}

基础使用

PropertyField - 自动处理属性显示

1
2
3
4
5
6
7
8
9
public override void OnInspectorGUI() {
serializedObject.Update();

// 自动根据属性类型显示相应的控件
EditorGUILayout.PropertyField(healthProperty, new GUIContent("生命值"));
EditorGUILayout.PropertyField(damageProperty, new GUIContent("伤害"));

serializedObject.ApplyModifiedProperties();
}

PropertyField 会根据属性类型自动选择合适的控件。

获取属性值

1
2
3
4
int health = healthProperty.intValue;
float damage = damageProperty.floatValue;
string name = nameProperty.stringValue;
Vector3 pos = posProperty.vector3Value;

修改属性值

1
2
healthProperty.intValue = 100;
damageProperty.floatValue = 50.5f;

更新和应用

1
2
3
4
5
6
7
8
public override void OnInspectorGUI() {
serializedObject.Update(); // 从目标对象读取最新值

// 修改属性
EditorGUILayout.PropertyField(healthProperty);

serializedObject.ApplyModifiedProperties(); // 将修改保存回目标对象
}

获取目标对象

target 属性

1
2
3
4
5
public override void OnInspectorGUI() {
MyScript myScript = (MyScript)target;

EditorGUILayout.LabelField("脚本名:" + myScript.gameObject.name);
}

target 直接返回绑定的脚本实例。

数组和 List 处理

使用 PropertyField 显示数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[CustomEditor(typeof(MyScript))]
public class MyScriptEditor : Editor {
private SerializedProperty itemsProperty;

private void OnEnable() {
itemsProperty = serializedObject.FindProperty("items");
}

public override void OnInspectorGUI() {
serializedObject.Update();

EditorGUILayout.PropertyField(itemsProperty, true); // true 表示显示子元素

serializedObject.ApplyModifiedProperties();
}
}

自定义数组显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public override void OnInspectorGUI() {
serializedObject.Update();

int arraySize = itemsProperty.arraySize;
int newSize = EditorGUILayout.IntField("大小", arraySize);

if (newSize != arraySize) {
// 增加元素
while (itemsProperty.arraySize < newSize) {
itemsProperty.InsertArrayElementAtIndex(itemsProperty.arraySize);
}
// 减少元素
while (itemsProperty.arraySize > newSize) {
itemsProperty.DeleteArrayElementAtIndex(itemsProperty.arraySize - 1);
}
}

// 逐个显示数组元素
for (int i = 0; i < itemsProperty.arraySize; i++) {
SerializedProperty element = itemsProperty.GetArrayElementAtIndex(i);
EditorGUILayout.PropertyField(element, new GUIContent("项目 " + i));
}

serializedObject.ApplyModifiedProperties();
}

数组 API

  • arraySize:获取/设置数组大小
  • InsertArrayElementAtIndex(int):在指定索引插入默认元素
  • DeleteArrayElementAtIndex(int):删除指定索引的元素
  • GetArrayElementAtIndex(int):获取指定索引的 SerializedProperty

实战示例

示例 1:自定义角色编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[CustomEditor(typeof(Character))]
public class CharacterEditor : Editor {
private SerializedProperty nameProperty;
private SerializedProperty levelProperty;
private SerializedProperty healthProperty;

private void OnEnable() {
nameProperty = serializedObject.FindProperty("characterName");
levelProperty = serializedObject.FindProperty("level");
healthProperty = serializedObject.FindProperty("health");
}

public override void OnInspectorGUI() {
serializedObject.Update();

EditorGUILayout.LabelField("角色编辑器", EditorStyles.boldLabel);
EditorGUILayout.Space(5);

EditorGUILayout.PropertyField(nameProperty, new GUIContent("角色名"));
EditorGUILayout.PropertyField(levelProperty, new GUIContent("等级"));
EditorGUILayout.PropertyField(healthProperty, new GUIContent("生命值"));

EditorGUILayout.Space(10);

if (GUILayout.Button("充满血量")) {
healthProperty.intValue = 100;
}

serializedObject.ApplyModifiedProperties();
}
}

示例 2:显示只读属性

1
2
3
4
5
6
7
8
9
10
11
12
public override void OnInspectorGUI() {
serializedObject.Update();

// 显示只读属性
GUI.enabled = false;
EditorGUILayout.PropertyField(readOnlyProperty);
GUI.enabled = true;

EditorGUILayout.PropertyField(editableProperty);

serializedObject.ApplyModifiedProperties();
}

示例 3:条件显示属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public override void OnInspectorGUI() {
serializedObject.Update();

EditorGUILayout.PropertyField(typeProperty);

// 根据 type 值条件显示其他属性
if (typeProperty.stringValue == "Damage") {
EditorGUILayout.PropertyField(damageProperty);
} else if (typeProperty.stringValue == "Heal") {
EditorGUILayout.PropertyField(healProperty);
}

serializedObject.ApplyModifiedProperties();
}

注意事项

  1. 必须在 Update 和 ApplyModifiedProperties 之间修改属性:这样改动才能正确同步到目标对象。
  2. 引用比较要用 Equals:比较 SerializedProperty 时用 Equals 而不是 ==。
  3. 数组操作会改变大小:InsertArrayElementAtIndex 和 DeleteArrayElementAtIndex 都会改变数组大小。
  4. 避免频繁的 Update 和 Apply:在一个 OnInspectorGUI 中只需调用一次 Update 和 ApplyModifiedProperties。
  5. 在 OnEnable 中查找属性:这样性能更好,避免每帧都查找一遍。