GFramework/docs/zh-CN/game/storage.md
GeWuYou fb14d7122c docs(style): 更新文档中的命名空间导入格式
- 将所有小写的命名空间导入更正为首字母大写格式
- 统一 GFramework 框架的命名空间引用规范
- 修复 core、ecs、godot 等模块的命名空间导入错误
- 标准化文档示例代码中的 using 语句格式
- 确保所有文档中的命名空间引用保持一致性
- 更新 global using 语句以匹配正确的命名空间格式
2026-03-10 07:18:49 +08:00

19 KiB
Raw Permalink Blame History

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&lt;T&gt;(string key);
    T Read&lt;T&gt;(string key, T defaultValue);
    Task&lt;T&gt; ReadAsync&lt;T&gt;(string key);

    // 写入数据
    void Write&lt;T&gt;(string key, T value);
    Task WriteAsync&lt;T&gt;(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&lt;T&gt;(string key)
    {
        // 先从缓存读取
        if (_cache.TryGetValue(key, out var cached))
        {
            return (T)cached;
        }

        // 从存储读取并缓存
        var value = _innerStorage.Read&lt;T&gt;(key);
        _cache[key] = value;
        return value;
    }

    public void Write&lt;T&gt;(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);

最佳实践

  1. 使用作用域隔离不同类型的数据

     var playerStorage = new ScopedStorage(baseStorage, "player");
     var settingsStorage = new ScopedStorage(baseStorage, "settings");
     storage.Write("player_name", name);  // 不使用作用域
    
  2. 使用异步操作避免阻塞

     await storage.WriteAsync("data", value);
     storage.Write("data", value);  // 在 UI 线程中同步操作
    
  3. 读取时提供默认值

     int score = storage.Read("score", 0);
     int score = storage.Read<int>("score");  // 键不存在时抛异常
    
  4. 使用层级键组织数据

     storage.Write("player/inventory/gold", 1000);
     storage.Write("player_inventory_gold", 1000);
    
  5. 处理存储异常

    try
    {
        await storage.WriteAsync("data", value);
    }
    catch (IOException ex)
    {
        Logger.Error($"存储失败: {ex.Message}");
        ShowErrorMessage("保存失败,请检查磁盘空间");
    }
    
  6. 定期清理过期数据

    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}");
            }
        }
    }
    
  7. 使用合适的序列化器

    // 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&lt;T&gt;(string key, T value)
    {
        var json = JsonSerializer.Serialize(value);
        var encrypted = _encryption.Encrypt(json);
        _innerStorage.Write(key, encrypted);
    }

    public T Read&lt;T&gt;(string key)
    {
        var encrypted = _innerStorage.Read<byte[]>(key);
        var json = _encryption.Decrypt(encrypted);
        return JsonSerializer.Deserialize&lt;T&gt;(json);
    }
}

问题:如何限制存储大小?

解答 实现配额管理:

public class QuotaStorage : IStorage
{
    private readonly IStorage _innerStorage;
    private readonly long _maxSize;
    private long _currentSize;

    public void Write&lt;T&gt;(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&lt;T&gt;(T value)
    {
        var json = _innerSerializer.Serialize(value);
        var bytes = Encoding.UTF8.GetBytes(json);
        var compressed = Compress(bytes);
        return Convert.ToBase64String(compressed);
    }

    public T Deserialize&lt;T&gt;(string data)
    {
        var compressed = Convert.FromBase64String(data);
        var bytes = Decompress(compressed);
        var json = Encoding.UTF8.GetString(bytes);
        return _innerSerializer.Deserialize&lt;T&gt;(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&lt;T&gt;(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&lt;T&gt;(string key)
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            var value = _innerStorage.Read&lt;T&gt;(key);
            _logger.Info($"读取成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
            return value;
        }
        catch (Exception ex)
        {
            _logger.Error($"读取失败: {key}, 错误: {ex.Message}");
            throw;
        }
    }
}

相关文档