💾 Unity Editor 数据存储完全指南:从配置到资产的持久化方案

💡 数据管理的痛点

  • 编辑器工具的配置每次重启都丢失?
  • 想保存一些编辑器用户设置,不知道用什么方案?
  • ScriptableObject、EditorPrefs、JSON 该怎么选?
  • 数据序列化出问题,调试半天找不到原因?

别担心! 这篇文章将系统介绍 Unity 编辑器的数据存储方案,帮你选择最合适的持久化策略!

一、数据存储方案概览

Unity 编辑器提供了多种数据存储方式,各有适用场景:

方案 类型 适用场景 持久化位置
EditorPrefs 键值对 简单配置存储 注册表
EditorUserSettings 键值对/二进制 编辑器用户设置 配置文件
ScriptableObject 资产文件 数据资产、运行时数据库 Assets 文件夹
JSON 文本 数据交换、序列化 自定义路径

💡 选择建议:简单配置用 EditorPrefs,复杂数据用 ScriptableObject。


二、EditorPrefs 编辑器偏好设置

EditorPrefs 类似于运行时的 PlayerPrefs,用于存储编辑器相关的键值对数据。

2.1 常用 API

方法 说明
SetBool/SetInt/SetFloat/String 保存值
GetBool/GetInt/GetFloat/GetString 读取值
HasKey 检查键是否存在
DeleteKey 删除指定键
DeleteAll 清除所有数据

2.2 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEditor;

public class EditorPrefsExample
{
private const string WINDOW_POS_KEY = "MyWindow_Position";
private const string AUTO_SAVE_KEY = "MyWindow_AutoSave";

// 保存窗口位置
public static void SaveWindowPosition(Vector2 pos)
{
EditorPrefs.SetFloat(WINDOW_POS_KEY + "_X", pos.x);
EditorPrefs.SetFloat(WINDOW_POS_KEY + "_Y", pos.y);
}

// 读取窗口位置
public static Vector2 LoadWindowPosition()
{
float x = EditorPrefs.GetFloat(WINDOW_POS_KEY + "_X", 100);
float y = EditorPrefs.GetFloat(WINDOW_POS_KEY + "_Y", 100);
return new Vector2(x, y);
}

// 自动保存设置
public static bool AutoSave
{
get => EditorPrefs.GetBool(AUTO_SAVE_KEY, true);
set => EditorPrefs.SetBool(AUTO_SAVE_KEY, value);
}
}

2.3 注意事项

⚠️ EditorPrefs 数据存储在系统注册表中,谨慎使用,出现问题不官方不负责

💡 实际项目中,复杂配置建议使用 ScriptableObject 替代。


三、EditorUserSettings 用户设置

EditorUserSettings 用于存储编辑器的用户级配置数据。

3.1 API 方法

方法 说明
SetConfigValue(key, value) 保存配置值
GetConfigValue(key, defaultValue) 读取配置值

3.2 支持的数据类型

  • 基本类型:bool, int, float, string
  • 二进制数据:byte[]
  • 序列化对象

3.3 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEditor;

public class EditorUserSettingsExample
{
private const string PROJECT_SETTINGS_KEY = "Project_CustomSettings";

public class ProjectSettings
{
public bool enableAutoBuild = true;
public string buildPath = "Builds/";
public int buildVersion = 1;
}

public static void SaveSettings(ProjectSettings settings)
{
string json = JsonUtility.ToJson(settings);
EditorUserSettings.SetConfigValue(PROJECT_SETTINGS_KEY, json);
}

public static ProjectSettings LoadSettings()
{
string json = EditorUserSettings.GetConfigValue(PROJECT_SETTINGS_KEY);
if (string.IsNullOrEmpty(json))
{
return new ProjectSettings();
}
return JsonUtility.FromJson<ProjectSettings>(json);
}
}

四、JSON 序列化

Unity 内置了 JSON 序列化工具。

4.1 JsonUtility vs EditorJsonUtility

命名空间 说明
JsonUtility UnityEngine 运行时和编辑器通用
EditorJsonUtility UnityEditor 编辑器专用,支持更多类型

4.2 JsonUtility 基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;

public class PlayerData
{
public string playerName;
public int level;
public float[] position;
}

