Unity编辑器扩展实战:基于IMGUI实现可拖拽排序的格子列表
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 Toolkit?
Unity提供了两套主要的编辑器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 List<GridItemData> itemList = new List<GridItemData>(); public int gridColumns = 4; // 网格列数 public float gridCellSize = 80f; // 格子大小 public float gridSpacing = 5f; // 格子间距 }关键点与避坑指南:
[System.Serializable]是关键:忘记给数据类加这个属性,会导致列表在Inspector中显示为空,或者数据无法被保存。- 使用
List<T>而非数组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() { GetWindow<DragDropGridEditorWindow>("格子列表编辑器"); } void OnGUI() { // 1. 获取或创建目标数据管理器 if (targetManager == null) { // 这里简单示例:创建一个临时的。实际项目中可能从Selection中获取。 targetManager = CreateInstance<DragDropGridManager>(); // 初始化一些测试数据 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); } }实操心得:
GUILayoutvsGUI:GUILayout系统自动布局,方便但控制精度稍弱。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()。
实现拖出到外部的简要思路:
- 在开始拖拽时(
MouseDown+MouseDrag),除了设置自己的状态,还可以初始化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)。 - 如果是在
CustomEditor(OnInspectorGUI)中操作,你应该通过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)
- 问题:拖拽排序后,按Ctrl+Z无法撤销。
- 原因:直接操作数据对象,没有通过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的撤销、预制体覆盖、多对象编辑等功能正常工作。绘制和交互逻辑本身可以复用大部分窗口中的代码。