💾 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 { 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 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
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); } 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 { [SerializeField ] public string playerName; [SerializeField ] private int uniqueId; [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); 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 ) { 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(); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField( nodePointsProp, new GUIContent("位置节点" ), true ); 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