做Unity项目做久了,你会发现一个特别真实的规律:真正耗时间的往往不是写玩法逻辑,而是处理那些看起来不起眼的配置数据。尤其是用ScriptableObject管理道具、技能、关卡、音效、任务这些内容时,数据量一上来,手动在Inspector里一个个填、一个个改,效率低到让人怀疑人生。我之前维护过一个包含两百多个武器配置的项目,每次策划提需求改数值,我都得在Inspector里翻半天,改完还要逐个检查引用关系,那种感觉真的是在浪费生命。
后来我花时间写了一套Unity编辑器脚本,把ScriptableObject的创建、查找、批量修改、数据校验全都自动化了。效果立竿见影,原来一下午的配置工作,现在几分钟搞定。这篇文章我就把这些脚本的核心思路、完整实现、踩过的坑全部整理出来,希望能给正在被ScriptableObject配置折磨的开发者一点参考。
1. 内容整体设计与思路拆解
1.1 为什么我建议用ScriptableObject管理游戏配置数据
先聊一个基础问题:为什么团队里越来越多人选择用ScriptableObject做配置,而不是JSON、XML或者Excel导出?我用下来的感受是,ScriptableObject最大的优势在于它和Unity引擎天生是一家人。
你可以直接在Inspector里可视化编辑,支持数据拖动赋值,能引用场景里的对象或者项目里的资源,还可以把数据做成资产文件,给不同场景、不同预制体共用。团队里非程序岗位的同事,比如策划、音频,他们不需要打开IDE或者第三方的表格工具,只要在Unity编辑器里就能完成配置修改,这对工作流的顺畅度提升是巨大的。
但它也有一个让人头疼的问题:编辑器环境下,它本质上是序列化到.asset文件里的对象。当你需要批量操作时,纯手工会卡在Inspector面板上,一个个点开,一个个改,像极了在Excel里数格子。而且ScriptableObject本身不会自动检查重复ID、不会校验必填字段、不会告诉你哪些配置被引用而哪些成了孤儿资产。这时候,编辑器脚本的价值就体现出来了。
1.2 编辑器脚本的几种自动化方案对比
做ScriptableObject自动化的思路,我梳理下来主要有三种,这里直接给大家做个对比:
| 方案 | 适用场景 | 实现成本 | 扩展性 |
|---|---|---|---|
| MenuItem上下文菜单 | 快速创建单个/批量资产、一键执行某种处理 | 低 | 一般 |
| 自定义EditorWindow窗口 | 批量导入、批量修改、多条件筛选、数据看板 | 中 | 高 |
| 自定义Inspector(PropertyDrawer/Editor) | 在资产自身面板上增加按钮、即时校验提示 | 中 | 高 |
实际项目里,这三者不是互斥的。我习惯的组合是:用MenuItem实现“右键直接创建”“选中批量执行”,用EditorWindow实现“集中的数据管理面板”,再在ScriptableObject的Inspector上挂一个自定义Editor,加几个快捷按钮和校验提示。三者配合,基本覆盖了所有日常操作场景。
1.3 方案选型背后的考量
为什么这种组合最实用?拿MenuItem来说,它是Unity提供的最轻量的编辑器扩展入口,写个静态方法加个特性就完事,特别适合“选中某个文件夹,点击创建一组数据资产”“选出所有资产,一键检查字段是否合法”这种低频但对执行时机没要求的操作。
EditorWindow则适合高频、需要反复交互的场景。比如我要把Excel里的道具表导入成一组ScriptableObject,或者要在几十个资产里批量修改某个字段,如果在MenuItem里写死逻辑,每次参数不同都重新编译,效率很低。做一个窗口,上面放输入框、下拉筛选、按钮,所见即所得,灵活很多。
至于自定义Inspector,更多是为了降低误操作率。我见过太多因为手滑把关键字段填错的案例,在Inspector上写个OnValidate校验,数值超出范围直接标红,甚至直接禁止保存,能在源头拦截掉大部分问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 从零搭建自动化脚本的基础框架
动手写脚本之前,先明确两个命名空间和一个核心API:
csharp复制using UnityEditor;
using UnityEngine;
所有编辑器脚本必须放在Editor文件夹下,这个文件夹可以被Unity特殊识别,不会被打进最终发布包。这一点太重要了,我见过有人把编辑器脚本放在普通文件夹里,结果打包的时候疯狂报错,就是因为编辑器相关代码被打进了运行时程序集。
还要熟悉AssetDatabase这个类,它是编辑器操作资产的核心入口。比如AssetDatabase.CreateAsset用来创建资产文件,AssetDatabase.LoadAssetAtPath用来加载资产,AssetDatabase.SaveAssets和AssetDatabase.Refresh用来保存和刷新。
下面这段代码是MenuItem创建ScriptableObject的基础写法:
csharp复制[MenuItem("Assets/Create/GameConfig/WeaponConfig", false, 0)]
public static void CreateWeaponConfig()
{
WeaponConfig config = ScriptableObject.CreateInstance<WeaponConfig>();
string path = "Assets/GameData/WeaponConfigs";
if (!AssetDatabase.IsValidFolder(path))
{
AssetDatabase.CreateFolder("Assets", "GameData");
AssetDatabase.CreateFolder("Assets/GameData", "WeaponConfigs");
}
AssetDatabase.CreateAsset(config, $"{path}/NewWeaponConfig.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Selection.activeObject = config;
}
注意MenuItem特性的第一个参数,Assets/Create/...这个路径会让按钮出现在Project面板的右键菜单里,非常方便。第三个参数是菜单项优先级,小数字排前面。创建完资产后,用Selection.activeObject = config直接把新资产选上,省去手动查找的步骤,这个小细节很提升体验。
2.2 批量创建ScriptableObject的正确姿势
单次创建太基础了,实际项目里更常用的是“批量创建”。比如新版本要加十把武器,你不可能手动执行十次创建,然后一个个重命名。正确做法是写一个批处理,参数通过带对话框的窗口让使用者输入。
我的做法是弹出一个简单的EditorWindow,输入武器名称列表(用逗号或换行分隔),然后点一下按钮,自动生成对应数量的资产文件:
csharp复制public class WeaponConfigBatchCreator : EditorWindow
{
private string weaponNames = "Iron_Sword\nSteel_Sword\nMagic_Staff";
private string savePath = "Assets/GameData/WeaponConfigs";
private int baseAttack = 10;
[MenuItem("Tools/GameConfig/Batch Create WeaponConfigs")]
private static void OpenWindow()
{
WeaponConfigBatchCreator window = GetWindow<WeaponConfigBatchCreator>();
window.titleContent = new GUIContent("批量创建武器配置");
window.Show();
}
private void OnGUI()
{
GUILayout.Label("武器名称(每行一个)", EditorStyles.boldLabel);
weaponNames = EditorGUILayout.TextArea(weaponNames, GUILayout.Height(100));
savePath = EditorGUILayout.TextField("保存路径", savePath);
baseAttack = EditorGUILayout.IntField("初始攻击力", baseAttack);
if (GUILayout.Button("批量创建"))
{
ExecuteBatchCreate();
}
}
private void ExecuteBatchCreate()
{
string[] names = weaponNames.Split('\n');
foreach (string name in names)
{
string trimmed = name.Trim();
if (string.IsNullOrEmpty(trimmed)) continue;
WeaponConfig config = ScriptableObject.CreateInstance<WeaponConfig>();
config.attack = baseAttack;
string fileName = $"{trimmed}.asset";
string fullPath = $"{savePath}/{fileName}";
fullPath = AssetDatabase.GenerateUniqueAssetPath(fullPath);
AssetDatabase.CreateAsset(config, fullPath);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"完成批量创建,共生成 {names.Length} 个资产");
}
}
核心细节在AssetDatabase.GenerateUniqueAssetPath这个方法上。如果你不调用它,生成的文件名重复时Unity会直接报错,创建过程中断。用它处理后,Unity会自动在文件名后面加数字后缀,比如Iron_Sword 1.asset,确保文件不冲突。
2.3 用AssetDatabase进行批量查找和修改
批量创建只是第一步。自动化真正的威力体现在“批量修改”上。
之前维护过一个项目,所有武器配置都有个qualityLevel字段,取值范围1到5。某天策划突然要做数值压缩,要求把所有5级品质降到4级,同时攻击力乘个0.8系数。手动改的话,几十个资产一个个来,光点开文件就够烦的。
用编辑器脚本就是几行代码的事:
csharp复制[MenuItem("Tools/GameConfig/Batch Adjust WeaponConfigs")]
private static void BatchAdjustWeaponConfigs()
{
// 找到所有WeaponConfig资产
string[] guids = AssetDatabase.FindAssets("t:WeaponConfig");
int count = 0;
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
WeaponConfig config = AssetDatabase.LoadAssetAtPath<WeaponConfig>(path);
if (config == null) continue;
// 数值调整逻辑
if (config.qualityLevel >= 5)
{
config.qualityLevel = 4;
}
config.attack = Mathf.RoundToInt(config.attack * 0.8f);
EditorUtility.SetDirty(config);
count++;
}
AssetDatabase.SaveAssets();
Debug.Log($"批量调整完成,共处理 {count} 个资产");
}
关键点是EditorUtility.SetDirty(config)。修改ScriptableObject的字段后,Unity不会自动标记资产为“需要保存”,如果不调用SetDirty,你看到内存里改好了,但关闭编辑器重新打开,数据又变回去了。这个坑我栽过好几次,大家一定要记住。
AssetDatabase.FindAssets("t:WeaponConfig")这行也很关键,它是按类型全局搜索资产的高效方式。如果只想搜索某个目录,可以加第二个参数,比如AssetDatabase.FindAssets("t:WeaponConfig", new[] { "Assets/GameData" }),这样能显著减少搜索范围,尤其项目资产多的时候,性能差距很明显。
3. 实操过程与核心环节实现
3.1 实战案例:做一个配置管理面板
聊了这么多细节,下面我完整展示一个实战项目,就是写一个“Weapon配置管理面板”。功能包含:
- 展示当前所有武器配置的核心字段
- 支持按品质等级筛选
- 一键修改选中资产的攻击力数值
- 一键检查并报告数据完整性问题
这个面板的代码结构相对大一些,但整体思路清晰。第一步是设计窗口布局,第二步是绘制资产列表和筛选区域,第三步是处理批量操作逻辑。
csharp复制public class WeaponConfigManagerWindow : EditorWindow
{
private Vector2 scrollPos;
private List<WeaponConfig> allConfigs = new List<WeaponConfig>();
private List<WeaponConfig> filteredConfigs = new List<WeaponConfig>();
private int qualityFilter = -1; // -1表示全部
private int newAttackValue = 99;
[MenuItem("Tools/GameConfig/Weapon Config Manager")]
private static void OpenWindow()
{
WeaponConfigManagerWindow window = GetWindow<WeaponConfigManagerWindow>();
window.titleContent = new GUIContent("武器配置管理面板");
window.minSize = new Vector2(600, 400);
window.Show();
}
private void OnEnable()
{
LoadConfigs();
}
private void LoadConfigs()
{
allConfigs.Clear();
string[] guids = AssetDatabase.FindAssets("t:WeaponConfig");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
WeaponConfig config = AssetDatabase.LoadAssetAtPath<WeaponConfig>(path);
if (config != null)
{
allConfigs.Add(config);
}
}
ApplyFilter();
}
private void ApplyFilter()
{
filteredConfigs = allConfigs.Where(c => qualityFilter == -1 || c.qualityLevel == qualityFilter).ToList();
}
}
这里OnEnable里加载数据有个好处:每次窗口打开时,自动拉取最新的资产列表。如果窗口一直开着,在外部新增了资产,可以手动加一个“刷新”按钮调用LoadConfigs。
接着绘制界面。我习惯用EditorGUILayout.BeginHorizontal/BeginVertical做布局,顶部放筛选下拉框和操作按钮,下方用EditorGUILayout.BeginScrollView包裹列表区域:
csharp复制private void OnGUI()
{
DrawToolbar();
DrawList();
}
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label("品质筛选:", GUILayout.Width(70));
int[] options = { -1, 1, 2, 3, 4, 5 };
string[] labels = { "全部", "1星", "2星", "3星", "4星", "5星" };
int selectedIndex = System.Array.IndexOf(options, qualityFilter);
if (selectedIndex < 0) selectedIndex = 0;
int newSelection = EditorGUILayout.Popup(selectedIndex, labels, GUILayout.Width(100));
qualityFilter = options[newSelection];
if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
LoadConfigs();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("批量修改选中项攻击力", EditorStyles.toolbarButton))
{
ExecuteBatchModifyAttack();
}
EditorGUILayout.EndHorizontal();
}
private void DrawList()
{
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("名称", EditorStyles.boldLabel, GUILayout.Width(200));
GUILayout.Label("品质", EditorStyles.boldLabel, GUILayout.Width(60));
GUILayout.Label("攻击力", EditorStyles.boldLabel, GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
foreach (WeaponConfig config in filteredConfigs)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Label(config.weaponName, GUILayout.Width(200));
GUILayout.Label($"{config.qualityLevel} 星", GUILayout.Width(60));
GUILayout.Label(config.attack.ToString(), GUILayout.Width(80));
if (GUILayout.Button("选择", GUILayout.Width(60)))
{
Selection.activeObject = config;
EditorGUIUtility.PingObject(config);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
EditorGUIUtility.PingObject是个很贴心的小功能,点击按钮后Project面板会高亮闪烁对应资产,方便确认当前操作的文件是哪个,这个在资产一多的时候特别有用。
批量修改攻击力的逻辑,写成一个独立方法,遍历所有选中项(这里用Selection.objects):
csharp复制private void ExecuteBatchModifyAttack()
{
if (Selection.objects == null || Selection.objects.Length == 0)
{
EditorUtility.DisplayDialog("提示", "请先在列表或Project面板中选择要修改的资产。", "确定");
return;
}
int modifiedCount = 0;
foreach (Object obj in Selection.objects)
{
if (obj is WeaponConfig config)
{
Undo.RecordObject(config, "Batch Modify Attack");
config.attack = newAttackValue;
EditorUtility.SetDirty(config);
modifiedCount++;
}
}
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("完成", $"已修改 {modifiedCount} 个资产的攻击力。", "确定");
}
这里注意我加了一个Undo.RecordObject调用。这个方法的含义是:在修改对象之前,先记录下来当前状态,这样用户可以通过Ctrl+Z撤销这次批量操作。在编辑器工具里写撤销支持,是个很容易被忽略但很重要的习惯。没有这行的话,自动化脚本改完数据就彻底覆盖了,误操作了也只能手动改回来,体验非常糟糕。
3.2 数据校验与自动修复
管理面板除了能改数据,我还加了一键校验功能。ScriptableObject资产多起来以后,必填字段为空、数值越界、引用丢失这些问题非常普遍。校验逻辑也不复杂,就是遍历所有资产,检查关键字段:
csharp复制private void ExecuteDataValidation()
{
List<string> issues = new List<string>();
foreach (WeaponConfig config in allConfigs)
{
if (string.IsNullOrEmpty(config.weaponName))
{
issues.Add($"{AssetDatabase.GetAssetPath(config)}:武器名称为空");
}
if (config.attack <= 0)
{
issues.Add($"{AssetDatabase.GetAssetPath(config)}:攻击力必须大于0,当前为 {config.attack}");
}
if (config.icon == null)
{
issues.Add($"{AssetDatabase.GetAssetPath(config)}:缺少图标引用");
}
// 检查重复ID
string id = config.configId;
if (allConfigs.Count(c => c.configId == id) > 1)
{
issues.Add($"配置ID重复:{id}(文件:{AssetDatabase.GetAssetPath(config)})");
}
}
if (issues.Count == 0)
{
EditorUtility.DisplayDialog("校验结果", "所有配置数据均正常,未发现任何问题。", "确定");
}
else
{
string detail = string.Join("\n", issues.Take(20));
if (issues.Count > 20)
{
detail += $"\n... 还有 {issues.Count - 20} 个问题未显示";
}
EditorUtility.DisplayDialog("校验结果", $"发现 {issues.Count} 个潜在问题:\n\n{detail}", "确定");
}
}
校验和修复要区分开,不要一上来就自动修复。为什么?因为很多问题需要策划或程序人工判断,比如图标缺失,自动处理只能清空引用或者挂一个默认图,这不一定符合需求。脚本只做“发现问题并报告”,把决定权留给人,这个边界很重要。
3.3 用自定义Inspector提升单资产操作体验
批量工具解决了规模化的问题,但日常单资产编辑时,Inspector面板的体验同样值得优化。我一般在ScriptableObject上挂一个自定义Editor,加几个一键填充或一键整理的按钮。
比如WeaponConfig上挂一个自定义Inspector:
csharp复制[CustomEditor(typeof(WeaponConfig))]
public class WeaponConfigEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
WeaponConfig config = (WeaponConfig)target;
EditorGUILayout.Space();
EditorGUILayout.LabelField("快捷操作", EditorStyles.boldLabel);
if (GUILayout.Button("根据名称自动生成配置ID"))
{
Undo.RecordObject(config, "Generate Config ID");
config.configId = "Weapon_" + config.weaponName.Replace(" ", "_").ToUpper();
EditorUtility.SetDirty(config);
}
if (GUILayout.Button("攻击力随机化(用于测试)"))
{
Undo.RecordObject(config, "Randomize Attack");
config.attack = Random.Range(10, 100);
EditorUtility.SetDirty(config);
}
}
}
这个脚本放到Editor文件夹之后,所有WeaponConfig资产的Inspector面板下方都会出现“快捷操作”区域。这里的target就是当前正在查看的那个资产实例。注意这里也要Undo.RecordObject和EditorUtility.SetDirty,原因前面已经说过了。
3.4 结合Excel:从外部表格批量导入配置
最后再分享一个进阶方向:把Excel里的配置表批量导入到ScriptableObject里。
这个需求几乎每个中大型项目都有。我尝试过几种方案,初期是让策划把Excel导出成CSV,然后写个脚本解析CSV。后来发现CSV对多语言、带逗号的文本支持比较麻烦,就换成了读取.xlsx文件。用第三方库EPPlus或ClosedXML都可以,在编辑器脚本里引用开源的Excel读取库完全合法,而且只作用于编辑器环境,不影响运行时包体。
核心流程是:
- 读取Excel文件,把每一行当作一条配置
- 检查配置ID是否已存在,存在则更新,不存在则新建
- 根据单元格内容给ScriptableObject字段赋值
- 保存并刷新
读取Excel的代码这里就不展开了,不同库的API差异比较大。但我想强调一个设计原则:Excel导入脚本里一定要有“预览确认”的步骤。我见过不少人导入时报错,才发现Excel表头列顺序和代码里字段映射对不上,导致整批次数据错乱。比较好的做法是,导入前先解析第一行表头,以“列名-字段”映射的方式配置对应关系,并弹窗展示前三行数据预览,确认没问题后再真正导入。这是用血泪教训换来的经验。
4. 常见问题与排查技巧实录
4.1 修改了数据但重新打开后恢复原样
这个问题非常经典,几乎每个编辑器脚本新手都会遇到。原因就是我前面提到的:修改ScriptableObject字段后,没有调用EditorUtility.SetDirty(config)。
还有个连带的坑是:调用了SetDirty但忘记调用AssetDatabase.SaveAssets()。SetDirty只是标记了资产为脏状态,真正写入磁盘的操作是SaveAssets。如果你只调用了SetDirty,Switch到别的窗口再回来,Unity可能会让你选择是否保存,但如果你在编辑器脚本里结束后直接关闭编辑器,不保存的话修改就丢了。
所以请记住这个固定套路:
csharp复制EditorUtility.SetDirty(config); // 1. 标记为需要保存
AssetDatabase.SaveAssets(); // 2. 立即写入磁盘
不过也别矫枉过正。如果批量修改了几十个资产,建议中途不要多次调用SaveAssets,统一在最后调用一次就够了,频繁IO操作反而拖慢编辑器速度。
4.2 编辑器脚本报“无法在运行时修改”错误
Unity编辑器脚本运行时的上下文和游戏运行时是两套。如果你在编辑器模式下加载了ScriptableObject准备修改,但此时Unity正处于Play Mode的运行时状态,ScriptableObject实例可能已经变成了运行时实例,修改会导致报错或者数据不同步。
解决办法:在修改前检查EditorApplication.isPlaying,如果正在播放,就跳过或者用AssetDatabase.LoadAssetAtPath重新加载一份编辑器实例。简单粗暴点就是弹窗提示“请先退出Play Mode再执行批量操作”。
csharp复制if (EditorApplication.isPlaying)
{
EditorUtility.DisplayDialog("提示", "请先退出Play Mode再执行批量修改。", "确定");
return;
}
4.3 批量查找时脚本卡死
AssetDatabase.FindAssets在大项目里如果搜索范围是全项目,首次执行可能会卡一两秒,这是正常的。但如果你在OnGUI里每次鼠标移动都执行一遍查找,那编辑器肯定会卡成PPT。
解决办法是:用缓存机制,只在OnEnable或点击“刷新”按钮时加载数据,不要在OnGUI里直接执行FindAssets。如果资产数量非常庞大,可以用异步方式加载,但大多数项目用不到,缓存就足够解决了。
4.4 创建出的资产文件名带空格或非法字符
Windows和Unity对文件名都有字符限制,包含/\:*?"<>|这些字符时,CreateAsset会直接抛异常。处理方式是在创建前做一次文件名清理:
csharp复制private static string SanitizeFileName(string name)
{
char[] invalidChars = System.IO.Path.GetInvalidFileNameChars();
string cleaned = new string(name.Where(c => !invalidChars.Contains(c)).ToArray());
return string.IsNullOrEmpty(cleaned) ? "UnnamedConfig" : cleaned;
}
这个坑我印象特别深。有次策划在Excel里用了带“/”的武器名称,结果批量导入脚本中途崩溃,已经创建了一半的资产停在那里,名字乱七八糟,还得手动清理,现在写工具都会默认加一层文件名清洗。
4.5 常见问题速查表
为了方便大家排查问题,我把高频问题整理成一张表:
| 现象 | 根本原因 | 解决思路 |
|---|---|---|
| 修改数据重启后丢失 | 未调用SetDirty/SaveAssets | 修改后补上两次调用 |
| 脚本报对象被销毁 | 在Play Mode下操作编辑器资产 | 操作前判断EditorApplication.isPlaying |
| 创建资产时报文件名冲突 | 未做重名检查 | 使用GenerateUniqueAssetPath |
| 菜单找不到 | 脚本未放在Editor文件夹 | 将脚本移动到Editor目录 |
| 导入Excel中文乱码 | 编码格式不匹配 | 统一用UTF-8,或使用EPPlus/ClosedXML读取xlsx |
| 批量操作卡顿 | OnGUI里频繁FindAssets | 数据缓存,手动刷新 |
| 找不到自定义窗口 | 菜单路径或类名拼写错误 | 检查MenuItem路径,确认类名不冲突 |
4.6 一个容易忽略的细节:菜单项的优先级和分组
MenuItem特性的第三个参数是优先级,数字越小越靠前,而且可以通过相同优先级的数字来起到分组效果。例如:
csharp复制[MenuItem("Assets/Create/GameConfig/WeaponConfig", false, 0)]
[MenuItem("Assets/Create/GameConfig/ArmorConfig", false, 1)]
[MenuItem("Assets/Create/GameConfig/ItemConfig", false, 2)]
Unity会自动在1和2之间插入一条分隔线。初次探索这个机制时我没留意,导致右键菜单全挤在一起,差点找不到想要的选项。现在项目里我统一用这种优先级规划:0-10是创建类菜单,11-20是处理类菜单,21-30是工具类菜单,分工明确。
5. 一些我踩过的坑和心法总结
整个过程走下来,我最大的感受是:编辑器脚本不是什么高深的技术,它就是给自己写的“外挂”,核心价值在于把重复劳动压缩到极致。一开始写第一版批量创建工具时,只花了一个多小时,但长期下来节约的时间是翻倍的。
实操过程中,我还总结出几条心法,分享给大家:
第一,任何批量操作都要支持撤销。这句话我说再多次都不为过。Undo.RecordObject只有一行代码,但它的价值在误操作时是巨大的。
第二,工具要有可见性反馈。批量操作完成后,一定要通过Debug.Log或者EditorUtility.DisplayDialog告诉用户执行了多少条、成功了多少条、失败了哪些。我见过很多脚本执行完静悄悄的,出了问题也不知道是没执行还是执行了但没成功。
第三,尽量把工具做成窗口,而不是一堆菜单项散落各处。窗口可以承载筛选、预览、批量操作、输出结果,使用体验远好于一个孤零零的菜单按钮。
第四,写工具的时候一定要考虑非程序员用户。策划和美术不一定懂报错信息,在工具里用中文写清楚提示,远比让用户看一堆英文异常栈友好得多。
最后再分享一个小技巧:如果你的项目有多个人协作,可以在MenuItem的按钮旁边加键盘快捷键。比如%代表Ctrl(Mac上是Command),#代表Shift,&代表Alt,写法是[MenuItem("Tools/GameConfig/Batch Create Configs %#c")],这样按下Ctrl+Shift+C就能直接调出工具,省得每次都在菜单栏里翻。
编辑器脚本这个方向,其实还有很大的挖掘空间。比如配合AssetPostprocessor实现资源导入自动处理,或者用ScriptableObject做数据驱动的关卡编辑器。自动化操作ScriptableObject只是其中一块拼图,但它带来的效率提升,绝对值得你花一个下午去搭建起来。如果你也被重复的配置工作折磨得头痛,趁早试试这套方案,说不定会有惊喜。
