mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 02:24:30 +08:00
- 将所有小写的命名空间导入更正为首字母大写格式 - 统一 GFramework 框架的命名空间引用规范 - 修复 core、ecs、godot 等模块的命名空间导入错误 - 标准化文档示例代码中的 using 语句格式 - 确保所有文档中的命名空间引用保持一致性 - 更新 global using 语句以匹配正确的命名空间格式
3443 lines
92 KiB
Markdown
3443 lines
92 KiB
Markdown
# 架构设计模式指南
|
||
|
||
> 全面介绍 GFramework 中的架构设计模式,帮助你构建清晰、可维护、可扩展的游戏架构。
|
||
|
||
## 📋 目录
|
||
|
||
- [概述](#概述)
|
||
- [MVC 模式](#mvc-模式)
|
||
- [MVVM 模式](#mvvm-模式)
|
||
- [命令模式](#命令模式)
|
||
- [查询模式](#查询模式)
|
||
- [事件驱动模式](#事件驱动模式)
|
||
- [依赖注入模式](#依赖注入模式)
|
||
- [服务定位器模式](#服务定位器模式)
|
||
- [对象池模式](#对象池模式)
|
||
- [状态模式](#状态模式)
|
||
- [设计原则](#设计原则)
|
||
- [架构分层](#架构分层)
|
||
- [依赖管理](#依赖管理)
|
||
- [事件系统设计](#事件系统设计)
|
||
- [模块化架构](#模块化架构)
|
||
- [错误处理策略](#错误处理策略)
|
||
- [测试策略](#测试策略)
|
||
- [重构指南](#重构指南)
|
||
- [模式选择与组合](#模式选择与组合)
|
||
- [常见问题](#常见问题)
|
||
|
||
## 概述
|
||
|
||
架构设计模式是经过验证的解决方案,用于解决软件开发中的常见问题。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 = this.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
|
||
this.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 = this.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
|
||
});
|
||
}
|
||
}
|
||
|
||
// 使用命令
|
||
[ContextAware]
|
||
public partial class ShopController : IController
|
||
{
|
||
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
|
||
};
|
||
}
|
||
}
|
||
|
||
// 使用查询
|
||
[ContextAware]
|
||
public partial class CharacterPanelController : IController
|
||
{
|
||
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 监听事件
|
||
[ContextAware]
|
||
public partial class UIController : IController
|
||
{
|
||
private IUnRegisterList _unregisterList = new UnRegisterList();
|
||
|
||
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
|
||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||
|
||
// 使用 OrEvent 组合多个事件
|
||
[ContextAware]
|
||
public partial 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 中使用
|
||
[ContextAware]
|
||
public partial class MenuController : IController
|
||
{
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 使用状态机
|
||
[ContextAware]
|
||
public partial class GameController : IController
|
||
{
|
||
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. **异步操作使用异步状态**:避免阻塞主线程
|
||
|
||
## 设计原则
|
||
|
||
### 1. 单一职责原则 (SRP)
|
||
|
||
确保每个类只负责一个功能领域:
|
||
|
||
```csharp
|
||
// ✅ 好的做法:职责单一
|
||
public class PlayerMovementController : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<PlayerInputEvent>(OnPlayerInput);
|
||
}
|
||
|
||
private void OnPlayerInput(PlayerInputEvent e)
|
||
{
|
||
// 只负责移动逻辑
|
||
ProcessMovement(e.Direction);
|
||
}
|
||
|
||
private void ProcessMovement(Vector2 direction)
|
||
{
|
||
// 移动相关的业务逻辑
|
||
}
|
||
}
|
||
|
||
public class PlayerCombatController : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<AttackInputEvent>(OnAttackInput);
|
||
}
|
||
|
||
private void OnAttackInput(AttackInputEvent e)
|
||
{
|
||
// 只负责战斗逻辑
|
||
ProcessAttack(e.Target);
|
||
}
|
||
|
||
private void ProcessAttack(Entity target)
|
||
{
|
||
// 战斗相关的业务逻辑
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:职责混乱
|
||
public class PlayerController : AbstractSystem
|
||
{
|
||
private void OnPlayerInput(PlayerInputEvent e)
|
||
{
|
||
// 移动逻辑
|
||
ProcessMovement(e.Direction);
|
||
|
||
// 战斗逻辑
|
||
if (e.IsAttacking)
|
||
{
|
||
ProcessAttack(e.Target);
|
||
}
|
||
|
||
// UI逻辑
|
||
UpdateHealthBar();
|
||
|
||
// 音效逻辑
|
||
PlaySoundEffect();
|
||
|
||
// 存档逻辑
|
||
SaveGame();
|
||
|
||
// 职责太多,难以维护
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 开闭原则 (OCP)
|
||
|
||
设计应该对扩展开放,对修改封闭:
|
||
|
||
```csharp
|
||
// ✅ 好的做法:使用接口和策略模式
|
||
public interface IWeaponStrategy
|
||
{
|
||
void Attack(Entity attacker, Entity target);
|
||
int CalculateDamage(Entity attacker, Entity target);
|
||
}
|
||
|
||
public class SwordWeaponStrategy : IWeaponStrategy
|
||
{
|
||
public void Attack(Entity attacker, Entity target)
|
||
{
|
||
var damage = CalculateDamage(attacker, target);
|
||
target.TakeDamage(damage);
|
||
PlaySwingAnimation();
|
||
}
|
||
|
||
public int CalculateDamage(Entity attacker, Entity target)
|
||
{
|
||
return attacker.Strength + GetSwordBonus() - target.Armor;
|
||
}
|
||
}
|
||
|
||
public class MagicWeaponStrategy : IWeaponStrategy
|
||
{
|
||
public void Attack(Entity attacker, Entity target)
|
||
{
|
||
var damage = CalculateDamage(attacker, target);
|
||
target.TakeDamage(damage);
|
||
CastMagicEffect();
|
||
}
|
||
|
||
public int CalculateDamage(Entity attacker, Entity target)
|
||
{
|
||
return attacker.Intelligence * 2 + GetMagicBonus() - target.MagicResistance;
|
||
}
|
||
}
|
||
|
||
public class CombatSystem : AbstractSystem
|
||
{
|
||
private readonly Dictionary<WeaponType, IWeaponStrategy> _weaponStrategies;
|
||
|
||
public CombatSystem()
|
||
{
|
||
_weaponStrategies = new()
|
||
{
|
||
{ WeaponType.Sword, new SwordWeaponStrategy() },
|
||
{ WeaponType.Magic, new MagicWeaponStrategy() }
|
||
};
|
||
}
|
||
|
||
public void Attack(Entity attacker, Entity target)
|
||
{
|
||
var weaponType = attacker.EquippedWeapon.Type;
|
||
|
||
if (_weaponStrategies.TryGetValue(weaponType, out var strategy))
|
||
{
|
||
strategy.Attack(attacker, target);
|
||
}
|
||
}
|
||
|
||
// 添加新武器类型时,只需要添加新的策略,不需要修改现有代码
|
||
public void RegisterWeaponStrategy(WeaponType type, IWeaponStrategy strategy)
|
||
{
|
||
_weaponStrategies[type] = strategy;
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:需要修改现有代码来扩展
|
||
public class CombatSystem : AbstractSystem
|
||
{
|
||
public void Attack(Entity attacker, Entity target)
|
||
{
|
||
var weaponType = attacker.EquippedWeapon.Type;
|
||
|
||
switch (weaponType)
|
||
{
|
||
case WeaponType.Sword:
|
||
// 剑的攻击逻辑
|
||
break;
|
||
case WeaponType.Bow:
|
||
// 弓的攻击逻辑
|
||
break;
|
||
default:
|
||
throw new NotSupportedException($"Weapon type {weaponType} not supported");
|
||
}
|
||
|
||
// 添加新武器类型时需要修改这里的 switch 语句
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 依赖倒置原则 (DIP)
|
||
|
||
高层模块不应该依赖低层模块,两者都应该依赖抽象:
|
||
|
||
```csharp
|
||
// ✅ 好的做法:依赖抽象
|
||
public interface IDataStorage
|
||
{
|
||
Task SaveAsync<T>(string key, T data);
|
||
Task<T> LoadAsync<T>(string key, T defaultValue = default);
|
||
Task<bool> ExistsAsync(string key);
|
||
}
|
||
|
||
public class FileStorage : IDataStorage
|
||
{
|
||
public async Task SaveAsync<T>(string key, T data)
|
||
{
|
||
var json = JsonConvert.SerializeObject(data);
|
||
await File.WriteAllTextAsync(GetFilePath(key), json);
|
||
}
|
||
|
||
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<T>(json);
|
||
}
|
||
|
||
public async Task<bool> ExistsAsync(string key)
|
||
{
|
||
return File.Exists(GetFilePath(key));
|
||
}
|
||
|
||
private string GetFilePath(string key)
|
||
{
|
||
return $"saves/{key}.json";
|
||
}
|
||
}
|
||
|
||
public class CloudStorage : IDataStorage
|
||
{
|
||
public async Task SaveAsync<T>(string key, T data)
|
||
{
|
||
// 云存储实现
|
||
await UploadToCloud(key, data);
|
||
}
|
||
|
||
public async Task<T> LoadAsync<T>(string key, T defaultValue = default)
|
||
{
|
||
// 云存储实现
|
||
return await DownloadFromCloud<T>(key, defaultValue);
|
||
}
|
||
|
||
public async Task<bool> ExistsAsync(string key)
|
||
{
|
||
// 云存储实现
|
||
return await CheckCloudExists(key);
|
||
}
|
||
}
|
||
|
||
// 高层模块依赖抽象
|
||
public class SaveSystem : AbstractSystem
|
||
{
|
||
private readonly IDataStorage _storage;
|
||
|
||
public SaveSystem(IDataStorage storage)
|
||
{
|
||
_storage = storage;
|
||
}
|
||
|
||
public async Task SaveGameAsync(SaveData data)
|
||
{
|
||
await _storage.SaveAsync("current_save", data);
|
||
}
|
||
|
||
public async Task<SaveData> LoadGameAsync()
|
||
{
|
||
return await _storage.LoadAsync<SaveData>("current_save");
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:依赖具体实现
|
||
public class SaveSystem : AbstractSystem
|
||
{
|
||
private readonly FileStorage _storage; // 直接依赖具体实现
|
||
|
||
public SaveSystem()
|
||
{
|
||
_storage = new FileStorage(); // 硬编码依赖
|
||
}
|
||
|
||
// 无法轻松切换到其他存储方式
|
||
}
|
||
```
|
||
|
||
## 架构分层
|
||
|
||
### 1. 清晰的层次结构
|
||
|
||
```csharp
|
||
// ✅ 好的做法:清晰的分层架构
|
||
namespace Game.Models
|
||
{
|
||
// 数据层:只负责存储状态
|
||
public class PlayerModel : AbstractModel
|
||
{
|
||
public BindableProperty<int> Health { get; } = new(100);
|
||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
||
public BindableProperty<Vector2> Position { get; } = new(Vector2.Zero);
|
||
public BindableProperty<PlayerState> State { get; } = new(PlayerState.Idle);
|
||
|
||
protected override void OnInit()
|
||
{
|
||
// 只处理数据相关的逻辑
|
||
Health.Register(OnHealthChanged);
|
||
}
|
||
|
||
private void OnHealthChanged(int newHealth)
|
||
{
|
||
if (newHealth <= 0)
|
||
{
|
||
State.Value = PlayerState.Dead;
|
||
SendEvent(new PlayerDeathEvent());
|
||
}
|
||
}
|
||
}
|
||
|
||
public enum PlayerState
|
||
{
|
||
Idle,
|
||
Moving,
|
||
Attacking,
|
||
Dead
|
||
}
|
||
}
|
||
|
||
namespace Game.Systems
|
||
{
|
||
// 业务逻辑层:处理游戏逻辑
|
||
public class PlayerMovementSystem : AbstractSystem
|
||
{
|
||
private PlayerModel _playerModel;
|
||
private GameModel _gameModel;
|
||
|
||
protected override void OnInit()
|
||
{
|
||
_playerModel = GetModel<PlayerModel>();
|
||
_gameModel = GetModel<GameModel>();
|
||
|
||
this.RegisterEvent<PlayerInputEvent>(OnPlayerInput);
|
||
}
|
||
|
||
private void OnPlayerInput(PlayerInputEvent e)
|
||
{
|
||
if (_gameModel.State.Value != GameState.Playing)
|
||
return;
|
||
|
||
if (_playerModel.State.Value == PlayerState.Dead)
|
||
return;
|
||
|
||
// 处理移动逻辑
|
||
ProcessMovement(e.Direction);
|
||
}
|
||
|
||
private void ProcessMovement(Vector2 direction)
|
||
{
|
||
if (direction != Vector2.Zero)
|
||
{
|
||
_playerModel.Position.Value += direction.Normalized() * GetMovementSpeed();
|
||
_playerModel.State.Value = PlayerState.Moving;
|
||
|
||
SendEvent(new PlayerMovedEvent {
|
||
NewPosition = _playerModel.Position.Value,
|
||
Direction = direction
|
||
});
|
||
}
|
||
else
|
||
{
|
||
_playerModel.State.Value = PlayerState.Idle;
|
||
}
|
||
}
|
||
|
||
private float GetMovementSpeed()
|
||
{
|
||
// 从玩家属性或其他地方获取速度
|
||
return 5.0f;
|
||
}
|
||
}
|
||
}
|
||
|
||
namespace Game.Controllers
|
||
{
|
||
// 控制层:连接用户输入和业务逻辑
|
||
[ContextAware]
|
||
public partial class PlayerController : Node, IController
|
||
{
|
||
private PlayerModel _playerModel;
|
||
|
||
public override void _Ready()
|
||
{
|
||
_playerModel = this.GetModel<PlayerModel>();
|
||
|
||
// 监听用户输入
|
||
SetProcessInput(true);
|
||
|
||
// 监听数据变化,更新UI
|
||
_playerModel.Health.Register(UpdateHealthUI);
|
||
_playerModel.Position.Register(UpdatePosition);
|
||
}
|
||
|
||
public override void _Input(InputEvent @event)
|
||
{
|
||
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
|
||
{
|
||
var direction = GetInputDirection(keyEvent);
|
||
this.SendEvent(new PlayerInputEvent { Direction = direction });
|
||
}
|
||
}
|
||
|
||
private void UpdateHealthUI(int health)
|
||
{
|
||
// 更新UI显示
|
||
var healthBar = GetNode<ProgressBar>("UI/HealthBar");
|
||
healthBar.Value = (float)health / _playerModel.MaxHealth.Value * 100;
|
||
}
|
||
|
||
private void UpdatePosition(Vector2 position)
|
||
{
|
||
// 更新玩家位置
|
||
Position = position;
|
||
}
|
||
|
||
private Vector2 GetInputDirection(InputEventKey keyEvent)
|
||
{
|
||
return keyEvent.Keycode switch
|
||
{
|
||
Key.W => Vector2.Up,
|
||
Key.S => Vector2.Down,
|
||
Key.A => Vector2.Left,
|
||
Key.D => Vector2.Right,
|
||
_ => Vector2.Zero
|
||
};
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 避免层次混乱
|
||
|
||
```csharp
|
||
// ❌ 避免:层次混乱
|
||
public class PlayerController : Node, IController
|
||
{
|
||
// 混合了数据层、业务逻辑层和控制层的职责
|
||
public BindableProperty<int> Health { get; } = new(100); // 数据层职责
|
||
|
||
public override void _Input(InputEvent @event) // 控制层职责
|
||
{
|
||
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
|
||
{
|
||
if (keyEvent.Keycode == Key.W)
|
||
{
|
||
Position += Vector2.Up * MovementSpeed; // 业务逻辑层职责
|
||
}
|
||
|
||
if (keyEvent.Keycode == Key.Space)
|
||
{
|
||
Health -= 10; // 业务逻辑层职责
|
||
PlaySoundEffect(); // 业务逻辑层职责
|
||
}
|
||
}
|
||
}
|
||
|
||
// 这样会导致代码难以测试和维护
|
||
}
|
||
```
|
||
|
||
## 依赖管理
|
||
|
||
### 1. 构造函数注入
|
||
|
||
```csharp
|
||
// ✅ 好的做法:构造函数注入
|
||
public class PlayerCombatSystem : AbstractSystem
|
||
{
|
||
private readonly PlayerModel _playerModel;
|
||
private readonly IWeaponService _weaponService;
|
||
private readonly ISoundService _soundService;
|
||
private readonly IEffectService _effectService;
|
||
|
||
// 通过构造函数注入依赖
|
||
public PlayerCombatSystem(
|
||
PlayerModel playerModel,
|
||
IWeaponService weaponService,
|
||
ISoundService soundService,
|
||
IEffectService effectService)
|
||
{
|
||
_playerModel = playerModel;
|
||
_weaponService = weaponService;
|
||
_soundService = soundService;
|
||
_effectService = effectService;
|
||
}
|
||
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<AttackEvent>(OnAttack);
|
||
}
|
||
|
||
private void OnAttack(AttackEvent e)
|
||
{
|
||
var weapon = _weaponService.GetEquippedWeapon(_playerModel);
|
||
var damage = _weaponService.CalculateDamage(weapon, e.Target);
|
||
|
||
e.Target.TakeDamage(damage);
|
||
_soundService.PlayAttackSound(weapon.Type);
|
||
_effectService.PlayAttackEffect(_playerModel.Position, weapon.Type);
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:依赖注入容器
|
||
public class PlayerCombatSystem : AbstractSystem
|
||
{
|
||
private PlayerModel _playerModel;
|
||
private IWeaponService _weaponService;
|
||
private ISoundService _soundService;
|
||
private IEffectService _effectService;
|
||
|
||
protected override void OnInit()
|
||
{
|
||
// 在运行时获取依赖,难以测试
|
||
_playerModel = GetModel<PlayerModel>();
|
||
_weaponService = GetService<IWeaponService>();
|
||
_soundService = GetService<ISoundService>();
|
||
_effectService = GetService<IEffectService>();
|
||
}
|
||
|
||
// 测试时难以模拟依赖
|
||
}
|
||
```
|
||
|
||
### 2. 接口隔离
|
||
|
||
```csharp
|
||
// ✅ 好的做法:小而专注的接口
|
||
public interface IMovementController
|
||
{
|
||
void Move(Vector2 direction);
|
||
void Stop();
|
||
bool CanMove();
|
||
}
|
||
|
||
public interface ICombatController
|
||
{
|
||
void Attack(Entity target);
|
||
void Defend();
|
||
bool CanAttack();
|
||
}
|
||
|
||
public interface IUIController
|
||
{
|
||
void ShowHealthBar();
|
||
void HideHealthBar();
|
||
void UpdateHealthDisplay(int currentHealth, int maxHealth);
|
||
}
|
||
|
||
public class PlayerController : Node, IMovementController, ICombatController, IUIController
|
||
{
|
||
// 实现各个接口,职责清晰
|
||
}
|
||
|
||
// ❌ 避免:大而全的接口
|
||
public interface IPlayerController
|
||
{
|
||
void Move(Vector2 direction);
|
||
void Stop();
|
||
void Attack(Entity target);
|
||
void Defend();
|
||
void ShowHealthBar();
|
||
void HideHealthBar();
|
||
void UpdateHealthDisplay(int currentHealth, int maxHealth);
|
||
void SaveGame();
|
||
void LoadGame();
|
||
void Respawn();
|
||
void PlayAnimation(string animationName);
|
||
void StopAnimation();
|
||
// ... 更多方法,接口过于庞大
|
||
}
|
||
```
|
||
|
||
## 事件系统设计
|
||
|
||
### 1. 事件命名和结构
|
||
|
||
```csharp
|
||
// ✅ 好的做法:清晰的事件命名和结构
|
||
public struct PlayerHealthChangedEvent
|
||
{
|
||
public int PreviousHealth { get; }
|
||
public int NewHealth { get; }
|
||
public int MaxHealth { get; }
|
||
public Vector3 DamagePosition { get; }
|
||
public DamageType DamageType { get; }
|
||
}
|
||
|
||
public struct PlayerDiedEvent
|
||
{
|
||
public Vector3 DeathPosition { get; }
|
||
public string CauseOfDeath { get; }
|
||
public TimeSpan SurvivalTime { get; }
|
||
}
|
||
|
||
public struct WeaponEquippedEvent
|
||
{
|
||
public string PlayerId { get; }
|
||
public WeaponType WeaponType { get; }
|
||
public string WeaponId { get; }
|
||
}
|
||
|
||
// ❌ 避免:模糊的事件命名和结构
|
||
public struct PlayerEvent
|
||
{
|
||
public EventType Type { get; }
|
||
public object Data { get; } // 类型不安全
|
||
public Dictionary<string, object> Properties { get; } // 难以理解
|
||
}
|
||
```
|
||
|
||
### 2. 事件处理职责
|
||
|
||
```csharp
|
||
// ✅ 好的做法:单一职责的事件处理
|
||
public class UIHealthBarController : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<PlayerHealthChangedEvent>(OnPlayerHealthChanged);
|
||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||
}
|
||
|
||
private void OnPlayerHealthChanged(PlayerHealthChangedEvent e)
|
||
{
|
||
UpdateHealthBar(e.NewHealth, e.MaxHealth);
|
||
|
||
if (e.NewHealth < e.PreviousHealth)
|
||
{
|
||
ShowDamageEffect(e.DamagePosition, e.PreviousHealth - e.NewHealth);
|
||
}
|
||
}
|
||
|
||
private void OnPlayerDied(PlayerDiedEvent e)
|
||
{
|
||
HideHealthBar();
|
||
ShowDeathScreen(e.CauseOfDeath);
|
||
}
|
||
}
|
||
|
||
public class AudioController : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<PlayerHealthChangedEvent>(OnPlayerHealthChanged);
|
||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||
}
|
||
|
||
private void OnPlayerHealthChanged(PlayerHealthChangedEvent e)
|
||
{
|
||
if (e.NewHealth < e.PreviousHealth)
|
||
{
|
||
PlayHurtSound(e.DamageType);
|
||
}
|
||
}
|
||
|
||
private void OnPlayerDied(PlayerDiedEvent e)
|
||
{
|
||
PlayDeathSound();
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:一个处理器处理多种不相关的事件
|
||
public class PlayerEventHandler : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<PlayerHealthChangedEvent>(OnPlayerHealthChanged);
|
||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||
this.RegisterEvent<WeaponEquippedEvent>(OnWeaponEquipped);
|
||
this.RegisterEvent<LevelUpEvent>(OnLevelUp);
|
||
// 注册太多事件,职责混乱
|
||
}
|
||
|
||
private void OnPlayerHealthChanged(PlayerHealthChangedEvent e)
|
||
{
|
||
UpdateUI(); // UI职责
|
||
PlayAudio(); // 音频职责
|
||
SaveStatistics(); // 存档职责
|
||
UpdateAchievements(); // 成就系统职责
|
||
// 一个事件处理器承担太多职责
|
||
}
|
||
}
|
||
```
|
||
|
||
## 模块化架构
|
||
|
||
### 1. 模块边界清晰
|
||
|
||
```csharp
|
||
// ✅ 好的做法:清晰的模块边界
|
||
public class AudioModule : AbstractModule
|
||
{
|
||
// 模块只负责音频相关的功能
|
||
public override void Install(IArchitecture architecture)
|
||
{
|
||
architecture.RegisterSystem(new AudioSystem());
|
||
architecture.RegisterSystem(new MusicSystem());
|
||
architecture.RegisterUtility(new AudioUtility());
|
||
}
|
||
}
|
||
|
||
public class InputModule : AbstractModule
|
||
{
|
||
// 模块只负责输入相关的功能
|
||
public override void Install(IArchitecture architecture)
|
||
{
|
||
architecture.RegisterSystem(new InputSystem());
|
||
architecture.RegisterSystem(new InputMappingSystem());
|
||
architecture.RegisterUtility(new InputUtility());
|
||
}
|
||
}
|
||
|
||
public class UIModule : AbstractModule
|
||
{
|
||
// 模块只负责UI相关的功能
|
||
public override void Install(IArchitecture architecture)
|
||
{
|
||
architecture.RegisterSystem(new UISystem());
|
||
architecture.RegisterSystem(new HUDSystem());
|
||
architecture.RegisterSystem(new MenuSystem());
|
||
architecture.RegisterUtility(new UIUtility());
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:模块职责混乱
|
||
public class GameModule : AbstractModule
|
||
{
|
||
public override void Install(IArchitecture architecture)
|
||
{
|
||
// 一个模块包含所有功能
|
||
architecture.RegisterSystem(new AudioSystem()); // 音频
|
||
architecture.RegisterSystem(new InputSystem()); // 输入
|
||
architecture.RegisterSystem(new UISystem()); // UI
|
||
architecture.RegisterSystem(new CombatSystem()); // 战斗
|
||
architecture.RegisterSystem(new InventorySystem()); // 背包
|
||
architecture.RegisterSystem(new QuestSystem()); // 任务
|
||
// 模块过于庞大,难以维护
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 模块间通信
|
||
|
||
```csharp
|
||
// ✅ 好的做法:通过事件进行模块间通信
|
||
public class AudioModule : AbstractModule
|
||
{
|
||
public override void Install(IArchitecture architecture)
|
||
{
|
||
architecture.RegisterSystem(new AudioSystem());
|
||
}
|
||
}
|
||
|
||
public class AudioSystem : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
// 监听其他模块发送的事件
|
||
this.RegisterEvent<PlayerAttackEvent>(OnPlayerAttack);
|
||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||
this.RegisterEvent<WeaponEquippedEvent>(OnWeaponEquipped);
|
||
}
|
||
|
||
private void OnPlayerAttack(PlayerAttackEvent e)
|
||
{
|
||
PlayAttackSound(e.WeaponType);
|
||
}
|
||
|
||
private void OnPlayerDied(PlayerDiedEvent e)
|
||
{
|
||
PlayDeathSound();
|
||
}
|
||
|
||
private void OnWeaponEquipped(WeaponEquippedEvent e)
|
||
{
|
||
PlayEquipSound(e.WeaponType);
|
||
}
|
||
}
|
||
|
||
public class CombatModule : AbstractModule
|
||
{
|
||
public override void Install(IArchitecture architecture)
|
||
{
|
||
architecture.RegisterSystem(new CombatSystem());
|
||
}
|
||
}
|
||
|
||
public class CombatSystem : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<AttackInputEvent>(OnAttackInput);
|
||
}
|
||
|
||
private void OnAttackInput(AttackInputEvent e)
|
||
{
|
||
ProcessAttack(e);
|
||
|
||
// 发送事件通知其他模块
|
||
SendEvent(new PlayerAttackEvent {
|
||
PlayerId = e.PlayerId,
|
||
WeaponType = GetPlayerWeaponType(e.PlayerId)
|
||
});
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:模块间直接依赖
|
||
public class CombatSystem : AbstractSystem
|
||
{
|
||
private AudioSystem _audioSystem; // 直接依赖其他模块
|
||
|
||
protected override void OnInit()
|
||
{
|
||
// 直接获取其他模块的系统
|
||
_audioSystem = GetSystem<AudioSystem>();
|
||
}
|
||
|
||
private void OnAttackInput(AttackInputEvent e)
|
||
{
|
||
ProcessAttack(e);
|
||
|
||
// 直接调用其他模块的方法
|
||
_audioSystem.PlayAttackSound(weaponType);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 错误处理策略
|
||
|
||
### 1. 异常处理层次
|
||
|
||
```csharp
|
||
// ✅ 好的做法:分层异常处理
|
||
public class GameApplicationException : Exception
|
||
{
|
||
public string ErrorCode { get; }
|
||
public Dictionary<string, object> Context { get; }
|
||
|
||
public GameApplicationException(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 PlayerException : GameApplicationException
|
||
{
|
||
public PlayerException(string message, string errorCode,
|
||
Dictionary<string, object> context = null, Exception innerException = null)
|
||
: base(message, errorCode, context, innerException)
|
||
{
|
||
}
|
||
}
|
||
|
||
public class InventoryException : GameApplicationException
|
||
{
|
||
public InventoryException(string message, string errorCode,
|
||
Dictionary<string, object> context = null, Exception innerException = null)
|
||
: base(message, errorCode, context, innerException)
|
||
{
|
||
}
|
||
}
|
||
|
||
// 在系统中的使用
|
||
public class PlayerInventorySystem : AbstractSystem
|
||
{
|
||
public void AddItem(string playerId, Item item)
|
||
{
|
||
try
|
||
{
|
||
ValidateItem(item);
|
||
CheckInventorySpace(playerId, item);
|
||
|
||
AddItemToInventory(playerId, item);
|
||
|
||
SendEvent(new ItemAddedEvent { PlayerId = playerId, Item = item });
|
||
}
|
||
catch (ItemValidationException ex)
|
||
{
|
||
throw new InventoryException(
|
||
$"Failed to add item {item.Id} to player {playerId}",
|
||
"ITEM_VALIDATION_FAILED",
|
||
new Dictionary<string, object>
|
||
{
|
||
["playerId"] = playerId,
|
||
["itemId"] = item.Id,
|
||
["validationError"] = ex.Message
|
||
},
|
||
ex
|
||
);
|
||
}
|
||
catch (InventoryFullException ex)
|
||
{
|
||
throw new InventoryException(
|
||
$"Player {playerId} inventory is full",
|
||
"INVENTORY_FULL",
|
||
new Dictionary<string, object>
|
||
{
|
||
["playerId"] = playerId,
|
||
["itemId"] = item.Id,
|
||
["maxSlots"] = ex.MaxSlots,
|
||
["currentSlots"] = ex.CurrentSlots
|
||
},
|
||
ex
|
||
);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 捕获未知异常并包装
|
||
throw new InventoryException(
|
||
$"Unexpected error adding item {item.Id} to player {playerId}",
|
||
"UNKNOWN_ERROR",
|
||
new Dictionary<string, object>
|
||
{
|
||
["playerId"] = playerId,
|
||
["itemId"] = item.Id,
|
||
["originalError"] = ex.Message
|
||
},
|
||
ex
|
||
);
|
||
}
|
||
}
|
||
|
||
private void ValidateItem(Item item)
|
||
{
|
||
if (item == null)
|
||
throw new ItemValidationException("Item cannot be null");
|
||
|
||
if (string.IsNullOrEmpty(item.Id))
|
||
throw new ItemValidationException("Item ID cannot be empty");
|
||
|
||
if (item.StackSize <= 0)
|
||
throw new ItemValidationException("Item stack size must be positive");
|
||
}
|
||
|
||
private void CheckInventorySpace(string playerId, Item item)
|
||
{
|
||
var inventory = GetPlayerInventory(playerId);
|
||
var requiredSpace = CalculateRequiredSpace(item);
|
||
|
||
if (inventory.FreeSpace < requiredSpace)
|
||
{
|
||
throw new InventoryFullException(
|
||
inventory.FreeSpace,
|
||
inventory.MaxSlots
|
||
);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 错误恢复策略
|
||
|
||
```csharp
|
||
// ✅ 好的做法:优雅的错误恢复
|
||
public class SaveSystem : AbstractSystem
|
||
{
|
||
private readonly IStorage _primaryStorage;
|
||
private readonly IStorage _backupStorage;
|
||
|
||
public SaveSystem(IStorage primaryStorage, IStorage backupStorage = null)
|
||
{
|
||
_primaryStorage = primaryStorage;
|
||
_backupStorage = backupStorage ?? new LocalStorage("backup");
|
||
}
|
||
|
||
public async Task<SaveData> LoadSaveDataAsync(string saveId)
|
||
{
|
||
try
|
||
{
|
||
// 尝试从主存储加载
|
||
return await _primaryStorage.ReadAsync<SaveData>(saveId);
|
||
}
|
||
catch (StorageException ex)
|
||
{
|
||
Logger.Warning($"Failed to load from primary storage: {ex.Message}");
|
||
|
||
try
|
||
{
|
||
// 尝试从备份存储加载
|
||
var backupData = await _backupStorage.ReadAsync<SaveData>(saveId);
|
||
Logger.Info($"Successfully loaded from backup storage: {saveId}");
|
||
|
||
// 恢复到主存储
|
||
await _primaryStorage.WriteAsync(saveId, backupData);
|
||
|
||
return backupData;
|
||
}
|
||
catch (Exception backupEx)
|
||
{
|
||
Logger.Error($"Failed to load from backup storage: {backupEx.Message}");
|
||
|
||
// 返回默认存档数据
|
||
return GetDefaultSaveData();
|
||
}
|
||
}
|
||
}
|
||
|
||
private SaveData GetDefaultSaveData()
|
||
{
|
||
Logger.Warning("Returning default save data due to loading failures");
|
||
return new SaveData
|
||
{
|
||
PlayerId = "default",
|
||
Level = 1,
|
||
Health = 100,
|
||
Position = Vector3.Zero,
|
||
CreatedAt = DateTime.UtcNow
|
||
};
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:粗暴的错误处理
|
||
public class SaveSystem : AbstractSystem
|
||
{
|
||
public async Task<SaveData> LoadSaveDataAsync(string saveId)
|
||
{
|
||
try
|
||
{
|
||
return await _storage.ReadAsync<SaveData>(saveId);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 直接抛出异常,不提供恢复机制
|
||
throw new Exception($"Failed to load save: {ex.Message}", ex);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 测试策略
|
||
|
||
### 1. 可测试的架构设计
|
||
|
||
```csharp
|
||
// ✅ 好的做法:可测试的架构
|
||
public interface IPlayerMovementService
|
||
{
|
||
void MovePlayer(string playerId, Vector2 direction);
|
||
bool CanPlayerMove(string playerId);
|
||
}
|
||
|
||
public class PlayerMovementService : IPlayerMovementService
|
||
{
|
||
private readonly IPlayerRepository _playerRepository;
|
||
private readonly ICollisionService _collisionService;
|
||
private readonly IMapService _mapService;
|
||
|
||
public PlayerMovementService(
|
||
IPlayerRepository playerRepository,
|
||
ICollisionService collisionService,
|
||
IMapService mapService)
|
||
{
|
||
_playerRepository = playerRepository;
|
||
_collisionService = collisionService;
|
||
_mapService = mapService;
|
||
}
|
||
|
||
public void MovePlayer(string playerId, Vector2 direction)
|
||
{
|
||
if (!CanPlayerMove(playerId))
|
||
return;
|
||
|
||
var player = _playerRepository.GetById(playerId);
|
||
var newPosition = player.Position + direction * player.Speed;
|
||
|
||
if (_collisionService.CanMoveTo(newPosition))
|
||
{
|
||
player.Position = newPosition;
|
||
_playerRepository.Update(player);
|
||
}
|
||
}
|
||
|
||
public bool CanPlayerMove(string playerId)
|
||
{
|
||
var player = _playerRepository.GetById(playerId);
|
||
return player != null && player.IsAlive && !player.IsStunned;
|
||
}
|
||
}
|
||
|
||
// 测试代码
|
||
[TestFixture]
|
||
public class PlayerMovementServiceTests
|
||
{
|
||
private Mock<IPlayerRepository> _mockPlayerRepository;
|
||
private Mock<ICollisionService> _mockCollisionService;
|
||
private Mock<IMapService> _mockMapService;
|
||
private PlayerMovementService _movementService;
|
||
|
||
[SetUp]
|
||
public void Setup()
|
||
{
|
||
_mockPlayerRepository = new Mock<IPlayerRepository>();
|
||
_mockCollisionService = new Mock<ICollisionService>();
|
||
_mockMapService = new Mock<IMapService>();
|
||
|
||
_movementService = new PlayerMovementService(
|
||
_mockPlayerRepository.Object,
|
||
_mockCollisionService.Object,
|
||
_mockMapService.Object
|
||
);
|
||
}
|
||
|
||
[Test]
|
||
public void MovePlayer_ValidMovement_ShouldUpdatePlayerPosition()
|
||
{
|
||
// Arrange
|
||
var playerId = "player1";
|
||
var player = new Player { Id = playerId, Position = Vector2.Zero, Speed = 5.0f };
|
||
var direction = Vector2.Right;
|
||
|
||
_mockPlayerRepository.Setup(r => r.GetById(playerId)).Returns(player);
|
||
_mockCollisionService.Setup(c => c.CanMoveTo(It.IsAny<Vector2>())).Returns(true);
|
||
|
||
// Act
|
||
_movementService.MovePlayer(playerId, direction);
|
||
|
||
// Assert
|
||
_mockPlayerRepository.Verify(r => r.Update(It.Is<Player>(p => p.Position == Vector2.Right * 5.0f)), Times.Once);
|
||
}
|
||
|
||
[Test]
|
||
public void MovePlayer_CollisionBlocked_ShouldNotUpdatePlayerPosition()
|
||
{
|
||
// Arrange
|
||
var playerId = "player1";
|
||
var player = new Player { Id = playerId, Position = Vector2.Zero, Speed = 5.0f };
|
||
var direction = Vector2.Right;
|
||
|
||
_mockPlayerRepository.Setup(r => r.GetById(playerId)).Returns(player);
|
||
_mockCollisionService.Setup(c => c.CanMoveTo(It.IsAny<Vector2>())).Returns(false);
|
||
|
||
// Act
|
||
_movementService.MovePlayer(playerId, direction);
|
||
|
||
// Assert
|
||
_mockPlayerRepository.Verify(r => r.Update(It.IsAny<Player>()), Times.Never);
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:难以测试的设计
|
||
public class PlayerMovementSystem : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<MovementInputEvent>(OnMovementInput);
|
||
}
|
||
|
||
private void OnMovementInput(MovementInputEvent e)
|
||
{
|
||
var player = GetModel<PlayerModel>(); // 依赖架构,难以测试
|
||
var newPosition = player.Position + e.Direction * player.Speed;
|
||
|
||
if (CanMoveTo(newPosition)) // 私有方法,难以直接测试
|
||
{
|
||
player.Position = newPosition;
|
||
}
|
||
}
|
||
|
||
private bool CanMoveTo(Vector2 position)
|
||
{
|
||
// 复杂的碰撞检测逻辑,难以测试
|
||
return true;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 重构指南
|
||
|
||
### 1. 识别代码异味
|
||
|
||
```csharp
|
||
// ❌ 代码异味:长方法、重复代码、上帝类
|
||
public class GameManager : Node
|
||
{
|
||
public void ProcessPlayerInput(InputEvent @event)
|
||
{
|
||
// 长方法 - 做太多事情
|
||
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
|
||
{
|
||
switch (keyEvent.Keycode)
|
||
{
|
||
case Key.W:
|
||
MovePlayer(Vector2.Up);
|
||
PlayFootstepSound();
|
||
UpdatePlayerAnimation("walk_up");
|
||
CheckPlayerCollisions();
|
||
UpdateCameraPosition();
|
||
SavePlayerPosition();
|
||
break;
|
||
case Key.S:
|
||
MovePlayer(Vector2.Down);
|
||
PlayFootstepSound();
|
||
UpdatePlayerAnimation("walk_down");
|
||
CheckPlayerCollisions();
|
||
UpdateCameraPosition();
|
||
SavePlayerPosition();
|
||
break;
|
||
// 重复代码
|
||
}
|
||
}
|
||
}
|
||
|
||
private void MovePlayer(Vector2 direction)
|
||
{
|
||
Player.Position += direction * Player.Speed;
|
||
}
|
||
|
||
private void PlayFootstepSound()
|
||
{
|
||
AudioPlayer.Play("footstep.wav");
|
||
}
|
||
|
||
// ... 更多方法,类过于庞大
|
||
}
|
||
|
||
// ✅ 重构后:职责分离
|
||
public class PlayerInputController : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<InputEvent>(OnInput);
|
||
}
|
||
|
||
private void OnInput(InputEvent e)
|
||
{
|
||
if (e is InputEventKey keyEvent && keyEvent.Pressed)
|
||
{
|
||
var direction = GetDirectionFromKey(keyEvent.Keycode);
|
||
if (direction != Vector2.Zero)
|
||
{
|
||
SendEvent(new PlayerMoveEvent { Direction = direction });
|
||
}
|
||
}
|
||
}
|
||
|
||
private Vector2 GetDirectionFromKey(Key keycode)
|
||
{
|
||
return keycode switch
|
||
{
|
||
Key.W => Vector2.Up,
|
||
Key.S => Vector2.Down,
|
||
Key.A => Vector2.Left,
|
||
Key.D => Vector2.Right,
|
||
_ => Vector2.Zero
|
||
};
|
||
}
|
||
}
|
||
|
||
public class PlayerMovementSystem : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<PlayerMoveEvent>(OnPlayerMove);
|
||
}
|
||
|
||
private void OnPlayerMove(PlayerMoveEvent e)
|
||
{
|
||
var playerModel = GetModel<PlayerModel>();
|
||
var newPosition = playerModel.Position + e.Direction * playerModel.Speed;
|
||
|
||
if (CanMoveTo(newPosition))
|
||
{
|
||
playerModel.Position = newPosition;
|
||
SendEvent(new PlayerMovedEvent { NewPosition = newPosition });
|
||
}
|
||
}
|
||
|
||
private bool CanMoveTo(Vector2 position)
|
||
{
|
||
var collisionService = GetUtility<ICollisionService>();
|
||
return collisionService.CanMoveTo(position);
|
||
}
|
||
}
|
||
|
||
public class AudioSystem : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<PlayerMovedEvent>(OnPlayerMoved);
|
||
}
|
||
|
||
private void OnPlayerMoved(PlayerMovedEvent e)
|
||
{
|
||
PlayFootstepSound();
|
||
}
|
||
|
||
private void PlayFootstepSound()
|
||
{
|
||
var audioUtility = GetUtility<AudioUtility>();
|
||
audioUtility.PlaySound("footstep.wav");
|
||
}
|
||
}
|
||
|
||
public class AnimationSystem : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<PlayerMovedEvent>(OnPlayerMoved);
|
||
}
|
||
|
||
private void OnPlayerMoved(PlayerMovedEvent e)
|
||
{
|
||
var animationName = GetAnimationNameFromDirection(e.Direction);
|
||
SendEvent(new PlayAnimationEvent { AnimationName = animationName });
|
||
}
|
||
|
||
private string GetAnimationNameFromDirection(Vector2 direction)
|
||
{
|
||
if (direction == Vector2.Up) return "walk_up";
|
||
if (direction == Vector2.Down) return "walk_down";
|
||
if (direction == Vector2.Left) return "walk_left";
|
||
if (direction == Vector2.Right) return "walk_right";
|
||
return "idle";
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 渐进式重构
|
||
|
||
```csharp
|
||
// 第一步:提取重复代码
|
||
public class PlayerController : Node
|
||
{
|
||
public void ProcessInput(InputEvent @event)
|
||
{
|
||
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
|
||
{
|
||
Vector2 direction;
|
||
switch (keyEvent.Keycode)
|
||
{
|
||
case Key.W:
|
||
direction = Vector2.Up;
|
||
break;
|
||
case Key.S:
|
||
direction = Vector2.Down;
|
||
break;
|
||
case Key.A:
|
||
direction = Vector2.Left;
|
||
break;
|
||
case Key.D:
|
||
direction = Vector2.Right;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
|
||
MovePlayer(direction);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 第二步:提取方法
|
||
public class PlayerController : Node
|
||
{
|
||
public void ProcessInput(InputEvent @event)
|
||
{
|
||
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
|
||
{
|
||
var direction = GetDirectionFromKey(keyEvent.Keycode);
|
||
if (direction != Vector2.Zero)
|
||
{
|
||
MovePlayer(direction);
|
||
}
|
||
}
|
||
}
|
||
|
||
private Vector2 GetDirectionFromKey(Key keycode)
|
||
{
|
||
return keycode switch
|
||
{
|
||
Key.W => Vector2.Up,
|
||
Key.S => Vector2.Down,
|
||
Key.A => Vector2.Left,
|
||
Key.D => Vector2.Right,
|
||
_ => Vector2.Zero
|
||
};
|
||
}
|
||
}
|
||
|
||
// 第三步:引入系统和事件
|
||
public class PlayerController : AbstractSystem
|
||
{
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<InputEvent>(OnInput);
|
||
}
|
||
|
||
private void OnInput(InputEvent e)
|
||
{
|
||
if (e is InputEventKey keyEvent && keyEvent.Pressed)
|
||
{
|
||
var direction = GetDirectionFromKey(keyEvent.Keycode);
|
||
if (direction != Vector2.Zero)
|
||
{
|
||
SendEvent(new PlayerMoveEvent { Direction = direction });
|
||
}
|
||
}
|
||
}
|
||
|
||
private Vector2 GetDirectionFromKey(Key keycode)
|
||
{
|
||
return keycode switch
|
||
{
|
||
Key.W => Vector2.Up,
|
||
Key.S => Vector2.Down,
|
||
Key.A => Vector2.Left,
|
||
Key.D => Vector2.Right,
|
||
_ => Vector2.Zero
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 模式选择与组合
|
||
|
||
### 何时使用哪种模式?
|
||
|
||
#### 小型项目(原型、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);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
遵循这些架构模式最佳实践,你将能够构建:
|
||
|
||
- ✅ **清晰的代码结构** - 易于理解和维护
|
||
- ✅ **松耦合的组件** - 便于测试和扩展
|
||
- ✅ **可重用的模块** - 提高开发效率
|
||
- ✅ **健壮的错误处理** - 提高系统稳定性
|
||
- ✅ **完善的测试覆盖** - 保证代码质量
|
||
|
||
### 关键要点
|
||
|
||
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)
|
||
|
||
记住,好的架构不是一蹴而就的,需要持续的学习、实践和改进。
|
||
|
||
---
|
||
|
||
**文档版本**: 2.0.0
|
||
**最后更新**: 2026-03-07
|
||
**作者**: GFramework Team |