--- title: 存储系统详解 description: 存储系统提供了灵活的文件存储和作用域隔离功能,支持跨平台数据持久化。 --- # 存储系统详解 ## 概述 存储系统是 GFramework.Game 中用于管理文件存储的核心组件。它提供了统一的存储接口,支持键值对存储、作用域隔离、目录操作等功能,让你可以轻松实现游戏数据的持久化。 存储系统采用装饰器模式设计,通过 `IStorage` 接口定义统一的存储操作,`FileStorage` 提供基于文件系统的实现,`ScopedStorage` 提供作用域隔离功能。 **主要特性**: - 统一的键值对存储接口 - 基于文件系统的持久化 - 作用域隔离和命名空间管理 - 线程安全的并发访问 - 支持同步和异步操作 - 目录和文件列举功能 - 路径安全防护 - 跨平台支持(包括 Godot) ## 核心概念 ### 存储接口 `IStorage` 定义了统一的存储操作: ```csharp public interface IStorage : IUtility { // 检查键是否存在 bool Exists(string key); Task ExistsAsync(string key); // 读取数据 T Read(string key); T Read(string key, T defaultValue); Task ReadAsync(string key); // 写入数据 void Write(string key, T value); Task WriteAsync(string key, T value); // 删除数据 void Delete(string key); Task DeleteAsync(string key); // 目录操作 Task> ListDirectoriesAsync(string path = ""); Task> ListFilesAsync(string path = ""); Task DirectoryExistsAsync(string path); Task CreateDirectoryAsync(string path); } ``` ### 文件存储 `FileStorage` 是基于文件系统的存储实现: - 将数据序列化后保存为文件 - 支持自定义文件扩展名(默认 `.dat`) - 使用细粒度锁保证线程安全 - 自动创建目录结构 - 防止路径遍历攻击 ### 作用域存储 `ScopedStorage` 提供命名空间隔离: - 为所有键添加前缀 - 支持嵌套作用域 - 透明包装底层存储 - 实现逻辑分组 ### 存储类型 `StorageKinds` 枚举定义了不同的存储方式: ```csharp [Flags] public enum StorageKinds { None = 0, Local = 1 << 0, // 本地文件系统 Memory = 1 << 1, // 内存存储 Remote = 1 << 2, // 远程存储 Database = 1 << 3 // 数据库存储 } ``` ## 基本用法 ### 创建文件存储 ```csharp using GFramework.Game.storage; using GFramework.Game.serializer; // 创建序列化器 var serializer = new JsonSerializer(); // 创Windows 示例) var storage = new FileStorage(@"C:\MyGame\Data", serializer); // 或使用自定义扩展名 var storage = new FileStorage(@"C:\MyGame\Data", serializer, ".json"); ``` ### 写入和读取数据 ```csharp // 写入简单类型 storage.Write("player_score", 1000); storage.Write("player_name", "Alice"); // 写入复杂对象 var settings = new GameSettings { Volume = 0.8f, Difficulty = "Hard", Language = "zh-CN" }; storage.Write("settings", settings); // 读取数据 int score = storage.Read("player_score"); string name = storage.Read("player_name"); var loadedSettings = storage.Read("settings"); // 读取数据(带默认值) int highScore = storage.Read("high_score", 0); ``` ### 异步操作 ```csharp // 异步写入 await storage.WriteAsync("player_level", 10); // 异步读取 int level = await storage.ReadAsync("player_level"); // 异步检查存在 bool exists = await storage.ExistsAsync("player_level"); // 异步删除 await storage.DeleteAsync("player_level"); ``` ### 检查和删除 ```csharp // 检查键是否存在 if (storage.Exists("player_score")) { Console.WriteLine("存档存在"); } // 删除数据 storage.Delete("player_score"); // 异步检查 bool exists = await storage.ExistsAsync("player_score"); ``` ### 使用层级键 ```csharp // 使用 / 分隔符创建层级结构 storage.Write("player/profile/name", "Alice"); storage.Write("player/profile/level", 10); storage.Write("player/inventory/gold", 1000); // 文件结构: // Data/ // player/ // profile/ // name.dat // level.dat // inventory/ // gold.dat // 读取层级数据 string name = storage.Read("player/profile/name"); int gold = storage.Read("player/inventory/gold"); ``` ## 作用域存储 ### 创建作用域存储 ```csharp using GFramework.Game.storage; // 基于文件存储创建作用域存储 var baseStorage = new FileStorage(@"C:\MyGame\Data", serializer); var playerStorage = new ScopedStorage(baseStorage, "player"); // 所有操作都会添加 "player/" 前缀 playerStorage.Write("name", "Alice"); // 实际存储为 "player/name.dat" playerStorage.Write("level", 10); // 实际存储为 "player/level.dat" // 读取时也使用相同的前缀 string name = playerStorage.Read("name"); // 从 "player/name.dat" 读取 ``` ### 嵌套作用域 ```csharp // 创建嵌套作用域 var settingsStorage = new ScopedStorage(baseStorage, "settings"); var graphicsStorage = new ScopedStorage(settingsStorage, "graphics"); // 前缀变为 "settings/graphics/" graphicsStorage.Write("resolution", "1920x1080"); // 实际存储为 "settings/graphics/resolution.dat" // 或使用 Scope 方法 var audioStorage = settingsStorage.Scope("audio"); audioStorage.Write("volume", 0.8f); // 实际存储为 "settings/audio/volume.dat" ``` ### 多作用域隔离 ```csharp // 创建不同作用域的存储 var playerStorage = new ScopedStorage(baseStorage, "player"); var gameStorage = new ScopedStorage(baseStorage, "game"); var settingsStorage = new ScopedStorage(baseStorage, "settings"); // 在不同作用域中使用相同的键不会冲突 playerStorage.Write("level", 5); // player/level.dat gameStorage.Write("level", "forest_area_1"); // game/level.dat settingsStorage.Write("level", "high"); // settings/level.dat // 读取时各自独立 int playerLevel = playerStorage.Read("level"); // 5 string gameLevel = gameStorage.Read("level"); // "forest_area_1" string settingsLevel = settingsStorage.Read("level"); // "high" ``` ## 高级用法 ### 目录操作 ```csharp // 列举子目录 var directories = await storage.ListDirectoriesAsync("player"); foreach (var dir in directories) { Console.WriteLine($"目录: {dir}"); } // 列举文件 var files = await storage.ListFilesAsync("player/inventory"); foreach (var file in files) { Console.WriteLine($"文件: {file}"); } // 检查目录是否存在 bool exists = await storage.DirectoryExistsAsync("player/quests"); // 创建目录 await storage.CreateDirectoryAsync("player/achievements"); ``` ### 批量操作 ```csharp public async Task SaveAllPlayerData(PlayerData player) { var playerStorage = new ScopedStorage(baseStorage, $"player_{player.Id}"); // 批量写入 var tasks = new List { playerStorage.WriteAsync("profile", player.Profile), playerStorage.WriteAsync("inventory", player.Inventory), playerStorage.WriteAsync("quests", player.Quests), playerStorage.WriteAsync("achievements", player.Achievements) }; await Task.WhenAll(tasks); Console.WriteLine("所有玩家数据已保存"); } public async Task LoadAllPlayerData(int playerId) { var playerStorage = new ScopedStorage(baseStorage, $"player_{playerId}"); // 批量读取 var tasks = new[] { playerStorage.ReadAsync("profile"), playerStorage.ReadAsync("inventory"), playerStorage.ReadAsync("quests"), playerStorage.ReadAsync("achievements") }; await Task.WhenAll(tasks); return new PlayerData { Id = playerId, Profile = tasks[0].Result, Inventory = tasks[1].Result, Quests = tasks[2].Result, Achievements = tasks[3].Result }; } ``` ### 存储迁移 ```csharp public async Task MigrateStorage(IStorage oldStorage, IStorage newStorage, string path = "") { // 列举所有文件 var files = await oldStorage.ListFilesAsync(path); foreach (var file in files) { var key = string.IsNullOrEmpty(path) ? file : $"{path}/{file}"; // 读取旧数据 var data = await oldStorage.ReadAsync(key); // 写入新存储 await newStorage.WriteAsync(key, data); Console.WriteLine($"已迁移: {key}"); } // 递归处理子目录 var directories = await oldStorage.ListDirectoriesAsync(path); foreach (var dir in directories) { var subPath = string.IsNullOrEmpty(path) ? dir : $"{path}/{dir}"; await MigrateStorage(oldStorage, newStorage, subPath); } } ``` ### 存储备份 ```csharp public class StorageBackupSystem { private readonly IStorage _storage; private readonly string _backupPrefix = "backup"; public async Task CreateBackup(string sourcePath) { var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); var backupPath = $"{_backupPrefix}/{timestamp}"; await CopyDirectory(sourcePath, backupPath); Console.WriteLine($"备份已创建: {backupPath}"); } public async Task RestoreBackup(string backupName, string targetPath) { var backupPath = $"{_backupPrefix}/{backupName}"; if (!await _storage.DirectoryExistsAsync(backupPath)) { throw new DirectoryNotFoundException($"备份不存在: {backupName}"); } await CopyDirectory(backupPath, targetPath); Console.WriteLine($"已从备份恢复: {backupName}"); } private async Task CopyDirectory(string source, string target) { var files = await _storage.ListFilesAsync(source); foreach (var file in files) { var sourceKey = $"{source}/{file}"; var targetKey = $"{target}/{file}"; var data = await _storage.ReadAsync(sourceKey); await _storage.WriteAsync(targetKey, data); } var directories = await _storage.ListDirectoriesAsync(source); foreach (var dir in directories) { await CopyDirectory($"{source}/{dir}", $"{target}/{dir}"); } } } ``` ### 缓存层 ```csharp public class CachedStorage : IStorage { private readonly IStorage _innerStorage; private readonly ConcurrentDictionary _cache = new(); public CachedStorage(IStorage innerStorage) { _innerStorage = innerStorage; } public T Read(string key) { // 先从缓存读取 if (_cache.TryGetValue(key, out var cached)) { return (T)cached; } // 从存储读取并缓存 var value = _innerStorage.Read(key); _cache[key] = value; return value; } public void Write(string key, T value) { // 写入存储 _innerStorage.Write(key, value); // 更新缓存 _cache[key] = value; } public void Delete(string key) { _innerStorage.Delete(key); _cache.TryRemove(key, out _); } public void ClearCache() { _cache.Clear(); } } ``` ## Godot 集成 ### 使用 Godot 文件存储 ```csharp using GFramework.Godot.storage; // 创建 Godot 文件存储 var storage = new GodotFileStorage(serializer); // 使用 user:// 路径(用户数据目录) storage.Write("user://saves/slot1.dat", saveData); var data = storage.Read("user://saves/slot1.dat"); // 使用 res:// 路径(资源目录,只读) var config = storage.Read("res://config/default.json"); // 普通文件路径也支持 storage.Write("/tmp/temp_data.dat", tempData); ``` ### Godot 路径说明 ```csharp // user:// - 用户数据目录 // Windows: %APPDATA%/Godot/app_userdata/[project_name] // Linux: ~/.local/share/godot/app_userdata/[project_name] // macOS: ~/Library/Application Support/Godot/app_userdata/[project_name] storage.Write("user://save.dat", data); // res:// - 项目资源目录(只读) var config = storage.Read("res://data/config.json"); // 绝对路径 storage.Write("/home/user/game/data.dat", data); ``` ## 最佳实践 1. **使用作用域隔离不同类型的数据** ```csharp ✓ var playerStorage = new ScopedStorage(baseStorage, "player"); ✓ var settingsStorage = new ScopedStorage(baseStorage, "settings"); ✗ storage.Write("player_name", name); // 不使用作用域 ``` 2. **使用异步操作避免阻塞** ```csharp ✓ await storage.WriteAsync("data", value); ✗ storage.Write("data", value); // 在 UI 线程中同步操作 ``` 3. **读取时提供默认值** ```csharp ✓ int score = storage.Read("score", 0); ✗ int score = storage.Read("score"); // 键不存在时抛异常 ``` 4. **使用层级键组织数据** ```csharp ✓ storage.Write("player/inventory/gold", 1000); ✗ storage.Write("player_inventory_gold", 1000); ``` 5. **处理存储异常** ```csharp try { await storage.WriteAsync("data", value); } catch (IOException ex) { Logger.Error($"存储失败: {ex.Message}"); ShowErrorMessage("保存失败,请检查磁盘空间"); } ``` 6. **定期清理过期数据** ```csharp public async Task CleanupOldData(TimeSpan maxAge) { var files = await storage.ListFilesAsync("temp"); foreach (var file in files) { var data = await storage.ReadAsync($"temp/{file}"); if (DateTime.Now - data.Timestamp > maxAge) { await storage.DeleteAsync($"temp/{file}"); } } } ``` 7. **使用合适的序列化器** ```csharp // JSON - 可读性好,适合配置文件 var jsonStorage = new FileStorage(path, new JsonSerializer(), ".json"); // 二进制 - 性能好,适合大量数据 var binaryStorage = new FileStorage(path, new BinarySerializer(), ".dat"); ``` ## 常见问题 ### 问题:如何实现跨平台存储路径? **解答**: 使用 `Environment.GetFolderPath` 获取平台特定路径: ```csharp public static string GetStoragePath() { var appData = Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData); return Path.Combine(appData, "MyGame", "Data"); } var storage = new FileStorage(GetStoragePath(), serializer); ``` ### 问题:存储系统是否线程安全? **解答**: 是的,`FileStorage` 使用细粒度锁机制保证线程安全: ```csharp // 不同键的操作可以并发执行 Task.Run(() => storage.Write("key1", value1)); Task.Run(() => storage.Write("key2", value2)); // 相同键的操作会串行化 Task.Run(() => storage.Write("key", value1)); Task.Run(() => storage.Write("key", value2)); // 等待第一个完成 ``` ### 问题:如何实现存储加密? **解答**: 创建加密存储包装器: ```csharp public class EncryptedStorage : IStorage { private readonly IStorage _innerStorage; private readonly IEncryption _encryption; public void Write(string key, T value) { var json = JsonSerializer.Serialize(value); var encrypted = _encryption.Encrypt(json); _innerStorage.Write(key, encrypted); } public T Read(string key) { var encrypted = _innerStorage.Read(key); var json = _encryption.Decrypt(encrypted); return JsonSerializer.Deserialize(json); } } ``` ### 问题:如何限制存储大小? **解答**: 实现配额管理: ```csharp public class QuotaStorage : IStorage { private readonly IStorage _innerStorage; private readonly long _maxSize; private long _currentSize; public void Write(string key, T value) { var data = Serialize(value); var size = data.Length; if (_currentSize + size > _maxSize) { throw new InvalidOperationException("存储配额已满"); } _innerStorage.Write(key, value); _currentSize += size; } } ``` ### 问题:如何实现存储压缩? **解答**: 使用压缩序列化器: ```csharp public class CompressedSerializer : ISerializer { private readonly ISerializer _innerSerializer; public string Serialize(T value) { var json = _innerSerializer.Serialize(value); var bytes = Encoding.UTF8.GetBytes(json); var compressed = Compress(bytes); return Convert.ToBase64String(compressed); } public T Deserialize(string data) { var compressed = Convert.FromBase64String(data); var bytes = Decompress(compressed); var json = Encoding.UTF8.GetString(bytes); return _innerSerializer.Deserialize(json); } private byte[] Compress(byte[] data) { using var output = new MemoryStream(); using (var gzip = new GZipStream(output, CompressionMode.Compress)) { gzip.Write(data, 0, data.Length); } return output.ToArray(); } private byte[] Decompress(byte[] data) { using var input = new MemoryStream(data); using var gzip = new GZipStream(input, CompressionMode.Decompress); using var output = new MemoryStream(); gzip.CopyTo(output); return output.ToArray(); } } ``` ### 问题:如何监控存储操作? **解答**: 实现日志存储包装器: ```csharp public class LoggingStorage : IStorage { private readonly IStorage _innerStorage; private readonly ILogger _logger; public void Write(string key, T value) { var stopwatch = Stopwatch.StartNew(); try { _innerStorage.Write(key, value); _logger.Info($"写入成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms"); } catch (Exception ex) { _logger.Error($"写入失败: {key}, 错误: {ex.Message}"); throw; } } public T Read(string key) { var stopwatch = Stopwatch.StartNew(); try { var value = _innerStorage.Read(key); _logger.Info($"读取成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms"); return value; } catch (Exception ex) { _logger.Error($"读取失败: {key}, 错误: {ex.Message}"); throw; } } } ``` ## 相关文档 - [数据与存档系统](/zh-CN/game/data) - 数据持久化 - [序列化系统](/zh-CN/core/serializer) - 数据序列化 - [Godot 集成](/zh-CN/godot/index) - Godot 中的存储 - [存档系统教程](/zh-CN/tutorials/save-system) - 完整示例