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

发布于:2025-09-09 ⋅ 阅读:(14) ⋅ 点赞:(0)

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、代码实现:完整流程

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

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

using UnityEngine;

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

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

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


网站公告

今日签到

点亮在社区的每一天
去签到