// 序列化
PlayerData data = new PlayerData
{
playerName = "Player1",
level = 10,
position = new float[] { 1.5f, 2.0f, 0f }
};
string json = JsonUtility.ToJson(data, prettyPrint: true);

// 反序列化
PlayerData loadedData = JsonUtility.FromJson<PlayerData>(json);

4.3 EditorJsonUtility 编辑器用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEditor;
using UnityEngine;

public class EditorJsonExample
{
// 支持序列化 Unity 对象
public static void SaveGameObject(GameObject obj, string path)
{
string json = EditorJsonUtility.ToJson(obj, prettyPrint: true);
System.IO.File.WriteAllText(path, json);
}

public static GameObject LoadGameObject(string path)
{
string json = System.IO.File.ReadAllText(path);
return EditorJsonUtility.FromJson<GameObject>(json);
}
}

4.4 注意事项

⚠️ Unity 内置 JSON 工具功能有限,复杂项目建议使用第三方库:

  • Newtonsoft.Json (Json.NET) - 功能最全
  • Utf8Json - 高性能
  • Jil - 高速序列化

五、ScriptableObject 数据资产

ScriptableObject 是 Unity 最常用的数据容器,可用于编辑器、文件、运行时数据库。

5.1 ScriptableObject 特点

特点 说明
资产文件 存储为 .asset 文件
支持 Inspector 可视化编辑数据
运行时加载 可在游戏中动态加载
版本控制友好 支持差异对比
跨场景共享 数据不随场景销毁

5.2 创建 ScriptableObject

方式一:CreateAssetMenu 菜单创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;

[CreateAssetMenu(menuName = "Tools/GameConfig", fileName = "GameConfig")]
public class GameConfig : ScriptableObject
{
[Header("游戏设置")]
[Range(1, 100)]
public int maxLevel = 50;

public bool enableDebugMode = false;

[Header("文本数据")]
public string[] tipTexts = new string[5];

[Header("颜色配置")]
public Color playerColor = Color.white;
public Color enemyColor = Color.red;
}

使用方式:Project 窗口右键 → Create → Tools → GameConfig

方式二:MenuItem 代码创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using UnityEngine;
using UnityEditor;

public class ExampleAsset : ScriptableObject
{
[Range(0, 10)]
public int number = 3;

public bool toggle = false;

public string[] texts = new string[5];

[MenuItem("Tools/Create ExampleAsset")]
static void CreateExampleAsset()
{
// 创建实例
var asset = CreateInstance<ExampleAsset>();
asset.number = 5;
asset.toggle = true;

// 保存为资产文件
string path = "Assets/Editor/ExampleAsset.asset";
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.Refresh();

// 选中创建的资产
Selection.activeObject = asset;
EditorGUIUtility.PingObject(asset);
}
}

5.3 读取 ScriptableObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using UnityEditor;

public class ScriptableObjectLoader
{
private const string ASSET_PATH = "Assets/Editor/ExampleAsset.asset";

// 在编辑器中加载
public static ExampleAsset LoadAsset()
{
return AssetDatabase.LoadAssetAtPath<ExampleAsset>(ASSET_PATH);
}

// 在运行时加载(文件必须在 Resources 文件夹下)
public static GameConfig LoadRuntime()
{
return Resources.Load<GameConfig>("GameConfig");
}
}

5.4 数据展示与修改

ScriptableObject 的字段在 Inspector 中自动显示,可使用 [SerializeField] 控制可见性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;

public class PlayerDataAsset : ScriptableObject
{
// Inspector 中显示并可编辑
[SerializeField]
public string playerName;

// Inspector 中显示但设为只读
[SerializeField]
private int uniqueId;

// Inspector 中隐藏
[HideInInspector]
public string internalData;

// 完全不序列化
[System.NonSerialized]
public int runtimeValue;
}

六、ScriptableObject 父子关系

ScriptableObject 支持嵌套结构,但需要注意数据持久化问题。

6.1 基本嵌套结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;

public class ChildScriptableObject : ScriptableObject
{
[SerializeField]
private string childName;

public ChildScriptableObject()
{
childName = "Default Child";
name = "New ChildScriptableObject";
}
}

