尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity编辑器可视化表格配置工具开发实战:基于ISerializationCallbackReceiver

Unity编辑器可视化表格配置工具开发实战:基于ISerializationCallbackReceiver 1. 项目概述为什么我们需要一个可视化的二维表格配置工具在Unity项目开发中尤其是涉及数值策划、关卡设计或技能系统时我们常常需要处理大量的配置数据。这些数据通常以Excel、CSV或JSON文件的形式存在策划同学修改后程序需要手动或通过脚本将其导入到Unity中转换为ScriptableObject或序列化到Prefab里。这个过程存在几个明显的痛点首先工作流割裂策划和程序需要在不同工具间切换沟通成本高容易出错其次数据验证滞后策划在Excel里填错了数据类型或范围往往要到游戏运行时才能发现再者迭代效率低下每次微调一个数值都需要重新导出、导入、打包严重拖慢开发节奏。我接手过不少项目早期都吃过这个亏。策划改个怪物血量程序就得跑一遍导入流程一天下来光处理配置表就能耗掉小半天。后来我们尝试用ScriptableObject直接编辑虽然解决了运行时读取的问题但面对成百上千行、数十列的数据时Unity Inspector那单一的垂直列表视图简直就是灾难——查找、对比、批量修改都极其不便。于是打造一个内置于Unity编辑器、以可视化二维表格形式直接编辑配置数据的工具就成了一个非常自然且强烈的需求。这不仅仅是把Excel搬进Unity更是将数据配置、验证、序列化与游戏对象资产如ScriptableObject深度绑定实现真正的“所见即所得”和“即改即用”。本次实战我们将利用ISerializationCallbackReceiver接口这一核心机制结合EditorWindow和IMGUI/UIElements从头构建这样一个工具。你会发现它不仅能极大提升策划和程序的工作幸福感其设计思路也能复用到许多需要复杂数据编辑的场景中。2. 核心机制解析ISerializationCallbackReceiver的妙用在深入动手之前我们必须先理解本次实战的灵魂——ISerializationCallbackReceiver接口。Unity的序列化系统非常强大但有时也有点“黑盒”。这个接口为我们打开了两扇窗让我们能在序列化过程的关键节点插入自定义逻辑。2.1 序列化流程中的关键钩子ISerializationCallbackReceiver接口只包含两个方法public interface ISerializationCallbackReceiver { void OnBeforeSerialize(); void OnAfterDeserialize(); }OnBeforeSerialize(): 在Unity即将把对象序列化如保存场景、预制体、ScriptableObject之前调用。这是我们将内存中友好、易操作的数据结构如二维ListListT转换为Unity能够高效序列化的格式如一维数组、基础类型列表的最佳时机。OnAfterDeserialize(): 在Unity从序列化数据如加载场景、预制体、ScriptableObject之后调用。此时Unity已经用序列化数据填充了对象的字段。这是我们反向转换将一维的序列化数据重新组装成内存中便于使用的二维数据结构的关键时刻。你可以把它想象成一个数据格式的翻译官。在存档序列化时它把复杂的“本地语言”内存结构翻译成标准的“通用语”序列化格式在读档反序列化时它再把“通用语”翻译回“本地语言”。2.2 为何它是表格工具的核心对于二维表格数据我们理想的内存模型可能是一个ListRowData其中RowData包含多个字段。但Unity对泛型容器嵌套如ListListint或复杂自定义类构成的容器的序列化支持并不完美直接序列化这种结构可能导致数据丢失或性能问题。一个经典且稳定的模式是在数据类内部维护两个数据结构。编辑时结构例如public ListDataRow rows;方便我们在编辑器工具和游戏逻辑中直接使用。序列化结构例如[SerializeField] private string[] serializedData;用于实际存储。ISerializationCallbackReceiver的职责就是在OnBeforeSerialize中将rows压缩、编码如用JSON到serializedData中在OnAfterDeserialize中从serializedData解压、解码重新填充rows。注意这里选择string[]作为序列化载体只是一个例子。你也可以用byte[]、int[]等取决于数据的复杂度和对可读性的要求。用JSON如JsonUtility.ToJson编码的好处是在Inspector里也能勉强看懂serializedData的内容便于极端情况下的调试。2.3 一个简单的数据类示例让我们先定义一个最简单的表格数据类它继承自ScriptableObject并实现了ISerializationCallbackReceiver。using UnityEngine; using System; using System.Collections.Generic; [CreateAssetMenu(fileName NewTableData.asset, menuName Config/Table Data)] public class TableData : ScriptableObject, ISerializationCallbackReceiver { // 编辑时使用的数据结构一个行列表每行是一个数据条目 [System.Serializable] public class DataRow { public int id; public string name; public float health; public int attackPower; // ... 可以扩展更多字段 } public ListDataRow rows new ListDataRow(); // 用于实际序列化存储的“扁平化”数据 [SerializeField, HideInInspector] private string[] serializedRows; public void OnBeforeSerialize() { // 在保存前将rows列表转换为JSON字符串数组 serializedRows new string[rows.Count]; for (int i 0; i rows.Count; i) { serializedRows[i] JsonUtility.ToJson(rows[i]); } // 注意此时rows字段本身可能也会被序列化但可能是空的或旧的 // 不过因为我们主要使用serializedRows所以没关系。 // 更干净的做法是给rows加上 [NonSerialized] 特性完全依赖我们自定义的序列化。 } public void OnAfterDeserialize() { // 在加载后从JSON字符串数组重建rows列表 rows.Clear(); if (serializedRows ! null) { foreach (var json in serializedRows) { var row JsonUtility.FromJsonDataRow(json); if (row ! null) { rows.Add(row); } } } } // 一个辅助方法用于在编辑器中添加新行 public void AddNewRow() { rows.Add(new DataRow()); } }这个类已经具备了核心的序列化能力。在Unity编辑器中如果你创建了一个TableData资产直接修改rows列表保存项目时数据会通过我们的自定义逻辑被正确存储到serializedRows中。下次打开项目数据也能正确恢复。实操心得在实际项目中我强烈建议给rows字段加上[NonSerialized]特性即[NonSerialized] public ListDataRow rows;。这明确告知Unity不要尝试序列化这个列表完全由我们通过serializedRows来控制。这可以避免一些潜在的序列化冲突或数据不一致的诡异问题。同时记得在OnAfterDeserialize中初始化rows new ListDataRow();。3. 编辑器窗口与表格视图构建有了可序列化的数据容器下一步就是为它创建一个友好的编辑界面。我们将创建一个自定义的EditorWindow。3.1 创建基础编辑器窗口在Editor文件夹下创建脚本TableDataEditorWindow.cs。using UnityEditor; using UnityEngine; using System.Linq; public class TableDataEditorWindow : EditorWindow { private TableData currentTableData; private Vector2 scrollPosition; private string searchString ; [MenuItem(Tools/Config Table Editor)] public static void ShowWindow() { var window GetWindowTableDataEditorWindow(); window.titleContent new GUIContent(Config Table); window.Show(); } void OnGUI() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); { // 对象选择字段 currentTableData (TableData)EditorGUILayout.ObjectField(currentTableData, typeof(TableData), false); GUILayout.FlexibleSpace(); // 搜索框 searchString EditorGUILayout.TextField(searchString, EditorStyles.toolbarSearchField, GUILayout.Width(200)); } EditorGUILayout.EndHorizontal(); if (currentTableData null) { EditorGUILayout.HelpBox(Please assign a TableData asset., MessageType.Info); return; } // 开始绘制表格区域 DrawTable(); } private void DrawTable() { // 简单的表格绘制逻辑占位 EditorGUILayout.LabelField(Table view will be here., EditorStyles.centeredGreyLargeLabel); // 后续将在这里实现具体的表格绘制 } }这个窗口目前只有一个对象选择框和一个搜索框以及一个简单的提示。通过[MenuItem]我们可以在Unity顶部菜单的Tools下找到并打开它。3.2 实现核心表格绘制逻辑绘制一个可编辑的表格是核心难点。我们需要考虑表头、每一行的数据字段、滚动、以及编辑后的数据标记为脏需要保存。这里我们使用IMGUI来实现因为它对于这种动态、复杂的自定义UI有更直接的控制力。我们将大幅修改DrawTable方法并添加一些辅助的绘制方法。private void DrawTable() { if (currentTableData.rows null || currentTableData.rows.Count 0) { if (GUILayout.Button(Add New Row, GUILayout.Width(100))) { currentTableData.AddNewRow(); EditorUtility.SetDirty(currentTableData); // 标记资产已修改 } return; } // 定义列信息显示名、属性名、宽度比例 var columns new[] { new ColumnInfo(ID, id, 0.1f, (rect, row) row.id EditorGUI.IntField(rect, row.id)), new ColumnInfo(Name, name, 0.3f, (rect, row) row.name EditorGUI.TextField(rect, row.name)), new ColumnInfo(Health, health, 0.2f, (rect, row) row.health EditorGUI.FloatField(rect, row.health)), new ColumnInfo(Attack, attackPower, 0.2f, (rect, row) row.attackPower EditorGUI.IntField(rect, row.attackPower)), new ColumnInfo(Actions, , 0.2f, DrawActionButtons), }; float rowHeight EditorGUIUtility.singleLineHeight EditorGUIUtility.standardVerticalSpacing; float tableWidth position.width - 20; // 留出滚动条宽度 float scrollViewHeight Mathf.Min(currentTableData.rows.Count * rowHeight rowHeight, 600); // 表头内容最大高度600 // 1. 绘制表头 Rect headerRect GUILayoutUtility.GetRect(tableWidth, rowHeight); float xPos headerRect.x; for (int i 0; i columns.Length; i) { float colWidth tableWidth * columns[i].widthRatio; Rect colRect new Rect(xPos, headerRect.y, colWidth, rowHeight); GUI.Label(colRect, columns[i].displayName, EditorStyles.boldLabel); xPos colWidth; } // 2. 绘制表格内容可滚动区域 scrollPosition EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(scrollViewHeight)); { for (int rowIndex 0; rowIndex currentTableData.rows.Count; rowIndex) { var row currentTableData.rows[rowIndex]; // 根据搜索过滤行简单实现搜索所有字符串字段 if (!string.IsNullOrEmpty(searchString)) { string searchLower searchString.ToLower(); if (!row.name.ToLower().Contains(searchLower) !row.id.ToString().Contains(searchString)) // 注意数字直接转字符串比较 { continue; // 跳过不匹配的行 } } Rect rowRect GUILayoutUtility.GetRect(tableWidth, rowHeight); // 交替行背景色提高可读性 if (rowIndex % 2 0) EditorGUI.DrawRect(new Rect(rowRect.x, rowRect.y, tableWidth, rowHeight), new Color(0.3f, 0.3f, 0.3f, 0.1f)); xPos rowRect.x; for (int colIndex 0; colIndex columns.Length; colIndex) { float colWidth tableWidth * columns[colIndex].widthRatio; Rect cellRect new Rect(xPos, rowRect.y, colWidth, rowHeight); // 使用定义好的绘制委托来绘制单元格 EditorGUI.BeginChangeCheck(); columns[colIndex].drawCell(cellRect, row); if (EditorGUI.EndChangeCheck()) { // 如果单元格内容被修改标记整个资产为脏 EditorUtility.SetDirty(currentTableData); } xPos colWidth; } } } EditorGUILayout.EndScrollView(); // 3. 绘制底部操作按钮 EditorGUILayout.BeginHorizontal(); { GUILayout.FlexibleSpace(); if (GUILayout.Button(Add Row, GUILayout.Width(80))) { currentTableData.AddNewRow(); EditorUtility.SetDirty(currentTableData); } if (GUILayout.Button(Save Asset, GUILayout.Width(80))) { AssetDatabase.SaveAssetIfDirty(currentTableData); } } EditorGUILayout.EndHorizontal(); } // 辅助类描述一列的信息 private class ColumnInfo { public string displayName; public string propertyName; public float widthRatio; public System.ActionRect, TableData.DataRow drawCell; public ColumnInfo(string displayName, string propertyName, float widthRatio, System.ActionRect, TableData.DataRow drawCell) { this.displayName displayName; this.propertyName propertyName; this.widthRatio widthRatio; this.drawCell drawCell; } } // 绘制操作按钮列 private void DrawActionButtons(Rect rect, TableData.DataRow row) { // 注意这里需要获取row在列表中的索引但row是值类型class是引用但我们需要索引来删除。 // 因此这个简单示例中Action按钮的功能需要重新设计。一个常见做法是在绘制循环时传入rowIndex。 // 这里我们先留空下一节优化。 if (GUI.Button(new Rect(rect.x, rect.y, 40, EditorGUIUtility.singleLineHeight), Del)) { // 删除逻辑需要索引暂时不实现 } }这段代码构建了一个基本的表格视图框架。它定义了列信息绘制了表头和可滚动的数据行并实现了简单的搜索过滤。ColumnInfo类中的drawCell委托使得为不同类型字段定义不同的绘制逻辑变得非常灵活。注意事项上面的DrawActionButtons函数遇到了一个典型问题在委托中我们只知道当前行的数据对象row但不知道它在列表中的索引因此无法直接删除它。我们需要调整设计将行索引也传递进来。这通常意味着需要重构drawCell委托的定义或者采用不同的循环绘制策略。3.3 优化表格交互删除、排序与撤销支持为了解决上述问题并增强工具实用性我们进行如下优化修改ColumnInfo和绘制逻辑将行索引传递给绘制委托。集成撤销系统使用Undo.RecordObject记录数据变更支持CtrlZ。添加行排序功能通过拖拽或其他方式。首先修改ColumnInfo的drawCell委托增加行索引参数public System.ActionRect, TableData.DataRow, int drawCell; // 增加int rowIndex参数相应地在DrawTable的绘制循环中调用时传入rowIndexcolumns[colIndex].drawCell(cellRect, row, rowIndex);然后实现完整的操作按钮绘制和删除逻辑private void DrawActionButtons(Rect rect, TableData.DataRow row, int rowIndex) { // 使用一个水平布局的Rect来放置按钮 Rect buttonRect new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight); EditorGUI.BeginDisabledGroup(rowIndex 0); // 第一行不能上移 if (GUI.Button(new Rect(buttonRect.x, buttonRect.y, 20, buttonRect.height), ↑)) { Undo.RecordObject(currentTableData, Move Row Up); var list currentTableData.rows; var temp list[rowIndex]; list[rowIndex] list[rowIndex - 1]; list[rowIndex - 1] temp; EditorUtility.SetDirty(currentTableData); } EditorGUI.EndDisabledGroup(); buttonRect.x 22; EditorGUI.BeginDisabledGroup(rowIndex currentTableData.rows.Count - 1); // 最后一行不能下移 if (GUI.Button(new Rect(buttonRect.x, buttonRect.y, 20, buttonRect.height), ↓)) { Undo.RecordObject(currentTableData, Move Row Down); var list currentTableData.rows; var temp list[rowIndex]; list[rowIndex] list[rowIndex 1]; list[rowIndex 1] temp; EditorUtility.SetDirty(currentTableData); } EditorGUI.EndDisabledGroup(); buttonRect.x 22; if (GUI.Button(new Rect(buttonRect.x, buttonRect.y, 20, buttonRect.height), ×)) { if (EditorUtility.DisplayDialog(Delete Row, $Are you sure you want to delete row {rowIndex}?, Delete, Cancel)) { Undo.RecordObject(currentTableData, Delete Row); currentTableData.rows.RemoveAt(rowIndex); EditorUtility.SetDirty(currentTableData); } } }同时在DrawTable中绘制每个数据单元格时包裹Undo.RecordObjectEditorGUI.BeginChangeCheck(); Undo.RecordObject(currentTableData, $Modify Table Cell ({columns[colIndex].displayName})); columns[colIndex].drawCell(cellRect, row, rowIndex); if (EditorGUI.EndChangeCheck()) { EditorUtility.SetDirty(currentTableData); }实操心得集成Undo系统是专业编辑器工具的必要特性。Undo.RecordObject会在操作前为对象创建一个快照。务必注意对于ScriptableObject内的容器如List进行修改Undo也能正确记录这比我们手动处理撤销栈要可靠得多。另外EditorUtility.SetDirty是通知Unity该资产已被修改、需要保存的关键调用缺少它可能导致修改丢失。4. 高级功能与生产环境优化一个基础的表格编辑器已经成型但要投入生产环境还需要考虑更多。4.1 数据类型扩展与自定义绘制器我们的DataRow目前只有int,string,float。现实中会有enum,Vector3,引用其他Unity对象等需求。我们可以扩展ColumnInfo的绘制委托系统。例如增加一个枚举类型public class DataRow { // ... 其他字段 public EnemyType enemyType; } public enum EnemyType { Melee, Ranged, Mage, Boss }在定义columns时可以这样添加new ColumnInfo(Type, enemyType, 0.2f, (rect, row, idx) row.enemyType (EnemyType)EditorGUI.EnumPopup(rect, row.enemyType)),对于Unity对象引用如Sprite,GameObjectpublic class DataRow { public Sprite icon; } // 在columns定义中 new ColumnInfo(Icon, icon, 0.15f, (rect, row, idx) row.icon (Sprite)EditorGUI.ObjectField(rect, row.icon, typeof(Sprite), false)),更进阶的做法是利用C#的反射和PropertyDrawer概念根据DataRow类的字段定义自动生成列信息实现一个通用的表格编辑器。但这会显著增加复杂度对于特定项目明确定义列信息往往更可控。4.2 数据验证与错误提示在OnBeforeSerialize或表格编辑过程中加入验证逻辑至关重要。public void OnBeforeSerialize() { // 序列化前验证 Liststring errors new Liststring(); for (int i 0; i rows.Count; i) { var row rows[i]; if (row.health 0) errors.Add($Row {i}: Health cannot be negative.); if (string.IsNullOrEmpty(row.name)) errors.Add($Row {i}: Name is required.); // ... 更多验证规则 } if (errors.Count 0) { // 可以选择记录错误、抛出异常或者在编辑器窗口中显示 Debug.LogWarning($TableData {name} has validation errors:\n string.Join(\n, errors)); // 在实际工具中可以缓存这些错误信息在EditorWindow的OnGUI中绘制出来 } // ... 后续序列化逻辑 }在编辑器窗口中可以预留一个区域来显示这些验证错误用EditorGUILayout.HelpBox以错误信息样式呈现。4.3 性能考量处理超大型表格当表格有成千上万行时一次性绘制所有行会导致UI卡顿。解决方案是虚拟化只绘制可视区域内的行。计算可视范围根据scrollPosition.y和滚动视图高度计算出当前需要渲染的行索引范围startIndex到endIndex。仅绘制可见行在for循环中只绘制startIndex到endIndex之间的行。使用占位空间在可见行的前后使用GUILayout.Space或GUILayoutUtility.GetRect来撑开不可见区域的高度以保持正确的滚动条比例。这是一个简化的虚拟化绘制思路float visibleHeight scrollViewHeight; float startY scrollPosition.y; float endY startY visibleHeight; int startIndex Mathf.FloorToInt(startY / rowHeight); int endIndex Mathf.CeilToInt(endY / rowHeight); startIndex Mathf.Clamp(startIndex, 0, currentTableData.rows.Count - 1); endIndex Mathf.Clamp(endIndex, 0, currentTableData.rows.Count - 1); // 顶部占位 GUILayout.Space(startIndex * rowHeight); for (int rowIndex startIndex; rowIndex endIndex; rowIndex) { // 只绘制这个索引范围内的行 // ... 绘制单行逻辑 } // 底部占位 GUILayout.Space((currentTableData.rows.Count - endIndex - 1) * rowHeight);实现完整的虚拟化列表需要更精细的控制但能极大提升超大表格的编辑体验。4.4 导入与导出功能为了提高与外部工作流如Excel的兼容性可以添加导入/导出功能。导出为CSV遍历rows用逗号分隔每个字段的值写入到一个.csv文件。可以使用System.IO.File.WriteAllText。从CSV导入读取CSV文件按行和列解析字符串转换为对应的数据类型并赋值给新的DataRow对象然后替换或追加到rows列表中。需要注意数据类型转换的错误处理。与Excel交互更专业的做法是使用第三方库如EPPlus但注意许可证来读写真正的Excel文件支持多个工作表、单元格格式等。在编辑器窗口中添加两个按钮“Import CSV”和“Export CSV”并实现相应的文件选择对话框EditorUtility.OpenFilePanel/SaveFilePanel和数据处理逻辑即可。5. 常见问题与排查技巧实录在实际开发和团队使用这类工具时我踩过不少坑这里总结一下。5.1 数据丢失或损坏问题现象保存项目或重启Unity后表格数据部分或全部丢失或者rows列表为空。排查点1序列化/反序列化逻辑错误。这是最常见的原因。仔细检查OnBeforeSerialize和OnAfterDeserialize方法。确保serializedRows字段被正确标记为[SerializeField]。在OnAfterDeserialize中确保在填充rows前进行了初始化rows new ListDataRow();。排查点2ScriptableObject未正确保存。确保在修改数据后调用了EditorUtility.SetDirty(currentTableData)。如果通过代码创建了新的TableData资产记得调用AssetDatabase.CreateAsset和AssetDatabase.SaveAssets。排查点3Unity序列化限制。避免在DataRow中使用Unity无法直接序列化的类型如多维数组、复杂的泛型字典、非[Serializable]的类。如果必须使用需要将它们也通过ISerializationCallbackReceiver或自定义序列化来处理。避坑技巧在开发阶段可以临时在OnBeforeSerialize和OnAfterDeserialize中加入Debug.Log打印序列化前后的数据快照对比验证转换过程是否正确。也可以直接在Inspector中查看serializedRows数组的内容看其是否包含有效的JSON字符串。5.2 编辑器窗口卡顿或响应慢问题现象打开表格窗口或滚动、编辑时编辑器有明显的卡顿感。排查点1未做虚拟化列表。如果行数超过几百行务必实现前面提到的虚拟化列表只绘制可见行。排查点2单次GUI绘制过于复杂。检查OnGUI方法中是否有昂贵的计算或频繁的GUILayout分配。对于静态内容如表头考虑使用GUI而不是GUILayout因为后者每帧都会进行布局计算。可以将不变的计算结果缓存起来。排查点3数据验证或监听逻辑过于频繁。避免在OnGUI的每一行绘制循环中执行复杂的验证或搜索。可以将这些操作放在按钮触发时或使用EditorApplication.delayCall延迟执行。5.3 撤销Undo功能不工作问题现象编辑单元格后按CtrlZ无法撤销。排查点1未调用Undo.RecordObject。任何直接修改SerializedObject目标对象数据的操作都必须包裹在Undo.RecordObject调用中。确保在修改rows列表或其元素字段之前调用了它。排查点2修改的是局部变量。确保你修改的是实际存储在rows列表中的对象引用。例如var row currentTableData.rows[i]; row.value newValue;是有效的因为row是引用。但如果DataRow是结构体struct则需要直接修改列表中的元素这会更复杂这也是为什么通常将行数据定义为class的原因。排查点3操作被合并。Undo.RecordObject的第二个参数是操作名。连续对同一对象进行多次记录如果操作名相同Unity可能会将它们合并为一步撤销。如果需要每一步独立撤销可以传入不同的操作名如包含索引信息。5.4 与其他编辑器功能集成不佳问题现象资产不能被版本控制系统如Git正确识别改动或者Prefab覆盖等操作异常。排查点1确保EditorUtility.SetDirty被调用。这是通知Unity资产状态变化的关键。任何导致数据变化的操作添加行、删除行、修改单元格后都应调用它。排查点2处理Prefab实例的覆盖。如果你的TableData被用作Prefab的一部分在Prefab实例上编辑表格可能会触发Prefab覆盖操作。你需要理解Unity的Prefab实例化系统和SerializedObject的ApplyModifiedProperties机制。对于复杂的编辑器工具直接操作SerializedObject和SerializedProperty可能是更安全、与Unity编辑器集成更好的方式但这会带来更高的代码复杂度。5.5 扩展性维护难题问题场景项目初期只有5个字段后来策划要求加到50个字段每次增减字段都要手动修改DataRow类和ColumnInfo定义非常麻烦。解决方案采用数据驱动或代码生成。数据驱动定义一个ColumnDefinition的ScriptableObject里面描述每一列的名称、类型、默认值等。表格数据类TableData根据这个定义在运行时或OnAfterDeserialize时动态生成数据结构例如使用Dictionarystring, object存储每行数据。编辑器窗口也根据这个定义动态生成列。这种方式极其灵活但运行时访问数据的效率和类型安全会下降。代码生成编写一个编辑器脚本根据一个配置文件如JSON或Excel自动生成DataRow类和对应的ColumnInfo定义代码。策划修改配置表后程序员运行一下生成脚本即可更新。这平衡了灵活性和类型安全是许多大型项目的选择。我个人在中小型项目中更倾向于明确定义的方式虽然修改字段需要动代码但逻辑清晰、性能好、编译时就能发现错误。在大型、配置表结构频繁变动的项目中则会考虑代码生成方案。最后这个可视化表格配置工具的核心思路——利用ISerializationCallbackReceiver桥接内存模型与序列化存储并通过自定义EditorWindow提供友好的编辑界面——可以广泛应用于Unity编辑器的各种扩展场景。无论是管理技能效果、道具属性、本地化文本还是编辑关卡路径点只要是需要结构化数据批量编辑的地方这套模式都能派上用场。关键在于根据具体需求在易用性、性能和扩展性之间找到合适的平衡点。
返回列表