mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
docs(tutorials): 添加系统实现教程并完善核心组件文档
- 新增协程系统、状态机、暂停系统、资源管理和存档系统教程 - 添加 Configuration 包详细使用说明文档 - 创建 ECS 系统集成指南,介绍 Arch.Core 集成方案 - 提供完整的组件定义、系统创建和实体管理示例 - 包含性能优化建议和最佳实践指导
This commit is contained in:
parent
b4ef62c731
commit
84d7408bef
938
docs/zh-CN/core/configuration.md
Normal file
938
docs/zh-CN/core/configuration.md
Normal file
@ -0,0 +1,938 @@
|
|||||||
|
# Configuration 包使用说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Configuration 包提供了线程安全的配置管理系统,支持类型安全的配置存储、访问、监听和持久化。配置管理器可以用于管理游戏设置、运行时参数、开发配置等各种键值对数据。
|
||||||
|
|
||||||
|
配置系统是 GFramework 架构中的实用工具(Utility),可以在架构的任何层级中使用,提供统一的配置管理能力。
|
||||||
|
|
||||||
|
## 核心接口
|
||||||
|
|
||||||
|
### IConfigurationManager
|
||||||
|
|
||||||
|
配置管理器接口,提供类型安全的配置存储和访问。所有方法都是线程安全的。
|
||||||
|
|
||||||
|
**核心方法:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 配置访问
|
||||||
|
T? GetConfig<T>(string key); // 获取配置值
|
||||||
|
T GetConfig<T>(string key, T defaultValue); // 获取配置值(带默认值)
|
||||||
|
void SetConfig<T>(string key, T value); // 设置配置值
|
||||||
|
bool HasConfig(string key); // 检查配置是否存在
|
||||||
|
bool RemoveConfig(string key); // 移除配置
|
||||||
|
void Clear(); // 清空所有配置
|
||||||
|
|
||||||
|
// 配置监听
|
||||||
|
IUnRegister WatchConfig<T>(string key, Action<T> onChange); // 监听配置变化
|
||||||
|
|
||||||
|
// 持久化
|
||||||
|
void LoadFromJson(string json); // 从 JSON 加载
|
||||||
|
string SaveToJson(); // 保存为 JSON
|
||||||
|
void LoadFromFile(string path); // 从文件加载
|
||||||
|
void SaveToFile(string path); // 保存到文件
|
||||||
|
|
||||||
|
// 工具方法
|
||||||
|
int Count { get; } // 获取配置数量
|
||||||
|
IEnumerable<string> GetAllKeys(); // 获取所有配置键
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心类
|
||||||
|
|
||||||
|
### ConfigurationManager
|
||||||
|
|
||||||
|
配置管理器实现,提供线程安全的配置存储和访问。
|
||||||
|
|
||||||
|
**特性:**
|
||||||
|
|
||||||
|
- 线程安全:所有公共方法都是线程安全的
|
||||||
|
- 类型安全:支持泛型类型的配置值
|
||||||
|
- 自动类型转换:支持基本类型的自动转换
|
||||||
|
- 配置监听:支持监听配置变化并触发回调
|
||||||
|
- JSON 持久化:支持 JSON 格式的配置加载和保存
|
||||||
|
|
||||||
|
**使用示例:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 创建配置管理器
|
||||||
|
var configManager = new ConfigurationManager();
|
||||||
|
|
||||||
|
// 设置配置
|
||||||
|
configManager.SetConfig("game.difficulty", "Normal");
|
||||||
|
configManager.SetConfig("audio.volume", 0.8f);
|
||||||
|
configManager.SetConfig("graphics.quality", 2);
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
var difficulty = configManager.GetConfig<string>("game.difficulty");
|
||||||
|
var volume = configManager.GetConfig<float>("audio.volume");
|
||||||
|
var quality = configManager.GetConfig<int>("graphics.quality");
|
||||||
|
|
||||||
|
// 使用默认值
|
||||||
|
var fov = configManager.GetConfig("graphics.fov", 90);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 1. 设置和获取配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GameSettings : IUtility
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config = new ConfigurationManager();
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
// 设置游戏配置
|
||||||
|
_config.SetConfig("player.name", "Player1");
|
||||||
|
_config.SetConfig("player.level", 1);
|
||||||
|
_config.SetConfig("player.experience", 0);
|
||||||
|
|
||||||
|
// 设置游戏选项
|
||||||
|
_config.SetConfig("options.showTutorial", true);
|
||||||
|
_config.SetConfig("options.language", "zh-CN");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetPlayerName()
|
||||||
|
{
|
||||||
|
return _config.GetConfig<string>("player.name") ?? "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetPlayerLevel()
|
||||||
|
{
|
||||||
|
return _config.GetConfig("player.level", 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查和移除配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConfigurationService
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
|
||||||
|
public ConfigurationService(IConfigurationManager config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetPlayerData()
|
||||||
|
{
|
||||||
|
// 检查配置是否存在
|
||||||
|
if (_config.HasConfig("player.name"))
|
||||||
|
{
|
||||||
|
_config.RemoveConfig("player.name");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config.HasConfig("player.level"))
|
||||||
|
{
|
||||||
|
_config.RemoveConfig("player.level");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 或者清空所有配置
|
||||||
|
// _config.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrintAllConfigs()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Total configs: {_config.Count}");
|
||||||
|
|
||||||
|
foreach (var key in _config.GetAllKeys())
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Key: {key}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 支持的数据类型
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class TypeExamples
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config = new ConfigurationManager();
|
||||||
|
|
||||||
|
public void SetupConfigs()
|
||||||
|
{
|
||||||
|
// 基本类型
|
||||||
|
_config.SetConfig("int.value", 42);
|
||||||
|
_config.SetConfig("float.value", 3.14f);
|
||||||
|
_config.SetConfig("double.value", 2.718);
|
||||||
|
_config.SetConfig("bool.value", true);
|
||||||
|
_config.SetConfig("string.value", "Hello");
|
||||||
|
|
||||||
|
// 复杂类型
|
||||||
|
_config.SetConfig("vector.position", new Vector3(1, 2, 3));
|
||||||
|
_config.SetConfig("list.items", new List<string> { "A", "B", "C" });
|
||||||
|
_config.SetConfig("dict.data", new Dictionary<string, int>
|
||||||
|
{
|
||||||
|
["key1"] = 1,
|
||||||
|
["key2"] = 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetConfigs()
|
||||||
|
{
|
||||||
|
var intValue = _config.GetConfig<int>("int.value");
|
||||||
|
var floatValue = _config.GetConfig<float>("float.value");
|
||||||
|
var boolValue = _config.GetConfig<bool>("bool.value");
|
||||||
|
var stringValue = _config.GetConfig<string>("string.value");
|
||||||
|
|
||||||
|
var position = _config.GetConfig<Vector3>("vector.position");
|
||||||
|
var items = _config.GetConfig<List<string>>("list.items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 1. 配置监听(热更新)
|
||||||
|
|
||||||
|
配置监听允许在配置值变化时自动触发回调,实现配置的热更新。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AudioManager : AbstractSystem
|
||||||
|
{
|
||||||
|
private IUnRegister _volumeWatcher;
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
var config = this.GetUtility<IConfigurationManager>();
|
||||||
|
|
||||||
|
// 监听音量配置变化
|
||||||
|
_volumeWatcher = config.WatchConfig<float>("audio.masterVolume", newVolume =>
|
||||||
|
{
|
||||||
|
UpdateMasterVolume(newVolume);
|
||||||
|
this.GetUtility<ILogger>()?.Info($"Master volume changed to: {newVolume}");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听音效开关
|
||||||
|
config.WatchConfig<bool>("audio.sfxEnabled", enabled =>
|
||||||
|
{
|
||||||
|
if (enabled)
|
||||||
|
EnableSoundEffects();
|
||||||
|
else
|
||||||
|
DisableSoundEffects();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMasterVolume(float volume)
|
||||||
|
{
|
||||||
|
// 更新音频引擎的主音量
|
||||||
|
AudioEngine.SetMasterVolume(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDestroy()
|
||||||
|
{
|
||||||
|
// 取消监听
|
||||||
|
_volumeWatcher?.UnRegister();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 多个监听器
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GraphicsManager : AbstractSystem
|
||||||
|
{
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
var config = this.GetUtility<IConfigurationManager>();
|
||||||
|
|
||||||
|
// 多个组件监听同一个配置
|
||||||
|
config.WatchConfig<int>("graphics.quality", quality =>
|
||||||
|
{
|
||||||
|
UpdateTextureQuality(quality);
|
||||||
|
});
|
||||||
|
|
||||||
|
config.WatchConfig<int>("graphics.quality", quality =>
|
||||||
|
{
|
||||||
|
UpdateShadowQuality(quality);
|
||||||
|
});
|
||||||
|
|
||||||
|
config.WatchConfig<int>("graphics.quality", quality =>
|
||||||
|
{
|
||||||
|
UpdatePostProcessing(quality);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTextureQuality(int quality) { }
|
||||||
|
private void UpdateShadowQuality(int quality) { }
|
||||||
|
private void UpdatePostProcessing(int quality) { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置持久化
|
||||||
|
|
||||||
|
#### 保存和加载 JSON
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConfigurationPersistence
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
private readonly string _configPath = "config/game_settings.json";
|
||||||
|
|
||||||
|
public ConfigurationPersistence(IConfigurationManager config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveConfiguration()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 保存到文件
|
||||||
|
_config.SaveToFile(_configPath);
|
||||||
|
Console.WriteLine($"Configuration saved to {_configPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to save configuration: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadConfiguration()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 从文件加载
|
||||||
|
if (File.Exists(_configPath))
|
||||||
|
{
|
||||||
|
_config.LoadFromFile(_configPath);
|
||||||
|
Console.WriteLine($"Configuration loaded from {_configPath}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("Configuration file not found, using defaults");
|
||||||
|
SetDefaultConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to load configuration: {ex.Message}");
|
||||||
|
SetDefaultConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDefaultConfiguration()
|
||||||
|
{
|
||||||
|
_config.SetConfig("audio.masterVolume", 1.0f);
|
||||||
|
_config.SetConfig("audio.musicVolume", 0.8f);
|
||||||
|
_config.SetConfig("audio.sfxVolume", 1.0f);
|
||||||
|
_config.SetConfig("graphics.quality", 2);
|
||||||
|
_config.SetConfig("graphics.fullscreen", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSON 字符串操作
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConfigurationExport
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
|
||||||
|
public ConfigurationExport(IConfigurationManager config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExportToJson()
|
||||||
|
{
|
||||||
|
// 导出为 JSON 字符串
|
||||||
|
return _config.SaveToJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ImportFromJson(string json)
|
||||||
|
{
|
||||||
|
// 从 JSON 字符串导入
|
||||||
|
_config.LoadFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShareConfiguration()
|
||||||
|
{
|
||||||
|
// 导出配置用于分享
|
||||||
|
var json = _config.SaveToJson();
|
||||||
|
|
||||||
|
// 可以通过网络发送、保存到剪贴板等
|
||||||
|
Clipboard.SetText(json);
|
||||||
|
Console.WriteLine("Configuration copied to clipboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 多环境配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class EnvironmentConfiguration
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
private readonly string _environment;
|
||||||
|
|
||||||
|
public EnvironmentConfiguration(IConfigurationManager config, string environment)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadEnvironmentConfig()
|
||||||
|
{
|
||||||
|
// 根据环境加载不同的配置文件
|
||||||
|
var configPath = _environment switch
|
||||||
|
{
|
||||||
|
"development" => "config/dev.json",
|
||||||
|
"staging" => "config/staging.json",
|
||||||
|
"production" => "config/prod.json",
|
||||||
|
_ => "config/default.json"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
{
|
||||||
|
_config.LoadFromFile(configPath);
|
||||||
|
Console.WriteLine($"Loaded {_environment} configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置环境特定的配置
|
||||||
|
ApplyEnvironmentOverrides();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyEnvironmentOverrides()
|
||||||
|
{
|
||||||
|
switch (_environment)
|
||||||
|
{
|
||||||
|
case "development":
|
||||||
|
_config.SetConfig("debug.enabled", true);
|
||||||
|
_config.SetConfig("logging.level", "Debug");
|
||||||
|
_config.SetConfig("api.endpoint", "http://localhost:3000");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "production":
|
||||||
|
_config.SetConfig("debug.enabled", false);
|
||||||
|
_config.SetConfig("logging.level", "Warning");
|
||||||
|
_config.SetConfig("api.endpoint", "https://api.production.com");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 配置验证
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConfigurationValidator
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
|
||||||
|
public ConfigurationValidator(IConfigurationManager config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ValidateConfiguration()
|
||||||
|
{
|
||||||
|
var isValid = true;
|
||||||
|
|
||||||
|
// 验证必需的配置项
|
||||||
|
if (!_config.HasConfig("game.version"))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Error: game.version is required");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证配置值范围
|
||||||
|
var volume = _config.GetConfig("audio.masterVolume", -1f);
|
||||||
|
if (volume < 0f || volume > 1f)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Error: audio.masterVolume must be between 0 and 1");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证配置类型
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var quality = _config.GetConfig<int>("graphics.quality");
|
||||||
|
if (quality < 0 || quality > 3)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Error: graphics.quality must be between 0 and 3");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.WriteLine("Error: graphics.quality must be an integer");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyConstraints()
|
||||||
|
{
|
||||||
|
// 自动修正超出范围的值
|
||||||
|
var volume = _config.GetConfig("audio.masterVolume", 1f);
|
||||||
|
if (volume < 0f) _config.SetConfig("audio.masterVolume", 0f);
|
||||||
|
if (volume > 1f) _config.SetConfig("audio.masterVolume", 1f);
|
||||||
|
|
||||||
|
var quality = _config.GetConfig("graphics.quality", 2);
|
||||||
|
if (quality < 0) _config.SetConfig("graphics.quality", 0);
|
||||||
|
if (quality > 3) _config.SetConfig("graphics.quality", 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 配置分组管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConfigurationGroups
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
|
||||||
|
public ConfigurationGroups(IConfigurationManager config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频配置组
|
||||||
|
public class AudioConfig
|
||||||
|
{
|
||||||
|
public float MasterVolume { get; set; } = 1.0f;
|
||||||
|
public float MusicVolume { get; set; } = 0.8f;
|
||||||
|
public float SfxVolume { get; set; } = 1.0f;
|
||||||
|
public bool Muted { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图形配置组
|
||||||
|
public class GraphicsConfig
|
||||||
|
{
|
||||||
|
public int Quality { get; set; } = 2;
|
||||||
|
public bool Fullscreen { get; set; } = true;
|
||||||
|
public int ResolutionWidth { get; set; } = 1920;
|
||||||
|
public int ResolutionHeight { get; set; } = 1080;
|
||||||
|
public bool VSync { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveAudioConfig(AudioConfig audio)
|
||||||
|
{
|
||||||
|
_config.SetConfig("audio.masterVolume", audio.MasterVolume);
|
||||||
|
_config.SetConfig("audio.musicVolume", audio.MusicVolume);
|
||||||
|
_config.SetConfig("audio.sfxVolume", audio.SfxVolume);
|
||||||
|
_config.SetConfig("audio.muted", audio.Muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioConfig LoadAudioConfig()
|
||||||
|
{
|
||||||
|
return new AudioConfig
|
||||||
|
{
|
||||||
|
MasterVolume = _config.GetConfig("audio.masterVolume", 1.0f),
|
||||||
|
MusicVolume = _config.GetConfig("audio.musicVolume", 0.8f),
|
||||||
|
SfxVolume = _config.GetConfig("audio.sfxVolume", 1.0f),
|
||||||
|
Muted = _config.GetConfig("audio.muted", false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveGraphicsConfig(GraphicsConfig graphics)
|
||||||
|
{
|
||||||
|
_config.SetConfig("graphics.quality", graphics.Quality);
|
||||||
|
_config.SetConfig("graphics.fullscreen", graphics.Fullscreen);
|
||||||
|
_config.SetConfig("graphics.resolutionWidth", graphics.ResolutionWidth);
|
||||||
|
_config.SetConfig("graphics.resolutionHeight", graphics.ResolutionHeight);
|
||||||
|
_config.SetConfig("graphics.vsync", graphics.VSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GraphicsConfig LoadGraphicsConfig()
|
||||||
|
{
|
||||||
|
return new GraphicsConfig
|
||||||
|
{
|
||||||
|
Quality = _config.GetConfig("graphics.quality", 2),
|
||||||
|
Fullscreen = _config.GetConfig("graphics.fullscreen", true),
|
||||||
|
ResolutionWidth = _config.GetConfig("graphics.resolutionWidth", 1920),
|
||||||
|
ResolutionHeight = _config.GetConfig("graphics.resolutionHeight", 1080),
|
||||||
|
VSync = _config.GetConfig("graphics.vsync", true)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 在架构中使用
|
||||||
|
|
||||||
|
### 注册为 Utility
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GameArchitecture : Architecture<GameArchitecture>
|
||||||
|
{
|
||||||
|
protected override void Init()
|
||||||
|
{
|
||||||
|
// 注册配置管理器
|
||||||
|
this.RegisterUtility<IConfigurationManager>(new ConfigurationManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在 System 中使用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SettingsSystem : AbstractSystem
|
||||||
|
{
|
||||||
|
private IConfigurationManager _config;
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
_config = this.GetUtility<IConfigurationManager>();
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
LoadSettings();
|
||||||
|
|
||||||
|
// 监听配置变化
|
||||||
|
_config.WatchConfig<string>("game.language", OnLanguageChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadSettings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_config.LoadFromFile("settings.json");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 使用默认设置
|
||||||
|
SetDefaultSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDefaultSettings()
|
||||||
|
{
|
||||||
|
_config.SetConfig("game.language", "en-US");
|
||||||
|
_config.SetConfig("game.difficulty", "Normal");
|
||||||
|
_config.SetConfig("audio.masterVolume", 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLanguageChanged(string newLanguage)
|
||||||
|
{
|
||||||
|
// 切换游戏语言
|
||||||
|
LocalizationManager.SetLanguage(newLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSettings()
|
||||||
|
{
|
||||||
|
_config.SaveToFile("settings.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在 Controller 中使用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SettingsController : IController
|
||||||
|
{
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void ApplyGraphicsSettings(int quality, bool fullscreen)
|
||||||
|
{
|
||||||
|
var config = this.GetUtility<IConfigurationManager>();
|
||||||
|
|
||||||
|
// 更新配置(会自动触发监听器)
|
||||||
|
config.SetConfig("graphics.quality", quality);
|
||||||
|
config.SetConfig("graphics.fullscreen", fullscreen);
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetToDefaults()
|
||||||
|
{
|
||||||
|
var config = this.GetUtility<IConfigurationManager>();
|
||||||
|
|
||||||
|
// 清空所有配置
|
||||||
|
config.Clear();
|
||||||
|
|
||||||
|
// 重新设置默认值
|
||||||
|
config.SetConfig("audio.masterVolume", 1.0f);
|
||||||
|
config.SetConfig("graphics.quality", 2);
|
||||||
|
config.SetConfig("game.language", "en-US");
|
||||||
|
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveSettings()
|
||||||
|
{
|
||||||
|
var config = this.GetUtility<IConfigurationManager>();
|
||||||
|
config.SaveToFile("settings.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 配置键命名规范
|
||||||
|
|
||||||
|
使用分层的点号命名法,便于组织和管理:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 推荐的命名方式
|
||||||
|
_config.SetConfig("audio.master.volume", 1.0f);
|
||||||
|
_config.SetConfig("audio.music.volume", 0.8f);
|
||||||
|
_config.SetConfig("graphics.quality.level", 2);
|
||||||
|
_config.SetConfig("graphics.resolution.width", 1920);
|
||||||
|
_config.SetConfig("player.stats.health", 100);
|
||||||
|
_config.SetConfig("player.stats.mana", 50);
|
||||||
|
|
||||||
|
// 避免的命名方式
|
||||||
|
_config.SetConfig("AudioMasterVolume", 1.0f); // 不使用驼峰命名
|
||||||
|
_config.SetConfig("vol", 1.0f); // 不使用缩写
|
||||||
|
_config.SetConfig("config_1", 1.0f); // 不使用无意义的名称
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用默认值
|
||||||
|
|
||||||
|
始终为 `GetConfig` 提供合理的默认值,避免空引用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 推荐
|
||||||
|
var volume = _config.GetConfig("audio.volume", 1.0f);
|
||||||
|
var quality = _config.GetConfig("graphics.quality", 2);
|
||||||
|
|
||||||
|
// 不推荐
|
||||||
|
var volume = _config.GetConfig<float>("audio.volume"); // 可能返回 0
|
||||||
|
if (volume == 0) volume = 1.0f; // 需要额外的检查
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置文件组织
|
||||||
|
|
||||||
|
将配置文件按环境和用途分类:
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── default.json # 默认配置
|
||||||
|
├── dev.json # 开发环境配置
|
||||||
|
├── staging.json # 测试环境配置
|
||||||
|
├── prod.json # 生产环境配置
|
||||||
|
└── user/
|
||||||
|
├── settings.json # 用户设置
|
||||||
|
└── keybindings.json # 键位绑定
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 配置安全
|
||||||
|
|
||||||
|
不要在配置中存储敏感信息:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 不要这样做
|
||||||
|
_config.SetConfig("api.key", "secret_key_12345");
|
||||||
|
_config.SetConfig("user.password", "password123");
|
||||||
|
|
||||||
|
// 应该使用专门的安全存储
|
||||||
|
SecureStorage.SetSecret("api.key", "secret_key_12345");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 监听器管理
|
||||||
|
|
||||||
|
及时注销不再需要的监听器,避免内存泄漏:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MySystem : AbstractSystem
|
||||||
|
{
|
||||||
|
private readonly List<IUnRegister> _watchers = new();
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
var config = this.GetUtility<IConfigurationManager>();
|
||||||
|
|
||||||
|
// 保存监听器引用
|
||||||
|
_watchers.Add(config.WatchConfig<float>("audio.volume", OnVolumeChanged));
|
||||||
|
_watchers.Add(config.WatchConfig<int>("graphics.quality", OnQualityChanged));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDestroy()
|
||||||
|
{
|
||||||
|
// 注销所有监听器
|
||||||
|
foreach (var watcher in _watchers)
|
||||||
|
{
|
||||||
|
watcher.UnRegister();
|
||||||
|
}
|
||||||
|
_watchers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 线程安全使用
|
||||||
|
|
||||||
|
虽然 `ConfigurationManager` 是线程安全的,但在多线程环境中仍需注意:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ThreadSafeConfigAccess
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
|
||||||
|
public void UpdateFromMultipleThreads()
|
||||||
|
{
|
||||||
|
// 可以安全地从多个线程访问
|
||||||
|
Parallel.For(0, 10, i =>
|
||||||
|
{
|
||||||
|
_config.SetConfig($"thread.{i}.value", i);
|
||||||
|
var value = _config.GetConfig($"thread.{i}.value", 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WatchFromMultipleThreads()
|
||||||
|
{
|
||||||
|
// 监听器回调可能在不同线程执行
|
||||||
|
_config.WatchConfig<int>("shared.value", newValue =>
|
||||||
|
{
|
||||||
|
// 确保线程安全的操作
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
UpdateSharedResource(newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
private void UpdateSharedResource(int value) { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 配置变更通知
|
||||||
|
|
||||||
|
避免在配置监听器中触发大量的配置变更,可能导致循环调用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 不推荐:可能导致无限循环
|
||||||
|
_config.WatchConfig<int>("value.a", a =>
|
||||||
|
{
|
||||||
|
_config.SetConfig("value.b", a + 1); // 触发 b 的监听器
|
||||||
|
});
|
||||||
|
|
||||||
|
_config.WatchConfig<int>("value.b", b =>
|
||||||
|
{
|
||||||
|
_config.SetConfig("value.a", b + 1); // 触发 a 的监听器
|
||||||
|
});
|
||||||
|
|
||||||
|
// 推荐:使用标志位避免循环
|
||||||
|
private bool _isUpdating = false;
|
||||||
|
|
||||||
|
_config.WatchConfig<int>("value.a", a =>
|
||||||
|
{
|
||||||
|
if (_isUpdating) return;
|
||||||
|
_isUpdating = true;
|
||||||
|
_config.SetConfig("value.b", a + 1);
|
||||||
|
_isUpdating = false;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q1: 配置值类型转换失败怎么办?
|
||||||
|
|
||||||
|
A: `ConfigurationManager` 会尝试自动转换类型,如果失败会返回默认值。建议使用带默认值的 `GetConfig` 方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 如果转换失败,返回默认值 1.0f
|
||||||
|
var volume = _config.GetConfig("audio.volume", 1.0f);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q2: 如何处理配置文件不存在的情况?
|
||||||
|
|
||||||
|
A: 使用 try-catch 捕获异常,并提供默认配置:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_config.LoadFromFile("settings.json");
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
// 使用默认配置
|
||||||
|
SetDefaultConfiguration();
|
||||||
|
// 保存默认配置
|
||||||
|
_config.SaveToFile("settings.json");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q3: 配置监听器何时被触发?
|
||||||
|
|
||||||
|
A: 只有当配置值真正发生变化时才会触发监听器。如果设置相同的值,监听器不会被触发:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
_config.SetConfig("key", 42);
|
||||||
|
|
||||||
|
_config.WatchConfig<int>("key", value =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Changed to: {value}");
|
||||||
|
});
|
||||||
|
|
||||||
|
_config.SetConfig("key", 42); // 不会触发(值未变化)
|
||||||
|
_config.SetConfig("key", 100); // 会触发
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q4: 如何实现配置的版本控制?
|
||||||
|
|
||||||
|
A: 可以在配置中添加版本号,并在加载时进行迁移:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConfigurationMigration
|
||||||
|
{
|
||||||
|
private readonly IConfigurationManager _config;
|
||||||
|
|
||||||
|
public void LoadAndMigrate(string path)
|
||||||
|
{
|
||||||
|
_config.LoadFromFile(path);
|
||||||
|
|
||||||
|
var version = _config.GetConfig("config.version", 1);
|
||||||
|
|
||||||
|
if (version < 2)
|
||||||
|
{
|
||||||
|
MigrateToV2();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 3)
|
||||||
|
{
|
||||||
|
MigrateToV3();
|
||||||
|
}
|
||||||
|
|
||||||
|
_config.SetConfig("config.version", 3);
|
||||||
|
_config.SaveToFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MigrateToV2()
|
||||||
|
{
|
||||||
|
// 迁移逻辑
|
||||||
|
if (_config.HasConfig("old.key"))
|
||||||
|
{
|
||||||
|
var value = _config.GetConfig<string>("old.key");
|
||||||
|
_config.SetConfig("new.key", value);
|
||||||
|
_config.RemoveConfig("old.key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MigrateToV3() { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q5: 配置管理器的性能如何?
|
||||||
|
|
||||||
|
A: `ConfigurationManager` 使用 `ConcurrentDictionary` 实现,具有良好的并发性能。但要注意:
|
||||||
|
|
||||||
|
- 避免频繁的文件 I/O 操作
|
||||||
|
- 监听器回调应保持轻量
|
||||||
|
- 大量配置项时考虑分组管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 推荐:批量更新后一次性保存
|
||||||
|
_config.SetConfig("key1", value1);
|
||||||
|
_config.SetConfig("key2", value2);
|
||||||
|
_config.SetConfig("key3", value3);
|
||||||
|
_config.SaveToFile("settings.json"); // 一次性保存
|
||||||
|
|
||||||
|
// 不推荐:每次更新都保存
|
||||||
|
_config.SetConfig("key1", value1);
|
||||||
|
_config.SaveToFile("settings.json");
|
||||||
|
_config.SetConfig("key2", value2);
|
||||||
|
_config.SaveToFile("settings.json");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关包
|
||||||
|
|
||||||
|
- [`architecture`](./architecture.md) - 配置管理器作为 Utility 注册到架构
|
||||||
|
- [`utility`](./utility.md) - 配置管理器实现 IUtility 接口
|
||||||
|
- [`events`](./events.md) - 配置变化可以触发事件
|
||||||
|
- [`logging`](./logging.md) - 配置管理器内部使用日志记录
|
||||||
1005
docs/zh-CN/core/ecs.md
Normal file
1005
docs/zh-CN/core/ecs.md
Normal file
File diff suppressed because it is too large
Load Diff
677
docs/zh-CN/core/functional.md
Normal file
677
docs/zh-CN/core/functional.md
Normal file
@ -0,0 +1,677 @@
|
|||||||
|
# 函数式编程指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
GFramework.Core 提供了一套完整的函数式编程工具,帮助开发者编写更安全、更简洁、更易维护的代码。函数式编程强调不可变性、纯函数和声明式编程风格,能够有效减少副作用,提高代码的可测试性和可组合性。
|
||||||
|
|
||||||
|
本模块提供以下核心功能:
|
||||||
|
|
||||||
|
- **Option 类型**:安全处理可能不存在的值,替代 null 引用
|
||||||
|
- **Result 类型**:优雅处理操作结果和错误,避免异常传播
|
||||||
|
- **管道操作**:构建流式的函数调用链
|
||||||
|
- **函数组合**:组合多个函数形成新函数
|
||||||
|
- **控制流扩展**:函数式风格的条件执行和重试机制
|
||||||
|
- **异步函数式编程**:支持异步操作的函数式封装
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### Option 类型
|
||||||
|
|
||||||
|
`Option<T>` 表示可能存在或不存在的值,用于替代 null 引用。它有两种状态:
|
||||||
|
|
||||||
|
- **Some**:包含一个值
|
||||||
|
- **None**:不包含值
|
||||||
|
|
||||||
|
使用 Option 可以在编译时强制处理"无值"的情况,避免空引用异常。
|
||||||
|
|
||||||
|
### Result 类型
|
||||||
|
|
||||||
|
`Result<T>` 表示操作的结果,可能是成功值或失败异常。它有三种状态:
|
||||||
|
|
||||||
|
- **Success**:操作成功,包含返回值
|
||||||
|
- **Faulted**:操作失败,包含异常信息
|
||||||
|
- **Bottom**:未初始化状态
|
||||||
|
|
||||||
|
Result 类型将错误处理显式化,避免使用异常进行流程控制。
|
||||||
|
|
||||||
|
### 管道操作
|
||||||
|
|
||||||
|
管道操作允许将值通过一系列函数进行转换,形成流式的调用链。这种风格使代码更易读,逻辑更清晰。
|
||||||
|
|
||||||
|
### 函数组合
|
||||||
|
|
||||||
|
函数组合是将多个简单函数组合成复杂函数的技术。通过组合,可以构建可复用的函数库,提高代码的模块化程度。
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### Option 基础
|
||||||
|
|
||||||
|
#### 创建 Option
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional;
|
||||||
|
|
||||||
|
// 创建包含值的 Option
|
||||||
|
var someValue = Option<int>.Some(42);
|
||||||
|
|
||||||
|
// 创建空 Option
|
||||||
|
var noneValue = Option<int>.None;
|
||||||
|
|
||||||
|
// 隐式转换
|
||||||
|
Option<string> name = "Alice"; // Some("Alice")
|
||||||
|
Option<string> empty = null; // None
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取值
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 使用默认值
|
||||||
|
var value1 = someValue.GetOrElse(0); // 42
|
||||||
|
var value2 = noneValue.GetOrElse(0); // 0
|
||||||
|
|
||||||
|
// 使用工厂函数(延迟计算)
|
||||||
|
var value3 = noneValue.GetOrElse(() => ExpensiveDefault());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 转换值
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Map:映射值到新类型
|
||||||
|
var option = Option<int>.Some(42);
|
||||||
|
var mapped = option.Map(x => x.ToString()); // Option<string>.Some("42")
|
||||||
|
|
||||||
|
// Bind:链式转换(单子绑定)
|
||||||
|
var result = Option<string>.Some("42")
|
||||||
|
.Bind(s => int.TryParse(s, out var i)
|
||||||
|
? Option<int>.Some(i)
|
||||||
|
: Option<int>.None);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 过滤值
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var option = Option<int>.Some(42);
|
||||||
|
var filtered = option.Filter(x => x > 0); // Some(42)
|
||||||
|
var filtered2 = option.Filter(x => x < 0); // None
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 模式匹配
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 返回值的模式匹配
|
||||||
|
var message = option.Match(
|
||||||
|
some: value => $"Value: {value}",
|
||||||
|
none: () => "No value"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 副作用的模式匹配
|
||||||
|
option.Match(
|
||||||
|
some: value => Console.WriteLine($"Value: {value}"),
|
||||||
|
none: () => Console.WriteLine("No value")
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Result 基础
|
||||||
|
|
||||||
|
#### 创建 Result
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional;
|
||||||
|
|
||||||
|
// 创建成功结果
|
||||||
|
var success = Result<int>.Succeed(42);
|
||||||
|
var success2 = Result<int>.Success(42); // 别名
|
||||||
|
|
||||||
|
// 创建失败结果
|
||||||
|
var failure = Result<int>.Fail(new Exception("Error"));
|
||||||
|
var failure2 = Result<int>.Failure("Error message");
|
||||||
|
|
||||||
|
// 隐式转换
|
||||||
|
Result<int> result = 42; // Success(42)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 安全执行
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 自动捕获异常
|
||||||
|
var result = Result<int>.Try(() => int.Parse("42"));
|
||||||
|
|
||||||
|
// 异步安全执行
|
||||||
|
var asyncResult = await ResultExtensions.TryAsync(async () =>
|
||||||
|
await GetDataAsync());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取值
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 失败时返回默认值
|
||||||
|
var value1 = result.IfFail(0);
|
||||||
|
|
||||||
|
// 失败时通过函数处理
|
||||||
|
var value2 = result.IfFail(ex =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error: {ex.Message}");
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 转换值
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Map:映射成功值
|
||||||
|
var mapped = result.Map(x => x * 2);
|
||||||
|
|
||||||
|
// Bind:链式转换
|
||||||
|
var bound = result.Bind(x => x > 0
|
||||||
|
? Result<string>.Succeed(x.ToString())
|
||||||
|
: Result<string>.Fail(new ArgumentException("Must be positive")));
|
||||||
|
|
||||||
|
// 异步映射
|
||||||
|
var asyncMapped = await result.MapAsync(async x =>
|
||||||
|
await ProcessAsync(x));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 模式匹配
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 返回值的模式匹配
|
||||||
|
var message = result.Match(
|
||||||
|
succ: value => $"Success: {value}",
|
||||||
|
fail: ex => $"Error: {ex.Message}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 副作用的模式匹配
|
||||||
|
result.Match(
|
||||||
|
succ: value => Console.WriteLine($"Success: {value}"),
|
||||||
|
fail: ex => Console.WriteLine($"Error: {ex.Message}")
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 管道操作
|
||||||
|
|
||||||
|
#### Pipe:管道转换
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional.pipe;
|
||||||
|
|
||||||
|
var result = 42
|
||||||
|
.Pipe(x => x * 2) // 84
|
||||||
|
.Pipe(x => x.ToString()) // "84"
|
||||||
|
.Pipe(s => $"Result: {s}"); // "Result: 84"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tap:副作用操作
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = GetUser()
|
||||||
|
.Tap(user => Console.WriteLine($"User: {user.Name}"))
|
||||||
|
.Tap(user => _logger.LogInfo($"Processing user {user.Id}"))
|
||||||
|
.Pipe(user => new UserDto { Id = user.Id, Name = user.Name });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Let:作用域转换
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var dto = GetUser().Let(user => new UserDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Name = user.Name,
|
||||||
|
Email = user.Email
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PipeIf:条件管道
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = 42.PipeIf(
|
||||||
|
predicate: x => x > 0,
|
||||||
|
ifTrue: x => $"Positive: {x}",
|
||||||
|
ifFalse: x => $"Non-positive: {x}"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 函数组合
|
||||||
|
|
||||||
|
#### Compose:函数组合
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional.functions;
|
||||||
|
|
||||||
|
Func<int, int> addOne = x => x + 1;
|
||||||
|
Func<int, int> multiplyTwo = x => x * 2;
|
||||||
|
|
||||||
|
// f(g(x)) = (x + 1) * 2
|
||||||
|
var composed = multiplyTwo.Compose(addOne);
|
||||||
|
var result = composed(5); // (5 + 1) * 2 = 12
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AndThen:链式组合
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// g(f(x)) = (x + 1) * 2
|
||||||
|
var chained = addOne.AndThen(multiplyTwo);
|
||||||
|
var result = chained(5); // (5 + 1) * 2 = 12
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Curry:柯里化
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Func<int, int, int> add = (x, y) => x + y;
|
||||||
|
var curriedAdd = add.Curry();
|
||||||
|
|
||||||
|
var add5 = curriedAdd(5);
|
||||||
|
var result = add5(3); // 8
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### Result 扩展操作
|
||||||
|
|
||||||
|
#### 链式副作用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional.result;
|
||||||
|
|
||||||
|
Result<int>.Succeed(42)
|
||||||
|
.OnSuccess(x => Console.WriteLine($"Value: {x}"))
|
||||||
|
.OnFailure(ex => Console.WriteLine($"Error: {ex.Message}"))
|
||||||
|
.Map(x => x * 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 验证约束
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = Result<int>.Succeed(42)
|
||||||
|
.Ensure(x => x > 0, "Value must be positive")
|
||||||
|
.Ensure(x => x < 100, "Value must be less than 100");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 聚合多个结果
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var results = new[]
|
||||||
|
{
|
||||||
|
Result<int>.Succeed(1),
|
||||||
|
Result<int>.Succeed(2),
|
||||||
|
Result<int>.Succeed(3)
|
||||||
|
};
|
||||||
|
|
||||||
|
var combined = results.Combine(); // Result<List<int>>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 控制流扩展
|
||||||
|
|
||||||
|
#### TakeIf:条件返回
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional.control;
|
||||||
|
|
||||||
|
var user = GetUser().TakeIf(u => u.IsActive); // 活跃用户或 null
|
||||||
|
|
||||||
|
// 值类型版本
|
||||||
|
var number = 42.TakeIfValue(x => x > 0); // 42 或 null
|
||||||
|
```
|
||||||
|
|
||||||
|
#### When:条件执行
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = 42
|
||||||
|
.When(x => x > 0, x => Console.WriteLine($"Positive: {x}"))
|
||||||
|
.When(x => x % 2 == 0, x => Console.WriteLine("Even"));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### RepeatUntil:重复执行
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = 1.RepeatUntil(
|
||||||
|
func: x => x * 2,
|
||||||
|
predicate: x => x >= 100,
|
||||||
|
maxIterations: 10
|
||||||
|
); // 128
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Retry:同步重试
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = ControlExtensions.Retry(
|
||||||
|
func: () => UnstableOperation(),
|
||||||
|
maxRetries: 3,
|
||||||
|
delayMilliseconds: 100
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异步函数式编程
|
||||||
|
|
||||||
|
#### 异步重试
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional.async;
|
||||||
|
|
||||||
|
var result = await (() => UnreliableOperationAsync())
|
||||||
|
.WithRetryAsync(
|
||||||
|
maxRetries: 3,
|
||||||
|
delay: TimeSpan.FromSeconds(1),
|
||||||
|
shouldRetry: ex => ex is TimeoutException
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 异步安全执行
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await (() => RiskyOperationAsync()).TryAsync();
|
||||||
|
|
||||||
|
result.Match(
|
||||||
|
value => Console.WriteLine($"Success: {value}"),
|
||||||
|
error => Console.WriteLine($"Failed: {error.Message}")
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 异步绑定
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = Result<int>.Succeed(42);
|
||||||
|
var bound = await result.BindAsync(async x =>
|
||||||
|
await GetUserAsync(x) is User user
|
||||||
|
? Result<User>.Succeed(user)
|
||||||
|
: Result<User>.Fail(new Exception("User not found"))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级函数操作
|
||||||
|
|
||||||
|
#### Partial:偏函数应用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Func<int, int, int> add = (x, y) => x + y;
|
||||||
|
var add5 = add.Partial(5);
|
||||||
|
var result = add5(3); // 8
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Repeat:重复应用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = 2.Repeat(3, x => x * 2); // 2 * 2 * 2 * 2 = 16
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Once:单次执行
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var counter = 0;
|
||||||
|
var once = (() => ++counter).Once();
|
||||||
|
|
||||||
|
var result1 = once(); // 1
|
||||||
|
var result2 = once(); // 1(不会再次执行)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Defer:延迟执行
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var lazy = (() => ExpensiveComputation()).Defer();
|
||||||
|
// 此时尚未执行
|
||||||
|
var result = lazy.Value; // 首次访问时才执行
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Memoize:结果缓存
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Func<int, int> expensive = x =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
return x * x;
|
||||||
|
};
|
||||||
|
|
||||||
|
var memoized = expensive.MemoizeUnbounded();
|
||||||
|
var result1 = memoized(5); // 耗时 1 秒
|
||||||
|
var result2 = memoized(5); // 立即返回(从缓存)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 何时使用 Option
|
||||||
|
|
||||||
|
1. **替代 null 引用**:当函数可能返回空值时
|
||||||
|
2. **配置参数**:表示可选的配置项
|
||||||
|
3. **查找操作**:字典查找、数据库查询等可能失败的操作
|
||||||
|
4. **链式操作**:需要安全地链式调用多个可能返回空值的方法
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 不推荐:使用 null
|
||||||
|
public User? FindUser(int id)
|
||||||
|
{
|
||||||
|
return _users.ContainsKey(id) ? _users[id] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推荐:使用 Option
|
||||||
|
public Option<User> FindUser(int id)
|
||||||
|
{
|
||||||
|
return _users.TryGetValue(id, out var user)
|
||||||
|
? Option<User>.Some(user)
|
||||||
|
: Option<User>.None;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 何时使用 Result
|
||||||
|
|
||||||
|
1. **错误处理**:需要显式处理错误的操作
|
||||||
|
2. **验证逻辑**:输入验证、业务规则检查
|
||||||
|
3. **外部调用**:网络请求、文件操作等可能失败的 I/O 操作
|
||||||
|
4. **避免异常**:不希望使用异常进行流程控制的场景
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 不推荐:使用异常
|
||||||
|
public int ParseNumber(string input)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(input, out var number))
|
||||||
|
throw new ArgumentException("Invalid number");
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推荐:使用 Result
|
||||||
|
public Result<int> ParseNumber(string input)
|
||||||
|
{
|
||||||
|
return int.TryParse(input, out var number)
|
||||||
|
? Result<int>.Succeed(number)
|
||||||
|
: Result<int>.Failure("Invalid number");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 何时使用管道操作
|
||||||
|
|
||||||
|
1. **数据转换**:需要对数据进行多步转换
|
||||||
|
2. **流式处理**:构建数据处理管道
|
||||||
|
3. **副作用隔离**:使用 Tap 隔离副作用操作
|
||||||
|
4. **提高可读性**:使复杂的嵌套调用变得线性
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 不推荐:嵌套调用
|
||||||
|
var result = FormatResult(
|
||||||
|
ValidateInput(
|
||||||
|
ParseInput(
|
||||||
|
GetInput()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 推荐:管道操作
|
||||||
|
var result = GetInput()
|
||||||
|
.Pipe(ParseInput)
|
||||||
|
.Pipe(ValidateInput)
|
||||||
|
.Tap(x => _logger.LogInfo($"Validated: {x}"))
|
||||||
|
.Pipe(FormatResult);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理模式
|
||||||
|
|
||||||
|
#### 模式 1:早期返回
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Result<User> CreateUser(string name, string email)
|
||||||
|
{
|
||||||
|
return ValidateName(name)
|
||||||
|
.Bind(_ => ValidateEmail(email))
|
||||||
|
.Bind(_ => CheckDuplicate(email))
|
||||||
|
.Bind(_ => SaveUser(name, email));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 模式 2:聚合验证
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Result<User> CreateUser(UserDto dto)
|
||||||
|
{
|
||||||
|
var validations = new[]
|
||||||
|
{
|
||||||
|
ValidateName(dto.Name),
|
||||||
|
ValidateEmail(dto.Email),
|
||||||
|
ValidateAge(dto.Age)
|
||||||
|
};
|
||||||
|
|
||||||
|
return validations.Combine()
|
||||||
|
.Bind(_ => SaveUser(dto));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 模式 3:错误恢复
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Result<Data> GetData(int id)
|
||||||
|
{
|
||||||
|
return GetFromCache(id)
|
||||||
|
.Match(
|
||||||
|
succ: data => Result<Data>.Succeed(data),
|
||||||
|
fail: _ => GetFromDatabase(id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组合模式
|
||||||
|
|
||||||
|
#### 模式 1:Option + Result
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Result<User> GetActiveUser(int id)
|
||||||
|
{
|
||||||
|
return FindUser(id) // Option<User>
|
||||||
|
.ToResult("User not found") // Result<User>
|
||||||
|
.Ensure(u => u.IsActive, "User is not active");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 模式 2:Result + 管道
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Result<UserDto> ProcessUser(int id)
|
||||||
|
{
|
||||||
|
return Result<int>.Succeed(id)
|
||||||
|
.Bind(GetUser)
|
||||||
|
.Map(user => user.Tap(u => _logger.LogInfo($"Processing {u.Name}")))
|
||||||
|
.Map(user => new UserDto { Id = user.Id, Name = user.Name });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 模式 3:异步组合
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<Result<Response>> ProcessRequestAsync(Request request)
|
||||||
|
{
|
||||||
|
return await Result<Request>.Succeed(request)
|
||||||
|
.Ensure(r => r.IsValid, "Invalid request")
|
||||||
|
.BindAsync(async r => await ValidateAsync(r))
|
||||||
|
.BindAsync(async r => await ProcessAsync(r))
|
||||||
|
.MapAsync(async r => await FormatResponseAsync(r));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Option vs Nullable
|
||||||
|
|
||||||
|
**Q: Option 和 Nullable<T> 有什么区别?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
|
||||||
|
- `Nullable<T>` 只能用于值类型,`Option<T>` 可用于任何类型
|
||||||
|
- `Option<T>` 提供丰富的函数式操作(Map、Bind、Filter 等)
|
||||||
|
- `Option<T>` 强制显式处理"无值"情况,更安全
|
||||||
|
- `Option<T>` 可以与 Result 等其他函数式类型组合
|
||||||
|
|
||||||
|
### Result vs Exception
|
||||||
|
|
||||||
|
**Q: 什么时候应该使用 Result 而不是异常?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
|
||||||
|
- **使用 Result**:预期的错误情况(验证失败、资源不存在等)
|
||||||
|
- **使用 Exception**:意外的错误情况(系统错误、编程错误等)
|
||||||
|
- Result 使错误处理显式化,提高代码可读性
|
||||||
|
- Result 避免异常的性能开销
|
||||||
|
|
||||||
|
### 性能考虑
|
||||||
|
|
||||||
|
**Q: 函数式编程会影响性能吗?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
|
||||||
|
- Option 和 Result 是值类型(struct),性能开销很小
|
||||||
|
- 管道操作本质是方法调用,JIT 会进行内联优化
|
||||||
|
- Memoize 等缓存机制可以提高性能
|
||||||
|
- 对于性能敏感的代码,可以选择性使用函数式特性
|
||||||
|
|
||||||
|
### 与 LINQ 的关系
|
||||||
|
|
||||||
|
**Q: 函数式扩展与 LINQ 有什么区别?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
|
||||||
|
- LINQ 主要用于集合操作,函数式扩展用于单值操作
|
||||||
|
- 两者可以很好地组合使用
|
||||||
|
- Option 和 Result 可以转换为 IEnumerable 与 LINQ 集成
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Option 转 LINQ
|
||||||
|
var options = new[]
|
||||||
|
{
|
||||||
|
Option<int>.Some(1),
|
||||||
|
Option<int>.None,
|
||||||
|
Option<int>.Some(3)
|
||||||
|
};
|
||||||
|
|
||||||
|
var values = options
|
||||||
|
.SelectMany(o => o.ToEnumerable())
|
||||||
|
.ToList(); // [1, 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 学习曲线
|
||||||
|
|
||||||
|
**Q: 函数式编程难学吗?**
|
||||||
|
|
||||||
|
A:
|
||||||
|
|
||||||
|
- 从简单的 Option 和 Result 开始
|
||||||
|
- 逐步引入管道操作和函数组合
|
||||||
|
- 不需要一次性掌握所有特性
|
||||||
|
- 在实际项目中逐步应用,积累经验
|
||||||
|
|
||||||
|
## 参考资源
|
||||||
|
|
||||||
|
### 相关文档
|
||||||
|
|
||||||
|
- [Architecture 包使用说明](./architecture.md)
|
||||||
|
- [Extensions 扩展方法](./extensions.md)
|
||||||
|
- [CQRS 模式](./cqrs.md)
|
||||||
|
|
||||||
|
### 外部资源
|
||||||
|
|
||||||
|
- [函数式编程原理](https://en.wikipedia.org/wiki/Functional_programming)
|
||||||
|
- [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)
|
||||||
|
- [Option 类型模式](https://en.wikipedia.org/wiki/Option_type)
|
||||||
|
|
||||||
|
### 示例代码
|
||||||
|
|
||||||
|
完整的示例代码可以在测试项目中找到:
|
||||||
|
|
||||||
|
- `GFramework.Core.Tests/functional/OptionTests.cs`
|
||||||
|
- `GFramework.Core.Tests/functional/ResultTests.cs`
|
||||||
|
- `GFramework.Core.Tests/functional/pipe/PipeExtensionsTests.cs`
|
||||||
|
- `GFramework.Core.Tests/functional/functions/FunctionExtensionsTests.cs`
|
||||||
|
- `GFramework.Core.Tests/functional/control/ControlExtensionsTests.cs`
|
||||||
918
docs/zh-CN/core/pause.md
Normal file
918
docs/zh-CN/core/pause.md
Normal file
@ -0,0 +1,918 @@
|
|||||||
|
# 暂停管理系统使用说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
暂停管理系统(Pause System)提供了一套完整的游戏暂停控制机制,支持多层嵌套暂停、分组暂停、以及灵活的暂停处理器扩展。该系统基于栈结构实现,能够优雅地处理复杂的暂停场景,如菜单叠加、对话框弹出等。
|
||||||
|
|
||||||
|
暂停系统是 GFramework 架构中的核心工具(Utility),与其他系统协同工作,为游戏提供统一的暂停管理能力。
|
||||||
|
|
||||||
|
**主要特性:**
|
||||||
|
|
||||||
|
- **嵌套暂停**:支持多层暂停请求,只有所有请求都解除后才恢复
|
||||||
|
- **分组管理**:不同系统可以独立暂停(游戏逻辑、动画、音频等)
|
||||||
|
- **线程安全**:使用读写锁保证并发安全
|
||||||
|
- **作用域管理**:支持 `using` 语法自动管理暂停生命周期
|
||||||
|
- **事件通知**:状态变化时通知所有注册的处理器
|
||||||
|
- **优先级控制**:处理器按优先级顺序执行
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 暂停栈(Pause Stack)
|
||||||
|
|
||||||
|
暂停系统使用栈结构管理暂停请求。每次调用 `Push` 会将暂停请求压入栈中,调用 `Pop` 会从栈中移除对应的请求。只有当栈为空时,游戏才会恢复运行。
|
||||||
|
|
||||||
|
```
|
||||||
|
栈深度 3: [暂停原因: "库存界面"]
|
||||||
|
栈深度 2: [暂停原因: "对话框"]
|
||||||
|
栈深度 1: [暂停原因: "暂停菜单"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 暂停组(Pause Group)
|
||||||
|
|
||||||
|
暂停组允许不同系统独立控制暂停状态。例如,打开菜单时可以暂停游戏逻辑但保持 UI 动画运行。
|
||||||
|
|
||||||
|
**预定义组:**
|
||||||
|
|
||||||
|
- `Global` - 全局暂停(影响所有系统)
|
||||||
|
- `Gameplay` - 游戏逻辑暂停(不影响 UI)
|
||||||
|
- `Animation` - 动画暂停
|
||||||
|
- `Audio` - 音频暂停
|
||||||
|
- `Custom1/2/3` - 自定义组
|
||||||
|
|
||||||
|
### 暂停令牌(Pause Token)
|
||||||
|
|
||||||
|
每次暂停请求都会返回一个唯一的令牌,用于后续恢复操作。令牌基于 GUID 实现,确保唯一性。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public readonly struct PauseToken
|
||||||
|
{
|
||||||
|
public Guid Id { get; }
|
||||||
|
public bool IsValid => Id != Guid.Empty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 暂停处理器(Pause Handler)
|
||||||
|
|
||||||
|
处理器实现具体的暂停/恢复逻辑,如控制物理引擎、音频系统等。处理器按优先级顺序执行。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IPauseHandler
|
||||||
|
{
|
||||||
|
int Priority { get; } // 优先级(数值越小越高)
|
||||||
|
void OnPauseStateChanged(PauseGroup group, bool isPaused);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心接口
|
||||||
|
|
||||||
|
### IPauseStackManager
|
||||||
|
|
||||||
|
暂停栈管理器接口,提供暂停控制的所有功能。
|
||||||
|
|
||||||
|
**核心方法:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 推入暂停请求
|
||||||
|
PauseToken Push(string reason, PauseGroup group = PauseGroup.Global);
|
||||||
|
|
||||||
|
// 弹出暂停请求
|
||||||
|
bool Pop(PauseToken token);
|
||||||
|
|
||||||
|
// 查询暂停状态
|
||||||
|
bool IsPaused(PauseGroup group = PauseGroup.Global);
|
||||||
|
|
||||||
|
// 获取暂停深度
|
||||||
|
int GetPauseDepth(PauseGroup group = PauseGroup.Global);
|
||||||
|
|
||||||
|
// 获取暂停原因列表
|
||||||
|
IReadOnlyList<string> GetPauseReasons(PauseGroup group = PauseGroup.Global);
|
||||||
|
|
||||||
|
// 创建暂停作用域
|
||||||
|
IDisposable PauseScope(string reason, PauseGroup group = PauseGroup.Global);
|
||||||
|
|
||||||
|
// 清空指定组
|
||||||
|
void ClearGroup(PauseGroup group);
|
||||||
|
|
||||||
|
// 清空所有组
|
||||||
|
void ClearAll();
|
||||||
|
|
||||||
|
// 注册/注销处理器
|
||||||
|
void RegisterHandler(IPauseHandler handler);
|
||||||
|
void UnregisterHandler(IPauseHandler handler);
|
||||||
|
|
||||||
|
// 状态变化事件
|
||||||
|
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 1. 获取暂停管理器
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GameController : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
// 从架构中获取暂停管理器
|
||||||
|
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 简单的暂停/恢复
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PauseMenuController : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
private PauseToken _pauseToken;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenPauseMenu()
|
||||||
|
{
|
||||||
|
// 暂停游戏
|
||||||
|
_pauseToken = _pauseManager.Push("暂停菜单");
|
||||||
|
|
||||||
|
Console.WriteLine($"游戏已暂停,深度: {_pauseManager.GetPauseDepth()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClosePauseMenu()
|
||||||
|
{
|
||||||
|
// 恢复游戏
|
||||||
|
if (_pauseToken.IsValid)
|
||||||
|
{
|
||||||
|
_pauseManager.Pop(_pauseToken);
|
||||||
|
Console.WriteLine("游戏已恢复");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用作用域自动管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DialogController : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void ShowDialog(string message)
|
||||||
|
{
|
||||||
|
// 使用 using 语法,自动管理暂停生命周期
|
||||||
|
using (_pauseManager.PauseScope("对话框"))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"显示对话框: {message}");
|
||||||
|
// 对话框显示期间游戏暂停
|
||||||
|
WaitForUserInput();
|
||||||
|
}
|
||||||
|
// 离开作用域后自动恢复
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 查询暂停状态
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GameplaySystem : AbstractSystem
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(float deltaTime)
|
||||||
|
{
|
||||||
|
// 检查是否暂停
|
||||||
|
if (_pauseManager.IsPaused(PauseGroup.Gameplay))
|
||||||
|
{
|
||||||
|
return; // 暂停时跳过更新
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常游戏逻辑
|
||||||
|
UpdateGameLogic(deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 1. 嵌套暂停
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UIManager : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void ShowNestedMenus()
|
||||||
|
{
|
||||||
|
// 第一层:主菜单
|
||||||
|
var token1 = _pauseManager.Push("主菜单");
|
||||||
|
Console.WriteLine($"深度: {_pauseManager.GetPauseDepth()}"); // 输出: 1
|
||||||
|
|
||||||
|
// 第二层:设置菜单
|
||||||
|
var token2 = _pauseManager.Push("设置菜单");
|
||||||
|
Console.WriteLine($"深度: {_pauseManager.GetPauseDepth()}"); // 输出: 2
|
||||||
|
|
||||||
|
// 第三层:确认对话框
|
||||||
|
var token3 = _pauseManager.Push("确认对话框");
|
||||||
|
Console.WriteLine($"深度: {_pauseManager.GetPauseDepth()}"); // 输出: 3
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
_pauseManager.Pop(token3);
|
||||||
|
Console.WriteLine($"仍然暂停: {_pauseManager.IsPaused()}"); // 输出: True
|
||||||
|
|
||||||
|
// 关闭设置菜单
|
||||||
|
_pauseManager.Pop(token2);
|
||||||
|
Console.WriteLine($"仍然暂停: {_pauseManager.IsPaused()}"); // 输出: True
|
||||||
|
|
||||||
|
// 关闭主菜单
|
||||||
|
_pauseManager.Pop(token1);
|
||||||
|
Console.WriteLine($"已恢复: {!_pauseManager.IsPaused()}"); // 输出: True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 分组暂停
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GameManager : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void OpenInventory()
|
||||||
|
{
|
||||||
|
// 只暂停游戏逻辑,UI 和音频继续运行
|
||||||
|
var token = _pauseManager.Push("库存界面", PauseGroup.Gameplay);
|
||||||
|
|
||||||
|
Console.WriteLine($"游戏逻辑暂停: {_pauseManager.IsPaused(PauseGroup.Gameplay)}");
|
||||||
|
Console.WriteLine($"音频暂停: {_pauseManager.IsPaused(PauseGroup.Audio)}");
|
||||||
|
Console.WriteLine($"全局暂停: {_pauseManager.IsPaused(PauseGroup.Global)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenPauseMenu()
|
||||||
|
{
|
||||||
|
// 全局暂停,影响所有系统
|
||||||
|
var token = _pauseManager.Push("暂停菜单", PauseGroup.Global);
|
||||||
|
|
||||||
|
Console.WriteLine($"所有系统已暂停");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MuteAudio()
|
||||||
|
{
|
||||||
|
// 只暂停音频
|
||||||
|
var token = _pauseManager.Push("静音", PauseGroup.Audio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 自定义暂停处理器
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 物理引擎暂停处理器
|
||||||
|
public class PhysicsPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
private readonly PhysicsWorld _physicsWorld;
|
||||||
|
|
||||||
|
public PhysicsPauseHandler(PhysicsWorld physicsWorld)
|
||||||
|
{
|
||||||
|
_physicsWorld = physicsWorld;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高优先级,确保物理引擎最先暂停
|
||||||
|
public int Priority => 10;
|
||||||
|
|
||||||
|
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
// 只响应游戏逻辑和全局暂停
|
||||||
|
if (group == PauseGroup.Gameplay || group == PauseGroup.Global)
|
||||||
|
{
|
||||||
|
_physicsWorld.Enabled = !isPaused;
|
||||||
|
Console.WriteLine($"物理引擎 {(isPaused ? "已暂停" : "已恢复")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频系统暂停处理器
|
||||||
|
public class AudioPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
private readonly AudioSystem _audioSystem;
|
||||||
|
|
||||||
|
public AudioPauseHandler(AudioSystem audioSystem)
|
||||||
|
{
|
||||||
|
_audioSystem = audioSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Priority => 20;
|
||||||
|
|
||||||
|
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
// 响应音频和全局暂停
|
||||||
|
if (group == PauseGroup.Audio || group == PauseGroup.Global)
|
||||||
|
{
|
||||||
|
if (isPaused)
|
||||||
|
{
|
||||||
|
_audioSystem.PauseAll();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_audioSystem.ResumeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册处理器
|
||||||
|
public class GameInitializer
|
||||||
|
{
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
var pauseManager = architecture.GetUtility<IPauseStackManager>();
|
||||||
|
var physicsWorld = GetPhysicsWorld();
|
||||||
|
var audioSystem = GetAudioSystem();
|
||||||
|
|
||||||
|
// 注册处理器
|
||||||
|
pauseManager.RegisterHandler(new PhysicsPauseHandler(physicsWorld));
|
||||||
|
pauseManager.RegisterHandler(new AudioPauseHandler(audioSystem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 监听暂停状态变化
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PauseIndicator : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// 订阅状态变化事件
|
||||||
|
_pauseManager.OnPauseStateChanged += OnPauseStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"暂停状态变化: 组={group}, 暂停={isPaused}");
|
||||||
|
|
||||||
|
if (group == PauseGroup.Global)
|
||||||
|
{
|
||||||
|
if (isPaused)
|
||||||
|
{
|
||||||
|
ShowPauseIndicator();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HidePauseIndicator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 调试暂停状态
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PauseDebugger : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void PrintPauseStatus()
|
||||||
|
{
|
||||||
|
Console.WriteLine("=== 暂停状态 ===");
|
||||||
|
|
||||||
|
foreach (PauseGroup group in Enum.GetValues(typeof(PauseGroup)))
|
||||||
|
{
|
||||||
|
var isPaused = _pauseManager.IsPaused(group);
|
||||||
|
var depth = _pauseManager.GetPauseDepth(group);
|
||||||
|
var reasons = _pauseManager.GetPauseReasons(group);
|
||||||
|
|
||||||
|
Console.WriteLine($"\n组: {group}");
|
||||||
|
Console.WriteLine($" 状态: {(isPaused ? "暂停" : "运行")}");
|
||||||
|
Console.WriteLine($" 深度: {depth}");
|
||||||
|
|
||||||
|
if (reasons.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" 原因:");
|
||||||
|
foreach (var reason in reasons)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {reason}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 紧急恢复
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class EmergencyController : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void ForceResumeAll()
|
||||||
|
{
|
||||||
|
// 清空所有暂停请求(谨慎使用)
|
||||||
|
_pauseManager.ClearAll();
|
||||||
|
Console.WriteLine("已强制恢复所有系统");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ForceResumeGameplay()
|
||||||
|
{
|
||||||
|
// 只清空游戏逻辑组
|
||||||
|
_pauseManager.ClearGroup(PauseGroup.Gameplay);
|
||||||
|
Console.WriteLine("已强制恢复游戏逻辑");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 集成
|
||||||
|
|
||||||
|
### GodotPauseHandler
|
||||||
|
|
||||||
|
GFramework.Godot 提供了 Godot 引擎的暂停处理器实现:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GodotPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
private readonly SceneTree _tree;
|
||||||
|
|
||||||
|
public GodotPauseHandler(SceneTree tree)
|
||||||
|
{
|
||||||
|
_tree = tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Priority => 0;
|
||||||
|
|
||||||
|
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
// 只有 Global 组影响 Godot 的全局暂停
|
||||||
|
if (group == PauseGroup.Global)
|
||||||
|
{
|
||||||
|
_tree.Paused = isPaused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在 Godot 中使用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class GameRoot : Node
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 获取暂停管理器
|
||||||
|
_pauseManager = architecture.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// 注册 Godot 处理器
|
||||||
|
var godotHandler = new GodotPauseHandler(GetTree());
|
||||||
|
_pauseManager.RegisterHandler(godotHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPauseButtonPressed()
|
||||||
|
{
|
||||||
|
// 暂停游戏
|
||||||
|
_pauseManager.Push("玩家暂停", PauseGroup.Global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配合 ProcessMode
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class PauseMenu : Control
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 设置为 Always 模式,暂停时仍然处理输入
|
||||||
|
ProcessMode = ProcessModeEnum.Always;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Input(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (@event.IsActionPressed("ui_cancel"))
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
if (pauseManager.IsPaused())
|
||||||
|
{
|
||||||
|
// 恢复游戏
|
||||||
|
ResumeGame();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 暂停游戏
|
||||||
|
PauseGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 使用作用域管理
|
||||||
|
|
||||||
|
优先使用 `PauseScope` 而不是手动 `Push/Pop`,避免忘记恢复:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐
|
||||||
|
public void ShowDialog()
|
||||||
|
{
|
||||||
|
using (_pauseManager.PauseScope("对话框"))
|
||||||
|
{
|
||||||
|
// 对话框逻辑
|
||||||
|
}
|
||||||
|
// 自动恢复
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 不推荐
|
||||||
|
public void ShowDialog()
|
||||||
|
{
|
||||||
|
var token = _pauseManager.Push("对话框");
|
||||||
|
// 对话框逻辑
|
||||||
|
_pauseManager.Pop(token); // 容易忘记
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 提供清晰的暂停原因
|
||||||
|
|
||||||
|
暂停原因用于调试,应该清晰描述暂停来源:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐
|
||||||
|
_pauseManager.Push("主菜单 - 设置页面");
|
||||||
|
_pauseManager.Push("过场动画 - 关卡加载");
|
||||||
|
_pauseManager.Push("教程对话框 - 第一关");
|
||||||
|
|
||||||
|
// ❌ 不推荐
|
||||||
|
_pauseManager.Push("pause");
|
||||||
|
_pauseManager.Push("menu");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 合理选择暂停组
|
||||||
|
|
||||||
|
根据实际需求选择合适的暂停组:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 打开库存:只暂停游戏逻辑
|
||||||
|
_pauseManager.Push("库存界面", PauseGroup.Gameplay);
|
||||||
|
|
||||||
|
// 打开暂停菜单:全局暂停
|
||||||
|
_pauseManager.Push("暂停菜单", PauseGroup.Global);
|
||||||
|
|
||||||
|
// 播放过场动画:暂停游戏逻辑和输入
|
||||||
|
_pauseManager.Push("过场动画", PauseGroup.Gameplay);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 处理器优先级设计
|
||||||
|
|
||||||
|
合理设置处理器优先级,确保正确的执行顺序:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 物理引擎:高优先级(10),最先暂停
|
||||||
|
public class PhysicsPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
public int Priority => 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频系统:中优先级(20)
|
||||||
|
public class AudioPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
public int Priority => 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI 动画:低优先级(30),最后暂停
|
||||||
|
public class UiAnimationPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
public int Priority => 30;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 避免在处理器中抛出异常
|
||||||
|
|
||||||
|
处理器异常会被捕获并记录,但不会中断其他处理器:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SafePauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
public int Priority => 0;
|
||||||
|
|
||||||
|
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 可能失败的操作
|
||||||
|
RiskyOperation();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 记录错误但不抛出
|
||||||
|
Console.WriteLine($"暂停处理失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 线程安全考虑
|
||||||
|
|
||||||
|
暂停管理器是线程安全的,但处理器回调在主线程执行:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ThreadSafeUsage
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public void WorkerThread()
|
||||||
|
{
|
||||||
|
// ✅ 可以从任何线程调用
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
var token = _pauseManager.Push("后台任务");
|
||||||
|
// 执行任务
|
||||||
|
_pauseManager.Pop(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 清理资源
|
||||||
|
|
||||||
|
在组件销毁时注销处理器和事件:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ProperCleanup : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
private IPauseHandler _customHandler;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
_customHandler = new CustomPauseHandler();
|
||||||
|
|
||||||
|
_pauseManager.RegisterHandler(_customHandler);
|
||||||
|
_pauseManager.OnPauseStateChanged += OnPauseChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_pauseManager.UnregisterHandler(_customHandler);
|
||||||
|
_pauseManager.OnPauseStateChanged -= OnPauseChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPauseChanged(PauseGroup group, bool isPaused) { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q1: 为什么调用 Pop 后游戏还是暂停?
|
||||||
|
|
||||||
|
A: 暂停系统使用栈结构,只有当栈为空时才会恢复。检查是否有其他暂停请求:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 调试暂停状态
|
||||||
|
var depth = _pauseManager.GetPauseDepth();
|
||||||
|
var reasons = _pauseManager.GetPauseReasons();
|
||||||
|
|
||||||
|
Console.WriteLine($"当前暂停深度: {depth}");
|
||||||
|
Console.WriteLine("暂停原因:");
|
||||||
|
foreach (var reason in reasons)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {reason}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q2: 如何实现"暂停时显示菜单"?
|
||||||
|
|
||||||
|
A: 使用 Godot 的 `ProcessMode` 或监听暂停事件:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class PauseMenu : Control
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 方案 1: 设置为 Always 模式
|
||||||
|
ProcessMode = ProcessModeEnum.Always;
|
||||||
|
Visible = false;
|
||||||
|
|
||||||
|
// 方案 2: 监听暂停事件
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||||
|
{
|
||||||
|
if (group == PauseGroup.Global)
|
||||||
|
{
|
||||||
|
Visible = isPaused;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q3: 可以在暂停期间执行某些逻辑吗?
|
||||||
|
|
||||||
|
A: 可以,通过检查暂停状态或使用不同的暂停组:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SelectiveSystem : AbstractSystem
|
||||||
|
{
|
||||||
|
protected override void OnInit() { }
|
||||||
|
|
||||||
|
public void Update(float deltaTime)
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// 方案 1: 检查特定组
|
||||||
|
if (!pauseManager.IsPaused(PauseGroup.Gameplay))
|
||||||
|
{
|
||||||
|
UpdateGameplay(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI 始终更新(不检查暂停)
|
||||||
|
UpdateUI(deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q4: 如何实现"慢动作"效果?
|
||||||
|
|
||||||
|
A: 暂停系统控制是否执行,时间缩放需要使用时间系统:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SlowMotionController : IController
|
||||||
|
{
|
||||||
|
private ITimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void EnableSlowMotion()
|
||||||
|
{
|
||||||
|
// 使用时间缩放而不是暂停
|
||||||
|
_timeProvider.TimeScale = 0.3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisableSlowMotion()
|
||||||
|
{
|
||||||
|
_timeProvider.TimeScale = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q5: 暂停管理器的性能如何?
|
||||||
|
|
||||||
|
A: 暂停管理器使用读写锁优化并发性能:
|
||||||
|
|
||||||
|
- 查询操作(`IsPaused`)使用读锁,支持并发
|
||||||
|
- 修改操作(`Push/Pop`)使用写锁,互斥执行
|
||||||
|
- 事件通知在锁外执行,避免死锁
|
||||||
|
- 适合频繁查询、偶尔修改的场景
|
||||||
|
|
||||||
|
### Q6: 可以动态添加/移除暂停组吗?
|
||||||
|
|
||||||
|
A: 暂停组是枚举类型,不支持动态添加。可以使用自定义组:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 使用预定义的自定义组
|
||||||
|
_pauseManager.Push("特殊效果", PauseGroup.Custom1);
|
||||||
|
_pauseManager.Push("天气系统", PauseGroup.Custom2);
|
||||||
|
_pauseManager.Push("AI 系统", PauseGroup.Custom3);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q7: 如何处理异步操作中的暂停?
|
||||||
|
|
||||||
|
A: 使用 `PauseScope` 配合 `async/await`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AsyncPauseExample : IController
|
||||||
|
{
|
||||||
|
private IPauseStackManager _pauseManager;
|
||||||
|
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public async Task ShowAsyncDialog()
|
||||||
|
{
|
||||||
|
using (_pauseManager.PauseScope("异步对话框"))
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
Console.WriteLine("对话框显示中...");
|
||||||
|
await WaitForUserInput();
|
||||||
|
}
|
||||||
|
// 自动恢复
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 架构集成
|
||||||
|
|
||||||
|
### 在架构中注册
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GameArchitecture : Architecture<GameArchitecture>
|
||||||
|
{
|
||||||
|
protected override void OnRegisterUtility()
|
||||||
|
{
|
||||||
|
// 注册暂停管理器
|
||||||
|
RegisterUtility<IPauseStackManager>(new PauseStackManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
// 注册默认处理器
|
||||||
|
var pauseManager = GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// Godot 处理器
|
||||||
|
if (Engine.IsEditorHint() == false)
|
||||||
|
{
|
||||||
|
var tree = (GetTree() as SceneTree)!;
|
||||||
|
pauseManager.RegisterHandler(new GodotPauseHandler(tree));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 与其他系统协同
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 与事件系统配合
|
||||||
|
public class PauseEventBridge : AbstractSystem
|
||||||
|
{
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||||
|
{
|
||||||
|
// 发送暂停事件
|
||||||
|
this.SendEvent(new GamePausedEvent
|
||||||
|
{
|
||||||
|
Group = group,
|
||||||
|
IsPaused = isPaused
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与命令系统配合
|
||||||
|
public class PauseCommand : AbstractCommand
|
||||||
|
{
|
||||||
|
private readonly string _reason;
|
||||||
|
private readonly PauseGroup _group;
|
||||||
|
|
||||||
|
public PauseCommand(string reason, PauseGroup group = PauseGroup.Global)
|
||||||
|
{
|
||||||
|
_reason = reason;
|
||||||
|
_group = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExecute()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
pauseManager.Push(_reason, _group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关包
|
||||||
|
|
||||||
|
- [`architecture`](./architecture.md) - 架构核心,提供工具注册
|
||||||
|
- [`utility`](./utility.md) - 工具基类
|
||||||
|
- [`events`](./events.md) - 事件系统,用于状态通知
|
||||||
|
- [`lifecycle`](./lifecycle.md) - 生命周期管理
|
||||||
|
- [`logging`](./logging.md) - 日志系统,用于调试
|
||||||
|
- [Godot 集成](../godot/index.md) - Godot 引擎集成
|
||||||
756
docs/zh-CN/game/serialization.md
Normal file
756
docs/zh-CN/game/serialization.md
Normal file
@ -0,0 +1,756 @@
|
|||||||
|
---
|
||||||
|
title: 序列化系统
|
||||||
|
description: 序列化系统提供了统一的对象序列化和反序列化接口,支持 JSON 格式和运行时类型处理。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 序列化系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
序列化系统是 GFramework.Game 中用于对象序列化和反序列化的核心组件。它提供了统一的序列化接口,支持将对象转换为字符串格式(如
|
||||||
|
JSON)进行存储或传输,并能够将字符串数据还原为对象。
|
||||||
|
|
||||||
|
序列化系统与数据存储、配置管理、存档系统等模块深度集成,为游戏数据的持久化提供了基础支持。
|
||||||
|
|
||||||
|
**主要特性**:
|
||||||
|
|
||||||
|
- 统一的序列化接口
|
||||||
|
- JSON 格式支持
|
||||||
|
- 运行时类型序列化
|
||||||
|
- 泛型和非泛型 API
|
||||||
|
- 与存储系统无缝集成
|
||||||
|
- 类型安全的反序列化
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 序列化器接口
|
||||||
|
|
||||||
|
`ISerializer` 定义了基本的序列化操作:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface ISerializer : IUtility
|
||||||
|
{
|
||||||
|
// 将对象序列化为字符串
|
||||||
|
string Serialize<T>(T value);
|
||||||
|
|
||||||
|
// 将字符串反序列化为对象
|
||||||
|
T Deserialize<T>(string data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行时类型序列化器
|
||||||
|
|
||||||
|
`IRuntimeTypeSerializer` 扩展了基本接口,支持运行时类型处理:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IRuntimeTypeSerializer : ISerializer
|
||||||
|
{
|
||||||
|
// 使用运行时类型序列化对象
|
||||||
|
string Serialize(object obj, Type type);
|
||||||
|
|
||||||
|
// 使用运行时类型反序列化对象
|
||||||
|
object Deserialize(string data, Type type);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON 序列化器
|
||||||
|
|
||||||
|
`JsonSerializer` 是基于 Newtonsoft.Json 的实现:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class JsonSerializer : IRuntimeTypeSerializer
|
||||||
|
{
|
||||||
|
string Serialize<T>(T value);
|
||||||
|
T Deserialize<T>(string data);
|
||||||
|
string Serialize(object obj, Type type);
|
||||||
|
object Deserialize(string data, Type type);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 注册序列化器
|
||||||
|
|
||||||
|
在架构中注册序列化器:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.Abstractions.serializer;
|
||||||
|
using GFramework.Game.serializer;
|
||||||
|
|
||||||
|
public class GameArchitecture : Architecture
|
||||||
|
{
|
||||||
|
protected override void Init()
|
||||||
|
{
|
||||||
|
// 注册 JSON 序列化器
|
||||||
|
var jsonSerializer = new JsonSerializer();
|
||||||
|
RegisterUtility<ISerializer>(jsonSerializer);
|
||||||
|
RegisterUtility<IRuntimeTypeSerializer>(jsonSerializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 序列化对象
|
||||||
|
|
||||||
|
使用泛型 API 序列化对象:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PlayerData
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Level { get; set; }
|
||||||
|
public int Experience { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SaveController : IController
|
||||||
|
{
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public void SavePlayer()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
|
||||||
|
var player = new PlayerData
|
||||||
|
{
|
||||||
|
Name = "Player1",
|
||||||
|
Level = 10,
|
||||||
|
Experience = 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 序列化为 JSON 字符串
|
||||||
|
string json = serializer.Serialize(player);
|
||||||
|
Console.WriteLine(json);
|
||||||
|
// 输出: {"Name":"Player1","Level":10,"Experience":1000}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 反序列化对象
|
||||||
|
|
||||||
|
从字符串还原对象:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void LoadPlayer()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
|
||||||
|
string json = "{\"Name\":\"Player1\",\"Level\":10,\"Experience\":1000}";
|
||||||
|
|
||||||
|
// 反序列化为对象
|
||||||
|
var player = serializer.Deserialize<PlayerData>(json);
|
||||||
|
|
||||||
|
Console.WriteLine($"玩家: {player.Name}, 等级: {player.Level}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行时类型序列化
|
||||||
|
|
||||||
|
处理不确定类型的对象:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void SerializeRuntimeType()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||||
|
|
||||||
|
object data = new PlayerData { Name = "Player1", Level = 10 };
|
||||||
|
Type dataType = data.GetType();
|
||||||
|
|
||||||
|
// 使用运行时类型序列化
|
||||||
|
string json = serializer.Serialize(data, dataType);
|
||||||
|
|
||||||
|
// 使用运行时类型反序列化
|
||||||
|
object restored = serializer.Deserialize(json, dataType);
|
||||||
|
|
||||||
|
var player = restored as PlayerData;
|
||||||
|
Console.WriteLine($"玩家: {player?.Name}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 与存储系统集成
|
||||||
|
|
||||||
|
序列化器与存储系统配合使用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.Abstractions.storage;
|
||||||
|
using GFramework.Game.storage;
|
||||||
|
|
||||||
|
public class DataManager : IController
|
||||||
|
{
|
||||||
|
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||||
|
|
||||||
|
public async Task SaveData()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
var gameData = new GameData
|
||||||
|
{
|
||||||
|
Score = 1000,
|
||||||
|
Coins = 500
|
||||||
|
};
|
||||||
|
|
||||||
|
// 序列化数据
|
||||||
|
string json = serializer.Serialize(gameData);
|
||||||
|
|
||||||
|
// 写入存储
|
||||||
|
await storage.WriteAsync("game_data", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GameData> LoadData()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
// 从存储读取
|
||||||
|
string json = await storage.ReadAsync<string>("game_data");
|
||||||
|
|
||||||
|
// 反序列化数据
|
||||||
|
return serializer.Deserialize<GameData>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 序列化复杂对象
|
||||||
|
|
||||||
|
处理嵌套和集合类型:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class InventoryData
|
||||||
|
{
|
||||||
|
public List<ItemData> Items { get; set; }
|
||||||
|
public Dictionary<string, int> Resources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ItemData
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SerializeComplexData()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
|
||||||
|
var inventory = new InventoryData
|
||||||
|
{
|
||||||
|
Items = new List<ItemData>
|
||||||
|
{
|
||||||
|
new ItemData { Id = "sword_01", Name = "铁剑", Quantity = 1 },
|
||||||
|
new ItemData { Id = "potion_hp", Name = "生命药水", Quantity = 5 }
|
||||||
|
},
|
||||||
|
Resources = new Dictionary<string, int>
|
||||||
|
{
|
||||||
|
{ "gold", 1000 },
|
||||||
|
{ "wood", 500 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 序列化复杂对象
|
||||||
|
string json = serializer.Serialize(inventory);
|
||||||
|
|
||||||
|
// 反序列化
|
||||||
|
var restored = serializer.Deserialize<InventoryData>(json);
|
||||||
|
|
||||||
|
Console.WriteLine($"物品数量: {restored.Items.Count}");
|
||||||
|
Console.WriteLine($"金币: {restored.Resources["gold"]}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 处理多态类型
|
||||||
|
|
||||||
|
序列化继承层次结构:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public abstract class EntityData
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerEntityData : EntityData
|
||||||
|
{
|
||||||
|
public int Level { get; set; }
|
||||||
|
public int Experience { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EnemyEntityData : EntityData
|
||||||
|
{
|
||||||
|
public int Health { get; set; }
|
||||||
|
public int Damage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SerializePolymorphic()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||||
|
|
||||||
|
// 创建不同类型的实体
|
||||||
|
EntityData player = new PlayerEntityData
|
||||||
|
{
|
||||||
|
Id = "player_1",
|
||||||
|
Type = "Player",
|
||||||
|
Level = 10,
|
||||||
|
Experience = 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
EntityData enemy = new EnemyEntityData
|
||||||
|
{
|
||||||
|
Id = "enemy_1",
|
||||||
|
Type = "Enemy",
|
||||||
|
Health = 100,
|
||||||
|
Damage = 20
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用运行时类型序列化
|
||||||
|
string playerJson = serializer.Serialize(player, player.GetType());
|
||||||
|
string enemyJson = serializer.Serialize(enemy, enemy.GetType());
|
||||||
|
|
||||||
|
// 根据类型反序列化
|
||||||
|
var restoredPlayer = serializer.Deserialize(playerJson, typeof(PlayerEntityData));
|
||||||
|
var restoredEnemy = serializer.Deserialize(enemyJson, typeof(EnemyEntityData));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义序列化逻辑
|
||||||
|
|
||||||
|
虽然 GFramework 使用 Newtonsoft.Json,但你可以通过特性控制序列化行为:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
public class CustomData
|
||||||
|
{
|
||||||
|
// 忽略此属性
|
||||||
|
[JsonIgnore]
|
||||||
|
public string InternalId { get; set; }
|
||||||
|
|
||||||
|
// 使用不同的属性名
|
||||||
|
[JsonProperty("player_name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
// 仅在值不为 null 时序列化
|
||||||
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string? OptionalField { get; set; }
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
[JsonProperty("created_at")]
|
||||||
|
[JsonConverter(typeof(IsoDateTimeConverter))]
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量序列化
|
||||||
|
|
||||||
|
处理多个对象的序列化:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task SaveMultipleData()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
var dataList = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "player", new PlayerData { Name = "Player1", Level = 10 } },
|
||||||
|
{ "inventory", new InventoryData { Items = new List<ItemData>() } },
|
||||||
|
{ "settings", new SettingsData { Volume = 0.8f } }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量序列化和保存
|
||||||
|
foreach (var (key, data) in dataList)
|
||||||
|
{
|
||||||
|
string json = serializer.Serialize(data);
|
||||||
|
await storage.WriteAsync(key, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"已保存 {dataList.Count} 个数据文件");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
处理序列化和反序列化错误:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void SafeDeserialize()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
|
||||||
|
string json = "{\"Name\":\"Player1\",\"Level\":\"invalid\"}"; // 错误的数据
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var player = serializer.Deserialize<PlayerData>(json);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"反序列化失败: {ex.Message}");
|
||||||
|
// 返回默认值或重新尝试
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"JSON 格式错误: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerData DeserializeWithFallback(string json)
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return serializer.Deserialize<PlayerData>(json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 返回默认数据
|
||||||
|
return new PlayerData
|
||||||
|
{
|
||||||
|
Name = "DefaultPlayer",
|
||||||
|
Level = 1,
|
||||||
|
Experience = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 版本兼容性
|
||||||
|
|
||||||
|
处理数据结构变化:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 旧版本数据
|
||||||
|
public class PlayerDataV1
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Level { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新版本数据(添加了新字段)
|
||||||
|
public class PlayerDataV2
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Level { get; set; }
|
||||||
|
public int Experience { get; set; } = 0; // 新增字段,提供默认值
|
||||||
|
public DateTime LastLogin { get; set; } = DateTime.Now; // 新增字段
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerDataV2 LoadWithMigration(string json)
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试加载新版本
|
||||||
|
return serializer.Deserialize<PlayerDataV2>(json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 如果失败,尝试加载旧版本并迁移
|
||||||
|
var oldData = serializer.Deserialize<PlayerDataV1>(json);
|
||||||
|
return new PlayerDataV2
|
||||||
|
{
|
||||||
|
Name = oldData.Name,
|
||||||
|
Level = oldData.Level,
|
||||||
|
Experience = oldData.Level * 100, // 根据等级计算经验
|
||||||
|
LastLogin = DateTime.Now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **使用接口而非具体类型**:依赖 `ISerializer` 接口
|
||||||
|
```csharp
|
||||||
|
✓ var serializer = this.GetUtility<ISerializer>();
|
||||||
|
✗ var serializer = new JsonSerializer(); // 避免直接实例化
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **为数据类提供默认值**:确保反序列化的健壮性
|
||||||
|
```csharp
|
||||||
|
public class GameData
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "Default";
|
||||||
|
public int Score { get; set; } = 0;
|
||||||
|
public List<string> Items { get; set; } = new();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **处理反序列化异常**:避免程序崩溃
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = serializer.Deserialize<GameData>(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error($"反序列化失败: {ex.Message}");
|
||||||
|
return GetDefaultData();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **避免序列化敏感数据**:使用 `[JsonIgnore]` 标记
|
||||||
|
```csharp
|
||||||
|
public class UserData
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Password { get; set; } // 不序列化密码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **使用运行时类型处理多态**:保持类型信息
|
||||||
|
```csharp
|
||||||
|
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||||
|
string json = serializer.Serialize(obj, obj.GetType());
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **验证反序列化的数据**:确保数据完整性
|
||||||
|
```csharp
|
||||||
|
var data = serializer.Deserialize<GameData>(json);
|
||||||
|
if (string.IsNullOrEmpty(data.Name) || data.Score < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("数据验证失败");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 减少序列化开销
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 避免频繁序列化大对象
|
||||||
|
public class CachedSerializer
|
||||||
|
{
|
||||||
|
private string? _cachedJson;
|
||||||
|
private GameData? _cachedData;
|
||||||
|
|
||||||
|
public string GetJson(GameData data)
|
||||||
|
{
|
||||||
|
if (_cachedData == data && _cachedJson != null)
|
||||||
|
{
|
||||||
|
return _cachedJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
var serializer = GetSerializer();
|
||||||
|
_cachedJson = serializer.Serialize(data);
|
||||||
|
_cachedData = data;
|
||||||
|
return _cachedJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异步序列化
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task SaveDataAsync()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
var data = GetLargeData();
|
||||||
|
|
||||||
|
// 在后台线程序列化
|
||||||
|
string json = await Task.Run(() => serializer.Serialize(data));
|
||||||
|
|
||||||
|
// 异步写入存储
|
||||||
|
await storage.WriteAsync("large_data", json);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分块序列化
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task SaveLargeDataset()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
var largeDataset = GetLargeDataset();
|
||||||
|
|
||||||
|
// 分块保存
|
||||||
|
const int chunkSize = 100;
|
||||||
|
for (int i = 0; i < largeDataset.Count; i += chunkSize)
|
||||||
|
{
|
||||||
|
var chunk = largeDataset.Skip(i).Take(chunkSize).ToList();
|
||||||
|
string json = serializer.Serialize(chunk);
|
||||||
|
await storage.WriteAsync($"data_chunk_{i / chunkSize}", json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 问题:如何序列化循环引用的对象?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
Newtonsoft.Json 默认不支持循环引用,需要配置:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 注意:GFramework 的 JsonSerializer 使用默认设置
|
||||||
|
// 如需处理循环引用,避免创建循环引用的数据结构
|
||||||
|
// 或使用 [JsonIgnore] 打破循环
|
||||||
|
|
||||||
|
public class Node
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public List<Node> Children { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] // 忽略父节点引用,避免循环
|
||||||
|
public Node? Parent { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:序列化后的 JSON 太大怎么办?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
使用压缩或分块存储:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task SaveCompressed()
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
var data = GetLargeData();
|
||||||
|
string json = serializer.Serialize(data);
|
||||||
|
|
||||||
|
// 压缩 JSON
|
||||||
|
byte[] compressed = Compress(json);
|
||||||
|
|
||||||
|
// 保存压缩数据
|
||||||
|
await storage.WriteAsync("data_compressed", compressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] Compress(string text)
|
||||||
|
{
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
using (var gzip = new GZipStream(output, CompressionMode.Compress))
|
||||||
|
using (var writer = new StreamWriter(gzip))
|
||||||
|
{
|
||||||
|
writer.Write(text);
|
||||||
|
}
|
||||||
|
return output.ToArray();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何处理不同平台的序列化差异?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
使用平台无关的数据类型:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CrossPlatformData
|
||||||
|
{
|
||||||
|
// 使用 string 而非 DateTime(避免时区问题)
|
||||||
|
public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("O");
|
||||||
|
|
||||||
|
// 使用 double 而非 float(精度一致)
|
||||||
|
public double Score { get; set; }
|
||||||
|
|
||||||
|
// 明确指定编码
|
||||||
|
public string Text { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:反序列化失败时如何恢复?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
实现备份和恢复机制:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<GameData> LoadWithBackup(string key)
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试加载主数据
|
||||||
|
string json = await storage.ReadAsync<string>(key);
|
||||||
|
return serializer.Deserialize<GameData>(json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 尝试加载备份
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string backupJson = await storage.ReadAsync<string>($"{key}_backup");
|
||||||
|
return serializer.Deserialize<GameData>(backupJson);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 返回默认数据
|
||||||
|
return new GameData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何加密序列化的数据?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
在序列化后加密:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task SaveEncrypted(string key, GameData data)
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
// 序列化
|
||||||
|
string json = serializer.Serialize(data);
|
||||||
|
|
||||||
|
// 加密
|
||||||
|
byte[] encrypted = EncryptString(json);
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
await storage.WriteAsync(key, encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GameData> LoadEncrypted(string key)
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var storage = this.GetUtility<IStorage>();
|
||||||
|
|
||||||
|
// 读取
|
||||||
|
byte[] encrypted = await storage.ReadAsync<byte[]>(key);
|
||||||
|
|
||||||
|
// 解密
|
||||||
|
string json = DecryptToString(encrypted);
|
||||||
|
|
||||||
|
// 反序列化
|
||||||
|
return serializer.Deserialize<GameData>(json);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:序列化器是线程安全的吗?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
`JsonSerializer` 本身是线程安全的,但建议通过架构的 Utility 系统访问:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 线程安全的访问方式
|
||||||
|
public async Task ParallelSave()
|
||||||
|
{
|
||||||
|
var tasks = Enumerable.Range(0, 10).Select(async i =>
|
||||||
|
{
|
||||||
|
var serializer = this.GetUtility<ISerializer>();
|
||||||
|
var data = new GameData { Score = i };
|
||||||
|
string json = serializer.Serialize(data);
|
||||||
|
await SaveToStorage($"data_{i}", json);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||||||
|
- [存储系统](/zh-CN/game/storage) - 文件存储
|
||||||
|
- [设置系统](/zh-CN/game/setting) - 设置数据序列化
|
||||||
|
- [Utility 系统](/zh-CN/core/utility) - 工具类注册
|
||||||
735
docs/zh-CN/game/storage.md
Normal file
735
docs/zh-CN/game/storage.md
Normal file
@ -0,0 +1,735 @@
|
|||||||
|
---
|
||||||
|
title: 存储系统详解
|
||||||
|
description: 存储系统提供了灵活的文件存储和作用域隔离功能,支持跨平台数据持久化。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 存储系统详解
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
存储系统是 GFramework.Game 中用于管理文件存储的核心组件。它提供了统一的存储接口,支持键值对存储、作用域隔离、目录操作等功能,让你可以轻松实现游戏数据的持久化。
|
||||||
|
|
||||||
|
存储系统采用装饰器模式设计,通过 `IStorage` 接口定义统一的存储操作,`FileStorage` 提供基于文件系统的实现,`ScopedStorage`
|
||||||
|
提供作用域隔离功能。
|
||||||
|
|
||||||
|
**主要特性**:
|
||||||
|
|
||||||
|
- 统一的键值对存储接口
|
||||||
|
- 基于文件系统的持久化
|
||||||
|
- 作用域隔离和命名空间管理
|
||||||
|
- 线程安全的并发访问
|
||||||
|
- 支持同步和异步操作
|
||||||
|
- 目录和文件列举功能
|
||||||
|
- 路径安全防护
|
||||||
|
- 跨平台支持(包括 Godot)
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 存储接口
|
||||||
|
|
||||||
|
`IStorage` 定义了统一的存储操作:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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` 枚举定义了不同的存储方式:
|
||||||
|
|
||||||
|
```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<int>("player_score");
|
||||||
|
string name = storage.Read<string>("player_name");
|
||||||
|
var loadedSettings = storage.Read<GameSettings>("settings");
|
||||||
|
|
||||||
|
// 读取数据(带默认值)
|
||||||
|
int highScore = storage.Read("high_score", 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异步操作
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 异步写入
|
||||||
|
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");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查和删除
|
||||||
|
|
||||||
|
```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<string>("player/profile/name");
|
||||||
|
int gold = storage.Read<int>("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<string>("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<int>("level"); // 5
|
||||||
|
string gameLevel = gameStorage.Read<string>("level"); // "forest_area_1"
|
||||||
|
string settingsLevel = settingsStorage.Read<string>("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<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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 存储迁移
|
||||||
|
|
||||||
|
```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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 存储备份
|
||||||
|
|
||||||
|
```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<object>(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<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 文件存储
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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 路径说明
|
||||||
|
|
||||||
|
```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<Config>("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<int>("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<TimestampedData>($"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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何限制存储大小?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
实现配额管理:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何实现存储压缩?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
使用压缩序列化器:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何监控存储操作?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
实现日志存储包装器:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||||||
|
- [序列化系统](/zh-CN/core/serializer) - 数据序列化
|
||||||
|
- [Godot 集成](/zh-CN/godot/index) - Godot 中的存储
|
||||||
|
- [存档系统教程](/zh-CN/tutorials/save-system) - 完整示例
|
||||||
628
docs/zh-CN/godot/logging.md
Normal file
628
docs/zh-CN/godot/logging.md
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
---
|
||||||
|
title: Godot 日志系统
|
||||||
|
description: Godot 日志系统提供了 GFramework 日志功能与 Godot 引擎控制台的完整集成。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot 日志系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Godot 日志系统是 GFramework.Godot 中连接框架日志功能与 Godot 引擎控制台的核心组件。它提供了与 Godot
|
||||||
|
控制台的深度集成,支持彩色输出、多级别日志记录,以及与 GFramework 日志系统的无缝对接。
|
||||||
|
|
||||||
|
通过 Godot 日志系统,你可以在 Godot 项目中使用统一的日志接口,日志会自动输出到 Godot 编辑器控制台,并根据日志级别使用不同的颜色和输出方式。
|
||||||
|
|
||||||
|
**主要特性**:
|
||||||
|
|
||||||
|
- 与 Godot 控制台深度集成
|
||||||
|
- 支持彩色日志输出
|
||||||
|
- 多级别日志记录(Trace、Debug、Info、Warning、Error、Fatal)
|
||||||
|
- 日志缓存机制
|
||||||
|
- 时间戳和格式化支持
|
||||||
|
- 异常信息记录
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### GodotLogger
|
||||||
|
|
||||||
|
`GodotLogger` 是 Godot 平台的日志记录器实现,继承自 `AbstractLogger`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class GodotLogger : AbstractLogger
|
||||||
|
{
|
||||||
|
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info);
|
||||||
|
protected override void Write(LogLevel level, string message, Exception? exception);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GodotLoggerFactory
|
||||||
|
|
||||||
|
`GodotLoggerFactory` 用于创建 Godot 日志记录器实例:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GodotLoggerFactory : ILoggerFactory
|
||||||
|
{
|
||||||
|
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GodotLoggerFactoryProvider
|
||||||
|
|
||||||
|
`GodotLoggerFactoryProvider` 提供日志工厂实例,并支持日志缓存:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||||
|
{
|
||||||
|
public LogLevel MinLevel { get; set; }
|
||||||
|
public ILogger CreateLogger(string name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 配置 Godot 日志系统
|
||||||
|
|
||||||
|
在架构初始化时配置日志提供程序:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.architecture;
|
||||||
|
using GFramework.Godot.logging;
|
||||||
|
using GFramework.Core.logging;
|
||||||
|
using GFramework.Core.Abstractions.logging;
|
||||||
|
|
||||||
|
public class GameArchitecture : AbstractArchitecture
|
||||||
|
{
|
||||||
|
public static GameArchitecture Interface { get; private set; }
|
||||||
|
|
||||||
|
public GameArchitecture()
|
||||||
|
{
|
||||||
|
Interface = this;
|
||||||
|
|
||||||
|
// 配置 Godot 日志系统
|
||||||
|
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||||
|
{
|
||||||
|
MinLevel = LogLevel.Debug // 设置最小日志级别
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InstallModules()
|
||||||
|
{
|
||||||
|
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
|
||||||
|
logger.Info("游戏架构初始化开始");
|
||||||
|
|
||||||
|
RegisterModel(new PlayerModel());
|
||||||
|
RegisterSystem(new GameplaySystem());
|
||||||
|
|
||||||
|
logger.Info("游戏架构初始化完成");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建和使用日志记录器
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Core.logging;
|
||||||
|
using GFramework.Core.Abstractions.logging;
|
||||||
|
|
||||||
|
public partial class Player : CharacterBody2D
|
||||||
|
{
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 创建日志记录器
|
||||||
|
_logger = LoggerFactoryResolver.Provider.CreateLogger("Player");
|
||||||
|
|
||||||
|
_logger.Info("玩家初始化");
|
||||||
|
_logger.Debug("玩家位置: {0}", Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (_logger.IsDebugEnabled())
|
||||||
|
{
|
||||||
|
_logger.Debug("玩家速度: {0}", Velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TakeDamage(float damage)
|
||||||
|
{
|
||||||
|
_logger.Warn("玩家受到伤害: {0}", damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnError()
|
||||||
|
{
|
||||||
|
_logger.Error("玩家状态异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 记录不同级别的日志
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameSystem");
|
||||||
|
|
||||||
|
// Trace - 最详细的跟踪信息(灰色)
|
||||||
|
logger.Trace("执行函数: UpdatePlayerPosition");
|
||||||
|
|
||||||
|
// Debug - 调试信息(青色)
|
||||||
|
logger.Debug("当前帧率: {0}", Engine.GetFramesPerSecond());
|
||||||
|
|
||||||
|
// Info - 一般信息(白色)
|
||||||
|
logger.Info("游戏开始");
|
||||||
|
|
||||||
|
// Warning - 警告信息(黄色)
|
||||||
|
logger.Warn("资源加载缓慢: {0}ms", loadTime);
|
||||||
|
|
||||||
|
// Error - 错误信息(红色)
|
||||||
|
logger.Error("无法加载配置文件");
|
||||||
|
|
||||||
|
// Fatal - 致命错误(红色,使用 PushError)
|
||||||
|
logger.Fatal("游戏崩溃");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 记录异常信息
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var logger = LoggerFactoryResolver.Provider.CreateLogger("SaveSystem");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SaveGame();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 记录异常信息
|
||||||
|
logger.Error("保存游戏失败", ex);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 在 System 中使用日志
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.system;
|
||||||
|
using GFramework.Core.logging;
|
||||||
|
using GFramework.Core.Abstractions.logging;
|
||||||
|
|
||||||
|
public class CombatSystem : AbstractSystem
|
||||||
|
{
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
_logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
|
||||||
|
_logger.Info("战斗系统初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessCombat(Entity attacker, Entity target, float damage)
|
||||||
|
{
|
||||||
|
_logger.Debug("战斗处理: {0} 攻击 {1}, 伤害: {2}",
|
||||||
|
attacker.Name, target.Name, damage);
|
||||||
|
|
||||||
|
if (damage > 100)
|
||||||
|
{
|
||||||
|
_logger.Warn("高伤害攻击: {0}", damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDestroy()
|
||||||
|
{
|
||||||
|
_logger.Info("战斗系统已销毁");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在 Model 中使用日志
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.model;
|
||||||
|
using GFramework.Core.logging;
|
||||||
|
using GFramework.Core.Abstractions.logging;
|
||||||
|
|
||||||
|
public class PlayerModel : AbstractModel
|
||||||
|
{
|
||||||
|
private ILogger _logger;
|
||||||
|
private int _health;
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
_logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel");
|
||||||
|
_logger.Info("玩家模型初始化");
|
||||||
|
|
||||||
|
_health = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHealth(int value)
|
||||||
|
{
|
||||||
|
var oldHealth = _health;
|
||||||
|
_health = value;
|
||||||
|
|
||||||
|
_logger.Debug("玩家生命值变化: {0} -> {1}", oldHealth, _health);
|
||||||
|
|
||||||
|
if (_health <= 0)
|
||||||
|
{
|
||||||
|
_logger.Warn("玩家生命值归零");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 条件日志记录
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var logger = LoggerFactoryResolver.Provider.CreateLogger("PerformanceMonitor");
|
||||||
|
|
||||||
|
// 检查日志级别是否启用,避免不必要的字符串格式化
|
||||||
|
if (logger.IsDebugEnabled())
|
||||||
|
{
|
||||||
|
var stats = CalculateComplexStats(); // 耗时操作
|
||||||
|
logger.Debug("性能统计: {0}", stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化写法
|
||||||
|
if (logger.IsTraceEnabled())
|
||||||
|
{
|
||||||
|
logger.Trace("详细的执行流程信息");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分类日志记录
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 为不同模块创建独立的日志记录器
|
||||||
|
var networkLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
|
||||||
|
var databaseLogger = LoggerFactoryResolver.Provider.CreateLogger("Database");
|
||||||
|
var aiLogger = LoggerFactoryResolver.Provider.CreateLogger("AI");
|
||||||
|
|
||||||
|
networkLogger.Info("连接到服务器");
|
||||||
|
databaseLogger.Debug("查询用户数据");
|
||||||
|
aiLogger.Trace("AI 决策树遍历");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义日志级别
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 在开发环境使用 Debug 级别
|
||||||
|
#if DEBUG
|
||||||
|
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||||
|
{
|
||||||
|
MinLevel = LogLevel.Debug
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
// 在生产环境使用 Info 级别
|
||||||
|
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||||
|
{
|
||||||
|
MinLevel = LogLevel.Info
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在 Godot 模块中使用日志
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.architecture;
|
||||||
|
using GFramework.Core.logging;
|
||||||
|
using GFramework.Core.Abstractions.logging;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public class SceneModule : AbstractGodotModule
|
||||||
|
{
|
||||||
|
private ILogger _logger;
|
||||||
|
private Node _sceneRoot;
|
||||||
|
|
||||||
|
public override Node Node => _sceneRoot;
|
||||||
|
|
||||||
|
public SceneModule()
|
||||||
|
{
|
||||||
|
_sceneRoot = new Node { Name = "SceneRoot" };
|
||||||
|
_logger = LoggerFactoryResolver.Provider.CreateLogger("SceneModule");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Install(IArchitecture architecture)
|
||||||
|
{
|
||||||
|
_logger.Info("场景模块安装开始");
|
||||||
|
|
||||||
|
// 安装场景系统
|
||||||
|
var sceneSystem = new SceneSystem();
|
||||||
|
architecture.RegisterSystem<ISceneSystem>(sceneSystem);
|
||||||
|
|
||||||
|
_logger.Info("场景模块安装完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||||
|
{
|
||||||
|
_logger.Debug("场景模块阶段: {0}", phase);
|
||||||
|
|
||||||
|
if (phase == ArchitecturePhase.Ready)
|
||||||
|
{
|
||||||
|
_logger.Info("场景模块已就绪");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnDetach()
|
||||||
|
{
|
||||||
|
_logger.Info("场景模块已分离");
|
||||||
|
_sceneRoot?.QueueFree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 日志输出格式
|
||||||
|
|
||||||
|
### 输出格式说明
|
||||||
|
|
||||||
|
Godot 日志系统使用以下格式输出日志:
|
||||||
|
|
||||||
|
```
|
||||||
|
[时间戳] 日志级别 [日志器名称] 日志消息
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例输出**:
|
||||||
|
|
||||||
|
```
|
||||||
|
[2025-01-09 10:30:45.123] INFO [GameArchitecture] 游戏架构初始化开始
|
||||||
|
[2025-01-09 10:30:45.456] DEBUG [Player] 玩家位置: (100, 200)
|
||||||
|
[2025-01-09 10:30:46.789] WARNING [CombatSystem] 高伤害攻击: 150
|
||||||
|
[2025-01-09 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志级别与 Godot 输出方法
|
||||||
|
|
||||||
|
| 日志级别 | Godot 方法 | 颜色 | 说明 |
|
||||||
|
|-------------|------------------|----|----------|
|
||||||
|
| **Trace** | `GD.PrintRich` | 灰色 | 最详细的跟踪信息 |
|
||||||
|
| **Debug** | `GD.PrintRich` | 青色 | 调试信息 |
|
||||||
|
| **Info** | `GD.Print` | 白色 | 一般信息 |
|
||||||
|
| **Warning** | `GD.PushWarning` | 黄色 | 警告信息 |
|
||||||
|
| **Error** | `GD.PrintErr` | 红色 | 错误信息 |
|
||||||
|
| **Fatal** | `GD.PushError` | 红色 | 致命错误 |
|
||||||
|
|
||||||
|
### 异常信息格式
|
||||||
|
|
||||||
|
当记录异常时,异常信息会附加到日志消息后:
|
||||||
|
|
||||||
|
```
|
||||||
|
[2025-01-09 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
|
||||||
|
System.IO.IOException: 文件访问被拒绝
|
||||||
|
at SaveSystem.SaveGame() in SaveSystem.cs:line 42
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **在架构初始化时配置日志系统**:
|
||||||
|
```csharp
|
||||||
|
public GameArchitecture()
|
||||||
|
{
|
||||||
|
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||||
|
{
|
||||||
|
MinLevel = LogLevel.Debug
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **为每个类创建独立的日志记录器**:
|
||||||
|
```csharp
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **使用合适的日志级别**:
|
||||||
|
- `Trace`:详细的执行流程,仅在深度调试时使用
|
||||||
|
- `Debug`:调试信息,开发阶段使用
|
||||||
|
- `Info`:重要的业务流程和状态变化
|
||||||
|
- `Warning`:潜在问题但不影响功能
|
||||||
|
- `Error`:错误但程序可以继续运行
|
||||||
|
- `Fatal`:严重错误,程序无法继续
|
||||||
|
|
||||||
|
4. **检查日志级别避免性能损失**:
|
||||||
|
```csharp
|
||||||
|
if (_logger.IsDebugEnabled())
|
||||||
|
{
|
||||||
|
var expensiveData = CalculateExpensiveData();
|
||||||
|
_logger.Debug("数据: {0}", expensiveData);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **提供有意义的上下文信息**:
|
||||||
|
```csharp
|
||||||
|
// ✗ 不好
|
||||||
|
logger.Error("错误");
|
||||||
|
|
||||||
|
// ✓ 好
|
||||||
|
logger.Error("加载场景失败: SceneKey={0}, Path={1}", sceneKey, scenePath);
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **记录异常时提供上下文**:
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LoadScene(sceneKey);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error($"加载场景失败: {sceneKey}", ex);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **使用分类日志记录器**:
|
||||||
|
```csharp
|
||||||
|
var networkLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
|
||||||
|
var aiLogger = LoggerFactoryResolver.Provider.CreateLogger("AI");
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **在生命周期方法中记录关键事件**:
|
||||||
|
```csharp
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
_logger.Info("系统初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDestroy()
|
||||||
|
{
|
||||||
|
_logger.Info("系统已销毁");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
1. **日志缓存**:
|
||||||
|
- `GodotLoggerFactoryProvider` 使用 `CachedLoggerFactory` 缓存日志记录器实例
|
||||||
|
- 相同名称和级别的日志记录器会被复用
|
||||||
|
|
||||||
|
2. **级别检查**:
|
||||||
|
- 日志方法会自动检查日志级别
|
||||||
|
- 低于最小级别的日志不会被处理
|
||||||
|
|
||||||
|
3. **字符串格式化**:
|
||||||
|
- 使用参数化日志避免不必要的字符串拼接
|
||||||
|
```csharp
|
||||||
|
// ✗ 不好 - 总是执行字符串拼接
|
||||||
|
logger.Debug("位置: " + position.ToString());
|
||||||
|
|
||||||
|
// ✓ 好 - 只在 Debug 启用时格式化
|
||||||
|
logger.Debug("位置: {0}", position);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **条件日志**:
|
||||||
|
- 对于耗时的数据计算,先检查日志级别
|
||||||
|
```csharp
|
||||||
|
if (logger.IsDebugEnabled())
|
||||||
|
{
|
||||||
|
var stats = CalculateComplexStats();
|
||||||
|
logger.Debug("统计: {0}", stats);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 问题:如何配置 Godot 日志系统?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
在架构构造函数中配置日志提供程序:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public GameArchitecture()
|
||||||
|
{
|
||||||
|
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||||
|
{
|
||||||
|
MinLevel = LogLevel.Debug
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:日志没有输出到 Godot 控制台?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
检查以下几点:
|
||||||
|
|
||||||
|
1. 确认已配置 `GodotLoggerFactoryProvider`
|
||||||
|
2. 检查日志级别是否低于最小级别
|
||||||
|
3. 确认使用了正确的日志记录器
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 确认配置
|
||||||
|
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||||
|
{
|
||||||
|
MinLevel = LogLevel.Trace // 设置为最低级别测试
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建日志记录器
|
||||||
|
var logger = LoggerFactoryResolver.Provider.CreateLogger("Test");
|
||||||
|
logger.Info("测试日志"); // 应该能看到输出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何在不同环境使用不同的日志级别?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
使用条件编译或环境检测:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public GameArchitecture()
|
||||||
|
{
|
||||||
|
var minLevel = OS.IsDebugBuild() ? LogLevel.Debug : LogLevel.Info;
|
||||||
|
|
||||||
|
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||||
|
{
|
||||||
|
MinLevel = minLevel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何禁用某个模块的日志?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
为该模块创建一个高级别的日志记录器:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 只记录 Error 及以上级别
|
||||||
|
var logger = new GodotLogger("VerboseModule", LogLevel.Error);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:日志输出影响性能怎么办?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
|
||||||
|
1. 提高最小日志级别
|
||||||
|
2. 使用条件日志
|
||||||
|
3. 避免在高频调用的方法中记录日志
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 提高日志级别
|
||||||
|
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||||
|
{
|
||||||
|
MinLevel = LogLevel.Warning // 只记录警告及以上
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用条件日志
|
||||||
|
if (_logger.IsDebugEnabled())
|
||||||
|
{
|
||||||
|
_logger.Debug("高频数据: {0}", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 避免在 _Process 中频繁记录
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
// ✗ 不好 - 每帧都记录
|
||||||
|
// _logger.Debug("帧更新");
|
||||||
|
|
||||||
|
// ✓ 好 - 只在特定条件下记录
|
||||||
|
if (someErrorCondition)
|
||||||
|
{
|
||||||
|
_logger.Error("检测到错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何记录结构化日志?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
使用参数化日志或 `IStructuredLogger` 接口:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 参数化日志
|
||||||
|
logger.Info("玩家登录: UserId={0}, UserName={1}, Level={2}",
|
||||||
|
userId, userName, level);
|
||||||
|
|
||||||
|
// 使用结构化日志(如果实现了 IStructuredLogger)
|
||||||
|
if (logger is IStructuredLogger structuredLogger)
|
||||||
|
{
|
||||||
|
structuredLogger.Log(LogLevel.Info, "玩家登录",
|
||||||
|
("UserId", userId),
|
||||||
|
("UserName", userName),
|
||||||
|
("Level", level));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [核心日志系统](/zh-CN/core/logging) - GFramework 核心日志功能
|
||||||
|
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构系统
|
||||||
|
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||||
|
- [最佳实践](/zh-CN/best-practices/architecture-patterns) - 架构最佳实践
|
||||||
736
docs/zh-CN/godot/pause.md
Normal file
736
docs/zh-CN/godot/pause.md
Normal file
@ -0,0 +1,736 @@
|
|||||||
|
---
|
||||||
|
title: Godot 暂停处理
|
||||||
|
description: Godot 暂停处理系统提供了 GFramework 暂停管理与 Godot SceneTree 暂停的完整集成。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot 暂停处理
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Godot 暂停处理系统是 GFramework.Godot 中连接框架暂停管理与 Godot 引擎暂停机制的核心组件。它提供了暂停栈管理、分组暂停、嵌套暂停等功能,让你可以在
|
||||||
|
Godot 项目中使用 GFramework 的暂停系统。
|
||||||
|
|
||||||
|
通过 Godot 暂停处理系统,你可以实现精细的暂停控制,支持游戏逻辑暂停、UI 暂停、动画暂停等多种场景,同时保持与 Godot SceneTree
|
||||||
|
暂停机制的完美兼容。
|
||||||
|
|
||||||
|
**主要特性**:
|
||||||
|
|
||||||
|
- 暂停栈管理(支持嵌套暂停)
|
||||||
|
- 分组暂停(Global、Gameplay、Animation、Audio 等)
|
||||||
|
- 与 Godot SceneTree.Paused 集成
|
||||||
|
- 暂停处理器机制
|
||||||
|
- 暂停作用域(支持 using 语法)
|
||||||
|
- 线程安全的暂停管理
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 暂停栈管理器
|
||||||
|
|
||||||
|
`IPauseStackManager` 管理游戏中的暂停状态:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IPauseStackManager : IContextUtility
|
||||||
|
{
|
||||||
|
// 推入暂停请求
|
||||||
|
PauseToken Push(string reason, PauseGroup group = PauseGroup.Global);
|
||||||
|
|
||||||
|
// 弹出暂停请求
|
||||||
|
bool Pop(PauseToken token);
|
||||||
|
|
||||||
|
// 查询是否暂停
|
||||||
|
bool IsPaused(PauseGroup group = PauseGroup.Global);
|
||||||
|
|
||||||
|
// 获取暂停深度
|
||||||
|
int GetPauseDepth(PauseGroup group = PauseGroup.Global);
|
||||||
|
|
||||||
|
// 暂停状态变化事件
|
||||||
|
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 暂停组
|
||||||
|
|
||||||
|
`PauseGroup` 定义不同的暂停作用域:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum PauseGroup
|
||||||
|
{
|
||||||
|
Global = 0, // 全局暂停(影响所有系统)
|
||||||
|
Gameplay = 1, // 游戏逻辑暂停(不影响 UI)
|
||||||
|
Animation = 2, // 动画暂停
|
||||||
|
Audio = 3, // 音频暂停
|
||||||
|
Custom1 = 10, // 自定义组 1
|
||||||
|
Custom2 = 11, // 自定义组 2
|
||||||
|
Custom3 = 12 // 自定义组 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 暂停令牌
|
||||||
|
|
||||||
|
`PauseToken` 唯一标识一个暂停请求:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public readonly struct PauseToken
|
||||||
|
{
|
||||||
|
public Guid Id { get; }
|
||||||
|
public bool IsValid { get; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Godot 暂停处理器
|
||||||
|
|
||||||
|
`GodotPauseHandler` 响应暂停栈状态变化,控制 SceneTree.Paused:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GodotPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
public int Priority => 0;
|
||||||
|
|
||||||
|
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
// 只有 Global 组影响 Godot 的全局暂停
|
||||||
|
if (group == PauseGroup.Global)
|
||||||
|
{
|
||||||
|
_tree.Paused = isPaused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 设置暂停系统
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.architecture;
|
||||||
|
using GFramework.Godot.pause;
|
||||||
|
using GFramework.Core.pause;
|
||||||
|
|
||||||
|
public class GameArchitecture : AbstractArchitecture
|
||||||
|
{
|
||||||
|
protected override void InstallModules()
|
||||||
|
{
|
||||||
|
// 注册暂停栈管理器
|
||||||
|
var pauseManager = new PauseStackManager();
|
||||||
|
RegisterUtility<IPauseStackManager>(pauseManager);
|
||||||
|
|
||||||
|
// 注册 Godot 暂停处理器
|
||||||
|
var pauseHandler = new GodotPauseHandler(GetTree());
|
||||||
|
pauseManager.RegisterHandler(pauseHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基本暂停和恢复
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class PauseMenu : Control
|
||||||
|
{
|
||||||
|
private PauseToken _pauseToken;
|
||||||
|
|
||||||
|
public void ShowPauseMenu()
|
||||||
|
{
|
||||||
|
// 暂停游戏
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
_pauseToken = pauseManager.Push("Pause menu opened");
|
||||||
|
|
||||||
|
Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HidePauseMenu()
|
||||||
|
{
|
||||||
|
// 恢复游戏
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
pauseManager.Pop(_pauseToken);
|
||||||
|
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用暂停作用域
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class DialogBox : Control
|
||||||
|
{
|
||||||
|
public async void ShowDialog()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// 使用 using 语法自动管理暂停
|
||||||
|
using (pauseManager.PauseScope("Dialog shown"))
|
||||||
|
{
|
||||||
|
Show();
|
||||||
|
await ToSignal(GetTree().CreateTimer(3.0f), "timeout");
|
||||||
|
Hide();
|
||||||
|
} // 自动恢复
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询暂停状态
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class GameController : Node
|
||||||
|
{
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// 检查是否暂停
|
||||||
|
if (pauseManager.IsPaused())
|
||||||
|
{
|
||||||
|
GD.Print("游戏已暂停");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 游戏逻辑
|
||||||
|
UpdateGame(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGame(double delta)
|
||||||
|
{
|
||||||
|
// 游戏更新逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 分组暂停
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class GameManager : Node
|
||||||
|
{
|
||||||
|
private PauseToken _gameplayPauseToken;
|
||||||
|
private PauseToken _animationPauseToken;
|
||||||
|
|
||||||
|
// 只暂停游戏逻辑,UI 仍然可以交互
|
||||||
|
public void PauseGameplay()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
_gameplayPauseToken = pauseManager.Push("Gameplay paused", PauseGroup.Gameplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeGameplay()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
pauseManager.Pop(_gameplayPauseToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只暂停动画
|
||||||
|
public void PauseAnimations()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
_animationPauseToken = pauseManager.Push("Animations paused", PauseGroup.Animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeAnimations()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
pauseManager.Pop(_animationPauseToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查特定组的暂停状态
|
||||||
|
public bool IsGameplayPaused()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
return pauseManager.IsPaused(PauseGroup.Gameplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 嵌套暂停
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class GameScene : Node
|
||||||
|
{
|
||||||
|
public async void ShowNestedDialogs()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// 第一层暂停
|
||||||
|
using (pauseManager.PauseScope("First dialog"))
|
||||||
|
{
|
||||||
|
GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 1
|
||||||
|
ShowDialog("第一个对话框");
|
||||||
|
await ToSignal(GetTree().CreateTimer(2.0f), "timeout");
|
||||||
|
|
||||||
|
// 第二层暂停
|
||||||
|
using (pauseManager.PauseScope("Second dialog"))
|
||||||
|
{
|
||||||
|
GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 2
|
||||||
|
ShowDialog("第二个对话框");
|
||||||
|
await ToSignal(GetTree().CreateTimer(2.0f), "timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowDialog(string message)
|
||||||
|
{
|
||||||
|
GD.Print(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义暂停处理器
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.Abstractions.pause;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
// 自定义动画暂停处理器
|
||||||
|
public class AnimationPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
private readonly AnimationPlayer _animationPlayer;
|
||||||
|
|
||||||
|
public AnimationPauseHandler(AnimationPlayer animationPlayer)
|
||||||
|
{
|
||||||
|
_animationPlayer = animationPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Priority => 10;
|
||||||
|
|
||||||
|
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
// 只响应 Animation 组
|
||||||
|
if (group == PauseGroup.Animation)
|
||||||
|
{
|
||||||
|
if (isPaused)
|
||||||
|
{
|
||||||
|
_animationPlayer.Pause();
|
||||||
|
GD.Print("动画已暂停");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_animationPlayer.Play();
|
||||||
|
GD.Print("动画已恢复");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册自定义处理器
|
||||||
|
public partial class GameController : Node
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
var animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
|
||||||
|
|
||||||
|
var animationHandler = new AnimationPauseHandler(animationPlayer);
|
||||||
|
pauseManager.RegisterHandler(animationHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 音频暂停处理器
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.Abstractions.pause;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public class AudioPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
private readonly AudioStreamPlayer _musicPlayer;
|
||||||
|
private readonly AudioStreamPlayer _sfxPlayer;
|
||||||
|
|
||||||
|
public AudioPauseHandler(AudioStreamPlayer musicPlayer, AudioStreamPlayer sfxPlayer)
|
||||||
|
{
|
||||||
|
_musicPlayer = musicPlayer;
|
||||||
|
_sfxPlayer = sfxPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Priority => 20;
|
||||||
|
|
||||||
|
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
if (group == PauseGroup.Audio || group == PauseGroup.Global)
|
||||||
|
{
|
||||||
|
if (isPaused)
|
||||||
|
{
|
||||||
|
_musicPlayer.StreamPaused = true;
|
||||||
|
_sfxPlayer.StreamPaused = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_musicPlayer.StreamPaused = false;
|
||||||
|
_sfxPlayer.StreamPaused = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 节点暂停模式控制
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class GameNode : Node
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 设置节点在暂停时的行为
|
||||||
|
|
||||||
|
// 暂停时停止处理
|
||||||
|
ProcessMode = ProcessModeEnum.Pausable;
|
||||||
|
|
||||||
|
// 暂停时继续处理(用于 UI)
|
||||||
|
// ProcessMode = ProcessModeEnum.Always;
|
||||||
|
|
||||||
|
// 暂停时停止,且子节点也停止
|
||||||
|
// ProcessMode = ProcessModeEnum.Inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
// 当 SceneTree.Paused = true 且 ProcessMode = Pausable 时
|
||||||
|
// 此方法不会被调用
|
||||||
|
UpdateGameLogic(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGameLogic(double delta)
|
||||||
|
{
|
||||||
|
// 游戏逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI 在暂停时继续工作
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class PauseMenuUI : Control
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// UI 在游戏暂停时仍然可以交互
|
||||||
|
ProcessMode = ProcessModeEnum.Always;
|
||||||
|
|
||||||
|
GetNode<Button>("ResumeButton").Pressed += OnResumePressed;
|
||||||
|
GetNode<Button>("QuitButton").Pressed += OnQuitPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResumePressed()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// 获取所有暂停原因
|
||||||
|
var reasons = pauseManager.GetPauseReasons();
|
||||||
|
GD.Print($"当前暂停原因: {string.Join(", ", reasons)}");
|
||||||
|
|
||||||
|
// 清空所有暂停
|
||||||
|
pauseManager.ClearAll();
|
||||||
|
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnQuitPressed()
|
||||||
|
{
|
||||||
|
GetTree().Quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 监听暂停状态变化
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class PauseIndicator : Label
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
// 订阅暂停状态变化事件
|
||||||
|
pauseManager.OnPauseStateChanged += OnPauseStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
if (group == PauseGroup.Global)
|
||||||
|
{
|
||||||
|
Text = isPaused ? "游戏已暂停" : "游戏运行中";
|
||||||
|
Visible = isPaused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试暂停状态
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class PauseDebugger : Node
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
pauseManager.OnPauseStateChanged += OnPauseStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
GD.Print($"=== 暂停状态变化 ===");
|
||||||
|
GD.Print($"组: {group}");
|
||||||
|
GD.Print($"状态: {(isPaused ? "暂停" : "恢复")}");
|
||||||
|
GD.Print($"深度: {pauseManager.GetPauseDepth(group)}");
|
||||||
|
|
||||||
|
var reasons = pauseManager.GetPauseReasons(group);
|
||||||
|
if (reasons.Count > 0)
|
||||||
|
{
|
||||||
|
GD.Print($"原因:");
|
||||||
|
foreach (var reason in reasons)
|
||||||
|
{
|
||||||
|
GD.Print($" - {reason}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Input(InputEvent @event)
|
||||||
|
{
|
||||||
|
// 按 F12 显示所有暂停状态
|
||||||
|
if (@event is InputEventKey keyEvent && keyEvent.Pressed && keyEvent.Keycode == Key.F12)
|
||||||
|
{
|
||||||
|
PrintAllPauseStates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintAllPauseStates()
|
||||||
|
{
|
||||||
|
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||||
|
|
||||||
|
GD.Print("=== 所有暂停状态 ===");
|
||||||
|
|
||||||
|
foreach (PauseGroup group in Enum.GetValues(typeof(PauseGroup)))
|
||||||
|
{
|
||||||
|
var isPaused = pauseManager.IsPaused(group);
|
||||||
|
var depth = pauseManager.GetPauseDepth(group);
|
||||||
|
|
||||||
|
if (depth > 0)
|
||||||
|
{
|
||||||
|
GD.Print($"{group}: 暂停 (深度: {depth})");
|
||||||
|
var reasons = pauseManager.GetPauseReasons(group);
|
||||||
|
foreach (var reason in reasons)
|
||||||
|
{
|
||||||
|
GD.Print($" - {reason}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **使用暂停作用域管理生命周期**:避免忘记恢复
|
||||||
|
```csharp
|
||||||
|
✓ using (pauseManager.PauseScope("Dialog")) { ... }
|
||||||
|
✗ var token = pauseManager.Push("Dialog"); // 可能忘记 Pop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **为暂停提供清晰的原因**:便于调试
|
||||||
|
```csharp
|
||||||
|
✓ pauseManager.Push("Inventory opened");
|
||||||
|
✗ pauseManager.Push("pause"); // 原因不明确
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **使用正确的暂停组**:避免影响不该暂停的系统
|
||||||
|
```csharp
|
||||||
|
✓ pauseManager.Push("Menu", PauseGroup.Gameplay); // 只暂停游戏逻辑
|
||||||
|
✗ pauseManager.Push("Menu", PauseGroup.Global); // 暂停所有系统包括 UI
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **UI 节点设置 ProcessMode.Always**:确保 UI 在暂停时可用
|
||||||
|
```csharp
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
ProcessMode = ProcessModeEnum.Always;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **游戏逻辑节点设置 ProcessMode.Pausable**:确保暂停时停止
|
||||||
|
```csharp
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
ProcessMode = ProcessModeEnum.Pausable;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **保存暂停令牌以便恢复**:确保能正确恢复暂停
|
||||||
|
```csharp
|
||||||
|
private PauseToken _pauseToken;
|
||||||
|
|
||||||
|
public void Pause()
|
||||||
|
{
|
||||||
|
_pauseToken = pauseManager.Push("Paused");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Resume()
|
||||||
|
{
|
||||||
|
pauseManager.Pop(_pauseToken);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **使用事件监听暂停状态**:实现响应式 UI
|
||||||
|
```csharp
|
||||||
|
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||||
|
{
|
||||||
|
UpdateUI(isPaused);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **清理时注销事件监听**:避免内存泄漏
|
||||||
|
```csharp
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 问题:如何暂停游戏但保持 UI 可交互?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
使用 `PauseGroup.Gameplay` 而不是 `PauseGroup.Global`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 只暂停游戏逻辑
|
||||||
|
pauseManager.Push("Menu opened", PauseGroup.Gameplay);
|
||||||
|
|
||||||
|
// UI 节点设置为 Always
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
ProcessMode = ProcessModeEnum.Always;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:嵌套暂停如何工作?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
暂停栈支持嵌套,需要所有 Pop 才能完全恢复:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var token1 = pauseManager.Push("First"); // 深度: 1, 暂停
|
||||||
|
var token2 = pauseManager.Push("Second"); // 深度: 2, 仍然暂停
|
||||||
|
|
||||||
|
pauseManager.Pop(token1); // 深度: 1, 仍然暂停
|
||||||
|
pauseManager.Pop(token2); // 深度: 0, 恢复
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何实现自定义暂停行为?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
实现 `IPauseHandler` 接口并注册:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CustomPauseHandler : IPauseHandler
|
||||||
|
{
|
||||||
|
public int Priority => 0;
|
||||||
|
|
||||||
|
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||||
|
{
|
||||||
|
// 自定义暂停逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseManager.RegisterHandler(new CustomPauseHandler());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:暂停处理器的优先级如何工作?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
数值越小优先级越高,按优先级顺序调用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
handler1.Priority = 0; // 最先调用
|
||||||
|
handler2.Priority = 10; // 其次调用
|
||||||
|
handler3.Priority = 20; // 最后调用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何清空所有暂停?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
使用 `ClearAll()` 或 `ClearGroup()`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 清空所有组的暂停
|
||||||
|
pauseManager.ClearAll();
|
||||||
|
|
||||||
|
// 只清空特定组
|
||||||
|
pauseManager.ClearGroup(PauseGroup.Gameplay);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:暂停系统是线程安全的吗?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
是的,`PauseStackManager` 使用 `ReaderWriterLockSlim` 确保线程安全:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 可以在多个线程中安全调用
|
||||||
|
Task.Run(() => pauseManager.Push("Thread 1"));
|
||||||
|
Task.Run(() => pauseManager.Push("Thread 2"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何调试暂停问题?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
使用暂停状态查询方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 检查是否暂停
|
||||||
|
bool isPaused = pauseManager.IsPaused(PauseGroup.Global);
|
||||||
|
|
||||||
|
// 获取暂停深度
|
||||||
|
int depth = pauseManager.GetPauseDepth(PauseGroup.Global);
|
||||||
|
|
||||||
|
// 获取所有暂停原因
|
||||||
|
var reasons = pauseManager.GetPauseReasons(PauseGroup.Global);
|
||||||
|
foreach (var reason in reasons)
|
||||||
|
{
|
||||||
|
GD.Print($"暂停原因: {reason}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||||
|
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
|
||||||
|
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
|
||||||
|
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||||
684
docs/zh-CN/godot/pool.md
Normal file
684
docs/zh-CN/godot/pool.md
Normal file
@ -0,0 +1,684 @@
|
|||||||
|
---
|
||||||
|
title: Godot 节点池系统
|
||||||
|
description: Godot 节点池系统提供了高性能的节点复用机制,减少频繁创建和销毁节点带来的性能开销。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot 节点池系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Godot 节点池系统是 GFramework.Godot 中用于管理和复用 Godot
|
||||||
|
节点的高性能组件。通过对象池模式,它可以显著减少频繁创建和销毁节点带来的性能开销,特别适用于需要大量动态生成节点的场景,如子弹、特效、敌人等。
|
||||||
|
|
||||||
|
节点池系统基于 GFramework 核心的对象池系统,专门针对 Godot 节点进行了优化,提供了完整的生命周期管理和统计功能。
|
||||||
|
|
||||||
|
**主要特性**:
|
||||||
|
|
||||||
|
- 节点复用机制,减少 GC 压力
|
||||||
|
- 自动生命周期管理
|
||||||
|
- 池容量限制和预热功能
|
||||||
|
- 详细的统计信息
|
||||||
|
- 类型安全的泛型设计
|
||||||
|
- 与 Godot PackedScene 无缝集成
|
||||||
|
|
||||||
|
**性能优势**:
|
||||||
|
|
||||||
|
- 减少内存分配和垃圾回收
|
||||||
|
- 降低节点实例化开销
|
||||||
|
- 提高游戏运行时性能
|
||||||
|
- 优化大量对象场景的帧率
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 节点池
|
||||||
|
|
||||||
|
节点池是一个存储可复用节点的容器。当需要节点时从池中获取,使用完毕后归还到池中,而不是销毁。这种复用机制可以显著提升性能。
|
||||||
|
|
||||||
|
### 可池化节点
|
||||||
|
|
||||||
|
实现 `IPoolableNode` 接口的节点可以被对象池管理。接口定义了节点在池中的生命周期回调:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IPoolableNode : IPoolableObject
|
||||||
|
{
|
||||||
|
// 从池中获取时调用
|
||||||
|
void OnAcquire();
|
||||||
|
|
||||||
|
// 归还到池中时调用
|
||||||
|
void OnRelease();
|
||||||
|
|
||||||
|
// 池被销毁时调用
|
||||||
|
void OnPoolDestroy();
|
||||||
|
|
||||||
|
// 转换为 Node 类型
|
||||||
|
Node AsNode();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 节点复用
|
||||||
|
|
||||||
|
节点复用是指重复使用已创建的节点实例,而不是每次都创建新实例。这可以:
|
||||||
|
|
||||||
|
- 减少内存分配
|
||||||
|
- 降低 GC 压力
|
||||||
|
- 提高实例化速度
|
||||||
|
- 优化运行时性能
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 创建可池化节点
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.pool;
|
||||||
|
|
||||||
|
public partial class Bullet : Node2D, IPoolableNode
|
||||||
|
{
|
||||||
|
private Vector2 _velocity;
|
||||||
|
private float _lifetime;
|
||||||
|
|
||||||
|
public void OnAcquire()
|
||||||
|
{
|
||||||
|
// 从池中获取时重置状态
|
||||||
|
_lifetime = 5.0f;
|
||||||
|
Show();
|
||||||
|
SetProcess(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnRelease()
|
||||||
|
{
|
||||||
|
// 归还到池中时清理状态
|
||||||
|
Hide();
|
||||||
|
SetProcess(false);
|
||||||
|
_velocity = Vector2.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPoolDestroy()
|
||||||
|
{
|
||||||
|
// 池被销毁时的清理工作
|
||||||
|
QueueFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node AsNode()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(Vector2 position, Vector2 velocity)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
_velocity = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
Position += _velocity * (float)delta;
|
||||||
|
_lifetime -= (float)delta;
|
||||||
|
|
||||||
|
if (_lifetime <= 0)
|
||||||
|
{
|
||||||
|
// 归还到池中
|
||||||
|
ReturnToPool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReturnToPool()
|
||||||
|
{
|
||||||
|
// 通过池系统归还
|
||||||
|
var poolSystem = this.GetSystem<BulletPoolSystem>();
|
||||||
|
poolSystem.Release("Bullet", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建节点池系统
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.pool;
|
||||||
|
|
||||||
|
public class BulletPoolSystem : AbstractNodePoolSystem<string, Bullet>
|
||||||
|
{
|
||||||
|
protected override PackedScene LoadScene(string key)
|
||||||
|
{
|
||||||
|
// 根据键加载对应的场景
|
||||||
|
return key switch
|
||||||
|
{
|
||||||
|
"Bullet" => GD.Load<PackedScene>("res://prefabs/Bullet.tscn"),
|
||||||
|
"EnemyBullet" => GD.Load<PackedScene>("res://prefabs/EnemyBullet.tscn"),
|
||||||
|
_ => throw new ArgumentException($"Unknown bullet type: {key}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
// 预热池,提前创建一些对象
|
||||||
|
Prewarm("Bullet", 50);
|
||||||
|
Prewarm("EnemyBullet", 30);
|
||||||
|
|
||||||
|
// 设置最大容量
|
||||||
|
SetMaxCapacity("Bullet", 100);
|
||||||
|
SetMaxCapacity("EnemyBullet", 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注册节点池系统
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.architecture;
|
||||||
|
|
||||||
|
public class GameArchitecture : AbstractArchitecture
|
||||||
|
{
|
||||||
|
protected override void InstallModules()
|
||||||
|
{
|
||||||
|
// 注册节点池系统
|
||||||
|
RegisterSystem<BulletPoolSystem>(new BulletPoolSystem());
|
||||||
|
RegisterSystem<EffectPoolSystem>(new EffectPoolSystem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用节点池
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class Player : Node2D
|
||||||
|
{
|
||||||
|
private BulletPoolSystem _bulletPool;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_bulletPool = this.GetSystem<BulletPoolSystem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Shoot()
|
||||||
|
{
|
||||||
|
// 从池中获取子弹
|
||||||
|
var bullet = _bulletPool.Acquire("Bullet");
|
||||||
|
|
||||||
|
// 初始化子弹
|
||||||
|
bullet.Initialize(GlobalPosition, Vector2.Right.Rotated(Rotation) * 500);
|
||||||
|
|
||||||
|
// 添加到场景树
|
||||||
|
GetParent().AddChild(bullet.AsNode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 多类型节点池
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class EffectPoolSystem : AbstractNodePoolSystem<string, PoolableEffect>
|
||||||
|
{
|
||||||
|
protected override PackedScene LoadScene(string key)
|
||||||
|
{
|
||||||
|
return key switch
|
||||||
|
{
|
||||||
|
"Explosion" => GD.Load<PackedScene>("res://effects/Explosion.tscn"),
|
||||||
|
"Hit" => GD.Load<PackedScene>("res://effects/Hit.tscn"),
|
||||||
|
"Smoke" => GD.Load<PackedScene>("res://effects/Smoke.tscn"),
|
||||||
|
"Spark" => GD.Load<PackedScene>("res://effects/Spark.tscn"),
|
||||||
|
_ => throw new ArgumentException($"Unknown effect type: {key}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
// 为不同类型的特效设置不同的池配置
|
||||||
|
Prewarm("Explosion", 10);
|
||||||
|
SetMaxCapacity("Explosion", 20);
|
||||||
|
|
||||||
|
Prewarm("Hit", 20);
|
||||||
|
SetMaxCapacity("Hit", 50);
|
||||||
|
|
||||||
|
Prewarm("Smoke", 15);
|
||||||
|
SetMaxCapacity("Smoke", 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用特效池
|
||||||
|
public partial class Enemy : Node2D
|
||||||
|
{
|
||||||
|
public void Die()
|
||||||
|
{
|
||||||
|
var effectPool = this.GetSystem<EffectPoolSystem>();
|
||||||
|
var explosion = effectPool.Acquire("Explosion");
|
||||||
|
|
||||||
|
explosion.AsNode().GlobalPosition = GlobalPosition;
|
||||||
|
GetParent().AddChild(explosion.AsNode());
|
||||||
|
|
||||||
|
QueueFree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动归还的节点
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class PoolableEffect : Node2D, IPoolableNode
|
||||||
|
{
|
||||||
|
private AnimationPlayer _animationPlayer;
|
||||||
|
private EffectPoolSystem _poolSystem;
|
||||||
|
private string _effectKey;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
|
||||||
|
_poolSystem = this.GetSystem<EffectPoolSystem>();
|
||||||
|
|
||||||
|
// 动画播放完毕后自动归还
|
||||||
|
_animationPlayer.AnimationFinished += OnAnimationFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnAcquire()
|
||||||
|
{
|
||||||
|
Show();
|
||||||
|
_animationPlayer.Play("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnRelease()
|
||||||
|
{
|
||||||
|
Hide();
|
||||||
|
_animationPlayer.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPoolDestroy()
|
||||||
|
{
|
||||||
|
_animationPlayer.AnimationFinished -= OnAnimationFinished;
|
||||||
|
QueueFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node AsNode() => this;
|
||||||
|
|
||||||
|
public void SetEffectKey(string key)
|
||||||
|
{
|
||||||
|
_effectKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAnimationFinished(StringName animName)
|
||||||
|
{
|
||||||
|
// 动画播放完毕,自动归还到池中
|
||||||
|
_poolSystem.Release(_effectKey, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 池容量管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DynamicPoolSystem : AbstractNodePoolSystem<string, PoolableEnemy>
|
||||||
|
{
|
||||||
|
protected override PackedScene LoadScene(string key)
|
||||||
|
{
|
||||||
|
return GD.Load<PackedScene>($"res://enemies/{key}.tscn");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
// 初始配置
|
||||||
|
SetMaxCapacity("Slime", 50);
|
||||||
|
SetMaxCapacity("Goblin", 30);
|
||||||
|
SetMaxCapacity("Boss", 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态调整池容量
|
||||||
|
public void AdjustPoolCapacity(string key, int newCapacity)
|
||||||
|
{
|
||||||
|
var currentSize = GetPoolSize(key);
|
||||||
|
var activeCount = GetActiveCount(key);
|
||||||
|
|
||||||
|
GD.Print($"池 '{key}' 当前状态:");
|
||||||
|
GD.Print($" 可用: {currentSize}");
|
||||||
|
GD.Print($" 活跃: {activeCount}");
|
||||||
|
GD.Print($" 新容量: {newCapacity}");
|
||||||
|
|
||||||
|
SetMaxCapacity(key, newCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据游戏阶段预热
|
||||||
|
public void PrewarmForStage(int stage)
|
||||||
|
{
|
||||||
|
switch (stage)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
Prewarm("Slime", 20);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
Prewarm("Slime", 30);
|
||||||
|
Prewarm("Goblin", 15);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
Prewarm("Goblin", 25);
|
||||||
|
Prewarm("Boss", 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 池统计和监控
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class PoolMonitor : Control
|
||||||
|
{
|
||||||
|
private BulletPoolSystem _bulletPool;
|
||||||
|
private Label _statsLabel;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_bulletPool = this.GetSystem<BulletPoolSystem>();
|
||||||
|
_statsLabel = GetNode<Label>("StatsLabel");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
// 获取统计信息
|
||||||
|
var stats = _bulletPool.GetStatistics("Bullet");
|
||||||
|
|
||||||
|
// 显示统计信息
|
||||||
|
_statsLabel.Text = $@"
|
||||||
|
子弹池统计:
|
||||||
|
可用对象: {stats.AvailableCount}
|
||||||
|
活跃对象: {stats.ActiveCount}
|
||||||
|
最大容量: {stats.MaxCapacity}
|
||||||
|
总创建数: {stats.TotalCreated}
|
||||||
|
总获取数: {stats.TotalAcquired}
|
||||||
|
总释放数: {stats.TotalReleased}
|
||||||
|
总销毁数: {stats.TotalDestroyed}
|
||||||
|
复用率: {CalculateReuseRate(stats):P2}
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateReuseRate(PoolStatistics stats)
|
||||||
|
{
|
||||||
|
if (stats.TotalAcquired == 0) return 0;
|
||||||
|
return 1.0f - (float)stats.TotalCreated / stats.TotalAcquired;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 条件释放和清理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SmartPoolSystem : AbstractNodePoolSystem<string, PoolableNode>
|
||||||
|
{
|
||||||
|
protected override PackedScene LoadScene(string key)
|
||||||
|
{
|
||||||
|
return GD.Load<PackedScene>($"res://poolable/{key}.tscn");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理超出屏幕的对象
|
||||||
|
public void CleanupOffscreenObjects(Rect2 screenRect)
|
||||||
|
{
|
||||||
|
foreach (var pool in Pools)
|
||||||
|
{
|
||||||
|
var stats = GetStatistics(pool.Key);
|
||||||
|
GD.Print($"清理前 '{pool.Key}': 活跃={stats.ActiveCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据内存压力调整池大小
|
||||||
|
public void AdjustForMemoryPressure(float memoryUsage)
|
||||||
|
{
|
||||||
|
if (memoryUsage > 0.8f)
|
||||||
|
{
|
||||||
|
// 内存压力大,减小池容量
|
||||||
|
foreach (var pool in Pools)
|
||||||
|
{
|
||||||
|
var currentCapacity = GetStatistics(pool.Key).MaxCapacity;
|
||||||
|
SetMaxCapacity(pool.Key, Math.Max(10, currentCapacity / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print("内存压力大,减小池容量");
|
||||||
|
}
|
||||||
|
else if (memoryUsage < 0.5f)
|
||||||
|
{
|
||||||
|
// 内存充足,增加池容量
|
||||||
|
foreach (var pool in Pools)
|
||||||
|
{
|
||||||
|
var currentCapacity = GetStatistics(pool.Key).MaxCapacity;
|
||||||
|
SetMaxCapacity(pool.Key, currentCapacity * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print("内存充足,增加池容量");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **在 OnAcquire 中重置状态**:确保对象从池中获取时处于干净状态
|
||||||
|
```csharp
|
||||||
|
public void OnAcquire()
|
||||||
|
{
|
||||||
|
// 重置所有状态
|
||||||
|
Position = Vector2.Zero;
|
||||||
|
Rotation = 0;
|
||||||
|
Scale = Vector2.One;
|
||||||
|
Modulate = Colors.White;
|
||||||
|
Show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **在 OnRelease 中清理资源**:避免内存泄漏
|
||||||
|
```csharp
|
||||||
|
public void OnRelease()
|
||||||
|
{
|
||||||
|
// 清理引用
|
||||||
|
_target = null;
|
||||||
|
_callbacks.Clear();
|
||||||
|
|
||||||
|
// 停止所有动画和计时器
|
||||||
|
_animationPlayer.Stop();
|
||||||
|
_timer.Stop();
|
||||||
|
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **合理设置池容量**:根据实际需求设置最大容量
|
||||||
|
```csharp
|
||||||
|
// 根据游戏设计设置合理的容量
|
||||||
|
SetMaxCapacity("Bullet", 100); // 屏幕上最多100个子弹
|
||||||
|
SetMaxCapacity("Enemy", 50); // 同时最多50个敌人
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **使用预热优化启动性能**:在游戏开始前预创建对象
|
||||||
|
```csharp
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
// 在加载界面预热池
|
||||||
|
Prewarm("Bullet", 50);
|
||||||
|
Prewarm("Effect", 30);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **及时归还对象**:使用完毕后立即归还到池中
|
||||||
|
```csharp
|
||||||
|
✓ poolSystem.Release("Bullet", bullet); // 使用完立即归还
|
||||||
|
✗ // 忘记归还,导致池耗尽
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **监控池统计信息**:定期检查池的使用情况
|
||||||
|
```csharp
|
||||||
|
var stats = poolSystem.GetStatistics("Bullet");
|
||||||
|
if (stats.ActiveCount > stats.MaxCapacity * 0.9f)
|
||||||
|
{
|
||||||
|
GD.PrintErr("警告:子弹池接近容量上限");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **避免在池中存储过大的对象**:大对象应该按需创建
|
||||||
|
```csharp
|
||||||
|
✓ 小对象:子弹、特效、UI元素
|
||||||
|
✗ 大对象:完整的关卡、大型模型
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 问题:什么时候应该使用节点池?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
以下场景适合使用节点池:
|
||||||
|
|
||||||
|
- 频繁创建和销毁的对象(子弹、特效)
|
||||||
|
- 数量较多的对象(敌人、道具)
|
||||||
|
- 生命周期短的对象(粒子、UI提示)
|
||||||
|
- 性能敏感的场景(移动平台、大量对象)
|
||||||
|
|
||||||
|
不适合使用节点池的场景:
|
||||||
|
|
||||||
|
- 只创建一次的对象(玩家、UI界面)
|
||||||
|
- 数量很少的对象(Boss、关键NPC)
|
||||||
|
- 状态复杂难以重置的对象
|
||||||
|
|
||||||
|
### 问题:如何确定合适的池容量?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
根据游戏实际情况设置:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 1. 测量峰值使用量
|
||||||
|
var stats = poolSystem.GetStatistics("Bullet");
|
||||||
|
GD.Print($"峰值活跃数: {stats.ActiveCount}");
|
||||||
|
|
||||||
|
// 2. 设置容量为峰值的 1.2-1.5 倍
|
||||||
|
SetMaxCapacity("Bullet", (int)(peakCount * 1.3f));
|
||||||
|
|
||||||
|
// 3. 监控并调整
|
||||||
|
if (stats.TotalDestroyed > stats.TotalCreated * 0.1f)
|
||||||
|
{
|
||||||
|
// 销毁过多,容量可能太小
|
||||||
|
SetMaxCapacity("Bullet", stats.MaxCapacity * 2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:对象没有正确归还到池中怎么办?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
检查以下几点:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 1. 确保调用了 Release
|
||||||
|
poolSystem.Release("Bullet", bullet);
|
||||||
|
|
||||||
|
// 2. 检查是否使用了正确的键
|
||||||
|
✓ poolSystem.Release("Bullet", bullet);
|
||||||
|
✗ poolSystem.Release("Enemy", bullet); // 错误的键
|
||||||
|
|
||||||
|
// 3. 避免重复释放
|
||||||
|
if (!_isReleased)
|
||||||
|
{
|
||||||
|
poolSystem.Release("Bullet", this);
|
||||||
|
_isReleased = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 使用统计信息诊断
|
||||||
|
var stats = poolSystem.GetStatistics("Bullet");
|
||||||
|
if (stats.ActiveCount != stats.TotalAcquired - stats.TotalReleased)
|
||||||
|
{
|
||||||
|
GD.PrintErr("检测到对象泄漏");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:池中的对象状态没有正确重置?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
在 OnAcquire 中完整重置所有状态:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void OnAcquire()
|
||||||
|
{
|
||||||
|
// 重置变换
|
||||||
|
Position = Vector2.Zero;
|
||||||
|
Rotation = 0;
|
||||||
|
Scale = Vector2.One;
|
||||||
|
|
||||||
|
// 重置视觉
|
||||||
|
Modulate = Colors.White;
|
||||||
|
Visible = true;
|
||||||
|
|
||||||
|
// 重置物理
|
||||||
|
if (this is RigidBody2D rb)
|
||||||
|
{
|
||||||
|
rb.LinearVelocity = Vector2.Zero;
|
||||||
|
rb.AngularVelocity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置逻辑状态
|
||||||
|
_health = _maxHealth;
|
||||||
|
_isActive = true;
|
||||||
|
|
||||||
|
// 重启动画
|
||||||
|
_animationPlayer.Play("idle");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何处理节点的父子关系?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
在归还前移除父节点:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void ReturnToPool()
|
||||||
|
{
|
||||||
|
// 从场景树中移除
|
||||||
|
if (GetParent() != null)
|
||||||
|
{
|
||||||
|
GetParent().RemoveChild(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归还到池中
|
||||||
|
var poolSystem = this.GetSystem<BulletPoolSystem>();
|
||||||
|
poolSystem.Release("Bullet", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取时重新添加到场景树
|
||||||
|
var bullet = poolSystem.Acquire("Bullet");
|
||||||
|
GetParent().AddChild(bullet.AsNode());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:池系统对性能的提升有多大?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
性能提升取决于具体场景:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 测试代码
|
||||||
|
var stopwatch = new Stopwatch();
|
||||||
|
|
||||||
|
// 不使用池
|
||||||
|
stopwatch.Start();
|
||||||
|
for (int i = 0; i < 1000; i++)
|
||||||
|
{
|
||||||
|
var bullet = scene.Instantiate<Bullet>();
|
||||||
|
bullet.QueueFree();
|
||||||
|
}
|
||||||
|
stopwatch.Stop();
|
||||||
|
GD.Print($"不使用池: {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
// 使用池
|
||||||
|
stopwatch.Restart();
|
||||||
|
for (int i = 0; i < 1000; i++)
|
||||||
|
{
|
||||||
|
var bullet = poolSystem.Acquire("Bullet");
|
||||||
|
poolSystem.Release("Bullet", bullet);
|
||||||
|
}
|
||||||
|
stopwatch.Stop();
|
||||||
|
GD.Print($"使用池: {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
// 典型结果:使用池可以提升 3-10 倍性能
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [对象池系统](/zh-CN/core/pool) - 核心对象池实现
|
||||||
|
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||||
|
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景管理
|
||||||
|
- [性能优化](/zh-CN/best-practices/performance) - 性能优化最佳实践
|
||||||
637
docs/zh-CN/godot/resource.md
Normal file
637
docs/zh-CN/godot/resource.md
Normal file
@ -0,0 +1,637 @@
|
|||||||
|
---
|
||||||
|
title: Godot 资源仓储系统
|
||||||
|
description: Godot 资源仓储系统提供了 Godot Resource 的集中管理和高效加载功能。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot 资源仓储系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Godot 资源仓储系统是 GFramework.Godot 中用于管理 Godot Resource
|
||||||
|
资源的核心组件。它提供了基于键值对的资源存储、批量加载、路径扫描等功能,让你可以高效地组织和访问游戏中的各类资源。
|
||||||
|
|
||||||
|
通过资源仓储系统,你可以将 Godot 的 `.tres` 和 `.res` 资源文件集中管理,支持按键快速查找、批量预加载、递归扫描目录等功能,简化资源管理流程。
|
||||||
|
|
||||||
|
**主要特性**:
|
||||||
|
|
||||||
|
- 基于键值对的资源管理
|
||||||
|
- 支持 Godot Resource 类型
|
||||||
|
- 路径扫描和批量加载
|
||||||
|
- 递归目录遍历
|
||||||
|
- 类型安全的资源访问
|
||||||
|
- 与 GFramework 架构集成
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 资源仓储接口
|
||||||
|
|
||||||
|
`IResourceRepository<TKey, TResource>` 定义了资源仓储的基本操作:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IResourceRepository<in TKey, TResource> : IRepository<TKey, TResource>
|
||||||
|
where TResource : Resource
|
||||||
|
{
|
||||||
|
void LoadFromPath(IEnumerable<string> paths);
|
||||||
|
void LoadFromPath(params string[] paths);
|
||||||
|
void LoadFromPathRecursive(IEnumerable<string> paths);
|
||||||
|
void LoadFromPathRecursive(params string[] paths);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源仓储实现
|
||||||
|
|
||||||
|
`GodotResourceRepository<TKey, TResource>` 提供了完整的实现:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GodotResourceRepository<TKey, TResource>
|
||||||
|
: IResourceRepository<TKey, TResource>
|
||||||
|
where TResource : Resource, IHasKey<TKey>
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
public void Add(TKey key, TResource value);
|
||||||
|
public TResource Get(TKey key);
|
||||||
|
public bool TryGet(TKey key, out TResource value);
|
||||||
|
public IReadOnlyCollection<TResource> GetAll();
|
||||||
|
public bool Contains(TKey key);
|
||||||
|
public void Remove(TKey key);
|
||||||
|
public void Clear();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源键接口
|
||||||
|
|
||||||
|
资源必须实现 `IHasKey<TKey>` 接口:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IHasKey<out TKey>
|
||||||
|
{
|
||||||
|
TKey Key { get; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 定义资源类型
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Core.Abstractions.bases;
|
||||||
|
|
||||||
|
// 定义资源数据类
|
||||||
|
[GlobalClass]
|
||||||
|
public partial class ItemData : Resource, IHasKey<string>
|
||||||
|
{
|
||||||
|
[Export]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public Texture2D Icon { get; set; }
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public int MaxStack { get; set; } = 99;
|
||||||
|
|
||||||
|
// 实现 IHasKey 接口
|
||||||
|
public string Key => Id;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建资源仓储
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.data;
|
||||||
|
|
||||||
|
public class ItemRepository : GodotResourceRepository<string, ItemData>
|
||||||
|
{
|
||||||
|
public ItemRepository()
|
||||||
|
{
|
||||||
|
// 从指定路径加载所有物品资源
|
||||||
|
LoadFromPath("res://data/items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注册到架构
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.architecture;
|
||||||
|
|
||||||
|
public class GameArchitecture : AbstractArchitecture
|
||||||
|
{
|
||||||
|
protected override void InstallModules()
|
||||||
|
{
|
||||||
|
// 注册物品仓储
|
||||||
|
var itemRepo = new ItemRepository();
|
||||||
|
RegisterUtility<IResourceRepository<string, ItemData>>(itemRepo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用资源仓储
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
using GFramework.Godot.extensions;
|
||||||
|
|
||||||
|
public partial class InventoryController : Node
|
||||||
|
{
|
||||||
|
private IResourceRepository<string, ItemData> _itemRepo;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 获取资源仓储
|
||||||
|
_itemRepo = this.GetUtility<IResourceRepository<string, ItemData>>();
|
||||||
|
|
||||||
|
// 使用资源
|
||||||
|
ShowItemInfo("sword_001");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowItemInfo(string itemId)
|
||||||
|
{
|
||||||
|
// 获取物品数据
|
||||||
|
if (_itemRepo.TryGet(itemId, out var itemData))
|
||||||
|
{
|
||||||
|
GD.Print($"物品: {itemData.Name}");
|
||||||
|
GD.Print($"描述: {itemData.Description}");
|
||||||
|
GD.Print($"最大堆叠: {itemData.MaxStack}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GD.Print($"物品 {itemId} 不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 递归加载资源
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AssetRepository : GodotResourceRepository<string, AssetData>
|
||||||
|
{
|
||||||
|
public AssetRepository()
|
||||||
|
{
|
||||||
|
// 递归加载所有子目录中的资源
|
||||||
|
LoadFromPathRecursive("res://assets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多路径加载
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConfigRepository : GodotResourceRepository<string, ConfigData>
|
||||||
|
{
|
||||||
|
public ConfigRepository()
|
||||||
|
{
|
||||||
|
// 从多个路径加载资源
|
||||||
|
LoadFromPath(
|
||||||
|
"res://config/gameplay",
|
||||||
|
"res://config/ui",
|
||||||
|
"res://config/audio"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 动态添加资源
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class ResourceManager : Node
|
||||||
|
{
|
||||||
|
private IResourceRepository<string, ItemData> _itemRepo;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_itemRepo = this.GetUtility<IResourceRepository<string, ItemData>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCustomItem(ItemData item)
|
||||||
|
{
|
||||||
|
// 动态添加资源
|
||||||
|
_itemRepo.Add(item.Id, item);
|
||||||
|
GD.Print($"添加物品: {item.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveItem(string itemId)
|
||||||
|
{
|
||||||
|
// 移除资源
|
||||||
|
if (_itemRepo.Contains(itemId))
|
||||||
|
{
|
||||||
|
_itemRepo.Remove(itemId);
|
||||||
|
GD.Print($"移除物品: {itemId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取所有资源
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class ItemListUI : Control
|
||||||
|
{
|
||||||
|
private IResourceRepository<string, ItemData> _itemRepo;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_itemRepo = this.GetUtility<IResourceRepository<string, ItemData>>();
|
||||||
|
DisplayAllItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisplayAllItems()
|
||||||
|
{
|
||||||
|
// 获取所有物品
|
||||||
|
var allItems = _itemRepo.GetAll();
|
||||||
|
|
||||||
|
GD.Print($"共有 {allItems.Count} 个物品:");
|
||||||
|
foreach (var item in allItems)
|
||||||
|
{
|
||||||
|
GD.Print($"- {item.Name} ({item.Id})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源预加载
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class GameInitializer : Node
|
||||||
|
{
|
||||||
|
public override async void _Ready()
|
||||||
|
{
|
||||||
|
await PreloadAllResources();
|
||||||
|
GD.Print("所有资源预加载完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PreloadAllResources()
|
||||||
|
{
|
||||||
|
// 预加载物品资源
|
||||||
|
var itemRepo = new ItemRepository();
|
||||||
|
this.RegisterUtility<IResourceRepository<string, ItemData>>(itemRepo);
|
||||||
|
|
||||||
|
// 预加载技能资源
|
||||||
|
var skillRepo = new SkillRepository();
|
||||||
|
this.RegisterUtility<IResourceRepository<string, SkillData>>(skillRepo);
|
||||||
|
|
||||||
|
// 预加载敌人资源
|
||||||
|
var enemyRepo = new EnemyRepository();
|
||||||
|
this.RegisterUtility<IResourceRepository<string, EnemyData>>(enemyRepo);
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源缓存管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CachedResourceRepository<TKey, TResource>
|
||||||
|
where TResource : Resource, IHasKey<TKey>
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
private readonly GodotResourceRepository<TKey, TResource> _repository;
|
||||||
|
private readonly Dictionary<TKey, TResource> _cache = new();
|
||||||
|
|
||||||
|
public CachedResourceRepository(params string[] paths)
|
||||||
|
{
|
||||||
|
_repository = new GodotResourceRepository<TKey, TResource>();
|
||||||
|
_repository.LoadFromPath(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TResource Get(TKey key)
|
||||||
|
{
|
||||||
|
// 先从缓存获取
|
||||||
|
if (_cache.TryGetValue(key, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从仓储获取并缓存
|
||||||
|
var resource = _repository.Get(key);
|
||||||
|
_cache[key] = resource;
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearCache()
|
||||||
|
{
|
||||||
|
_cache.Clear();
|
||||||
|
GD.Print("资源缓存已清空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源版本管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[GlobalClass]
|
||||||
|
public partial class VersionedItemData : Resource, IHasKey<string>
|
||||||
|
{
|
||||||
|
[Export]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
public string Key => Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VersionedItemRepository : GodotResourceRepository<string, VersionedItemData>
|
||||||
|
{
|
||||||
|
public VersionedItemRepository()
|
||||||
|
{
|
||||||
|
LoadFromPath("res://data/items");
|
||||||
|
ValidateVersions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateVersions()
|
||||||
|
{
|
||||||
|
var allItems = GetAll();
|
||||||
|
foreach (var item in allItems)
|
||||||
|
{
|
||||||
|
if (item.Version < 2)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"物品 {item.Id} 版本过旧: v{item.Version}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多类型资源管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 武器资源
|
||||||
|
[GlobalClass]
|
||||||
|
public partial class WeaponData : Resource, IHasKey<string>
|
||||||
|
{
|
||||||
|
[Export] public string Id { get; set; }
|
||||||
|
[Export] public string Name { get; set; }
|
||||||
|
[Export] public int Damage { get; set; }
|
||||||
|
public string Key => Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 护甲资源
|
||||||
|
[GlobalClass]
|
||||||
|
public partial class ArmorData : Resource, IHasKey<string>
|
||||||
|
{
|
||||||
|
[Export] public string Id { get; set; }
|
||||||
|
[Export] public string Name { get; set; }
|
||||||
|
[Export] public int Defense { get; set; }
|
||||||
|
public string Key => Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一管理
|
||||||
|
public class EquipmentManager
|
||||||
|
{
|
||||||
|
private readonly IResourceRepository<string, WeaponData> _weaponRepo;
|
||||||
|
private readonly IResourceRepository<string, ArmorData> _armorRepo;
|
||||||
|
|
||||||
|
public EquipmentManager(
|
||||||
|
IResourceRepository<string, WeaponData> weaponRepo,
|
||||||
|
IResourceRepository<string, ArmorData> armorRepo)
|
||||||
|
{
|
||||||
|
_weaponRepo = weaponRepo;
|
||||||
|
_armorRepo = armorRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowAllEquipment()
|
||||||
|
{
|
||||||
|
GD.Print("=== 武器 ===");
|
||||||
|
foreach (var weapon in _weaponRepo.GetAll())
|
||||||
|
{
|
||||||
|
GD.Print($"{weapon.Name}: 伤害 {weapon.Damage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print("=== 护甲 ===");
|
||||||
|
foreach (var armor in _armorRepo.GetAll())
|
||||||
|
{
|
||||||
|
GD.Print($"{armor.Name}: 防御 {armor.Defense}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源热重载
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class HotReloadManager : Node
|
||||||
|
{
|
||||||
|
private IResourceRepository<string, ItemData> _itemRepo;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_itemRepo = this.GetUtility<IResourceRepository<string, ItemData>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReloadResources()
|
||||||
|
{
|
||||||
|
// 清空现有资源
|
||||||
|
_itemRepo.Clear();
|
||||||
|
|
||||||
|
// 重新加载
|
||||||
|
var repo = _itemRepo as GodotResourceRepository<string, ItemData>;
|
||||||
|
repo?.LoadFromPath("res://data/items");
|
||||||
|
|
||||||
|
GD.Print("资源已重新加载");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Input(InputEvent @event)
|
||||||
|
{
|
||||||
|
// 按 F5 热重载
|
||||||
|
if (@event is InputEventKey keyEvent &&
|
||||||
|
keyEvent.Pressed &&
|
||||||
|
keyEvent.Keycode == Key.F5)
|
||||||
|
{
|
||||||
|
ReloadResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **资源实现 IHasKey 接口**:确保资源可以被仓储管理
|
||||||
|
```csharp
|
||||||
|
✓ public partial class ItemData : Resource, IHasKey<string> { }
|
||||||
|
✗ public partial class ItemData : Resource { } // 无法使用仓储
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **使用有意义的键类型**:根据业务需求选择合适的键类型
|
||||||
|
```csharp
|
||||||
|
✓ IResourceRepository<string, ItemData> // 字符串 ID
|
||||||
|
✓ IResourceRepository<int, LevelData> // 整数关卡号
|
||||||
|
✓ IResourceRepository<Guid, SaveData> // GUID 唯一标识
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **在架构初始化时加载资源**:避免运行时加载卡顿
|
||||||
|
```csharp
|
||||||
|
protected override void InstallModules()
|
||||||
|
{
|
||||||
|
var itemRepo = new ItemRepository();
|
||||||
|
RegisterUtility<IResourceRepository<string, ItemData>>(itemRepo);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **使用递归加载组织资源**:保持目录结构清晰
|
||||||
|
```csharp
|
||||||
|
// 推荐的目录结构
|
||||||
|
res://data/
|
||||||
|
├── items/
|
||||||
|
│ ├── weapons/
|
||||||
|
│ ├── armors/
|
||||||
|
│ └── consumables/
|
||||||
|
└── enemies/
|
||||||
|
├── bosses/
|
||||||
|
└── minions/
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **处理资源不存在的情况**:使用 TryGet 避免异常
|
||||||
|
```csharp
|
||||||
|
✓ if (_itemRepo.TryGet(itemId, out var item)) { }
|
||||||
|
✗ var item = _itemRepo.Get(itemId); // 可能抛出异常
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **合理使用资源缓存**:平衡内存和性能
|
||||||
|
```csharp
|
||||||
|
// 频繁访问的资源可以缓存
|
||||||
|
private ItemData _cachedPlayerWeapon;
|
||||||
|
|
||||||
|
public ItemData GetPlayerWeapon()
|
||||||
|
{
|
||||||
|
return _cachedPlayerWeapon ??= _itemRepo.Get("player_weapon");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 问题:如何让资源支持仓储管理?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
资源类必须实现 `IHasKey<TKey>` 接口:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[GlobalClass]
|
||||||
|
public partial class MyResource : Resource, IHasKey<string>
|
||||||
|
{
|
||||||
|
[Export]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Key => Id;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:资源文件必须是什么格式?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
资源仓储支持 Godot 的 `.tres` 和 `.res` 文件格式:
|
||||||
|
|
||||||
|
- `.tres`:文本格式,可读性好,适合版本控制
|
||||||
|
- `.res`:二进制格式,加载更快,适合发布版本
|
||||||
|
|
||||||
|
### 问题:如何组织资源目录结构?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
推荐按类型和功能组织:
|
||||||
|
|
||||||
|
```
|
||||||
|
res://data/
|
||||||
|
├── items/ # 物品资源
|
||||||
|
│ ├── weapons/
|
||||||
|
│ ├── armors/
|
||||||
|
│ └── consumables/
|
||||||
|
├── enemies/ # 敌人资源
|
||||||
|
├── skills/ # 技能资源
|
||||||
|
└── levels/ # 关卡资源
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:资源加载会阻塞主线程吗?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
`LoadFromPath` 是同步操作,建议在游戏初始化时加载:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 在游戏启动时加载
|
||||||
|
var itemRepo = new ItemRepository();
|
||||||
|
RegisterUtility(itemRepo);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何处理重复的资源键?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
仓储会记录警告但不会抛出异常:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 日志会显示: "Duplicate key detected: item_001"
|
||||||
|
// 后加载的资源会被忽略
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:可以动态添加和移除资源吗?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
可以,使用 `Add` 和 `Remove` 方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 添加资源
|
||||||
|
_itemRepo.Add("new_item", newItemData);
|
||||||
|
|
||||||
|
// 移除资源
|
||||||
|
_itemRepo.Remove("old_item");
|
||||||
|
|
||||||
|
// 清空所有资源
|
||||||
|
_itemRepo.Clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:如何实现资源的延迟加载?
|
||||||
|
|
||||||
|
**解答**:
|
||||||
|
可以创建包装类实现延迟加载:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class LazyResourceRepository<TKey, TResource>
|
||||||
|
where TResource : Resource, IHasKey<TKey>
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
private GodotResourceRepository<TKey, TResource> _repository;
|
||||||
|
private readonly string[] _paths;
|
||||||
|
private bool _loaded;
|
||||||
|
|
||||||
|
public LazyResourceRepository(params string[] paths)
|
||||||
|
{
|
||||||
|
_paths = paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureLoaded()
|
||||||
|
{
|
||||||
|
if (_loaded) return;
|
||||||
|
|
||||||
|
_repository = new GodotResourceRepository<TKey, TResource>();
|
||||||
|
_repository.LoadFromPath(_paths);
|
||||||
|
_loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TResource Get(TKey key)
|
||||||
|
{
|
||||||
|
EnsureLoaded();
|
||||||
|
return _repository.Get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||||||
|
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||||
|
- [Godot 场景系统](/zh-CN/godot/scene) - 场景资源管理
|
||||||
|
- [资源管理系统](/zh-CN/core/resource) - 核心资源管理
|
||||||
785
docs/zh-CN/tutorials/data-migration.md
Normal file
785
docs/zh-CN/tutorials/data-migration.md
Normal file
@ -0,0 +1,785 @@
|
|||||||
|
---
|
||||||
|
title: 实现数据版本迁移
|
||||||
|
description: 学习如何实现数据版本迁移系统,处理不同版本间的数据升级
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现数据版本迁移
|
||||||
|
|
||||||
|
## 学习目标
|
||||||
|
|
||||||
|
完成本教程后,你将能够:
|
||||||
|
|
||||||
|
- 理解数据版本迁移的重要性和应用场景
|
||||||
|
- 定义版本化数据结构
|
||||||
|
- 实现数据迁移接口
|
||||||
|
- 注册和管理迁移策略
|
||||||
|
- 处理多版本连续升级
|
||||||
|
- 测试迁移流程的正确性
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
- 已安装 GFramework.Game NuGet 包
|
||||||
|
- 了解 C# 基础语法和接口实现
|
||||||
|
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
|
||||||
|
- 了解[数据与存档系统](/zh-CN/game/data)
|
||||||
|
- 建议先完成[实现存档系统](/zh-CN/tutorials/save-system)教程
|
||||||
|
|
||||||
|
## 为什么需要数据迁移
|
||||||
|
|
||||||
|
在游戏开发过程中,数据结构经常会发生变化:
|
||||||
|
|
||||||
|
- **新增功能**:添加新的游戏系统需要新的数据字段
|
||||||
|
- **重构优化**:改进数据结构以提升性能或可维护性
|
||||||
|
- **修复问题**:修正早期设计的缺陷
|
||||||
|
- **平衡调整**:调整游戏数值和配置
|
||||||
|
|
||||||
|
数据迁移系统能够:
|
||||||
|
|
||||||
|
- 自动将旧版本数据升级到新版本
|
||||||
|
- 保证玩家存档的兼容性
|
||||||
|
- 避免数据丢失和游戏崩溃
|
||||||
|
- 提供平滑的版本过渡体验
|
||||||
|
|
||||||
|
## 步骤 1:定义版本化数据结构
|
||||||
|
|
||||||
|
首先,让我们定义一个支持版本控制的游戏数据结构。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Game.Abstractions.data;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MyGame.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 玩家数据 - 版本 1(初始版本)
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerSaveData : IVersionedData
|
||||||
|
{
|
||||||
|
// IVersionedData 接口要求的属性
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
public DateTime LastModified { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
// 基础数据
|
||||||
|
public string PlayerName { get; set; } = "Player";
|
||||||
|
public int Level { get; set; } = 1;
|
||||||
|
public int Gold { get; set; } = 0;
|
||||||
|
|
||||||
|
// 版本 1 的简单位置数据
|
||||||
|
public float PositionX { get; set; }
|
||||||
|
public float PositionY { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- 实现 `IVersionedData` 接口以支持版本管理
|
||||||
|
- `Version` 属性标识当前数据版本(从 1 开始)
|
||||||
|
- `LastModified` 记录最后修改时间
|
||||||
|
- 初始版本使用简单的 X、Y 坐标表示位置
|
||||||
|
|
||||||
|
## 步骤 2:定义新版本数据结构
|
||||||
|
|
||||||
|
随着游戏开发,我们需要添加新功能,数据结构也需要升级。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace MyGame.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 玩家数据 - 版本 2(添加 Z 轴和经验值)
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerSaveDataV2 : IVersionedData
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 2;
|
||||||
|
public DateTime LastModified { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
// 基础数据
|
||||||
|
public string PlayerName { get; set; } = "Player";
|
||||||
|
public int Level { get; set; } = 1;
|
||||||
|
public int Gold { get; set; } = 0;
|
||||||
|
|
||||||
|
// 版本 2:添加 Z 轴支持 3D 游戏
|
||||||
|
public float PositionX { get; set; }
|
||||||
|
public float PositionY { get; set; }
|
||||||
|
public float PositionZ { get; set; } // 新增
|
||||||
|
|
||||||
|
// 版本 2:添加经验值系统
|
||||||
|
public int Experience { get; set; } // 新增
|
||||||
|
public int ExperienceToNextLevel { get; set; } = 100; // 新增
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 玩家数据 - 版本 3(重构为结构化数据)
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerSaveDataV3 : IVersionedData
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 3;
|
||||||
|
public DateTime LastModified { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
// 基础数据
|
||||||
|
public string PlayerName { get; set; } = "Player";
|
||||||
|
public int Level { get; set; } = 1;
|
||||||
|
public int Gold { get; set; } = 0;
|
||||||
|
|
||||||
|
// 版本 3:使用结构化的位置数据
|
||||||
|
public Vector3Data Position { get; set; } = new();
|
||||||
|
|
||||||
|
// 版本 3:使用结构化的经验值数据
|
||||||
|
public ExperienceData Experience { get; set; } = new();
|
||||||
|
|
||||||
|
// 版本 3:新增技能系统
|
||||||
|
public List<string> UnlockedSkills { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 3D 位置数据
|
||||||
|
/// </summary>
|
||||||
|
public class Vector3Data
|
||||||
|
{
|
||||||
|
public float X { get; set; }
|
||||||
|
public float Y { get; set; }
|
||||||
|
public float Z { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经验值数据
|
||||||
|
/// </summary>
|
||||||
|
public class ExperienceData
|
||||||
|
{
|
||||||
|
public int Current { get; set; }
|
||||||
|
public int ToNextLevel { get; set; } = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- **版本 2**:添加 Z 轴坐标和经验值系统
|
||||||
|
- **版本 3**:重构为更清晰的结构化数据
|
||||||
|
- 每个版本的 `Version` 属性递增
|
||||||
|
- 保持向后兼容,新字段提供默认值
|
||||||
|
|
||||||
|
## 步骤 3:实现数据迁移器
|
||||||
|
|
||||||
|
创建迁移器来处理版本间的数据转换。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Game.Abstractions.setting;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MyGame.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 从版本 1 迁移到版本 2
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerDataMigration_V1_to_V2 : ISettingsMigration
|
||||||
|
{
|
||||||
|
public Type SettingsType => typeof(PlayerSaveData);
|
||||||
|
public int FromVersion => 1;
|
||||||
|
public int ToVersion => 2;
|
||||||
|
|
||||||
|
public ISettingsSection Migrate(ISettingsSection oldData)
|
||||||
|
{
|
||||||
|
if (oldData is not PlayerSaveData v1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Expected PlayerSaveData, got {oldData.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[迁移] 版本 1 -> 2: {v1.PlayerName}");
|
||||||
|
|
||||||
|
// 创建版本 2 数据
|
||||||
|
var v2 = new PlayerSaveDataV2
|
||||||
|
{
|
||||||
|
Version = 2,
|
||||||
|
LastModified = DateTime.Now,
|
||||||
|
|
||||||
|
// 复制现有数据
|
||||||
|
PlayerName = v1.PlayerName,
|
||||||
|
Level = v1.Level,
|
||||||
|
Gold = v1.Gold,
|
||||||
|
PositionX = v1.PositionX,
|
||||||
|
PositionY = v1.PositionY,
|
||||||
|
|
||||||
|
// 新字段:Z 轴默认为 0
|
||||||
|
PositionZ = 0f,
|
||||||
|
|
||||||
|
// 新字段:根据等级计算经验值
|
||||||
|
Experience = 0,
|
||||||
|
ExperienceToNextLevel = 100 * v1.Level
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine($" - 添加 Z 轴坐标: {v2.PositionZ}");
|
||||||
|
Console.WriteLine($" - 初始化经验值系统: {v2.Experience}/{v2.ExperienceToNextLevel}");
|
||||||
|
|
||||||
|
return v2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从版本 2 迁移到版本 3
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerDataMigration_V2_to_V3 : ISettingsMigration
|
||||||
|
{
|
||||||
|
public Type SettingsType => typeof(PlayerSaveDataV2);
|
||||||
|
public int FromVersion => 2;
|
||||||
|
public int ToVersion => 3;
|
||||||
|
|
||||||
|
public ISettingsSection Migrate(ISettingsSection oldData)
|
||||||
|
{
|
||||||
|
if (oldData is not PlayerSaveDataV2 v2)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Expected PlayerSaveDataV2, got {oldData.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[迁移] 版本 2 -> 3: {v2.PlayerName}");
|
||||||
|
|
||||||
|
// 创建版本 3 数据
|
||||||
|
var v3 = new PlayerSaveDataV3
|
||||||
|
{
|
||||||
|
Version = 3,
|
||||||
|
LastModified = DateTime.Now,
|
||||||
|
|
||||||
|
// 复制基础数据
|
||||||
|
PlayerName = v2.PlayerName,
|
||||||
|
Level = v2.Level,
|
||||||
|
Gold = v2.Gold,
|
||||||
|
|
||||||
|
// 迁移位置数据到结构化格式
|
||||||
|
Position = new Vector3Data
|
||||||
|
{
|
||||||
|
X = v2.PositionX,
|
||||||
|
Y = v2.PositionY,
|
||||||
|
Z = v2.PositionZ
|
||||||
|
},
|
||||||
|
|
||||||
|
// 迁移经验值数据到结构化格式
|
||||||
|
Experience = new ExperienceData
|
||||||
|
{
|
||||||
|
Current = v2.Experience,
|
||||||
|
ToNextLevel = v2.ExperienceToNextLevel
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新字段:根据等级解锁基础技能
|
||||||
|
UnlockedSkills = GenerateDefaultSkills(v2.Level)
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine($" - 重构位置数据: ({v3.Position.X}, {v3.Position.Y}, {v3.Position.Z})");
|
||||||
|
Console.WriteLine($" - 重构经验值数据: {v3.Experience.Current}/{v3.Experience.ToNextLevel}");
|
||||||
|
Console.WriteLine($" - 初始化技能系统: {v3.UnlockedSkills.Count} 个技能");
|
||||||
|
|
||||||
|
return v3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据等级生成默认技能
|
||||||
|
/// </summary>
|
||||||
|
private List<string> GenerateDefaultSkills(int level)
|
||||||
|
{
|
||||||
|
var skills = new List<string> { "basic_attack" };
|
||||||
|
|
||||||
|
if (level >= 5)
|
||||||
|
skills.Add("power_strike");
|
||||||
|
|
||||||
|
if (level >= 10)
|
||||||
|
skills.Add("shield_block");
|
||||||
|
|
||||||
|
if (level >= 15)
|
||||||
|
skills.Add("ultimate_skill");
|
||||||
|
|
||||||
|
return skills;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- 实现 `ISettingsMigration` 接口
|
||||||
|
- `SettingsType` 指定要迁移的数据类型
|
||||||
|
- `FromVersion` 和 `ToVersion` 定义迁移的版本范围
|
||||||
|
- `Migrate` 方法执行实际的数据转换
|
||||||
|
- 为新字段提供合理的默认值或计算值
|
||||||
|
- 添加日志输出便于调试
|
||||||
|
|
||||||
|
## 步骤 4:注册迁移策略
|
||||||
|
|
||||||
|
创建迁移管理器来注册和执行迁移。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Game.Abstractions.setting;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace MyGame.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据迁移管理器
|
||||||
|
/// </summary>
|
||||||
|
public class DataMigrationManager
|
||||||
|
{
|
||||||
|
private readonly Dictionary<(Type type, int from), ISettingsMigration> _migrations = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册迁移器
|
||||||
|
/// </summary>
|
||||||
|
public void RegisterMigration(ISettingsMigration migration)
|
||||||
|
{
|
||||||
|
var key = (migration.SettingsType, migration.FromVersion);
|
||||||
|
|
||||||
|
if (_migrations.ContainsKey(key))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"警告: 迁移器已存在 {migration.SettingsType.Name} " +
|
||||||
|
$"v{migration.FromVersion}->v{migration.ToVersion}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_migrations[key] = migration;
|
||||||
|
Console.WriteLine($"注册迁移器: {migration.SettingsType.Name} " +
|
||||||
|
$"v{migration.FromVersion} -> v{migration.ToVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行迁移(支持跨多个版本)
|
||||||
|
/// </summary>
|
||||||
|
public ISettingsSection MigrateToLatest(ISettingsSection data, int targetVersion)
|
||||||
|
{
|
||||||
|
if (data is not IVersionedData versioned)
|
||||||
|
{
|
||||||
|
Console.WriteLine("数据不支持版本控制,跳过迁移");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentVersion = versioned.Version;
|
||||||
|
|
||||||
|
if (currentVersion == targetVersion)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"数据已是最新版本 v{targetVersion}");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVersion > targetVersion)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"警告: 数据版本 v{currentVersion} 高于目标版本 v{targetVersion}");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"\n开始迁移: v{currentVersion} -> v{targetVersion}");
|
||||||
|
|
||||||
|
var current = data;
|
||||||
|
var currentVer = currentVersion;
|
||||||
|
|
||||||
|
// 逐步迁移到目标版本
|
||||||
|
while (currentVer < targetVersion)
|
||||||
|
{
|
||||||
|
var key = (current.GetType(), currentVer);
|
||||||
|
|
||||||
|
if (!_migrations.TryGetValue(key, out var migration))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"错误: 找不到迁移器 {current.GetType().Name} v{currentVer}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = migration.Migrate(current);
|
||||||
|
currentVer = migration.ToVersion;
|
||||||
|
Console.WriteLine($"迁移完成: v{currentVersion} -> v{currentVer}\n");
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取迁移路径
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetMigrationPath(Type dataType, int fromVersion, int toVersion)
|
||||||
|
{
|
||||||
|
var path = new List<string>();
|
||||||
|
var currentVer = fromVersion;
|
||||||
|
var currentType = dataType;
|
||||||
|
|
||||||
|
while (currentVer < toVersion)
|
||||||
|
{
|
||||||
|
var key = (currentType, currentVer);
|
||||||
|
|
||||||
|
if (!_migrations.TryGetValue(key, out var migration))
|
||||||
|
{
|
||||||
|
path.Add($"v{currentVer} -> ? (缺失迁移器)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.Add($"v{migration.FromVersion} -> v{migration.ToVersion}");
|
||||||
|
currentVer = migration.ToVersion;
|
||||||
|
currentType = migration.SettingsType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- 使用字典存储迁移器,键为 (类型, 源版本)
|
||||||
|
- `RegisterMigration` 注册单个迁移器
|
||||||
|
- `MigrateToLatest` 自动执行多步迁移
|
||||||
|
- `GetMigrationPath` 显示迁移路径,便于调试
|
||||||
|
- 支持跨多个版本的连续迁移
|
||||||
|
|
||||||
|
## 步骤 5:测试迁移流程
|
||||||
|
|
||||||
|
创建完整的测试程序验证迁移功能。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using MyGame.Data;
|
||||||
|
using MyGame.Data.Migrations;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MyGame
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("=== 数据迁移系统测试 ===\n");
|
||||||
|
|
||||||
|
// 1. 创建迁移管理器
|
||||||
|
var migrationManager = new DataMigrationManager();
|
||||||
|
|
||||||
|
// 2. 注册所有迁移器
|
||||||
|
Console.WriteLine("--- 注册迁移器 ---");
|
||||||
|
migrationManager.RegisterMigration(new PlayerDataMigration_V1_to_V2());
|
||||||
|
migrationManager.RegisterMigration(new PlayerDataMigration_V2_to_V3());
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 3. 测试场景 1:从版本 1 迁移到版本 3
|
||||||
|
Console.WriteLine("--- 测试 1: V1 -> V3 迁移 ---");
|
||||||
|
var v1Data = new PlayerSaveData
|
||||||
|
{
|
||||||
|
Version = 1,
|
||||||
|
PlayerName = "老玩家",
|
||||||
|
Level = 12,
|
||||||
|
Gold = 5000,
|
||||||
|
PositionX = 100.5f,
|
||||||
|
PositionY = 200.3f
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine("原始数据 (V1):");
|
||||||
|
Console.WriteLine($" 玩家: {v1Data.PlayerName}");
|
||||||
|
Console.WriteLine($" 等级: {v1Data.Level}");
|
||||||
|
Console.WriteLine($" 金币: {v1Data.Gold}");
|
||||||
|
Console.WriteLine($" 位置: ({v1Data.PositionX}, {v1Data.PositionY})");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 显示迁移路径
|
||||||
|
var path = migrationManager.GetMigrationPath(typeof(PlayerSaveData), 1, 3);
|
||||||
|
Console.WriteLine("迁移路径:");
|
||||||
|
foreach (var step in path)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {step}");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 执行迁移
|
||||||
|
var v3Data = (PlayerSaveDataV3)migrationManager.MigrateToLatest(v1Data, 3);
|
||||||
|
|
||||||
|
Console.WriteLine("迁移后数据 (V3):");
|
||||||
|
Console.WriteLine($" 玩家: {v3Data.PlayerName}");
|
||||||
|
Console.WriteLine($" 等级: {v3Data.Level}");
|
||||||
|
Console.WriteLine($" 金币: {v3Data.Gold}");
|
||||||
|
Console.WriteLine($" 位置: ({v3Data.Position.X}, {v3Data.Position.Y}, {v3Data.Position.Z})");
|
||||||
|
Console.WriteLine($" 经验值: {v3Data.Experience.Current}/{v3Data.Experience.ToNextLevel}");
|
||||||
|
Console.WriteLine($" 技能: {string.Join(", ", v3Data.UnlockedSkills)}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 4. 测试场景 2:从版本 2 迁移到版本 3
|
||||||
|
Console.WriteLine("--- 测试 2: V2 -> V3 迁移 ---");
|
||||||
|
var v2Data = new PlayerSaveDataV2
|
||||||
|
{
|
||||||
|
Version = 2,
|
||||||
|
PlayerName = "中期玩家",
|
||||||
|
Level = 8,
|
||||||
|
Gold = 2000,
|
||||||
|
PositionX = 50.0f,
|
||||||
|
PositionY = 75.0f,
|
||||||
|
PositionZ = 10.0f,
|
||||||
|
Experience = 350,
|
||||||
|
ExperienceToNextLevel = 800
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine("原始数据 (V2):");
|
||||||
|
Console.WriteLine($" 玩家: {v2Data.PlayerName}");
|
||||||
|
Console.WriteLine($" 等级: {v2Data.Level}");
|
||||||
|
Console.WriteLine($" 位置: ({v2Data.PositionX}, {v2Data.PositionY}, {v2Data.PositionZ})");
|
||||||
|
Console.WriteLine($" 经验值: {v2Data.Experience}/{v2Data.ExperienceToNextLevel}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
var v3Data2 = (PlayerSaveDataV3)migrationManager.MigrateToLatest(v2Data, 3);
|
||||||
|
|
||||||
|
Console.WriteLine("迁移后数据 (V3):");
|
||||||
|
Console.WriteLine($" 玩家: {v3Data2.PlayerName}");
|
||||||
|
Console.WriteLine($" 等级: {v3Data2.Level}");
|
||||||
|
Console.WriteLine($" 位置: ({v3Data2.Position.X}, {v3Data2.Position.Y}, {v3Data2.Position.Z})");
|
||||||
|
Console.WriteLine($" 经验值: {v3Data2.Experience.Current}/{v3Data2.Experience.ToNextLevel}");
|
||||||
|
Console.WriteLine($" 技能: {string.Join(", ", v3Data2.UnlockedSkills)}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 5. 测试场景 3:已是最新版本
|
||||||
|
Console.WriteLine("--- 测试 3: 已是最新版本 ---");
|
||||||
|
var v3DataLatest = new PlayerSaveDataV3
|
||||||
|
{
|
||||||
|
Version = 3,
|
||||||
|
PlayerName = "新玩家",
|
||||||
|
Level = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
migrationManager.MigrateToLatest(v3DataLatest, 3);
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine("=== 测试完成 ===");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- 创建不同版本的测试数据
|
||||||
|
- 测试单步迁移(V2 -> V3)
|
||||||
|
- 测试多步迁移(V1 -> V3)
|
||||||
|
- 测试已是最新版本的情况
|
||||||
|
- 显示迁移前后的数据对比
|
||||||
|
|
||||||
|
## 完整代码
|
||||||
|
|
||||||
|
所有代码文件已在上述步骤中提供。项目结构如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
MyGame/
|
||||||
|
├── Data/
|
||||||
|
│ ├── PlayerSaveData.cs # 版本 1 数据结构
|
||||||
|
│ ├── PlayerSaveDataV2.cs # 版本 2 数据结构
|
||||||
|
│ ├── PlayerSaveDataV3.cs # 版本 3 数据结构
|
||||||
|
│ └── Migrations/
|
||||||
|
│ ├── PlayerDataMigration_V1_to_V2.cs
|
||||||
|
│ ├── PlayerDataMigration_V2_to_V3.cs
|
||||||
|
│ └── DataMigrationManager.cs
|
||||||
|
└── Program.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行结果
|
||||||
|
|
||||||
|
运行程序后,你将看到类似以下的输出:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 数据迁移系统测试 ===
|
||||||
|
|
||||||
|
--- 注册迁移器 ---
|
||||||
|
注册迁移器: PlayerSaveData v1 -> v2
|
||||||
|
注册迁移器: PlayerSaveDataV2 v2 -> v3
|
||||||
|
|
||||||
|
--- 测试 1: V1 -> V3 迁移 ---
|
||||||
|
原始数据 (V1):
|
||||||
|
玩家: 老玩家
|
||||||
|
等级: 12
|
||||||
|
金币: 5000
|
||||||
|
位置: (100.5, 200.3)
|
||||||
|
|
||||||
|
迁移路径:
|
||||||
|
v1 -> v2
|
||||||
|
v2 -> v3
|
||||||
|
|
||||||
|
开始迁移: v1 -> v3
|
||||||
|
[迁移] 版本 1 -> 2: 老玩家
|
||||||
|
- 添加 Z 轴坐标: 0
|
||||||
|
- 初始化经验值系统: 0/1200
|
||||||
|
[迁移] 版本 2 -> 3: 老玩家
|
||||||
|
- 重构位置数据: (100.5, 200.3, 0)
|
||||||
|
- 重构经验值数据: 0/1200
|
||||||
|
- 初始化技能系统: 3 个技能
|
||||||
|
迁移完成: v1 -> v3
|
||||||
|
|
||||||
|
迁移后数据 (V3):
|
||||||
|
玩家: 老玩家
|
||||||
|
等级: 12
|
||||||
|
金币: 5000
|
||||||
|
位置: (100.5, 200.3, 0)
|
||||||
|
经验值: 0/1200
|
||||||
|
技能: basic_attack, power_strike, shield_block
|
||||||
|
|
||||||
|
--- 测试 2: V2 -> V3 迁移 ---
|
||||||
|
原始数据 (V2):
|
||||||
|
玩家: 中期玩家
|
||||||
|
等级: 8
|
||||||
|
金币: 2000
|
||||||
|
位置: (50, 75, 10)
|
||||||
|
经验值: 350/800
|
||||||
|
|
||||||
|
开始迁移: v2 -> v3
|
||||||
|
[迁移] 版本 2 -> 3: 中期玩家
|
||||||
|
- 重构位置数据: (50, 75, 10)
|
||||||
|
- 重构经验值数据: 350/800
|
||||||
|
- 初始化技能系统: 2 个技能
|
||||||
|
迁移完成: v2 -> v3
|
||||||
|
|
||||||
|
迁移后数据 (V3):
|
||||||
|
玩家: 中期玩家
|
||||||
|
等级: 8
|
||||||
|
位置: (50, 75, 10)
|
||||||
|
经验值: 350/800
|
||||||
|
技能: basic_attack, power_strike
|
||||||
|
|
||||||
|
--- 测试 3: 已是最新版本 ---
|
||||||
|
数据已是最新版本 v3
|
||||||
|
|
||||||
|
=== 测试完成 ===
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证步骤**:
|
||||||
|
|
||||||
|
1. 迁移器成功注册
|
||||||
|
2. V1 数据正确迁移到 V3
|
||||||
|
3. V2 数据正确迁移到 V3
|
||||||
|
4. 新字段获得合理的默认值
|
||||||
|
5. 已是最新版本的数据不会重复迁移
|
||||||
|
6. 迁移路径清晰可追踪
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
恭喜!你已经实现了一个完整的数据版本迁移系统。接下来可以学习:
|
||||||
|
|
||||||
|
- [实现存档系统](/zh-CN/tutorials/save-system) - 结合存档系统使用迁移
|
||||||
|
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project) - 在实际项目中应用
|
||||||
|
- [数据与存档系统](/zh-CN/game/data) - 深入了解数据系统
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 版本号管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 使用常量管理版本号
|
||||||
|
public static class DataVersions
|
||||||
|
{
|
||||||
|
public const int PlayerData_V1 = 1;
|
||||||
|
public const int PlayerData_V2 = 2;
|
||||||
|
public const int PlayerData_V3 = 3;
|
||||||
|
public const int PlayerData_Latest = PlayerData_V3;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 迁移测试
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 为每个迁移器编写单元测试
|
||||||
|
[Test]
|
||||||
|
public void TestMigration_V1_to_V2()
|
||||||
|
{
|
||||||
|
var v1 = new PlayerSaveData { Level = 10 };
|
||||||
|
var migration = new PlayerDataMigration_V1_to_V2();
|
||||||
|
var v2 = (PlayerSaveDataV2)migration.Migrate(v1);
|
||||||
|
|
||||||
|
Assert.AreEqual(10, v2.Level);
|
||||||
|
Assert.AreEqual(0, v2.PositionZ);
|
||||||
|
Assert.AreEqual(1000, v2.ExperienceToNextLevel);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 数据备份
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 迁移前自动备份
|
||||||
|
public ISettingsSection MigrateWithBackup(ISettingsSection data)
|
||||||
|
{
|
||||||
|
// 备份原始数据
|
||||||
|
var backup = SerializeData(data);
|
||||||
|
SaveBackup(backup);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 执行迁移
|
||||||
|
var migrated = MigrateToLatest(data, targetVersion);
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 迁移失败,恢复备份
|
||||||
|
RestoreBackup(backup);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 迁移日志
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 记录详细的迁移日志
|
||||||
|
public class MigrationLogger
|
||||||
|
{
|
||||||
|
public void LogMigration(string playerName, int from, int to)
|
||||||
|
{
|
||||||
|
var log = $"[{DateTime.Now}] {playerName}: v{from} -> v{to}";
|
||||||
|
File.AppendAllText("migration.log", log + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 向后兼容
|
||||||
|
|
||||||
|
- 新版本保留所有旧字段
|
||||||
|
- 为新字段提供合理的默认值
|
||||||
|
- 避免删除或重命名字段
|
||||||
|
- 使用 `[Obsolete]` 标记废弃字段
|
||||||
|
|
||||||
|
### 6. 性能优化
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 批量迁移优化
|
||||||
|
public async Task<List<ISettingsSection>> MigrateBatchAsync(
|
||||||
|
List<ISettingsSection> dataList,
|
||||||
|
int targetVersion)
|
||||||
|
{
|
||||||
|
var tasks = dataList.Select(data =>
|
||||||
|
Task.Run(() => MigrateToLatest(data, targetVersion)));
|
||||||
|
|
||||||
|
return (await Task.WhenAll(tasks)).ToList();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. 如何处理跨多个版本的迁移?
|
||||||
|
|
||||||
|
迁移管理器会自动按顺序应用所有必要的迁移。例如从 V1 到 V3,会先执行 V1->V2,再执行 V2->V3。
|
||||||
|
|
||||||
|
### 2. 迁移失败如何处理?
|
||||||
|
|
||||||
|
建议在迁移前备份原始数据,迁移失败时可以恢复。同时在迁移过程中添加详细的日志记录。
|
||||||
|
|
||||||
|
### 3. 如何处理不兼容的数据变更?
|
||||||
|
|
||||||
|
对于破坏性变更,建议:
|
||||||
|
|
||||||
|
- 提供数据转换工具
|
||||||
|
- 在迁移中添加数据验证
|
||||||
|
- 通知用户可能的数据丢失
|
||||||
|
- 提供回滚机制
|
||||||
|
|
||||||
|
### 4. 是否需要保留所有历史版本的数据结构?
|
||||||
|
|
||||||
|
建议保留,这样可以:
|
||||||
|
|
||||||
|
- 支持从任意旧版本迁移
|
||||||
|
- 便于调试和测试
|
||||||
|
- 作为文档记录数据演变
|
||||||
|
|
||||||
|
### 5. 如何测试迁移功能?
|
||||||
|
|
||||||
|
- 创建各个版本的测试数据
|
||||||
|
- 验证迁移后的数据完整性
|
||||||
|
- 测试迁移链的正确性
|
||||||
|
- 使用真实的历史数据进行测试
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
|
||||||
|
- [实现存档系统](/zh-CN/tutorials/save-system) - 存档系统教程
|
||||||
|
- [架构系统](/zh-CN/core/architecture) - 架构设计原则
|
||||||
822
docs/zh-CN/tutorials/functional-programming.md
Normal file
822
docs/zh-CN/tutorials/functional-programming.md
Normal file
@ -0,0 +1,822 @@
|
|||||||
|
---
|
||||||
|
title: 函数式编程实践
|
||||||
|
description: 学习如何在实际项目中使用 Option、Result 和管道操作等函数式编程特性
|
||||||
|
---
|
||||||
|
|
||||||
|
# 函数式编程实践
|
||||||
|
|
||||||
|
## 学习目标
|
||||||
|
|
||||||
|
完成本教程后,你将能够:
|
||||||
|
|
||||||
|
- 理解函数式编程的核心概念和优势
|
||||||
|
- 使用 Option 类型安全地处理可空值
|
||||||
|
- 使用 Result 类型进行优雅的错误处理
|
||||||
|
- 使用管道操作构建流式的数据处理流程
|
||||||
|
- 组合多个函数式操作实现复杂的业务逻辑
|
||||||
|
- 在实际游戏开发中应用函数式编程模式
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
- 已安装 GFramework.Core NuGet 包
|
||||||
|
- 了解 C# 基础语法和泛型
|
||||||
|
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
|
||||||
|
- 了解 Lambda 表达式和 LINQ
|
||||||
|
|
||||||
|
## 步骤 1:使用 Option 处理可空值
|
||||||
|
|
||||||
|
首先,让我们学习如何使用 Option 类型替代传统的 null 检查,使代码更加安全和优雅。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional;
|
||||||
|
using GFramework.Core.functional.pipe;
|
||||||
|
|
||||||
|
namespace MyGame.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 玩家数据服务
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerDataService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, PlayerData> _players = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 ID 查找玩家(返回 Option)
|
||||||
|
/// </summary>
|
||||||
|
public Option<PlayerData> FindPlayerById(int playerId)
|
||||||
|
{
|
||||||
|
// 使用 Option 包装可能不存在的值
|
||||||
|
return _players.TryGetValue(playerId, out var player)
|
||||||
|
? Option<PlayerData>.Some(player)
|
||||||
|
: Option<PlayerData>.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取玩家名称(安全处理)
|
||||||
|
/// </summary>
|
||||||
|
public string GetPlayerName(int playerId)
|
||||||
|
{
|
||||||
|
// 使用 Match 模式匹配处理有值和无值的情况
|
||||||
|
return FindPlayerById(playerId).Match(
|
||||||
|
some: player => player.Name,
|
||||||
|
none: () => "未知玩家"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取玩家等级(使用默认值)
|
||||||
|
/// </summary>
|
||||||
|
public int GetPlayerLevel(int playerId)
|
||||||
|
{
|
||||||
|
// 使用 GetOrElse 提供默认值
|
||||||
|
return FindPlayerById(playerId)
|
||||||
|
.Map(player => player.Level)
|
||||||
|
.GetOrElse(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找高级玩家
|
||||||
|
/// </summary>
|
||||||
|
public Option<PlayerData> FindAdvancedPlayer(int playerId)
|
||||||
|
{
|
||||||
|
// 使用 Filter 过滤值
|
||||||
|
return FindPlayerById(playerId)
|
||||||
|
.Filter(player => player.Level >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取玩家公会名称(链式调用)
|
||||||
|
/// </summary>
|
||||||
|
public string GetPlayerGuildName(int playerId)
|
||||||
|
{
|
||||||
|
// 使用 Bind 处理嵌套的 Option
|
||||||
|
return FindPlayerById(playerId)
|
||||||
|
.Bind(player => player.Guild) // Guild 也是 Option<Guild>
|
||||||
|
.Map(guild => guild.Name)
|
||||||
|
.GetOrElse("无公会");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 玩家数据
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerData
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public int Level { get; set; }
|
||||||
|
public Option<Guild> Guild { get; set; } = Option<Guild>.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公会数据
|
||||||
|
/// </summary>
|
||||||
|
public class Guild
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- `Option<T>` 明确表示值可能不存在,避免 NullReferenceException
|
||||||
|
- `Match` 强制处理两种情况,不会遗漏 null 检查
|
||||||
|
- `Map` 和 `Bind` 实现链式转换,代码更简洁
|
||||||
|
- `Filter` 可以安全地过滤值
|
||||||
|
- `GetOrElse` 提供默认值,避免空值传播
|
||||||
|
|
||||||
|
## 步骤 2:使用 Result 进行错误处理
|
||||||
|
|
||||||
|
接下来,学习如何使用 Result 类型替代异常处理,实现更可控的错误管理。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MyGame.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 存档服务
|
||||||
|
/// </summary>
|
||||||
|
public class SaveService
|
||||||
|
{
|
||||||
|
private readonly string _saveDirectory = "./saves";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存游戏数据
|
||||||
|
/// </summary>
|
||||||
|
public Result<string> SaveGame(GameSaveData data)
|
||||||
|
{
|
||||||
|
// 使用 Result.Try 自动捕获异常
|
||||||
|
return Result<string>.Try(() =>
|
||||||
|
{
|
||||||
|
// 验证数据
|
||||||
|
if (string.IsNullOrEmpty(data.PlayerName))
|
||||||
|
throw new ArgumentException("玩家名称不能为空");
|
||||||
|
|
||||||
|
// 创建保存目录
|
||||||
|
if (!Directory.Exists(_saveDirectory))
|
||||||
|
Directory.CreateDirectory(_saveDirectory);
|
||||||
|
|
||||||
|
// 序列化数据
|
||||||
|
var json = JsonSerializer.Serialize(data);
|
||||||
|
var fileName = $"save_{data.PlayerId}_{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
||||||
|
var filePath = Path.Combine(_saveDirectory, fileName);
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加载游戏数据
|
||||||
|
/// </summary>
|
||||||
|
public Result<GameSaveData> LoadGame(int playerId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 查找最新的存档文件
|
||||||
|
var files = Directory.GetFiles(_saveDirectory, $"save_{playerId}_*.json");
|
||||||
|
|
||||||
|
if (files.Length == 0)
|
||||||
|
return Result<GameSaveData>.Failure("未找到存档文件");
|
||||||
|
|
||||||
|
var latestFile = files.OrderByDescending(f => f).First();
|
||||||
|
var json = File.ReadAllText(latestFile);
|
||||||
|
var data = JsonSerializer.Deserialize<GameSaveData>(json);
|
||||||
|
|
||||||
|
return data != null
|
||||||
|
? Result<GameSaveData>.Success(data)
|
||||||
|
: Result<GameSaveData>.Failure("存档数据解析失败");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<GameSaveData>.Failure(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存并加载游戏(链式操作)
|
||||||
|
/// </summary>
|
||||||
|
public Result<GameSaveData> SaveAndReload(GameSaveData data)
|
||||||
|
{
|
||||||
|
// 使用 Bind 链接多个 Result 操作
|
||||||
|
return SaveGame(data)
|
||||||
|
.Bind(_ => LoadGame(data.PlayerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取存档信息(使用 Match)
|
||||||
|
/// </summary>
|
||||||
|
public string GetSaveInfo(int playerId)
|
||||||
|
{
|
||||||
|
return LoadGame(playerId).Match(
|
||||||
|
succ: data => $"存档加载成功: {data.PlayerName}, 等级 {data.Level}",
|
||||||
|
fail: ex => $"加载失败: {ex.Message}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 安全加载游戏(提供默认值)
|
||||||
|
/// </summary>
|
||||||
|
public GameSaveData LoadGameOrDefault(int playerId)
|
||||||
|
{
|
||||||
|
return LoadGame(playerId).IfFail(new GameSaveData
|
||||||
|
{
|
||||||
|
PlayerId = playerId,
|
||||||
|
PlayerName = "新玩家",
|
||||||
|
Level = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 游戏存档数据
|
||||||
|
/// </summary>
|
||||||
|
public class GameSaveData
|
||||||
|
{
|
||||||
|
public int PlayerId { get; set; }
|
||||||
|
public string PlayerName { get; set; } = "";
|
||||||
|
public int Level { get; set; }
|
||||||
|
public int Gold { get; set; }
|
||||||
|
public DateTime SaveTime { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- `Result<T>` 将错误作为值返回,而不是抛出异常
|
||||||
|
- `Result.Try` 自动捕获异常并转换为 Result
|
||||||
|
- `Bind` 可以链接多个可能失败的操作
|
||||||
|
- `Match` 强制处理成功和失败两种情况
|
||||||
|
- `IfFail` 提供失败时的默认值
|
||||||
|
|
||||||
|
## 步骤 3:使用管道操作组合函数
|
||||||
|
|
||||||
|
学习如何使用管道操作符构建流式的数据处理流程。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional.pipe;
|
||||||
|
using GFramework.Core.functional.functions;
|
||||||
|
|
||||||
|
namespace MyGame.Systems
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 物品处理系统
|
||||||
|
/// </summary>
|
||||||
|
public class ItemProcessingSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 处理物品掉落
|
||||||
|
/// </summary>
|
||||||
|
public ItemDrop ProcessItemDrop(Enemy enemy, Player player)
|
||||||
|
{
|
||||||
|
// 使用管道操作构建处理流程
|
||||||
|
return enemy
|
||||||
|
.Pipe(e => CalculateDropRate(e, player))
|
||||||
|
.Tap(rate => Console.WriteLine($"掉落率: {rate:P}"))
|
||||||
|
.Pipe(rate => GenerateItems(rate))
|
||||||
|
.Tap(items => Console.WriteLine($"生成 {items.Count} 个物品"))
|
||||||
|
.Pipe(items => ApplyLuckBonus(items, player))
|
||||||
|
.Pipe(items => FilterByQuality(items))
|
||||||
|
.Tap(items => Console.WriteLine($"过滤后剩余 {items.Count} 个物品"))
|
||||||
|
.Let(items => new ItemDrop
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
TotalValue = items.Sum(i => i.Value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算掉落率
|
||||||
|
/// </summary>
|
||||||
|
private double CalculateDropRate(Enemy enemy, Player player)
|
||||||
|
{
|
||||||
|
return (enemy.Level * 0.1 + player.Luck * 0.05)
|
||||||
|
.Pipe(rate => Math.Min(rate, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成物品
|
||||||
|
/// </summary>
|
||||||
|
private List<Item> GenerateItems(double dropRate)
|
||||||
|
{
|
||||||
|
var random = new Random();
|
||||||
|
var itemCount = random.NextDouble() < dropRate ? random.Next(1, 5) : 0;
|
||||||
|
|
||||||
|
return Enumerable.Range(0, itemCount)
|
||||||
|
.Select(_ => new Item
|
||||||
|
{
|
||||||
|
Id = random.Next(1000),
|
||||||
|
Name = $"物品_{random.Next(100)}",
|
||||||
|
Quality = (ItemQuality)random.Next(0, 4),
|
||||||
|
Value = random.Next(10, 100)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用幸运加成
|
||||||
|
/// </summary>
|
||||||
|
private List<Item> ApplyLuckBonus(List<Item> items, Player player)
|
||||||
|
{
|
||||||
|
return items
|
||||||
|
.Select(item => item.Also(i =>
|
||||||
|
{
|
||||||
|
if (player.Luck > 50)
|
||||||
|
i.Quality = (ItemQuality)Math.Min((int)i.Quality + 1, 3);
|
||||||
|
}))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按品质过滤
|
||||||
|
/// </summary>
|
||||||
|
private List<Item> FilterByQuality(List<Item> items)
|
||||||
|
{
|
||||||
|
return items
|
||||||
|
.Where(item => item.Quality >= ItemQuality.Uncommon)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 条件处理物品
|
||||||
|
/// </summary>
|
||||||
|
public string ProcessItemConditionally(Item item, bool isVip)
|
||||||
|
{
|
||||||
|
return item.PipeIf(
|
||||||
|
predicate: i => isVip,
|
||||||
|
ifTrue: i => $"VIP 物品: {i.Name} (价值 {i.Value * 2})",
|
||||||
|
ifFalse: i => $"普通物品: {i.Name} (价值 {i.Value})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Enemy
|
||||||
|
{
|
||||||
|
public int Level { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Player
|
||||||
|
{
|
||||||
|
public int Luck { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Item
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public ItemQuality Quality { get; set; }
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ItemQuality
|
||||||
|
{
|
||||||
|
Common,
|
||||||
|
Uncommon,
|
||||||
|
Rare,
|
||||||
|
Epic
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ItemDrop
|
||||||
|
{
|
||||||
|
public List<Item> Items { get; set; } = new();
|
||||||
|
public int TotalValue { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- `Pipe` 将值传递给函数,构建流式处理链
|
||||||
|
- `Tap` 执行副作用(如日志)但不改变值
|
||||||
|
- `Let` 在作用域内转换值
|
||||||
|
- `Also` 对值执行操作后返回原值
|
||||||
|
- `PipeIf` 根据条件选择不同的处理路径
|
||||||
|
|
||||||
|
## 步骤 4:实现完整的数据处理流程
|
||||||
|
|
||||||
|
现在让我们结合 Option、Result 和管道操作,实现一个完整的游戏功能。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Core.functional;
|
||||||
|
using GFramework.Core.functional.pipe;
|
||||||
|
using GFramework.Core.functional.control;
|
||||||
|
|
||||||
|
namespace MyGame.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 任务系统
|
||||||
|
/// </summary>
|
||||||
|
public class QuestSystem
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, Quest> _quests = new();
|
||||||
|
private readonly Dictionary<int, List<int>> _playerQuests = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 接受任务(完整流程)
|
||||||
|
/// </summary>
|
||||||
|
public Result<QuestAcceptResult> AcceptQuest(int playerId, int questId)
|
||||||
|
{
|
||||||
|
return FindQuest(questId)
|
||||||
|
// 转换 Option 为 Result
|
||||||
|
.ToResult("任务不存在")
|
||||||
|
// 验证任务等级要求
|
||||||
|
.Bind(quest => ValidateQuestLevel(playerId, quest))
|
||||||
|
// 检查前置任务
|
||||||
|
.Bind(quest => CheckPrerequisites(playerId, quest))
|
||||||
|
// 检查任务槽位
|
||||||
|
.Bind(quest => CheckQuestSlots(playerId))
|
||||||
|
// 添加到玩家任务列表
|
||||||
|
.Map(quest => AddQuestToPlayer(playerId, quest))
|
||||||
|
// 记录日志
|
||||||
|
.Tap(result => Console.WriteLine($"玩家 {playerId} 接受任务: {result.QuestName}"))
|
||||||
|
// 发放初始奖励
|
||||||
|
.Map(result => GiveInitialRewards(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找任务
|
||||||
|
/// </summary>
|
||||||
|
private Option<Quest> FindQuest(int questId)
|
||||||
|
{
|
||||||
|
return _quests.TryGetValue(questId, out var quest)
|
||||||
|
? Option<Quest>.Some(quest)
|
||||||
|
: Option<Quest>.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证任务等级
|
||||||
|
/// </summary>
|
||||||
|
private Result<Quest> ValidateQuestLevel(int playerId, Quest quest)
|
||||||
|
{
|
||||||
|
var playerLevel = GetPlayerLevel(playerId);
|
||||||
|
|
||||||
|
return playerLevel >= quest.RequiredLevel
|
||||||
|
? Result<Quest>.Success(quest)
|
||||||
|
: Result<Quest>.Failure($"等级不足,需要 {quest.RequiredLevel} 级");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查前置任务
|
||||||
|
/// </summary>
|
||||||
|
private Result<Quest> CheckPrerequisites(int playerId, Quest quest)
|
||||||
|
{
|
||||||
|
if (quest.PrerequisiteQuestIds.Count == 0)
|
||||||
|
return Result<Quest>.Success(quest);
|
||||||
|
|
||||||
|
var completedQuests = GetCompletedQuests(playerId);
|
||||||
|
var hasAllPrerequisites = quest.PrerequisiteQuestIds
|
||||||
|
.All(id => completedQuests.Contains(id));
|
||||||
|
|
||||||
|
return hasAllPrerequisites
|
||||||
|
? Result<Quest>.Success(quest)
|
||||||
|
: Result<Quest>.Failure("未完成前置任务");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查任务槽位
|
||||||
|
/// </summary>
|
||||||
|
private Result<Quest> CheckQuestSlots(int playerId)
|
||||||
|
{
|
||||||
|
var activeQuests = GetActiveQuests(playerId);
|
||||||
|
|
||||||
|
return activeQuests.Count < 10
|
||||||
|
? Result<Quest>.Success(default!)
|
||||||
|
: Result<Quest>.Failure("任务栏已满");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加任务到玩家
|
||||||
|
/// </summary>
|
||||||
|
private QuestAcceptResult AddQuestToPlayer(int playerId, Quest quest)
|
||||||
|
{
|
||||||
|
if (!_playerQuests.ContainsKey(playerId))
|
||||||
|
_playerQuests[playerId] = new List<int>();
|
||||||
|
|
||||||
|
_playerQuests[playerId].Add(quest.Id);
|
||||||
|
|
||||||
|
return new QuestAcceptResult
|
||||||
|
{
|
||||||
|
QuestId = quest.Id,
|
||||||
|
QuestName = quest.Name,
|
||||||
|
Description = quest.Description,
|
||||||
|
Rewards = quest.Rewards
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发放初始奖励
|
||||||
|
/// </summary>
|
||||||
|
private QuestAcceptResult GiveInitialRewards(QuestAcceptResult result)
|
||||||
|
{
|
||||||
|
// 某些任务接受时就有奖励
|
||||||
|
if (result.Rewards.Gold > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"获得金币: {result.Rewards.Gold}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成任务(使用函数组合)
|
||||||
|
/// </summary>
|
||||||
|
public Result<QuestCompleteResult> CompleteQuest(int playerId, int questId)
|
||||||
|
{
|
||||||
|
return FindQuest(questId)
|
||||||
|
.ToResult("任务不存在")
|
||||||
|
.Bind(quest => ValidateQuestOwnership(playerId, quest))
|
||||||
|
.Bind(quest => ValidateQuestObjectives(quest))
|
||||||
|
.Map(quest => RemoveQuestFromPlayer(playerId, quest))
|
||||||
|
.Map(quest => CalculateRewards(quest))
|
||||||
|
.Tap(result => Console.WriteLine($"任务完成: {result.QuestName}"))
|
||||||
|
.Map(result => GiveRewards(playerId, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证任务所有权
|
||||||
|
/// </summary>
|
||||||
|
private Result<Quest> ValidateQuestOwnership(int playerId, Quest quest)
|
||||||
|
{
|
||||||
|
var activeQuests = GetActiveQuests(playerId);
|
||||||
|
|
||||||
|
return activeQuests.Contains(quest.Id)
|
||||||
|
? Result<Quest>.Success(quest)
|
||||||
|
: Result<Quest>.Failure("玩家未接受此任务");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证任务目标
|
||||||
|
/// </summary>
|
||||||
|
private Result<Quest> ValidateQuestObjectives(Quest quest)
|
||||||
|
{
|
||||||
|
return quest.IsCompleted
|
||||||
|
? Result<Quest>.Success(quest)
|
||||||
|
: Result<Quest>.Failure("任务目标未完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从玩家移除任务
|
||||||
|
/// </summary>
|
||||||
|
private Quest RemoveQuestFromPlayer(int playerId, Quest quest)
|
||||||
|
{
|
||||||
|
if (_playerQuests.ContainsKey(playerId))
|
||||||
|
{
|
||||||
|
_playerQuests[playerId].Remove(quest.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return quest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算奖励
|
||||||
|
/// </summary>
|
||||||
|
private QuestCompleteResult CalculateRewards(Quest quest)
|
||||||
|
{
|
||||||
|
return new QuestCompleteResult
|
||||||
|
{
|
||||||
|
QuestId = quest.Id,
|
||||||
|
QuestName = quest.Name,
|
||||||
|
Rewards = quest.Rewards,
|
||||||
|
BonusRewards = CalculateBonusRewards(quest)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算额外奖励
|
||||||
|
/// </summary>
|
||||||
|
private QuestRewards CalculateBonusRewards(Quest quest)
|
||||||
|
{
|
||||||
|
// 根据任务难度给予额外奖励
|
||||||
|
return new QuestRewards
|
||||||
|
{
|
||||||
|
Gold = quest.Rewards.Gold / 10,
|
||||||
|
Experience = quest.Rewards.Experience / 10
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发放奖励
|
||||||
|
/// </summary>
|
||||||
|
private QuestCompleteResult GiveRewards(int playerId, QuestCompleteResult result)
|
||||||
|
{
|
||||||
|
var totalGold = result.Rewards.Gold + result.BonusRewards.Gold;
|
||||||
|
var totalExp = result.Rewards.Experience + result.BonusRewards.Experience;
|
||||||
|
|
||||||
|
Console.WriteLine($"获得金币: {totalGold}");
|
||||||
|
Console.WriteLine($"获得经验: {totalExp}");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法
|
||||||
|
private int GetPlayerLevel(int playerId) => 10;
|
||||||
|
private List<int> GetCompletedQuests(int playerId) => new();
|
||||||
|
private List<int> GetActiveQuests(int playerId) =>
|
||||||
|
_playerQuests.GetValueOrDefault(playerId, new List<int>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Quest
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public int RequiredLevel { get; set; }
|
||||||
|
public List<int> PrerequisiteQuestIds { get; set; } = new();
|
||||||
|
public bool IsCompleted { get; set; }
|
||||||
|
public QuestRewards Rewards { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuestRewards
|
||||||
|
{
|
||||||
|
public int Gold { get; set; }
|
||||||
|
public int Experience { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuestAcceptResult
|
||||||
|
{
|
||||||
|
public int QuestId { get; set; }
|
||||||
|
public string QuestName { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public QuestRewards Rewards { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuestCompleteResult
|
||||||
|
{
|
||||||
|
public int QuestId { get; set; }
|
||||||
|
public string QuestName { get; set; } = "";
|
||||||
|
public QuestRewards Rewards { get; set; } = new();
|
||||||
|
public QuestRewards BonusRewards { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码说明**:
|
||||||
|
|
||||||
|
- 使用 `Option.ToResult` 将可选值转换为结果
|
||||||
|
- 使用 `Bind` 链接多个验证步骤
|
||||||
|
- 使用 `Map` 转换成功的值
|
||||||
|
- 使用 `Tap` 添加日志而不中断流程
|
||||||
|
- 每个步骤都是纯函数,易于测试和维护
|
||||||
|
|
||||||
|
## 完整代码
|
||||||
|
|
||||||
|
### Program.cs
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using MyGame.Services;
|
||||||
|
using MyGame.Systems;
|
||||||
|
using MyGame.Features;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("=== 函数式编程实践示例 ===\n");
|
||||||
|
|
||||||
|
// 测试 Option
|
||||||
|
TestOptionUsage();
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 测试 Result
|
||||||
|
TestResultUsage();
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 测试管道操作
|
||||||
|
TestPipelineUsage();
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 测试完整流程
|
||||||
|
TestCompleteWorkflow();
|
||||||
|
|
||||||
|
Console.WriteLine("\n=== 测试完成 ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TestOptionUsage()
|
||||||
|
{
|
||||||
|
Console.WriteLine("--- 测试 Option ---");
|
||||||
|
|
||||||
|
var service = new PlayerDataService();
|
||||||
|
|
||||||
|
// 测试查找存在的玩家
|
||||||
|
Console.WriteLine(service.GetPlayerName(1));
|
||||||
|
|
||||||
|
// 测试查找不存在的玩家
|
||||||
|
Console.WriteLine(service.GetPlayerName(999));
|
||||||
|
|
||||||
|
// 测试获取等级
|
||||||
|
Console.WriteLine($"玩家等级: {service.GetPlayerLevel(1)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TestResultUsage()
|
||||||
|
{
|
||||||
|
Console.WriteLine("--- 测试 Result ---");
|
||||||
|
|
||||||
|
var saveService = new SaveService();
|
||||||
|
|
||||||
|
var saveData = new GameSaveData
|
||||||
|
{
|
||||||
|
PlayerId = 1,
|
||||||
|
PlayerName = "测试玩家",
|
||||||
|
Level = 10,
|
||||||
|
Gold = 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试保存
|
||||||
|
var saveResult = saveService.SaveGame(saveData);
|
||||||
|
saveResult.Match(
|
||||||
|
succ: path => Console.WriteLine($"保存成功: {path}"),
|
||||||
|
fail: ex => Console.WriteLine($"保存失败: {ex.Message}")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试加载
|
||||||
|
Console.WriteLine(saveService.GetSaveInfo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TestPipelineUsage()
|
||||||
|
{
|
||||||
|
Console.WriteLine("--- 测试管道操作 ---");
|
||||||
|
|
||||||
|
var itemSystem = new ItemProcessingSystem();
|
||||||
|
|
||||||
|
var enemy = new Enemy { Level = 5 };
|
||||||
|
var player = new Player { Luck = 60 };
|
||||||
|
|
||||||
|
var drop = itemSystem.ProcessItemDrop(enemy, player);
|
||||||
|
Console.WriteLine($"掉落总价值: {drop.TotalValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TestCompleteWorkflow()
|
||||||
|
{
|
||||||
|
Console.WriteLine("--- 测试完整工作流 ---");
|
||||||
|
|
||||||
|
var questSystem = new QuestSystem();
|
||||||
|
|
||||||
|
// 测试接受任务
|
||||||
|
var acceptResult = questSystem.AcceptQuest(1, 101);
|
||||||
|
acceptResult.Match(
|
||||||
|
succ: result => Console.WriteLine($"接受任务成功: {result.QuestName}"),
|
||||||
|
fail: ex => Console.WriteLine($"接受任务失败: {ex.Message}")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行结果
|
||||||
|
|
||||||
|
运行程序后,你将看到类似以下的输出:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 函数式编程实践示例 ===
|
||||||
|
|
||||||
|
--- 测试 Option ---
|
||||||
|
未知玩家
|
||||||
|
未知玩家
|
||||||
|
玩家等级: 1
|
||||||
|
|
||||||
|
--- 测试 Result ---
|
||||||
|
保存成功: ./saves/save_1_20260307_143022.json
|
||||||
|
存档加载成功: 测试玩家, 等级 10
|
||||||
|
|
||||||
|
--- 测试管道操作 ---
|
||||||
|
掉落率: 35.00%
|
||||||
|
生成 3 个物品
|
||||||
|
过滤后剩余 2 个物品
|
||||||
|
掉落总价值: 150
|
||||||
|
|
||||||
|
--- 测试完整工作流 ---
|
||||||
|
玩家 1 接受任务: 新手任务
|
||||||
|
接受任务成功: 新手任务
|
||||||
|
|
||||||
|
=== 测试完成 ===
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证步骤**:
|
||||||
|
|
||||||
|
1. Option 正确处理了不存在的值
|
||||||
|
2. Result 成功捕获和传播错误
|
||||||
|
3. 管道操作构建了清晰的处理流程
|
||||||
|
4. 完整工作流展示了多种技术的组合使用
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
恭喜!你已经掌握了函数式编程的核心技术。接下来可以学习:
|
||||||
|
|
||||||
|
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 结合函数式编程和协程
|
||||||
|
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 在状态机中应用函数式模式
|
||||||
|
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 使用 Result 处理资源加载
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [扩展方法](/zh-CN/core/extensions) - 更多函数式扩展方法
|
||||||
|
- [架构组件](/zh-CN/core/architecture) - 在架构中使用函数式编程
|
||||||
|
- [最佳实践](/zh-CN/best-practices/architecture-patterns) - 函数式编程最佳实践
|
||||||
@ -71,6 +71,50 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 系统实现教程
|
||||||
|
|
||||||
|
#### [使用协程系统](./coroutine-tutorial.md)
|
||||||
|
|
||||||
|
> 学习如何使用协程系统实现异步操作和时间控制。
|
||||||
|
|
||||||
|
**学习内容**:创建协程、等待指令、事件等待、协程组合
|
||||||
|
|
||||||
|
**预计时间**:1-2 小时
|
||||||
|
|
||||||
|
#### [实现状态机](./state-machine-tutorial.md)
|
||||||
|
|
||||||
|
> 学习如何使用状态机系统管理游戏状态和场景切换。
|
||||||
|
|
||||||
|
**学习内容**:定义状态、状态转换、异步状态、状态历史
|
||||||
|
|
||||||
|
**预计时间**:1-2 小时
|
||||||
|
|
||||||
|
#### [实现暂停系统](./pause-system.md)
|
||||||
|
|
||||||
|
> 学习如何使用暂停系统实现多层暂停管理和游戏流程控制。
|
||||||
|
|
||||||
|
**学习内容**:基本暂停、分组暂停、暂停栈、自定义处理器
|
||||||
|
|
||||||
|
**预计时间**:1-2 小时
|
||||||
|
|
||||||
|
#### [资源管理最佳实践](./resource-management.md)
|
||||||
|
|
||||||
|
> 学习如何高效管理游戏资源的加载、缓存和释放。
|
||||||
|
|
||||||
|
**学习内容**:资源加载、缓存策略、释放策略、内存优化
|
||||||
|
|
||||||
|
**预计时间**:1-2 小时
|
||||||
|
|
||||||
|
#### [实现存档系统](./save-system.md)
|
||||||
|
|
||||||
|
> 学习如何实现完整的游戏存档和读档系统。
|
||||||
|
|
||||||
|
**学习内容**:数据序列化、存档管理、版本控制、加密保护
|
||||||
|
|
||||||
|
**预计时间**:1-2 小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🎯 学习路径建议
|
## 🎯 学习路径建议
|
||||||
|
|
||||||
### 路径一:快速上手(推荐新手)
|
### 路径一:快速上手(推荐新手)
|
||||||
|
|||||||
1135
docs/zh-CN/tutorials/pause-system.md
Normal file
1135
docs/zh-CN/tutorials/pause-system.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user