
1. 项目概述为什么我们需要深入掌握EditorGUILayout如果你在Unity里做过编辑器扩展无论是为了提升团队的工作流效率还是给自己开发的小工具做个配置面板那你一定绕不开EditorGUILayout这个命名空间。乍一看它和运行时用的GUILayout很像都是用来画UI的。但当你真正上手想把一个功能复杂的自定义Inspector或者编辑器窗口做得既美观又实用时就会发现事情没那么简单。按钮挤在一起、控件宽度对不齐、滚动区域卡顿、嵌套布局一团糟……这些问题我都经历过也花了不少时间才摸索出高效、稳定的解决方案。EditorGUILayout是Unity编辑器扩展的基石。它不仅仅是“画几个输入框和按钮”更关乎如何组织信息流、如何引导用户操作、如何构建一个符合直觉且高效的生产力界面。很多教程和文档只告诉你EditorGUILayout.TextField怎么用但很少系统性地讲清楚为什么我的布局会乱如何让复杂的表单自适应宽度怎样处理大量数据时的性能问题以及有哪些官方文档没写但实践中至关重要的“潜规则”这篇文章我就结合自己这些年踩过的坑和积累的经验从最基础的控件绘制讲起一直深入到高级的布局管理与性能优化。目标很明确让你不仅能“用”EditorGUILayout更能“用好”它设计出专业级的编辑器界面。无论你是想优化一个资源导入工具的配置面板还是构建一个全新的关卡编辑器这里面的技巧都能直接派上用场。2. 核心思路理解EditorGUILayout的布局引擎与设计哲学在动手写代码之前我们必须先理解EditorGUILayout底层是怎么工作的。这能从根本上解释很多令人困惑的布局问题并指导我们做出正确的设计选择。2.1 自动布局 vs. 绝对布局核心差异与选用时机Unity提供了两套GUI系统GUI/EditorGUI和GUILayout/EditorGUILayout。它们最核心的区别在于布局方式。GUI/EditorGUI(绝对布局)你需要为每个控件指定一个精确的矩形区域Rect。比如EditorGUI.TextField(new Rect(10, 10, 200, 20), “Label”, value)。它的优点是控制精准性能开销极小因为系统不需要计算控件大小和位置。但缺点也非常明显代码冗长难以维护并且无法自适应窗口大小。一旦你调整了某个控件的位置或大小后续所有控件的Rect参数可能都需要手动调整这在复杂界面中是一场噩梦。GUILayout/EditorGUILayout(自动布局)你只需要关心控件的绘制顺序系统会自动为你计算它们的位置和大小。它会根据控件的内容如文本长度、当前可用空间以及你提供的布局选项如GUILayout.Width来动态排列。这正是我们进行编辑器扩展时绝大部分情况下的首选因为它能创建出可伸缩、易维护的界面。EditorGUILayout在GUILayout的基础上增加了大量为编辑器环境优化的专用控件比如ObjectField用于拖拽资源、LayerField、Popup等并且样式与Unity原生编辑器保持一致。关键理解EditorGUILayout的布局是一个“流式”过程。想象你在一张白纸上从上到下、从左到右地放置控件。系统会维护一个“当前光标位置”。每绘制一个控件这个光标就会向下移动对于垂直布局或向右移动对于水平布局。布局组如HorizontalScope和选项如ExpandWidth则是在控制这个“流”的方向和规则。2.2 控件绘制的生命周期与重绘机制编辑器UI不是游戏运行时UI它的更新频率受不同事件驱动Repaint 事件这是界面重绘的主要事件。当窗口需要被刷新时如被其他窗口遮挡后再次显示、鼠标滑过需要高亮时Unity会调用OnGUI方法。Layout 事件在Repaint之前发生用于计算所有GUILayout控件的大小和位置。你通常不需要直接处理它。交互事件如鼠标点击、拖拽、键盘输入等也会触发OnGUI以便处理用户交互。一个常见的性能陷阱是在OnGUI中执行昂贵的计算或资源加载。因为OnGUI可能每帧都被调用多次尤其在用户交互时。正确的做法是将数据准备和UI绘制分离。例如在OnInspectorGUI中只进行UI绘制和简单的数据获取/设置复杂的数据验证、网络请求或文件读取应该在按钮事件触发时或通过EditorApplication.update委托在后台进行。2.3 为“扩展性”而设计构建可维护的编辑器界面在设计自定义Inspector或编辑器窗口时要有模块化的思想。不要把所有控件的绘制代码都堆在一个巨大的OnGUI方法里。按功能分区将相关的控件分组用EditorGUILayout.BeginVertical(“GroupBox”)或EditorGUILayout.Foldout来组织。这样逻辑清晰也便于用户理解。抽象绘制方法对于重复出现的复杂控件组合比如一个包含标签、输入框和单位说明的“速度”属性将其封装成一个独立的静态方法如DrawSpeedField(ref float speed)。这能极大减少代码重复也方便统一修改样式。使用PropertyDrawer如果你发现自己频繁地为某种自定义数据类型如一个Serializable的类或结构体绘制相同的UI那么为其创建一个PropertyDrawer是更优雅、复用性更强的方案。它能让该数据类型在任何SerializedProperty出现的地方都自动使用你定义的UI。理解了这些底层逻辑我们就能避免很多初级错误并开始构建稳健的UI代码结构。3. 基础控件深度解析与高效使用指南掌握了理论我们来逐一拆解最常用的EditorGUILayout控件。我会重点讲那些容易用错、或者有隐藏技巧的部分。3.1 文本与输入TextField、TextArea与Delayed控件的妙用EditorGUILayout.TextField是最基础的文本输入框。但这里有几个细节DelayedTextField这是我最常使用的文本输入变体。用户输入内容后只有在按下回车或输入框失去焦点时绑定的字符串变量才会更新。这避免了在用户输入每个字符时都触发可能昂贵的验证或序列化操作对于需要频繁编辑的配置项非常有用。TextArea用于多行文本输入。一个关键技巧是通常你需要为它指定一个高度否则它可能只显示一行。EditorGUILayout.TextArea(content, GUILayout.Height(60))。标签与控件的对齐EditorGUILayout的控件方法大多有一个重载版本第一个参数是label。系统会自动处理标签和控件的对齐这比你自己用LabelField再加一个TextField要规范得多。// 推荐做法使用带标签参数的控件方法 string playerName EditorGUILayout.TextField(“玩家名称”, playerName); // 不推荐手动组合标签和控件难以对齐 EditorGUILayout.LabelField(“玩家名称”); playerName EditorGUILayout.TextField(playerName);3.2 对象与资源选择ObjectField的完整实践ObjectField用于让用户通过拖拽或点击选择器来赋值一个UnityEngine.Object引用。类型安全始终使用泛型版本EditorGUILayout.ObjectFieldT这能确保选择器只显示指定类型T或其子类的资源避免运行时类型错误。allowSceneObjects参数这个参数至关重要。如果为true用户可以选择场景中的游戏对象GameObject如果为false则只能从项目资产Project中选择。对于配置预制体或材质等资源通常设为false对于需要关联场景中某个实体的工具则设为true。处理空值ObjectField允许值为null。如果你的逻辑不允许空值需要在绘制后进行检查并给出明确的UI提示比如将字段背景标红。// 选择任意类型的材质 Material mat (Material)EditorGUILayout.ObjectField(“基础材质”, mat, typeof(Material), false); // 更好的做法使用泛型类型安全且代码更简洁 Material mat EditorGUILayout.ObjectFieldMaterial(“基础材质”, mat, false);3.3 枚举、遮罩与弹出菜单EnumPopup、MaskField与PopupEnumPopup用于绘制枚举下拉菜单。如果枚举定义了[Flags]特性它会自动以遮罩字段多选框的形式呈现。MaskField专门用于绘制位掩码LayerMask、Flags Enum。它的返回值是一个int位掩码值你需要自己处理与枚举值之间的转换。一个常见的封装模式是创建一个通用的绘制方法。Popup最灵活的弹出选择器。你需要自己提供显示的字符串数组和当前选中的索引。它非常适合动态生成的选项列表比如从一个文件夹里读取所有预设文件的名字。// 绘制一个标准的枚举下拉框 MyEnumType enumValue (MyEnumType)EditorGUILayout.EnumPopup(“选项”, enumValue); // 绘制一个LayerMask字段 LayerMask selectedLayers EditorGUILayout.MaskField(“检测层”, selectedLayers, LayerMask.LayerToName); // 注意MaskField返回int而LayerMask是struct需要转换。更常见的做法是直接使用 LayerMask selectedLayers EditorGUILayout.LayerMaskField(“检测层”, selectedLayers); // 使用Popup绘制动态选项 string[] weaponNames GetWeaponNamesFromFolder(); int selectedIndex EditorGUILayout.Popup(“选择武器”, currentWeaponIndex, weaponNames);3.4 滑动条与进度条Slider与ProgressBar的进阶控制Slider用于在一个范围内选择数值。注意它有float和int两个版本。一个高级技巧是结合MinMaxSlider它可以让你同时拖动最小值和最大值非常适合做范围筛选如粒子生命值区间。自定义ProgressBarUnity没有提供直接的EditorGUILayout.ProgressBar但我们可以用EditorGUI.ProgressBar配合GUILayout的GetRect方法来模拟。关键在于先通过GUILayoutUtility.GetRect获取一个确定大小的矩形区域再在这个矩形上绘制进度条。// 绘制一个最小-最大值滑块 float min 0, max 1; EditorGUILayout.MinMaxSlider(“生命值范围”, ref min, ref max, 0f, 10f); // 绘制一个自定义的进度条 Rect progressRect GUILayoutUtility.GetRect(200, 20); // 获取一个200x20的矩形区域 EditorGUI.ProgressBar(progressRect, currentProgress, “加载中…”); // 为了让布局流继续通常需要在后面加一个空的布局空间因为GetRect“消耗”了布局空间但布局光标没移动 GUILayout.Space(20);4. 核心布局策略构建灵活且美观的界面结构控件的绘制是砖瓦布局则是建筑的骨架。混乱的布局会让再好的功能也难以使用。4.1 垂直与水平布局组BeginVertical/Horizontal的正确打开方式BeginVertical和BeginHorizontal是构建布局的基础。强烈建议使用using语句块来管理它们这能确保End调用不会因为异常或提前返回而被遗漏代码也更清晰。// 旧式写法易出错 EditorGUILayout.BeginVertical(“box”); // … 绘制控件 EditorGUILayout.EndVertical(); // 推荐写法安全、清晰 using (new EditorGUILayout.VerticalScope(“box”)) { // … 绘制控件 EditorGUILayout.LabelField(“在盒子内”); using (new EditorGUILayout.HorizontalScope()) { // 水平排列两个按钮 if (GUILayout.Button(“确定”)) { } if (GUILayout.Button(“取消”)) { } } } // 离开using块时EndVertical和EndHorizontal会自动调用水平布局的宽度分配在水平布局中控件默认会收缩到最小宽度。如果你希望它们按比例填充可用空间需要使用GUILayout.ExpandWidth选项。using (new EditorGUILayout.HorizontalScope()) { // 这个标签只占用所需宽度 EditorGUILayout.LabelField(“名称:”, GUILayout.Width(50)); // 这个输入框会扩展以填充剩余的水平空间 name EditorGUILayout.TextField(name, GUILayout.ExpandWidth(true)); // 这个按钮保持固定宽度 if (GUILayout.Button(“查找”, GUILayout.Width(60))) { } }4.2 灵活空间与固定间隔Space、FlexibleSpace与Width/Height选项GUILayout.Space(pixels)插入固定像素的空白。用于微调控件间距。GUILayout.FlexibleSpace()插入一个“弹性空间”。它会尽可能多地吸收所有剩余的空间。常用于实现右对齐或两端对齐。实现右对齐按钮在按钮前加一个FlexibleSpace按钮就会被推到最右边。GUILayout.Width/Height为下一个控件指定一个明确的宽度或高度。对于TextField、Button等控件非常有用。也可以使用MinWidth/MaxWidth来设置约束。EditorGUILayout.GetControlRect当你需要精确控制一个控件区域但又想利用自动布局的垂直流时可以使用这个方法。它返回一个用于下一行控件的标准高度的矩形。using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField(“状态:”); EditorGUILayout.LabelField(“运行中”, EditorStyles.boldLabel); // 弹性空间将后面的控件推到右侧 GUILayout.FlexibleSpace(); if (GUILayout.Button(“停止”, GUILayout.Width(80))) { } } // 添加一个10像素的垂直间隔 GUILayout.Space(10);4.3 折叠组与滚动视图管理复杂信息层级Foldout用于创建可折叠/展开的区域是组织大量可选配置项的利器。你需要自己维护一个布尔变量来控制其开关状态。一个常见的模式是将Foldout的绘制和其内容的绘制放在一个条件判断里。bool showAdvancedSettings EditorGUILayout.Foldout(showAdvancedSettings, “高级设置”); if (showAdvancedSettings) { using (new EditorGUI.IndentLevelScope()) // 增加缩进视觉上更清晰 { advancedOption1 EditorGUILayout.Toggle(“选项1”, advancedOption1); // … 更多高级设置 } }ScrollView当内容超出显示区域时必须使用滚动视图。和布局组一样推荐使用ScrollViewScope。关键点在于你需要维护一个Vector2变量来存储滚动位置。// 在类的成员变量中定义 private Vector2 scrollPosition; // 在OnGUI中使用 scrollPosition EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); { // 这里放置可能很长的内容比如一个列表 for (int i 0; i 100; i) { EditorGUILayout.LabelField(“项目 “ i); } } EditorGUILayout.EndScrollView();重要性能提示ScrollView只裁剪了视觉区域但其中的所有控件仍然会被完整绘制和布局计算。如果列表项非常多比如成千上万这会导致严重的性能问题。此时必须引入虚拟化列表的概念即只绘制当前滚动区域可见的项。这需要更复杂的逻辑通常需要自己计算滚动位置和项索引。4.4 对齐、缩进与样式打造原生编辑器体验EditorGUIUtility.labelWidth全局设置所有控件标签的宽度。统一调整这个值可以让你的表单对齐得非常整齐。通常在绘制一组相关控件前临时修改它绘制完再恢复。EditorGUI.IndentLevelScope临时增加缩进级别。非常适合用于折叠组内部或表示层级关系。使用内置EditorStyles不要自己定义字体、颜色。Unity提供了丰富的EditorStyles如EditorStyles.boldLabel,EditorStyles.helpBox,EditorStyles.wordWrappedLabel使用它们能确保你的工具看起来和Unity编辑器是一体的。// 保存旧标签宽度并设置新宽度 float oldLabelWidth EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth 150; // 设置一个更宽的标签宽度 EditorGUILayout.TextField(“这是一个非常长的配置项名称”, value); EditorGUILayout.FloatField(“另一个长标签项”, floatValue); // 恢复旧标签宽度 EditorGUIUtility.labelWidth oldLabelWidth; // 使用帮助框样式显示提示信息 EditorGUILayout.HelpBox(“请确保资源已放入指定文件夹。”, MessageType.Info);5. 高级技巧与性能优化实战当界面变得复杂时一些高级技巧和性能考量就变得必不可少。5.1 处理数组与列表可排序、可调整大小的列表界面在Inspector中绘制数组SerializedProperty的数组是常见需求。虽然Unity默认提供了基础的增删箭头但体验往往不佳。我们可以创建更友好的列表界面。核心思路是获取SerializedProperty的arraySize然后在一个循环中遍历每个元素使用PropertyField来绘制。同时我们提供自定义的“添加”、“删除”、“上移”、“下移”按钮。SerializedProperty myListProperty serializedObject.FindProperty(“myList”); EditorGUILayout.LabelField(“物品列表”, EditorStyles.boldLabel); for (int i 0; i myListProperty.arraySize; i) { using (new EditorGUILayout.HorizontalScope()) { // 绘制列表元素 EditorGUILayout.PropertyField(myListProperty.GetArrayElementAtIndex(i), GUIContent.none); // 删除按钮 if (GUILayout.Button(“-“, GUILayout.Width(20))) { myListProperty.DeleteArrayElementAtIndex(i); break; // 删除后退出循环避免索引错误 } // 上移/下移按钮可选 if (i 0 GUILayout.Button(“↑”, GUILayout.Width(20))) { /* 交换逻辑 */ } if (i myListProperty.arraySize - 1 GUILayout.Button(“↓”, GUILayout.Width(20))) { /* 交换逻辑 */ } } } // 添加按钮 if (GUILayout.Button(“添加新物品”)) { myListProperty.arraySize; }5.2 自定义控件与PropertyDrawer实现高度复用当你发现某段UI代码在多个地方重复出现时就是考虑封装的时候了。自定义静态方法最简单的封装适合逻辑简单的控件组。PropertyDrawer这是Unity提供的标准扩展机制。为你自定义的[Serializable]类或结构体创建一个继承自PropertyDrawer的类重写OnGUI方法。之后在任何SerializedProperty中用到这个类型都会自动使用你的绘制器。这是保持Inspector UI一致性的最佳实践。// 假设有一个自定义的数值范围结构体 [System.Serializable] public struct FloatRange { public float min; public float max; } // 为其创建PropertyDrawer [CustomPropertyDrawer(typeof(FloatRange))] public class FloatRangeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); // 将单行高度拆分成多行来绘制 Rect lineRect new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); EditorGUI.LabelField(lineRect, label); lineRect.y EditorGUIUtility.singleLineHeight EditorGUIUtility.standardVerticalSpacing; float indent EditorGUIUtility.labelWidth; float fieldWidth (lineRect.width - indent) / 2 - 5; Rect minRect new Rect(lineRect.x indent, lineRect.y, fieldWidth, lineRect.height); Rect maxRect new Rect(minRect.x fieldWidth 10, lineRect.y, fieldWidth, lineRect.height); EditorGUI.PropertyField(minRect, property.FindPropertyRelative(“min”), GUIContent.none); EditorGUI.PropertyField(maxRect, property.FindPropertyRelative(“max”), GUIContent.none); EditorGUI.EndProperty(); } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { // 告诉Unity这个属性绘制需要两行的高度 return EditorGUIUtility.singleLineHeight * 2 EditorGUIUtility.standardVerticalSpacing; } }5.3 响应式布局与窗口缩放处理编辑器窗口可能被用户拖拽改变大小。好的UI应该能适应不同宽度。使用ExpandWidth/ExpandHeight让控件填充可用空间。避免绝对宽度尽量不要使用固定的GUILayout.Width除非是针对图标、按钮等需要确定大小的元素。对于文本输入框、滑动条等让其扩展。EditorGUIUtility.currentViewWidth这个属性可以获取当前编辑器区域的宽度。你可以根据它来动态调整布局比如在宽度足够时水平排列在狭窄时垂直排列。float availableWidth EditorGUIUtility.currentViewWidth; if (availableWidth 500) { // 宽屏模式水平排列 using (new EditorGUILayout.HorizontalScope()) { DrawSettingsPanel(); DrawPreviewPanel(); } } else { // 窄屏模式垂直排列 DrawSettingsPanel(); DrawPreviewPanel(); }5.4 性能关键减少OnGUI调用与控件数量OnGUI调用成本很高。优化原则是只绘制需要绘制的。条件绘制如果某个折叠区域是收起的就不要绘制里面的控件。延迟加载对于非常复杂的UI如包含大量缩略图的资源浏览器考虑分帧加载或仅在需要时如展开时才初始化其数据。重用GUIContentGUIContent对象可以缓存和重用避免每次OnGUI都创建新的。这对于在循环中绘制的标签尤其有效。EditorGUI.BeginChangeCheck()/EditorGUI.EndChangeCheck()将这两个调用包裹住一系列控件。只有当用户真正修改了其中任何一个控件的值时EndChangeCheck()才会返回true。这样你可以避免在每次OnGUI时都执行昂贵的验证或保存逻辑。// 缓存GUIContent private static readonly GUIContent NameLabel new GUIContent(“名称”); void OnGUI() { // 检查值是否变化只有变化了才执行后续操作 EditorGUI.BeginChangeCheck(); { myString EditorGUILayout.TextField(NameLabel, myString); myFloat EditorGUILayout.FloatField(“数值”, myFloat); } if (EditorGUI.EndChangeCheck()) { // 只有字段被修改了才执行这个相对昂贵的操作 SaveConfig(); Repaint(); // 如果需要请求重绘 } // 条件绘制 if (showAdvanced) { // … 绘制大量高级控件 } }6. 常见问题排查与实战心得即使掌握了所有API在实际开发中还是会遇到各种奇怪的问题。这里分享一些我踩过的坑和解决方案。6.1 布局错乱、重叠或不对齐的典型原因混合使用GUILayout和绝对Rect这是最常见的问题。如果你在BeginVertical/Horizontal的using块内使用了EditorGUI.LabelField(rect, …)这种需要传入Rect的方法布局引擎就会混乱。确保在自动布局块内只使用EditorGUILayout前缀的方法。忘记关闭布局组每个Begin必须有对应的End。使用using语句块是杜绝此问题的最佳实践。FlexibleSpace使用不当多个FlexibleSpace会平分剩余空间。如果你想要一个控件居中应该在它前后各加一个FlexibleSpace。ScrollView内部高度计算错误如果ScrollView内部的内容高度是动态变化的比如可折叠的列表你需要准确计算总高度或者使用GUILayout.Height为ScrollView指定一个固定高度否则滚动条行为会异常。6.2 控件状态不同步或值不更新的排查思路检查变量是否被序列化在自定义编辑器窗口EditorWindow中非静态的成员变量在窗口关闭后不会自动保存。你需要使用ScriptableObject来存储数据或者手动实现序列化如使用EditorPrefs。在Inspector中使用serializedObject.Update()和serializedObject.ApplyModifiedProperties()这是黄金法则。在OnInspectorGUI开始时调用Update()来将磁盘上的属性值拉取到SerializedProperty中在绘制完所有控件后或响应用户操作后调用ApplyModifiedProperties()将修改写回磁盘。忘记调用ApplyModifiedProperties()是修改不生效的最常见原因。Delayed控件的误解Delayed控件只有在完成编辑回车或失去焦点后才更新绑定变量。在编辑过程中变量值是不会变的。6.3 编辑器窗口与Inspector扩展中的特殊考量EditorWindowvsEditorEditorWindow用于创建独立的工具窗口拥有完整的OnGUI生命周期。Editor自定义Inspector用于扩展特定组件在Inspector面板中的显示。两者的UI绘制原理相同但数据获取方式不同EditorWindow直接操作类成员Editor通过SerializedProperty。CreateEditor与预览渲染如果你想在自定义Inspector或窗口内渲染一个对象的预览比如一个模型的缩略图可以使用Editor.CreateEditor为目标对象创建一个临时的Editor实例然后调用其OnPreviewGUI方法。处理多对象编辑在自定义Inspector的OnInspectorGUI中targets数组包含了当前选中的所有对象。你需要遍历它们来应用修改并处理好值不一致的情况使用EditorGUI.showMixedValue。6.4 从问题到解决方案的速查表问题现象可能原因解决方案控件重叠或位置错乱混合使用了EditorGUI(Rect)和EditorGUILayout在自动布局组内统一使用EditorGUILayout系列方法修改了值但未保存Inspector中未调用serializedObject.ApplyModifiedProperties()在绘制代码后或按钮事件中调用ApplyModifiedProperties()滚动视图(ScrollView)行为异常未给BeginScrollView指定高度或内部内容高度计算不准为BeginScrollView添加GUILayout.Height(固定高度)或确保内部内容高度稳定列表操作增删后界面显示旧数据操作数组后未立即退出循环或未请求重绘删除元素后使用break跳出循环并调用this.Repaint()或serializedObject.Update()编辑器窗口关闭后数据丢失数据存储在非静态成员变量中使用ScriptableObject存储配置或重写EditorWindow的OnEnable/OnDisable来读写EditorPrefs界面在宽屏和窄屏下布局不佳使用了过多的绝对宽度使用ExpandWidth或根据EditorGUIUtility.currentViewWidth动态切换布局方式包含大量控件的界面卡顿每次OnGUI都绘制全部内容且未做优化使用Foldout折叠非必要区域对长列表实现虚拟化绘制缓存GUIContent我个人在构建复杂编辑器工具时有一个习惯先在一张纸上或白板软件上画出界面的草图划分好功能区如“输入区”、“配置区”、“操作按钮区”、“预览区”。然后用VerticalScope和HorizontalScope将这些区域在代码中搭建出骨架再往里面填充具体的控件。这种“先布局后控件”的方式能让代码结构非常清晰也更容易调试布局问题。最后再分享一个小心得多观察和学习Unity官方编辑器的界面设计。比如Project窗口的列表视图、Inspector中不同组件的排版、Animation窗口的时间轴。思考它们是如何使用EditorGUILayout来实现的这往往是学习高级布局技巧最快的方式。当你能够仿造出接近原生体验的界面时你的编辑器扩展能力就已经达到一个很高的水准了。