public class ParentScriptableObject : ScriptableObject
{
[SerializeField]
private ChildScriptableObject child;
}

6.2 正确保存嵌套数据

⚠️ 问题:直接创建子对象实例,重启 Unity 会丢失数据。

💡 解决:使用 AddObjectToAsset 建立资产父子关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using UnityEngine;
using UnityEditor;

public class ParentScriptableObject : ScriptableObject
{
private const string PATH = "Assets/Editor/ParentScriptableObject.asset";

[SerializeField]
private ChildScriptableObject child;

[MenuItem("Assets/Create Nested ScriptableObject")]
static void CreateNestedScriptableObject()
{
// 创建父对象
var parent = CreateInstance<ParentScriptableObject>();

// 创建子对象
parent.child = CreateInstance<ChildScriptableObject>();
parent.child.name = "Child Data";

// 关键:将子对象添加到父资产中
AssetDatabase.AddObjectToAsset(parent.child, PATH);

// 可选:隐藏子资产,使其在 Project 窗口中不可见
parent.child.hideFlags = HideFlags.HideInHierarchy;

// 保存父资产
AssetDatabase.CreateAsset(parent, PATH);
AssetDatabase.ImportAsset(PATH);

Debug.Log($"创建成功: {PATH}");
}
}

6.3 HideFlags 选项

HideFlags 说明
None 正常显示
HideInHierarchy 在 Hierarchy 中隐藏
HideInInspector 在 Inspector 中隐藏
DontSaveInEditor 不保存到场景
DontSaveInBuild 不保存到构建
DontUnloadUnusedAsset 不自动卸载

6.4 显示/操作子资产

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using UnityEngine;
using UnityEditor;

public class SubAssetOperations
{
// 显示所有子资产
[MenuItem("Assets/Show All SubAssets")]
static void ShowSubAssets()
{
var path = AssetDatabase.GetAssetPath(Selection.activeObject);
foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(path))
{
asset.hideFlags = HideFlags.None;
}
AssetDatabase.ImportAsset(path);
}

// 删除子资产
[MenuItem("Assets/Delete SubAsset")]
static void DeleteSubAsset()
{
var parent = Selection.activeObject as ParentScriptableObject;
if (parent != null && parent.child != null)
{
// 必须使用 DestroyImmediate,且第二个参数为 true
Object.DestroyImmediate(parent.child, true);
parent.child = null;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(parent));
}
}
}

七、EditorWindow 与 ScriptableObject

EditorWindow 本身继承自 ScriptableObject,可以使用相同的数据持久化机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using UnityEngine;
using UnityEditor;

public class MyEditorWindow : EditorWindow
{
private SerializedObject serializedObject;
private SerializedProperty numberProp;

// 数据存储在窗口资产中
[SerializeField]
private int windowNumber = 0;

[MenuItem("Tools/My Window")]
static void ShowWindow()
{
var window = GetWindow<MyEditorWindow>("My Window");
window.Show();
}

private void OnEnable()
{
// 使用序列化对象管理数据
serializedObject = new SerializedObject(this);
numberProp = serializedObject.FindProperty("windowNumber");
}

private void OnGUI()
{
serializedObject.Update();

EditorGUILayout.LabelField("窗口数据", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(numberProp, new GUIContent("数字"));

serializedObject.ApplyModifiedProperties();
}
}

八、Inspector 中的数组展示

在自定义 Inspector 中展示数组/列表,使用 SerializedProperty

8.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(NodeGraph))]
public class NodeGraphInspector : Editor
{
private SerializedObject serializedObj;
private SerializedProperty nodePointsProp;

private void OnEnable()
{
serializedObj = new SerializedObject(target);
nodePointsProp = serializedObj.FindProperty("nodePoints");
}

public override void OnInspectorGUI()
{
serializedObj.Update();

// 绘制数组,true 表示显示子元素
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(
nodePointsProp,
new GUIContent("位置节点"),
true // includeChildren
);

if (EditorGUI.EndChangeCheck())
{
serializedObj.ApplyModifiedProperties();
}
}
}

// 目标类
public class NodeGraph : MonoBehaviour
{
public Vector3[] nodePoints = new Vector3[0];
}

8.2 数组操作 API

