diff --git a/docs/zh-CN/best-practices/architecture-patterns.md b/docs/zh-CN/best-practices/architecture-patterns.md index a0b6b7d..7067b15 100644 --- a/docs/zh-CN/best-practices/architecture-patterns.md +++ b/docs/zh-CN/best-practices/architecture-patterns.md @@ -1,9 +1,19 @@ -# 架构模式最佳实践 +# 架构设计模式指南 -> 指导你如何设计清晰、可维护、可扩展的游戏架构,遵循 GFramework 的设计原则。 +> 全面介绍 GFramework 中的架构设计模式,帮助你构建清晰、可维护、可扩展的游戏架构。 ## 📋 目录 +- [概述](#概述) +- [MVC 模式](#mvc-模式) +- [MVVM 模式](#mvvm-模式) +- [命令模式](#命令模式) +- [查询模式](#查询模式) +- [事件驱动模式](#事件驱动模式) +- [依赖注入模式](#依赖注入模式) +- [服务定位器模式](#服务定位器模式) +- [对象池模式](#对象池模式) +- [状态模式](#状态模式) - [设计原则](#设计原则) - [架构分层](#架构分层) - [依赖管理](#依赖管理) @@ -12,6 +22,1320 @@ - [错误处理策略](#错误处理策略) - [测试策略](#测试策略) - [重构指南](#重构指南) +- [模式选择与组合](#模式选择与组合) +- [常见问题](#常见问题) + +## 概述 + +架构设计模式是经过验证的解决方案,用于解决软件开发中的常见问题。GFramework 内置了多种设计模式,帮助你构建高质量的游戏应用。 + +### 为什么需要架构设计模式? + +1. **提高代码质量**:遵循最佳实践,减少 bug +2. **增强可维护性**:清晰的结构,易于理解和修改 +3. **促进团队协作**:统一的代码风格和架构 +4. **提升可扩展性**:轻松添加新功能 +5. **简化测试**:解耦的组件更容易测试 + +### GFramework 支持的核心模式 + +| 模式 | 用途 | 核心组件 | +|-----------|--------------|-------------------------| +| **MVC** | 分离数据、视图和控制逻辑 | Model, Controller | +| **MVVM** | 数据绑定和响应式 UI | Model, BindableProperty | +| **命令模式** | 封装操作请求 | ICommand, CommandBus | +| **查询模式** | 分离读操作 | IQuery, QueryBus | +| **事件驱动** | 松耦合通信 | IEventBus, Event | +| **依赖注入** | 控制反转 | IIocContainer | +| **服务定位器** | 服务查找 | Architecture.GetSystem | +| **对象池** | 对象复用 | IObjectPoolSystem | +| **状态模式** | 状态管理 | IStateMachine | + +## MVC 模式 + +### 概念 + +MVC(Model-View-Controller)是一种将应用程序分为三个核心组件的架构模式: + +- **Model(模型)**:管理数据和业务逻辑 +- **View(视图)**:显示数据给用户 +- **Controller(控制器)**:处理用户输入,协调 Model 和 View + +### 在 GFramework 中的实现 + +```csharp +// Model - 数据层 +public class PlayerModel : AbstractModel +{ + public BindableProperty<int> Health { get; } = new(100); + public BindableProperty<int> Score { get; } = new(0); + public BindableProperty<string> Name { get; } = new("Player"); + + protected override void OnInit() + { + // 监听数据变化 + Health.Register(newHealth => + { + if (newHealth <= 0) + { + this.SendEvent(new PlayerDiedEvent()); + } + }); + } +} + +// Controller - 控制层 +[ContextAware] +public partial class PlayerController : Node, IController +{ + private PlayerModel _playerModel; + + public override void _Ready() + { + _playerModel = Context.GetModel<PlayerModel>(); + + // 监听数据变化,更新视图 + _playerModel.Health.Register(UpdateHealthUI); + _playerModel.Score.Register(UpdateScoreUI); + } + + // 处理用户输入 + public override void _Input(InputEvent @event) + { + if (@event is InputEventKey keyEvent && keyEvent.Pressed) + { + if (keyEvent.Keycode == Key.Space) + { + // 发送命令修改 Model + Context.SendCommand(new AttackCommand()); + } + } + } + + // 更新视图 + private void UpdateHealthUI(int health) + { + var healthBar = GetNode<ProgressBar>("HealthBar"); + healthBar.Value = health; + } + + private void UpdateScoreUI(int score) + { + var scoreLabel = GetNode<Label>("ScoreLabel"); + scoreLabel.Text = $"Score: {score}"; + } +} + +// System - 业务逻辑层(可选) +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent<AttackCommand>(OnAttack); + } + + private void OnAttack(AttackCommand cmd) + { + var playerModel = this.GetModel<PlayerModel>(); + var enemyModel = this.GetModel<EnemyModel>(); + + // 计算伤害 + int damage = CalculateDamage(playerModel, enemyModel); + enemyModel.Health.Value -= damage; + + // 增加分数 + if (enemyModel.Health.Value <= 0) + { + playerModel.Score.Value += 100; + } + } + + private int CalculateDamage(PlayerModel player, EnemyModel enemy) + { + return 10; // 简化示例 + } +} +``` + +### MVC 优势 + +- ✅ **职责分离**:Model、View、Controller 各司其职 +- ✅ **易于测试**:可以独立测试每个组件 +- ✅ **可维护性高**:修改一个组件不影响其他组件 +- ✅ **支持多视图**:同一个 Model 可以有多个 View + +### 最佳实践 + +1. **Model 只负责数据**:不包含 UI 逻辑 +2. **Controller 协调交互**:不直接操作 UI 细节 +3. **View 只负责显示**:不包含业务逻辑 +4. **使用事件通信**:Model 变化通过事件通知 Controller + +## MVVM 模式 + +### 概念 + +MVVM(Model-View-ViewModel)是 MVC 的变体,强调数据绑定和响应式编程: + +- **Model**:数据和业务逻辑 +- **View**:用户界面 +- **ViewModel**:View 的抽象,提供数据绑定 + +### 在 GFramework 中的实现 + +```csharp +// Model - 数据层 +public class GameModel : AbstractModel +{ + public BindableProperty<int> CurrentLevel { get; } = new(1); + public BindableProperty<float> Progress { get; } = new(0f); + public BindableProperty<bool> IsLoading { get; } = new(false); + + protected override void OnInit() + { + Progress.Register(progress => + { + if (progress >= 1.0f) + { + this.SendEvent(new LevelCompletedEvent()); + } + }); + } +} + +// ViewModel - 视图模型(在 GFramework 中,Model 本身就是 ViewModel) +public class PlayerViewModel : AbstractModel +{ + private PlayerModel _playerModel; + + // 计算属性 + public BindableProperty<string> HealthText { get; } = new(""); + public BindableProperty<float> HealthPercentage { get; } = new(1.0f); + public BindableProperty<bool> IsAlive { get; } = new(true); + + protected override void OnInit() + { + _playerModel = this.GetModel<PlayerModel>(); + + // 绑定数据转换 + _playerModel.Health.Register(health => + { + HealthText.Value = $"{health} / {_playerModel.MaxHealth.Value}"; + HealthPercentage.Value = (float)health / _playerModel.MaxHealth.Value; + IsAlive.Value = health > 0; + }); + } +} + +// View - 视图层 +[ContextAware] +public partial class PlayerView : Control, IController +{ + private PlayerViewModel _viewModel; + private Label _healthLabel; + private ProgressBar _healthBar; + private Panel _deathPanel; + + public override void _Ready() + { + _viewModel = Context.GetModel<PlayerViewModel>(); + + _healthLabel = GetNode<Label>("HealthLabel"); + _healthBar = GetNode<ProgressBar>("HealthBar"); + _deathPanel = GetNode<Panel>("DeathPanel"); + + // 数据绑定 + _viewModel.HealthText.Register(text => _healthLabel.Text = text); + _viewModel.HealthPercentage.Register(pct => _healthBar.Value = pct * 100); + _viewModel.IsAlive.Register(alive => _deathPanel.Visible = !alive); + } +} +``` + +### MVVM 优势 + +- ✅ **自动更新 UI**:数据变化自动反映到界面 +- ✅ **减少样板代码**:不需要手动更新 UI +- ✅ **易于测试**:ViewModel 可以独立测试 +- ✅ **支持复杂 UI**:适合数据驱动的界面 + +### 最佳实践 + +1. **使用 BindableProperty**:实现响应式数据 +2. **ViewModel 不依赖 View**:保持单向依赖 +3. **计算属性放在 ViewModel**:如百分比、格式化文本 +4. **避免在 View 中写业务逻辑**:只负责数据绑定 + +## 命令模式 + +### 概念 + +命令模式将请求封装为对象,从而支持参数化、队列化、日志记录和撤销操作。 + +### 在 GFramework 中的实现 + +```csharp +// 定义命令输入 +public class BuyItemInput : ICommandInput +{ + public string ItemId { get; set; } + public int Quantity { get; set; } +} + +// 实现命令 +public class BuyItemCommand : AbstractCommand<BuyItemInput> +{ + protected override void OnExecute(BuyItemInput input) + { + var playerModel = this.GetModel<PlayerModel>(); + var inventoryModel = this.GetModel<InventoryModel>(); + var shopModel = this.GetModel<ShopModel>(); + + // 获取物品信息 + var item = shopModel.GetItem(input.ItemId); + var totalCost = item.Price * input.Quantity; + + // 检查金币 + if (playerModel.Gold.Value < totalCost) + { + this.SendEvent(new InsufficientGoldEvent()); + return; + } + + // 扣除金币 + playerModel.Gold.Value -= totalCost; + + // 添加物品 + inventoryModel.AddItem(input.ItemId, input.Quantity); + + // 发送事件 + this.SendEvent(new ItemPurchasedEvent + { + ItemId = input.ItemId, + Quantity = input.Quantity, + Cost = totalCost + }); + } +} + +// 使用命令 +public class ShopController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void OnBuyButtonClicked(string itemId, int quantity) + { + // 创建并发送命令 + var input = new BuyItemInput + { + ItemId = itemId, + Quantity = quantity + }; + + this.SendCommand(new BuyItemCommand { Input = input }); + } +} +``` + +### 支持撤销的命令 + +```csharp +public interface IUndoableCommand : ICommand +{ + void Undo(); +} + +public class MoveCommand : AbstractCommand, IUndoableCommand +{ + private Vector2 _previousPosition; + private Vector2 _newPosition; + + public MoveCommand(Vector2 newPosition) + { + _newPosition = newPosition; + } + + protected override void OnExecute() + { + var playerModel = this.GetModel<PlayerModel>(); + _previousPosition = playerModel.Position.Value; + playerModel.Position.Value = _newPosition; + } + + public void Undo() + { + var playerModel = this.GetModel<PlayerModel>(); + playerModel.Position.Value = _previousPosition; + } +} + +// 命令历史管理器 +public class CommandHistory +{ + private readonly Stack<IUndoableCommand> _history = new(); + + public void Execute(IUndoableCommand command) + { + command.Execute(); + _history.Push(command); + } + + public void Undo() + { + if (_history.Count > 0) + { + var command = _history.Pop(); + command.Undo(); + } + } +} +``` + +### 命令模式优势 + +- ✅ **解耦发送者和接收者**:调用者不需要知道实现细节 +- ✅ **支持撤销/重做**:保存命令历史 +- ✅ **支持队列和日志**:可以记录所有操作 +- ✅ **易于扩展**:添加新命令不影响现有代码 + +### 最佳实践 + +1. **命令保持原子性**:一个命令完成一个完整操作 +2. **使用输入对象传参**:避免构造函数参数过多 +3. **命令无状态**:执行完即可丢弃 +4. **发送事件通知结果**:而不是返回值 + +## 查询模式 + +### 概念 + +查询模式(CQRS 的一部分)将读操作与写操作分离,查询只读取数据,不修改状态。 + +### 在 GFramework 中的实现 + +```csharp +// 定义查询输入 +public class GetPlayerStatsInput : IQueryInput +{ + public string PlayerId { get; set; } +} + +// 定义查询结果 +public class PlayerStats +{ + public int Level { get; set; } + public int Health { get; set; } + public int MaxHealth { get; set; } + public int Attack { get; set; } + public int Defense { get; set; } + public int TotalPower { get; set; } +} + +// 实现查询 +public class GetPlayerStatsQuery : AbstractQuery<GetPlayerStatsInput, PlayerStats> +{ + protected override PlayerStats OnDo(GetPlayerStatsInput input) + { + var playerModel = this.GetModel<PlayerModel>(); + var equipmentModel = this.GetModel<EquipmentModel>(); + + // 计算总战力 + int basePower = playerModel.Level.Value * 10; + int equipmentPower = equipmentModel.GetTotalPower(); + + return new PlayerStats + { + Level = playerModel.Level.Value, + Health = playerModel.Health.Value, + MaxHealth = playerModel.MaxHealth.Value, + Attack = playerModel.Attack.Value + equipmentModel.GetAttackBonus(), + Defense = playerModel.Defense.Value + equipmentModel.GetDefenseBonus(), + TotalPower = basePower + equipmentPower + }; + } +} + +// 使用查询 +public class CharacterPanelController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void ShowCharacterStats() + { + var input = new GetPlayerStatsInput { PlayerId = "player1" }; + var query = new GetPlayerStatsQuery { Input = input }; + var stats = this.SendQuery(query); + + // 显示统计信息 + DisplayStats(stats); + } + + private void DisplayStats(PlayerStats stats) + { + Console.WriteLine($"Level: {stats.Level}"); + Console.WriteLine($"Health: {stats.Health}/{stats.MaxHealth}"); + Console.WriteLine($"Attack: {stats.Attack}"); + Console.WriteLine($"Defense: {stats.Defense}"); + Console.WriteLine($"Total Power: {stats.TotalPower}"); + } +} +``` + +### 复杂查询示例 + +```csharp +// 查询背包中可装备的物品 +public class GetEquippableItemsQuery : AbstractQuery<EmptyQueryInput, List<Item>> +{ + protected override List<Item> OnDo(EmptyQueryInput input) + { + var inventoryModel = this.GetModel<InventoryModel>(); + var playerModel = this.GetModel<PlayerModel>(); + + return inventoryModel.GetAllItems() + .Where(item => item.Type == ItemType.Equipment) + .Where(item => item.RequiredLevel <= playerModel.Level.Value) + .OrderByDescending(item => item.Power) + .ToList(); + } +} + +// 组合查询 +public class CanUseSkillQuery : AbstractQuery<CanUseSkillInput, bool> +{ + protected override bool OnDo(CanUseSkillInput input) + { + var playerModel = this.GetModel<PlayerModel>(); + + // 查询技能消耗 + var costQuery = new GetSkillCostQuery { Input = new GetSkillCostInput { SkillId = input.SkillId } }; + var cost = this.SendQuery(costQuery); + + // 查询冷却状态 + var cooldownQuery = new IsSkillOnCooldownQuery { Input = new IsSkillOnCooldownInput { SkillId = input.SkillId } }; + var onCooldown = this.SendQuery(cooldownQuery); + + // 综合判断 + return playerModel.Mana.Value >= cost.ManaCost + && !onCooldown + && playerModel.Health.Value > 0; + } +} +``` + +### 查询模式优势 + +- ✅ **职责分离**:读写操作明确分离 +- ✅ **易于优化**:可以针对查询进行缓存优化 +- ✅ **提高可读性**:查询意图清晰 +- ✅ **支持复杂查询**:可以组合多个简单查询 + +### 最佳实践 + +1. **查询只读取,不修改**:保持查询的纯粹性 +2. **使用清晰的命名**:Get、Is、Can、Has 等前缀 +3. **避免过度查询**:频繁查询考虑使用 BindableProperty +4. **合理使用缓存**:复杂计算结果可以缓存 + +## 事件驱动模式 + +### 概念 + +事件驱动模式通过事件实现组件间的松耦合通信。发送者不需要知道接收者,接收者通过订阅事件来响应变化。 + +### 在 GFramework 中的实现 + +```csharp +// 定义事件 +public struct PlayerDiedEvent +{ + public Vector3 Position { get; set; } + public string Cause { get; set; } + public int FinalScore { get; set; } +} + +public struct EnemyKilledEvent +{ + public string EnemyId { get; set; } + public int Reward { get; set; } +} + +// Model 发送事件 +public class PlayerModel : AbstractModel +{ + public BindableProperty<int> Health { get; } = new(100); + public BindableProperty<Vector3> Position { get; } = new(Vector3.Zero); + + protected override void OnInit() + { + Health.Register(newHealth => + { + if (newHealth <= 0) + { + // 发送玩家死亡事件 + this.SendEvent(new PlayerDiedEvent + { + Position = Position.Value, + Cause = "Health depleted", + FinalScore = this.GetModel<GameModel>().Score.Value + }); + } + }); + } +} + +// System 监听和发送事件 +public class AchievementSystem : AbstractSystem +{ + private int _enemyKillCount = 0; + + protected override void OnInit() + { + // 监听敌人被杀事件 + this.RegisterEvent<EnemyKilledEvent>(OnEnemyKilled); + + // 监听玩家死亡事件 + this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied); + } + + private void OnEnemyKilled(EnemyKilledEvent e) + { + _enemyKillCount++; + + // 检查成就条件 + if (_enemyKillCount == 10) + { + this.SendEvent(new AchievementUnlockedEvent + { + AchievementId = "first_blood_10", + Title = "新手猎人", + Description = "击败10个敌人" + }); + } + } + + private void OnPlayerDied(PlayerDiedEvent e) + { + // 记录统计数据 + var statsModel = this.GetModel<StatisticsModel>(); + statsModel.RecordDeath(e.Position, e.Cause); + } +} + +// Controller 监听事件 +public class UIController : IController +{ + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void Initialize() + { + // 监听成就解锁事件 + this.RegisterEvent<AchievementUnlockedEvent>(OnAchievementUnlocked) + .AddToUnregisterList(_unregisterList); + + // 监听玩家死亡事件 + this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied) + .AddToUnregisterList(_unregisterList); + } + + private void OnAchievementUnlocked(AchievementUnlockedEvent e) + { + ShowAchievementNotification(e.Title, e.Description); + } + + private void OnPlayerDied(PlayerDiedEvent e) + { + ShowGameOverScreen(e.FinalScore); + } + + public void Cleanup() + { + _unregisterList.UnRegisterAll(); + } +} +``` + +### 事件组合 + +```csharp +// 使用 OrEvent 组合多个事件 +public class InputController : IController +{ + public void Initialize() + { + var onAnyInput = new OrEvent() + .Or(keyboardEvent) + .Or(mouseEvent) + .Or(gamepadEvent); + + onAnyInput.Register(() => + { + ResetIdleTimer(); + }); + } +} +``` + +### 事件驱动模式优势 + +- ✅ **松耦合**:发送者和接收者互不依赖 +- ✅ **一对多通信**:一个事件可以有多个监听者 +- ✅ **易于扩展**:添加新监听者不影响现有代码 +- ✅ **支持异步**:事件可以异步处理 + +### 最佳实践 + +1. **事件命名使用过去式**:PlayerDiedEvent、LevelCompletedEvent +2. **事件使用结构体**:减少内存分配 +3. **及时注销事件**:使用 IUnRegisterList 管理 +4. **避免事件循环**:事件处理器中谨慎发送新事件 + +## 依赖注入模式 + +### 概念 + +依赖注入(DI)是一种实现控制反转(IoC)的技术,通过外部注入依赖而不是在类内部创建。 + +### 在 GFramework 中的实现 + +```csharp +// 定义接口 +public interface IStorageService +{ + Task SaveAsync<T>(string key, T data); + Task<T> LoadAsync<T>(string key); +} + +public interface IAudioService +{ + void PlaySound(string soundId); + void PlayMusic(string musicId); +} + +// 实现服务 +public class LocalStorageService : IStorageService +{ + public async Task SaveAsync<T>(string key, T data) + { + var json = JsonSerializer.Serialize(data); + await File.WriteAllTextAsync($"saves/{key}.json", json); + } + + public async Task<T> LoadAsync<T>(string key) + { + var json = await File.ReadAllTextAsync($"saves/{key}.json"); + return JsonSerializer.Deserialize<T>(json); + } +} + +public class GodotAudioService : IAudioService +{ + private AudioStreamPlayer _soundPlayer; + private AudioStreamPlayer _musicPlayer; + + public void PlaySound(string soundId) + { + var sound = GD.Load<AudioStream>($"res://sounds/{soundId}.ogg"); + _soundPlayer.Stream = sound; + _soundPlayer.Play(); + } + + public void PlayMusic(string musicId) + { + var music = GD.Load<AudioStream>($"res://music/{musicId}.ogg"); + _musicPlayer.Stream = music; + _musicPlayer.Play(); + } +} + +// 在架构中注册服务 +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册服务实现 + RegisterUtility<IStorageService>(new LocalStorageService()); + RegisterUtility<IAudioService>(new GodotAudioService()); + + // 注册 System(System 会自动获取依赖) + RegisterSystem(new SaveSystem()); + RegisterSystem(new AudioSystem()); + } +} + +// System 使用依赖注入 +public class SaveSystem : AbstractSystem +{ + private IStorageService _storageService; + + protected override void OnInit() + { + // 从容器获取依赖 + _storageService = this.GetUtility<IStorageService>(); + + this.RegisterEvent<SaveGameEvent>(OnSaveGame); + } + + private async void OnSaveGame(SaveGameEvent e) + { + var playerModel = this.GetModel<PlayerModel>(); + var saveData = new SaveData + { + PlayerName = playerModel.Name.Value, + Level = playerModel.Level.Value, + Health = playerModel.Health.Value + }; + + await _storageService.SaveAsync("current_save", saveData); + } +} +``` + +### 构造函数注入(推荐) + +```csharp +public class SaveSystem : AbstractSystem +{ + private readonly IStorageService _storageService; + private readonly IAudioService _audioService; + + // 通过构造函数注入依赖 + public SaveSystem(IStorageService storageService, IAudioService audioService) + { + _storageService = storageService; + _audioService = audioService; + } + + protected override void OnInit() + { + this.RegisterEvent<SaveGameEvent>(OnSaveGame); + } + + private async void OnSaveGame(SaveGameEvent e) + { + await _storageService.SaveAsync("save", e.Data); + _audioService.PlaySound("save_success"); + } +} + +// 注册时传入依赖 +public class GameArchitecture : Architecture +{ + protected override void Init() + { + var storageService = new LocalStorageService(); + var audioService = new GodotAudioService(); + + RegisterUtility<IStorageService>(storageService); + RegisterUtility<IAudioService>(audioService); + + // 构造函数注入 + RegisterSystem(new SaveSystem(storageService, audioService)); + } +} +``` + +### 依赖注入优势 + +- ✅ **易于测试**:可以注入模拟对象 +- ✅ **松耦合**:依赖接口而非实现 +- ✅ **灵活配置**:运行时选择实现 +- ✅ **提高可维护性**:依赖关系清晰 + +### 最佳实践 + +1. **依赖接口而非实现**:使用 IStorageService 而非 LocalStorageService +2. **优先使用构造函数注入**:依赖关系更明确 +3. **避免循环依赖**:System 不应相互依赖 +4. **使用 IoC 容器管理生命周期**:让框架管理对象创建 + +## 服务定位器模式 + +### 概念 + +服务定位器模式提供一个全局访问点来获取服务,是依赖注入的替代方案。 + +### 在 GFramework 中的实现 + +```csharp +// GFramework 的 Architecture 本身就是服务定位器 +public class GameplaySystem : AbstractSystem +{ + protected override void OnInit() + { + // 通过服务定位器获取服务 + var playerModel = this.GetModel<PlayerModel>(); + var audioSystem = this.GetSystem<AudioSystem>(); + var storageUtility = this.GetUtility<IStorageUtility>(); + + // 使用服务 + playerModel.Health.Value = 100; + audioSystem.PlayBGM("gameplay"); + } +} + +// 在 Controller 中使用 +public class MenuController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void OnStartButtonClicked() + { + // 通过架构获取服务 + var gameModel = this.GetModel<GameModel>(); + gameModel.GameState.Value = GameState.Playing; + + // 发送命令 + this.SendCommand(new StartGameCommand()); + } +} +``` + +### 自定义服务定位器 + +```csharp +// 创建专门的服务定位器 +public static class ServiceLocator +{ + private static readonly Dictionary<Type, object> _services = new(); + + public static void Register<T>(T service) + { + _services[typeof(T)] = service; + } + + public static T Get<T>() + { + if (_services.TryGetValue(typeof(T), out var service)) + { + return (T)service; + } + throw new InvalidOperationException($"Service {typeof(T)} not registered"); + } + + public static void Clear() + { + _services.Clear(); + } +} + +// 使用自定义服务定位器 +public class GameInitializer +{ + public void Initialize() + { + // 注册服务 + ServiceLocator.Register<IAnalyticsService>(new AnalyticsService()); + ServiceLocator.Register<ILeaderboardService>(new LeaderboardService()); + } +} + +public class GameOverScreen +{ + public void SubmitScore(int score) + { + // 获取服务 + var leaderboard = ServiceLocator.Get<ILeaderboardService>(); + leaderboard.SubmitScore(score); + + var analytics = ServiceLocator.Get<IAnalyticsService>(); + analytics.TrackEvent("game_over", new { score }); + } +} +``` + +### 服务定位器 vs 依赖注入 + +| 特性 | 服务定位器 | 依赖注入 | +|-----------|--------------|------------| +| **依赖可见性** | 隐式(运行时获取) | 显式(构造函数参数) | +| **易用性** | 简单直接 | 需要配置 | +| **测试性** | 较难(需要模拟全局状态) | 容易(注入模拟对象) | +| **编译时检查** | 无 | 有 | +| **适用场景** | 快速原型、小项目 | 大型项目、团队协作 | + +### 最佳实践 + +1. **小项目使用服务定位器**:简单直接 +2. **大项目使用依赖注入**:更易维护 +3. **避免过度使用**:不要把所有东西都放入定位器 +4. **提供清晰的 API**:GetModel、GetSystem、GetUtility + +## 对象池模式 + +### 概念 + +对象池模式通过复用对象来减少内存分配和垃圾回收,提高性能。 + +### 在 GFramework 中的实现 + +```csharp +// 定义可池化对象 +public class Bullet : Node2D, IPoolableNode +{ + public bool IsInPool { get; set; } + + public void OnSpawn() + { + // 从池中取出时调用 + Visible = true; + IsInPool = false; + } + + public void OnRecycle() + { + // 回收到池中时调用 + Visible = false; + IsInPool = true; + Position = Vector2.Zero; + Rotation = 0; + } +} + +// 创建对象池系统 +public class BulletPoolSystem : AbstractNodePoolSystem<Bullet> +{ + protected override Bullet CreateInstance() + { + var bullet = new Bullet(); + // 初始化子弹 + return bullet; + } + + protected override void OnInit() + { + // 预创建对象 + PrewarmPool(50); + } +} + +// 使用对象池 +public class WeaponSystem : AbstractSystem +{ + private BulletPoolSystem _bulletPool; + + protected override void OnInit() + { + _bulletPool = this.GetSystem<BulletPoolSystem>(); + this.RegisterEvent<FireWeaponEvent>(OnFireWeapon); + } + + private void OnFireWeapon(FireWeaponEvent e) + { + // 从池中获取子弹 + var bullet = _bulletPool.Spawn(); + bullet.Position = e.Position; + bullet.Rotation = e.Direction; + + // 3秒后回收 + ScheduleRecycle(bullet, 3.0f); + } + + private async void ScheduleRecycle(Bullet bullet, float delay) + { + await Task.Delay((int)(delay * 1000)); + _bulletPool.Recycle(bullet); + } +} +``` + +### 通用对象池 + +```csharp +// 通用对象池实现 +public class ObjectPool<T> where T : class, new() +{ + private readonly Stack<T> _pool = new(); + private readonly Action<T> _onSpawn; + private readonly Action<T> _onRecycle; + private readonly int _maxSize; + + public ObjectPool(int initialSize = 10, int maxSize = 100, + Action<T> onSpawn = null, Action<T> onRecycle = null) + { + _maxSize = maxSize; + _onSpawn = onSpawn; + _onRecycle = onRecycle; + + // 预创建对象 + for (int i = 0; i < initialSize; i++) + { + _pool.Push(new T()); + } + } + + public T Spawn() + { + T obj = _pool.Count > 0 ? _pool.Pop() : new T(); + _onSpawn?.Invoke(obj); + return obj; + } + + public void Recycle(T obj) + { + if (_pool.Count < _maxSize) + { + _onRecycle?.Invoke(obj); + _pool.Push(obj); + } + } + + public void Clear() + { + _pool.Clear(); + } +} + +// 使用通用对象池 +public class ParticleSystem : AbstractSystem +{ + private ObjectPool<Particle> _particlePool; + + protected override void OnInit() + { + _particlePool = new ObjectPool<Particle>( + initialSize: 100, + maxSize: 500, + onSpawn: p => p.Reset(), + onRecycle: p => p.Clear() + ); + } + + public void EmitParticles(Vector3 position, int count) + { + for (int i = 0; i < count; i++) + { + var particle = _particlePool.Spawn(); + particle.Position = position; + particle.Velocity = GetRandomVelocity(); + } + } +} +``` + +### 对象池优势 + +- ✅ **减少 GC 压力**:复用对象,减少内存分配 +- ✅ **提高性能**:避免频繁创建销毁对象 +- ✅ **稳定帧率**:减少 GC 导致的卡顿 +- ✅ **适合高频对象**:子弹、粒子、特效等 + +### 最佳实践 + +1. **预热池**:提前创建对象避免运行时分配 +2. **设置最大容量**:防止池无限增长 +3. **重置对象状态**:OnRecycle 中清理状态 +4. **监控池使用情况**:记录 Spawn/Recycle 次数 + +## 状态模式 + +### 概念 + +状态模式允许对象在内部状态改变时改变其行为,将状态相关的行为封装到独立的状态类中。 + +### 在 GFramework 中的实现 + +```csharp +// 定义游戏状态 +public class MenuState : ContextAwareStateBase +{ + public override void OnEnter(IState from) + { + Console.WriteLine("进入菜单状态"); + + // 显示菜单 UI + var uiSystem = this.GetSystem<UISystem>(); + uiSystem.ShowMenu(); + + // 播放菜单音乐 + var audioSystem = this.GetSystem<AudioSystem>(); + audioSystem.PlayBGM("menu_theme"); + } + + public override void OnExit(IState to) + { + Console.WriteLine("退出菜单状态"); + + // 隐藏菜单 UI + var uiSystem = this.GetSystem<UISystem>(); + uiSystem.HideMenu(); + } + + public override bool CanTransitionTo(IState target) + { + // 菜单可以转换到游戏或设置状态 + return target is GameplayState or SettingsState; + } +} + +public class GameplayState : ContextAwareStateBase +{ + public override void OnEnter(IState from) + { + Console.WriteLine("进入游戏状态"); + + // 初始化游戏 + var gameModel = this.GetModel<GameModel>(); + gameModel.Reset(); + + // 加载关卡 + this.SendCommand(new LoadLevelCommand { LevelId = 1 }); + + // 播放游戏音乐 + var audioSystem = this.GetSystem<AudioSystem>(); + audioSystem.PlayBGM("gameplay_theme"); + } + + public override void OnExit(IState to) + { + Console.WriteLine("退出游戏状态"); + + // 保存游戏进度 + this.SendCommand(new SaveGameCommand()); + } + + public override bool CanTransitionTo(IState target) + { + // 游戏中可以暂停或结束 + return target is PauseState or GameOverState; + } +} + +public class PauseState : ContextAwareStateBase +{ + public override void OnEnter(IState from) + { + Console.WriteLine("进入暂停状态"); + + // 暂停游戏 + var timeSystem = this.GetSystem<TimeSystem>(); + timeSystem.Pause(); + + // 显示暂停菜单 + var uiSystem = this.GetSystem<UISystem>(); + uiSystem.ShowPauseMenu(); + } + + public override void OnExit(IState to) + { + Console.WriteLine("退出暂停状态"); + + // 恢复游戏 + var timeSystem = this.GetSystem<TimeSystem>(); + timeSystem.Resume(); + + // 隐藏暂停菜单 + var uiSystem = this.GetSystem<UISystem>(); + uiSystem.HidePauseMenu(); + } + + public override bool CanTransitionTo(IState target) + { + // 暂停只能返回游戏或退出到菜单 + return target is GameplayState or MenuState; + } +} + +// 注册状态机 +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 创建状态机系统 + var stateMachine = new StateMachineSystem(); + + // 注册所有状态 + stateMachine + .Register(new MenuState()) + .Register(new GameplayState()) + .Register(new PauseState()) + .Register(new GameOverState()) + .Register(new SettingsState()); + + RegisterSystem<IStateMachineSystem>(stateMachine); + } +} + +// 使用状态机 +public class GameController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public async Task StartGame() + { + var stateMachine = this.GetSystem<IStateMachineSystem>(); + await stateMachine.ChangeToAsync<GameplayState>(); + } + + public async Task PauseGame() + { + var stateMachine = this.GetSystem<IStateMachineSystem>(); + await stateMachine.ChangeToAsync<PauseState>(); + } + + public async Task ResumeGame() + { + var stateMachine = this.GetSystem<IStateMachineSystem>(); + await stateMachine.ChangeToAsync<GameplayState>(); + } +} +``` + +### 异步状态 + +```csharp +public class LoadingState : AsyncContextAwareStateBase +{ + public override async Task OnEnterAsync(IState from) + { + Console.WriteLine("开始加载..."); + + // 显示加载界面 + var uiSystem = this.GetSystem<UISystem>(); + uiSystem.ShowLoadingScreen(); + + // 异步加载资源 + await LoadResourcesAsync(); + + Console.WriteLine("加载完成"); + + // 自动切换到游戏状态 + var stateMachine = this.GetSystem<IStateMachineSystem>(); + await stateMachine.ChangeToAsync<GameplayState>(); + } + + private async Task LoadResourcesAsync() + { + // 模拟异步加载 + await Task.Delay(2000); + } + + public override async Task OnExitAsync(IState to) + { + // 隐藏加载界面 + var uiSystem = this.GetSystem<UISystem>(); + uiSystem.HideLoadingScreen(); + + await Task.CompletedTask; + } +} +``` + +### 状态模式优势 + +- ✅ **清晰的状态管理**:每个状态独立封装 +- ✅ **易于扩展**:添加新状态不影响现有代码 +- ✅ **状态转换验证**:CanTransitionTo 控制合法转换 +- ✅ **支持异步操作**:异步状态处理加载等操作 + +### 最佳实践 + +1. **状态保持单一职责**:每个状态只负责一个场景 +2. **使用转换验证**:防止非法状态转换 +3. **在 OnEnter 初始化,OnExit 清理**:保持状态独立 +4. **异步操作使用异步状态**:避免阻塞主线程 ## 设计原则 @@ -191,27 +1515,27 @@ public class CombatSystem : AbstractSystem // ✅ 好的做法:依赖抽象 public interface IDataStorage { - Task SaveAsync(string key, T data); - Task LoadAsync(string key, T defaultValue = default); + Task SaveAsync<T>(string key, T data); + Task<T> LoadAsync<T>(string key, T defaultValue = default); Task ExistsAsync(string key); } public class FileStorage : IDataStorage { - public async Task SaveAsync(string key, T data) + public async Task SaveAsync<T>(string key, T data) { var json = JsonConvert.SerializeObject(data); await File.WriteAllTextAsync(GetFilePath(key), json); } - public async Task LoadAsync(string key, T defaultValue = default) + public async Task<T> LoadAsync<T>(string key, T defaultValue = default) { var filePath = GetFilePath(key); if (!File.Exists(filePath)) return defaultValue; var json = await File.ReadAllTextAsync(filePath); - return JsonConvert.DeserializeObject(json); + return JsonConvert.DeserializeObject<T>(json); } public async Task ExistsAsync(string key) @@ -227,16 +1551,16 @@ public class FileStorage : IDataStorage public class CloudStorage : IDataStorage { - public async Task SaveAsync(string key, T data) + public async Task SaveAsync<T>(string key, T data) { // 云存储实现 await UploadToCloud(key, data); } - public async Task LoadAsync(string key, T defaultValue = default) + public async Task<T> LoadAsync<T>(string key, T defaultValue = default) { // 云存储实现 - return await DownloadFromCloud(key, defaultValue); + return await DownloadFromCloud<T>(key, defaultValue); } public async Task ExistsAsync(string key) @@ -1421,6 +2745,663 @@ public class PlayerController : AbstractSystem --- +## 模式选择与组合 + +### 何时使用哪种模式? + +#### 小型项目(原型、Demo) + +```csharp +// 推荐组合:MVC + 事件驱动 + 服务定位器 +public class SimpleGameArchitecture : Architecture +{ + protected override void Init() + { + // Model + RegisterModel(new PlayerModel()); + RegisterModel(new GameModel()); + + // System + RegisterSystem(new GameplaySystem()); + RegisterSystem(new AudioSystem()); + + // 使用服务定位器模式 + // Controller 通过 this.GetModel/GetSystem 获取服务 + } +} +``` + +**优势**: + +- 快速开发 +- 代码简洁 +- 易于理解 + +#### 中型项目(独立游戏) + +```csharp +// 推荐组合:MVC + MVVM + 命令/查询 + 事件驱动 + 依赖注入 +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册 Utility(依赖注入) + RegisterUtility<IStorageService>(new LocalStorageService()); + RegisterUtility<IAudioService>(new GodotAudioService()); + + // 注册 Model(MVVM) + RegisterModel(new PlayerModel()); + RegisterModel(new PlayerViewModel()); + + // 注册 System(命令/查询处理) + RegisterSystem(new CombatSystem()); + RegisterSystem(new InventorySystem()); + + // 状态机 + var stateMachine = new StateMachineSystem(); + stateMachine + .Register(new MenuState()) + .Register(new GameplayState()); + RegisterSystem<IStateMachineSystem>(stateMachine); + } +} +``` + +**优势**: + +- 职责清晰 +- 易于测试 +- 支持团队协作 + +#### 大型项目(商业游戏) + +```csharp +// 推荐组合:所有模式 + 模块化架构 +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 模块化安装 + InstallModule(new CoreModule()); + InstallModule(new AudioModule()); + InstallModule(new NetworkModule()); + InstallModule(new UIModule()); + InstallModule(new GameplayModule()); + } +} + +// 核心模块 +public class CoreModule : IArchitectureModule +{ + public void Install(IArchitecture architecture) + { + // 依赖注入 + architecture.RegisterUtility<IStorageService>(new CloudStorageService()); + architecture.RegisterUtility<IAnalyticsService>(new AnalyticsService()); + + // 对象池 + architecture.RegisterSystem(new BulletPoolSystem()); + architecture.RegisterSystem(new ParticlePoolSystem()); + } +} + +// 游戏玩法模块 +public class GameplayModule : IArchitectureModule +{ + public void Install(IArchitecture architecture) + { + // Model + architecture.RegisterModel(new PlayerModel()); + architecture.RegisterModel(new EnemyModel()); + + // System(使用命令/查询模式) + architecture.RegisterSystem(new CombatSystem()); + architecture.RegisterSystem(new MovementSystem()); + + // 状态机 + var stateMachine = new StateMachineSystem(); + stateMachine + .Register(new MenuState()) + .Register(new LoadingState()) + .Register(new GameplayState()) + .Register(new PauseState()) + .Register(new GameOverState()); + architecture.RegisterSystem<IStateMachineSystem>(stateMachine); + } +} +``` + +**优势**: + +- 高度模块化 +- 易于维护和扩展 +- 支持大型团队 +- 完善的测试覆盖 + +### 模式组合示例 + +#### 组合 1:MVVM + 命令模式 + +```csharp +// ViewModel +public class ShopViewModel : AbstractModel +{ + public BindableProperty<int> PlayerGold { get; } = new(1000); + public BindableProperty<bool> CanBuy { get; } = new(true); + public BindableProperty<string> StatusMessage { get; } = new(""); + + protected override void OnInit() + { + // 监听购买事件 + this.RegisterEvent<ItemPurchasedEvent>(OnItemPurchased); + this.RegisterEvent<InsufficientGoldEvent>(OnInsufficientGold); + + // 监听金币变化 + PlayerGold.Register(gold => + { + CanBuy.Value = gold >= 100; + }); + } + + private void OnItemPurchased(ItemPurchasedEvent e) + { + PlayerGold.Value -= e.Cost; + StatusMessage.Value = $"购买成功:{e.ItemName}"; + } + + private void OnInsufficientGold(InsufficientGoldEvent e) + { + StatusMessage.Value = "金币不足!"; + } +} + +// View +public class ShopView : Control +{ + private ShopViewModel _viewModel; + + public override void _Ready() + { + _viewModel = GetModel<ShopViewModel>(); + + // 数据绑定 + _viewModel.PlayerGold.Register(gold => + { + GetNode<Label>("GoldLabel").Text = $"金币:{gold}"; + }); + + _viewModel.CanBuy.Register(canBuy => + { + GetNode<Button>("BuyButton").Disabled = !canBuy; + }); + + _viewModel.StatusMessage.Register(msg => + { + GetNode<Label>("StatusLabel").Text = msg; + }); + } + + private void OnBuyButtonPressed() + { + // 发送命令 + this.SendCommand(new BuyItemCommand + { + Input = new BuyItemInput { ItemId = "sword_01" } + }); + } +} +``` + +#### 组合 2:状态模式 + 对象池 + +```csharp +// 游戏状态使用对象池 +public class GameplayState : ContextAwareStateBase +{ + private BulletPoolSystem _bulletPool; + private ParticlePoolSystem _particlePool; + + public override void OnEnter(IState from) + { + // 获取对象池 + _bulletPool = this.GetSystem<BulletPoolSystem>(); + _particlePool = this.GetSystem<ParticlePoolSystem>(); + + // 预热对象池 + _bulletPool.PrewarmPool(100); + _particlePool.PrewarmPool(200); + + // 注册事件 + this.RegisterEvent<FireWeaponEvent>(OnFireWeapon); + } + + private void OnFireWeapon(FireWeaponEvent e) + { + // 从池中获取子弹 + var bullet = _bulletPool.Spawn(); + bullet.Position = e.Position; + bullet.Direction = e.Direction; + + // 生成粒子效果 + var particle = _particlePool.Spawn(); + particle.Position = e.Position; + } + + public override void OnExit(IState to) + { + // 回收所有对象 + _bulletPool.RecycleAll(); + _particlePool.RecycleAll(); + } +} +``` + +#### 组合 3:事件驱动 + 查询模式 + +```csharp +// 成就系统:监听事件,使用查询验证条件 +public class AchievementSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent<EnemyKilledEvent>(OnEnemyKilled); + this.RegisterEvent<LevelCompletedEvent>(OnLevelCompleted); + } + + private void OnEnemyKilled(EnemyKilledEvent e) + { + // 查询击杀总数 + var query = new GetTotalKillsQuery { Input = new EmptyQueryInput() }; + var totalKills = this.SendQuery(query); + + // 检查成就 + if (totalKills == 100) + { + UnlockAchievement("kill_100_enemies"); + } + } + + private void OnLevelCompleted(LevelCompletedEvent e) + { + // 查询是否满足完美通关条件 + var query = new IsPerfectClearQuery + { + Input = new IsPerfectClearInput { LevelId = e.LevelId } + }; + var isPerfect = this.SendQuery(query); + + if (isPerfect) + { + UnlockAchievement($"perfect_clear_level_{e.LevelId}"); + } + } +} +``` + +### 模式选择决策树 + +``` +需要管理游戏状态? +├─ 是 → 使用状态模式 +└─ 否 → 继续 + +需要频繁创建/销毁对象? +├─ 是 → 使用对象池模式 +└─ 否 → 继续 + +需要解耦组件通信? +├─ 是 → 使用事件驱动模式 +└─ 否 → 继续 + +需要封装操作? +├─ 是 → 使用命令模式 +└─ 否 → 继续 + +需要分离读写操作? +├─ 是 → 使用查询模式(CQRS) +└─ 否 → 继续 + +需要数据绑定和响应式 UI? +├─ 是 → 使用 MVVM 模式 +└─ 否 → 使用 MVC 模式 + +需要管理依赖? +├─ 大型项目 → 使用依赖注入 +└─ 小型项目 → 使用服务定位器 +``` + +## 常见问题 + +### Q1: 应该使用 MVC 还是 MVVM? + +**A**: 取决于项目需求: + +- **使用 MVC**: + - 简单的 UI 更新 + - 不需要复杂的数据绑定 + - 快速原型开发 + +- **使用 MVVM**: + - 复杂的数据驱动 UI + - 需要自动更新界面 + - 大量计算属性 + +**推荐**:可以混合使用,简单界面用 MVC,复杂界面用 MVVM。 + +### Q2: 命令模式和查询模式有什么区别? + +**A**: + +| 特性 | 命令模式 | 查询模式 | +|---------|----------------|---------------------| +| **目的** | 修改状态 | 读取数据 | +| **返回值** | 可选 | 必须有 | +| **副作用** | 有 | 无 | +| **示例** | BuyItemCommand | GetPlayerStatsQuery | + +**原则**:命令改变状态,查询读取状态,两者不混用。 + +### Q3: 何时使用事件,何时使用命令? + +**A**: + +- **使用事件**: + - 通知状态变化 + - 一对多通信 + - 跨模块通信 + - 不关心处理结果 + +- **使用命令**: + - 执行具体操作 + - 需要封装逻辑 + - 需要撤销/重做 + - 需要返回结果 + +**示例**: + +```csharp +// 使用命令执行操作 +this.SendCommand(new BuyItemCommand()); + +// 使用事件通知结果 +this.SendEvent(new ItemPurchasedEvent()); +``` + +### Q4: 依赖注入和服务定位器哪个更好? + +**A**: + +- **依赖注入**: + - ✅ 依赖关系明确 + - ✅ 易于测试 + - ✅ 编译时检查 + - ❌ 配置复杂 + +- **服务定位器**: + - ✅ 简单直接 + - ✅ 易于使用 + - ❌ 依赖隐式 + - ❌ 难以测试 + +**推荐**: + +- 小项目:服务定位器 +- 大项目:依赖注入 +- 混合使用:核心服务用依赖注入,辅助服务用服务定位器 + +### Q5: 对象池适合哪些场景? + +**A**: + +**适合**: + +- 频繁创建/销毁的对象(子弹、粒子) +- 创建成本高的对象(网络连接) +- 需要稳定帧率的场景 + +**不适合**: + +- 创建频率低的对象 +- 对象状态复杂难以重置 +- 内存受限的场景 + +**示例**: + +```csharp +// ✅ 适合使用对象池 +- 子弹、导弹 +- 粒子效果 +- UI 元素(列表项) +- 音效播放器 + +// ❌ 不适合使用对象池 +- 玩家角色 +- 关卡数据 +- 配置对象 +``` + +### Q6: 状态机和简单的 if-else 有什么区别? + +**A**: + +**简单 if-else**: + +```csharp +// ❌ 难以维护 +public void Update() +{ + if (gameState == GameState.Menu) + { + UpdateMenu(); + } + else if (gameState == GameState.Playing) + { + UpdateGameplay(); + } + else if (gameState == GameState.Paused) + { + UpdatePause(); + } + // 状态逻辑分散,难以管理 +} +``` + +**状态机**: + +```csharp +// ✅ 清晰易维护 +public class MenuState : ContextAwareStateBase +{ + public override void OnEnter(IState from) + { + // 进入菜单的所有逻辑集中在这里 + } + + public override void OnExit(IState to) + { + // 退出菜单的所有逻辑集中在这里 + } +} +``` + +**优势**: + +- 状态逻辑封装 +- 易于添加新状态 +- 支持状态转换验证 +- 支持状态历史 + +### Q7: 如何避免过度设计? + +**A**: + +**原则**: + +1. **从简单开始**:先用最简单的方案 +2. **按需重构**:遇到问题再优化 +3. **YAGNI 原则**:You Aren't Gonna Need It + +**示例**: + +```csharp +// 第一版:简单直接 +public class Player +{ + public int Health = 100; +} + +// 第二版:需要通知时添加事件 +public class Player +{ + private int _health = 100; + public int Health + { + get => _health; + set + { + _health = value; + OnHealthChanged?.Invoke(value); + } + } + public event Action<int> OnHealthChanged; +} + +// 第三版:需要更多功能时使用 BindableProperty +public class PlayerModel : AbstractModel +{ + public BindableProperty<int> Health { get; } = new(100); +} +``` + +### Q8: 如何在现有项目中引入这些模式? + +**A**: + +**渐进式重构**: + +1. **第一步:引入事件系统** + ```csharp + // 替换直接调用为事件 + // 之前:uiManager.UpdateHealth(health); + // 之后:SendEvent(new HealthChangedEvent { Health = health }); + ``` + +2. **第二步:提取 Model** + ```csharp + // 将数据从各处集中到 Model + public class PlayerModel : AbstractModel + { + public BindableProperty<int> Health { get; } = new(100); + } + ``` + +3. **第三步:引入命令模式** + ```csharp + // 封装操作为命令 + public class HealPlayerCommand : AbstractCommand + { + protected override void OnExecute() + { + var player = this.GetModel<PlayerModel>(); + player.Health.Value = player.MaxHealth.Value; + } + } + ``` + +4. **第四步:添加查询模式** + ```csharp + // 分离读操作 + public class GetPlayerStatsQuery : AbstractQuery<PlayerStats> + { + protected override PlayerStats OnDo() + { + // 查询逻辑 + } + } + ``` + +### Q9: 性能会受到影响吗? + +**A**: + +**影响很小**: + +- 事件系统:微秒级开销 +- 命令/查询:几乎无开销 +- IoC 容器:字典查找,O(1) + +**优化建议**: + +1. **避免频繁事件**:不要每帧发送事件 +2. **缓存查询结果**:复杂查询结果可以缓存 +3. **使用对象池**:减少 GC 压力 +4. **批量操作**:合并多个小操作 + +**性能对比**: + +```csharp +// 直接调用:~1ns +player.Health = 100; + +// 通过命令:~100ns +SendCommand(new SetHealthCommand { Health = 100 }); + +// 差异可以忽略不计,但带来了更好的架构 +``` + +### Q10: 如何测试使用这些模式的代码? + +**A**: + +**单元测试示例**: + +```csharp +[Test] +public void BuyItemCommand_InsufficientGold_ShouldNotBuyItem() +{ + // Arrange + var architecture = new TestArchitecture(); + var playerModel = new PlayerModel(); + playerModel.Gold.Value = 50; // 金币不足 + architecture.RegisterModel(playerModel); + + var command = new BuyItemCommand + { + Input = new BuyItemInput { ItemId = "sword", Price = 100 } + }; + command.SetArchitecture(architecture); + + // Act + command.Execute(); + + // Assert + Assert.AreEqual(50, playerModel.Gold.Value); // 金币未变化 +} + +[Test] +public void GetPlayerStatsQuery_ShouldReturnCorrectStats() +{ + // Arrange + var architecture = new TestArchitecture(); + var playerModel = new PlayerModel(); + playerModel.Level.Value = 10; + playerModel.Health.Value = 80; + architecture.RegisterModel(playerModel); + + var query = new GetPlayerStatsQuery(); + query.SetArchitecture(architecture); + + // Act + var stats = query.Do(); + + // Assert + Assert.AreEqual(10, stats.Level); + Assert.AreEqual(80, stats.Health); +} +``` + +--- + ## 总结 遵循这些架构模式最佳实践,你将能够构建: @@ -1431,9 +3412,34 @@ public class PlayerController : AbstractSystem - ✅ **健壮的错误处理** - 提高系统稳定性 - ✅ **完善的测试覆盖** - 保证代码质量 -记住,好的架构不是一蹴而就的,需要持续的重构和改进。 +### 关键要点 + +1. **从简单开始**:不要过度设计,按需添加模式 +2. **理解每个模式的适用场景**:选择合适的模式解决问题 +3. **模式可以组合使用**:发挥各自优势 +4. **持续重构**:随着项目发展优化架构 +5. **注重可测试性**:好的架构应该易于测试 + +### 推荐学习路径 + +1. **入门**:MVC + 事件驱动 +2. **进阶**:命令模式 + 查询模式 + MVVM +3. **高级**:状态模式 + 对象池 + 依赖注入 +4. **专家**:模块化架构 + 所有模式组合 + +### 相关资源 + +- [架构核心文档](/zh-CN/core/architecture) +- [命令模式文档](/zh-CN/core/command) +- [查询模式文档](/zh-CN/core/query) +- [事件系统文档](/zh-CN/core/events) +- [状态机文档](/zh-CN/core/state-machine) +- [IoC 容器文档](/zh-CN/core/ioc) + +记住,好的架构不是一蹴而就的,需要持续的学习、实践和改进。 --- -**文档版本**: 1.0.0 -**更新日期**: 2026-01-12 \ No newline at end of file +**文档版本**: 2.0.0 +**最后更新**: 2026-03-07 +**作者**: GFramework Team \ No newline at end of file diff --git a/docs/zh-CN/best-practices/error-handling.md b/docs/zh-CN/best-practices/error-handling.md new file mode 100644 index 0000000..b27bac0 --- /dev/null +++ b/docs/zh-CN/best-practices/error-handling.md @@ -0,0 +1,1303 @@ +# 错误处理最佳实践 + +> 本指南介绍 GFramework 中的错误处理模式和最佳实践,帮助你构建健壮、可维护的游戏应用。 + +## 📋 目录 + +- [概述](#概述) +- [核心概念](#核心概念) +- [Result 模式](#result-模式) +- [Option 模式](#option-模式) +- [异常处理](#异常处理) +- [日志记录](#日志记录) +- [用户反馈](#用户反馈) +- [错误恢复](#错误恢复) +- [最佳实践](#最佳实践) +- [常见问题](#常见问题) + +## 概述 + +### 为什么错误处理很重要 + +良好的错误处理是构建健壮应用的基础: + +- **提高稳定性**:优雅地处理异常情况,避免崩溃 +- **改善用户体验**:提供清晰的错误提示和恢复选项 +- **简化调试**:记录详细的错误信息,快速定位问题 +- **增强可维护性**:使用类型安全的错误处理模式 + +### GFramework 的错误处理策略 + +GFramework 提供多种错误处理机制: + +1. **Result<T> 模式**:函数式错误处理,类型安全 +2. **Option<T> 模式**:处理可空值,避免 null 引用 +3. **异常处理**:处理不可恢复的错误 +4. **日志系统**:记录错误信息和调试数据 + +## 核心概念 + +### 错误类型 + +根据错误的性质和处理方式,可以分为以下几类: + +**1. 可预期的错误** + +这类错误是业务逻辑的一部分,应该使用 Result 或 Option 模式处理: + +```csharp +// 示例:用户输入验证 +public Result<PlayerData> ValidatePlayerName(string name) +{ + if (string.IsNullOrWhiteSpace(name)) + return Result<PlayerData>.Failure("玩家名称不能为空"); + + if (name.Length < 3) + return Result<PlayerData>.Failure("玩家名称至少需要 3 个字符"); + + if (name.Length > 20) + return Result<PlayerData>.Failure("玩家名称不能超过 20 个字符"); + + return Result<PlayerData>.Success(new PlayerData { Name = name }); +} +``` + +**2. 不可预期的错误** + +这类错误通常是程序错误或系统故障,应该使用异常处理: + +```csharp +// 示例:文件系统错误 +public async Task<SaveData> LoadSaveFileAsync(string path) +{ + try + { + var json = await File.ReadAllTextAsync(path); + return JsonSerializer.Deserialize<SaveData>(json); + } + catch (FileNotFoundException ex) + { + _logger.Error($"存档文件不存在: {path}", ex); + throw new SaveSystemException("无法加载存档文件", ex); + } + catch (JsonException ex) + { + _logger.Error($"存档文件格式错误: {path}", ex); + throw new SaveSystemException("存档文件已损坏", ex); + } +} +``` + +**3. 可空值** + +使用 Option 模式处理可能不存在的值: + +```csharp +// 示例:查找玩家 +public Option<Player> FindPlayerById(string playerId) +{ + var player = _players.FirstOrDefault(p => p.Id == playerId); + return player != null ? Option<Player>.Some(player) : Option<Player>.None; +} +``` + +### 错误传播 + +错误应该在合适的层级处理,不要过早捕获: + +```csharp +// ✅ 好的做法:让错误向上传播 +public class InventorySystem : AbstractSystem +{ + public Result<Item> AddItem(string itemId, int quantity) + { + // 验证参数 + var validationResult = ValidateAddItem(itemId, quantity); + if (validationResult.IsFaulted) + return validationResult; + + // 检查空间 + var spaceResult = CheckInventorySpace(quantity); + if (spaceResult.IsFaulted) + return Result<Item>.Failure(spaceResult.Exception); + + // 添加物品 + var item = CreateItem(itemId, quantity); + _items.Add(item); + + return Result<Item>.Success(item); + } +} + +// ❌ 避免:过早捕获错误 +public class InventorySystem : AbstractSystem +{ + public Result<Item> AddItem(string itemId, int quantity) + { + try + { + // 所有操作都在 try 块中 + ValidateAddItem(itemId, quantity); + CheckInventorySpace(quantity); + var item = CreateItem(itemId, quantity); + _items.Add(item); + return Result<Item>.Success(item); + } + catch (Exception ex) + { + // 捕获所有异常,丢失了错误的具体信息 + return Result<Item>.Failure("添加物品失败"); + } + } +} +``` + +### 错误恢复 + +设计错误恢复策略,提供降级方案: + +```csharp +// 示例:多级降级策略 +public async Task<GameConfig> LoadConfigAsync() +{ + // 1. 尝试从云端加载 + var cloudResult = await TryLoadFromCloudAsync(); + if (cloudResult.IsSuccess) + { + _logger.Info("从云端加载配置成功"); + return cloudResult.Match(succ: c => c, fail: _ => null); + } + + // 2. 尝试从本地缓存加载 + var cacheResult = await TryLoadFromCacheAsync(); + if (cacheResult.IsSuccess) + { + _logger.Warn("云端加载失败,使用本地缓存"); + return cacheResult.Match(succ: c => c, fail: _ => null); + } + + // 3. 使用默认配置 + _logger.Error("所有配置源加载失败,使用默认配置"); + return GetDefaultConfig(); +} +``` + +## Result 模式 + +### 基本用法 + +Result<T> 是一个函数式类型,表示操作可能成功(包含值)或失败(包含异常): + +```csharp +// 创建成功结果 +var successResult = Result<int>.Success(42); + +// 创建失败结果 +var failureResult = Result<int>.Failure(new Exception("操作失败")); +var failureResult2 = Result<int>.Failure("操作失败"); // 使用字符串 + +// 检查状态 +if (result.IsSuccess) +{ + var value = result.Match(succ: v => v, fail: _ => 0); +} + +if (result.IsFaulted) +{ + var error = result.Exception; +} +``` + +### 链式操作 + +Result 支持函数式编程的链式操作: + +```csharp +// Map:转换成功值 +var result = Result<int>.Success(42) + .Map(x => x * 2) // Result<int>(84) + .Map(x => x.ToString()); // Result<string>("84") + +// Bind:链式转换,可能失败 +var result = Result<string>.Success("42") + .Bind(s => int.TryParse(s, out var i) + ? Result<int>.Success(i) + : Result<int>.Failure("解析失败")) + .Map(i => i * 2); + +// Ensure:验证条件 +var result = Result<int>.Success(42) + .Ensure(x => x > 0, "值必须为正数") + .Ensure(x => x < 100, "值必须小于 100"); + +// OnSuccess/OnFailure:执行副作用 +result + .OnSuccess(value => _logger.Info($"成功: {value}")) + .OnFailure(ex => _logger.Error($"失败: {ex.Message}")); +``` + +### 实际应用示例 + +**示例 1:用户注册** + +```csharp +public class UserRegistrationSystem : AbstractSystem +{ + public Result<User> RegisterUser(string username, string email, string password) + { + return ValidateUsername(username) + .Bind(_ => ValidateEmail(email)) + .Bind(_ => ValidatePassword(password)) + .Bind(_ => CheckUsernameAvailability(username)) + .Bind(_ => CheckEmailAvailability(email)) + .Map(_ => CreateUser(username, email, password)) + .OnSuccess(user => { + _logger.Info($"用户注册成功: {username}"); + this.SendEvent(new UserRegisteredEvent { User = user }); + }) + .OnFailure(ex => _logger.Warn($"用户注册失败: {ex.Message}")); + } + + private Result<string> ValidateUsername(string username) + { + if (string.IsNullOrWhiteSpace(username)) + return Result<string>.Failure("用户名不能为空"); + + if (username.Length < 3 || username.Length > 20) + return Result<string>.Failure("用户名长度必须在 3-20 个字符之间"); + + if (!Regex.IsMatch(username, @"^[a-zA-Z0-9_]+$")) + return Result<string>.Failure("用户名只能包含字母、数字和下划线"); + + return Result<string>.Success(username); + } + + private Result<string> ValidateEmail(string email) + { + if (string.IsNullOrWhiteSpace(email)) + return Result<string>.Failure("邮箱不能为空"); + + if (!Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$")) + return Result<string>.Failure("邮箱格式不正确"); + + return Result<string>.Success(email); + } + + private Result<string> ValidatePassword(string password) + { + if (string.IsNullOrWhiteSpace(password)) + return Result<string>.Failure("密码不能为空"); + + if (password.Length < 8) + return Result<string>.Failure("密码长度至少为 8 个字符"); + + if (!passwordchar.IsUpper)) + return Result<string>.Failure("密码必须包含至少一个大写字母"); + + if (!password.Any(char.IsDigit)) + return Result<string>.Failure("密码必须包含至少一个数字"); + + return Result<string>.Success(password); + } + + private Result<bool> CheckUsernameAvailability(string username) + { + var exists = _userRepository.ExistsByUsername(username); + return exists + ? Result<bool>.Failure("用户名已被使用") + : Result<bool>.Success(true); + } + + private Result<bool> CheckEmailAvailability(string email) + { + var exists = _userRepository.ExistsByEmail(email); + return exists + ? Result<bool>.Failure("邮箱已被注册") + : Result<bool>.Success(true); + } + + private User CreateUser(string username, string email, string password) + { + var user = new User + { + Id = Guid.NewGuid().ToString(), + Username = username, + Email = email, + PasswordHash = HashPassword(password), + CreatedAt = DateTime.UtcNow + }; + + _userRepository.Add(user); + return user; + } +} +``` + +**示例 2:物品交易** + +```csharp +public class TradingSystem : AbstractSystem +{ + public Result<Trade> ExecuteTrade(string sellerId, string buyerId, string itemId, int price) + { + return ValidateTradeParticipants(sellerId, buyerId) + .Bind(_ => ValidateItem(sellerId, itemId)) + .Bind(_ => ValidateBuyerFunds(buyerId, price)) + .Bind(_ => TransferItem(sellerId, buyerId, itemId)) + .Bind(_ =&gerCurrency(buyerId, sellerId, price)) + .Map(_ => CreateTradeRecord(sellerId, buyerId, itemId, price)) + .OnSuccess(trade => { + _logger.Info($"交易完成: {trade.Id}"); + this.SendEvent(new TradeCompletedEvent { Trade = trade }); + }) + .OnFailure(ex => { + _logger.Error($"交易失败: {ex.Message}"); + RollbackTrade(sellerId, buyerId, itemId, price); + }); + } + + private Result<bool> ValidateTradeParticipants(string sellerId, string buyerId) + { + if (sellerId == buyerId) + return Result<bool>.Failure("不能与自己交易"); + + var seller = _playerRepository.GetById(sellerId); + if (seller == null) + return Result<bool>.Failure("卖家不存在"); + + var buyer = _playerRepository.GetById(buyerId); + if (buyer == null) + return Result<bool>.Failure("买家不存在"); + + return Result<bool>.Success(true); + } + + private Result<bool> ValidateItem(string sellerId, string itemId) + { + var inventory = _inventoryRepository.GetByPlayerId(sellerId); + var item = inventory.Items.FirstOrDefault(i => i.Id == itemId); + + if (item == null) + return Result<bool>.Failure("物品不存在"); + + if (!item.IsTradeable) + return Result<bool>.Failure("该物品不可交易"); + + return Result<bool>.Success(true); + } + + private Result<bool> ValidateBuyerFunds(string buyerId, int price) + { + var buyer = _playerRepository.GetById(buyerId); + if (buyer.Currency < price) + return Result<bool>.Failure($"金币不足,需要 {price},当前 {buyer.Currency}"); + + return Result<bool>.Success(true); + } +} +``` + +### 异步操作 + +Result 支持异步操作: + +```csharp +// 异步 Map +var result = await Result<int>.Success(42) + .MapAsync(async x => await GetUserDataAsync(x)); + +// 异步 Bind +var result = await Result<string>.Success("user123") + .BindAsync(async userId => await LoadUserAsync(userId)); + +// TryAsync:安全执行异步操作 +var result = await ResultExtensions.TryAsync(async () => +{ + var data = await LoadDataAsync(); + return ProcessData(data); +}); +``` + +### 组合多个 Result + +```csharp +// 组合多个结果,全部成功才返回成功 +var results = new[] +{ + ValidateUsername("player1"), + ValidateEmail("player1@example.com"), + ValidatePassword("Password123") +}; + +var combinedResult = results.Combine(); // Result<List<string>> + +if (combinedResult.IsSuccess) +{ + _logger.Info("所有验证通过"); +} +else +{ + _logger.Error($"验证失败: {combinedResult.Exception.Message}"); +} +``` + +## Option 模式 + +### 基本用法 + +Option<T> 用于表示可能存在或不存在的值,避免 null 引用异常: + +```csharp +// 创建 Some(有值) +var someOption = Option<int>.Some(42); + +// 创建 None(无值) +var noneOption = Option<int>.None; + +// 检查状态 +if (option.IsSome) +{ + var value = option.GetOrElse(0); +} + +if (option.IsNone) +{ + _logger.Warn("值不存在"); +} +``` + +### 常用操作 + +```csharp +// GetOrElse:获取值或返回默认值 +var value = option.GetOrElse(0); +var value2 = option.GetOrElse(() => GetDefaultValue()); + +// Map:转换值 +var mapped = option.Map(x => x * 2); + +// Bind:链式转换 +var bound = option.Bind(x => x > 0 + ? Option<string>.Some(x.ToString()) + : Option<string>.None); + +// Filter:过滤值 +var filtered = option.Filter(x => x > 0); + +// Match:模式匹配 +var result = option.Match( + some: value => $"值: {value}", + none: () => "无值" +); +``` + +### 实际应用示例 + +**示例 1:查找玩家** + +```csharp +public class PlayerSystem : AbstractSystem +{ + public Option<Player> FindPlayerById(string playerId) + { + var player = _players.FirstOrDefault(p => p.Id == playerId); + return player != null ? Option<Player>.Some(player) : Option<Player>.None; + } + + public Option<Player> FindPlayerByName(string playerName) + { + var player = _players.FirstOrDefault(p => p.Name == playerName); + return player != null ? Option<Player>.Some(player) : Option<Player>.None; + } + + public void UpdatePlayerScore(string playerId, int score) + { + FindPlayerById(playerId).Match( + some: player => { + player.Score += score; + _logger.Info($"玩家 {player.Name} 得分更新: {player.Score}"); + this.SendEvent(new PlayerScoreUpdatedEvent { Player = player }); + }, + none: () => _logger.Warn($"玩家不存在: {playerId}") + ); + } +} +``` + +**示例 2:配置管理** + +```csharp +public class ConfigurationSystem : AbstractSystem +{ + private readonly Dictionary<string, string> _config = new(); + + public Option<string> GetConfig(string key) + { + return _config.TryGetValue(key, out var value) + ? Option<string>.Some(value) + : Option<string>.None; + } + + public Option<int> GetIntConfig(string key) + { + return GetConfig(key) + .Bind(value => int.TryParse(value, out var result) + ? Option<int>.Some(result) + : Option<int>.None); + } + + public Option<bool> GetBoolConfig(string key) + { + return GetConfig(key) + .Bind(value => bool.TryParse(value, out var result) + ? Option<bool>.Some(result) + : Option<bool>.None); + } + + public void ApplyGraphicsSettings() + { + var width = GetIntConfig("screen_width").GetOrElse(1920); + var height = GetIntConfig("screen_height").GetOrElse(1080); + var fullscreen = GetBoolConfig("fullscreen").GetOrElse(false); + var vsync = GetBoolConfig("vsync").GetOrElse(true); + + ApplyScreenSettings(width, height, fullscreen, vsync); + } +} +``` + +**示例 3:物品查找** + +```csharp +public class InventorySystem : AbstractSystem +{ + public Option<Item> FindItemById(string itemId) + { + var item = _items.FirstOrDefault(i => i.Id == itemId); + return item != null ? Option<Item>.Some(item) : Option<Item>.None; + } + + public Option<Item> FindItemByType(ItemType type) + { + var item = _items.FirstOrDefault(i => i.Type == type); + return item != null ? Option<Item>.Some(item) : Option<Item>.None; + } + + public Result<Item> UseItem(string itemId) + { + return FindItemById(itemId) + .ToResult("物品不存在") + .Bind(item => item.IsUsable + ? Result<Item>.Success(item) + : Result<Item>.Failure("物品不可使用")) + .OnSuccess(item => { + item.Use(); + _logger.Info($"使用物品: {item.Name}"); + this.SendEvent(new ItemUsedEvent { Item = item }); + }); + } +} +``` + +### Option 与 Result 的转换 + +```csharp +// Option 转 Result +var option = Option<int>.Some(42); +var result = option.ToResult("值不存在"); // Result<int> + +// Result 转 Option(需要自定义扩展) +var result = Result<int>.Success(42); +var option = result.IsSuccess + ? Option<int>.Some(result.Match(succ: v => v, fail: _ => 0)) + : Option<int>.None; +``` + +## 异常处理 + +### 何时使用异常 + +异常应该用于处理**不可预期的错误**和**不可恢复的错误**: + +```csharp +// ✅ 适合使用异常的场景 +public class SaveSystem : AbstractSystem +{ + public async Task SaveGameAsync(SaveData data) + { + try + { + var json = JsonSerializer.Serialize(data); + await File.WriteAllTextAsync(_savePath, json); + } + catch (UnauthorizedAccessException ex) + { + _logger.Fatal("没有写入权限", ex); + throw new SaveSystemException("无法保存游戏:权限不足", ex); + } + catch (IOException ex) + { + _logger.Error("IO 错误", ex); + throw new SaveSystemException("无法保存游戏:磁盘错误", ex); + } + } +} + +// ❌ 不适合使用异常的场景 +public class PlayerSystem : AbstractSystem +{ + // 不要用异常处理业务逻辑 + public void UpdatePlayerHealth(string playerId, int damage) + { + var player = FindPlayerById(playerId); + if (player == null) + throw new PlayerNotFoundException(playerId); // ❌ 应该用 Option 或 Result + + player.Health -= damage; + } +} +``` + +### 自定义异常 + +创建有意义的异常类型: + +```csharp +// 基础游戏异常 +public class GameException : Exception +{ + public string ErrorCode { get; } + public Dictionary<string, object> Context { get; } + + public GameException( + string message, + string errorCode, + Dictionary<string, object> context = null, + Exception innerException = null) + : base(message, innerException) + { + ErrorCode = errorCode; + Context = context ?? new Dictionary<string, object>(); + } +} + +// 存档系统异常 +public class SaveSystemException : GameException +{ + public SaveSystemException( + string message, + Exception innerException = null) + : base(message, "SAVE_ERROR", null, innerException) + { + } +} + +// 网络异常 +public class NetworkException : GameException +{ + public int StatusCode { get; } + + public NetworkException( + string message, + int statusCode, + Exception innerException = null) + : base( + message, + "NETWORK_ERROR", + new Dictionary<string, object> { ["statusCode"] = statusCode }, + innerException) + { + StatusCode = statusCode; + } +} + +// 使用示例 +public class NetworkSystem : AbstractSystem +{ + public async Task<PlayerData> FetchPlayerDataAsync(string playerId) + { + try + { + var response = await _httpClient.GetAsync($"/api/players/{playerId}"); + + if (!response.IsSuccessStatusCode) + { + throw new NetworkException( + $"获取玩家数据失败: {playerId}", + (int)response.StatusCode + ); + } + + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize<PlayerData>(json); + } + catch (HttpRequestException ex) + { + _logger.Error($"网络请求失败: {playerId}", ex); + throw new NetworkException("网络连接失败", 0, ex); + } + } +} +``` + +### 异常处理最佳实践 + +**1. 捕获具体的异常类型** + +```csharp +// ✅ 好的做法 +try +{ + var data = await LoadDataAsync(); +} +catch (FileNotFoundException ex) +{ + _logger.Warn("文件不存在", ex); + return GetDefaultData(); +} +catch (JsonException ex) +{ + _logger.Error("JSON 解析失败", ex); + throw new DataException("数据格式错误", ex); +} +catch (IOException ex) +{ + _logger.Error("IO 错误", ex); + throw new DataException("读取数据失败", ex); +} + +// ❌ 避免:捕获所有异常 +try +{ + var data = await LoadDataAsync(); +} +catch (Exception ex) // 太宽泛 +{ + _logger.Error("加载失败", ex); + return null; +} +``` + +**2. 不要吞掉异常** + +```csharp +// ❌ 避免:吞掉异常 +try +{ + RiskyOperation(); +} +catch (Exception ex) +{ + // 什么都不做,异常被吞掉了 +} + +// ✅ 好的做法:记录并重新抛出或处理 +try +{ + RiskyOperation(); +} +catch (Exception ex) +{ + _logger.Error("操作失败", ex); + throw; // 重新抛出原始异常 +} +``` + +**3. 使用 finally 清理资源** + +```csharp +// ✅ 好的做法 +FileStream stream = null; +try +{ + stream = File.OpenRead(path); + // 处理文件 +} +catch (IOException ex) +{ + _logger.Error("文件读取失败", ex); + throw; +} +finally +{ + stream?.Dispose(); // 确保资源被释放 +} + +// 更好的做法:使用 using 语句 +using (var stream = File.OpenRead(path)) +{ + // 处理文件 +} // 自动释放资源 +``` + +## 日志记录 + +### 日志级别 + +GFramework 提供多个日志级别,根据错误严重程度选择合适的级别: + +```csharp +// Trace:详细的调试信息 +_logger.Trace("进入 ProcessInput 方法"); + +// Debug:调试信息 +_logger.Debug($"玩家位置: {player.Position}"); + +// Info:一般信息 +_logger.Info("游戏启动成功"); + +// Warning:警告信息 +_logger.Warn("配置文件不存在,使用默认配置"); + +// Error:错误信息 +_logger.Error("保存游戏失败", exception); + +// Fatal:致命错误 +_logger.Fatal("无法初始化渲染系统", exception); +``` + +### 结构化日志 + +使用结构化日志记录上下文信息: + +```csharp +// 记录带上下文的日志 +_logger.Log( + LogLevel.Error, + "玩家操作失败", + ("playerId", playerId), + ("action", "attack"), + ("targetId", targetId), + ("timestamp", DateTime.UtcNow) +); + +// 记录带异常的结构化日志 +_logger.Log( + LogLevel.Error, + "数据库操作失败", + exception, + ("operation", "insert"), + ("table", "players"), + ("retryCount", retryCount) +); +``` + +### 日志最佳实践 + +**1. 记录足够的上下文** + +```csharp +// ❌ 避免:信息不足 +_logger.Error("操作失败"); + +// ✅ 好的做法:包含上下文 +_logger.Error( + $"保存玩家数据失败: PlayerId={playerId}, Reason={ex.Message}", + ex +); +``` + +**2. 不要记录敏感信息** + +```csharp +// ❌ 避免:记录密码等敏感信息 +_logger.Info($"用户登录: {username}, 密码: {password}"); + +// ✅ 好的做法:不记录敏感信息 +_logger.Info($"用户登录: {username}"); +``` + +**3. 使用合适的日志级别** + +```csharp +public class SaveSystem : AbstractSystem +{ + public async Task<Result<SaveData>> LoadSaveAsync(string saveId) + { + _logger.Debug($"开始加载存档: {saveId}"); + + try + { + var data = await _storage.LoadAsync<SaveData>(saveId); + + if (data == null) + { + _logger.Warn($"存档不存在: {saveId}"); + return Result<SaveData>.Failure("存档不存在"); + } + + _logger.Info($"存档加载成功: {saveId}"); + return Result<SaveData>.Success(data); + } + catch (Exception ex) + { + _logger.Error($"加载存档失败: {saveId}", ex); + return Result<SaveData>.Failure(ex); + } + } +} +``` + +## 用户反馈 + +### 友好的错误提示 + +将技术错误转换为用户友好的消息: + +```csharp +public class ErrorMessageService +{ + private readonly Dictionary<string, string> _errorMessages = new() + { + ["NETWORK_ERROR"] = "网络连接失败,请检查网络设置", + ["SAVE_ERROR"] = "保存失败,请确保有足够的磁盘空间", + ["INVALID_INPUT"] = "输入无效,请检查后重试", + ["PERMISSION_DENIED"] = "权限不足,无法执行此操作", + ["RESOURCE_NOT_FOUND"] = "资源不存在", + ["TIMEOUT"] = "操作超时,请稍后重试" + }; + + public string GetUserFriendlyMessage(string errorCode) + { + return _errorMessages.TryGetValue(errorCode, out var message) + ? message + : "发生未知错误,请联系技术支持"; + } + + public string GetUserFriendlyMessage(Exception ex) + { + return ex switch + { + GameException gameEx => GetUserFriendlyMessage(gameEx.ErrorCode), + FileNotFoundException => "文件不存在", + UnauthorizedAccessException => "权限不足", + TimeoutException => "操作超时", + _ => "发生未知错误" + }; + } +} +``` + +### 错误提示 UI + +```csharp +public class ErrorNotificationSystem : AbstractSystem +{ + private readonly ErrorMessageService _messageService; + + protected override void OnInit() + { + this.RegisterEvent<ErrorOccurredEvent>(OnErrorOccurred); + } + + private void OnErrorOccurred(ErrorOccurredEvent e) + { + var userMessage = _messageService.GetUserFriendlyMessage(e.Exception); + + // 根据错误严重程度显示不同的 UI + if (e.Exception is GameException gameEx) + { + switch (gameEx.ErrorCode) + { + case "NETWORK_ERROR": + ShowNetworkErrorDialog(userMessage); + break; + + case "SAVE_ERROR": + ShowSaveErrorDialog(userMessage); + break; + + default: + ShowGenericErrorToast(userMessage); + break; + } + } + else + { + ShowGenericErrorDialog(userMessage); + } + } + + private void ShowNetworkErrorDialog(string message) + { + this.SendEvent(new ShowDialogEvent + { + Title = "网络错误", + Message = message, + Buttons = new[] { "重试", "取消" }, + OnButtonClicked = buttonIndex => + { + if (buttonIndex == 0) // 重试 + { + this.SendEvent(new RetryNetworkOperationEvent()); + } + } + }); + } + + private void ShowSaveErrorDialog(string message) + { + this.SendEvent(new ShowDialogEvent + { + Title = "保存失败", + Message = message, + Buttons = new[] { "确定" } + }); + } + + private void ShowGenericErrorToast(string message) + { + this.SendEvent(new ShowToastEvent + { + Message = message, + Duration = 3000 + }); + } + + private void ShowGenericErrorDialog(string message) + { + this.SendEvent(new ShowDialogEvent + { + Title = "错误", + Message = message, + Buttons = new[] { "确定" } + }); + } +} +``` + +## 错误恢复 + +### 重试机制 + +实现自动重试策略: + +```csharp +public class RetryPolicy +{ + public int MaxRetries { get; set; } = 3; + public TimeSpan InitialDelay { get; set; } = TimeSpan.FromSeconds(1); + public double BackoffMultiplier { get; set; } = 2.0; + + public async Task<Result<T>> ExecuteAsync<T>( + Func<Task<T>> operation, + Func<Exception, bool> shouldRetry = null) + { + var delay = InitialDelay; + Exception lastException = null; + + for (int attempt = 0; attempt <= MaxRetries; attempt++) + { + try + { + var result = await operation(); + return Result<T>.Success(result); + } + catch (Exception ex) + { + lastException = ex; + + // 检查是否应该重试 + if (shouldRetry != null && !shouldRetry(ex)) + { + break; + } + + // 最后一次尝试失败 + if (attempt == MaxRetries) + { + break; + } + + // 等待后重试 + await Task.Delay(delay); + delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * BackoffMultiplier); + } + } + + return Result<T>.Failure(lastException); + } +} + +// 使用示例 +public class NetworkSystem : AbstractSystem +{ + private readonly RetryPolicy _retryPolicy = new() + { + MaxRetries = 3, + InitialDelay = TimeSpan.FromSeconds(1), + BackoffMultiplier = 2.0 + }; + + public async Task<Result<PlayerData>> FetchPlayerDataAsync(string playerId) + { + return await _retryPolicy.ExecuteAsync( + operation: async () => + { + var response = await _httpClient.GetAsync($"/api/players/{playerId}"); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize<PlayerData>(json); + }, + shouldRetry: ex => ex is HttpRequestException || ex is TimeoutException + ); + } +} +``` + +### 降级策略 + +提供多级降级方案: + +```csharp +public class ConfigurationSystem : AbstractSystem +{ + public async Task<GameConfig> LoadConfigAsync() + { + // 1. 尝试从远程服务器加载 + var remoteResult = await TryLoadFromRemoteAsync(); + if (remoteResult.IsSuccess) + { + _logger.Info("从远程服务器加载配置成功"); + await CacheConfigAsync(remoteResult.Match(succ: c => c, fail: _ => null)); + return remoteResult.Match(succ: c => c, fail: _ => null); + } + + _logger.Warn($"远程加载失败: {remoteResult.Exception.Message}"); + + // 2. 尝试从本地缓存加载 + var cacheResult = await TryLoadFromCacheAsync(); + if (cacheResult.IsSuccess) + { + _logger.Info("从本地缓存加载配置成功"); + return cacheResult.Match(succ: c => c, fail: _ => null); + } + + _logger.Warn($"缓存加载失败: {cacheResult.Exception.Message}"); + + // 3. 使用内置默认配置 + _logger.Error("所有配置源加载失败,使用默认配置"); + return GetDefaultConfig(); + } + + private async Task<Result<GameConfig>> TryLoadFromRemoteAsync() + { + try + { + var response = await _httpClient.GetAsync("/api/config"); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + var config = JsonSerializer.Deserialize<GameConfig>(json); + return Result<GameConfig>.Success(config); + } + catch (Exception ex) + { + return Result<GameConfig>.Failure(ex); + } + } + + private async Task<Result<GameConfig>> TryLoadFromCacheAsync() + { + try + { + var json = await File.ReadAllTextAsync(_cachePath); + var config = JsonSerializer.Deserialize<GameConfig>(json); + return Result<GameConfig>.Success(config); + } + catch (Exception ex) + { + return Result<GameConfig>.Failure(ex); + } + } + + private async Task CacheConfigAsync(GameConfig config) + { + try + { + var json = JsonSerializer.Serialize(config); + await File.WriteAllTextAsync(_cachePath, json); + } + catch (Exception ex) + { + _logger.Warn("缓存配置失败", ex); + } + } + + private GameConfig GetDefaultConfig() + { + return new GameConfig + { + ServerUrl = "https://default.example.com", + Timeout = 30, + MaxRetries = 3 + }; + } +} +``` + +### 断路器模式 + +防止级联失败: + +```csharp +public class CircuitBreaker +{ + private int _failureCount = 0; + private DateTime _lastFailureTime = DateTime.MinValue; + private CircuitState _state = CircuitState.Closed; + + public int FailureThreshold { get; set; } = 5; + public TimeSpan OpenDuration { get; set; } = TimeSpan.FromMinutes(1); + + public async Task<Result<T>> ExecuteAsync<T>(Func<Task<T>> operation) + { + // 检查断路器状态 + if (_state == CircuitState.Open) + { + if (DateTime.UtcNow - _lastFailureTime > OpenDuration) + { + _state = CircuitState.HalfOpen; + } + else + { + return Result<T>.Failure("断路器已打开,操作被拒绝"); + } + } + + try + { + var result = await operation(); + + // 成功,重置计数器 + if (_state == CircuitState.HalfOpen) + { + _state = CircuitState.Closed; + } + _failureCount = 0; + + return Result<T>.Success(result); + } + catch (Exception ex) + { + _failureCount++; + _lastFailureTime = DateTime.UtcNow; + + // 达到阈值,打开断路器 + if (_failureCount >= FailureThreshold) + { + _state = CircuitState.Open; + } + + return Result<T>.Failure(ex); + } + } + + private enum CircuitState + { + Closed, // 正常状态 + Open, // 断路器打开,拒绝请求 + HalfOpen // 半开状态,尝试恢复 + } +} +``` + diff --git a/docs/zh-CN/best-practices/mobile-optimization.md b/docs/zh-CN/best-practices/mobile-optimization.md new file mode 100644 index 0000000..452bbe1 --- /dev/null +++ b/docs/zh-CN/best-practices/mobile-optimization.md @@ -0,0 +1,384 @@ +--- +title: 移动平台优化指南 +description: 针对移动平台的性能优化、内存管理和电池优化最佳实践 +--- + +# 移动平台优化指南 + +## 概述 + +移动平台游戏开发面临着独特的挑战:有限的内存、较弱的处理器、电池续航限制、触摸输入、多样的屏幕尺寸等。本指南将帮助你使用 +GFramework 开发高性能的移动游戏,提供针对性的优化策略和最佳实践。 + +**移动平台的主要限制**: + +- **内存限制**:移动设备内存通常在 2-8GB,远低于 PC +- **CPU 性能**:移动 CPU 性能较弱,且受热量限制 +- **GPU 性能**:移动 GPU 功能有限,填充率和带宽受限 +- **电池续航**:高性能运行会快速消耗电池 +- **存储空间**:应用包大小受限,用户存储空间有限 +- **网络环境**:移动网络不稳定,延迟较高 + +**优化目标**: + +- 减少内存占用(目标:<200MB) +- 降低 CPU 使用率(目标:<30%) +- 优化 GPU 渲染(目标:60 FPS) +- 延长电池续航(目标:3+ 小时) +- 减小包体大小(目标:<100MB) + +## 核心概念 + +### 1. 内存管理 + +移动设备内存有限,需要精细管理: + +```csharp +// 监控内存使用 +public class MemoryMonitor : AbstractSystem +{ + private const long MemoryWarningThreshold = 150 * 1024 * 1024; // 150MB + private const long MemoryCriticalThreshold = 200 * 1024 * 1024; // 200MB + + protected override void OnInit() + { + this.RegisterEvent<GameUpdateEvent>(OnUpdate); + } + + private void OnUpdate(GameUpdateEvent e) + { + // 每 5 秒检查一次内存 + if (e.TotalTime % 5.0 < e.DeltaTime) + { + CheckMemoryUsage(); + } + } + + private void CheckMemoryUsage() + { + var memoryUsage = GC.GetTotalMemory(false); + + if (memoryUsage > MemoryCriticalThreshold) + { + // 内存严重不足,强制清理 + SendEvent(new MemoryCriticalEvent()); + ForceMemoryCleanup(); + } + else if (memoryUsage > MemoryWarningThreshold) + { + // 内存警告,温和清理 + SendEvent(new MemoryWarningEvent()); + SoftMemoryCleanup(); + } + } + + private void ForceMemoryCleanup() + { + // 卸载不必要的资源 + var resourceManager = this.GetUtility<IResourceManager>(); + resourceManager.UnloadUnusedResources(); + + // 清理对象池 + var poolSystem = this.GetSystem<ObjectPoolSystem>(); + poolSystem.TrimPools(); + + // 强制 GC + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + private void SoftMemoryCleanup() + { + // 温和清理:只清理明确不需要的资源 + var resourceManager = this.GetUtility<IResourceManager>(); + resourceManager.UnloadUnusedResources(); + } +} +``` + +### 2. 性能分析 + +使用性能分析工具识别瓶颈: + +```csharp +public class PerformanceProfiler : AbstractSystem +{ + private readonly Dictionary<string, PerformanceMetrics> _metrics = new(); + + public IDisposable Profile(string name) + { + return new ProfileScope(name, this); + } + + private void RecordMetric(string name, double duration) + { + if (!_metrics.TryGetValue(name, out var metrics)) + { + metrics = new PerformanceMetrics(); + _metrics[name] = metrics; + } + + metrics.AddSample(duration); + } + + public void PrintReport() + { + Console.WriteLine("\n=== 性能报告 ==="); + foreach (var (name, metrics) in _metrics.OrderByDescending(x => x.Value.AverageMs)) + { + Console.WriteLine($"{name}:"); + Console.WriteLine($" 平均: {metrics.AverageMs:F2}ms"); + Console.WriteLine($" 最大: {metrics.MaxMs:F2}ms"); + Console.WriteLine($" 最小: {metrics.MinMs:F2}ms"); + Console.WriteLine($" 调用次数: {metrics.SampleCount}"); + } + } + + private class ProfileScope : IDisposable + { + private readonly string _name; + private readonly PerformanceProfiler _profiler; + private readonly Stopwatch _stopwatch; + + public ProfileScope(string name, PerformanceProfiler profiler) + { + _name = name; + _profiler = profiler; + _stopwatch = Stopwatch.StartNew(); + } + + public void Dispose() + { + _stopwatch.Stop(); + _profiler.RecordMetric(_name, _stopwatch.Elapsed.TotalMilliseconds); + } + } +} + +// 使用示例 +public class GameSystem : AbstractSystem +{ + private PerformanceProfiler _profiler; + + protected override void OnInit() + { + _profiler = this.GetSystem<PerformanceProfiler>(); + } + + private void UpdateGame() + { + using (_profiler.Profile("GameUpdate")) + { + // 游戏更新逻辑 + } + } +} +``` + +### 3. 电池优化 + +减少不必要的计算和渲染: + +```csharp +public class PowerSavingSystem : AbstractSystem +{ + private bool _isPowerSavingMode; + private int _targetFrameRate = 60; + + protected override void OnInit() + { + this.RegisterEvent<BatteryLowEvent>(OnBatteryLow); + this.RegisterEvent<BatteryNormalEvent>(OnBatteryNormal); + } + + private void OnBatteryLow(BatteryLowEvent e) + { + EnablePowerSavingMode(); + } + + private void OnBatteryNormal(BatteryNormalEvent e) + { + DisablePowerSavingMode(); + } + + private void EnablePowerSavingMode() + { + _isPowerSavingMode = true; + + // 降低帧率 + _targetFrameRate = 30; + Application.targetFrameRate = _targetFrameRate; + + // 降低渲染质量 + QualitySettings.SetQualityLevel(0); + + // 减少粒子效果 + SendEvent(new ReduceEffectsEvent()); + + // 暂停非关键系统 + PauseNonCriticalSystems(); + + Console.WriteLine("省电模式已启用"); + } + + private void DisablePowerSavingMode() + { + _isPowerSavingMode = false; + + // 恢复帧率 + _targetFrameRate = 60; + Application.targetFrameRate = _targetFrameRate; + + // 恢复渲染质量 + QualitySettings.SetQualityLevel(2); + + // 恢复粒子效果 + SendEvent(new RestoreEffectsEvent()); + + // 恢复非关键系统 + ResumeNonCriticalSystems(); + + Console.WriteLine("省电模式已禁用"); + } + + private void PauseNonCriticalSystems() + { + // 暂停动画系统 + var animationSystem = this.GetSystem<AnimationSystem>(); + animationSystem?.Pause(); + + // 暂停音效系统(保留音乐) + var audioSystem = this.GetSystem<AudioSystem>(); + audioSystem?.PauseSoundEffects(); + } + + private void ResumeNonCriticalSystems() + { + var animationSystem = this.GetSystem<AnimationSystem>(); + animationSystem?.Resume(); + + var audioSystem = this.GetSystem<AudioSystem>(); + audioSystem?.ResumeSoundEffects(); + } +} +``` + +## 内存优化 + +### 1. 资源管理策略 + +实现智能资源加载和卸载: + +```csharp +public class MobileResourceManager : AbstractSystem +{ + private readonly IResourceManager _resourceManager; + private readonly Dictionary<string, ResourcePriority> _resourcePriorities = new(); + private readonly HashSet<string> _loadedResources = new(); + + public MobileResourceManager(IResourceManager resourceManager) + { + _resourceManager = resourceManager; + } + + protected override void OnInit() + { + // 配置资源优先级 + ConfigureResourcePriorities(); + + // 监听场景切换事件 + this.RegisterEvent<SceneChangedEvent>(OnSceneChanged); + + // 监听内存警告 + this.RegisterEvent<MemoryWarningEvent>(OnMemoryWarning); + } + + private void ConfigureResourcePriorities() + { + // 高优先级:UI、玩家资源 + _resourcePriorities["ui/"] = ResourcePriority.High; + _resourcePriorities["player/"] = ResourcePriority.High; + + // 中优先级:敌人、道具 + _resourcePriorities["enemy/"] = ResourcePriority.Medium; + _resourcePriorities["item/"] = ResourcePriority.Medium; + + // 低优先级:特效、装饰 + _resourcePriorities["effect/"] = ResourcePriority.Low; + _resourcePriorities["decoration/"] = ResourcePriority.Low; + } + + public async Task<T> LoadResourceAsync<T>(string path) where T : class + { + // 检查内存 + if (IsMemoryLow()) + { + // 内存不足,先清理低优先级资源 + UnloadLowPriorityResources(); + } + + var resource = await _resourceManager.LoadAsync<T>(path); + _loadedResources.Add(path); + + return resource; + } + + private void OnSceneChanged(SceneChangedEvent e) + { + // 场景切换时,卸载旧场景资源 + UnloadSceneResources(e.PreviousScene); + + // 预加载新场景资源 + PreloadSceneResources(e.NewScene); + } + + private void OnMemoryWarning(MemoryWarningEvent e) + { + // 内存警告,卸载低优先级资源 + UnloadLowPriorityResources(); + } + + private void UnloadLowPriorityResources() + { + var resourcesToUnload = _loadedResources + .Where(path => GetResourcePriority(path) == ResourcePriority.Low) + .ToList(); + + foreach (var path in resourcesToUnload) + { + _resourceManager.Unload(path); + _loadedResources.Remove(path); + } + + Console.WriteLine($"卸载了 {resourcesToUnload.Count} 个低优先级资源"); + } + + private ResourcePriority GetResourcePriority(string path) + { + foreach (var (prefix, priority) in _resourcePriorities) + { + if (path.StartsWith(prefix)) + return priority; + } + + return ResourcePriority.Medium; + } + + private bool IsMemoryLow() + { + var memoryUsage = GC.GetTotalMemory(false); + return memoryUsage > 150 * 1024 * 1024; // 150MB + } +} + +public enum ResourcePriority +{ + Low, + Medium, + High +} +``` + +### 2. 纹理压缩和优化 + diff --git a/docs/zh-CN/best-practices/multiplayer.md b/docs/zh-CN/best-practices/multiplayer.md new file mode 100644 index 0000000..7366fac --- /dev/null +++ b/docs/zh-CN/best-practices/multiplayer.md @@ -0,0 +1,546 @@ +# 多人游戏架构指南 + +> 基于 GFramework 架构设计高性能、可扩展的多人游戏系统。 + +## 📋 目录 + +- [概述](#概述) +- [核心概念](#核心概念) +- [架构设计](#架构设计) +- [状态管理](#状态管理) +- [命令模式与输入处理](#命令模式与输入处理) +- [事件同步](#事件同步) +- [网络优化](#网络优化) +- [安全考虑](#安全考虑) +- [最佳实践](#最佳实践) +- [常见问题](#常见问题) + +## 概述 + +多人游戏开发面临着单机游戏所没有的独特挑战: + +### 主要挑战 + +1. **网络延迟** - 玩家操作和服务器响应之间存在不可避免的延迟 +2. **状态同步** - 确保所有客户端看到一致的游戏状态 +3. **带宽限制** - 需要高效传输游戏数据,避免网络拥塞 +4. **作弊防护** - 防止客户端篡改游戏逻辑和数据 +5. **并发处理** - 同时处理多个玩家的输入和状态更新 +6. **断线重连** - 优雅处理网络中断和玩家重新连接 + +### GFramework 的优势 + +GFramework 的架构设计天然适合多人游戏开发: + +- **分层架构** - 清晰分离客户端逻辑、网络层和服务器逻辑 +- **事件系统** - 松耦合的事件驱动架构便于状态同步 +- **命令模式** - 统一的输入处理和验证机制 +- **Model-System 分离** - 数据和逻辑分离便于状态管理 +- **模块化设计** - 网络功能可以作为独立模块集成 + +## 核心概念 + +### 1. 客户端-服务器架构 + +```csharp +// 服务器架构 +public class ServerArchitecture : Architecture +{ + protected override void Init() + { + // 注册服务器专用的 Model + RegisterModel(new ServerGameStateModel()); + RegisterModel(new PlayerConnectionModel()); + + // 注册服务器专用的 System + RegisterSystem(new ServerNetworkSystem()); + RegisterSystem(new AuthorityGameLogicSystem()); + RegisterSystem(new StateReplicationSystem()); + RegisterSystem(new AntiCheatSystem()); + + // 注册工具 + RegisterUtility(new NetworkUtility()); + RegisterUtility(new ValidationUtility()); + } +} + +// 客户端架构 +public class ClientArchitecture : Architecture +{ + protected override void Init() + { + // 注册客户端专用的 Model + RegisterModel(new ClientGameStateModel()); + RegisterModel(new PredictionModel()); + + // 注册客户端专用的 System + RegisterSystem(new ClientNetworkSystem()); + RegisterSystem(new PredictionSystem()); + RegisterSystem(new InterpolationSystem()); + RegisterSystem(new ClientInputSystem()); + + // 注册工具 + RegisterUtility(new NetworkUtility()); + } +} +``` + +### 2. 状态同步策略 + +#### 状态同步 (State Synchronization) + +服务器定期向客户端发送完整的游戏状态。 + +```csharp +// 游戏状态快照 +public struct GameStateSnapshot +{ + public uint Tick { get; set; } + public long Timestamp { get; set; } + public PlayerState[] Players { get; set; } + public EntityState[] Entities { get; set; } +} + +public struct PlayerState +{ + public string PlayerId { get; set; } + public Vector3 Position { get; set; } + public Quaternion Rotation { get; set; } + public int Health { get; set; } + public PlayerAnimationState AnimationState { get; set; } +} + +// 状态复制系统 +public class StateReplicationSystem : AbstractSystem +{ + private ServerGameStateModel _gameState; + private PlayerConnectionModel _connections; + private uint _currentTick; + + protected override void OnInit() + { + _gameState = this.GetModel<ServerGameStateModel>(); + _connections = this.GetModel<PlayerConnectionModel>(); + + // 每个 tick 复制状态 + this.RegisterEvent<ServerTickEvent>(OnServerTick); + } + + private void OnServerTick(ServerTickEvent e) + { + _currentTick++; + + // 创建状态快照 + var snapshot = CreateSnapshot(); + + // 发送给所有连接的客户端 + foreach (var connection in _connections.ActiveConnections) + { + SendSnapshotToClient(connection, snapshot); + } + } + + private GameStateSnapshot CreateSnapshot() + { + return new GameStateSnapshot + { + Tick = _currentTick, + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Players = _gameState.Players.Select(p => new PlayerState + { + PlayerId = p.Id, + Position = p.Position, + Rotation = p.Rotation, + Health = p.Health, + AnimationState = p.AnimationState + }).ToArray(), + Entities = _gameState.Entities.Select(e => CreateEntityState(e)).ToArray() + }; + } +} +``` + +#### 增量同步 (Delta Synchronization) + +只发送状态变化,减少带宽消耗。 + +```csharp +// 增量状态 +public struct DeltaState +{ + public uint Tick { get; set; } + public uint BaseTick { get; set; } + public PlayerDelta[] PlayerDeltas { get; set; } + public EntityDelta[] EntityDeltas { get; set; } +} + +public struct PlayerDelta +{ + public string PlayerId { get; set; } + public DeltaFlags Flags { get; set; } + public Vector3? Position { get; set; } + public Quaternion? Rotation { get; set; } + public int? Health { get; set; } +} + +[Flags] +public enum DeltaFlags +{ + None = 0, + Position = 1 << 0, + Rotation = 1 << 1, + Health = 1 << 2, + Animation = 1 << 3 +} + +// 增量复制系统 +public class DeltaReplicationSystem : AbstractSystem +{ + private readonly Dictionary<uint, GameStateSnapshot> _snapshotHistory = new(); + private const int MaxHistorySize = 60; // 保留 60 个快照 + + protected override void OnInit() + { + this.RegisterEvent<ServerTickEvent>(OnServerTick); + } + + private void OnServerTick(ServerTickEvent e) + { + var currentSnapshot = CreateSnapshot(); + _snapshotHistory[e.Tick] = currentSnapshot; + + // 清理旧快照 + CleanupOldSnapshots(e.Tick); + + // 为每个客户端生成增量 + foreach (var connection in GetConnections()) + { + var lastAckedTick = connection.LastAcknowledgedTick; + + if (_snapshotHistory.TryGetValue(lastAckedTick, out var baseSnapshot)) + { + var delta = CreateDelta(baseSnapshot, currentSnapshot); + SendDeltaToClient(connection, delta); + } + else + { + // 客户端太落后,发送完整快照 + SendSnapshotToClient(connection, currentSnapshot); + } + } + } + + private DeltaState CreateDelta(GameStateSnapshot baseSnapshot, GameStateSnapshot currentSnapshot) + { + var delta = new DeltaState + { + Tick = currentSnapshot.Tick, + BaseTick = baseSnapshot.Tick, + PlayerDeltas = new List<PlayerDelta>() + }; + + // 比较玩家状态 + foreach (var currentPlayer in currentSnapshot.Players) + { + var basePlayer = baseSnapshot.Players.FirstOrDefault(p => p.PlayerId == currentPlayer.PlayerId); + + if (basePlayer.PlayerId == null) + { + // 新玩家,发送完整状态 + delta.PlayerDeltas.Add(CreateFullPlayerDelta(currentPlayer)); + } + else + { + // 计算差异 + var playerDelta = CreatePlayerDelta(basePlayer, currentPlayer); + if (playerDelta.Flags != DeltaFlags.None) + { + delta.PlayerDeltas.Add(playerDelta); + } + } + } + + return delta; + } + + private PlayerDelta CreatePlayerDelta(PlayerState baseState, PlayerState currentState) + { + var delta = new PlayerDelta { PlayerId = currentState.PlayerId }; + + if (Vector3.Distance(baseState.Position, currentState.Position) > 0.01f) + { + delta.Flags |= DeltaFlags.Position; + delta.Position = currentState.Position; + } + + if (Quaternion.Angle(baseState.Rotation, currentState.Rotation) > 0.1f) + { + delta.Flags |= DeltaFlags.Rotation; + delta.Rotation = currentState.Rotation; + } + + if (baseState.Health != currentState.Health) + { + delta.Flags |= DeltaFlags.Health; + delta.Health = currentState.Health; + } + + return delta; + } +} +``` + +### 3. 客户端预测与回滚 + +客户端立即响应玩家输入,然后在收到服务器确认后进行校正。 + +```csharp +// 输入命令 +public struct PlayerInputCommand +{ + public uint Tick { get; set; } + public long Timestamp { get; set; } + public Vector2 MoveDirection { get; set; } + public Vector2 LookDirection { get; set; } + public InputFlags Flags { get; set; } +} + +[Flags] +public enum InputFlags +{ + None = 0, + Jump = 1 << 0, + Attack = 1 << 1, + Interact = 1 << 2, + Reload = 1 << 3 +} + +// 客户端预测系统 +public class ClientPredictionSystem : AbstractSystem +{ + private PredictionModel _prediction; + private ClientGameStateModel _gameState; + private readonly Queue<PlayerInputCommand> _pendingInputs = new(); + private uint _lastProcessedServerTick; + + protected override void OnInit() + { + _prediction = this.GetModel<PredictionModel>(); + _gameState = this.GetModel<ClientGameStateModel>(); + + this.RegisterEvent<PlayerInputEvent>(OnPlayerInput); + this.RegisterEvent<ServerStateReceivedEvent>(OnServerStateReceived); + } + + private void OnPlayerInput(PlayerInputEvent e) + { + var input = new PlayerInputCommand + { + Tick = _prediction.CurrentTick, + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + MoveDirection = e.MoveDirection, + LookDirection = e.LookDirection, + Flags = e.Flags + }; + + // 保存输入用于重放 + _pendingInputs.Enqueue(input); + + // 立即应用预测 + ApplyInput(input); + + // 发送到服务器 + SendInputToServer(input); + } + + private void OnServerStateReceived(ServerStateReceivedEvent e) + { + _lastProcessedServerTick = e.Snapshot.Tick; + + // 应用服务器状态 + ApplyServerState(e.Snapshot); + + // 移除已确认的输入 + while (_pendingInputs.Count > 0 && _pendingInputs.Peek().Tick <= e.Snapshot.Tick) + { + _pendingInputs.Dequeue(); + } + + // 重放未确认的输入 + ReplayPendingInputs(); + } + + private void ApplyInput(PlayerInputCommand input) + { + var player = _gameState.LocalPlayer; + + // 应用移动 + var movement = input.MoveDirection * player.Speed * Time.DeltaTime; + player.Position += new Vector3(movement.X, 0, movement.Y); + + // 应用旋转 + if (input.LookDirection != Vector2.Zero) + { + player.Rotation = Quaternion.LookRotation( + new Vector3(input.LookDirection.X, 0, input.LookDirection.Y) + ); + } + + // 应用动作 + if ((input.Flags & InputFlags.Jump) != 0 && player.IsGrounded) + { + player.Velocity = new Vector3(player.Velocity.X, player.JumpForce, player.Velocity.Z); + } + } + + private void ReplayPendingInputs() + { + // 从服务器状态开始重放所有未确认的输入 + var savedState = SavePlayerState(); + + foreach (var input in _pendingInputs) + { + ApplyInput(input); + } + } +} +``` + +## 架构设计 + +### 1. 分离逻辑层 + +```csharp +// 共享游戏逻辑 (客户端和服务器都使用) +public class SharedGameLogic +{ + public static void ProcessMovement(PlayerState player, Vector2 moveDirection, float deltaTime) + { + var movement = moveDirection.Normalized() * player.Speed * deltaTime; + player.Position += new Vector3(movement.X, 0, movement.Y); + } + + public static bool CanJump(PlayerState player) + { + return player.IsGrounded && !player.IsStunned; + } + + public static int CalculateDamage(int attackPower, int defense, float criticalChance) + { + var baseDamage = Math.Max(1, attackPower - defense); + var isCritical = Random.Shared.NextDouble() < criticalChance; + return isCritical ? baseDamage * 2 : baseDamage; + } +} + +// 服务器权威逻辑 +public class ServerGameLogicSystem : AbstractSystem +{ + private ServerGameStateModel _gameState; + + protected override void OnInit() + { + _gameState = this.GetModel<ServerGameStateModel>(); + + this.RegisterEvent<PlayerInputReceivedEvent>(OnPlayerInputReceived); + this.RegisterEvent<AttackRequestEvent>(OnAttackRequest); + } + + private void OnPlayerInputReceived(PlayerInputReceivedEvent e) + { + var player = _gameState.GetPlayer(e.PlayerId); + + // 验证输入 + if (!ValidateInput(e.Input)) + { + SendInputRejection(e.PlayerId, "Invalid input"); + return; + } + + // 应用共享逻辑 + SharedGameLogic.ProcessMovement(player, e.Input.MoveDirection, Time.DeltaTime); + + // 服务器端验证 + if ((e.Input.Flags & InputFlags.Jump) != 0) + { + if (SharedGameLogic.CanJump(player)) + { + player.Velocity = new Vector3(player.Velocity.X, player.JumpForce, player.Velocity.Z); + } + } + } + + private void OnAttackRequest(AttackRequestEvent e) + { + var attacker = _gameState.GetPlayer(e.AttackerId); + var target = _gameState.GetPlayer(e.TargetId); + + // 服务器端验证 + if (!CanAttack(attacker, target)) + { + return; + } + + // 计算伤害 + var damage = SharedGameLogic.CalculateDamage( + attacker.AttackPower, + target.Defense, + attacker.CriticalChance + ); + + // 应用伤害 + target.Health = Math.Max(0, target.Health - damage); + + // 广播事件 + this.SendEvent(new PlayerDamagedEvent + { + AttackerId = e.AttackerId, + TargetId = e.TargetId, + Damage = damage, + RemainingHealth = target.Health + }); + + if (target.Health == 0) + { + this.SendEvent(new PlayerDiedEvent + { + PlayerId = e.TargetId, + KillerId = e.AttackerId + }); + } + } +} + +// 客户端表现逻辑 +public class ClientPresentationSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent<PlayerDamagedEvent>(OnPlayerDamaged); + this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied); + } + + private void OnPlayerDamaged(PlayerDamagedEvent e) + { + // 播放受击特效 + PlayDamageEffect(e.TargetId, e.Damage); + + // 播放受击音效 + PlayDamageSound(e.TargetId); + + // 更新 UI + UpdateHealthBar(e.TargetId, e.RemainingHealth); + } + + private void OnPlayerDied(PlayerDiedEvent e) + { + // 播放死亡动画 + PlayDeathAnimation(e.PlayerId); + + // 播放死亡音效 + PlayDeathSound(e.PlayerId); + + // 显示击杀提示 + ShowKillFeed(e.KillerId, e.PlayerId); + } +} diff --git a/docs/zh-CN/best-practices/performance.md b/docs/zh-CN/best-practices/performance.md new file mode 100644 index 0000000..65e9c57 --- /dev/null +++ b/docs/zh-CN/best-practices/performance.md @@ -0,0 +1,1359 @@ +# 性能优化指南 + +> 全面的性能优化策略和最佳实践,帮助你构建高性能的游戏应用。 + +## 📋 目录 + +- [概述](#概述) +- [核心概念](#核心概念) +- [对象池优化](#对象池优化) +- [事件系统优化](#事件系统优化) +- [协程优化](#协程优化) +- [资源管理优化](#资源管理优化) +- [ECS 性能优化](#ecs-性能优化) +- [内存优化](#内存优化) +- [最佳实践](#最佳实践) +- [常见问题](#常见问题) + +## 概述 + +性能优化是游戏开发中的关键环节。良好的性能不仅能提供流畅的用户体验,还能降低设备功耗,延长电池寿命。本指南将介绍 GFramework +中的性能优化策略和最佳实践。 + +### 性能优化的重要性 + +- **用户体验** - 流畅的帧率和快速的响应时间 +- **设备兼容性** - 在低端设备上也能良好运行 +- **资源效率** - 降低内存占用和 CPU 使用率 +- **电池寿命** - 减少不必要的计算和内存分配 + +## 核心概念 + +### 1. 性能瓶颈 + +性能瓶颈是指限制系统整体性能的关键因素: + +- **CPU 瓶颈** - 过多的计算、复杂的逻辑 +- **内存瓶颈** - 频繁的 GC、内存泄漏 +- **GPU 瓶颈** - 过多的绘制调用、复杂的着色器 +- **I/O 瓶颈** - 频繁的文件读写、网络请求 + +### 2. 性能指标 + +关键的性能指标: + +- **帧率 (FPS)** - 每秒渲染的帧数,目标 60 FPS +- **帧时间** - 每帧的处理时间,目标 <16.67ms +- **内存占用** - 应用程序使用的内存量 +- **GC 频率** - 垃圾回收的频率和耗时 +- **加载时间** - 场景和资源的加载时间 + +### 3. 优化策略 + +性能优化的基本策略: + +- **测量优先** - 先测量,再优化 +- **找到瓶颈** - 使用性能分析工具定位问题 +- **渐进优化** - 逐步优化,避免过早优化 +- **权衡取舍** - 在性能和可维护性之间找到平衡 + +## 对象池优化 + +对象池是减少 GC 压力的有效手段,通过复用对象避免频繁的内存分配和释放。 + +### 1. 使用对象池系统 + +```csharp +// ✅ 好的做法:使用对象池 +public class BulletPoolSystem : AbstractObjectPoolSystem<string, Bullet> +{ + protected override Bullet Create(string key) + { + // 创建新的子弹对象 + var bullet = new Bullet(); + bullet.Initialize(key); + return bullet; + } +} + +public class Bullet : IPoolableObject +{ + public string Type { get; private set; } + public Vector2 Position { get; set; } + public Vector2 Velocity { get; set; } + public bool IsActive { get; private set; } + + public void Initialize(string type) + { + Type = type; + } + + public void OnAcquire() + { + // 从池中获取时重置状态 + IsActive = true; + Position = Vector2.Zero; + Velocity = Vector2.Zero; + } + + public void OnRelease() + { + // 归还到池中时清理状态 + IsActive = false; + } + + public void OnPoolDestroy() + { + // 对象被销毁时的清理 + } +} + +// 使用对象池 +public class CombatSystem : AbstractSystem +{ + private BulletPoolSystem _bulletPool; + + protected override void OnInit() + { + _bulletPool = GetSystem<BulletPoolSystem>(); + + // 预热对象池 + _bulletPool.Prewarm("normal_bullet", 50); + _bulletPool.Prewarm("fire_bullet", 20); + } + + private void FireBullet(string bulletType, Vector2 position, Vector2 direction) + { + // 从池中获取子弹 + var bullet = _bulletPool.Acquire(bulletType); + bullet.Position = position; + bullet.Velocity = direction * 10f; + + // 使用完毕后归还 + // bullet 会在生命周期结束时自动归还 + } +} + +// ❌ 避免:频繁创建和销毁对象 +public class CombatSystem : AbstractSystem +{ + private void FireBullet(string bulletType, Vector2 position, Vector2 direction) + { + // 每次都创建新对象,产生大量 GC + var bullet = new Bullet(); + bullet.Type = bulletType; + bullet.Position = position; + bullet.Velocity = direction * 10f; + + // 使用完毕后直接丢弃,等待 GC 回收 + } +} +``` + +### 2. StringBuilder 池 + +GFramework 提供了 `StringBuilderPool` 用于高效的字符串构建: + +```csharp +// ✅ 好的做法:使用 StringBuilderPool +public string FormatPlayerInfo(Player player) +{ + using var sb = StringBuilderPool.GetScoped(); + sb.Value.Append("Player: "); + sb.Value.Append(player.Name); + sb.Value.Append(", Level: "); + sb.Value.Append(player.Level); + sb.Value.Append(", HP: "); + sb.Value.Append(player.Health); + sb.Value.Append("/"); + sb.Value.Append(player.MaxHealth); + return sb.Value.ToString(); +} + +// 或者手动管理 +public string FormatPlayerInfo(Player player) +{ + var sb = StringBuilderPool.Rent(); + try + { + sb.Append("Player: ").Append(player.Name); + sb.Append(", Level: ").Append(player.Level); + sb.Append(", HP: ").Append(player.Health).Append("/").Append(player.MaxHealth); + return sb.ToString(); + } + finally + { + StringBuilderPool.Return(sb); + } +} + +// ❌ 避免:频繁的字符串拼接 +public string FormatPlayerInfo(Player player) +{ + // 每次拼接都会创建新的字符串对象 + return "Player: " + player.Name + + ", Level: " + player.Level + + ", HP: " + player.Health + "/" + player.MaxHealth; +} +``` + +### 3. ArrayPool 优化 + +使用 `ArrayPool` 避免频繁的数组分配: + +```csharp +// ✅ 好的做法:使用 ArrayPool +public void ProcessEntities(List<Entity> entities) +{ + using var scopedArray = ArrayPool<Entity>.Shared.GetScoped(entities.Count); + var array = scopedArray.Array; + + // 复制到数组进行处理 + entities.CopyTo(array, 0); + + // 处理数组 + for (int i = 0; i < entities.Count; i++) + { + ProcessEntity(array[i]); + } + + // 自动归还到池中 +} + +// ❌ 避免:频繁创建数组 +public void ProcessEntities(List<Entity> entities) +{ + // 每次都创建新数组 + var array = entities.ToArray(); + + foreach (var entity in array) + { + ProcessEntity(entity); + } + + // 数组等待 GC 回收 +} +``` + +### 4. 对象池统计 + +监控对象池的使用情况: + +```csharp +public class PoolMonitorSystem : AbstractSystem +{ + private BulletPoolSystem _bulletPool; + + protected override void OnInit() + { + _bulletPool = GetSystem<BulletPoolSystem>(); + } + + public void LogPoolStatistics(string poolKey) + { + var stats = _bulletPool.GetStatistics(poolKey); + + Logger.Info($"Pool Statistics for '{poolKey}':"); + Logger.Info($" Available: {stats.AvailableCount}"); + Logger.Info($" Active: {stats.ActiveCount}"); + Logger.Info($" Total Created: {stats.TotalCreated}"); + Logger.Info($" Total Acquired: {stats.TotalAcquired}"); + Logger.Info($" Total Released: {stats.TotalReleased}"); + Logger.Info($" Total Destroyed: {stats.TotalDestroyed}"); + + // 检查是否需要调整池大小 + if (stats.TotalDestroyed > stats.TotalCreated * 0.5) + { + Logger.Warning($"Pool '{poolKey}' has high destruction rate, consider increasing max capacity"); + } + } +} +``` + +## 事件系统优化 + +事件系统是游戏架构的核心,优化事件处理可以显著提升性能。 + +### 1. 避免事件订阅泄漏 + +```csharp +// ✅ 好的做法:正确管理事件订阅 +public class PlayerController : Node, IController +{ + private IUnRegisterList _unRegisterList = new UnRegisterList(); + private PlayerModel _playerModel; + + public void Initialize(IArchitectureContext context) + { + _playerModel = context.GetModel<PlayerModel>(); + + // 使用 UnRegisterList 管理订阅 + context.RegisterEvent<PlayerDamagedEvent>(OnPlayerDamaged) + .AddTo(_unRegisterList); + + _playerModel.Health.Register(OnHealthChanged) + .AddTo(_unRegisterList); + } + + public void Cleanup() + { + // 统一取消所有订阅 + _unRegisterList.UnRegisterAll(); + } + + private void OnPlayerDamaged(PlayerDamagedEvent e) { } + private void OnHealthChanged(int health) { } +} + +// ❌ 避免:忘记取消订阅 +public class PlayerController : Node, IController +{ + public void Initialize(IArchitectureContext context) + { + // 订阅事件但从不取消订阅 + context.RegisterEvent<PlayerDamagedEvent>(OnPlayerDamaged); + + var playerModel = context.GetModel<PlayerModel>(); + playerModel.Health.Register(OnHealthChanged); + + // 当对象被销毁时,这些订阅仍然存在,导致内存泄漏 + } +} +``` + +### 2. 使用结构体事件 + +使用值类型事件避免堆分配: + +```csharp +// ✅ 好的做法:使用结构体事件 +public struct PlayerMovedEvent +{ + public string PlayerId { get; init; } + public Vector2 OldPosition { get; init; } + public Vector2 NewPosition { get; init; } + public float DeltaTime { get; init; } +} + +public class MovementSystem : AbstractSystem +{ + private void NotifyPlayerMoved(string playerId, Vector2 oldPos, Vector2 newPos, float deltaTime) + { + // 结构体在栈上分配,无 GC 压力 + SendEvent(new PlayerMovedEvent + { + PlayerId = playerId, + OldPosition = oldPos, + NewPosition = newPos, + DeltaTime = deltaTime + }); + } +} + +// ❌ 避免:使用类事件 +public class PlayerMovedEvent +{ + public string PlayerId { get; set; } + public Vector2 OldPosition { get; set; } + public Vector2 NewPosition { get; set; } + public float DeltaTime { get; set; } +} + +// 每次发送事件都会在堆上分配对象 +``` + +### 3. 批量事件处理 + +对于高频事件,考虑批量处理: + +```csharp +// ✅ 好的做法:批量处理事件 +public class DamageSystem : AbstractSystem +{ + private readonly List<DamageInfo> _pendingDamages = new(); + private float _batchInterval = 0.1f; + private float _timeSinceLastBatch = 0f; + + public void Update(float deltaTime) + { + _timeSinceLastBatch += deltaTime; + + if (_timeSinceLastBatch >= _batchInterval) + { + ProcessDamageBatch(); + _timeSinceLastBatch = 0f; + } + } + + public void QueueDamage(string entityId, int damage, DamageType type) + { + _pendingDamages.Add(new DamageInfo + { + EntityId = entityId, + Damage = damage, + Type = type + }); + } + + private void ProcessDamageBatch() + { + if (_pendingDamages.Count == 0) + return; + + // 批量处理所有伤害 + foreach (var damageInfo in _pendingDamages) + { + ApplyDamage(damageInfo); + } + + // 发送单个批量事件 + SendEvent(new DamageBatchProcessedEvent + { + DamageCount = _pendingDamages.Count, + TotalDamage = _pendingDamages.Sum(d => d.Damage) + }); + + _pendingDamages.Clear(); + } +} + +// ❌ 避免:每次都立即处理 +public class DamageSystem : AbstractSystem +{ + public void ApplyDamage(string entityId, int damage, DamageType type) + { + // 每次伤害都立即处理并发送事件 + ProcessDamage(entityId, damage, type); + SendEvent(new DamageAppliedEvent { EntityId = entityId, Damage = damage }); + } +} +``` + +### 4. 事件优先级优化 + +合理使用事件优先级避免不必要的处理: + +```csharp +// ✅ 好的做法:使用优先级控制事件传播 +public class InputSystem : AbstractSystem +{ + protected override void OnInit() + { + // UI 输入处理器优先级最高 + this.RegisterEvent<InputEvent>(OnUIInput, priority: 100); + } + + private void OnUIInput(InputEvent e) + { + if (IsUIHandlingInput()) + { + // 停止事件传播,避免游戏逻辑处理 + e.StopPropagation(); + } + } +} + +public class GameplayInputSystem : AbstractSystem +{ + protected override void OnInit() + { + // 游戏逻辑输入处理器优先级较低 + this.RegisterEvent<InputEvent>(OnGameplayInput, priority: 50); + } + + private void OnGameplayInput(InputEvent e) + { + // 如果 UI 已经处理了输入,这里不会执行 + if (!e.IsPropagationStopped) + { + ProcessGameplayInput(e); + } + } +} +``` + +## 协程优化 + +协程是处理异步逻辑的强大工具,但不当使用会影响性能。 + +### 1. 避免过度嵌套 + +```csharp +// ✅ 好的做法:扁平化协程结构 +public IEnumerator<IYieldInstruction> LoadGameSequence() +{ + // 显示加载界面 + yield return ShowLoadingScreen(); + + // 加载配置 + yield return LoadConfiguration(); + + // 加载资源 + yield return LoadResources(); + + // 初始化系统 + yield return InitializeSystems(); + + // 隐藏加载界面 + yield return HideLoadingScreen(); +} + +private IEnumerator<IYieldInstruction> LoadConfiguration() +{ + var config = await ConfigManager.LoadAsync(); + ApplyConfiguration(config); + yield break; +} + +// ❌ 避免:深度嵌套 +public IEnumerator<IYieldInstruction> LoadGameSequence() +{ + yield return ShowLoadingScreen().Then(() => + { + return LoadConfiguration().Then(() => + { + return LoadResources().Then(() => + { + return InitializeSystems().Then(() => + { + return HideLoadingScreen(); + }); + }); + }); + }); +} +``` + +### 2. 协程池化 + +对于频繁启动的协程,考虑复用: + +```csharp +// ✅ 好的做法:复用协程逻辑 +public class EffectSystem : AbstractSystem +{ + private readonly Dictionary<string, IEnumerator<IYieldInstruction>> _effectCoroutines = new(); + + public CoroutineHandle PlayEffect(string effectId, Vector2 position) + { + // 复用协程逻辑 + return this.StartCoroutine(EffectCoroutine(effectId, position)); + } + + private IEnumerator<IYieldInstruction> EffectCoroutine(string effectId, Vector2 position) + { + var effect = CreateEffect(effectId, position); + + // 播放效果 + yield return new Delay(effect.Duration); + + // 清理效果 + DestroyEffect(effect); + } +} +``` + +### 3. 合理使用 WaitForFrames + +```csharp +// ✅ 好的做法:使用 WaitForFrames 分帧处理 +public IEnumerator<IYieldInstruction> ProcessLargeDataSet(List<Data> dataSet) +{ + const int batchSize = 100; + + for (int i = 0; i < dataSet.Count; i += batchSize) + { + int end = Math.Min(i + batchSize, dataSet.Count); + + // 处理一批数据 + for (int j = i; j < end; j++) + { + ProcessData(dataSet[j]); + } + + // 等待一帧,避免卡顿 + yield return new WaitOneFrame(); + } +} + +// ❌ 避免:一次性处理大量数据 +public void ProcessLargeDataSet(List<Data> dataSet) +{ + // 一次性处理所有数据,可能导致帧率下降 + foreach (var data in dataSet) + { + ProcessData(data); + } +} +``` + +### 4. 协程取消优化 + +```csharp +// ✅ 好的做法:及时取消不需要的协程 +public class AnimationController : AbstractSystem +{ + private CoroutineHandle _currentAnimation; + + public void PlayAnimation(string animationName) + { + // 取消当前动画 + if (_currentAnimation.IsValid) + { + this.StopCoroutine(_currentAnimation); + } + + // 播放新动画 + _currentAnimation = this.StartCoroutine(AnimationCoroutine(animationName)); + } + + private IEnumerator<IYieldInstruction> AnimationCoroutine(string animationName) + { + var animation = GetAnimation(animationName); + + while (!animation.IsComplete) + { + animation.Update(Time.DeltaTime); + yield return new WaitOneFrame(); + } + } +} +``` + +## 资源管理优化 + +高效的资源管理可以显著减少加载时间和内存占用。 + +### 1. 资源预加载 + +```csharp +// ✅ 好的做法:预加载常用资源 +public class ResourcePreloader : AbstractSystem +{ + private IResourceManager _resourceManager; + + protected override void OnInit() + { + _resourceManager = GetUtility<IResourceManager>(); + } + + public async Task PreloadCommonResources() + { + // 预加载 UI 资源 + await _resourceManager.PreloadAsync<Texture>("ui/button_normal"); + await _resourceManager.PreloadAsync<Texture>("ui/button_pressed"); + await _resourceManager.PreloadAsync<Texture>("ui/button_hover"); + + // 预加载音效 + await _resourceManager.PreloadAsync<AudioClip>("sfx/button_click"); + await _resourceManager.PreloadAsync<AudioClip>("sfx/button_hover"); + + // 预加载特效 + await _resourceManager.PreloadAsync<ParticleSystem>("effects/hit_effect"); + await _resourceManager.PreloadAsync<ParticleSystem>("effects/explosion"); + } +} +``` + +### 2. 异步加载 + +```csharp +// ✅ 好的做法:使用异步加载避免阻塞 +public class SceneLoader : AbstractSystem +{ + private IResourceManager _resourceManager; + + public async Task LoadSceneAsync(string sceneName) + { + // 显示加载进度 + var progress = 0f; + UpdateLoadingProgress(progress); + + // 异步加载场景资源 + var sceneData = await _resourceManager.LoadAsync<SceneData>($"scenes/{sceneName}"); + progress += 0.3f; + UpdateLoadingProgress(progress); + + // 异步加载场景依赖的资源 + await LoadSceneDependencies(sceneData); + progress += 0.5f; + UpdateLoadingProgress(progress); + + // 初始化场景 + await InitializeScene(sceneData); + progress = 1f; + UpdateLoadingProgress(progress); + } + + private async Task LoadSceneDependencies(SceneData sceneData) + { + var tasks = new List<Task>(); + + foreach (var dependency in sceneData.Dependencies) + { + tasks.Add(_resourceManager.LoadAsync<object>(dependency)); + } + + await Task.WhenAll(tasks); + } +} + +// ❌ 避免:同步加载阻塞主线程 +public class SceneLoader : AbstractSystem +{ + public void LoadScene(string sceneName) + { + // 同步加载会阻塞主线程,导致卡顿 + var sceneData = _resourceManager.Load<SceneData>($"scenes/{sceneName}"); + LoadSceneDependencies(sceneData); + InitializeScene(sceneData); + } +} +``` + +### 3. 资源引用计数 + +```csharp +// ✅ 好的做法:使用资源句柄管理引用 +public class EntityRenderer : AbstractSystem +{ + private readonly Dictionary<string, IResourceHandle<Texture>> _textureHandles = new(); + + public void LoadTexture(string entityId, string texturePath) + { + // 获取资源句柄 + var handle = _resourceManager.GetHandle<Texture>(texturePath); + if (handle != null) + { + _textureHandles[entityId] = handle; + } + } + + public void UnloadTexture(string entityId) + { + if (_textureHandles.TryGetValue(entityId, out var handle)) + { + // 释放句柄,自动管理引用计数 + handle.Dispose(); + _textureHandles.Remove(entityId); + } + } + + protected override void OnDestroy() + { + // 清理所有句柄 + foreach (var handle in _textureHandles.Values) + { + handle.Dispose(); + } + _textureHandles.Clear(); + } +} +``` + +### 4. 资源缓存策略 + +```csharp +// ✅ 好的做法:使用合适的释放策略 +public class GameResourceManager : AbstractSystem +{ + private IResourceManager _resourceManager; + + protected override void OnInit() + { + _resourceManager = GetUtility<IResourceManager>(); + + // 设置自动释放策略 + _resourceManager.SetReleaseStrategy(new AutoReleaseStrategy()); + } + + public void OnSceneChanged() + { + // 场景切换时卸载未使用的资源 + UnloadUnusedResources(); + } + + private void UnloadUnusedResources() + { + var loadedPaths = _resourceManager.GetLoadedResourcePaths().ToList(); + + foreach (var path in loadedPaths) + { + // 检查资源是否仍在使用 + if (!IsResourceInUse(path)) + { + _resourceManager.Unload(path); + } + } + } + + private bool IsResourceInUse(string path) + { + // 检查资源引用计数 + return false; // 实现具体逻辑 + } +} +``` + +## ECS 性能优化 + +Entity Component System (ECS) 是高性能游戏架构的关键。 + +### 1. 组件设计优化 + +```csharp +// ✅ 好的做法:使用值类型组件 +public struct Position +{ + public float X; + public float Y; + public float Z; +} + +public struct Velocity +{ + public float X; + public float Y; + public float Z; +} + +public struct Health +{ + public int Current; + public int Max; +} + +// ❌ 避免:使用引用类型组件 +public class Position +{ + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } +} +``` + +### 2. 查询优化 + +```csharp +// ✅ 好的做法:使用高效的查询 +public class MovementSystem : ArchSystemAdapter +{ + private QueryDescription _movementQuery; + + public override void Initialize() + { + // 预先构建查询 + _movementQuery = new QueryDescription() + .WithAll<Position, Velocity>() + .WithNone<Frozen>(); + } + + public override void Update(float deltaTime) + { + // 使用预构建的查询 + World.Query(in _movementQuery, (ref Position pos, ref Velocity vel) => + { + pos.X += vel.X * deltaTime; + pos.Y += vel.Y * deltaTime; + pos.Z += vel.Z * deltaTime; + }); + } +} + +// ❌ 避免:每帧构建查询 +public class MovementSystem : ArchSystemAdapter +{ + public override void Update(float deltaTime) + { + // 每帧都构建新查询,性能差 + var query = new QueryDescription() + .WithAll<Position, Velocity>() + .WithNone<Frozen>(); + + World.Query(in query, (ref Position pos, ref Velocity vel) => + { + pos.X += vel.X * deltaTime; + pos.Y += vel.Y * deltaTime; + pos.Z += vel.Z * deltaTime; + }); + } +} +``` + +### 3. 批量处理 + +```csharp +// ✅ 好的做法:批量处理实体 +public class DamageSystem : ArchSystemAdapter +{ + private readonly List<(EntityReference entity, int damage)> _pendingDamages = new(); + + public void QueueDamage(EntityReference entity, int damage) + { + _pendingDamages.Add((entity, damage)); + } + + public override void Update(float deltaTime) + { + if (_pendingDamages.Count == 0) + return; + + // 批量应用伤害 + foreach (var (entity, damage) in _pendingDamages) + { + if (World.IsAlive(entity)) + { + ref var health = ref World.Get<Health>(entity); + health.Current = Math.Max(0, health.Current - damage); + + if (health.Current == 0) + { + World.Destroy(entity); + } + } + } + + _pendingDamages.Clear(); + } +} +``` + +### 4. 避免装箱 + +```csharp +// ✅ 好的做法:避免装箱操作 +public struct EntityId : IEquatable<EntityId> +{ + public int Value; + + public bool Equals(EntityId other) => Value == other.Value; + public override bool Equals(object obj) => obj is EntityId other && Equals(other); + public override int GetHashCode() => Value; +} + +// 使用泛型避免装箱 +public class EntityCache<T> where T : struct +{ + private readonly Dictionary<EntityId, T> _cache = new(); + + public void Add(EntityId id, T data) + { + _cache[id] = data; // 无装箱 + } + + public bool TryGet(EntityId id, out T data) + { + return _cache.TryGetValue(id, out data); // 无装箱 + } +} + +// ❌ 避免:导致装箱的操作 +public class EntityCache +{ + private readonly Dictionary<int, object> _cache = new(); + + public void Add(int id, object data) + { + _cache[id] = data; // 值类型会装箱 + } +} +``` + +## 内存优化 + +减少内存分配和 GC 压力是性能优化的重要方面。 + +### 1. 使用 Span<T> + +```csharp +// ✅ 好的做法:使用 Span 避免分配 +public void ProcessData(ReadOnlySpan<byte> data) +{ + // 直接在栈上处理,无堆分配 + Span<int> results = stackalloc int[data.Length]; + + for (int i = 0; i < data.Length; i++) + { + results[i] = ProcessByte(data[i]); + } + + // 使用结果 + UseResults(results); +} + +// 解析字符串避免分配 +public bool TryParseValue(ReadOnlySpan<char> input, out int result) +{ + return int.TryParse(input, out result); +} + +// ❌ 避免:不必要的数组分配 +public void ProcessData(byte[] data) +{ + // 创建新数组,产生 GC + var results = new int[data.Length]; + + for (int i = 0; i < data.Length; i++) + { + results[i] = ProcessByte(data[i]); + } + + UseResults(results); +} +``` + +### 2. 结构体优化 + +```csharp +// ✅ 好的做法:使用 readonly struct +public readonly struct Vector2D +{ + public readonly float X; + public readonly float Y; + + public Vector2D(float x, float y) + { + X = x; + Y = y; + } + + public float Length() => MathF.Sqrt(X * X + Y * Y); + + public Vector2D Normalized() + { + var length = Length(); + return length > 0 ? new Vector2D(X / length, Y / length) : this; + } +} + +// ❌ 避免:可变结构体 +public struct Vector2D +{ + public float X { get; set; } + public float Y { get; set; } + + // 可变结构体可能导致意外的复制 +} +``` + +### 3. 避免闭包分配 + +```csharp +// ✅ 好的做法:避免闭包捕获 +public class EventProcessor : AbstractSystem +{ + private readonly Action<PlayerEvent> _cachedHandler; + + public EventProcessor() + { + // 缓存委托,避免每次分配 + _cachedHandler = HandlePlayerEvent; + } + + protected override void OnInit() + { + this.RegisterEvent(_cachedHandler); + } + + private void HandlePlayerEvent(PlayerEvent e) + { + ProcessEvent(e); + } +} + +// ❌ 避免:每次都创建新的闭包 +public class EventProcessor : AbstractSystem +{ + protected override void OnInit() + { + // 每次都创建新的委托和闭包 + this.RegisterEvent<PlayerEvent>(e => + { + ProcessEvent(e); + }); + } +} +``` + +### 4. 字符串优化 + +```csharp +// ✅ 好的做法:减少字符串分配 +public class Logger +{ + private readonly StringBuilder _sb = new(); + + public void LogFormat(string format, params object[] args) + { + _sb.Clear(); + _sb.AppendFormat(format, args); + Log(_sb.ToString()); + } + + // 使用字符串插值的优化版本 + public void LogInterpolated(ref DefaultInterpolatedStringHandler handler) + { + Log(handler.ToStringAndClear()); + } +} + +// 使用 string.Create 避免中间分配 +public string CreateEntityName(int id, string type) +{ + return string.Create(type.Length + 10, (id, type), (span, state) => + { + state.type.AsSpan().CopyTo(span); + span = span.Slice(state.type.Length); + span[0] = '_'; + state.id.TryFormat(span.Slice(1), out _); + }); +} + +// ❌ 避免:频繁的字符串拼接 +public string CreateEntityName(int id, string type) +{ + return type + "_" + id.ToString(); // 创建多个临时字符串 +} +``` + +## 最佳实践 + +### 1. 性能测试 + +```csharp +// ✅ 使用性能测试验证优化效果 +[TestFixture] +public class PerformanceTests +{ + [Test] + [Performance] + public void ObjectPool_Performance_Test() + { + var pool = new BulletPoolSystem(); + pool.Prewarm("bullet", 1000); + + Measure.Method(() => + { + var bullet = pool.Acquire("bullet"); + pool.Release("bullet", bullet); + }) + .WarmupCount(10) + .MeasurementCount(100) + .Run(); + } + + [Test] + public void CompareAllocationMethods() + { + // 测试对象池 + var poolTime = MeasureTime(() => + { + var pool = ArrayPool<int>.Shared; + var array = pool.Rent(1000); + pool.Return(array); + }); + + // 测试直接分配 + var allocTime = MeasureTime(() => + { + var array = new int[1000]; + }); + + Assert.Less(poolTime, allocTime, "Object pool should be faster"); + } + + private long MeasureTime(Action action) + { + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 10000; i++) + { + action(); + } + sw.Stop(); + return sw.ElapsedMilliseconds; + } +} +``` + +### 2. 性能监控 + +```csharp +// ✅ 实现性能监控系统 +public class PerformanceMonitor : AbstractSystem +{ + private readonly Dictionary<string, PerformanceMetric> _metrics = new(); + private float _updateInterval = 1.0f; + private float _timeSinceUpdate = 0f; + + public void Update(float deltaTime) + { + _timeSinceUpdate += deltaTime; + + if (_timeSinceUpdate >= _updateInterval) + { + UpdateMetrics(); + _timeSinceUpdate = 0f; + } + } + + public void RecordMetric(string name, float value) + { + if (!_metrics.TryGetValue(name, out var metric)) + { + metric = new PerformanceMetric(name); + _metrics[name] = metric; + } + + metric.AddSample(value); + } + + private void UpdateMetrics() + { + foreach (var metric in _metrics.Values) + { + Logger.Info($"{metric.Name}: Avg={metric.Average:F2}ms, " + + $"Min={metric.Min:F2}ms, Max={metric.Max:F2}ms"); + metric.Reset(); + } + } +} + +public class PerformanceMetric +{ + public string Name { get; } + public float Average => _count > 0 ? _sum / _count : 0; + public float Min { get; private set; } = float.MaxValue; + public float Max { get; private set; } = float.MinValue; + + private float _sum; + private int _count; + + public PerformanceMetric(string name) + { + Name = name; + } + + public void AddSample(float value) + { + _sum += value; + _count++; + Min = Math.Min(Min, value); + Max = Math.Max(Max, value); + } + + public void Reset() + { + _sum = 0; + _count = 0; + Min = float.MaxValue; + Max = float.MinValue; + } +} +``` + +### 3. 性能分析工具 + +使用性能分析工具定位瓶颈: + +- **Unity Profiler** - Unity 内置的性能分析工具 +- **Godot Profiler** - Godot 的性能监控工具 +- **dotTrace** - JetBrains 的 .NET 性能分析器 +- **PerfView** - Microsoft 的性能分析工具 + +### 4. 性能优化清单 + +在优化性能时,遵循以下清单: + +- [ ] 使用对象池减少 GC 压力 +- [ ] 正确管理事件订阅,避免内存泄漏 +- [ ] 使用结构体事件避免堆分配 +- [ ] 合理使用协程,避免过度嵌套 +- [ ] 异步加载资源,避免阻塞主线程 +- [ ] 使用 ECS 架构提高数据局部性 +- [ ] 使用 Span<T> 和 stackalloc 减少分配 +- [ ] 避免装箱操作 +- [ ] 缓存常用的委托和对象 +- [ ] 批量处理高频操作 +- [ ] 定期进行性能测试和监控 + +## 常见问题 + +### Q1: 什么时候应该使用对象池? + +**A**: 当满足以下条件时考虑使用对象池: + +- 对象创建成本高(如包含复杂初始化逻辑) +- 对象频繁创建和销毁(如子弹、特效粒子) +- 对象生命周期短暂但使用频繁 +- 需要减少 GC 压力 + +### Q2: 如何判断是否存在内存泄漏? + +**A**: 观察以下指标: + +- 内存使用持续增长,不会下降 +- GC 频率增加但内存不释放 +- 事件订阅数量持续增加 +- 对象池中的活跃对象数量异常 + +使用内存分析工具(如 dotMemory)定位泄漏源。 + +### Q3: 协程和 async/await 如何选择? + +**A**: 选择建议: + +- **协程** - 游戏逻辑、动画序列、分帧处理 +- **async/await** - I/O 操作、网络请求、文件读写 + +协程更适合游戏帧循环,async/await 更适合异步 I/O。 + +### Q4: 如何优化大量实体的性能? + +**A**: 使用 ECS 架构: + +- 使用值类型组件减少堆分配 +- 预构建查询避免每帧创建 +- 批量处理实体操作 +- 使用并行处理(如果支持) + +### Q5: 什么时候应该进行性能优化? + +**A**: 遵循以下原则: + +- **先测量,再优化** - 使用性能分析工具找到真正的瓶颈 +- **优先优化热点** - 集中优化占用时间最多的代码 +- **避免过早优化** - 在功能完成后再进行优化 +- **保持可读性** - 不要为了微小的性能提升牺牲代码可读性 + +### Q6: 如何减少 GC 压力? + +**A**: 采取以下措施: + +- 使用对象池复用对象 +- 使用值类型(struct)而非引用类型(class) +- 使用 Span<T> 和 stackalloc 进行栈分配 +- 避免闭包捕获和装箱操作 +- 缓存常用对象和委托 +- 使用 StringBuilder 而非字符串拼接 + +--- + +## 总结 + +性能优化是一个持续的过程,需要: + +- ✅ **测量优先** - 使用工具定位真正的瓶颈 +- ✅ **合理使用对象池** - 减少 GC 压力 +- ✅ **优化事件系统** - 避免内存泄漏和不必要的处理 +- ✅ **高效使用协程** - 避免过度嵌套和不必要的等待 +- ✅ **智能资源管理** - 异步加载和合理缓存 +- ✅ **ECS 架构优化** - 提高数据局部性和处理效率 +- ✅ **内存优化** - 减少分配,使用栈内存 +- ✅ **持续监控** - 建立性能监控系统 + +记住,性能优化要在可维护性和性能之间找到平衡,不要为了微小的性能提升而牺牲代码质量。 + +--- + +**文档版本**: 1.0.0 +**更新日期**: 2026-03-07 diff --git a/docs/zh-CN/contributing.md b/docs/zh-CN/contributing.md new file mode 100644 index 0000000..a2839b1 --- /dev/null +++ b/docs/zh-CN/contributing.md @@ -0,0 +1,839 @@ +# 贡献指南 + +欢迎为 GFramework 贡献代码!本指南将帮助你了解如何参与项目开发。 + +## 概述 + +GFramework 是一个开源的游戏开发框架,我们欢迎所有形式的贡献: + +- 报告 Bug 和提出功能建议 +- 提交代码修复和新功能 +- 改进文档和示例 +- 参与讨论和代码审查 + +## 行为准则 + +### 社区规范 + +我们致力于为所有贡献者提供友好、安全和包容的环境。参与本项目时,请遵守以下准则: + +- **尊重他人**:尊重不同的观点和经验 +- **建设性沟通**:提供有建设性的反馈,避免人身攻击 +- **协作精神**:帮助新贡献者融入社区 +- **专业态度**:保持专业和礼貌的交流方式 + +### 不可接受的行为 + +- 使用性别化语言或图像 +- 人身攻击或侮辱性评论 +- 骚扰行为(公开或私下) +- 未经许可发布他人的私人信息 +- 其他不道德或不专业的行为 + +## 如何贡献 + +### 报告问题 + +发现 Bug 或有功能建议时,请通过 GitHub Issues 提交: + +1. **搜索现有 Issue**:避免重复提交 +2. **使用清晰的标题**:简洁描述问题 +3. **提供详细信息**: + - Bug 报告:复现步骤、预期行为、实际行为、环境信息 + - 功能建议:使用场景、预期效果、可能的实现方案 + +**Bug 报告模板**: + +```markdown +**描述** +简要描述 Bug + +**复现步骤** +1. 执行操作 A +2. 执行操作 B +3. 观察到错误 + +**预期行为** +应该发生什么 + +**实际行为** +实际发生了什么 + +**环境信息** +- GFramework 版本: +- .NET 版本: +- 操作系统: +- Godot 版本(如适用): + +**附加信息** +日志、截图等 +``` + +### 提交 Pull Request + +#### 基本流程 + +1. **Fork 仓库**:在 GitHub 上 Fork 本项目 +2. **克隆到本地**: + ```bash + git clone https://github.com/your-username/GFramework.git + cd GFramework + ``` +3. **创建特性分支**: + ```bash + git checkout -b feature/your-feature-name + # 或 + git checkout -b fix/your-bug-fix + ``` +4. **进行开发**:编写代码、添加测试、更新文档 +5. **提交更改**:遵循提交规范(见下文) +6. **推送分支**: + ```bash + git push origin feature/your-feature-name + ``` +7. **创建 PR**:在 GitHub 上创建 Pull Request + +#### PR 要求 + +- **清晰的标题**:简洁描述变更内容 +- **详细的描述**: + - 变更的背景和动机 + - 实现方案说明 + - 测试验证结果 + - 相关 Issue 链接(如有) +- **代码质量**:通过所有 CI 检查 +- **测试覆盖**:为新功能添加测试 +- **文档更新**:更新相关文档 + +### 改进文档 + +文档改进同样重要: + +- **修正错误**:拼写、语法、技术错误 +- **补充示例**:添加代码示例和使用场景 +- **完善说明**:改进不清晰的描述 +- **翻译工作**:帮助翻译文档(如需要) + +文档位于 `docs/` 目录,使用 Markdown 格式编写。 + +## 开发环境设置 + +### 前置要求 + +- **.NET SDK**:8.0、9.0 或 10.0 +- **Git**:版本控制工具 +- **IDE**(推荐): + - Visual Studio 2022+ + - JetBrains Rider + - Visual Studio Code + C# Dev Kit + +### 克隆仓库 + +```bash +# 克隆你 Fork 的仓库 +git clone https://github.com/your-username/GFramework.git +cd GFramework + +# 添加上游仓库 +git remote add upstream https://github.com/GeWuYou/GFramework.git +``` + +### 安装依赖 + +```bash +# 恢复 NuGet 包 +dotnet restore + +# 恢复 .NET 本地工具 +dotnet tool restore +``` + +### 构建项目 + +```bash +# 构建所有项目 +dotnet build + +# 构建特定配置 +dotnet build -c Release +``` + +### 运行测试 + +```bash +# 运行所有测试 +dotnet test + +# 运行特定测试项目 +dotnet test GFramework.Core.Tests +dotnet test GFramework.SourceGenerators.Tests + +# 生成测试覆盖率报告 +dotnet test --collect:"XPlat Code Coverage" +``` + +### 验证代码质量 + +项目使用 MegaLinter 进行代码质量检查: + +```bash +# 本地运行 MegaLinter(需要 Docker) +docker run --rm -v $(pwd):/tmp/lint oxsecurity/megalinter:v9 + +# 或使用 CI 流程验证 +git push origin your-branch +``` + +## 代码规范 + +### 命名规范 + +遵循 C# 标准命名约定: + +- **类、接口、方法**:PascalCase + ```csharp + public class PlayerController { } + public interface IEventBus { } + public void ProcessInput() { } + ``` + +- **私有字段**:_camelCase(下划线前缀) + ```csharp + private int _health; + private readonly ILogger _logger; + ``` + +- **参数、局部变量**:camelCase + ```csharp + public void SetHealth(int newHealth) + { + var oldHealth = _health; + _health = newHealth; + } + ``` + +- **常量**:PascalCase + ```csharp + public const int MaxPlayers = 4; + private const string DefaultName = "Player"; + ``` + +- **接口**:I 前缀 + ```csharp + public interface IArchitecture { } + public interface ICommand<TInput> { } + ``` + +### 代码风格 + +- **缩进**:4 个空格(不使用 Tab) +- **大括号**:Allman 风格(独占一行) + ```csharp + if (condition) + { + DoSomething(); + } + ``` + +- **using 指令**:文件顶部,按字母顺序排列 + ```csharp + using System; + using System.Collections.Generic; + using GFramework.Core.Abstractions; + ``` + +- **空行**: + - 命名空间后空一行 + - 类成员之间空一行 + - 逻辑块之间适当空行 + +- **行长度**:建议不超过 120 字符 + +### 注释规范 + +#### XML 文档注释 + +所有公共 API 必须包含 XML 文档注释: + +```csharp +/// <summary> +/// 架构基类,提供系统、模型、工具等组件的注册与管理功能。 +/// </summary> +/// <typeparam name="TModel">模型类型</typeparam> +/// <param name="configuration">架构配置</param> +/// <returns>注册的模型实例</returns> +/// <exception cref="ArgumentNullException">当 model 为 null 时抛出</exception> +public TModel RegisterModel<TModel>(TModel model) where TModel : IModel +{ + // 实现代码 +} +``` + +#### 代码注释 + +- **何时添加注释**: + - 复杂的算法逻辑 + - 非显而易见的设计决策 + - 临时解决方案(使用 TODO 或 HACK 标记) + - 性能关键代码的优化说明 + +- **注释风格**: + ```csharp + // 单行注释使用双斜杠 + + // 多行注释可以使用多个单行注释 + // 每行都以双斜杠开始 + + /* 或使用块注释 + * 适用于较长的说明 + */ + ``` + +- **避免无用注释**: + ```csharp + // 不好:注释重复代码内容 + // 设置健康值为 100 + health = 100; + + // 好:解释为什么这样做 + // 初始化时设置满血,避免首次战斗时的边界情况 + health = MaxHealth; + ``` + +### 设计原则 + +- **SOLID 原则**:遵循面向对象设计原则 +- **依赖注入**:优先使用构造函数注入 +- **接口隔离**:定义小而专注的接口 +- **不可变性**:优先使用 `readonly` 和不可变类型 +- **异步编程**:I/O 操作使用 `async`/`await` + +## 提交规范 + +### Commit 消息格式 + +使用 Conventional Commits 规范: + +``` +(): + + + +