如何减少Unity静态MenuItem类中的重复模板代码?
解决方案
针对Unity中无法通过抽象基类复用静态[MenuItem]类代码的问题,这里提供三种优雅的优化方案,按实现复杂度和适用场景排序:
方案1:抽离通用逻辑到工具类(最简实现)
将重复的IsEnabled、菜单切换、验证逻辑全部封装到工具类中,每个具体的Symbol定义类仅需维护差异部分(Symbol常量和BuildTarget列表),并调用工具类方法。
步骤1:创建通用工具类
using UnityEditor; using System.Collections.Generic; public static class ScriptingSymbolDefinerTool { // 获取Symbol的启用状态 public static bool GetSymbolEnabled(string symbol) { string settingKey = $"EnableScriptingSymbol_{symbol}"; return EditorPrefs.GetBool(settingKey, false); } // 设置Symbol的启用状态 public static void SetSymbolEnabled(string symbol, bool value) { string settingKey = $"EnableScriptingSymbol_{symbol}"; EditorPrefs.SetBool(settingKey, value); } // 切换Symbol状态并更新Build配置 public static void ToggleSymbol(string symbol, List<NamedBuildTarget> buildTargets) { bool currentState = GetSymbolEnabled(symbol); SetSymbolEnabled(symbol, !currentState); SymbolDefinerUtilities.UpdateSymbolDefinition(symbol, !currentState, buildTargets); } // 菜单验证逻辑(处理勾选状态) public static bool ValidateMenu(string symbol, string menuPath) { Menu.SetChecked(menuPath, GetSymbolEnabled(symbol)); return true; } }
步骤2:简化每个Symbol定义类
每个类仅需3部分代码:差异配置、菜单入口、验证方法,重复代码压缩到最低:
using UnityEditor; using System.Collections.Generic; public static class ScriptingSymbolDefiner_JSON_DOT_NET { // 仅需维护这两个差异项 private const string Symbol = "JSON_DOT_NET"; private static readonly List<NamedBuildTarget> _buildTargets = new() { NamedBuildTarget.Android, NamedBuildTarget.iOS, NamedBuildTarget.NintendoSwitch, NamedBuildTarget.Server, NamedBuildTarget.Standalone }; private const string MenuName = "Wichtel/Scripting Symbols/" + Symbol; [MenuItem(MenuName)] private static void ToggleAction() { ScriptingSymbolDefinerTool.ToggleSymbol(Symbol, _buildTargets); } [MenuItem(MenuName, true)] private static bool ToggleActionValidate() { return ScriptingSymbolDefinerTool.ValidateMenu(Symbol, MenuName); } }
方案2:T4模板自动生成代码(适合大量Symbol场景)
如果需要创建十几个甚至更多此类静态类,使用T4模板可以一键生成所有代码,仅需维护配置列表即可。
创建T4模板文件(.tt后缀)
<#@ template language="C#" #> <#@ assembly name="UnityEditor.dll" #> <#@ assembly name="UnityEngine.dll" #> <#@ import namespace="UnityEditor" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Linq" #> // 此文件由T4模板自动生成,请勿手动修改 namespace YourProject.Editor { <# // 在这里添加所有需要生成的Symbol配置 var symbolConfigs = new List<(string Symbol, List<NamedBuildTarget> Targets)> { ("JSON_DOT_NET", new List<NamedBuildTarget> { NamedBuildTarget.Android, NamedBuildTarget.iOS, NamedBuildTarget.NintendoSwitch, NamedBuildTarget.Server, NamedBuildTarget.Standalone }), ("WEBGL_SUPPORT", new List<NamedBuildTarget> { NamedBuildTarget.WebGL }), ("VR_MODULE", new List<NamedBuildTarget> { NamedBuildTarget.Standalone, NamedBuildTarget.Android }) // 继续添加更多配置... }; foreach (var config in symbolConfigs) { string targetList = string.Join(", ", config.Targets.Select(t => $"NamedBuildTarget.{t}")); #> public static class ScriptingSymbolDefiner_<#= config.Symbol #> { private const string Symbol = "<#= config.Symbol #>"; private static readonly List<NamedBuildTarget> buildTargets = new() { <#= targetList #> }; private const string MenuName = "Wichtel/Scripting Symbols/" + Symbol; private const string SettingName = "EnableScriptingSymbol_" + Symbol; public static bool IsEnabled { get => EditorPrefs.GetBool(SettingName, false); set => EditorPrefs.SetBool(SettingName, value); } [MenuItem(MenuName)] private static void ToggleAction() { IsEnabled = !IsEnabled; SymbolDefinerUtilities.UpdateSymbolDefinition(Symbol, IsEnabled, buildTargets); } [MenuItem(MenuName, true)] private static bool ToggleActionValidate() { Menu.SetChecked(MenuName, IsEnabled); return true; } } <# } #> }
使用方式
修改模板中的symbolConfigs配置列表,保存后Unity会自动生成对应的静态类文件,完全避免手写重复代码。
方案3:自定义特性+动态菜单注册(进阶优雅方案)
利用Unity 2019+提供的CommandService,通过自定义特性标记Symbol信息,在编辑器启动时动态注册菜单命令,彻底消除重复代码。
步骤1:定义自定义特性
using UnityEditor; using System; [AttributeUsage(AttributeTargets.Class)] public class ScriptingSymbolDefinerAttribute : Attribute { public string Symbol { get; } public NamedBuildTarget[] BuildTargets { get; } public ScriptingSymbolDefinerAttribute(string symbol, params NamedBuildTarget[] buildTargets) { Symbol = symbol; BuildTargets = buildTargets; } }
步骤2:编写动态菜单加载器
using UnityEditor; using UnityEditor.Experimental; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; [InitializeOnLoad] public static class DynamicSymbolDefinerLoader { static DynamicSymbolDefinerLoader() { var commandService = CommandService.instance; // 扫描所有带自定义特性的类 foreach (Type type in AppDomain.CurrentDomain.GetAssemblies() .SelectMany(asm => asm.GetTypes()) .Where(t => t.GetCustomAttribute<ScriptingSymbolDefinerAttribute>() != null)) { var attr = type.GetCustomAttribute<ScriptingSymbolDefinerAttribute>(); string symbol = attr.Symbol; var buildTargets = attr.BuildTargets.ToList(); string menuPath = $"Wichtel/Scripting Symbols/{symbol}"; string settingKey = $"EnableScriptingSymbol_{symbol}"; // 注册菜单切换命令 commandService.AddCommand(menuPath, () => { bool currentState = EditorPrefs.GetBool(settingKey, false); EditorPrefs.SetBool(settingKey, !currentState); SymbolDefinerUtilities.UpdateSymbolDefinition(symbol, !currentState, buildTargets); }); // 注册菜单验证逻辑(处理勾选状态) commandService.AddValidationHandler(menuPath, () => new CommandState { checkedState = EditorPrefs.GetBool(settingKey, false) ? CommandCheckedState.Checked : CommandCheckedState.Unchecked, enabled = true }); } } }
步骤3:创建空类标记特性
每个Symbol仅需一个空的静态类,用特性标记即可:
using UnityEditor; [ScriptingSymbolDefiner("JSON_DOT_NET", NamedBuildTarget.Android, NamedBuildTarget.iOS, NamedBuildTarget.NintendoSwitch, NamedBuildTarget.Server, NamedBuildTarget.Standalone)] public static class JSONDotNetSymbolDefiner { // 无需任何其他代码 } [ScriptingSymbolDefiner("WEBGL_SUPPORT", NamedBuildTarget.WebGL)] public static class WebGLSymbolDefiner { // 无需任何其他代码 }
内容的提问来源于stack exchange,提问作者Meister der Magie
相关产品推荐
相关产品推荐

