首页 / 资讯中心 / 文章详情

Unity编辑器扩展:一键自动化创建脚本工具

Unity编辑器扩展:一键自动化创建脚本工具 ★ FEATURED ARTICLE
做 Unity 项目做到一定年头你会发现一个挺有意思的现象真正花在写玩法逻辑上的时间远没有想象中多。大量精力被新建脚本、改类名、补命名空间、选择继承基类、拖到物体上这套循环动作吃掉了一天下来建了十几个脚本代码行数可能还没超过两百。这活儿不难但重复而且极易出错——类名忘了改、文件名和类名对不上、脚本生成到了 Editor 目录导致运行时报空、命名空间拼错带来一屏红字。这也是我后来动手做一套Unity Editor编辑器扩展之创建脚本工具的起因把创建脚本这个动作从手工活变成一条菜单命令点一下模板、命名空间、基类、目录、甚至连挂载都给你办妥。这套东西本质上属于编辑器扩展的一个细分场景规模不大但覆盖面很广——小到独立开发者日常提效大到团队统一脚本规范、统一代码风格、统一目录结构都能用得上。它解决的核心问题就一句话把人肉重复劳动换成程序按模板生成。适合谁看写过一点 C# 脚本、能看懂using UnityEditor、知道 Inspector 面板里那些东西大概在哪的 Unity 使用者就够起步如果你已经写过自定义 Inspector 或简单的编辑器窗口这篇文章里关于刷新时机、域重载、编译等待的部分应该会让你少踩几个坑。1. 为什么值得为创建脚本单独做一个编辑器扩展1.1 手动创建脚本的隐性成本比你想的高很多人觉得新建脚本就两秒的事右键 → Create → C# Script → 改名字 → 双击打开 → 改类名 → 保存。听起来很快但一次完整的动作链条其实有七八步而且每一步都有出错概率。我统计过自己一周的操作平均每天新建 6 到 10 个脚本其中有大概两三次会出现文件名和类名不一致导致MonoBehaviour挂不上去或者新脚本被 Unity 默认丢到了 Project 窗口当前选中的目录——而那个目录往往是我刚点过的一个无关文件夹等发现的时候脚本已经在错误的位置躺了半天。更麻烦的是规范层面。团队里如果约定所有运行时脚本必须带Game.Runtime.XXX命名空间、所有 UI 相关脚本必须继承自定义基类UIBase那么手工创建就变成了一场记忆测试。新人来了记不住老人来了嫌麻烦直接跳过等 Code Review 的时候才发现一堆裸脚本改起来成本翻好几倍。注意这类小工具最容易犯的错是追求一次性做大做全结果写了两千行没人用。我的建议是先解决一个最痛的点——通常是创建时自动带上命名空间和基类——剩下的以后再说。从投入产出比看这套扩展一次写完大概两三百行代码之后整个项目周期都在受益。而且它是个很好的编辑器扩展入门练手项目涉及MenuItem、EditorWindow、AssetDatabase、ProjectWindowUtil、Undo这些核心 API但逻辑复杂度不高失败了也就是生成个空文件不会把项目搞坏。1.2 划清边界哪些该自动化哪些不该碰做这种工具最容易上头的地方是忍不住把脚本内容生成做成一个迷你代码生成器。我见过有人试图根据字段定义自动生成Awake、Start、Update骨架甚至连GetComponent缓存都给你自动写好。想法很好实际用起来很别扭——因为一旦生成完你还是要改动而改完之后模板再更新就会和已有代码打架。我的判断标准很简单只自动化每次都必须一样的部分不碰每个脚本都会不一样的部分。该自动化的文件名与类名同步、命名空间声明、using头部、基类继承、目录选择、.meta生成、程序集归属。不该自动化的具体字段、方法体、生命周期逻辑、业务代码结构。按这个边界划下来工具的核心其实就是模板字符串替换 文件写入 资产导入三件事剩下的都是体验优化。思路清楚了代码就不会跑偏。1.3 和 Unity 自带模板的关系不是替代是接管入口Unity 自带有一套脚本模板位置在编辑器安装目录的Resources/ScriptTemplates下文件名形如81-C# Script-NewBehaviourScript.cs.txt。你在 Project 窗口右键新建 C# Script 时Unity 就是拿这个文件做替换把#SCRIPTNAME#换成你输入的文件名。所以做自定义脚本创建工具有两条路方案做法优点缺点改官方模板直接改安装目录下的模板文件零代码换机器、重装、版本升级全部失效团队成员之间不同步自定义菜单入口自己写菜单项用ProjectWindowUtil创建全项目共享、可版本管理、可加逻辑需要写代码入口位置和官方的不完全一样我选第二条路。原因很直接官方模板放在安装目录属于本机配置天然不适合团队协作。自建入口则可以把模板文件一起提交到版本库谁拉下来都能用同一套规范。这个选择后来被验证是对的——中间换过一次开发机自建工具开箱即用官方模板的改动早没了。2. 方案选型三种入口形态怎么挑2.1 MenuItem、EditorWindow、ProjectWindowUtil 右键各管一段做脚本创建工具入口形态基本就三种它们的定位完全不同千万别混着用。第一种是MenuItem也就是顶部菜单栏或者右键菜单里的一条命令。它的特点是一键直达适合固定参数的场景比如创建一个空的 MonoBehaviour这种没有任何选项的操作。写法就是给静态方法打上[MenuItem(路径)]标签路径决定了它出现在哪Assets/Create/...会出现在 Project 窗口右键的 Create 子菜单里跟 Unity 自带的创建项排在一起最符合直觉。Assets/...出现在 Project 窗口右键的顶层菜单。GameObject/...出现在 Hierarchy 窗口右键菜单适合做创建并挂载的合并操作。Tools/...出现在顶部菜单栏适合放低频但重要的工具。第二种是EditorWindow一个独立窗口。它适合参数多的场景脚本名、命名空间、模板类型、输出目录、是否挂载到选中物体五六个选项用菜单项根本表达不了。窗口的代价是多一步操作但换来的是灵活性。第三种是ProjectWindowUtil系列的静态方法它本身不是入口而是创建资产的底层能力。ProjectWindowUtil.CreateScriptAssetFromTemplateFile(templatePath, defaultName)这个方法会把模板文件内容读出来、替换占位符、写入目标目录、触发导入一整套流程给你做完。它的关键优势是自动处理.meta文件和资源导入比自己File.WriteAllText靠谱得多。我的实际组合是菜单项负责高频无参场景窗口负责需要配置的场景两者底层都调ProjectWindowUtil。2.2 为什么优先用 ProjectWindowUtil 而不是直接写文件刚上手的人第一反应通常是File.WriteAllText(path, content)然后AssetDatabase.Refresh()。这么写能跑但坑不少。第一条坑是.meta文件。Unity 里每个资产都有对应的.meta里面存着 GUID。自己写文件再刷新Unity 会自动补.meta但如果写入和刷新之间有别的逻辑依赖这个脚本比如立刻LoadAssetAtPath就会拿不到。ProjectWindowUtil.CreateScriptAssetFromTemplateFile内部走的是编辑器正规的资产创建流程.meta生成和导入是一体的时序上更安全。第二条坑是 Undo 和选中状态。用ProjectWindowUtil创建完Unity 会自动选中新生成的资产并进入重命名状态——这个体验跟自己写文件完全不一样后者创建完你还得自己去 Project 窗口里找。第三条坑是路径推断。ProjectWindowUtil系列方法会自动以当前 Project 窗口选中的文件夹作为默认输出位置这符合用户直觉。自己实现的话得用AssetDatabase.GetAssetPath(Selection.activeObject)再判断是文件夹还是文件逻辑要多写十几行。提示ProjectWindowUtil.CreateScriptAssetFromTemplateFile在 Unity 2019.4 LTS 及以后的版本里都能用。更老的版本里叫CreateScriptAsset签名不一样如果你的项目要兼容多版本建议加个版本判断或者干脆只支持 2019.4 以上。2.3 模板体系的落点放在哪、叫什么后缀模板文件放哪这个问题比想象中重要。我的做法是统一放在Assets/Editor/ScriptTemplates/目录下文件名形如MonoBehaviourTemplate.txt。后缀一定要用.txt不能直接用.cs。原因很实在Unity 会把.cs文件当脚本编译而模板里的内容如果带了#SCRIPTNAME#这种占位符类名根本不存在编译直接报错。用.txt就不会被编译Unity 只当它是个普通文本资产。还有个细节整个Editor目录里的东西都不会被打进运行时构建所以模板文件放在这里既安全又不会污染包体。如果模板要给美术、策划看也可以放到Assets/Editor/ScriptTemplates/README之类的说明文件旁边方便查阅。3. 核心 API 拆解几个必须搞懂的参数和时机3.1 MenuItem 的路径、优先级与校验方法MenuItem的路径不只是个字符串它决定了菜单项的层级和位置。路径用/分隔最后一段是显示名。优先级priority是个整数控制同层级下的排序数值小的排前面。Unity 官方约定是0 到 49 放最顶部50 到 499 放中间500 以上放底部。Assets/Create菜单里通常用 1 到 49。还有一个容易被忽略的用法是校验方法。给MenuItem加第二个参数true对应的方法就变成校验函数返回值决定菜单项是否可点击[MenuItem(Assets/Create/C# 自定义脚本/MonoBehaviour, false, 100)] private static void CreateMonoBehaviour() { /* ... */ } [MenuItem(Assets/Create/C# 自定义脚本/MonoBehaviour, true, 100)] private static bool CreateMonoBehaviourValidate() { // 只有选中文件夹时才允许创建 return Selection.activeObject is DefaultAsset; }校验方法的价值在于防止误操作。比如创建后挂载到选中物体这种功能如果当前选中的不是 GameObject就应该禁用而不是报错。用校验方法把错误挡在入口处比在方法体里写一堆if判断体验好得多。3.2 快捷键与路径中的特殊前缀MenuItem的路径支持快捷键声明用空格加特殊字符表示%是 CtrlmacOS 上是 Cmd#是 Shift是 Alt_表示无修饰键。比如Tools/脚本生成器 %#g就是 CtrlShiftG。注意快捷键不要和 Unity 自带的热键冲突尤其是CtrlS、CtrlN、CtrlZ这些。我吃过一次亏给一个生成工具绑了CtrlN结果新建场景的功能直接被吃掉查了半天才反应过来。路径前缀里还有两个特殊值值得知道CONTEXT/组件名/方法名可以给特定组件的右键菜单加项Assets/Create/会自动出现在 Project 窗口的 Create 子菜单。前者用于给自定义组件加扩展操作后者就是我们这次要用的主力路径。3.3 AssetDatabase 的刷新时机是最容易翻车的地方写完文件之后调不调AssetDatabase.Refresh()什么时候调这个问题困扰过很多人。我的经验可以总结成几条第一用ProjectWindowUtil创建资产的话不需要手动Refresh它内部已经处理了导入流程。手动再调一次只是浪费性能。第二自己用File.WriteAllText写文件的话必须调AssetDatabase.Refresh()否则 Unity 根本不知道有新文件。但Refresh是个相对重的操作它会扫描整个 Assets 目录所以不要在循环里每个文件调一次攒完一起调。第三如果只是想导入某一个文件用AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate)比Refresh精准得多。这个 API 我用了很多次尤其在批量生成脚本的场景下先全部写完再统一ImportAsset速度明显比逐个Refresh快。第四别在ImportAsset之后立刻假设脚本已经编译完成。资源导入和脚本编译是两个不同的阶段ImportAsset返回时脚本可能还没进编译队列。3.4 EditorApplication.delayCall等一帧世界就不一样了很多功能看起来应该能用但就是不行根子都在时机上。比如创建完脚本想立刻AddComponent十有八九会失败因为脚本虽然写进磁盘了但还没编译类型在AppDomain里不存在GetType返回 null。EditorApplication.delayCall就是解这个问题的。它把一段委托推迟到下一次编辑器更新时执行相当于等这一轮操作走完再说。典型写法string createdPath Assets/Scripts/Generated/MyBehaviour.cs; // ... 创建脚本的代码 ... EditorApplication.delayCall () { var script AssetDatabase.LoadAssetAtPathMonoScript(createdPath); if (script null) return; var type script.GetClass(); if (type null) return; // 此时类型已经可用可以挂载了 };但要注意delayCall只延后一帧编辑器更新如果脚本编译时间较长比如项目里有一堆代码要重编一帧不够。更稳的做法是监听CompilationPipeline.compilationFinished事件或者用EditorApplication.isCompiling配合轮询。我的实际做法是两者结合先delayCall尝试一次如果GetClass()返回 null就挂到编译完成事件上再试。4. 完整实操从零搭一个能用的脚本生成器4.1 目录结构与 Editor 文件夹的分界线动手之前先把目录定下来这个顺序不能反。我的结构是这样的Assets/ Editor/ ScriptCreators/ ScriptCreatorMenu.cs ScriptGeneratorWindow.cs ScriptTemplateProvider.cs ScriptTemplates/ MonoBehaviourTemplate.txt ScriptableObjectTemplate.txt EmptyClassTemplate.txt InterfaceTemplate.txt Scripts/ Runtime/ Generated/Assets/Editor/这个目录名是 Unity 的硬性约定放在里面的脚本会被编进编辑器程序集只在编辑器环境下存在。这意味着两件事一是能在里面using UnityEditor而不会影响打包二是里面的代码不能被运行时脚本引用。注意如果你的项目用了 Assembly Definition.asmdefEditor目录的规则会被 asmdef 覆盖。这时候要给编辑器脚本单独建一个 asmdef并在includePlatforms里只勾选Editor。这一点很多人踩过坑明明放在Editor文件夹里打包还是报UnityEditor找不到。4.2 第一步菜单项版三十行解决单脚本创建先做最简版本跑通整条链路。新建Assets/Editor/ScriptCreators/ScriptCreatorMenu.csusing UnityEditor; using UnityEngine; public static class ScriptCreatorMenu { private const string TemplateDir Assets/Editor/ScriptTemplates/; [MenuItem(Assets/Create/C# 自定义脚本/MonoBehaviour 空壳, false, 101)] private static void CreateMonoBehaviour() { string templatePath TemplateDir MonoBehaviourTemplate.txt; ProjectWindowUtil.CreateScriptAssetFromTemplateFile( templatePath, NewMonoBehaviour.cs); } [MenuItem(Assets/Create/C# 自定义脚本/ScriptableObject 空壳, false, 102)] private static void CreateScriptableObject() { string templatePath TemplateDir ScriptableObjectTemplate.txt; ProjectWindowUtil.CreateScriptAssetFromTemplateFile( templatePath, NewScriptableObject.cs); } }模板文件MonoBehaviourTemplate.txt的内容using UnityEngine; namespace Game.Runtime { public class #SCRIPTNAME# : MonoBehaviour { private void Awake() { } private void OnDestroy() { } } }这里有个关键点#SCRIPTNAME#是占位符ProjectWindowUtil.CreateScriptAssetFromTemplateFile会把它替换成你在 Project 窗口里输入的文件名不含.cs后缀。所以你输入PlayerController生成的类名就是PlayerController文件名和类名天然一致那个经典的类名对不上问题直接被消灭。默认名NewMonoBehaviour.cs会预先填在重命名输入框里并全选用户直接打字就能覆盖体验比让他自己从头输入好得多。4.3 第二步窗口版处理需要配置的场景菜单项解决不了我要选命名空间、选目录、还要顺手挂到选中物体上这类需求。这时候上EditorWindow。using System.IO; using System.Text; using UnityEditor; using UnityEngine; public class ScriptGeneratorWindow : EditorWindow { private enum TemplateKind { MonoBehaviour, ScriptableObject, EmptyClass, Interface } [SerializeField] private string scriptName NewBehaviour; [SerializeField] private string namespaceName Game.Runtime; [SerializeField] private string outputFolder Assets/Scripts/Generated; [SerializeField] private TemplateKind templateKind TemplateKind.MonoBehaviour; [SerializeField] private bool attachToSelection; [MenuItem(Tools/脚本生成器 %#g)] private static void Open() { var window GetWindowScriptGeneratorWindow(脚本生成器); window.minSize new Vector2(400f, 280f); window.Show(); } private void OnGUI() { EditorGUILayout.LabelField(基础信息, EditorStyles.boldLabel); scriptName EditorGUILayout.TextField(脚本名, scriptName); namespaceName EditorGUILayout.TextField(命名空间, namespaceName); templateKind (TemplateKind)EditorGUILayout.EnumPopup(模板类型, templateKind); EditorGUILayout.Space(); EditorGUILayout.LabelField(输出设置, EditorStyles.boldLabel); outputFolder EditorGUILayout.TextField(输出目录, outputFolder); attachToSelection EditorGUILayout.Toggle(创建后挂到选中物体, attachToSelection); EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(!IsNameValid())) { if (GUILayout.Button(生成脚本, GUILayout.Height(34f))) { Generate(); } } } private bool IsNameValid() { return !string.IsNullOrWhiteSpace(scriptName) !scriptName.Contains( ) !File.Exists(Path.Combine(outputFolder, scriptName .cs)); } }这段 GUI 代码里有几个值得说的细节。[SerializeField]加在窗口字段上是为了应对域重载。Unity 在脚本编译或进入播放模式时会重载整个AppDomain窗口对象虽然还在但普通字段会被重置成默认值。加了[SerializeField]这些值会被序列化保存重载之后还能恢复。这个坑我踩过不止一次窗口开着编译一下填好的参数全没了纯粹是白干。EditorGUI.DisabledScope用来在名字不合法时把按钮置灰比点击之后再弹提示温和。判断条件里我加了文件存在性检查防止覆盖已有脚本——虽然生成器覆盖自己刚写的文件问题不大但如果目录填错覆盖了核心逻辑脚本那就不是小事了。GetWindowT会复用已存在的窗口实例不会每次点菜单都新开一个这一点比自己new EditorWindow要省心。4.4 第三步核心生成逻辑字符串拼装与文件写入窗口里那个Generate()方法才是真正干活的部分private void Generate() { if (!Directory.Exists(outputFolder)) { Directory.CreateDirectory(outputFolder); } string filePath Path.Combine(outputFolder, scriptName .cs); string content BuildContent(); File.WriteAllText(filePath, content, new UTF8Encoding(false)); AssetDatabase.ImportAsset(filePath, ImportAssetOptions.ForceUpdate); AssetDatabase.Refresh(); var createdAsset AssetDatabase.LoadAssetAtPathMonoScript(filePath); Selection.activeObject createdAsset; EditorGUIUtility.PingObject(createdAsset); if (attachToSelection templateKind TemplateKind.MonoBehaviour) { EditorApplication.delayCall () TryAttach(filePath); } }Directory.CreateDirectory顺手处理了目录不存在的情况不需要在外面单独判断。File.WriteAllText的第三个参数指定new UTF8Encoding(false)也就是不带 BOM 的 UTF-8。为什么要强调这个因为有些编辑器、有些 Unity 版本对带 BOM 的文件解析会出现诡异问题比如文件头部多出一个不可见字符using语句被识别成非法语句。统一写无 BOM 最稳妥。ImportAsset和Refresh都调了看着有点冗余但实际效果是ImportAsset精准导入这个文件Refresh兜底扫描一遍确保 Project 窗口立即刷新显示。这点开销可以接受换来的是用户不会看到文件生成了但 Project 窗口没显示的尴尬。EditorGUIUtility.PingObject会让新生成的脚本在 Project 窗口高亮闪一下视觉反馈很重要不然用户根本不知道文件生成到哪里去了。4.5 模板内容拼装与占位符替换BuildContent()是字符串拼装的核心我用的是简单直接的StringBuilderprivate string BuildContent() { var sb new StringBuilder(); sb.AppendLine(using UnityEngine;); sb.AppendLine(); bool hasNamespace !string.IsNullOrWhiteSpace(namespaceName); if (hasNamespace) { sb.AppendLine($namespace {namespaceName}); sb.AppendLine({); } string indent hasNamespace ? : string.Empty; switch (templateKind) { case TemplateKind.MonoBehaviour: sb.AppendLine(${indent}public class {scriptName} : MonoBehaviour); sb.AppendLine(${indent}{{); sb.AppendLine(${indent} private void Awake()); sb.AppendLine(${indent} {{); sb.AppendLine(${indent} }}); sb.AppendLine(${indent}}}); break; case TemplateKind.ScriptableObject: sb.AppendLine(${indent}[CreateAssetMenu(fileName \{scriptName}\, menuName \Config/{scriptName}\)]); sb.AppendLine(${indent}public class {scriptName} : ScriptableObject); sb.AppendLine(${indent}{{); sb.AppendLine(${indent}}}); break; case TemplateKind.EmptyClass: sb.AppendLine(${indent}public class {scriptName}); sb.AppendLine(${indent}{{); sb.AppendLine(${indent}}}); break; case TemplateKind.Interface: sb.AppendLine(${indent}public interface I{scriptName}); sb.AppendLine(${indent}{{); sb.AppendLine(${indent}}}); break; } if (hasNamespace) { sb.AppendLine(}); } return sb.ToString(); }代码看着啰嗦但它把所有格式细节缩进、空行、括号位置都固化下来了生成结果 100% 一致不会因为手滑多一个空格导致 StyleCop 之类的检查器报错。这种固化在多人协作里价值特别大代码风格统一不靠口头约定靠工具保证。接口那一条我特意加了I前缀IFooService这种命名是 C# 界的通行做法。如果你团队不用这个约定把I删掉就行。4.6 第四步生成后自动挂载时机才是难点TryAttach是整篇文章里最考验时机理解的一段private void TryAttach(string scriptPath) { var script AssetDatabase.LoadAssetAtPathMonoScript(scriptPath); if (script null) return; var type script.GetClass(); if (type null) { // 类型还没编译出来挂到编译完成事件上再试一次 void OnCompilationFinished(object obj) { CompilationPipeline.compilationFinished - OnCompilationFinished; TryAttach(scriptPath); } CompilationPipeline.compilationFinished OnCompilationFinished; return; } var selected Selection.activeGameObject; if (selected null) return; if (selected.GetComponent(type) ! null) { Debug.LogWarning(${selected.name} 上已经挂载了 {type.Name}); return; } Undo.AddComponent(selected, type); EditorUtility.SetDirty(selected); }这段代码里有几个点值得展开。script.GetClass()返回的是MonoScript里对应的System.Type如果脚本还没编译完成它会返回 null。这时候不能硬等得用事件回调。CompilationPipeline.compilationFinished会在编译结束时触发但要注意这个事件回调里不能做太重的操作否则会拖慢编辑器。Undo.AddComponent而不是selected.AddComponent(type)这是必须的。前者会注册 Undo 操作用户按CtrlZ能撤销后者不会加错了只能手动删除。这种小细节决定了工具是能用还是好用。EditorUtility.SetDirty(selected)标记场景对象已修改配合Undo使用保证场景保存时改动不丢。还有一个隐患如果当前正在 Prefab 编辑模式下Selection.activeGameObject拿到的是 Prefab 内部的对象AddComponent会改到 Prefab 上可能不是用户想要的。稳妥的做法是先判断PrefabStageUtility.GetCurrentPrefabStage()是否为 null不为 null 时给出提示而不是直接操作。5. 常见问题与排查技巧实录5.1 问题速查表下面这些是我在实际使用和帮别人排查时遇到频率最高的问题整理成表格方便对照现象最可能的原因排查与解决菜单项根本不出现脚本没放在Editor目录或类不是static确认路径含EditorMenuItem方法必须是static点击菜单报NullReferenceException模板文件路径不存在用AssetDatabase.LoadAssetAtPath先验证模板资产生成的文件是空的或只有乱码模板编码不是 UTF-8用文本编辑器另存为 UTF-8类名和文件名不一致用了自己拼字符串的路径没走占位符替换改回ProjectWindowUtil或手动同步文件名挂载组件时type为 null脚本还没编译完用delayCall或CompilationPipeline事件延后打包时报UnityEditor找不到编辑器脚本混进了运行时程序集检查Editor目录或 asmdef 的includePlatforms窗口关闭后参数丢失域重载导致字段重置加[SerializeField]或存EditorPrefs生成到错误目录误用了Selection.activeObject的路径判断选中对象是否为文件夹DefaultAsset5.2 编码问题中文注释乱码的根源模板里写中文注释生成出来变成乱码这个问题的原因八成是文件编码不是 UTF-8。Windows 下的记事本默认可能存成 ANSIUnity 读的时候按 UTF-8 解析中文自然就乱了。判断方法很简单用支持编码查看的编辑器打开模板文件看右下角显示的编码格式。解决方法是统一另存为 UTF-8。另外前面提到的写入时指定new UTF8Encoding(false)保证输出文件也不带 BOM两头都对上。提示如果团队里有非中文环境的人协作模板里干脆别写中文注释。这不是妥协是减少无谓的沟通成本。注释写在提交信息里或者单独维护一份说明文档比塞在模板里更合适。5.3 命名空间与程序集的隐性冲突生成的脚本如果落在了一个被 asmdef 覆盖的目录里而这个 asmdef 又没有引用UnityEngine.UI之类的包那么模板里带的using UnityEngine.UI;就会报编译错误。这个问题在新建项目时特别常见因为目录结构还没稳定asmdef 的划分也还在调整。处理办法有两个。一是生成前检查目标目录的 asmdef 归属用CompilationPipeline.GetAssemblies()拿到程序集信息判断目标目录是否被覆盖如果被覆盖就给出提示。二是干脆在工具里加一个输出目录下拉框列出项目里所有合法目录让用户选从源头避免填错。我更倾向于第二个因为简单直接。下拉框的数据来源可以用AssetDatabase.FindAssets(t:DefaultAsset, new[] { Assets })再过滤出文件夹虽然会扫全项目但只在打开窗口时扫一次性能可以接受。5.4 一个容易忽略的细节.meta与版本管理工具生成脚本之后别忘了.meta文件也要一起提交。有时候在版本管理里看到有人只提交了.cs没提交.meta拉下来之后 GUID 全变场景里挂的引用全部丢失——这种事故修起来相当痛苦。ProjectWindowUtil和AssetDatabase.ImportAsset都会自动生成.meta正常情况下不会漏。真正的风险在于用File.WriteAllText写了文件但没触发导入这时候.meta不存在如果有人手快提交了后面就埋雷了。所以我的原则是只要写了文件后面必须紧跟ImportAsset一步都不能省。6. 几套可以直接抄的模板与扩展玩法6.1 MonoBehaviour 模板的实战取舍模板里到底该放什么这个问题没有标准答案但有取舍逻辑。我的MonoBehaviourTemplate.txt只放三样东西using UnityEngine;、命名空间、类声明加一对空括号。Awake和OnDestroy我一开始也放了后来删了——因为大多数脚本并不需要这两个方法留着反而是噪音用的时候还得手动删。这一点跟写代码时的不要提前抽象是一个道理。模板是给所有脚本用的所以应该取最小公约数只保留 100% 会用到的东西。需要额外方法的场景单独做一个带生命周期的 MonoBehaviour模板就行多一个菜单项的事。6.2 ScriptableObject 模板里的CreateAssetMenuScriptableObject模板跟MonoBehaviour有个本质区别它需要[CreateAssetMenu]特性才能在 Project 窗口右键创建实例。所以模板里的特性参数必须和类名联动[CreateAssetMenu(fileName #SCRIPTNAME#, menuName Config/#SCRIPTNAME#)] public class #SCRIPTNAME# : ScriptableObject { }fileName是创建出来的资产默认名menuName是右键菜单路径。用#SCRIPTNAME#占位符可以保证三处文件名、资产名、菜单名自动同步不用手动改三遍。menuName里的Config/前缀是分类实际项目里可以按模块细分比如Config/战斗/技能配置。这个层级结构在配置资产多了之后会救命否则一长串平铺的菜单项找起来很痛苦。6.3 从配置批量生成脚本再往前走一步可以做成读一份 JSON 或 CSV批量生成一批脚本。这个玩法在需要快速铺量的场景里很好用比如要根据一份技能表生成几十个技能类每个类继承同一个基类、带上不同的常量。核心逻辑还是File.WriteAllText加模板替换区别在于外面套一层循环并且循环结束后统一AssetDatabase.Refresh()一次。注意不要在循环里调Refresh否则每写一个文件就扫一遍整个项目几十个文件能卡好几秒。批量生成时还有一个细节先校验全部输入再开始写文件。如果第 30 个文件名非法前面 29 个已经写进去了回滚很麻烦。我的做法是先跑一遍校验把所有问题收集到一个列表里全部通过才进入写文件阶段。这个思路跟数据库事务有点像宁可整体失败不留半成品。6.4 一个我用了很久的小技巧最后分享一个不太起眼但很实用的做法给生成器加一个最近使用记录。用EditorPrefs存最近三次的命名空间和输出目录打开窗口时自动填进去。因为大多数时候同一个人连续创建的脚本都在同一个模块命名空间和目录基本不变能少填两个框就少填两个框。EditorPrefs是存在本机的不会跟着项目走正好符合这个需求。写法也简单EditorPrefs.SetString(ScriptGen.LastNamespace, namespaceName)存EditorPrefs.GetString(ScriptGen.LastNamespace, Game.Runtime)读带默认值防止首次使用时为空。这套工具从最初的三十行菜单项到现在两百多行的窗口版中间改过好几轮。最大的体会是编辑器扩展的价值不在于代码多精妙而在于它有没有真的嵌进你的日常工作流。如果哪天你发现新建脚本的时候下意识地去点自己写的菜单项而不是右键 Create那这工具就算成了。
阅读完成 · 觉得有帮助?
咨询建站