1. 项目概述与核心价值如果你在Unity项目里管理过一堆需要排序、分类的配置项比如技能列表、道具背包、关卡编辑器里的地块那你肯定对Unity默认的列表或数组Inspector视图感到头疼。它就是个僵硬的、一维的、只能上下滚动的文本框列表想调整顺序只能靠复制粘贴。想直观地拖拽排序没门。想做成一个格子状的、能自由拖放的可视化界面那更是得自己动手。今天要聊的就是怎么用Unity Editor扩展和IMGUI从零开始搓一个“可拖拉放的格子列表”。这玩意儿听起来简单但真做起来从事件处理、数据序列化到视觉反馈每一步都有不少门道。这个功能的核心价值在于它能极大提升开发效率和工具链的易用性。想象一下策划同学要配置一个包含几十个技能的游戏角色每个技能有图标、名称、冷却时间等属性。如果只用默认的Inspector他得在一长串列表里找到想移动的技能记下它的所有属性删除再在目标位置重新创建一个并填上所有属性——这简直是灾难。而一个可视化的、可拖拽的格子列表让他能像在桌面整理文件一样用鼠标拖拽就能完成排序和分类配置体验和效率直接提升一个数量级。这不仅仅是“好看”而是实打实的生产力工具。接下来我会带你一步步拆解实现过程把每个技术细节和踩过的坑都讲清楚。2. 整体架构设计与核心思路要实现一个可拖放的格子列表我们不能把它看成一个单一的控件而应该拆解成几个相互协作的模块。整个架构的核心思路是数据与视图分离通过事件驱动交互。2.1 核心模块划分首先我们需要明确几个核心部分数据模型 (Data Model)这是列表的灵魂负责存储每个格子的数据。它必须能被Unity序列化以便在Inspector中显示和保存。通常我们会用一个ScriptableObject或者一个标记了[System.Serializable]的类来定义。视图绘制 (View Rendering)这部分负责在Editor窗口或Inspector中将数据模型以格子Grid的形式画出来。Unity Editor扩展主要使用IMGUI (Immediate Mode GUI)系统来绘制这意味着每一帧我们都需要主动调用绘制代码。交互逻辑 (Interaction Logic)这是最复杂的部分包括鼠标拾取判断鼠标点击在了哪个格子上。拖拽开始记录被拖拽格子的原始数据和索引。拖拽过程在鼠标位置绘制一个跟随的幽灵图标或预览并提供视觉反馈如高亮潜在的放置位置。拖拽结束计算鼠标释放位置对应的新索引并执行数据交换或插入操作。编辑器窗口容器 (Editor Window Container)提供一个承载我们自定义GUI的界面。可以是自定义的EditorWindow也可以是某个MonoBehaviour或ScriptableObject的定制化Inspector通过CustomEditor实现。2.2 为什么选择IMGUI而非UI ToolkitUnity提供了两套主要的编辑器UI系统老牌的IMGUI和较新的UI Toolkit。这里选择IMGUI主要基于以下几点实战考量成熟稳定API统一IMGUI是Unity编辑器自身的UI系统所有内置编辑器Inspector, SceneView等都由它驱动。其API稳定与EditorGUILayout、EditorGUI等类深度集成学习资源丰富。即时模式更适合工具开发IMGUI是“立即模式”每一帧重绘整个界面。这在开发需要复杂交互如拖拽的工具时逻辑反而更直接。我们可以在OnGUI方法里直接处理事件Event.current状态管理相对清晰。轻量级无额外开销对于编辑器工具尤其是内嵌在Inspector中的小部件IMGUI非常轻量不需要定义USS样式表或UXML模板上手快。广泛的社区支持与案例绝大多数现有的Unity编辑器插件和社区教程都基于IMGUI遇到问题时更容易找到解决方案和参考代码。当然UI Toolkit在构建复杂、现代化、样式统一的编辑器窗口方面有优势但对于我们这个聚焦于“一个可拖放格子列表控件”的目标IMGUI是更直接、更高效的选择。注意IMGUI的控件没有持久的对象引用其状态如哪个按钮被按下完全由你在每帧OnGUI调用时传入的参数和全局的GUI状态决定。理解这一点对处理拖拽这类连续交互至关重要。3. 核心细节解析与实操要点理解了整体架构我们深入到每个模块的内部看看具体有哪些技术细节和容易踩坑的地方。3.1 数据模型的设计与序列化数据模型的设计直接决定了工具的灵活性和健壮性。一个典型的格子数据类可能长这样using System; using UnityEngine; // 标记为可序列化这样它才能在Inspector中显示并且其列表能被Unity保存 [System.Serializable] public class GridItemData { public string itemName; // 物品名称 public Texture2D icon; // 图标 public int id; // 唯一ID public Color tintColor Color.white; // 格子色调用于可视化分类 // ... 可以根据需要添加更多字段如描述、数量、引用等 }然后在我们的主管理器可能是一个ScriptableObject或MonoBehaviour中需要持有一个这个数据模型的列表using System.Collections.Generic; using UnityEngine; public class DragDropGridManager : MonoBehaviour // 或继承自 ScriptableObject { public ListGridItemData itemList new ListGridItemData(); public int gridColumns 4; // 网格列数 public float gridCellSize 80f; // 格子大小 public float gridSpacing 5f; // 格子间距 }关键点与避坑指南[System.Serializable]是关键忘记给数据类加这个属性会导致列表在Inspector中显示为空或者数据无法被保存。使用ListT而非数组T[]List在Inspector中操作更方便有增减按钮并且在我们实现拖拽交换数据时使用Insert和RemoveAt方法比操作数组更简单高效。为管理器提供配置参数像gridColumns、gridCellSize这样的参数暴露出来允许使用者在Inspector中动态调整布局而无需修改代码工具通用性更强。3.2 IMGUI绘制基础绘制一个静态网格在自定义EditorWindow的OnGUI或CustomEditor的OnInspectorGUI中我们开始绘制。第一步是先画出静态的格子。using UnityEditor; using UnityEngine; public class DragDropGridEditorWindow : EditorWindow { private DragDropGridManager targetManager; private Vector2 scrollPosition; [MenuItem(Tools/拖放格子列表测试)] public static void ShowWindow() { GetWindowDragDropGridEditorWindow(格子列表编辑器); } void OnGUI() { // 1. 获取或创建目标数据管理器 if (targetManager null) { // 这里简单示例创建一个临时的。实际项目中可能从Selection中获取。 targetManager CreateInstanceDragDropGridManager(); // 初始化一些测试数据 for (int i 0; i 12; i) { targetManager.itemList.Add(new GridItemData { itemName $Item {i}, id i }); } } // 2. 开始一个滚动视图防止格子太多超出窗口 scrollPosition EditorGUILayout.BeginScrollView(scrollPosition); // 3. 计算网格布局 int columns Mathf.Max(1, targetManager.gridColumns); int totalCells targetManager.itemList.Count; int rows Mathf.CeilToInt((float)totalCells / columns); float cellSize targetManager.gridCellSize; float spacing targetManager.gridSpacing; float totalWidth columns * (cellSize spacing) - spacing; // 4. 使用GUILayout或GUI手动计算位置来绘制网格 // 这里使用GUILayout更简单但手动计算位置(GUI)对拖拽判断更精确。我们先展示GUILayout版本。 EditorGUILayout.BeginVertical(GUILayout.Width(totalWidth)); for (int i 0; i rows; i) { EditorGUILayout.BeginHorizontal(); for (int j 0; j columns; j) { int index i * columns j; if (index totalCells) break; // 超出数据范围停止绘制 GridItemData item targetManager.itemList[index]; // 绘制一个格子按钮或盒子 Rect cellRect GUILayoutUtility.GetRect(cellSize, cellSize, GUILayout.Width(cellSize), GUILayout.Height(cellSize)); DrawGridCell(cellRect, item, index); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } void DrawGridCell(Rect rect, GridItemData item, int index) { // 保存原始GUI颜色 Color originalColor GUI.color; // 使用物品的 tintColor 来给格子背景上色 GUI.color item.tintColor; // 绘制一个带纹理的盒子作为格子背景 GUI.Box(rect, item.icon, EditorStyles.helpBox); // 如果没有icon可以传 GUIContent.none // 恢复GUI颜色 GUI.color originalColor; // 在格子中央绘制物品名称 GUIStyle labelStyle new GUIStyle(GUI.skin.label); labelStyle.alignment TextAnchor.MiddleCenter; labelStyle.normal.textColor Color.black; // 根据背景色可能需要调整 GUI.Label(rect, item.itemName, labelStyle); // 在角落绘制ID Rect idRect new Rect(rect.x 5, rect.y 5, 20, 20); GUI.Label(idRect, item.id.ToString(), EditorStyles.miniLabel); } }实操心得GUILayoutvsGUIGUILayout系统自动布局方便但控制精度稍弱。GUI系统需要手动计算每个控件的Rect更繁琐但位置绝对精确。对于需要精确鼠标交互如拖拽的复杂控件后期我们往往会切换到手动计算Rect的GUI方式因为我们需要知道每一个格子的确切屏幕坐标。GUI.color的影响范围GUI.color和GUI.backgroundColor是全局状态在修改它们绘制自定义外观后务必记得恢复原值否则会影响后续所有GUI控件的绘制。这是一个非常常见的错误来源。滚动视图BeginScrollView当格子数量很多时必须使用滚动视图。注意BeginScrollView和EndScrollView必须成对调用并且传入的scrollPosition变量需要被更新。3.3 交互逻辑的灵魂Unity的Event系统IMGUI的交互核心是Event.current。它是一个静态类包含了当前帧所有的GUI事件信息鼠标位置、按键、类型等。处理拖拽本质上就是监听并处理特定的事件流。标准的拖拽事件流如下EventType.MouseDown鼠标在某个格子上按下。此时我们记录“拖拽开始”。EventType.MouseDrag鼠标按住并移动。此时我们设置GUIUtility.hotControl并开始绘制拖拽预览。EventType.MouseUp或EventType.DragPerform鼠标释放。此时我们判断释放位置执行数据操作并清理拖拽状态。EventType.Repaint每一帧都会发生用于绘制所有视觉元素包括拖拽过程中的预览效果。关键API解析Event.current.mousePosition获取当前鼠标在窗口内的坐标基于窗口左上角。Event.current.type判断当前事件类型。GUIUtility.hotControl一个全局整数ID用于标记“当前哪个控件是热门的”例如正在被拖拽的控件。设置它可以让IMGUI知道当前交互应该由你的控件处理防止事件被其他控件抢走。GUIUtility.GetControlID(FocusType.Passive)获取一个唯一的控件ID用于标识你的自定义控件。这个ID通常用来设置hotControl和keyboardControl。GUI.DragWindow()这个函数通常用于窗口拖拽但在处理自定义拖拽时理解其原理它内部处理了鼠标事件有助于我们编写自己的逻辑。4. 实操过程实现可拖放功能现在我们将静态网格升级为可交互的。我们需要在DrawGridCell方法和OnGUI的主循环中加入事件处理逻辑。我们会引入几个关键的成员变量来跟踪拖拽状态。4.1 定义拖拽状态变量在编辑器窗口类中添加public class DragDropGridEditorWindow : EditorWindow { // ... 之前的变量 ... // --- 拖拽状态变量 --- private bool isDragging false; private int draggedItemIndex -1; // 被拖拽物品的原始索引 private Vector2 dragStartPosition; // 拖拽开始的鼠标位置 private Rect draggedItemRect; // 被拖拽物品的原始矩形用于绘制预览 private GridItemData draggedItemData; // 被拖拽物品的数据副本 private int hoverOverIndex -1; // 鼠标悬停或准备放置的目标索引 }4.2 修改DrawGridCell处理鼠标按下DrawGridCell现在不仅要绘制还要处理点击事件判断拖拽是否开始。void DrawGridCell(Rect rect, GridItemData item, int index) { // ... 原有的绘制代码 ... // --- 事件处理 --- Event evt Event.current; int controlID GUIUtility.GetControlID(FocusType.Passive); // 为这个格子获取一个控件ID // 处理鼠标事件 switch (evt.GetTypeForControl(controlID)) { case EventType.MouseDown: if (rect.Contains(evt.mousePosition) evt.button 0) // 左键点击在格子内 { // 记录拖拽开始 isDragging true; draggedItemIndex index; draggedItemData item; // 注意这里直接引用复杂数据可能需要深拷贝 dragStartPosition evt.mousePosition; draggedItemRect rect; // 设置 hotControl告诉GUI系统这个控件现在正在被操作 GUIUtility.hotControl controlID; evt.Use(); // 标记事件已使用防止其他控件响应 } break; } // 在拖拽过程中如果鼠标经过此格子可以高亮它作为潜在的放置目标 if (isDragging draggedItemIndex ! index rect.Contains(evt.mousePosition)) { // 绘制一个高亮边框 EditorGUI.DrawRect(new Rect(rect.x - 2, rect.y - 2, rect.width 4, rect.height 4), new Color(0.5f, 0.8f, 1f, 0.3f)); hoverOverIndex index; // 记录悬停索引 } }4.3 在OnGUI主循环中处理拖拽与放置事件处理不能只放在每个格子里还需要在OnGUI的顶层处理持续的拖拽和释放逻辑。void OnGUI() { // ... 之前的滚动视图和网格绘制代码 ... // --- 顶层拖拽事件处理 --- HandleDragAndDropEvents(); // --- 绘制拖拽中的预览图 --- if (isDragging draggedItemData ! null Event.current.type EventType.Repaint) { // 在鼠标位置绘制一个半透明的预览图标 Rect previewRect new Rect(Event.current.mousePosition.x - draggedItemRect.width / 2, Event.current.mousePosition.y - draggedItemRect.height / 2, draggedItemRect.width, draggedItemRect.height); GUI.color new Color(1, 1, 1, 0.7f); GUI.Box(previewRect, draggedItemData.icon, EditorStyles.helpBox); GUI.color Color.white; // 也可以绘制一个提示文本 GUIStyle dragLabelStyle new GUIStyle(EditorStyles.whiteLabel); dragLabelStyle.alignment TextAnchor.MiddleCenter; GUI.Label(previewRect, draggedItemData.itemName, dragLabelStyle); } // ... 结束滚动视图等 ... } void HandleDragAndDropEvents() { Event evt Event.current; // 如果当前没有在拖拽直接返回 if (!isDragging) return; switch (evt.type) { case EventType.MouseDrag: if (GUIUtility.hotControl 0 evt.button 0) { // 鼠标移动超过一定阈值才真正开始拖拽视觉反馈避免误触 if (Vector2.Distance(dragStartPosition, evt.mousePosition) 5f) { // 可以在这里设置自定义光标但预览绘制已在Repaint事件中处理 evt.Use(); } } break; case EventType.MouseUp: case EventType.Ignore: // 有时拖拽到窗口外会触发Ignore if (GUIUtility.hotControl 0 evt.button 0) { // 拖拽结束执行放置操作 PerformDropOperation(); // 清理状态 ResetDragState(); GUIUtility.hotControl 0; evt.Use(); } break; case EventType.KeyDown: if (evt.keyCode KeyCode.Escape) { // 按ESC取消拖拽 ResetDragState(); GUIUtility.hotControl 0; evt.Use(); } break; } } void PerformDropOperation() { if (draggedItemIndex 0 || draggedItemIndex targetManager.itemList.Count) return; // 情况1拖拽到另一个格子上hoverOverIndex有效 if (hoverOverIndex 0 hoverOverIndex ! draggedItemIndex hoverOverIndex targetManager.itemList.Count) { // 交换两个格子的数据 GridItemData temp targetManager.itemList[draggedItemIndex]; targetManager.itemList[draggedItemIndex] targetManager.itemList[hoverOverIndex]; targetManager.itemList[hoverOverIndex] temp; // 标记目标对象为“脏”这样Unity才会保存修改 EditorUtility.SetDirty(targetManager); // 如果是MonoBehaviour可能需要 serializedObject.ApplyModifiedProperties(); } // 情况2拖拽到空白区域可以实现在列表末尾添加这里简单处理为无效操作 else { // 可以播放一个提示音或给出视觉反馈表示放置无效 Debug.Log(拖拽取消或放置位置无效。); } } void ResetDragState() { isDragging false; draggedItemIndex -1; draggedItemData null; hoverOverIndex -1; }4.4 进阶支持跨窗口拖拽与外部资源一个更专业的拖放列表可能还需要支持从项目视图Project Browser拖拽纹理或预制体到格子里或者将格子里的物品拖出到场景或其他编辑器窗口中。这涉及到Unity更通用的Drag and Drop API(UnityEditor.DragAndDrop)。实现外部拖入的简要思路在DrawGridCell或网格背景区域监听EventType.DragUpdated和EventType.DragPerform。在DragUpdated中检查DragAndDrop.objectReferences里是否有我们接受的类型如Texture2D。如果有设置DragAndDrop.visualMode DragAndDropVisualMode.Link/Copy;并提供视觉反馈。在DragPerform中接受拖拽根据拖入的数据创建或更新GridItemData并调用DragAndDrop.AcceptDrag()。实现拖出到外部的简要思路在开始拖拽时MouseDownMouseDrag除了设置自己的状态还可以初始化UnityEditor.DragAndDrop。设置DragAndDrop.PrepareStartDrag()并填充DragAndDrop.objectReferences例如可以放一个ScriptableObject资源引用和DragAndDrop.paths。调用DragAndDrop.StartDrag(“MyGridItemDrag”);启动一个系统级的拖拽操作。这样这个拖拽就可以被其他支持Unity拖拽的窗口如Project视图的文件夹识别。这部分代码更复杂但它将你的自定义控件与Unity的整个编辑器生态系统连接了起来极大地提升了工具的实用性。5. 性能优化与视觉增强一个基础的拖放格子列表已经完成了但要让它用起来舒服、专业还需要一些优化。5.1 避免不必要的重绘脏矩形与局部更新IMGUI默认每帧全量重绘。当格子数量成百上千时这可能成为性能瓶颈。虽然对于编辑器工具性能压力通常不大但良好的习惯是使用EditorGUIUtility.HasChanged()或者在OnInspectorGUI中使用serializedObject.Update()/ApplyModifiedProperties()来仅在数据变化时标记重绘。更高级的优化是手动计算需要重绘的区域脏矩形但这在IMGUI中实现较为复杂通常我们依赖Unity的优化。一个简单的技巧是将不需要频繁变化的UI部分如工具栏、设置面板与频繁变化的网格部分用GUILayout.BeginVertical/Horizontal或GUI.BeginGroup隔开。5.2 丰富的视觉反馈良好的视觉反馈是交互设计的核心悬停效果当鼠标悬停在可交互的格子上时改变其背景色或边框。拖拽预览如上所述绘制一个半透明、跟随鼠标的图标。预览的样式大小、透明度可以做得比原格子更醒目。放置目标高亮当拖拽物品经过一个有效的放置位置其他格子或空白区域时高亮该区域。可以用不同颜色的边框或背景色表示。无效放置提示当拖拽到无效区域时可以改变预览图标为“禁止”样式或播放一个轻微的抖动动画通过每帧微调绘制位置实现。// 示例在DrawGridCell中增强悬停效果 void DrawGridCell(Rect rect, GridItemData item, int index) { bool isHovered rect.Contains(Event.current.mousePosition) !isDragging; // 根据状态选择颜色 Color bgColor isHovered ? new Color(0.9f, 0.95f, 1.0f, 1.0f) : item.tintColor; if (index hoverOverIndex isDragging) { bgColor Color.cyan; // 放置目标高亮 } GUI.color bgColor; GUI.Box(rect, GUIContent.none, EditorStyles.helpBox); GUI.color Color.white; // ... 绘制图标和文字 ... }5.3 使用EditorStyles提升一致性Unity提供了一系列EditorStyles如EditorStyles.helpBox,EditorStyles.miniLabel,EditorStyles.objectField等来保证你的自定义编辑器控件与Unity原生界面风格一致。尽量使用这些样式而不是自己定义GUIStyle这样你的工具看起来会更“原生”、更专业。6. 常见问题排查与调试技巧即使按照步骤实现你也可能会遇到一些诡异的问题。这里记录几个我踩过的坑和解决方法。6.1 拖拽时控件“闪烁”或反应迟钝问题拖拽过程中预览图闪烁或者拖拽判定不跟手。原因这通常是因为事件处理逻辑有冲突或者Event.current.Use()调用不当导致一帧内多个逻辑都试图处理同一个事件或者事件没有被正确消耗。排查确保在MouseDown、MouseDrag、MouseUp等关键事件处理分支的最后都调用了evt.Use()。检查GUIUtility.hotControl的设置和清除是否成对出现。确保只在你的控件激活时设置它并在操作结束时MouseUp或Escape将其归零。在OnGUI方法开头添加Debug.Log(Event.current.type)观察事件流是否正常。正常情况下一次拖拽操作的事件流应该是MouseDown- 多次MouseDrag-MouseUp。如果出现奇怪的Layout或Repaint事件穿插可能是GUILayout布局计算引起的考虑切换到手动GUI矩形计算。6.2 数据交换后Inspector没有实时刷新问题拖拽交换了列表中的数据但Inspector视图中的数组显示没有立即更新。原因直接修改List的元素或顺序Unity的序列化系统可能不会自动触发视图更新。对于MonoBehaviour需要通知其对应的SerializedObject。解决如果是在自定义EditorWindow中操作一个独立的数据对象如ScriptableObject在修改数据后调用EditorUtility.SetDirty(targetManager)。如果是在CustomEditorOnInspectorGUI中操作你应该通过SerializedProperty来修改列表数据。修改后调用serializedObject.ApplyModifiedProperties()。这是更推荐的做法因为它能正确处理撤销/重做Undo/Redo功能。// 在CustomEditor中的正确做法示例 SerializedProperty itemListProperty; void OnEnable() { itemListProperty serializedObject.FindProperty(itemList); } void OnInspectorGUI() { serializedObject.Update(); // ... 绘制和交互逻辑 ... // 当需要交换数据时 if(needSwap) { itemListProperty.MoveArrayElement(draggedIndex, targetIndex); serializedObject.ApplyModifiedProperties(); // 应用修改并记录Undo } }6.3 拖拽操作无法撤销Undo问题拖拽排序后按CtrlZ无法撤销。原因直接操作数据对象没有通过Unity的撤销系统。解决在修改数据前使用Undo.RecordObject或Undo.RegisterCompleteObjectUndo。// 方法一记录单个对象的修改 Undo.RecordObject(targetManager, Reorder Grid Items); // ... 执行数据交换 ... EditorUtility.SetDirty(targetManager); // 方法二在EditorWindow中操作ScriptableObject时常用 Undo.RegisterCompleteObjectUndo(targetManager, Reorder Grid Items); // ... 执行数据交换 ...最佳实践如前所述在CustomEditor中使用SerializedProperty并调用ApplyModifiedProperties()Undo功能是自动处理的。6.4 格子绘制错位或重叠问题格子没有按预想的网格排列或者互相重叠。原因Rect计算错误或者GUILayout的自动布局与手动计算混用导致冲突。解决统一使用一种布局系统要么全部用GUILayout让Unity自动计算要么全部用GUI自己手动计算每个Rect。混合使用极易出错。仔细检查矩形计算如果手动计算确保考虑了间距(spacing)。每个格子的Rect的x坐标应该是startX j * (cellSize spacing)y坐标是startY i * (cellSize spacing)。使用GUI.BeginGroup如果网格在一个可滚动的区域内使用GUI.BeginGroup和GUI.EndGroup来定义一个局部坐标系可以简化滚动视图内的坐标计算。GUILayout.BeginScrollView内部其实就是用了BeginGroup。6.5 在Inspector中集成你的格子列表你可能希望这个格子列表不是在一个独立的窗口而是直接显示在某个组件的Inspector里。这需要创建一个CustomEditor。using UnityEditor; using UnityEngine; [CustomEditor(typeof(DragDropGridManager))] // 关联到你的管理器组件 public class DragDropGridManagerEditor : Editor { private SerializedProperty itemListProp; private SerializedProperty gridColumnsProp; // ... 其他属性 void OnEnable() { itemListProp serializedObject.FindProperty(itemList); gridColumnsProp serializedObject.FindProperty(gridColumns); // ... 查找其他属性 } public override void OnInspectorGUI() { serializedObject.Update(); // 将目标对象的数据拉取到SerializedProperty // 绘制其他标准属性 EditorGUILayout.PropertyField(gridColumnsProp); // ... // 留出空间绘制我们的自定义格子列表 EditorGUILayout.Space(10); EditorGUILayout.LabelField(物品格子列表, EditorStyles.boldLabel); // 这里可以调用一个方法传入 serializedObject 和 itemListProp 来绘制可拖放列表 // 注意在CustomEditor中处理拖拽需要将事件逻辑整合进来并且通过SerializedProperty来修改数据以保证Undo。 DrawDragDropGrid(serializedObject, itemListProp); serializedObject.ApplyModifiedProperties(); // 将修改应用回目标对象 } void DrawDragDropGrid(SerializedObject so, SerializedProperty listProp) { // 这里的实现逻辑与EditorWindow中类似 // 但所有对 listProp 的修改如交换元素都需要通过 SerializedProperty 的 API 进行 // 例如使用 listProp.MoveArrayElement(srcIndex, dstIndex); // 绘制部分则完全一样。 } }在Inspector中集成最大的变化是数据操作必须通过SerializedProperty这样才能保证Unity的撤销、预制体覆盖、多对象编辑等功能正常工作。绘制和交互逻辑本身可以复用大部分窗口中的代码。