【Unity笔记】Unity 编辑器扩展:打造一个可切换 Config.assets 的顶部菜单插件


Unity 编辑器扩展:打造一个可切换 Config(ScriptableObject)的顶部菜单插件

关键词:Unity 编辑器扩展、ScriptableObject、EditorWindow、自定义菜单、配置文件管理

文章目录

1、前言:从需求到想法的萌芽

在 Unity 的项目开发中,我们经常会遇到各种「配置文件」:

  • 游戏参数配置
  • 战斗数值配置
  • UI 样式配置
  • 网络服务端地址配置

这些配置往往存放在 ScriptableObject.asset 文件中,以方便可视化编辑。

但是,随着项目的迭代,我们会发现一个问题:

👉 配置文件数量变多,每次要手动在 Project 窗口搜索、点击、打开,非常低效。

特别是团队协作时,测试、策划、程序员都可能需要修改配置。光是「找到文件」这一步,就要多点几次。

于是,萌生了一个想法:

能不能在 Unity 编辑器顶部菜单栏添加一个「配置管理」菜单?点击它就能打开一个面板,里面有下拉列表,直接切换不同的 Config 文件,并在面板中修改它的内容。

进一步优化:

  • 第一次可以通过文件管理器选择一个配置文件。
  • 之后自动保存「上一次选择的配置」,下次打开时就能直接使用。

这就是本文要实现的功能。


2、技术选型:Unity 编辑器扩展的武器库

需求流程:

为了实现上述需求,我们需要掌握以下 Unity 编辑器扩展相关技术

  1. EditorWindow

    • Unity 提供的编辑器自定义窗口基类,可以在菜单栏打开一个专属面板。
  2. MenuItem

    • 可以在 Unity 顶部菜单栏注册自定义菜单。
  3. ScriptableObject

    • 作为配置文件的载体,可以序列化保存为 .asset 文件,天然适合做 Config。
  4. EditorGUILayout

    • 用于在编辑器面板中绘制 UI,例如下拉框、按钮、对象选择器等。
  5. EditorPrefs

    • Unity 提供的本地偏好设置存储,可以保存简单的 key-value 数据(比如用户选择的上次 Config 文件路径)。
  6. AssetDatabase

    • 用于加载、查找 .asset 文件资源。

类图:


3、原理解析:从输入到持久化

在动手写代码前,我们先理一下「实现原理」:

  1. 入口

    • 在 Unity 顶部菜单栏添加一个菜单项,例如:Tools/Config Manager
  2. 面板 UI

    • 使用 EditorWindow 打开一个窗口。

    • 窗口中有:

      • 一个 下拉框,显示所有已知 Config 文件的名字。
      • 一个 按钮,点击后可通过文件管理器选择新的 Config 文件。
      • 一个 配置内容编辑区,直接显示并可修改 Config 的字段。
  3. 配置文件识别

    • Config 文件是 ScriptableObject,我们通过 AssetDatabase.FindAssets("t:Config") 找到所有同类型的 .asset 文件。
  4. 默认配置记忆

    • 使用 EditorPrefs.SetString("LastConfigPath", path) 保存上次选择的路径。
    • 下次打开窗口时,从 EditorPrefs 读取该路径,并尝试加载对应的配置文件。
  5. 编辑内容保存

    • Unity 的 SerializedObject + EditorGUILayout.PropertyField 可以动态绘制 ScriptableObject 的字段,并保证修改后能保存。

4、代码实现:完整流程

下面给出一个完整的实现案例。

假设我们的配置类是这样的:

csharp 复制代码
using UnityEngine;

[CreateAssetMenu(fileName = "GameConfig", menuName = "Config/GameConfig")]
public class GameConfig : ScriptableObject
{
    public string gameName;
    public int maxPlayerCount;
    public float gravityScale;
}

然后我们写一个编辑器插件 ConfigManagerWindow.cs

csharp 复制代码
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;

public class ConfigManagerWindow : EditorWindow
{
    private const string PREF_KEY = "LastConfigPath";

    private List<GameConfig> configs = new List<GameConfig>();
    private string[] configNames;
    private int selectedIndex = -1;

    private SerializedObject serializedConfig;
    private Vector2 scrollPos;

    [MenuItem("Tools/Config Manager")]
    public static void ShowWindow()
    {
        GetWindow<ConfigManagerWindow>("Config Manager");
    }

    private void OnEnable()
    {
        LoadConfigs();

        // 尝试读取上一次选择的配置
        string lastPath = EditorPrefs.GetString(PREF_KEY, "");
        if (!string.IsNullOrEmpty(lastPath))
        {
            GameConfig lastConfig = AssetDatabase.LoadAssetAtPath<GameConfig>(lastPath);
            if (lastConfig != null)
            {
                selectedIndex = configs.IndexOf(lastConfig);
                if (selectedIndex >= 0)
                {
                    SetCurrentConfig(configs[selectedIndex]);
                }
            }
        }
    }

