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

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

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

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

评论 44
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EQ-雪梨蛋花汤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值