mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-23 03:04:29 +08:00
- 重构教程导航,将基础教程拆分为多个子章节 - 添加第1章:环境准备,包含.NET SDK和Godot引擎安装指南 - 添加第2章:项目创建与初始化,介绍GFramework项目结构搭建 - 添加第3章:基础计数器实现,演示传统MVC模式及问题分析 - 添加第4章:引入Model重构,展示GFramework的Model层设计 - 配置教程间的前后导航链接 - 更新导航菜单结构,支持折叠展开功能
752 lines
16 KiB
Markdown
752 lines
16 KiB
Markdown
---
|
||
prev:
|
||
text: 'Utility 与 System'
|
||
link: './06-utility-system'
|
||
---
|
||
|
||
# 第 7 章:总结与最佳实践
|
||
|
||
恭喜你完成了 GFramework 基础教程!本章将回顾整个架构设计,总结最佳实践,并解答常见问题。
|
||
|
||
## 架构演进回顾
|
||
|
||
### 阶段 1:基础实现
|
||
|
||
**代码**:
|
||
|
||
```csharp
|
||
private int _count;
|
||
|
||
AddButton.Pressed += () =>
|
||
{
|
||
_count++;
|
||
UpdateView();
|
||
};
|
||
```
|
||
|
||
**问题**:
|
||
|
||
- ❌ 状态、逻辑、UI 混在一起
|
||
- ❌ 无法复用
|
||
- ❌ 难以测试
|
||
- ❌ 扩展困难
|
||
|
||
---
|
||
|
||
### 阶段 2:引入 Model + 事件
|
||
|
||
**代码**:
|
||
|
||
```csharp
|
||
private ICounterModel _counterModel;
|
||
|
||
AddButton.Pressed += () =>
|
||
{
|
||
_counterModel.Increment();
|
||
};
|
||
|
||
this.RegisterEvent<ChangedCountEvent>(e =>
|
||
{
|
||
UpdateView(e.Count);
|
||
});
|
||
```
|
||
|
||
**改进**:
|
||
|
||
- ✅ 状态抽离到 Model
|
||
- ✅ 通过事件更新 UI
|
||
- ✅ Model 可复用、可测试
|
||
|
||
**剩余问题**:
|
||
|
||
- ⚠️ 交互逻辑仍在 Controller
|
||
|
||
---
|
||
|
||
### 阶段 3:引入 Command
|
||
|
||
**代码**:
|
||
|
||
```csharp
|
||
AddButton.Pressed += () =>
|
||
{
|
||
this.SendCommand(new IncreaseCountCommand());
|
||
};
|
||
```
|
||
|
||
**改进**:
|
||
|
||
- ✅ Controller 不关心"如何",只负责"转发"
|
||
- ✅ 逻辑封装在 Command 中
|
||
- ✅ 命令可复用、可测试
|
||
|
||
**剩余问题**:
|
||
|
||
- ⚠️ 业务规则写在 Command 里
|
||
|
||
---
|
||
|
||
### 阶段 4:引入 Utility + System
|
||
|
||
**代码**:
|
||
|
||
```csharp
|
||
// Command 使用 Utility 检查规则
|
||
if (!utility.CanIncrease(model.Count)) return;
|
||
model.Increment();
|
||
|
||
// System 响应状态变化
|
||
this.RegisterEvent<ChangedCountEvent>(e =>
|
||
{
|
||
CheckThreshold(e.Count);
|
||
});
|
||
```
|
||
|
||
**最终架构**:
|
||
|
||
- ✅ 完全的关注点分离
|
||
- ✅ 单向数据流
|
||
- ✅ 各层可测试、可复用
|
||
- ✅ 易于扩展和维护
|
||
|
||
## 完整架构图
|
||
|
||
```
|
||
┌──────────────────────────────────────────────┐
|
||
│ View (UI) │
|
||
│ Godot Nodes (Label, Button) │
|
||
└────────────┬─────────────────────────────────┘
|
||
│ 用户输入
|
||
┌────────────▼─────────────────────────────────┐
|
||
│ Controller │
|
||
│ - 接收用户输入 │
|
||
│ - 转发命令 │
|
||
│ - 监听事件更新 UI │
|
||
└────────────┬─────────────────────────────────┘
|
||
│ SendCommand
|
||
┌────────────▼─────────────────────────────────┐
|
||
│ Command │
|
||
│ - 获取 Utility 检查规则 │
|
||
│ - 调用 Model 修改状态 │
|
||
└────────────┬─────────────────────────────────┘
|
||
│ GetModel / GetUtility
|
||
│
|
||
┌─────────┼─────────┐
|
||
│ │
|
||
┌──▼──────────┐ ┌──────▼───────┐
|
||
│ Utility │ │ Model │
|
||
│ - 业务规则 │ │ - 存储状态 │
|
||
│ - 纯计算 │ │ - 发送事件 │
|
||
└─────────────┘ └──────┬───────┘
|
||
│ SendEvent
|
||
┌──────┴───────┐
|
||
│ │
|
||
┌───────▼─────┐ ┌──────▼──────┐
|
||
│ Controller │ │ System │
|
||
│ 更新 UI │ │ 响应状态 │
|
||
└─────────────┘ └─────────────┘
|
||
```
|
||
|
||
## 各层职责速查表
|
||
|
||
| 层级 | 职责 | 可以做 | 不能做 |
|
||
|----------------|-------|---------------------|-------------------------|
|
||
| **View** | UI 展示 | 渲染节点、接收输入 | 包含业务逻辑 |
|
||
| **Controller** | 协调层 | 转发命令、监听事件、更新 UI | 直接修改 Model |
|
||
| **Command** | 业务操作 | 调用 Model、使用 Utility | 持有状态、直接更新 UI |
|
||
| **Model** | 数据状态 | 存储数据、发送事件 | 知道 View、调用 Controller |
|
||
| **Utility** | 业务规则 | 无状态计算、验证 | 持有状态、依赖场景 |
|
||
| **System** | 系统逻辑 | 监听事件、协调 Model | 直接修改 Model(应通过 Command) |
|
||
|
||
## 设计原则
|
||
|
||
### 1. 单一职责原则(SRP)
|
||
|
||
每个类只做一件事:
|
||
|
||
```csharp
|
||
// ✅ Model 只负责状态
|
||
public class CounterModel : AbstractModel
|
||
{
|
||
public int Count { get; private set; }
|
||
public void Increment() { /* ... */ }
|
||
}
|
||
|
||
// ✅ Command 只负责操作
|
||
public class IncreaseCountCommand : AbstractCommand
|
||
{
|
||
protected override void OnExecute() { /* ... */ }
|
||
}
|
||
```
|
||
|
||
### 2. 依赖倒置原则(DIP)
|
||
|
||
依赖抽象,不依赖具体实现:
|
||
|
||
```csharp
|
||
// ✅ 依赖接口
|
||
private ICounterModel _counterModel;
|
||
|
||
// ❌ 依赖具体类
|
||
private CounterModel _counterModel;
|
||
```
|
||
|
||
### 3. 开闭原则(OCP)
|
||
|
||
对扩展开放,对修改封闭:
|
||
|
||
```csharp
|
||
// ✅ 新增功能不修改现有代码
|
||
architecture.RegisterSystem(new NewFeatureSystem());
|
||
|
||
// ❌ 修改现有类添加功能
|
||
public class CounterModel
|
||
{
|
||
// 每次新增功能都修改这个类
|
||
}
|
||
```
|
||
|
||
### 4. 事件驱动原则
|
||
|
||
通过事件解耦组件:
|
||
|
||
```csharp
|
||
// ✅ Model 不知道谁在监听
|
||
this.SendEvent(new ChangedCountEvent());
|
||
|
||
// ❌ Model 直接调用
|
||
_view.UpdateView();
|
||
```
|
||
|
||
### 5. 单向数据流
|
||
|
||
数据总是单向流动:
|
||
|
||
```
|
||
Action → Command → Model → Event → View/System
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 接口设计
|
||
|
||
**✅ 推荐**:
|
||
|
||
```csharp
|
||
public interface ICounterModel : IModel
|
||
{
|
||
int Count { get; }
|
||
void Increment();
|
||
}
|
||
```
|
||
|
||
**❌ 不推荐**:
|
||
|
||
```csharp
|
||
public class CounterModel // 没有接口
|
||
{
|
||
public int Count { get; set; } // 可被外部直接修改
|
||
}
|
||
```
|
||
|
||
### 2. 事件命名
|
||
|
||
**✅ 推荐**:
|
||
|
||
```csharp
|
||
public sealed record ChangedCountEvent // 描述事件
|
||
{
|
||
public int Count { get; init; }
|
||
}
|
||
```
|
||
|
||
**❌ 不推荐**:
|
||
|
||
```csharp
|
||
public class CountEvent { } // 不清晰
|
||
public class Data { } // 太泛化
|
||
```
|
||
|
||
### 3. Command 职责
|
||
|
||
**✅ 推荐**:
|
||
|
||
```csharp
|
||
protected override void OnExecute()
|
||
{
|
||
// 1. 获取依赖
|
||
var model = this.GetModel<ICounterModel>();
|
||
var utility = this.GetUtility<ICounterUtility>();
|
||
|
||
// 2. 检查规则
|
||
if (!utility.CanIncrease(model.Count)) return;
|
||
|
||
// 3. 执行操作
|
||
model.Increment();
|
||
}
|
||
```
|
||
|
||
**❌ 不推荐**:
|
||
|
||
```csharp
|
||
protected override void OnExecute()
|
||
{
|
||
_count++; // 直接修改状态
|
||
UpdateUI(); // 直接更新 UI
|
||
PlaySound(); // 混入太多逻辑
|
||
}
|
||
```
|
||
|
||
### 4. Utility 设计
|
||
|
||
**✅ 推荐**:
|
||
|
||
```csharp
|
||
public bool CanIncrease(int current)
|
||
{
|
||
return current < _maxCount; // 纯函数
|
||
}
|
||
```
|
||
|
||
**❌ 不推荐**:
|
||
|
||
```csharp
|
||
private int _state; // 持有状态
|
||
|
||
public void Increment()
|
||
{
|
||
_state++; // 修改状态
|
||
}
|
||
```
|
||
|
||
### 5. System 使用
|
||
|
||
**✅ 推荐**:
|
||
|
||
```csharp
|
||
protected override void OnInit()
|
||
{
|
||
// 监听事件
|
||
this.RegisterEvent<ChangedCountEvent>(e =>
|
||
{
|
||
CheckThreshold(e.Count);
|
||
});
|
||
}
|
||
```
|
||
|
||
**❌ 不推荐**:
|
||
|
||
```csharp
|
||
public void UpdateCounter()
|
||
{
|
||
// 直接修改 Model
|
||
model.Count++; // 应该通过 Command
|
||
}
|
||
```
|
||
|
||
## 常见问题 FAQ
|
||
|
||
### Q1: Model 可以调用其他 Model 吗?
|
||
|
||
**❌ 不推荐**:
|
||
|
||
```csharp
|
||
public class PlayerModel : AbstractModel
|
||
{
|
||
public void Attack()
|
||
{
|
||
// 直接调用其他 Model
|
||
this.GetModel<EnemyModel>().TakeDamage(10);
|
||
}
|
||
}
|
||
```
|
||
|
||
**✅ 推荐**:通过 Command 或 System 协调:
|
||
|
||
```csharp
|
||
public class AttackCommand : AbstractCommand
|
||
{
|
||
protected override void OnExecute()
|
||
{
|
||
var player = this.GetModel<IPlayerModel>();
|
||
var enemy = this.GetModel<IEnemyModel>();
|
||
|
||
enemy.TakeDamage(player.AttackPower);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Q2: Command 可以嵌套调用吗?
|
||
|
||
**✅ 可以**:
|
||
|
||
```csharp
|
||
protected override void OnExecute()
|
||
{
|
||
this.SendCommand(new SaveDataCommand());
|
||
this.SendCommand(new UpdateUICommand());
|
||
}
|
||
```
|
||
|
||
但要注意:
|
||
|
||
- 避免循环依赖
|
||
- 考虑使用 System 协调复杂流程
|
||
|
||
---
|
||
|
||
### Q3: 什么时候用 Utility,什么时候用 System?
|
||
|
||
| 场景 | 使用 |
|
||
|------------|---------|
|
||
| 无状态计算 | Utility |
|
||
| 业务规则验证 | Utility |
|
||
| 响应状态变化 | System |
|
||
| 协调多个 Model | System |
|
||
| 触发系统级反应 | System |
|
||
|
||
**示例**:
|
||
|
||
```csharp
|
||
// Utility:纯计算
|
||
utility.CanIncrease(count)
|
||
|
||
// System:状态响应
|
||
if (count > 10) PlaySound();
|
||
```
|
||
|
||
---
|
||
|
||
### Q4: Controller 可以直接调用 Model 吗?
|
||
|
||
**部分场景可以**:
|
||
|
||
```csharp
|
||
// ✅ 只读操作
|
||
var count = this.GetModel<ICounterModel>().Count;
|
||
|
||
// ❌ 修改操作(应通过 Command)
|
||
this.GetModel<ICounterModel>().Increment();
|
||
```
|
||
|
||
**原则**:
|
||
|
||
- 读取数据:可以直接调用
|
||
- 修改数据:应该通过 Command
|
||
|
||
---
|
||
|
||
### Q5: 如何处理异步操作?
|
||
|
||
使用 `AbstractAsyncCommand`:
|
||
|
||
```csharp
|
||
public class SaveDataCommand : AbstractAsyncCommand
|
||
{
|
||
protected override async Task OnExecuteAsync()
|
||
{
|
||
var model = this.GetModel<ICounterModel>();
|
||
await SaveToFileAsync(model.Count);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Q6: 如何在多个场景共享状态?
|
||
|
||
**Model 是全局的**:
|
||
|
||
```csharp
|
||
// 场景 A
|
||
var count = this.GetModel<ICounterModel>().Count;
|
||
|
||
// 场景 B
|
||
var count = this.GetModel<ICounterModel>().Count;
|
||
// 两者是同一个 Model 实例
|
||
```
|
||
|
||
如果需要场景独立的状态,考虑:
|
||
|
||
- 为每个场景创建独立的 Model
|
||
- 使用场景参数传递数据
|
||
|
||
---
|
||
|
||
### Q7: 如何测试这些组件?
|
||
|
||
**Model 测试**:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void Increment_ShouldIncreaseCount()
|
||
{
|
||
var model = new CounterModel();
|
||
model.Increment();
|
||
Assert.AreEqual(1, model.Count);
|
||
}
|
||
```
|
||
|
||
**Utility 测试**:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void CanIncrease_WhenAtMax_ReturnsFalse()
|
||
{
|
||
var utility = new CounterUtility(maxCount: 20);
|
||
Assert.IsFalse(utility.CanIncrease(20));
|
||
}
|
||
```
|
||
|
||
**Command 测试**(需要 mock):
|
||
|
||
```csharp
|
||
[Test]
|
||
public void ExecuteCommand_ShouldIncrementModel()
|
||
{
|
||
// 需要 mock IArchitecture
|
||
// 或使用集成测试
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Q8: 项目变大后如何组织代码?
|
||
|
||
**按功能模块划分**:
|
||
|
||
```
|
||
scripts/
|
||
├── counter/
|
||
│ ├── model/
|
||
│ ├── command/
|
||
│ └── system/
|
||
├── player/
|
||
│ ├── model/
|
||
│ ├── command/
|
||
│ └── system/
|
||
└── inventory/
|
||
├── model/
|
||
├── command/
|
||
└── system/
|
||
```
|
||
|
||
**按层级划分**:
|
||
|
||
```
|
||
scripts/
|
||
├── model/
|
||
│ ├── CounterModel.cs
|
||
│ ├── PlayerModel.cs
|
||
│ └── InventoryModel.cs
|
||
├── command/
|
||
│ ├── counter/
|
||
│ ├── player/
|
||
│ └── inventory/
|
||
└── system/
|
||
├── CounterSystem.cs
|
||
└── PlayerSystem.cs
|
||
```
|
||
|
||
选择适合团队的方式。
|
||
|
||
## 性能考虑
|
||
|
||
### 事件系统开销
|
||
|
||
**问题**:频繁发送事件会影响性能吗?
|
||
|
||
**答案**:
|
||
|
||
- GFramework 的事件系统经过优化,开销很小
|
||
- 对于游戏逻辑级别的事件(如计数变化),完全没问题
|
||
- 如果是高频事件(如每帧更新),考虑批处理
|
||
|
||
**示例**:
|
||
|
||
```csharp
|
||
// ❌ 高频事件(每帧发送)
|
||
public override void _Process(double delta)
|
||
{
|
||
this.SendEvent(new PositionChangedEvent());
|
||
}
|
||
|
||
// ✅ 批处理或降频
|
||
private float _eventTimer;
|
||
public override void _Process(double delta)
|
||
{
|
||
_eventTimer += (float)delta;
|
||
if (_eventTimer > 0.1f) // 每 100ms 发送一次
|
||
{
|
||
this.SendEvent(new PositionChangedEvent());
|
||
_eventTimer = 0;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 依赖注入开销
|
||
|
||
**问题**:`GetModel()` 会影响性能吗?
|
||
|
||
**答案**:
|
||
|
||
- 第一次调用会查找,之后会缓存
|
||
- 建议在 `_Ready` 中获取并缓存
|
||
|
||
**示例**:
|
||
|
||
```csharp
|
||
// ✅ 缓存引用
|
||
private ICounterModel _counterModel;
|
||
|
||
public override void _Ready()
|
||
{
|
||
_counterModel = this.GetModel<ICounterModel>();
|
||
}
|
||
|
||
public override void _Process(double delta)
|
||
{
|
||
var count = _counterModel.Count; // 使用缓存的引用
|
||
}
|
||
```
|
||
|
||
## 下一步学习
|
||
|
||
### 进阶主题
|
||
|
||
1. **高级命令模式**
|
||
- 异步命令
|
||
- 命令队列
|
||
- 撤销/重做
|
||
|
||
2. **复杂事件系统**
|
||
- 事件优先级
|
||
- 事件过滤
|
||
- 事件链
|
||
|
||
3. **高级 System**
|
||
- System 之间的通信
|
||
- System 生命周期管理
|
||
- System 优先级
|
||
|
||
4. **规则系统**
|
||
- 动态规则
|
||
- 规则链
|
||
- 规则引擎
|
||
|
||
5. **状态机**
|
||
- 使用 GFramework 实现状态机
|
||
- 分层状态机
|
||
- 状态转换规则
|
||
|
||
### 推荐资源
|
||
|
||
- **GFramework 文档**:
|
||
- [Core 核心框架](../../core/)
|
||
- [Game 游戏模块](../../game/)
|
||
- [Godot 集成](../../godot/)
|
||
- [源码生成器](../../source-generators/)
|
||
|
||
- **设计模式**:
|
||
- 命令模式(Command Pattern)
|
||
- 观察者模式(Observer Pattern)
|
||
- 依赖注入(Dependency Injection)
|
||
|
||
- **架构设计**:
|
||
- Clean Architecture
|
||
- MVC / MVVM
|
||
- Event-Driven Architecture
|
||
|
||
## 项目示例
|
||
|
||
查看完整的示例项目:
|
||
|
||
- [基础教程(本教程)](https://github.com/GeWuYou/GFramework/tree/main/examples/Counter)
|
||
- [Godot 集成示例](https://github.com/GeWuYou/GFramework/tree/main/examples/GodotIntegration)
|
||
- [高级模式示例](https://github.com/GeWuYou/GFramework/tree/main/examples/AdvancedPatterns)
|
||
|
||
## 总结
|
||
|
||
通过本教程,你学到了:
|
||
|
||
### 核心概念
|
||
|
||
- ✅ Model:存储状态,发送事件
|
||
- ✅ Command:封装业务逻辑
|
||
- ✅ Controller:协调用户输入
|
||
- ✅ Utility:提供业务规则
|
||
- ✅ System:响应状态变化
|
||
|
||
### 设计原则
|
||
|
||
- ✅ 单一职责
|
||
- ✅ 依赖倒置
|
||
- ✅ 事件驱动
|
||
- ✅ 单向数据流
|
||
- ✅ 关注点分离
|
||
|
||
### 架构优势
|
||
|
||
- ✅ 可测试
|
||
- ✅ 可复用
|
||
- ✅ 可扩展
|
||
- ✅ 易维护
|
||
- ✅ 解耦合
|
||
|
||
## 结语
|
||
|
||
恭喜你完成了 GFramework 基础教程!🎉
|
||
|
||
你现在已经掌握了使用 GFramework 构建清晰、可维护的游戏架构的核心知识。
|
||
|
||
记住:
|
||
|
||
- **架构是为了解决问题,不是为了炫技**
|
||
- **从简单开始,逐步优化**
|
||
- **理解原则比记住代码更重要**
|
||
|
||
继续探索,享受编程的乐趣!
|
||
|
||
---
|
||
|
||
::: tip 反馈与支持
|
||
|
||
- 遇到问题?查看 [GitHub Issues](https://github.com/GeWuYou/GFramework/issues)
|
||
- 有建议?欢迎提交 PR 或 Issue
|
||
- 加入社区,与其他开发者交流
|
||
:::
|
||
|
||
::: details 完整检查清单
|
||
**环境与项目**
|
||
|
||
- [ ] .NET SDK 和 Godot 已安装
|
||
- [ ] GFramework NuGet 包已引入
|
||
- [ ] 项目架构已搭建
|
||
|
||
**核心组件**
|
||
|
||
- [ ] Model 已创建并注册
|
||
- [ ] Command 已创建
|
||
- [ ] Utility 已创建并注册
|
||
- [ ] System 已创建并注册
|
||
- [ ] Controller 实现了 IController
|
||
|
||
**功能验证**
|
||
|
||
- [ ] 计数器功能正常
|
||
- [ ] 事件系统工作正常
|
||
- [ ] 上限限制生效
|
||
- [ ] 阈值检查触发
|
||
|
||
**理解验证**
|
||
|
||
- [ ] 理解了各层职责
|
||
- [ ] 理解了事件驱动架构
|
||
- [ ] 理解了单向数据流
|
||
- [ ] 理解了设计原则
|
||
:::
|
||
|
||
👏 再次恭喜你完成教程!期待看到你用 GFramework 创造出精彩的项目!
|