    private void OnGUI()
    {
        if (configs.Count == 0)
        {
            EditorGUILayout.HelpBox("未找到任何 Config 文件,请先创建 ScriptableObject。", MessageType.Info);
            if (GUILayout.Button("选择 Config 文件"))
            {
                SelectConfigFromFile();
            }
            return;
        }

        EditorGUILayout.LabelField("选择配置文件:", EditorStyles.boldLabel);

        int newIndex = EditorGUILayout.Popup(selectedIndex, configNames);
        if (newIndex != selectedIndex)
        {
            selectedIndex = newIndex;
            SetCurrentConfig(configs[selectedIndex]);
        }

        if (GUILayout.Button("通过文件管理器选择 Config"))
        {
            SelectConfigFromFile();
        }

        if (serializedConfig != null)
        {
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("配置内容:", EditorStyles.boldLabel);

            scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
            serializedConfig.Update();
            SerializedProperty prop = serializedConfig.GetIterator();
            prop.NextVisible(true);
            while (prop.NextVisible(false))
            {
                EditorGUILayout.PropertyField(prop, true);
            }
            serializedConfig.ApplyModifiedProperties();
            EditorGUILayout.EndScrollView();
        }
    }

    private void LoadConfigs()
    {
        configs.Clear();
        string[] guids = AssetDatabase.FindAssets("t:GameConfig");
        foreach (string guid in guids)
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            GameConfig config = AssetDatabase.LoadAssetAtPath<GameConfig>(path);
            if (config != null)
            {
                configs.Add(config);
            }
        }

        configNames = new string[configs.Count];
        for (int i = 0; i < configs.Count; i++)
        {
            configNames[i] = configs[i].name;
        }
    }

    private void SetCurrentConfig(GameConfig config)
    {
        serializedConfig = new SerializedObject(config);
        string path = AssetDatabase.GetAssetPath(config);
        EditorPrefs.SetString(PREF_KEY, path);
    }

    private void SelectConfigFromFile()
    {
        string path = EditorUtility.OpenFilePanel("选择 Config 文件", Application.dataPath, "asset");
        if (!string.IsNullOrEmpty(path))
        {
            path = "Assets" + path.Substring(Application.dataPath.Length);
            GameConfig config = AssetDatabase.LoadAssetAtPath<GameConfig>(path);
            if (config != null)
            {
                if (!configs.Contains(config))
                {
                    configs.Add(config);
                    List<string> names = new List<string>(configNames);
                    names.Add(config.name);
                    configNames = names.ToArray();
                }

                selectedIndex = configs.IndexOf(config);
                SetCurrentConfig(config);
            }
        }
    }
}

5、使用体验:效果演示

交互流程:

  1. 在菜单栏点击 Tools/Config Manager

  2. 弹出一个面板:

    • 上方下拉框显示已有的配置文件
    • 点击可切换不同 Config
    • 右侧按钮可打开文件管理器,手动选择新的 Config
  3. 下方面板显示所选 Config 的所有字段,可以直接修改

  4. 修改后 Unity 自动保存到对应的 .asset 文件


6、总结与拓展

通过这次实战,我们实现了一个 Unity 编辑器扩展插件,它解决了以下问题:

  • 配置文件散落在 Project 中 → 统一入口,集中管理
  • 每次要手动搜索配置 → 一键下拉切换
  • 修改不方便 → 在面板中直接可视化编辑

核心技术包括:

  • EditorWindow 自定义窗口
  • MenuItem 注册菜单
  • SerializedObject 保证 Unity 序列化
  • EditorPrefs 保存用户默认配置
  • AssetDatabase 搜索和加载 .asset 文件

可能的扩展方向

  1. 支持多类型 Config(不同的 ScriptableObject 类型)
  2. 添加「搜索框」快速过滤 Config
  3. 添加「收藏夹」功能,常用 Config 放到置顶位置
  4. 结合 EditorGUILayout.Toolbar 做更美观的 UI

相关推荐
SmalBox8 小时前
【URP】UnityHLSL顶点片元语义详解
unity·渲染
在路上看风景19 小时前
9. Mono项目与Unity的关系
unity
在路上看风景20 小时前
1.12 Memory Profiler Package - Summary
unity
byte轻骑兵1 天前
365 天技术创作手记:从一行代码到四万同行者的相遇
ide·vscode·编辑器
SmalBox1 天前
【URP】Unity Shader Tags
unity·渲染
奥特曼打小白1 天前
Visual Studio Code的第一次安装
ide·vscode·编辑器
凯哥Java1 天前
适应新环境:Trae编辑器下的IDEA快捷键定制
java·编辑器·intellij-idea
EveryPossible1 天前
如何终止画图
linux·编辑器·vim
极客柒1 天前
Unity 塔防自用可视化路点寻路编辑器
unity·编辑器·游戏引擎