You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何减少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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 20:56:09