GeWuYou e4e79e16dc docs(tutorials): 更新基础教程导航结构并添加完整教程内容
- 重构教程导航,将基础教程拆分为多个子章节
- 添加第1章:环境准备,包含.NET SDK和Godot引擎安装指南
- 添加第2章:项目创建与初始化,介绍GFramework项目结构搭建
- 添加第3章:基础计数器实现,演示传统MVC模式及问题分析
- 添加第4章:引入Model重构,展示GFramework的Model层设计
- 配置教程间的前后导航链接
- 更新导航菜单结构,支持折叠展开功能
2026-02-12 01:08:55 +08:00

16 KiB
Raw Permalink Blame History

prev
prev
text link
Utility 与 System ./06-utility-system

第 7 章:总结与最佳实践

恭喜你完成了 GFramework 基础教程!本章将回顾整个架构设计,总结最佳实践,并解答常见问题。

架构演进回顾

阶段 1基础实现

代码

private int _count;

AddButton.Pressed += () =>
{
    _count++;
    UpdateView();
};

问题

  • 状态、逻辑、UI 混在一起
  • 无法复用
  • 难以测试
  • 扩展困难

阶段 2引入 Model + 事件

代码

private ICounterModel _counterModel;

AddButton.Pressed += () =>
{
    _counterModel.Increment();
};

this.RegisterEvent<ChangedCountEvent>(e =>
{
    UpdateView(e.Count);
});

改进

  • 状态抽离到 Model
  • 通过事件更新 UI
  • Model 可复用、可测试

剩余问题

  • ⚠️ 交互逻辑仍在 Controller

阶段 3引入 Command

代码

AddButton.Pressed += () =>
{
    this.SendCommand(new IncreaseCountCommand());
};

改进

  • Controller 不关心"如何",只负责"转发"
  • 逻辑封装在 Command 中
  • 命令可复用、可测试

剩余问题

  • ⚠️ 业务规则写在 Command 里

阶段 4引入 Utility + System

代码

// 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

每个类只做一件事:

// ✅ 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

依赖抽象,不依赖具体实现:

// ✅ 依赖接口
private ICounterModel _counterModel;

// ❌ 依赖具体类
private CounterModel _counterModel;

3. 开闭原则OCP

对扩展开放,对修改封闭:

// ✅ 新增功能不修改现有代码
architecture.RegisterSystem(new NewFeatureSystem());

// ❌ 修改现有类添加功能
public class CounterModel
{
    // 每次新增功能都修改这个类
}

4. 事件驱动原则

通过事件解耦组件:

// ✅ Model 不知道谁在监听
this.SendEvent(new ChangedCountEvent());

// ❌ Model 直接调用
_view.UpdateView();

5. 单向数据流

数据总是单向流动:

Action → Command → Model → Event → View/System

最佳实践

1. 接口设计

推荐

public interface ICounterModel : IModel
{
    int Count { get; }
    void Increment();
}

不推荐

public class CounterModel  // 没有接口
{
    public int Count { get; set; }  // 可被外部直接修改
}

2. 事件命名

推荐

public sealed record ChangedCountEvent  // 描述事件
{
    public int Count { get; init; }
}

不推荐

public class CountEvent { }  // 不清晰
public class Data { }        // 太泛化

3. Command 职责

推荐

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();
}

不推荐

protected override void OnExecute()
{
    _count++;  // 直接修改状态
    UpdateUI();  // 直接更新 UI
    PlaySound();  // 混入太多逻辑
}

4. Utility 设计

推荐

public bool CanIncrease(int current)
{
    return current < _maxCount;  // 纯函数
}

不推荐

private int _state;  // 持有状态

public void Increment()
{
    _state++;  // 修改状态
}

5. System 使用

推荐

protected override void OnInit()
{
    // 监听事件
    this.RegisterEvent<ChangedCountEvent>(e =>
    {
        CheckThreshold(e.Count);
    });
}

不推荐

public void UpdateCounter()
{
    // 直接修改 Model
    model.Count++;  // 应该通过 Command
}

常见问题 FAQ

Q1: Model 可以调用其他 Model 吗?

不推荐

public class PlayerModel : AbstractModel
{
    public void Attack()
    {
        // 直接调用其他 Model
        this.GetModel<EnemyModel>().TakeDamage(10);
    }
}

推荐:通过 Command 或 System 协调:

public class AttackCommand : AbstractCommand
{
    protected override void OnExecute()
    {
        var player = this.GetModel<IPlayerModel>();
        var enemy = this.GetModel<IEnemyModel>();
        
        enemy.TakeDamage(player.AttackPower);
    }
}

Q2: Command 可以嵌套调用吗?

可以

protected override void OnExecute()
{
    this.SendCommand(new SaveDataCommand());
    this.SendCommand(new UpdateUICommand());
}

但要注意:

  • 避免循环依赖
  • 考虑使用 System 协调复杂流程

Q3: 什么时候用 Utility什么时候用 System

场景 使用
无状态计算 Utility
业务规则验证 Utility
响应状态变化 System
协调多个 Model System
触发系统级反应 System

示例

// Utility纯计算
utility.CanIncrease(count)

// System状态响应
if (count > 10) PlaySound();

Q4: Controller 可以直接调用 Model 吗?

部分场景可以

// ✅ 只读操作
var count = this.GetModel<ICounterModel>().Count;

// ❌ 修改操作(应通过 Command
this.GetModel<ICounterModel>().Increment();

原则

  • 读取数据:可以直接调用
  • 修改数据:应该通过 Command

Q5: 如何处理异步操作?

使用 AbstractAsyncCommand

public class SaveDataCommand : AbstractAsyncCommand
{
    protected override async Task OnExecuteAsync()
    {
        var model = this.GetModel<ICounterModel>();
        await SaveToFileAsync(model.Count);
    }
}

Q6: 如何在多个场景共享状态?

Model 是全局的

// 场景 A
var count = this.GetModel<ICounterModel>().Count;

// 场景 B
var count = this.GetModel<ICounterModel>().Count;
// 两者是同一个 Model 实例

如果需要场景独立的状态,考虑:

  • 为每个场景创建独立的 Model
  • 使用场景参数传递数据

Q7: 如何测试这些组件?

Model 测试

[Test]
public void Increment_ShouldIncreaseCount()
{
    var model = new CounterModel();
    model.Increment();
    Assert.AreEqual(1, model.Count);
}

Utility 测试

[Test]
public void CanIncrease_WhenAtMax_ReturnsFalse()
{
    var utility = new CounterUtility(maxCount: 20);
    Assert.IsFalse(utility.CanIncrease(20));
}

Command 测试(需要 mock

[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 的事件系统经过优化,开销很小
  • 对于游戏逻辑级别的事件(如计数变化),完全没问题
  • 如果是高频事件(如每帧更新),考虑批处理

示例

// ❌ 高频事件(每帧发送)
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 中获取并缓存

示例

// ✅ 缓存引用
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 构建清晰、可维护的游戏架构的核心知识。

记住:

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

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


::: tip 反馈与支持

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

::: details 完整检查清单 环境与项目

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

核心组件

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

功能验证

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

理解验证

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

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