Unity 编辑器扩展:打造一个可切换 Config(ScriptableObject)的顶部菜单插件
关键词:Unity 编辑器扩展、ScriptableObject、EditorWindow、自定义菜单、配置文件管理
文章目录
1、前言:从需求到想法的萌芽
在 Unity 的项目开发中,我们经常会遇到各种「配置文件」:
- 游戏参数配置
- 战斗数值配置
- UI 样式配置
- 网络服务端地址配置
这些配置往往存放在 ScriptableObject
的 .asset
文件中,以方便可视化编辑。
但是,随着项目的迭代,我们会发现一个问题:
👉 配置文件数量变多,每次要手动在 Project 窗口搜索、点击、打开,非常低效。
特别是团队协作时,测试、策划、程序员都可能需要修改配置。光是「找到文件」这一步,就要多点几次。
于是,萌生了一个想法:
能不能在 Unity 编辑器顶部菜单栏添加一个「配置管理」菜单?点击它就能打开一个面板,里面有下拉列表,直接切换不同的 Config 文件,并在面板中修改它的内容。
进一步优化:
- 第一次可以通过文件管理器选择一个配置文件。
- 之后自动保存「上一次选择的配置」,下次打开时就能直接使用。
这就是本文要实现的功能。
2、技术选型:Unity 编辑器扩展的武器库
需求流程:
为了实现上述需求,我们需要掌握以下 Unity 编辑器扩展相关技术:
EditorWindow
- Unity 提供的编辑器自定义窗口基类,可以在菜单栏打开一个专属面板。
MenuItem
- 可以在 Unity 顶部菜单栏注册自定义菜单。
ScriptableObject
- 作为配置文件的载体,可以序列化保存为
.asset
文件,天然适合做 Config。
- 作为配置文件的载体,可以序列化保存为
EditorGUILayout
- 用于在编辑器面板中绘制 UI,例如下拉框、按钮、对象选择器等。
EditorPrefs
- Unity 提供的本地偏好设置存储,可以保存简单的 key-value 数据(比如用户选择的上次 Config 文件路径)。
AssetDatabase
- 用于加载、查找
.asset
文件资源。
- 用于加载、查找
类图:
3、原理解析:从输入到持久化
在动手写代码前,我们先理一下「实现原理」:
入口
- 在 Unity 顶部菜单栏添加一个菜单项,例如:
Tools/Config Manager
。
- 在 Unity 顶部菜单栏添加一个菜单项,例如:
面板 UI
使用
EditorWindow
打开一个窗口。窗口中有:
- 一个 下拉框,显示所有已知 Config 文件的名字。
- 一个 按钮,点击后可通过文件管理器选择新的 Config 文件。
- 一个 配置内容编辑区,直接显示并可修改 Config 的字段。
配置文件识别
- Config 文件是
ScriptableObject
,我们通过AssetDatabase.FindAssets("t:Config")
找到所有同类型的.asset
文件。
- Config 文件是
默认配置记忆
- 使用
EditorPrefs.SetString("LastConfigPath", path)
保存上次选择的路径。 - 下次打开窗口时,从
EditorPrefs
读取该路径,并尝试加载对应的配置文件。
- 使用
编辑内容保存
- Unity 的
SerializedObject
+EditorGUILayout.PropertyField
可以动态绘制 ScriptableObject 的字段,并保证修改后能保存。
- Unity 的
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、使用体验:效果演示
交互流程:
在菜单栏点击
Tools/Config Manager
弹出一个面板:
- 上方下拉框显示已有的配置文件
- 点击可切换不同 Config
- 右侧按钮可打开文件管理器,手动选择新的 Config
下方面板显示所选 Config 的所有字段,可以直接修改
修改后 Unity 自动保存到对应的
.asset
文件
6、总结与拓展
通过这次实战,我们实现了一个 Unity 编辑器扩展插件,它解决了以下问题:
- 配置文件散落在 Project 中 → 统一入口,集中管理
- 每次要手动搜索配置 → 一键下拉切换
- 修改不方便 → 在面板中直接可视化编辑
核心技术包括:
EditorWindow
自定义窗口MenuItem
注册菜单SerializedObject
保证 Unity 序列化EditorPrefs
保存用户默认配置AssetDatabase
搜索和加载.asset
文件
可能的扩展方向:
- 支持多类型 Config(不同的 ScriptableObject 类型)
- 添加「搜索框」快速过滤 Config
- 添加「收藏夹」功能,常用 Config 放到置顶位置
- 结合
EditorGUILayout.Toolbar
做更美观的 UI