diff --git a/docs/zh-CN/core/configuration.md b/docs/zh-CN/core/configuration.md new file mode 100644 index 0000000..4a60b07 --- /dev/null +++ b/docs/zh-CN/core/configuration.md @@ -0,0 +1,938 @@ +# Configuration 包使用说明 + +## 概述 + +Configuration 包提供了线程安全的配置管理系统,支持类型安全的配置存储、访问、监听和持久化。配置管理器可以用于管理游戏设置、运行时参数、开发配置等各种键值对数据。 + +配置系统是 GFramework 架构中的实用工具(Utility),可以在架构的任何层级中使用,提供统一的配置管理能力。 + +## 核心接口 + +### IConfigurationManager + +配置管理器接口,提供类型安全的配置存储和访问。所有方法都是线程安全的。 + +**核心方法:** + +```csharp +// 配置访问 +T? GetConfig(string key); // 获取配置值 +T GetConfig(string key, T defaultValue); // 获取配置值(带默认值) +void SetConfig(string key, T value); // 设置配置值 +bool HasConfig(string key); // 检查配置是否存在 +bool RemoveConfig(string key); // 移除配置 +void Clear(); // 清空所有配置 + +// 配置监听 +IUnRegister WatchConfig(string key, Action onChange); // 监听配置变化 + +// 持久化 +void LoadFromJson(string json); // 从 JSON 加载 +string SaveToJson(); // 保存为 JSON +void LoadFromFile(string path); // 从文件加载 +void SaveToFile(string path); // 保存到文件 + +// 工具方法 +int Count { get; } // 获取配置数量 +IEnumerable 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("game.difficulty"); +var volume = configManager.GetConfig("audio.volume"); +var quality = configManager.GetConfig("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("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 { "A", "B", "C" }); + _config.SetConfig("dict.data", new Dictionary + { + ["key1"] = 1, + ["key2"] = 2 + }); + } + + public void GetConfigs() + { + var intValue = _config.GetConfig("int.value"); + var floatValue = _config.GetConfig("float.value"); + var boolValue = _config.GetConfig("bool.value"); + var stringValue = _config.GetConfig("string.value"); + + var position = _config.GetConfig("vector.position"); + var items = _config.GetConfig>("list.items"); + } +} +``` + +## 高级用法 + +### 1. 配置监听(热更新) + +配置监听允许在配置值变化时自动触发回调,实现配置的热更新。 + +```csharp +public class AudioManager : AbstractSystem +{ + private IUnRegister _volumeWatcher; + + protected override void OnInit() + { + var config = this.GetUtility(); + + // 监听音量配置变化 + _volumeWatcher = config.WatchConfig("audio.masterVolume", newVolume => + { + UpdateMasterVolume(newVolume); + this.GetUtility()?.Info($"Master volume changed to: {newVolume}"); + }); + + // 监听音效开关 + config.WatchConfig("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(); + + // 多个组件监听同一个配置 + config.WatchConfig("graphics.quality", quality => + { + UpdateTextureQuality(quality); + }); + + config.WatchConfig("graphics.quality", quality => + { + UpdateShadowQuality(quality); + }); + + config.WatchConfig("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("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 +{ + protected override void Init() + { + // 注册配置管理器 + this.RegisterUtility(new ConfigurationManager()); + } +} +``` + +### 在 System 中使用 + +```csharp +public class SettingsSystem : AbstractSystem +{ + private IConfigurationManager _config; + + protected override void OnInit() + { + _config = this.GetUtility(); + + // 加载配置 + LoadSettings(); + + // 监听配置变化 + _config.WatchConfig("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(); + + // 更新配置(会自动触发监听器) + config.SetConfig("graphics.quality", quality); + config.SetConfig("graphics.fullscreen", fullscreen); + + // 保存配置 + SaveSettings(); + } + + public void ResetToDefaults() + { + var config = this.GetUtility(); + + // 清空所有配置 + 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(); + 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("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 _watchers = new(); + + protected override void OnInit() + { + var config = this.GetUtility(); + + // 保存监听器引用 + _watchers.Add(config.WatchConfig("audio.volume", OnVolumeChanged)); + _watchers.Add(config.WatchConfig("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("shared.value", newValue => + { + // 确保线程安全的操作 + lock (_lockObject) + { + UpdateSharedResource(newValue); + } + }); + } + + private readonly object _lockObject = new(); + private void UpdateSharedResource(int value) { } +} +``` + +### 7. 配置变更通知 + +避免在配置监听器中触发大量的配置变更,可能导致循环调用: + +```csharp +// 不推荐:可能导致无限循环 +_config.WatchConfig("value.a", a => +{ + _config.SetConfig("value.b", a + 1); // 触发 b 的监听器 +}); + +_config.WatchConfig("value.b", b => +{ + _config.SetConfig("value.a", b + 1); // 触发 a 的监听器 +}); + +// 推荐:使用标志位避免循环 +private bool _isUpdating = false; + +_config.WatchConfig("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("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("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) - 配置管理器内部使用日志记录 diff --git a/docs/zh-CN/core/ecs.md b/docs/zh-CN/core/ecs.md new file mode 100644 index 0000000..286d7f3 --- /dev/null +++ b/docs/zh-CN/core/ecs.md @@ -0,0 +1,1005 @@ +--- +title: ECS 系统集成 +description: ECS(Entity Component System)系统集成指南,基于 Arch.Core 实现高性能的实体组件系统。 +--- + +# ECS 系统集成 + +## 概述 + +GFramework 集成了 [Arch.Core](https://github.com/genaray/Arch) ECS 框架,提供高性能的实体组件系统(Entity Component +System)架构。通过 ECS 模式,你可以构建数据驱动、高度可扩展的游戏系统。 + +**主要特性**: + +- 基于 Arch.Core 的高性能 ECS 实现 +- 与 GFramework 架构无缝集成 +- 支持组件查询和批量处理 +- 零 GC 分配的组件访问 +- 灵活的系统生命周期管理 +- 支持多线程并行处理(Arch 原生支持) + +**性能特点**: + +- 10,000 个实体更新 < 100ms +- 1,000 个实体创建 < 50ms +- 基于 Archetype 的高效内存布局 +- 支持 SIMD 优化 + +## 核心概念 + +### Entity(实体) + +实体是游戏世界中的基本对象,本质上是一个唯一标识符(ID)。实体本身不包含数据或逻辑,只是组件的容器。 + +```csharp +using Arch.Core; + +// 创建实体 +var entity = world.Create(); + +// 创建带组件的实体 +var entity = world.Create(new Position(0, 0), new Velocity(1, 1)); +``` + +### Component(组件) + +组件是纯数据结构,用于存储实体的状态。组件应该是简单的值类型(struct),不包含逻辑。 + +```csharp +using System.Runtime.InteropServices; + +/// +/// 位置组件 +/// +[StructLayout(LayoutKind.Sequential)] +public struct Position(float x, float y) +{ + public float X { get; set; } = x; + public float Y { get; set; } = y; +} + +/// +/// 速度组件 +/// +[StructLayout(LayoutKind.Sequential)] +public struct Velocity(float x, float y) +{ + public float X { get; set; } = x; + public float Y { get; set; } = y; +} +``` + +**组件设计原则**: + +- 使用 `struct` 而不是 `class` +- 只包含数据,不包含逻辑 +- 使用 `[StructLayout(LayoutKind.Sequential)]` 优化内存布局 +- 保持组件小而专注 + +### System(系统) + +系统包含游戏逻辑,负责处理具有特定组件组合的实体。在 GFramework 中,系统通过继承 `ArchSystemAdapter` 来实现。 + +```csharp +using Arch.Core; +using GFramework.Core.ecs; + +/// +/// 移动系统 - 更新实体位置 +/// +public sealed class MovementSystem : ArchSystemAdapter +{ + private QueryDescription _query; + + protected override void OnArchInitialize() + { + // 创建查询:查找所有同时拥有 Position 和 Velocity 组件的实体 + _query = new QueryDescription() + .WithAll(); + } + + protected override void OnUpdate(in float deltaTime) + { + // 查询并更新所有符合条件的实体 + World.Query(in _query, (ref Position pos, ref Velocity vel) => + { + pos.X += vel.X * deltaTime; + pos.Y += vel.Y * deltaTime; + }); + } +} +``` + +### World(世界) + +World 是 ECS 的核心容器,管理所有实体和组件。GFramework 通过 `ArchEcsModule` 自动创建和管理 World。 + +```csharp +// World 由 ArchEcsModule 自动创建和注册到 IoC 容器 +// 在系统中可以直接访问 +public class MySystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + // 访问 World + var entityCount = World.Size; + } +} +``` + +### Arch.Core 集成 + +GFramework 通过以下组件桥接 Arch.Core 到框架生命周期: + +- **ArchEcsModule**:ECS 模块,管理 World 和系统生命周期 +- **ArchSystemAdapter**:系统适配器,桥接 Arch 系统到 GFramework + +## 基本用法 + +### 1. 定义组件 + +```csharp +using System.Runtime.InteropServices; + +namespace MyGame.Components; + +[StructLayout(LayoutKind.Sequential)] +public struct Health(float current, float max) +{ + public float Current { get; set; } = current; + public float Max { get; set; } = max; +} + +[StructLayout(LayoutKind.Sequential)] +public struct Damage(float value) +{ + public float Value { get; set; } = value; +} + +[StructLayout(LayoutKind.Sequential)] +public struct PlayerTag +{ + // 标签组件,不需要数据 +} +``` + +### 2. 创建系统 + +```csharp +using Arch.Core; +using GFramework.Core.ecs; +using MyGame.Components; + +namespace MyGame.Systems; + +/// +/// 伤害系统 - 处理伤害逻辑 +/// +public sealed class DamageSystem : ArchSystemAdapter +{ + private QueryDescription _query; + + protected override void OnArchInitialize() + { + // 查询所有具有 Health 和 Damage 组件的实体 + _query = new QueryDescription() + .WithAll(); + } + + protected override void OnUpdate(in float deltaTime) + { + // 处理伤害 + World.Query(in _query, (Entity entity, ref Health health, ref Damage damage) => + { + health.Current -= damage.Value * deltaTime; + + // 如果生命值耗尽,移除伤害组件 + if (health.Current <= 0) + { + health.Current = 0; + World.Remove(entity); + } + }); + } +} +``` + +### 3. 注册 ECS 模块 + +```csharp +using GFramework.Core.architecture; +using GFramework.Core.ecs; +using MyGame.Systems; + +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册 ECS 系统 + RegisterSystem(new MovementSystem()); + RegisterSystem(new DamageSystem()); + + // 安装 ECS 模块 + InstallModule(new ArchEcsModule(enabled: true)); + } +} +``` + +### 4. 创建和管理实体 + +```csharp +using Arch.Core; +using GFramework.Core.Abstractions.controller; +using MyGame.Components; + +public class GameController : IController +{ + private World _world; + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void Start() + { + // 获取 World + _world = this.GetService(); + + // 创建玩家实体 + var player = _world.Create( + new Position(0, 0), + new Velocity(0, 0), + new Health(100, 100), + new PlayerTag() + ); + + // 创建敌人实体 + var enemy = _world.Create( + new Position(10, 10), + new Velocity(-1, 0), + new Health(50, 50) + ); + } + + public void ApplyDamage(Entity entity, float damageValue) + { + // 添加伤害组件 + if (_world.Has(entity)) + { + _world.Add(entity, new Damage(damageValue)); + } + } +} +``` + +### 5. 更新 ECS 系统 + +```csharp +// 在游戏主循环中更新 ECS +public class GameLoop +{ + private ArchEcsModule _ecsModule; + + public void Update(float deltaTime) + { + // 更新所有 ECS 系统 + _ecsModule.Update(deltaTime); + } +} +``` + +## 高级用法 + +### 查询实体 + +Arch 提供了强大的查询 API,支持多种过滤条件: + +```csharp +using Arch.Core; + +public class QueryExampleSystem : ArchSystemAdapter +{ + private QueryDescription _query1; + private QueryDescription _query2; + private QueryDescription _query3; + + protected override void OnArchInitialize() + { + // 查询:必须有 Position 和 Velocity + _query1 = new QueryDescription() + .WithAll(); + + // 查询:必须有 Health,但不能有 Damage + _query2 = new QueryDescription() + .WithAll() + .WithNone(); + + // 查询:必须有 Position,可选 Velocity + _query3 = new QueryDescription() + .WithAll() + .WithAny(); + } + + protected override void OnUpdate(in float deltaTime) + { + // 使用查询 1 + World.Query(in _query1, (ref Position pos, ref Velocity vel) => + { + // 处理逻辑 + }); + + // 使用查询 2 + World.Query(in _query2, (Entity entity, ref Health health) => + { + // 处理逻辑 + }); + } +} +``` + +### 系统生命周期钩子 + +`ArchSystemAdapter` 提供了多个生命周期钩子: + +```csharp +public class LifecycleExampleSystem : ArchSystemAdapter +{ + protected override void OnArchInitialize() + { + // Arch 系统初始化 + // 在这里创建查询、初始化资源 + } + + protected override void OnBeforeUpdate(in float deltaTime) + { + // 更新前调用 + // 可用于预处理逻辑 + } + + protected override void OnUpdate(in float deltaTime) + { + // 主更新逻辑 + } + + protected override void OnAfterUpdate(in float deltaTime) + { + // 更新后调用 + // 可用于后处理逻辑 + } + + protected override void OnArchDispose() + { + // 资源清理 + } +} +``` + +### 组件操作 + +```csharp +using Arch.Core; + +public class ComponentOperations +{ + private World _world; + + public void Examples() + { + var entity = _world.Create(); + + // 添加组件 + _world.Add(entity, new Position(0, 0)); + _world.Add(entity, new Velocity(1, 1)); + + // 检查组件 + if (_world.Has(entity)) + { + // 获取组件引用(零 GC 分配) + ref var pos = ref _world.Get(entity); + pos.X += 10; + } + + // 设置组件(替换现有值) + _world.Set(entity, new Position(100, 100)); + + // 移除组件 + _world.Remove(entity); + + // 销毁实体 + _world.Destroy(entity); + } +} +``` + +### 批量操作 + +```csharp +public class BatchOperations +{ + private World _world; + + public void CreateMultipleEntities() + { + // 批量创建实体 + for (int i = 0; i < 1000; i++) + { + _world.Create( + new Position(i, i), + new Velocity(1, 1) + ); + } + } + + public void ClearAllEntities() + { + // 清空所有实体 + _world.Clear(); + } +} +``` + +### 实体查询和迭代 + +```csharp +public class EntityIterationSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + // 方式 1:使用查询和 Lambda + var query = new QueryDescription().WithAll(); + World.Query(in query, (ref Position pos) => + { + // 处理每个实体 + }); + + // 方式 2:获取实体引用 + World.Query(in query, (Entity entity, ref Position pos) => + { + // 可以访问实体 ID + if (pos.X > 100) + { + World.Destroy(entity); + } + }); + + // 方式 3:多组件查询 + var multiQuery = new QueryDescription() + .WithAll(); + + World.Query(in multiQuery, ( + Entity entity, + ref Position pos, + ref Velocity vel, + ref Health health) => + { + // 处理多个组件 + }); + } +} +``` + +## 性能优化 + +### 1. 使用 struct 组件 + +```csharp +// ✅ 推荐:使用 struct +[StructLayout(LayoutKind.Sequential)] +public struct Position(float x, float y) +{ + public float X { get; set; } = x; + public float Y { get; set; } = y; +} + +// ❌ 不推荐:使用 class +public class Position +{ + public float X { get; set; } + public float Y { get; set; } +} +``` + +### 2. 缓存查询 + +```csharp +public class OptimizedSystem : ArchSystemAdapter +{ + // ✅ 推荐:缓存查询 + private QueryDescription _cachedQuery; + + protected override void OnArchInitialize() + { + _cachedQuery = new QueryDescription() + .WithAll(); + } + + protected override void OnUpdate(in float deltaTime) + { + // 使用缓存的查询 + World.Query(in _cachedQuery, (ref Position pos, ref Velocity vel) => + { + pos.X += vel.X * deltaTime; + pos.Y += vel.Y * deltaTime; + }); + } +} + +// ❌ 不推荐:每次创建新查询 +public class UnoptimizedSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + // 每帧创建新查询(性能差) + var query = new QueryDescription().WithAll(); + World.Query(in query, (ref Position pos, ref Velocity vel) => + { + // ... + }); + } +} +``` + +### 3. 使用 ref 访问组件 + +```csharp +// ✅ 推荐:使用 ref 避免复制 +World.Query(in query, (ref Position pos, ref Velocity vel) => +{ + pos.X += vel.X; // 直接修改,零 GC +}); + +// ❌ 不推荐:不使用 ref +World.Query(in query, (Position pos, Velocity vel) => +{ + pos.X += vel.X; // 复制值,修改不会生效 +}); +``` + +### 4. 组件大小优化 + +```csharp +// ✅ 推荐:小而专注的组件 +public struct Position(float x, float y) +{ + public float X { get; set; } = x; + public float Y { get; set; } = y; +} + +public struct Velocity(float x, float y) +{ + public float X { get; set; } = x; + public float Y { get; set; } = y; +} + +// ❌ 不推荐:大而全的组件 +public struct Transform +{ + public float X, Y, Z; + public float RotationX, RotationY, RotationZ; + public float ScaleX, ScaleY, ScaleZ; + public float VelocityX, VelocityY, VelocityZ; + // ... 太多数据 +} +``` + +### 5. 批量处理 + +```csharp +public class BatchProcessingSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + // ✅ 推荐:批量处理 + var query = new QueryDescription().WithAll(); + World.Query(in query, (ref Position pos, ref Velocity vel) => + { + // 一次查询处理所有实体 + pos.X += vel.X * deltaTime; + pos.Y += vel.Y * deltaTime; + }); + } +} + +// ❌ 不推荐:逐个处理 +public class IndividualProcessingSystem : ArchSystemAdapter +{ + private List _entities = new(); + + protected override void OnUpdate(in float deltaTime) + { + foreach (var entity in _entities) + { + ref var pos = ref World.Get(entity); + ref var vel = ref World.Get(entity); + pos.X += vel.X * deltaTime; + pos.Y += vel.Y * deltaTime; + } + } +} +``` + +## 最佳实践 + +### 1. ECS 设计模式 + +**组件组合优于继承**: + +```csharp +// ✅ 推荐:使用组件组合 +var player = world.Create( + new Position(0, 0), + new Velocity(0, 0), + new Health(100, 100), + new PlayerTag(), + new Controllable() +); + +var enemy = world.Create( + new Position(10, 10), + new Velocity(-1, 0), + new Health(50, 50), + new EnemyTag(), + new AI() +); + +// ❌ 不推荐:使用继承 +public class Player : Entity { } +public class Enemy : Entity { } +``` + +**单一职责系统**: + +```csharp +// ✅ 推荐:每个系统只负责一件事 +public class MovementSystem : ArchSystemAdapter +{ + // 只负责移动 +} + +public class CollisionSystem : ArchSystemAdapter +{ + // 只负责碰撞检测 +} + +public class DamageSystem : ArchSystemAdapter +{ + // 只负责伤害处理 +} + +// ❌ 不推荐:一个系统做太多事 +public class GameplaySystem : ArchSystemAdapter +{ + // 移动、碰撞、伤害、AI... 太多职责 +} +``` + +### 2. 与传统架构结合 + +ECS 可以与 GFramework 的传统架构(Model、System、Utility)结合使用: + +```csharp +// Model 存储全局状态 +public class GameStateModel : AbstractModel +{ + public int Score { get; set; } + public int Level { get; set; } +} + +// ECS System 处理实体逻辑 +public class EnemySpawnSystem : ArchSystemAdapter +{ + private float _spawnTimer; + + protected override void OnUpdate(in float deltaTime) + { + _spawnTimer += deltaTime; + + if (_spawnTimer >= 2.0f) + { + // 获取 Model + var gameState = this.GetModel(); + + // 根据关卡生成敌人 + var enemyCount = gameState.Level * 2; + for (int i = 0; i < enemyCount; i++) + { + World.Create( + new Position(Random.Shared.Next(0, 100), 0), + new Velocity(0, -1), + new Health(50, 50), + new EnemyTag() + ); + } + + _spawnTimer = 0; + } + } +} + +// 传统 System 处理游戏逻辑 +public class ScoreSystem : AbstractSystem +{ + protected override void OnInit() + { + // 监听敌人死亡事件 + this.RegisterEvent(OnEnemyDestroyed); + } + + private void OnEnemyDestroyed(EnemyDestroyedEvent e) + { + var gameState = this.GetModel(); + gameState.Score += 100; + } +} +``` + +### 3. 事件集成 + +ECS 系统可以发送和接收框架事件: + +```csharp +// 定义事件 +public struct EnemyDestroyedEvent +{ + public Entity Enemy { get; init; } + public int Score { get; init; } +} + +// ECS 系统发送事件 +public class HealthSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + var query = new QueryDescription() + .WithAll(); + + World.Query(in query, (Entity entity, ref Health health) => + { + if (health.Current <= 0) + { + // 发送事件 + this.SendEvent(new EnemyDestroyedEvent + { + Enemy = entity, + Score = 100 + }); + + // 销毁实体 + World.Destroy(entity); + } + }); + } +} + +// 传统系统接收事件 +public class UISystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnEnemyDestroyed); + } + + private void OnEnemyDestroyed(EnemyDestroyedEvent e) + { + // 更新 UI + Console.WriteLine($"Enemy destroyed! +{e.Score} points"); + } +} +``` + +### 4. 标签组件 + +使用空结构体作为标签来分类实体: + +```csharp +// 定义标签组件 +public struct PlayerTag { } +public struct EnemyTag { } +public struct BulletTag { } +public struct DeadTag { } + +// 使用标签过滤实体 +public class PlayerMovementSystem : ArchSystemAdapter +{ + private QueryDescription _query; + + protected override void OnArchInitialize() + { + // 只处理玩家实体 + _query = new QueryDescription() + .WithAll() + .WithNone(); + } + + protected override void OnUpdate(in float deltaTime) + { + World.Query(in _query, (ref Position pos, ref Velocity vel) => + { + // 只更新活着的玩家 + pos.X += vel.X * deltaTime; + pos.Y += vel.Y * deltaTime; + }); + } +} +``` + +### 5. 组件生命周期管理 + +```csharp +public class LifecycleManagementSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + // 处理临时效果 + var buffQuery = new QueryDescription().WithAll(); + World.Query(in buffQuery, (Entity entity, ref BuffComponent buff) => + { + buff.Duration -= deltaTime; + + if (buff.Duration <= 0) + { + // 移除过期的 Buff + World.Remove(entity); + } + }); + + // 清理死亡实体 + var deadQuery = new QueryDescription().WithAll(); + World.Query(in deadQuery, (Entity entity) => + { + World.Destroy(entity); + }); + } +} +``` + +## 常见问题 + +### Q: 如何在 ECS 系统中访问其他服务? + +A: `ArchSystemAdapter` 继承自 `AbstractSystem`,可以使用所有 GFramework 的扩展方法: + +```csharp +public class ServiceAccessSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + // 获取 Model + var playerModel = this.GetModel(); + + // 获取 Utility + var timeUtility = this.GetUtility(); + + // 发送命令 + this.SendCommand(new SaveGameCommand()); + + // 发送查询 + var score = this.SendQuery(new GetScoreQuery()); + + // 发送事件 + this.SendEvent(new GameOverEvent()); + } +} +``` + +### Q: ECS 和传统架构如何选择? + +A: 根据场景选择: + +- **使用 ECS**:大量相似实体、需要高性能批量处理(敌人、子弹、粒子) +- **使用传统架构**:全局状态、单例服务、UI 逻辑、游戏流程控制 + +### Q: 如何调试 ECS 系统? + +A: 使用以下方法: + +```csharp +public class DebugSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + // 打印实体数量 + Console.WriteLine($"Total entities: {World.Size}"); + + // 查询特定实体 + var query = new QueryDescription().WithAll(); + var count = 0; + World.Query(in query, (Entity entity, ref Position pos) => + { + count++; + Console.WriteLine($"Entity {entity.Id}: ({pos.X}, {pos.Y})"); + }); + + Console.WriteLine($"Entities with Position: {count}"); + } +} +``` + +### Q: 如何处理实体之间的交互? + +A: 使用查询和事件: + +```csharp +public class CollisionSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + var playerQuery = new QueryDescription() + .WithAll(); + var enemyQuery = new QueryDescription() + .WithAll(); + + // 检测玩家和敌人的碰撞 + World.Query(in playerQuery, (Entity player, ref Position playerPos) => + { + World.Query(in enemyQuery, (Entity enemy, ref Position enemyPos) => + { + var distance = Math.Sqrt( + Math.Pow(playerPos.X - enemyPos.X, 2) + + Math.Pow(playerPos.Y - enemyPos.Y, 2) + ); + + if (distance < 1.0f) + { + // 发送碰撞事件 + this.SendEvent(new CollisionEvent + { + Entity1 = player, + Entity2 = enemy + }); + } + }); + }); + } +} +``` + +### Q: 如何优化大量实体的性能? + +A: 参考性能优化章节,主要策略: + +1. 使用 struct 组件 +2. 缓存查询 +3. 使用 ref 访问组件 +4. 批量处理 +5. 合理设计组件大小 +6. 使用 Arch 的并行查询(高级特性) + +### Q: 可以在运行时动态添加/移除组件吗? + +A: 可以,Arch 支持运行时修改实体的组件: + +```csharp +public class DynamicComponentSystem : ArchSystemAdapter +{ + protected override void OnUpdate(in float deltaTime) + { + var query = new QueryDescription().WithAll(); + + World.Query(in query, (Entity entity, ref Position pos) => + { + // 动态添加组件 + if (pos.X > 100 && !World.Has(entity)) + { + World.Add(entity, new FastTag()); + } + + // 动态移除组件 + if (pos.X < 0 && World.Has(entity)) + { + World.Remove(entity); + } + }); + } +} +``` + +## 相关资源 + +- [Arch.Core 官方文档](https://github.com/genaray/Arch) +- [Architecture 包使用说明](./architecture.md) +- [System 包使用说明](./system.md) +- [事件系统](./events.md) + +--- + +**许可证**:Apache 2.0 diff --git a/docs/zh-CN/core/functional.md b/docs/zh-CN/core/functional.md new file mode 100644 index 0000000..a3e0e04 --- /dev/null +++ b/docs/zh-CN/core/functional.md @@ -0,0 +1,677 @@ +# 函数式编程指南 + +## 概述 + +GFramework.Core 提供了一套完整的函数式编程工具,帮助开发者编写更安全、更简洁、更易维护的代码。函数式编程强调不可变性、纯函数和声明式编程风格,能够有效减少副作用,提高代码的可测试性和可组合性。 + +本模块提供以下核心功能: + +- **Option 类型**:安全处理可能不存在的值,替代 null 引用 +- **Result 类型**:优雅处理操作结果和错误,避免异常传播 +- **管道操作**:构建流式的函数调用链 +- **函数组合**:组合多个函数形成新函数 +- **控制流扩展**:函数式风格的条件执行和重试机制 +- **异步函数式编程**:支持异步操作的函数式封装 + +## 核心概念 + +### Option 类型 + +`Option` 表示可能存在或不存在的值,用于替代 null 引用。它有两种状态: + +- **Some**:包含一个值 +- **None**:不包含值 + +使用 Option 可以在编译时强制处理"无值"的情况,避免空引用异常。 + +### Result 类型 + +`Result` 表示操作的结果,可能是成功值或失败异常。它有三种状态: + +- **Success**:操作成功,包含返回值 +- **Faulted**:操作失败,包含异常信息 +- **Bottom**:未初始化状态 + +Result 类型将错误处理显式化,避免使用异常进行流程控制。 + +### 管道操作 + +管道操作允许将值通过一系列函数进行转换,形成流式的调用链。这种风格使代码更易读,逻辑更清晰。 + +### 函数组合 + +函数组合是将多个简单函数组合成复杂函数的技术。通过组合,可以构建可复用的函数库,提高代码的模块化程度。 + +## 基本用法 + +### Option 基础 + +#### 创建 Option + +```csharp +using GFramework.Core.functional; + +// 创建包含值的 Option +var someValue = Option.Some(42); + +// 创建空 Option +var noneValue = Option.None; + +// 隐式转换 +Option name = "Alice"; // Some("Alice") +Option 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.Some(42); +var mapped = option.Map(x => x.ToString()); // Option.Some("42") + +// Bind:链式转换(单子绑定) +var result = Option.Some("42") + .Bind(s => int.TryParse(s, out var i) + ? Option.Some(i) + : Option.None); +``` + +#### 过滤值 + +```csharp +var option = Option.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.Succeed(42); +var success2 = Result.Success(42); // 别名 + +// 创建失败结果 +var failure = Result.Fail(new Exception("Error")); +var failure2 = Result.Failure("Error message"); + +// 隐式转换 +Result result = 42; // Success(42) +``` + +#### 安全执行 + +```csharp +// 自动捕获异常 +var result = Result.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.Succeed(x.ToString()) + : Result.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 addOne = x => x + 1; +Func 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 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.Succeed(42) + .OnSuccess(x => Console.WriteLine($"Value: {x}")) + .OnFailure(ex => Console.WriteLine($"Error: {ex.Message}")) + .Map(x => x * 2); +``` + +#### 验证约束 + +```csharp +var result = Result.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.Succeed(1), + Result.Succeed(2), + Result.Succeed(3) +}; + +var combined = results.Combine(); // Result> +``` + +### 控制流扩展 + +#### 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.Succeed(42); +var bound = await result.BindAsync(async x => + await GetUserAsync(x) is User user + ? Result.Succeed(user) + : Result.Fail(new Exception("User not found")) +); +``` + +### 高级函数操作 + +#### Partial:偏函数应用 + +```csharp +Func 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 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 FindUser(int id) +{ + return _users.TryGetValue(id, out var user) + ? Option.Some(user) + : Option.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 ParseNumber(string input) +{ + return int.TryParse(input, out var number) + ? Result.Succeed(number) + : Result.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 CreateUser(string name, string email) +{ + return ValidateName(name) + .Bind(_ => ValidateEmail(email)) + .Bind(_ => CheckDuplicate(email)) + .Bind(_ => SaveUser(name, email)); +} +``` + +#### 模式 2:聚合验证 + +```csharp +public Result 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 GetData(int id) +{ + return GetFromCache(id) + .Match( + succ: data => Result.Succeed(data), + fail: _ => GetFromDatabase(id) + ); +} +``` + +### 组合模式 + +#### 模式 1:Option + Result + +```csharp +public Result GetActiveUser(int id) +{ + return FindUser(id) // Option + .ToResult("User not found") // Result + .Ensure(u => u.IsActive, "User is not active"); +} +``` + +#### 模式 2:Result + 管道 + +```csharp +public Result ProcessUser(int id) +{ + return Result.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> ProcessRequestAsync(Request request) +{ + return await Result.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 有什么区别?** + +A: + +- `Nullable` 只能用于值类型,`Option` 可用于任何类型 +- `Option` 提供丰富的函数式操作(Map、Bind、Filter 等) +- `Option` 强制显式处理"无值"情况,更安全 +- `Option` 可以与 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.Some(1), + Option.None, + Option.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` diff --git a/docs/zh-CN/core/pause.md b/docs/zh-CN/core/pause.md new file mode 100644 index 0000000..3b64014 --- /dev/null +++ b/docs/zh-CN/core/pause.md @@ -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 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? OnPauseStateChanged; +``` + +## 基本用法 + +### 1. 获取暂停管理器 + +```csharp +public class GameController : IController +{ + private IPauseStackManager _pauseManager; + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void Initialize() + { + // 从架构中获取暂停管理器 + _pauseManager = this.GetUtility(); + } +} +``` + +### 2. 简单的暂停/恢复 + +```csharp +public class PauseMenuController : IController +{ + private IPauseStackManager _pauseManager; + private PauseToken _pauseToken; + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void Initialize() + { + _pauseManager = this.GetUtility(); + } + + 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(); + } + + 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(); + 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(); + + // 订阅状态变化事件 + _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(); + + // 注册 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(); + + 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(); + _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(); + 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(); + + // 方案 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 +{ + protected override void OnRegisterUtility() + { + // 注册暂停管理器 + RegisterUtility(new PauseStackManager()); + } + + protected override void OnInit() + { + // 注册默认处理器 + var pauseManager = GetUtility(); + + // 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(); + + 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(); + pauseManager.Push(_reason, _group); + } +} +``` + +## 相关包 + +- [`architecture`](./architecture.md) - 架构核心,提供工具注册 +- [`utility`](./utility.md) - 工具基类 +- [`events`](./events.md) - 事件系统,用于状态通知 +- [`lifecycle`](./lifecycle.md) - 生命周期管理 +- [`logging`](./logging.md) - 日志系统,用于调试 +- [Godot 集成](../godot/index.md) - Godot 引擎集成 diff --git a/docs/zh-CN/game/serialization.md b/docs/zh-CN/game/serialization.md new file mode 100644 index 0000000..c436871 --- /dev/null +++ b/docs/zh-CN/game/serialization.md @@ -0,0 +1,756 @@ +--- +title: 序列化系统 +description: 序列化系统提供了统一的对象序列化和反序列化接口,支持 JSON 格式和运行时类型处理。 +--- + +# 序列化系统 + +## 概述 + +序列化系统是 GFramework.Game 中用于对象序列化和反序列化的核心组件。它提供了统一的序列化接口,支持将对象转换为字符串格式(如 +JSON)进行存储或传输,并能够将字符串数据还原为对象。 + +序列化系统与数据存储、配置管理、存档系统等模块深度集成,为游戏数据的持久化提供了基础支持。 + +**主要特性**: + +- 统一的序列化接口 +- JSON 格式支持 +- 运行时类型序列化 +- 泛型和非泛型 API +- 与存储系统无缝集成 +- 类型安全的反序列化 + +## 核心概念 + +### 序列化器接口 + +`ISerializer` 定义了基本的序列化操作: + +```csharp +public interface ISerializer : IUtility +{ + // 将对象序列化为字符串 + string Serialize(T value); + + // 将字符串反序列化为对象 + T Deserialize(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 value); + T Deserialize(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(jsonSerializer); + RegisterUtility(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(); + + 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(); + + string json = "{\"Name\":\"Player1\",\"Level\":10,\"Experience\":1000}"; + + // 反序列化为对象 + var player = serializer.Deserialize(json); + + Console.WriteLine($"玩家: {player.Name}, 等级: {player.Level}"); +} +``` + +### 运行时类型序列化 + +处理不确定类型的对象: + +```csharp +public void SerializeRuntimeType() +{ + var serializer = this.GetUtility(); + + 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(); + var storage = this.GetUtility(); + + var gameData = new GameData + { + Score = 1000, + Coins = 500 + }; + + // 序列化数据 + string json = serializer.Serialize(gameData); + + // 写入存储 + await storage.WriteAsync("game_data", json); + } + + public async Task LoadData() + { + var serializer = this.GetUtility(); + var storage = this.GetUtility(); + + // 从存储读取 + string json = await storage.ReadAsync("game_data"); + + // 反序列化数据 + return serializer.Deserialize(json); + } +} +``` + +### 序列化复杂对象 + +处理嵌套和集合类型: + +```csharp +public class InventoryData +{ + public List Items { get; set; } + public Dictionary 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(); + + var inventory = new InventoryData + { + Items = new List + { + new ItemData { Id = "sword_01", Name = "铁剑", Quantity = 1 }, + new ItemData { Id = "potion_hp", Name = "生命药水", Quantity = 5 } + }, + Resources = new Dictionary + { + { "gold", 1000 }, + { "wood", 500 } + } + }; + + // 序列化复杂对象 + string json = serializer.Serialize(inventory); + + // 反序列化 + var restored = serializer.Deserialize(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(); + + // 创建不同类型的实体 + 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(); + var storage = this.GetUtility(); + + var dataList = new Dictionary + { + { "player", new PlayerData { Name = "Player1", Level = 10 } }, + { "inventory", new InventoryData { Items = new List() } }, + { "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(); + + string json = "{\"Name\":\"Player1\",\"Level\":\"invalid\"}"; // 错误的数据 + + try + { + var player = serializer.Deserialize(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(); + + try + { + return serializer.Deserialize(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(); + + try + { + // 尝试加载新版本 + return serializer.Deserialize(json); + } + catch + { + // 如果失败,尝试加载旧版本并迁移 + var oldData = serializer.Deserialize(json); + return new PlayerDataV2 + { + Name = oldData.Name, + Level = oldData.Level, + Experience = oldData.Level * 100, // 根据等级计算经验 + LastLogin = DateTime.Now + }; + } +} +``` + +## 最佳实践 + +1. **使用接口而非具体类型**:依赖 `ISerializer` 接口 + ```csharp + ✓ var serializer = this.GetUtility(); + ✗ var serializer = new JsonSerializer(); // 避免直接实例化 + ``` + +2. **为数据类提供默认值**:确保反序列化的健壮性 + ```csharp + public class GameData + { + public string Name { get; set; } = "Default"; + public int Score { get; set; } = 0; + public List Items { get; set; } = new(); + } + ``` + +3. **处理反序列化异常**:避免程序崩溃 + ```csharp + try + { + var data = serializer.Deserialize(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(); + string json = serializer.Serialize(obj, obj.GetType()); + ``` + +6. **验证反序列化的数据**:确保数据完整性 + ```csharp + var data = serializer.Deserialize(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(); + var storage = this.GetUtility(); + + 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(); + var storage = this.GetUtility(); + + 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 Children { get; set; } + + [JsonIgnore] // 忽略父节点引用,避免循环 + public Node? Parent { get; set; } +} +``` + +### 问题:序列化后的 JSON 太大怎么办? + +**解答**: +使用压缩或分块存储: + +```csharp +public async Task SaveCompressed() +{ + var serializer = this.GetUtility(); + var storage = this.GetUtility(); + + 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 LoadWithBackup(string key) +{ + var serializer = this.GetUtility(); + var storage = this.GetUtility(); + + try + { + // 尝试加载主数据 + string json = await storage.ReadAsync(key); + return serializer.Deserialize(json); + } + catch + { + // 尝试加载备份 + try + { + string backupJson = await storage.ReadAsync($"{key}_backup"); + return serializer.Deserialize(backupJson); + } + catch + { + // 返回默认数据 + return new GameData(); + } + } +} +``` + +### 问题:如何加密序列化的数据? + +**解答**: +在序列化后加密: + +```csharp +public async Task SaveEncrypted(string key, GameData data) +{ + var serializer = this.GetUtility(); + var storage = this.GetUtility(); + + // 序列化 + string json = serializer.Serialize(data); + + // 加密 + byte[] encrypted = EncryptString(json); + + // 保存 + await storage.WriteAsync(key, encrypted); +} + +public async Task LoadEncrypted(string key) +{ + var serializer = this.GetUtility(); + var storage = this.GetUtility(); + + // 读取 + byte[] encrypted = await storage.ReadAsync(key); + + // 解密 + string json = DecryptToString(encrypted); + + // 反序列化 + return serializer.Deserialize(json); +} +``` + +### 问题:序列化器是线程安全的吗? + +**解答**: +`JsonSerializer` 本身是线程安全的,但建议通过架构的 Utility 系统访问: + +```csharp +// 线程安全的访问方式 +public async Task ParallelSave() +{ + var tasks = Enumerable.Range(0, 10).Select(async i => + { + var serializer = this.GetUtility(); + 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) - 工具类注册 diff --git a/docs/zh-CN/game/storage.md b/docs/zh-CN/game/storage.md new file mode 100644 index 0000000..a72b8f7 --- /dev/null +++ b/docs/zh-CN/game/storage.md @@ -0,0 +1,735 @@ +--- +title: 存储系统详解 +description: 存储系统提供了灵活的文件存储和作用域隔离功能,支持跨平台数据持久化。 +--- + +# 存储系统详解 + +## 概述 + +存储系统是 GFramework.Game 中用于管理文件存储的核心组件。它提供了统一的存储接口,支持键值对存储、作用域隔离、目录操作等功能,让你可以轻松实现游戏数据的持久化。 + +存储系统采用装饰器模式设计,通过 `IStorage` 接口定义统一的存储操作,`FileStorage` 提供基于文件系统的实现,`ScopedStorage` +提供作用域隔离功能。 + +**主要特性**: + +- 统一的键值对存储接口 +- 基于文件系统的持久化 +- 作用域隔离和命名空间管理 +- 线程安全的并发访问 +- 支持同步和异步操作 +- 目录和文件列举功能 +- 路径安全防护 +- 跨平台支持(包括 Godot) + +## 核心概念 + +### 存储接口 + +`IStorage` 定义了统一的存储操作: + +```csharp +public interface IStorage : IUtility +{ + // 检查键是否存在 + bool Exists(string key); + Task ExistsAsync(string key); + + // 读取数据 + T Read(string key); + T Read(string key, T defaultValue); + Task ReadAsync(string key); + + // 写入数据 + void Write(string key, T value); + Task WriteAsync(string key, T value); + + // 删除数据 + void Delete(string key); + Task DeleteAsync(string key); + + // 目录操作 + Task> ListDirectoriesAsync(string path = ""); + Task> ListFilesAsync(string path = ""); + Task DirectoryExistsAsync(string path); + Task CreateDirectoryAsync(string path); +} +``` + +### 文件存储 + +`FileStorage` 是基于文件系统的存储实现: + +- 将数据序列化后保存为文件 +- 支持自定义文件扩展名(默认 `.dat`) +- 使用细粒度锁保证线程安全 +- 自动创建目录结构 +- 防止路径遍历攻击 + +### 作用域存储 + +`ScopedStorage` 提供命名空间隔离: + +- 为所有键添加前缀 +- 支持嵌套作用域 +- 透明包装底层存储 +- 实现逻辑分组 + +### 存储类型 + +`StorageKinds` 枚举定义了不同的存储方式: + +```csharp +[Flags] +public enum StorageKinds +{ + None = 0, + Local = 1 << 0, // 本地文件系统 + Memory = 1 << 1, // 内存存储 + Remote = 1 << 2, // 远程存储 + Database = 1 << 3 // 数据库存储 +} +``` + +## 基本用法 + +### 创建文件存储 + +```csharp +using GFramework.Game.storage; +using GFramework.Game.serializer; + +// 创建序列化器 +var serializer = new JsonSerializer(); + +// 创Windows 示例) +var storage = new FileStorage(@"C:\MyGame\Data", serializer); + +// 或使用自定义扩展名 +var storage = new FileStorage(@"C:\MyGame\Data", serializer, ".json"); +``` + +### 写入和读取数据 + +```csharp +// 写入简单类型 +storage.Write("player_score", 1000); +storage.Write("player_name", "Alice"); + +// 写入复杂对象 +var settings = new GameSettings +{ + Volume = 0.8f, + Difficulty = "Hard", + Language = "zh-CN" +}; +storage.Write("settings", settings); + +// 读取数据 +int score = storage.Read("player_score"); +string name = storage.Read("player_name"); +var loadedSettings = storage.Read("settings"); + +// 读取数据(带默认值) +int highScore = storage.Read("high_score", 0); +``` + +### 异步操作 + +```csharp +// 异步写入 +await storage.WriteAsync("player_level", 10); + +// 异步读取 +int level = await storage.ReadAsync("player_level"); + +// 异步检查存在 +bool exists = await storage.ExistsAsync("player_level"); + +// 异步删除 +await storage.DeleteAsync("player_level"); +``` + +### 检查和删除 + +```csharp +// 检查键是否存在 +if (storage.Exists("player_score")) +{ + Console.WriteLine("存档存在"); +} + +// 删除数据 +storage.Delete("player_score"); + +// 异步检查 +bool exists = await storage.ExistsAsync("player_score"); +``` + +### 使用层级键 + +```csharp +// 使用 / 分隔符创建层级结构 +storage.Write("player/profile/name", "Alice"); +storage.Write("player/profile/level", 10); +storage.Write("player/inventory/gold", 1000); + +// 文件结构: +// Data/ +// player/ +// profile/ +// name.dat +// level.dat +// inventory/ +// gold.dat + +// 读取层级数据 +string name = storage.Read("player/profile/name"); +int gold = storage.Read("player/inventory/gold"); +``` + +## 作用域存储 + +### 创建作用域存储 + +```csharp +using GFramework.Game.storage; + +// 基于文件存储创建作用域存储 +var baseStorage = new FileStorage(@"C:\MyGame\Data", serializer); +var playerStorage = new ScopedStorage(baseStorage, "player"); + +// 所有操作都会添加 "player/" 前缀 +playerStorage.Write("name", "Alice"); // 实际存储为 "player/name.dat" +playerStorage.Write("level", 10); // 实际存储为 "player/level.dat" + +// 读取时也使用相同的前缀 +string name = playerStorage.Read("name"); // 从 "player/name.dat" 读取 +``` + +### 嵌套作用域 + +```csharp +// 创建嵌套作用域 +var settingsStorage = new ScopedStorage(baseStorage, "settings"); +var graphicsStorage = new ScopedStorage(settingsStorage, "graphics"); + +// 前缀变为 "settings/graphics/" +graphicsStorage.Write("resolution", "1920x1080"); +// 实际存储为 "settings/graphics/resolution.dat" + +// 或使用 Scope 方法 +var audioStorage = settingsStorage.Scope("audio"); +audioStorage.Write("volume", 0.8f); +// 实际存储为 "settings/audio/volume.dat" +``` + +### 多作用域隔离 + +```csharp +// 创建不同作用域的存储 +var playerStorage = new ScopedStorage(baseStorage, "player"); +var gameStorage = new ScopedStorage(baseStorage, "game"); +var settingsStorage = new ScopedStorage(baseStorage, "settings"); + +// 在不同作用域中使用相同的键不会冲突 +playerStorage.Write("level", 5); // player/level.dat +gameStorage.Write("level", "forest_area_1"); // game/level.dat +settingsStorage.Write("level", "high"); // settings/level.dat + +// 读取时各自独立 +int playerLevel = playerStorage.Read("level"); // 5 +string gameLevel = gameStorage.Read("level"); // "forest_area_1" +string settingsLevel = settingsStorage.Read("level"); // "high" +``` + +## 高级用法 + +### 目录操作 + +```csharp +// 列举子目录 +var directories = await storage.ListDirectoriesAsync("player"); +foreach (var dir in directories) +{ + Console.WriteLine($"目录: {dir}"); +} + +// 列举文件 +var files = await storage.ListFilesAsync("player/inventory"); +foreach (var file in files) +{ + Console.WriteLine($"文件: {file}"); +} + +// 检查目录是否存在 +bool exists = await storage.DirectoryExistsAsync("player/quests"); + +// 创建目录 +await storage.CreateDirectoryAsync("player/achievements"); +``` + +### 批量操作 + +```csharp +public async Task SaveAllPlayerData(PlayerData player) +{ + var playerStorage = new ScopedStorage(baseStorage, $"player_{player.Id}"); + + // 批量写入 + var tasks = new List + { + playerStorage.WriteAsync("profile", player.Profile), + playerStorage.WriteAsync("inventory", player.Inventory), + playerStorage.WriteAsync("quests", player.Quests), + playerStorage.WriteAsync("achievements", player.Achievements) + }; + + await Task.WhenAll(tasks); + Console.WriteLine("所有玩家数据已保存"); +} + +public async Task LoadAllPlayerData(int playerId) +{ + var playerStorage = new ScopedStorage(baseStorage, $"player_{playerId}"); + + // 批量读取 + var tasks = new[] + { + playerStorage.ReadAsync("profile"), + playerStorage.ReadAsync("inventory"), + playerStorage.ReadAsync("quests"), + playerStorage.ReadAsync("achievements") + }; + + await Task.WhenAll(tasks); + + return new PlayerData + { + Id = playerId, + Profile = tasks[0].Result, + Inventory = tasks[1].Result, + Quests = tasks[2].Result, + Achievements = tasks[3].Result + }; +} +``` + +### 存储迁移 + +```csharp +public async Task MigrateStorage(IStorage oldStorage, IStorage newStorage, string path = "") +{ + // 列举所有文件 + var files = await oldStorage.ListFilesAsync(path); + + foreach (var file in files) + { + var key = string.IsNullOrEmpty(path) ? file : $"{path}/{file}"; + + // 读取旧数据 + var data = await oldStorage.ReadAsync(key); + + // 写入新存储 + await newStorage.WriteAsync(key, data); + + Console.WriteLine($"已迁移: {key}"); + } + + // 递归处理子目录 + var directories = await oldStorage.ListDirectoriesAsync(path); + foreach (var dir in directories) + { + var subPath = string.IsNullOrEmpty(path) ? dir : $"{path}/{dir}"; + await MigrateStorage(oldStorage, newStorage, subPath); + } +} +``` + +### 存储备份 + +```csharp +public class StorageBackupSystem +{ + private readonly IStorage _storage; + private readonly string _backupPrefix = "backup"; + + public async Task CreateBackup(string sourcePath) + { + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + var backupPath = $"{_backupPrefix}/{timestamp}"; + + await CopyDirectory(sourcePath, backupPath); + Console.WriteLine($"备份已创建: {backupPath}"); + } + + public async Task RestoreBackup(string backupName, string targetPath) + { + var backupPath = $"{_backupPrefix}/{backupName}"; + + if (!await _storage.DirectoryExistsAsync(backupPath)) + { + throw new DirectoryNotFoundException($"备份不存在: {backupName}"); + } + + await CopyDirectory(backupPath, targetPath); + Console.WriteLine($"已从备份恢复: {backupName}"); + } + + private async Task CopyDirectory(string source, string target) + { + var files = await _storage.ListFilesAsync(source); + foreach (var file in files) + { + var sourceKey = $"{source}/{file}"; + var targetKey = $"{target}/{file}"; + var data = await _storage.ReadAsync(sourceKey); + await _storage.WriteAsync(targetKey, data); + } + + var directories = await _storage.ListDirectoriesAsync(source); + foreach (var dir in directories) + { + await CopyDirectory($"{source}/{dir}", $"{target}/{dir}"); + } + } +} +``` + +### 缓存层 + +```csharp +public class CachedStorage : IStorage +{ + private readonly IStorage _innerStorage; + private readonly ConcurrentDictionary _cache = new(); + + public CachedStorage(IStorage innerStorage) + { + _innerStorage = innerStorage; + } + + public T Read(string key) + { + // 先从缓存读取 + if (_cache.TryGetValue(key, out var cached)) + { + return (T)cached; + } + + // 从存储读取并缓存 + var value = _innerStorage.Read(key); + _cache[key] = value; + return value; + } + + public void Write(string key, T value) + { + // 写入存储 + _innerStorage.Write(key, value); + + // 更新缓存 + _cache[key] = value; + } + + public void Delete(string key) + { + _innerStorage.Delete(key); + _cache.TryRemove(key, out _); + } + + public void ClearCache() + { + _cache.Clear(); + } +} +``` + +## Godot 集成 + +### 使用 Godot 文件存储 + +```csharp +using GFramework.Godot.storage; + +// 创建 Godot 文件存储 +var storage = new GodotFileStorage(serializer); + +// 使用 user:// 路径(用户数据目录) +storage.Write("user://saves/slot1.dat", saveData); +var data = storage.Read("user://saves/slot1.dat"); + +// 使用 res:// 路径(资源目录,只读) +var config = storage.Read("res://config/default.json"); + +// 普通文件路径也支持 +storage.Write("/tmp/temp_data.dat", tempData); +``` + +### Godot 路径说明 + +```csharp +// user:// - 用户数据目录 +// Windows: %APPDATA%/Godot/app_userdata/[project_name] +// Linux: ~/.local/share/godot/app_userdata/[project_name] +// macOS: ~/Library/Application Support/Godot/app_userdata/[project_name] +storage.Write("user://save.dat", data); + +// res:// - 项目资源目录(只读) +var config = storage.Read("res://data/config.json"); + +// 绝对路径 +storage.Write("/home/user/game/data.dat", data); +``` + +## 最佳实践 + +1. **使用作用域隔离不同类型的数据** + ```csharp + ✓ var playerStorage = new ScopedStorage(baseStorage, "player"); + ✓ var settingsStorage = new ScopedStorage(baseStorage, "settings"); + ✗ storage.Write("player_name", name); // 不使用作用域 + ``` + +2. **使用异步操作避免阻塞** + ```csharp + ✓ await storage.WriteAsync("data", value); + ✗ storage.Write("data", value); // 在 UI 线程中同步操作 + ``` + +3. **读取时提供默认值** + ```csharp + ✓ int score = storage.Read("score", 0); + ✗ int score = storage.Read("score"); // 键不存在时抛异常 + ``` + +4. **使用层级键组织数据** + ```csharp + ✓ storage.Write("player/inventory/gold", 1000); + ✗ storage.Write("player_inventory_gold", 1000); + ``` + +5. **处理存储异常** + ```csharp + try + { + await storage.WriteAsync("data", value); + } + catch (IOException ex) + { + Logger.Error($"存储失败: {ex.Message}"); + ShowErrorMessage("保存失败,请检查磁盘空间"); + } + ``` + +6. **定期清理过期数据** + ```csharp + public async Task CleanupOldData(TimeSpan maxAge) + { + var files = await storage.ListFilesAsync("temp"); + foreach (var file in files) + { + var data = await storage.ReadAsync($"temp/{file}"); + if (DateTime.Now - data.Timestamp > maxAge) + { + await storage.DeleteAsync($"temp/{file}"); + } + } + } + ``` + +7. **使用合适的序列化器** + ```csharp + // JSON - 可读性好,适合配置文件 + var jsonStorage = new FileStorage(path, new JsonSerializer(), ".json"); + + // 二进制 - 性能好,适合大量数据 + var binaryStorage = new FileStorage(path, new BinarySerializer(), ".dat"); + ``` + +## 常见问题 + +### 问题:如何实现跨平台存储路径? + +**解答**: +使用 `Environment.GetFolderPath` 获取平台特定路径: + +```csharp +public static string GetStoragePath() +{ + var appData = Environment.GetFolderPath( + Environment.SpecialFolder.ApplicationData); + return Path.Combine(appData, "MyGame", "Data"); +} + +var storage = new FileStorage(GetStoragePath(), serializer); +``` + +### 问题:存储系统是否线程安全? + +**解答**: +是的,`FileStorage` 使用细粒度锁机制保证线程安全: + +```csharp +// 不同键的操作可以并发执行 +Task.Run(() => storage.Write("key1", value1)); +Task.Run(() => storage.Write("key2", value2)); + +// 相同键的操作会串行化 +Task.Run(() => storage.Write("key", value1)); +Task.Run(() => storage.Write("key", value2)); // 等待第一个完成 +``` + +### 问题:如何实现存储加密? + +**解答**: +创建加密存储包装器: + +```csharp +public class EncryptedStorage : IStorage +{ + private readonly IStorage _innerStorage; + private readonly IEncryption _encryption; + + public void Write(string key, T value) + { + var json = JsonSerializer.Serialize(value); + var encrypted = _encryption.Encrypt(json); + _innerStorage.Write(key, encrypted); + } + + public T Read(string key) + { + var encrypted = _innerStorage.Read(key); + var json = _encryption.Decrypt(encrypted); + return JsonSerializer.Deserialize(json); + } +} +``` + +### 问题:如何限制存储大小? + +**解答**: +实现配额管理: + +```csharp +public class QuotaStorage : IStorage +{ + private readonly IStorage _innerStorage; + private readonly long _maxSize; + private long _currentSize; + + public void Write(string key, T value) + { + var data = Serialize(value); + var size = data.Length; + + if (_currentSize + size > _maxSize) + { + throw new InvalidOperationException("存储配额已满"); + } + + _innerStorage.Write(key, value); + _currentSize += size; + } +} +``` + +### 问题:如何实现存储压缩? + +**解答**: +使用压缩序列化器: + +```csharp +public class CompressedSerializer : ISerializer +{ + private readonly ISerializer _innerSerializer; + + public string Serialize(T value) + { + var json = _innerSerializer.Serialize(value); + var bytes = Encoding.UTF8.GetBytes(json); + var compressed = Compress(bytes); + return Convert.ToBase64String(compressed); + } + + public T Deserialize(string data) + { + var compressed = Convert.FromBase64String(data); + var bytes = Decompress(compressed); + var json = Encoding.UTF8.GetString(bytes); + return _innerSerializer.Deserialize(json); + } + + private byte[] Compress(byte[] data) + { + using var output = new MemoryStream(); + using (var gzip = new GZipStream(output, CompressionMode.Compress)) + { + gzip.Write(data, 0, data.Length); + } + return output.ToArray(); + } + + private byte[] Decompress(byte[] data) + { + using var input = new MemoryStream(data); + using var gzip = new GZipStream(input, CompressionMode.Decompress); + using var output = new MemoryStream(); + gzip.CopyTo(output); + return output.ToArray(); + } +} +``` + +### 问题:如何监控存储操作? + +**解答**: +实现日志存储包装器: + +```csharp +public class LoggingStorage : IStorage +{ + private readonly IStorage _innerStorage; + private readonly ILogger _logger; + + public void Write(string key, T value) + { + var stopwatch = Stopwatch.StartNew(); + try + { + _innerStorage.Write(key, value); + _logger.Info($"写入成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms"); + } + catch (Exception ex) + { + _logger.Error($"写入失败: {key}, 错误: {ex.Message}"); + throw; + } + } + + public T Read(string key) + { + var stopwatch = Stopwatch.StartNew(); + try + { + var value = _innerStorage.Read(key); + _logger.Info($"读取成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms"); + return value; + } + catch (Exception ex) + { + _logger.Error($"读取失败: {key}, 错误: {ex.Message}"); + throw; + } + } +} +``` + +## 相关文档 + +- [数据与存档系统](/zh-CN/game/data) - 数据持久化 +- [序列化系统](/zh-CN/core/serializer) - 数据序列化 +- [Godot 集成](/zh-CN/godot/index) - Godot 中的存储 +- [存档系统教程](/zh-CN/tutorials/save-system) - 完整示例 diff --git a/docs/zh-CN/godot/logging.md b/docs/zh-CN/godot/logging.md new file mode 100644 index 0000000..ea66f4f --- /dev/null +++ b/docs/zh-CN/godot/logging.md @@ -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(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) - 架构最佳实践 diff --git a/docs/zh-CN/godot/pause.md b/docs/zh-CN/godot/pause.md new file mode 100644 index 0000000..2c92717 --- /dev/null +++ b/docs/zh-CN/godot/pause.md @@ -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? 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(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(); + _pauseToken = pauseManager.Push("Pause menu opened"); + + Show(); + } + + public void HidePauseMenu() + { + // 恢复游戏 + var pauseManager = this.GetUtility(); + pauseManager.Pop(_pauseToken); + + Hide(); + } +} +``` + +### 使用暂停作用域 + +```csharp +using Godot; +using GFramework.Godot.extensions; + +public partial class DialogBox : Control +{ + public async void ShowDialog() + { + var pauseManager = this.GetUtility(); + + // 使用 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(); + + // 检查是否暂停 + 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(); + _gameplayPauseToken = pauseManager.Push("Gameplay paused", PauseGroup.Gameplay); + } + + public void ResumeGameplay() + { + var pauseManager = this.GetUtility(); + pauseManager.Pop(_gameplayPauseToken); + } + + // 只暂停动画 + public void PauseAnimations() + { + var pauseManager = this.GetUtility(); + _animationPauseToken = pauseManager.Push("Animations paused", PauseGroup.Animation); + } + + public void ResumeAnimations() + { + var pauseManager = this.GetUtility(); + pauseManager.Pop(_animationPauseToken); + } + + // 检查特定组的暂停状态 + public bool IsGameplayPaused() + { + var pauseManager = this.GetUtility(); + 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(); + + // 第一层暂停 + 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(); + var animationPlayer = GetNode("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