1. 项目概述:理解Unity视图叠加的核心价值
在Unity编辑器里摸爬滚打这么多年,我发现一个高效的工作流,往往就藏在那些看似不起眼的视图布局细节里。今天要聊的“视图叠加”,就是这样一个能极大提升开发效率的“隐藏技能”。简单来说,视图叠加(Overlay)不是指游戏运行时UI的叠加渲染,而是特指在Unity编辑器内部,如何将不同的功能窗口或信息面板,以非侵入、可自定义的方式“叠加”显示在Scene视图或Game视图上。这听起来可能有点抽象,我举个例子:你在调整一个角色的动画状态机时,是否希望不用频繁切换窗口,就能在Scene视图旁边实时看到Animator Controller的预览?或者,在布置场景灯光时,能否让灯光的参数面板直接悬浮在3D视图的一角,实现所见即所得的调整?这就是视图叠加要解决的问题。
对于Unity开发者,无论是刚入门的新手还是经验丰富的老手,掌握视图叠加的创建与使用,都意味着能将编辑器从“功能集合”升级为“个性化工作站”。它直接解决了多窗口频繁切换导致的注意力分散和工作流割裂问题,尤其在进行美术资源整合、关卡设计、动画调试或Shader编写时,效果提升立竿见影。本文将从一个实践者的角度,彻底拆解Unity编辑器中创建自定义视图叠加的原理、方法和实战技巧,让你能根据自己的工作习惯,打造出独一无二的高效开发环境。
2. 视图叠加的设计思路与实现原理
2.1 为什么需要自定义视图叠加?
在深入代码之前,我们得先想明白:Unity编辑器自带的布局(Layout)已经提供了窗口排列功能,为什么我们还要折腾叠加视图?核心原因在于“上下文无缝切换”。标准布局是将窗口平铺或层叠在标签页里,要查看或操作另一个窗口的内容,你必须进行点击或快捷键切换,这个动作打断了你在主视图(如Scene视图)上的连续操作。而视图叠加的核心思想是“信息伴随”,让辅助信息或工具紧贴着你的工作焦点(3D场景或游戏画面)显示,减少鼠标移动和视线跳转。
从技术实现上看,Unity的编辑器扩展体系提供了强大的EditorWindow和GUI系统。视图叠加本质上是EditorWindow的一种特殊呈现模式。普通的EditorWindow会作为一个独立的浮动或停靠窗口存在,而叠加视图则通过HasPreviewGUI、OnPreviewGUI或更底层的GUI与EditorGUI方法,将其内容绘制到另一个窗口(宿主窗口,通常是SceneView或GameView)的特定区域上。这个绘制过程由Unity编辑器框架管理,我们需要做的就是定义绘制什么内容,以及决定绘制在哪里。
2.2 实现叠加的两种核心路径
根据叠加内容的复杂度和集成深度,主要有两种实现路径:
路径一:利用EditorWindow的预览接口这是创建简单信息叠加最快捷的方式。通过创建一个继承自EditorWindow的类,并重写HasPreviewGUI返回true,然后实现OnPreviewGUI方法。在这个方法里,你可以使用GUILayout或GUI方法绘制任何UI。接着,你需要将这个窗口的“预览”拖拽到Scene视图的标签栏上。这种方式适合显示一些只读的、轻量的信息面板,例如当前选中物体的统计信息、实时帧率、自定义的调试数据等。它的优点是实现简单,与编辑器集成度尚可;缺点是自定义布局能力较弱,交互功能有限,且外观受编辑器主题影响较大。
路径二:通过Editor类或ScriptableObject监听与绘制这是功能更强大、更灵活的主流方案。其核心是创建一个编辑器脚本,通过UnityEditor.SceneView或UnityEditor.EditorApplication的回调(如duringSceneGui委托),在每一帧的GUI绘制周期中,向Scene视图添加自定义的GUI元素。具体步骤通常包括:
- 创建一个用于管理叠加状态和数据的类(可能继承自
ScriptableObject以便保存配置)。 - 在
[InitializeOnLoadMethod]标记的静态方法中,向SceneView.duringSceneGui事件添加监听函数。 - 在该监听函数中,使用
Handles.BeginGUI()和Handles.EndGUI()将GUI绘制上下文切换到Scene视图的屏幕空间。 - 在
BeginGUI和EndGUI之间,使用GUI或GUILayoutAPI自由地绘制按钮、滑块、标签、纹理等,完全自定义叠加面板的外观、位置和交互逻辑。
这种方式让你获得了完全的UI控制权,可以创建出功能复杂的工具面板,如自定义笔刷、地形编辑工具、快速材质切换器、甚至是集成在Scene视图内的小型关卡编辑器。这也是本文后续重点详解的方案。
注意:无论采用哪种路径,都必须牢记编辑器脚本的生命周期和性能。叠加的UI会在每一帧进行绘制,因此绘制逻辑应力求高效,避免在
OnGUI方法中进行昂贵的计算或资源加载。
3. 核心细节解析与实操要点
3.1 关键API深度剖析
要玩转视图叠加,必须吃透几个核心的UnityEditor API:
SceneView.duringSceneGui委托:这是通往Scene视图绘制管道的“门票”。任何添加到这个委托的方法,都会在Scene视图每次进行GUI渲染时被调用。这是注入自定义UI的主要入口点。// 添加监听 SceneView.duringSceneGui += OnSceneGUI; // 移除监听(非常重要,避免内存泄漏!) SceneView.duringSceneGui -= OnSceneGUI; private void OnSceneGUI(SceneView sceneView) { // 你的绘制逻辑将在这里执行 }Handles.BeginGUI()/Handles.EndGUI():这一对方法用于切换GUI矩阵。在OnSceneGUI方法中,默认的坐标系是世界空间或手柄坐标系。调用Handles.BeginGUI()后,后续的GUI/GUILayout调用将使用以Scene视图左上角为原点的屏幕像素坐标系,这让我们能够像在普通EditorWindow里一样使用GUIAPI进行绝对或相对布局。绘制完成后必须调用Handles.EndGUI()来恢复之前的矩阵状态。GUI与GUILayout在叠加中的使用:在叠加面板中,GUI(绝对定位)通常比GUILayout(自动布局)更可控。因为叠加面板的位置和大小往往是固定的或根据特定逻辑计算的,使用GUI可以精确控制每个元素在屏幕上的矩形区域。例如,GUI.Button(new Rect(10, 10, 100, 30), “Click Me”)。当然,对于面板内部简单垂直/水平排列,GUILayout依然方便。EditorGUIUtility相关工具:这个类提供了许多在编辑器GUI中使用的实用工具,对于叠加视图尤其有用。EditorGUIUtility.PixelsPerPoint:用于处理Retina等高DPI显示器的缩放。EditorGUIUtility.SingleLineHeight:获取标准行高,确保UI元素对齐。EditorGUIUtility.isProSkin:判断当前是否使用专业(深色)主题,以便适配UI颜色。
3.2 叠加面板的布局与定位策略
创建一个美观且实用的叠加面板,布局是关键。你不能让面板遮挡住场景中重要的物体,也不能让它太偏以至于不方便操作。以下是几种常见的定位策略:
- 固定角定位:这是最常见的方式。将面板固定在Scene视图的四个角之一(如右上角)。你需要获取Scene视图的当前
camera.pixelRect或position来得到视图的宽高,然后计算面板的位置。void DrawOverlayPanel(SceneView sceneView) { Handles.BeginGUI(); float panelWidth = 200; float panelHeight = 150; float margin = 10; // 将面板定位在右上角,留出边距 Rect panelRect = new Rect(sceneView.position.width - panelWidth - margin, margin, panelWidth, panelHeight); GUILayout.BeginArea(panelRect, EditorStyles.helpBox); // 使用helpBox样式增加背景 GUILayout.Label(“自定义工具面板”, EditorStyles.boldLabel); // ... 更多UI控件 GUILayout.EndArea(); Handles.EndGUI(); } - 可拖动面板:提升用户体验的进阶功能。你需要声明一个
Rect变量来存储面板位置,并在面板的标题栏区域处理鼠标拖拽事件。通过Event.current.type判断MouseDown、MouseDrag和MouseUp,并更新面板的Rect位置。 - 自适应与折叠:对于功能较多的面板,可以考虑加入折叠/展开功能。使用
EditorGUILayout.BeginFoldoutHeaderGroup可以轻松创建可折叠的区域,节省屏幕空间。
实操心得:在计算位置和尺寸时,务必考虑编辑器工具栏、状态栏以及可能存在的其他叠加面板所占用的空间。一个技巧是,可以先在
OnSceneGUI里用GUI.Box画一个半透明的参考矩形,动态调整位置,满意后再固定逻辑。
4. 完整实操:创建一个可配置的场景快速选择器叠加面板
接下来,我们通过一个完整的实例,一步步创建一个实用的叠加工具:一个悬浮在Scene视图上的面板,它可以列出场景中所有特定标签(如“Enemy”、“PickUp”)的物体,并允许你点击名称快速聚焦(Frame)到该物体上。这个工具对于在复杂场景中导航非常有用。
4.1 第一步:创建管理类与数据持久化
首先,我们创建一个ScriptableObject来保存工具的配置(如需要显示的标签),并管理其生命周期。
- 在项目中创建脚本
SceneQuickSelectorOverlay.cs。 - 让其继承自
ScriptableObject,并添加[CreateAssetMenu]属性,方便在项目中创建配置资产。using UnityEngine; using UnityEditor; using System.Collections.Generic; [CreateAssetMenu(fileName = “SceneQuickSelector”, menuName = “Tools/Scene Quick Selector”)] public class SceneQuickSelectorOverlay : ScriptableObject { public List<string> targetTags = new List<string> { “Enemy”, “PickUp” }; // 默认关注的标签 public bool isOverlayVisible = true; // 叠加面板是否可见 public Vector2 overlayPosition = new Vector2(10, 40); // 面板位置 public float overlayWidth = 200f; // 单例引用,便于访问 private static SceneQuickSelectorOverlay _instance; public static SceneQuickSelectorOverlay Instance { get { if (_instance == null) { // 尝试从资源中加载 string[] guids = AssetDatabase.FindAssets(“t:SceneQuickSelectorOverlay”); if (guids.Length > 0) { string path = AssetDatabase.GUIDToAssetPath(guids[0]); _instance = AssetDatabase.LoadAssetAtPath<SceneQuickSelectorOverlay>(path); } } return _instance; } } }
4.2 第二步:创建编辑器逻辑与GUI绘制
接下来,创建编辑器逻辑脚本。为了避免在每次编译后都需要手动重新添加事件监听,我们使用[InitializeOnLoadMethod]。
- 在
Editor文件夹下(如果没有则创建)创建脚本SceneQuickSelectorEditor.cs。 - 编写核心的初始化和绘制逻辑。
using UnityEditor; using UnityEngine; using System.Linq; [InitializeOnLoad] public static class SceneQuickSelectorEditor { private static SceneQuickSelectorOverlay _overlayData; private static bool _isDragging = false; private static Vector2 _dragStartMousePos; private static Vector2 _dragStartOverlayPos; static SceneQuickSelectorEditor() { // 当编辑器加载或脚本重编译后,自动注册监听 SceneView.duringSceneGui += OnSceneGUI; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } private static void OnPlayModeStateChanged(PlayModeStateChange state) { // 进入播放模式前,确保数据已加载(可选) if (state == PlayModeStateChange.ExitingEditMode) { GetOrCreateOverlayData(); } } private static SceneQuickSelectorOverlay GetOrCreateOverlayData() { if (_overlayData == null) { _overlayData = SceneQuickSelectorOverlay.Instance; if (_overlayData == null) { // 如果不存在,则创建一个默认的并保存到Assets根目录 _overlayData = ScriptableObject.CreateInstance<SceneQuickSelectorOverlay>(); AssetDatabase.CreateAsset(_overlayData, “Assets/SceneQuickSelectorOverlay.asset”); AssetDatabase.SaveAssets(); Debug.Log(“Created new SceneQuickSelectorOverlay asset.”); } } return _overlayData; } private static void OnSceneGUI(SceneView sceneView) { var data = GetOrCreateOverlayData(); if (data == null || !data.isOverlayVisible) return; Handles.BeginGUI(); // 切换到屏幕空间GUI // 计算面板矩形 Rect overlayRect = new Rect(data.overlayPosition.x, data.overlayPosition.y, data.overlayWidth, 0); // 先使用GUILayout计算动态高度 GUILayout.BeginArea(new Rect(overlayRect.x, overlayRect.y, overlayRect.width, Screen.height)); Rect contentRect = EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 标题栏 - 可拖动区域 GUILayout.BeginHorizontal(); GUILayout.Label(“快速选择器”, EditorStyles.boldLabel); GUILayout.FlexibleSpace(); // 开关按钮 data.isOverlayVisible = GUILayout.Toggle(data.isOverlayVisible, “”, “OL Toggle”, GUILayout.Width(15)); GUILayout.EndHorizontal(); // 处理标题栏拖拽 Rect titleRect = GUILayoutUtility.GetLastRect(); HandleDragging(titleRect, ref data.overlayPosition); EditorGUILayout.Space(5); // 标签列表编辑 EditorGUILayout.LabelField(“目标标签:”, EditorStyles.miniBoldLabel); for (int i = 0; i < data.targetTags.Count; i++) { EditorGUILayout.BeginHorizontal(); data.targetTags[i] = EditorGUILayout.TextField(data.targetTags[i]); if (GUILayout.Button(“-”, GUILayout.Width(20)) && data.targetTags.Count > 1) { data.targetTags.RemoveAt(i); EditorUtility.SetDirty(data); } EditorGUILayout.EndHorizontal(); } if (GUILayout.Button(“+ 添加标签”)) { data.targetTags.Add(“NewTag”); EditorUtility.SetDirty(data); } EditorGUILayout.Space(10); EditorGUILayout.LabelField(“场景对象:”, EditorStyles.miniBoldLabel); // 查找并列出所有带有目标标签的游戏对象 bool foundAny = false; foreach (var tag in data.targetTags) { GameObject[] taggedObjects = GameObject.FindGameObjectsWithTag(tag); if (taggedObjects.Length == 0) continue; foundAny = true; EditorGUILayout.LabelField($“Tag: {tag}”, EditorStyles.miniLabel); foreach (var go in taggedObjects.OrderBy(g => g.name)) { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(go.name, EditorStyles.miniButton)) { // 点击后选中并聚焦该物体 Selection.activeGameObject = go; sceneView.FrameSelected(); } GUILayout.Label(“(“ + tag + “)”, EditorStyles.miniLabel, GUILayout.Width(40)); EditorGUILayout.EndHorizontal(); } } if (!foundAny) { EditorGUILayout.HelpBox(“未找到带有指定标签的对象。”, MessageType.Info); } EditorGUILayout.EndVertical(); GUILayout.EndArea(); // 更新面板的实际高度,可用于更精确的交互判断(如点击外部关闭) overlayRect.height = contentRect.height; Handles.EndGUI(); // 恢复矩阵 // 如果数据被修改,标记为脏以保存 if (GUI.changed) { EditorUtility.SetDirty(data); } } private static void HandleDragging(Rect dragRect, ref Vector2 position) { Event e = Event.current; int controlID = GUIUtility.GetControlID(FocusType.Passive); switch (e.type) { case EventType.MouseDown: if (dragRect.Contains(e.mousePosition) && e.button == 0) { _isDragging = true; _dragStartMousePos = e.mousePosition; _dragStartOverlayPos = position; GUIUtility.hotControl = controlID; // 获取热点控制,防止事件穿透 e.Use(); } break; case EventType.MouseDrag: if (_isDragging && GUIUtility.hotControl == controlID) { Vector2 delta = e.mousePosition - _dragStartMousePos; position = _dragStartOverlayPos + delta; // 可选:限制位置在Scene视图范围内 position.x = Mathf.Clamp(position.x, 0, Screen.width - dragRect.width); position.y = Mathf.Clamp(position.y, 0, Screen.height - dragRect.height); e.Use(); } break; case EventType.MouseUp: if (_isDragging && GUIUtility.hotControl == controlID) { _isDragging = false; GUIUtility.hotControl = 0; e.Use(); } break; } } }
4.3 第三步:使用与测试
- 保存所有脚本,Unity编辑器会自动编译。
- 在Project视图中,右键点击 ->
Create->Tools->Scene Quick Selector,创建一个配置文件。 - 打开一个包含带有“Enemy”或“PickUp”标签物体的场景。
- 点击Scene视图,你应该能在视图的左上角(默认位置)看到一个名为“快速选择器”的浮动面板。你可以拖动标题栏移动它,点击开关隐藏/显示它,编辑关注的标签,并点击列表中的物体名称来快速选中并聚焦它们。
这个实例涵盖了从数据持久化、编辑器初始化、GUI绘制、交互处理(点击、拖拽)到与场景对象交互的完整流程,是一个功能完备的视图叠加工具。
5. 性能优化与高级技巧
5.1 性能考量与常见陷阱
视图叠加的代码在每一帧的Scene GUI周期中运行,性能不佳的代码会直接导致编辑器卡顿。以下是一些关键的优化点:
- 避免在
OnSceneGUI中进行昂贵的查找操作:上面的例子中,GameObject.FindGameObjectsWithTag是在每一帧调用的。这在小型场景中问题不大,但在大型场景中会成为性能瓶颈。优化方案是使用缓存。- 缓存机制:可以缓存查找结果,并仅在可能发生变化的时候(如进入播放模式、停止播放模式、通过
EditorApplication.hierarchyChanged委托感知层级变化时)更新缓存。
private static Dictionary<string, GameObject[]> _taggedObjectCache = new Dictionary<string, GameObject[]>(); private static double _lastCacheTime; private const double CACHE_REFRESH_INTERVAL = 0.5; // 每0.5秒最多更新一次缓存 private static GameObject[] GetCachedObjectsWithTag(string tag) { if (!_taggedObjectCache.ContainsKey(tag) || EditorApplication.timeSinceStartup - _lastCacheTime > CACHE_REFRESH_INTERVAL) { // 重新查找并更新缓存 _taggedObjectCache[tag] = GameObject.FindGameObjectsWithTag(tag); _lastCacheTime = EditorApplication.timeSinceStartup; } return _taggedObjectCache[tag]; } - 缓存机制:可以缓存查找结果,并仅在可能发生变化的时候(如进入播放模式、停止播放模式、通过
- 精简GUI绘制:
GUI和GUILayout的调用本身也有开销。对于静态或变化不频繁的UI部分,可以考虑使用EditorGUIUtility.HasState或自定义标志位来避免不必要的重绘。但通常,GUI开销远小于对象查找开销。 - 及时清理事件监听:这是内存泄漏的重灾区!如果你的编辑器脚本不是静态类,或者会在运行时动态创建/销毁,必须在
OnDisable或析构函数中移除对SceneView.duringSceneGui等静态事件的订阅。对于静态初始化,通常不需要手动移除,因为编辑器域重载时会清理。
5.2 提升用户体验的高级技巧
自定义样式与皮肤适配:为了让你的叠加面板看起来更原生或更独特,可以自定义
GUIStyle。你可以从内置样式(如EditorStyles.helpBox,EditorStyles.toolbarButton)开始修改,或者创建全新的样式。使用EditorGUIUtility.isProSkin来适配深色/浅色主题。private static GUIStyle _customHeaderStyle; private static GUIStyle CustomHeaderStyle { get { if (_customHeaderStyle == null) { _customHeaderStyle = new GUIStyle(EditorStyles.boldLabel); _customHeaderStyle.normal.textColor = EditorGUIUtility.isProSkin ? Color.cyan : Color.blue; _customHeaderStyle.alignment = TextAnchor.MiddleCenter; } return _customHeaderStyle; } } // 使用: GUILayout.Label(“我的面板”, CustomHeaderStyle);与Undo系统集成:如果你的叠加面板会修改场景中的对象(例如,通过它调整物体的位置),务必集成Undo系统,让操作可撤销。
if (GUILayout.Button(“重置位置”)) { Undo.RecordObject(selectedTransform, “Reset Position via Overlay”); selectedTransform.localPosition = Vector3.zero; }响应编辑器事件:除了
duringSceneGui,还可以监听其他编辑器事件来增强工具。例如,监听Selection.selectionChanged来高亮显示面板中当前选中的对象;监听EditorApplication.pauseStateChanged在暂停时更新特定信息。创建配置窗口:对于有复杂配置的工具,可以创建一个独立的
EditorWindow作为设置面板。在叠加面板上放一个齿轮图标,点击后打开这个配置窗口,让工具的配置更加友好和集中。
6. 常见问题排查与调试实录
即使按照步骤操作,也可能会遇到视图叠加不显示、位置错乱或功能异常的问题。下面是我在开发和教学中遇到的一些典型问题及解决方案。
6.1 叠加面板完全不显示
这是最常见的问题,可能的原因和排查步骤如下:
- 检查脚本编译错误:这是首要原因。如果编辑器脚本有任何编译错误,整个脚本都不会被执行。查看Console窗口是否有任何错误。
- 确认脚本位置:非编辑器核心逻辑的脚本(如
SceneQuickSelectorOverlay)可以放在任何Assets文件夹下。但包含[InitializeOnLoad]和SceneView.duringSceneGui监听的编辑器脚本(如SceneQuickSelectorEditor),必须放在名为Editor的文件夹内(或其子文件夹)。Unity不会将Editor文件夹下的脚本编译到游戏运行时,但会在编辑器中正常执行。 - 验证事件监听是否注册:在
SceneQuickSelectorEditor的静态构造函数或[InitializeOnLoadMethod]方法开始处添加Debug.Log(“SceneQuickSelectorEditor Initialized.”)。打开或切换Scene视图时,查看Console是否有这条日志。如果没有,说明初始化未执行。 - 检查绘制条件:确认
data.isOverlayVisible是否为true,以及GetOrCreateOverlayData()是否成功返回了有效的配置对象。可以在OnSceneGUI开始处添加日志打印这些值。 - 检查
Handles.BeginGUI()作用域:确保所有自定义的GUI/GUILayout调用都在Handles.BeginGUI()和Handles.EndGUI()之间。在这对调用之外使用GUI,内容不会显示在Scene视图上。
6.2 叠加面板位置错乱或交互异常
- 坐标系混淆:这是最可能的原因。记住,在
Handles.BeginGUI()之后,坐标系原点(0,0)是Scene视图客户区的左上角。如果你错误地使用了sceneView.camera.transform.position等世界坐标,面板位置会完全错乱。所有Rect的位置和尺寸都应基于sceneView.position(包含了工具栏)或Screen.width/Height来计算。 - 未处理DPI/缩放:在高DPI显示器上,Unity编辑器可能会有缩放。虽然
GUI和GUILayout系统在一定程度上会自动处理,但在计算精确位置时,使用EditorGUIUtility.PixelsPerPoint进行缩放是更稳妥的做法。float scale = EditorGUIUtility.pixelsPerPoint; Rect scaledRect = new Rect(originalRect.x * scale, originalRect.y * scale, originalRect.width * scale, originalRect.height * scale); - 事件处理冲突:如果你在叠加面板上处理了鼠标点击(
EventType.MouseDown)或键盘事件,一定要在处理后调用Event.current.Use()。这告诉Unity事件已被消费,防止它继续传递并触发Scene视图的默认操作(如点击选中物体)。反之,如果你希望点击面板后事件还能穿透(比如点击面板空白处取消选中),就不要Use它。在上面的拖拽示例中,我们只在处理拖拽逻辑时才Use事件。
6.3 编辑器变卡或输入延迟
- 性能热点:如前所述,首要怀疑对象是每帧进行的昂贵操作。使用Unity Profiler的Deep Profile模式,在编辑器下运行,查看
OnSceneGUI或你的监听函数占用的CPU时间。重点排查FindGameObjectsWithTag、GetComponents、Resources.Load等调用。 - 过度绘制:检查是否在每一帧都绘制了大量不需要变化的GUI元素。对于静态背景或文本,可以考虑将其渲染到
RenderTexture中缓存,但这对大多数工具来说属于过度优化。更实际的做法是确保没有在循环中创建不必要的GUIStyle对象,应将其缓存为静态变量。 - 内存泄漏:反复检查事件订阅。如果你的工具类不是静态的,并且可能被多次创建(例如,附着在ScriptableObject的编辑器类上),务必在
OnDisable中取消订阅所有静态事件。一个未被清理的订阅会导致旧实例一直存在于内存中并被持续调用。
6.4 配置不保存或重置
ScriptableObject数据未标记为Dirty:当你通过UI修改了ScriptableObject的字段(如列表中的标签)后,必须调用EditorUtility.SetDirty(data),然后通常还需要调用AssetDatabase.SaveAssets()来将更改写入磁盘。在上面的例子中,我们在GUI.changed为true时调用了SetDirty。GUI.changed会在任何GUI控件值改变时被设置为true。- 资产未正确保存:确保你的
ScriptableObject资产(.asset文件)确实保存在项目目录中,并且没有在版本控制中被忽略。你可以尝试在Inspector窗口中手动修改该资产的其他字段并保存,看是否能联动保存你的叠加配置。
掌握视图叠加的创建,相当于为你打开了一扇自定义Unity编辑器的大门。它不仅仅是一个显示技巧,更是一种思维模式——如何让工具主动适应人,而非让人去适应工具。从简单的信息显示,到复杂的场景内交互工具,其底层逻辑都是相通的。我个人的体会是,最好的工具往往是那些为了解决自己日常开发中某个微小痛点而诞生的。开始时不必追求大而全,从一个能提升你10%效率的小叠加面板做起,迭代优化,它最终会成长为你的独门利器。