这组笔记写一些在 Unity 里把脚本写得更可读、更有条理的编辑技巧。Inspector 是策划和美术每天都在用的界面,在它上面花一个小时,回报会翻好几倍。
暴露字段,但不要 public
在 MonoBehaviour 里我们可以声明很多类型的 public 变量,其中大部分都能在 Inspector 中设置。
C#public int age;
问题在于 public 同时意味着任何持有引用的脚本都能读写这个字段。通常我们想要的是 Inspector 的便利,而不是这份访问权。把字段声明为 private,再加一个 [SerializeField]:
C#[SerializeField] private int age;
这样字段依然出现在 Inspector 里,但对其它代码保持私有。
反过来也有对应的写法。一个不该被序列化的 public 字段——因为它在运行时才赋值,或者序列化它没有意义——用 [NonSerialized]:
C#[System.NonSerialized] public Transform runtimeTarget;
而 [HideInInspector] public int index; 让字段保持 public 且被序列化,但在面板上隐藏起来。适合那些由别的脚本写入、不该被手改的值。
控制排版的特性
方括号里的特性就是用来约束脚本外观的。下面这几个值得记住:
C#[Header("移动")] // 上方带间距的加粗小标题
[Tooltip("满输入时每秒移动的单位数。")] // 悬停时显示的说明
[Range(1, 10)] // 用滑杆代替数字输入框
[Min(0)] // 限制下界
[TextArea(3, 10)] // 多行文本框,指定最小/最大行数
[Space(12)] // 纵向留白(像素)
[SerializeField] private float moveSpeed = 5f;
[Header] 和 [Space] 不花任何成本,对可读性的提升却比其它任何改动都大。一个有二十个平铺字段的组件是没法用的;同样二十个字段分到四个 Header 下面就完全可以接受。
还有两个也几乎天天用到:
C#[RequireComponent(typeof(Rigidbody))] // 加在类上:Unity 会自动补上依赖组件
public class Mover : MonoBehaviour { }
[ContextMenu("重置路径")] // 加在方法上:从组件的 ⋮ 菜单里执行
private void ResetPath() { }
[RequireComponent] 是个小东西,但能挡掉一整类空引用 bug:组件不可能在缺少依赖的情况下存在,而且只要 Mover 还挂着,Unity 就不允许你移除 Rigidbody。
[ContextMenu] 是成本最低的工具。任何本来要写自定义编辑器才能做的编辑期操作——重新生成数据、贴地、清缓存——都可以只是一个带这个特性的私有方法。
校验与派生状态
OnValidate 会在编辑器里任何序列化值改变时被调用。它是维持派生状态一致的正确位置,也能让错误的数值在编辑期就暴露,而不是等到运行时:
C#private void OnValidate()
{
if (segmentCount < 1) segmentCount = 1;
if (mesh != null && mesh.isReadable == false)
Debug.LogWarning($"{name}: mesh 不可读", this);
}
OnValidate 要保持廉价,并且不要在组件之外产生副作用。它被调用的次数比你以为的多得多,包括域重载和预制体实例化的时候。
什么时候该写自定义编辑器
上面这些特性能覆盖大多数场景。只有在需要它们确实表达不了的东西时,才动用 [CustomEditor] 或 PropertyDrawer:
- 某个字段是否显示取决于另一个字段的值;
- 需要在 Scene 视图里直接拖一个位置或半径的手柄;
- 需要预览、校验汇总,或者一个真的会干活的按钮。
通常 PropertyDrawer 是更好的选择:它针对某个可序列化类型,在这个类型出现的任何地方都生效,包括在 List 里面;而 CustomEditor 会替换整个组件的面板,组件变复杂之后维护成本会跟着涨。
我自己的判断标准是:如果写编辑器代码只是为了给自己省点击,那么特性版本几乎总是够用;如果是为了让别人不出错,那自定义 drawer 就值得写。