mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
- 修复了配置模块文档中 GetConfig、SetConfig、WatchConfig 方法的泛型符号 - 修复了 ECS 模块文档中 ArchSystemAdapter 的泛型符号显示 - 修复了函数式编程文档中 Option 和 Result 类型的泛型符号 - 修复了序列化和存储模块中各类方法的泛型符号显示 - 修正了存储模块文档中的序列化系统链接路径 - 修复了函数式编程教程中 Option 和 Result 的泛型符号显示
19 KiB
19 KiB
title, description
| title | description |
|---|---|
| 存储系统详解 | 存储系统提供了灵活的文件存储和作用域隔离功能,支持跨平台数据持久化。 |
存储系统详解
概述
存储系统是 GFramework.Game 中用于管理文件存储的核心组件。它提供了统一的存储接口,支持键值对存储、作用域隔离、目录操作等功能,让你可以轻松实现游戏数据的持久化。
存储系统采用装饰器模式设计,通过 IStorage 接口定义统一的存储操作,FileStorage 提供基于文件系统的实现,ScopedStorage
提供作用域隔离功能。
主要特性:
- 统一的键值对存储接口
- 基于文件系统的持久化
- 作用域隔离和命名空间管理
- 线程安全的并发访问
- 支持同步和异步操作
- 目录和文件列举功能
- 路径安全防护
- 跨平台支持(包括 Godot)
核心概念
存储接口
IStorage 定义了统一的存储操作:
public interface IStorage : IUtility
{
// 检查键是否存在
bool Exists(string key);
Task<bool> ExistsAsync(string key);
// 读取数据
T Read<T>(string key);
T Read<T>(string key, T defaultValue);
Task<T> ReadAsync<T>(string key);
// 写入数据
void Write<T>(string key, T value);
Task WriteAsync<T>(string key, T value);
// 删除数据
void Delete(string key);
Task DeleteAsync(string key);
// 目录操作
Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "");
Task<IReadOnlyList<string>> ListFilesAsync(string path = "");
Task<bool> DirectoryExistsAsync(string path);
Task CreateDirectoryAsync(string path);
}
文件存储
FileStorage 是基于文件系统的存储实现:
- 将数据序列化后保存为文件
- 支持自定义文件扩展名(默认
.dat) - 使用细粒度锁保证线程安全
- 自动创建目录结构
- 防止路径遍历攻击
作用域存储
ScopedStorage 提供命名空间隔离:
- 为所有键添加前缀
- 支持嵌套作用域
- 透明包装底层存储
- 实现逻辑分组
存储类型
StorageKinds 枚举定义了不同的存储方式:
[Flags]
public enum StorageKinds
{
None = 0,
Local = 1 << 0, // 本地文件系统
Memory = 1 << 1, // 内存存储
Remote = 1 << 2, // 远程存储
Database = 1 << 3 // 数据库存储
}
基本用法
创建文件存储
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");
写入和读取数据
// 写入简单类型
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<int>("player_score");
string name = storage.Read<string>("player_name");
var loadedSettings = storage.Read<GameSettings>("settings");
// 读取数据(带默认值)
int highScore = storage.Read("high_score", 0);
异步操作
// 异步写入
await storage.WriteAsync("player_level", 10);
// 异步读取
int level = await storage.ReadAsync<int>("player_level");
// 异步检查存在
bool exists = await storage.ExistsAsync("player_level");
// 异步删除
await storage.DeleteAsync("player_level");
检查和删除
// 检查键是否存在
if (storage.Exists("player_score"))
{
Console.WriteLine("存档存在");
}
// 删除数据
storage.Delete("player_score");
// 异步检查
bool exists = await storage.ExistsAsync("player_score");
使用层级键
// 使用 / 分隔符创建层级结构
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<string>("player/profile/name");
int gold = storage.Read<int>("player/inventory/gold");
作用域存储
创建作用域存储
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<string>("name"); // 从 "player/name.dat" 读取
嵌套作用域
// 创建嵌套作用域
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"
多作用域隔离
// 创建不同作用域的存储
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<int>("level"); // 5
string gameLevel = gameStorage.Read<string>("level"); // "forest_area_1"
string settingsLevel = settingsStorage.Read<string>("level"); // "high"
高级用法
目录操作
// 列举子目录
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");
批量操作
public async Task SaveAllPlayerData(PlayerData player)
{
var playerStorage = new ScopedStorage(baseStorage, $"player_{player.Id}");
// 批量写入
var tasks = new List<Task>
{
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<PlayerData> LoadAllPlayerData(int playerId)
{
var playerStorage = new ScopedStorage(baseStorage, $"player_{playerId}");
// 批量读取
var tasks = new[]
{
playerStorage.ReadAsync<Profile>("profile"),
playerStorage.ReadAsync<Inventory>("inventory"),
playerStorage.ReadAsync<QuestData>("quests"),
playerStorage.ReadAsync<Achievements>("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
};
}
存储迁移
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<object>(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);
}
}
存储备份
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<object>(sourceKey);
await _storage.WriteAsync(targetKey, data);
}
var directories = await _storage.ListDirectoriesAsync(source);
foreach (var dir in directories)
{
await CopyDirectory($"{source}/{dir}", $"{target}/{dir}");
}
}
}
缓存层
public class CachedStorage : IStorage
{
private readonly IStorage _innerStorage;
private readonly ConcurrentDictionary<string, object> _cache = new();
public CachedStorage(IStorage innerStorage)
{
_innerStorage = innerStorage;
}
public T Read<T>(string key)
{
// 先从缓存读取
if (_cache.TryGetValue(key, out var cached))
{
return (T)cached;
}
// 从存储读取并缓存
var value = _innerStorage.Read<T>(key);
_cache[key] = value;
return value;
}
public void Write<T>(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 文件存储
using GFramework.Godot.storage;
// 创建 Godot 文件存储
var storage = new GodotFileStorage(serializer);
// 使用 user:// 路径(用户数据目录)
storage.Write("user://saves/slot1.dat", saveData);
var data = storage.Read<SaveData>("user://saves/slot1.dat");
// 使用 res:// 路径(资源目录,只读)
var config = storage.Read<Config>("res://config/default.json");
// 普通文件路径也支持
storage.Write("/tmp/temp_data.dat", tempData);
Godot 路径说明
// 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<Config>("res://data/config.json");
// 绝对路径
storage.Write("/home/user/game/data.dat", data);
最佳实践
-
使用作用域隔离不同类型的数据
✓ var playerStorage = new ScopedStorage(baseStorage, "player"); ✓ var settingsStorage = new ScopedStorage(baseStorage, "settings"); ✗ storage.Write("player_name", name); // 不使用作用域 -
使用异步操作避免阻塞
✓ await storage.WriteAsync("data", value); ✗ storage.Write("data", value); // 在 UI 线程中同步操作 -
读取时提供默认值
✓ int score = storage.Read("score", 0); ✗ int score = storage.Read<int>("score"); // 键不存在时抛异常 -
使用层级键组织数据
✓ storage.Write("player/inventory/gold", 1000); ✗ storage.Write("player_inventory_gold", 1000); -
处理存储异常
try { await storage.WriteAsync("data", value); } catch (IOException ex) { Logger.Error($"存储失败: {ex.Message}"); ShowErrorMessage("保存失败,请检查磁盘空间"); } -
定期清理过期数据
public async Task CleanupOldData(TimeSpan maxAge) { var files = await storage.ListFilesAsync("temp"); foreach (var file in files) { var data = await storage.ReadAsync<TimestampedData>($"temp/{file}"); if (DateTime.Now - data.Timestamp > maxAge) { await storage.DeleteAsync($"temp/{file}"); } } } -
使用合适的序列化器
// JSON - 可读性好,适合配置文件 var jsonStorage = new FileStorage(path, new JsonSerializer(), ".json"); // 二进制 - 性能好,适合大量数据 var binaryStorage = new FileStorage(path, new BinarySerializer(), ".dat");
常见问题
问题:如何实现跨平台存储路径?
解答:
使用 Environment.GetFolderPath 获取平台特定路径:
public static string GetStoragePath()
{
var appData = Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData);
return Path.Combine(appData, "MyGame", "Data");
}
var storage = new FileStorage(GetStoragePath(), serializer);
问题:存储系统是否线程安全?
解答:
是的,FileStorage 使用细粒度锁机制保证线程安全:
// 不同键的操作可以并发执行
Task.Run(() => storage.Write("key1", value1));
Task.Run(() => storage.Write("key2", value2));
// 相同键的操作会串行化
Task.Run(() => storage.Write("key", value1));
Task.Run(() => storage.Write("key", value2)); // 等待第一个完成
问题:如何实现存储加密?
解答: 创建加密存储包装器:
public class EncryptedStorage : IStorage
{
private readonly IStorage _innerStorage;
private readonly IEncryption _encryption;
public void Write<T>(string key, T value)
{
var json = JsonSerializer.Serialize(value);
var encrypted = _encryption.Encrypt(json);
_innerStorage.Write(key, encrypted);
}
public T Read<T>(string key)
{
var encrypted = _innerStorage.Read<byte[]>(key);
var json = _encryption.Decrypt(encrypted);
return JsonSerializer.Deserialize<T>(json);
}
}
问题:如何限制存储大小?
解答: 实现配额管理:
public class QuotaStorage : IStorage
{
private readonly IStorage _innerStorage;
private readonly long _maxSize;
private long _currentSize;
public void Write<T>(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;
}
}
问题:如何实现存储压缩?
解答: 使用压缩序列化器:
public class CompressedSerializer : ISerializer
{
private readonly ISerializer _innerSerializer;
public string Serialize<T>(T value)
{
var json = _innerSerializer.Serialize(value);
var bytes = Encoding.UTF8.GetBytes(json);
var compressed = Compress(bytes);
return Convert.ToBase64String(compressed);
}
public T Deserialize<T>(string data)
{
var compressed = Convert.FromBase64String(data);
var bytes = Decompress(compressed);
var json = Encoding.UTF8.GetString(bytes);
return _innerSerializer.Deserialize<T>(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();
}
}
问题:如何监控存储操作?
解答: 实现日志存储包装器:
public class LoggingStorage : IStorage
{
private readonly IStorage _innerStorage;
private readonly ILogger _logger;
public void Write<T>(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<T>(string key)
{
var stopwatch = Stopwatch.StartNew();
try
{
var value = _innerStorage.Read<T>(key);
_logger.Info($"读取成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
return value;
}
catch (Exception ex)
{
_logger.Error($"读取失败: {key}, 错误: {ex.Message}");
throw;
}
}
}