API 说明
arraySize 获取/设置数组大小
GetArrayElementAtIndex(index) 获取指定索引元素
InsertArrayElementAtIndex(index) 在指定位置插入元素
DeleteArrayElementAtIndex(index) 删除指定位置元素
ClearArray() 清空数组

8.3 自定义数组绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public override void OnInspectorGUI()
{
serializedObj.Update();

EditorGUILayout.LabelField("节点列表", EditorStyles.boldLabel);

// 显示数组大小
int size = nodePointsProp.arraySize;
EditorGUILayout.LabelField($"节点数量: {size}");

// 遍历绘制
for (int i = 0; i < size; i++)
{
SerializedProperty element = nodePointsProp.GetArrayElementAtIndex(i);
EditorGUILayout.PropertyField(element, new GUIContent($"节点 {i}"));
}

// 按钮
if (GUILayout.Button("添加节点"))
{
nodePointsProp.arraySize++;
}

if (size > 0 && GUILayout.Button("移除最后节点"))
{
nodePointsProp.arraySize--;
}

serializedObj.ApplyModifiedProperties();
}

九、完整示例:配置管理系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
using UnityEngine;
using UnityEditor;
using System.IO;

// 配置数据类
[CreateAssetMenu(menuName = "Tools/Project Config")]
public class ProjectConfig : ScriptableObject
{
[Header("构建配置")]
public string buildPath = "Builds/";
public string productName = "My Game";
public string version = "1.0.0";

[Header("编辑器设置")]
public bool autoSave = true;
public int autoSaveInterval = 300; // 秒

[Header("开发选项")]
public bool showDebugInfo = false;
public bool enableLog = true;
}

// 配置管理器
public class ConfigManager
{
private const string CONFIG_PATH = "Assets/Editor/ProjectConfig.asset";

private static ProjectConfig _instance;

public static ProjectConfig Instance
{
get
{
if (_instance == null)
{
_instance = AssetDatabase.LoadAssetAtPath<ProjectConfig>(CONFIG_PATH);
if (_instance == null)
{
_instance = CreateConfig();
}
}
return _instance;
}
}

private static ProjectConfig CreateConfig()
{
var config = CreateInstance<ProjectConfig>();
AssetDatabase.CreateAsset(config, CONFIG_PATH);
AssetDatabase.Refresh();
return config;
}

public static void SaveConfig()
{
EditorUtility.SetDirty(Instance);
AssetDatabase.SaveAssets();
}
}

// 配置编辑器窗口
public class ConfigEditorWindow : EditorWindow
{
private SerializedObject serializedConfig;

[MenuItem("Tools/Config Editor")]
static void ShowWindow()
{
GetWindow<ConfigEditorWindow>("配置编辑器");
}

private void OnGUI()
{
if (ConfigManager.Instance == null)
{
EditorGUILayout.HelpBox("配置文件不存在", MessageType.Warning);
return;
}

if (serializedConfig == null)
{
serializedConfig = new SerializedObject(ConfigManager.Instance);
}

serializedConfig.Update();

// 显示所有属性
SerializedProperty prop = serializedConfig.GetIterator();
prop.Next(true); // 跳过脚本字段
while (prop.NextVisible(false))
{
EditorGUILayout.PropertyField(prop, true);
}

serializedConfig.ApplyModifiedProperties();

// 保存按钮
if (GUILayout.Button("保存配置"))
{
ConfigManager.SaveConfig();
}
}
}

十、总结

本文介绍了 Unity 编辑器中的数据存储方案:

方案 优点 缺点 推荐场景
EditorPrefs 简单易用 存注册表,不官方负责 窗口位置等简单配置
EditorUserSettings 支持二进制 较少使用 用户级配置
ScriptableObject 可视化、版本控制友好 需要创建资产文件 数据资产、游戏配置
JSON 通用格式 Unity 内置功能有限 数据交换、导入导出

💡 最佳实践

  • 游戏配置数据 → ScriptableObject
  • 编辑器工具配置 → ScriptableObject + 自定义 Inspector
  • 窗口状态保存 → EditorPrefs
  • 数据导入导出 → JSON

下一篇将详细介绍 EditorGUI 的常用控件和方法


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com