【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

相关推荐
行走的陀螺仪8 小时前
.vscode 文件夹配置详解
前端·ide·vscode·编辑器·开发实践
avi911116 小时前
发现一个宝藏Unity开源AVG框架,视觉小说的脚手架
unity·开源·框架·插件·tolua·avg
艾莉丝努力练剑21 小时前
【Python基础:语法第一课】Python 基础语法详解:变量、类型、动态特性与运算符实战,构建完整的编程基础认知体系
大数据·人工智能·爬虫·python·pycharm·编辑器
skywalk81631 天前
FreeBSD系统安装VSCode Server(未成功,后来是在FreeBSD系统里的Linux虚拟子系统里安装启动了Code Server)
ide·vscode·编辑器·freebsd
一线灵1 天前
跨平台游戏引擎 Axmol-2.10.0 发布
游戏引擎
你还满意吗1 天前
开发工具推荐
编辑器
沉默金鱼2 天前
Unity实用技能-格式化format文字
ui·unity·游戏引擎
jyy_992 天前
通过网页地址打开unity的exe程序,并传参
unity
亮子AI2 天前
如何做一个类似Word的编辑器?要有修改标记功能
编辑器·word
qq_205279052 天前
Unity TileMap 使用经验
unity·游戏引擎