【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

相关推荐
wincheshe8 小时前
React Native inspector 点击组件跳转编辑器技术详解
react native·react.js·编辑器
孟无岐1 天前
【Laya】Laya 类使用说明
typescript·游戏引擎·游戏程序·laya
在路上看风景1 天前
1.2 Unity资源分类
unity·游戏引擎
one named slash1 天前
BMFont在Unity中生成艺术字
unity·游戏引擎
微醺的老虎1 天前
【工具】vscode格式化json文件
ide·vscode·编辑器
乔宕一1 天前
vscode 设置每次调试 powershell 脚本都使用临时的 powershell 终端
ide·vscode·编辑器
郝学胜-神的一滴1 天前
图形学中的纹理映射问题:摩尔纹与毛刺的深度解析
c++·程序人生·unity·游戏引擎·图形渲染·unreal engine
在路上看风景1 天前
10. CPU-GPU协作渲染
unity
程序员agions1 天前
Unity 游戏开发邪修秘籍:从入门到被策划追杀的艺术
unity·cocoa·lucene
JIes__1 天前
Unity(一)——场景切换、退出游戏、鼠标隐藏锁定...
unity·游戏引擎