Skip to content

第 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     │ │ 响应状态    │
        └─────────────┘ └─────────────┘

各层职责速查表

层级职责可以做不能做
ViewUI 展示渲染节点、接收输入包含业务逻辑
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
协调多个 ModelSystem
触发系统级反应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 实现状态机
    • 分层状态机
    • 状态转换规则

推荐资源

项目示例

查看完整的示例项目:

总结

通过本教程,你学到了:

核心概念

  • ✅ Model:存储状态,发送事件
  • ✅ Command:封装业务逻辑
  • ✅ Controller:协调用户输入
  • ✅ Utility:提供业务规则
  • ✅ System:响应状态变化

设计原则

  • ✅ 单一职责
  • ✅ 依赖倒置
  • ✅ 事件驱动
  • ✅ 单向数据流
  • ✅ 关注点分离

架构优势

  • ✅ 可测试
  • ✅ 可复用
  • ✅ 可扩展
  • ✅ 易维护
  • ✅ 解耦合

结语

恭喜你完成了 GFramework 基础教程!🎉

你现在已经掌握了使用 GFramework 构建清晰、可维护的游戏架构的核心知识。

记住:

  • 架构是为了解决问题,不是为了炫技
  • 从简单开始,逐步优化
  • 理解原则比记住代码更重要

继续探索,享受编程的乐趣!


反馈与支持

  • 遇到问题?查看 GitHub Issues
  • 有建议?欢迎提交 PR 或 Issue
  • 加入社区,与其他开发者交流
完整检查清单

环境与项目

  • [ ] .NET SDK 和 Godot 已安装
  • [ ] GFramework NuGet 包已引入
  • [ ] 项目架构已搭建

核心组件

  • [ ] Model 已创建并注册
  • [ ] Command 已创建
  • [ ] Utility 已创建并注册
  • [ ] System 已创建并注册
  • [ ] Controller 实现了 IController

功能验证

  • [ ] 计数器功能正常
  • [ ] 事件系统工作正常
  • [ ] 上限限制生效
  • [ ] 阈值检查触发

理解验证

  • [ ] 理解了各层职责
  • [ ] 理解了事件驱动架构
  • [ ] 理解了单向数据流
  • [ ] 理解了设计原则

👏 再次恭喜你完成教程!期待看到你用 GFramework 创造出精彩的项目!

基于 Apache 2.0 许可证发布