ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity编辑器脚本批量操作ScriptableObject:从手动配置到一键自动化

Unity编辑器脚本批量操作ScriptableObject:从手动配置到一键自动化 在Unity项目里ScriptableObject大概是除了Prefab和Scene之外最常用来存配置的东西了。但相信很多人都有过这种体验需求一改几十个配置资产要一个一个点开改策划给了一张表你要手动在Inspector里把几十条数据敲进去又或者是数值体系调整要在整个项目里批量替换字段。这种操作重复、低效还特别容易漏改、错改。我最早被这个问题卡住是在做一套卡牌游戏的数值配置时三百多个SkillData资产需要按新公式重算并统一更新。手动去改折腾了两天还没改完后来花了半天写了个编辑器脚本把这件事变成了“一键操作”。今天就把这套思路和具体实现完整拆开讲一讲。这里涉及的核心就是Unity编辑器脚本和ScriptableObject的自动化操作适合所有觉得Inspector点来点去效率太低、想用代码管资产的开发者参考。1. 整体设计与思路拆解1.1 自动化脚本到底要解决什么问题先说清楚编辑器脚本不是用来替代策划、程序手填配置的它解决的是“批量、规律、可重复”的操作。比如新建一批资产字段取值有固定规则比如从表格转换成配置比如全局替换某个枚举值再比如把A资产的引用统一挂到B资产上。这些事如果靠手点不仅慢而且人眼很容易漏掉一个字段、点错一个下拉框。脚本的好处是同样的逻辑执行一百次和一万次结果完全一致跑完还能自动打印日志告诉你哪些资产被改过、哪些字段异常。ScriptableObject之所以适合被自动化操作是因为它本质上就是“序列化在磁盘上的一个对象实例”。它和Prefab、Scene一样Unity提供了完整的编辑器API来读取和修改它。只要你理解了这些API就能在脚本里对任意数量的资产做增删改查甚至比手写JSON配置还顺手。1.2 为什么选择Editor脚本而不是运行时脚本很多人会问我能不能在游戏运行时用代码改ScriptableObject然后保存下来答案是运行时可以改但不能直接持久化到工程的资产文件resources、addressables之外。因为运行时的Application.persistenceDataPath和工程里的Assets目录是两个世界。运行时保存需要自己写序列化逻辑比如转成JSON写到本地麻烦且不稳定。而Editor脚本运行在Unity编辑器的进程里可以直接调用AssetDatabase、SerializedObject、EditorUtility等编辑器专属API这些API设计的初衷就是让你安全操作项目资产。换句话说Editor脚本是“开发期工具”运行时脚本是“游戏玩法”。我们要做的自动化操作本质上属于开发期工具所以应该用Editor脚本实现。它在Unity里被放在Assets/Editor文件夹下或者任意文件夹下的Editor子目录里编译时会自动进入Editor程序集不会被打进游戏包。1.3 一个通用自动化工具的核心结构无论自动化脚本要做多少件事核心结构基本固定先找到目标资产然后加载或创建对象接着修改数据最后保存并刷新。用代码来概括就是这个流程定位用AssetDatabase.FindAssets或者AssetDatabase.LoadAllAssetsAtPath把目标ScriptableObject的路径或GUID找出来。加载用AssetDatabase.LoadAssetAtPath ()拿到具体的对象实例。修改通过SerializedObject和SerializedProperty来改字段或者直接改对象属性后再调用EditorUtility.SetDirty标记。保存用AssetDatabase.SaveAssets()把内存里的修改写回磁盘必要时配合AssetDatabase.Refresh()刷新资源数据库。我建议把每一步都封装成独立函数因为“找、改、存”这三个动作在不同的自动化需求里都会重复用到。后面我会给出一个可以直接抄的封装类。2. 核心细节解析与实操要点2.1 ScriptableObject的序列化机制为什么不能只new一个对象要自动化操作ScriptableObject必须先理解它是怎么被序列化的。当你写了一个继承ScriptableObject的类并通过CreateAssetMenu在Assets里创建一个资产时Unity会把该对象的公共字段、[SerializeField]私有字段、甚至一些属性都序列化到.asset文件里。这份文件本质是YAML文本除非你开了Binary Serialization模式里面记录了对象的数据和内部引用。一个很重要的点是ScriptableObject并不是“场景里的对象”而是“资产”。所以我们不能用new来创建它应该用ScriptableObject.CreateInstance ()来创建内存实例再用AssetDatabase.CreateAsset把它落到磁盘上。直接用newUnity不会把它当作可序列化资产规范地处理后面保存和撤销都可能出问题。理解了这一点你就知道为什么有人直接写target.damage 100然后发现没保存或者保存后撤销无效——因为他们没有走序列化系统的流程尤其是没有标记“脏”。2.2 编辑器脚本里操作资产的正规姿势SerializedObject与SerializedProperty在Inspector上Unity编辑框之所以能显示并修改ScriptableObject的属性底层就是通过SerializedObject包装了对象再通过SerializedProperty逐字段访问。我们写自动化脚本时最好也沿用这套方式而不是直接改对象字段原因有三个直接改字段时Unity不会自动识别这次修改需要手动SetDirty而且无法产生正确的Undo记录。用SerializedObject的ApplyModifiedProperties会自动把改动同步回对象并标记脏。SerializedProperty能帮你处理数组、嵌套类型、枚举、引用等复杂结构直接用对象字段反而容易写错类型转换。用SerializedObject时你可以通过property.serializedObject.targetObject拿到底层对象在循环里统一处理很舒服。举个例子修改一个名为damage的float字段SerializedObject so new SerializedObject(skillData); SerializedProperty damage so.FindProperty(damage); damage.floatValue 100; so.ApplyModifiedProperties();这段代码看起来比直接写skillData.damage 100多但它保证了修改能进入Unity的撤销栈也能让Inspector刷新。对于那些“改完发现没保存”的诡异问题多半就是绕开了这套流程。2.3 标记脏、保存、撤销自动化操作的三姐妹在Editor脚本里三个API最容易搞混EditorUtility.SetDirty(target)把目标对象标记为“已修改”让Unity在保存场景或退出时提示保存也会让资源数据库认为它需要被写回。AssetDatabase.SaveAssets()把所有标记脏的资源真正写入磁盘文件。Undo.RecordObject(target, operation)记录对象修改前的状态之后按CtrlZ可以撤销到操作前。这三个API不是必须同时出现但要根据场景选对。比如你用AssetDatabase.CreateAsset创建了新资产新资产已经自然进入资源数据库不需要SetDirty但创建后需要SaveAssets来确保文件落地。再比如你用SerializedObject修改已有资产ApplyModifiedProperties内部已经调用了SetDirty你还是要SaveAssets才能落盘同时如果想支持撤销最好在修改前调用Undo.RecordObject或者在创建对象后调用Undo.RegisterCreatedObjectUndo。我踩过一个坑在批量循环里每改一个字段就SaveAssets一次导致几百个资产操作卡得要死。正确做法是先批量修改记录Undo全部改完只调一次AssetDatabase.SaveAssets配合AssetDatabase.Refresh()刷新。具体的优化后面会细讲。2.4 批量操作中的资产导入与刷新控制当你批量生成或修改大量.asset文件时Unity资源数据库会不停地扫描文件变化尤其是你用了AssetDatabase.CreateAsset或SaveAssets之后可能会触发导入器重新导入资源。这个导入过程会阻塞主线程一百个资产还好跑到一千个以上文件系统监听和导入管线很容易把编辑器卡到看起来像死机。所以Unity提供了两个方法AssetDatabase.StartAssetEditing()和AssetDatabase.StopAssetEditing()。在Start和Stop之间对资产进行批量增删改会让Unity暂停自动导入直到StopAssetEditing时才一次性刷新导入。这样能大幅缩短时间也能避免“改到一半被导入打断了引用”的问题。一个标准的批量操作外壳长这样AssetDatabase.StartAssetEditing(); try { // 批量创建或修改资产 } finally { AssetDatabase.StopAssetEditing(); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh();不要在try里漏了StopAssetEditing否则编辑器会一直处于暂停导出的状态后续所有资源操作都变得诡异。我一般写完之后还会加一个EditorUtility.DisplayProgressBar来做进度条让长时间批量操作有反馈。3. 实操过程与核心环节实现3.1 场景一批量创建并填充ScriptableObject资产我们先从最简单的场景说起需要一次性创建50个ItemData配置资产每个资产包含id、名称、数值等字段。直接手点CreateAssetMenu再填数据效率极低。下面脚本会在Assets/Items下按编号生成资产并自动填好默认值。假设资产类是[CreateAssetMenu(fileName ItemData, menuName Game/ItemData)] public class ItemData : ScriptableObject { public int id; public string itemName; public float weight; public int maxStack; }批量创建的核心代码如下using System.IO; using UnityEditor; using UnityEngine; public static class ItemDataBatchCreator { [MenuItem(Tools/Item Data/Batch Create 50 Items)] public static void CreateItems() { string folderPath Assets/Items; if (!Directory.Exists(folderPath)) { Directory.CreateDirectory(folderPath); AssetDatabase.Refresh(); } AssetDatabase.StartAssetEditing(); try { for (int i 1; i 50; i) { ItemData item ScriptableObject.CreateInstanceItemData(); item.id i; item.itemName $Item_{i:000}; item.weight Random.Range(1f, 10f); item.maxStack 99; string path ${folderPath}/Item_{i:000}.asset; AssetDatabase.CreateAsset(item, path); } } finally { AssetDatabase.StopAssetEditing(); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log(已完成50个ItemData资产的批量创建); } }注意几个细节创建前要确保目标目录存在否则CreateAsset会报“Directory not found”。StartAssetEditing和StopAssetEditing包住循环避免50次自动导入。CreateAsset本身就会让新资产进入数据库所以不需要SetDirty。foreach循环里用$“Item_{i:000}”这样的格式化字符串避免数字排序变成1、10、2这种乱序。如果你想在生成后顺便做一些计算比如把weight按id缩放可以在CreateAsset之前直接改字段然后用AssetDatabase.SaveAssets统一保存。这种纯新建场景比较直接。3.2 场景二批量修改已有资产中的数值或引用重点来了。我们经常需要对已有的一批资产做规律性修改比如“所有武器类SkillData的伤害提升15%”。如果只靠手改容易漏。脚本的做法是用AssetDatabase.FindAssets(t:SkillData)找到所有SkillData资产路径逐个加载再通过SerializedObject修改字段。下面是一段可以直接改成自己字段名的示例using UnityEditor; using UnityEngine; public static class SkillDataUpdater { [MenuItem(Tools/Skill Data/Batch Increase Damage 15%)] public static void IncreaseAllDamage() { string[] guids AssetDatabase.FindAssets(t:SkillData); if (guids.Length 0) { Debug.LogWarning(当前项目没有找到任何SkillData资产); return; } AssetDatabase.StartAssetEditing(); try { foreach (string guid in guids) { string path AssetDatabase.GUIDToAssetPath(guid); SkillData skill AssetDatabase.LoadAssetAtPathSkillData(path); if (skill null) continue; Undo.RecordObject(skill, Increase Damage 15%); SerializedObject so new SerializedObject(skill); SerializedProperty damage so.FindProperty(damage); if (damage ! null) { damage.floatValue * 1.15f; so.ApplyModifiedProperties(); } else { Debug.LogWarning(${path} 里没有找到damage字段跳过); } } } finally { AssetDatabase.StopAssetEditing(); } AssetDatabase.SaveAssets(); Debug.Log($已批量调整 {guids.Length} 个SkillData的damage); } }这里的关键点是Undo.RecordObject要放在SerializedObject修改之前而且要放在对象加载之后。这样每个资产都会在撤销栈中留下记录。如果你不想太支持撤销也可以省略但我的习惯是加上毕竟误操作了还能一键回滚。FindAssets的查找规则是t:SkillData它会按类型全项目搜索包括AssetBundle等目录。如果你只想搜某个子目录可以把路径传进FindAssets的第二个参数比如AssetDatabase.FindAssets(t:SkillData, new[] { Assets/SkillData })。3.3 场景三从外部数据CSV导入生成配置资产自动化操作ScriptableObject最有价值的场景就是对接策划配置表。策划通常习惯用Excel或CSV维护数值导成CSV后我们写个脚本把CSV的每一行变成对应的ScriptableObject资产省去手动录入还能顺便做一层校验。假设CSV内容格式是id,name,damage,armor,description 1,火球术,100,0,范围型火属性伤害 2,冰霜新星,80,5,范围型冰属性控制 3,暴风雪,120,10,持续AoE减速对应的SkillData类[CreateAssetMenu(fileName SkillData, menuName Game/SkillData)] public class SkillData : ScriptableObject { public int id; public string skillName; public float damage; public int armor; [TextArea] public string description; }导入脚本的核心函数using System.IO; using UnityEditor; using UnityEngine; public static class SkillCsvImporter { [MenuItem(Tools/Skill Data/Import From CSV)] public static void ImportFromCsv() { string csvPath EditorUtility.OpenFilePanel(选择技能CSV, Application.dataPath, csv); if (string.IsNullOrEmpty(csvPath)) return; string[] lines File.ReadAllLines(csvPath); if (lines.Length 2) { Debug.LogError(CSV内容为空或只有表头); return; } string outputFolder Assets/Skills; if (!Directory.Exists(outputFolder)) { Directory.CreateDirectory(outputFolder); AssetDatabase.Refresh(); } AssetDatabase.StartAssetEditing(); try { for (int i 1; i lines.Length; i) { string line lines[i]; if (string.IsNullOrWhiteSpace(line)) continue; string[] cols CSVLineSplit(line); if (cols.Length 5) { Debug.LogWarning($第{i 1}行列数不足已跳过: {line}); continue; } SkillData skill ScriptableObject.CreateInstanceSkillData(); skill.id int.Parse(cols[0].Trim()); skill.skillName cols[1].Trim(); skill.damage float.Parse(cols[2].Trim()); skill.armor int.Parse(cols[3].Trim()); skill.description cols[4].Trim(); string path ${outputFolder}/{skill.skillName}.asset; AssetDatabase.CreateAsset(skill, path); } } finally { AssetDatabase.StopAssetEditing(); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log(CSV导入完成); } private static string[] CSVLineSplit(string line) { // 做简单分割这里不处理引号内逗号的情况足够应付绝大多数简单配置表 return line.Split(,); } }CSV解析这里我故意简化了只按逗号分割。如果策划导出的CSV里带双引号包裹的字段比如description里含英文逗号简单Split会出错。实际项目里最好用现成解析库或者自己处理引号匹配。不过对绝大多数内部配置表这种简单切割够用。还有一个注意事项如果CSV里同一行对应的资产已经存在直接CreateAsset会报错。所以“导入前先删除旧资产”和“更新已有资产”是两种常见策略。我建议在导入脚本开头加一个选项是否清空输出目录后再导入这样能避免重复创建冲突。3.4 场景四生成资产时自动校验引用生成完资产后很有必要做一次自动校验。比如SkillData里有下一级技能等级配置引用了一个LevelData资产或者装备数据引用了图标Sprite。手填引用很容易填错脚本可以在批量操作后扫描所有资产把缺引用、枚举值超出范围等异常全部打印出来。下面是一个通用的校验脚本骨架using UnityEditor; using UnityEngine; public static class ScriptableObjectValidator { [MenuItem(Tools/Validate All ScriptableObjects)] public static void ValidateAll() { string[] guids AssetDatabase.FindAssets(t:SkillData); int errorCount 0; foreach (string guid in guids) { string path AssetDatabase.GUIDToAssetPath(guid); SkillData skill AssetDatabase.LoadAssetAtPathSkillData(path); if (skill null) continue; if (skill.damage 0) { Debug.LogError($[校验失败] {path} 的damage为负数: {skill.damage}); errorCount; } if (skill.icon null) { Debug.LogError($[校验失败] {path} 缺少图标引用); errorCount; } if (skill.id 0) { Debug.LogError($[校验失败] {path} 的id非法: {skill.id}); errorCount; } } if (errorCount 0) { Debug.Log(所有SkillData资产校验通过); } else { Debug.LogError($共发现 {errorCount} 个校验错误请查看日志); } } }这类校验脚本不一定要手动运行可以结合AssetPostprocessor在资产导入后自动校验。但注意AssetPostprocessor回调不能太耗时否则每个资产导入都会卡。我更喜欢把“校验”和“自动化操作”分开自动化操作跑完之后手动触发校验或者做成菜单项统一跑一遍。3.5 把自动化脚本挂到菜单、快捷键和右键菜单脚本写好了如果每次都要去Assets/Editor目录里点开脚本才能执行那自动化程度还不够。最好给它一个菜单入口用[MenuItem]特性挂在顶部菜单栏甚至可以注册快捷键。比如[MenuItem(Tools/Item Data/Batch Create 50 Items %#i)] public static void CreateItems() { ... }这里的%#i代表CtrlShiftImacOS上是CmdShiftI。当项目里有很多工具脚本时建议统一放到“Tools/项目名/功能名”这个路径下避免菜单散乱。如果你想在Project窗口里右键某个文件夹时直接对这个文件夹下的所有资产批量操作可以这样[MenuItem(Assets/Batch Process ScriptableObjects, false, 30)] public static void BatchProcessFromFolder() { // 拿到当前选中文件夹路径 string folderPath AssetDatabase.GetAssetPath(Selection.activeObject); string[] guids AssetDatabase.FindAssets(t:SkillData, new[] { folderPath }); // 后续处理... }这里用Selection.activeObject获取当前选中资源然后FindAssets限定搜索目录。右键菜单的路径前缀必须是“Assets/”数字是菜单排序数字越小越靠前。这种方式对美术、策划同事来说非常友好他们会用右键菜单一键处理自己目录下的资产而不用打开命令行或编辑器窗口。4. 常见问题与排查技巧实录4.1 修改后磁盘上不生效为什么asset文件还是旧数据这个问题新人遇到得最多。原因几乎都是没有调用AssetDatabase.SaveAssets()或者调用时机不对。SaveAssets只把“脏”的资产写入磁盘而“脏”的概念来自对象的变更是否被Unity识别。如果你用SerializedObject.ApplyModifiedPropertiesUnity会识别如果你直接改对象字段但没有调用EditorUtility.SetDirtyUnity会认为这个资产没变化。另外在Unity 2018及以上版本中某些情况下退出编辑器时才会自动保存脏资产所以你会看到Inspector里改了、日志也没报错但磁盘文件没变。排查思路是先确认有没有绿色“脏”标记或者改完代码里立即调用SaveAssets。一个更隐蔽的坑AssetDatabase.SaveAssets只保存资源数据库不会保存场景。如果你修改的是场景里的ScriptableObject实例像挂在场景物体上的数据应该使用EditorSceneManager.MarkSceneDirty和SaveScene。这个要看你的操作对象是资产还是场景对象别搞混。4.2 撤销失灵按CtrlZ没有回到修改前Undo记录生效有几个前提必须在修改发生前调用Undo.RecordObject。修改的目标对象必须是一个实例而且之后发生的修改应发生在主线程。如果你用SerializedObject修改字段Unity会自己维护部分撤销信息但最好仍然在修改前RecordObject一次这样整块操作可以作为一个撤销步骤。批量操作中如果每一条循环都RecordObject且用的是同一个操作名Unity会把这些步骤合并成一个撤销组这个行为比较直觉。如果你的撤销想要每一个资产都是独立步骤那就得给每个循环用不同操作名但我个人觉得合并成一个步骤更好用。还有一点如果你创建了新资产并调用了AssetDatabase.CreateAsset撤销这个创建动作需要用Undo.RegisterCreatedObjectUndo(asset, Create asset)。否则你按撤销时新建的资产不会消失这会让自动化脚本的“可逆性”大打折扣。4.3 批量修改导致引用丢失GUID漂移的坑ScriptableObject之间经常互相引用比如SkillData引用了另一个BuffData资产。如果你在自动化脚本里随手new了一个SkillData或者用AssetDatabase.CreateAsset创建新的替代旧资产而没有保持原有GUID那么其他资产对它的引用就会断掉。因为Unity的引用是靠GUID定位的不是靠路径。一个常见错误是为了“更新”某个资产把旧的删了再建新的这样引用全断。正确做法是尽量在同一资产上修改内部字段而不是删除重建。如果确实需要重建资产那重建后要重新挂引用或者用AssetDatabase.TryGetGUIDAndLocalFileIdentifier找回GUID再手动修复引用。比较稳妥的方案是批量修改前先备份原来的.asset文件或者在内存里保留原始对象的引用重建资产后用SerializedObject恢复引用。不过最省事的还是“修改已有资产”模式而不是“删除重建”模式。4.4 StartAssetEditing的坑忘记Stop会导致编辑器神游我在前面反复强调过StartAssetEditing和StopAssetEditing要成对出现因为如果中途返回或抛出异常没有调用StopAssetEditingUnity会一直认为你在编辑资源导致后续AssetDatabase.CreateAsset、AssetDatabase.SaveAssets等操作变得非常慢甚至不生效。最安全的方式是try/finallyAssetDatabase.StartAssetEditing(); try { // 批量操作 } finally { AssetDatabase.StopAssetEditing(); }有些情况下编辑器还会提醒你“Asset editing is still in progress”这时候重启编辑器也可能无效得在代码里手动调用一次AssetDatabase.StopAssetEditing()才能恢复。我在开发工具时会专门准备一个Debug菜单项用来强制调用StopAssetEditing防止自己忘了配对。4.5 脚本编译报错与API差异Unity编辑器脚本依赖的API在不同版本有差异特别是Unity 2019和2022有些Editor API改了签名。遇到报错时先去查你当前版本的Unity文档不要拿着老教程直接抄。常见问题包括AssetDatabase.LoadAssetAtPath ()从Unity 2017以后才支持泛型版本老代码可能返回Object需要手动转类型。SerializedProperty上找数组元素的写法是property.GetArrayElementAtIndex(i)不是property.arraySize[i]。FindAssets(t:ClassName)要求类名和文件名匹配否则会提示找不到类型。CreateAssetMenu的fileName参数建议预留空格或占位符不要直接写死否则创建时会默认按占位符生成。4.6 日志输出技巧定位批量操作中的异常当批量操作处理几百个资产时如果某个资产字段不合法整个循环可能会中断。我建议在关键步骤加日志并采用“收集错误最后统一输出”的模式而不是遇到第一个错误就return。这会让自动化脚本在项目里更实用。比如Liststring errors new Liststring(); foreach (var guid in guids) { try { // 操作 } catch (System.Exception e) { errors.Add(${path}: {e.Message}); } } if (errors.Count 0) { Debug.LogError($批量操作完成但有 {errors.Count} 个错误:\n{string.Join(\n, errors)}); } else { Debug.Log(批量操作全部成功); }这样做的好处是即使有个别资产异常其他资产也能继续处理而且最后你看到的错误日志包含路径和原因可以直接定位到具体文件。5. 进阶与架构建议5.1 把自动化操作封装成通用模块写多了就会发现不管是批量创建、批量修改还是CSV导入代码里都有一块“找资产、加StartAssetEditing、SaveAssets、Refresh”的模板。我建议把这部分抽成独立工具类比如AssetBatchHelper让具体业务脚本只关心字段怎么填。这样你的工具包会越来越干净新需求几分钟就能接入。一个简化的封装思路public static class AssetBatchHelper { public static T[] LoadAllAssetsT(string searchFolder null) where T : ScriptableObject { string[] guids string.IsNullOrEmpty(searchFolder) ? AssetDatabase.FindAssets($t:{typeof(T).Name}) : AssetDatabase.FindAssets($t:{typeof(T).Name}, new[] { searchFolder }); T[] result new T[guids.Length]; for (int i 0; i guids.Length; i) { string path AssetDatabase.GUIDToAssetPath(guids[i]); result[i] AssetDatabase.LoadAssetAtPathT(path); } return result; } public static void BatchEdit(Action editAction) { AssetDatabase.StartAssetEditing(); try { editAction(); } finally { AssetDatabase.StopAssetEditing(); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } }然后在具体业务里这样调用private static void UpdateAll() { var items AssetBatchHelper.LoadAllAssetsItemData(Assets/Items); AssetBatchHelper.BatchEdit(() { foreach (var item in items) { Undo.RecordObject(item, Update); item.maxStack 999; EditorUtility.SetDirty(item); } }); }这里直接改字段并SetDirty也是一个可行模式尤其是当字段不需要Inspector刷新、且你已经习惯了直接操作对象时。不过要注意直接改字段时不支持SerializedProperty所以撤销需要使用Undo.RecordObject。我自己的习惯是简单字段直接改SetDirty涉及数组/嵌套引用时用SerializedObject这样可以减少很多类型转换的麻烦。5.2 给自动化工具加一个可视化窗口命令行式菜单虽然方便编程的人但策划和非技术同事用起来不直观。这时候可以在EditorWindow里做一个小工具面板用按钮和输入框把操作暴露出来。比如一个窗口里有三个按钮“批量创建”“批量修改”“CSV导入”还有一个文本框显示日志。渲染逻辑不复杂主要是继承EditorWindow在OnGUI里用GUILayout画控件然后绑定MenuItem打开窗口。这种方式能把工具的门槛降到“点按钮”级别也能避免操作人员手误运行了危险命令。我往往会加一个确认弹窗if (!EditorUtility.DisplayDialog(确认, $即将修改 {count} 个资产, 是否继续?, 确认, 取消)) { return; }这招在自动化脚本里特别重要因为它能在你误点菜单或没拿准数据格式时救你一命。5.3 接入AssetPostprocessor和CI如果你对自动化要求更高可以把它嵌到Unity的导入管线里。比如写一个AssetPostprocessor在onPostprocessAllAssets回调里检测到某个目录新增了CSV文件就自动执行导入生成ScriptableObject。不过这要谨慎因为导入管线是每次进编辑器都可能触发处理不当会让编辑器卡顿。更保险的做法是在CI持续集成流程中跑编辑器脚本。比如在Jenkins或GitHub Actions里执行Unity的批处理命令调用Unity.exe -batchmode -projectPath xxx -executeMethod YourTool.BatchImport -quit让导入脚本自动生成全部配置资产再导出为AssetBundle。这样做能保证每次构建前的配置都是最新的也能在版本控制里看到资产更新。写批处理调用时注意纯批处理模式没有UIEditorUtility.DisplayDialog这类需要用户交互的API不能直接调用要提前判断Application.isBatchMode跳过。5.4 把数据驱动与自动化操作结合ScriptableObject的自动化操作不只是“改数据”还可以和数据驱动工具联动。比如你可以写一个工具根据命名规则自动为每个角色生成一组技能配置资产再写一个工具统计所有技能配置的强度曲线并输出报告。这类工具其实都在做同一件事把本来要靠人维护的配置变成由规则和代码维护。数据驱动的好处是当策划调整数值公式时你不需要改几十个资产只需要改工具里的公式然后一键重跑。久而久之配置的准确性、一致性会显著提升。写在最后的个人经验回想起来我用编辑器脚本批量操作ScriptableObject的最大心得是永远先把“找资产”和“改资产”分开写。找资产的方式主要靠FindAssets、GUID或路径改资产的方式要么是SerializedObject要么是直接SetDirty。只要这两个概念清晰剩下的就是组合。另一个很实用的小技巧是在批量操作前先在内存里打印一份《将要修改的资产清单》确认无误再执行尤其是要改几十上百个资产的时候。脚本不是用来省一次确认的是用来减少重复劳动和人为失误的。我后来做项目凡是涉及“改配置”的需求第一反应都是写个几行代码的菜单项而不是打开Inspector一个个点。这个习惯帮我省了不知多少时间。
返回列表