GFramework/docs/zh-CN/tutorials/basic/06-utility-system.md
GeWuYou fb14d7122c docs(style): 更新文档中的命名空间导入格式
- 将所有小写的命名空间导入更正为首字母大写格式
- 统一 GFramework 框架的命名空间引用规范
- 修复 core、ecs、godot 等模块的命名空间导入错误
- 标准化文档示例代码中的 using 语句格式
- 确保所有文档中的命名空间引用保持一致性
- 更新 global using 语句以匹配正确的命名空间格式
2026-03-10 07:18:49 +08:00

713 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
prev:
text: '命令系统优化'
link: './05-command-system'
next:
text: '总结与最佳实践'
link: './07-summary'
---
# 第 6 章Utility 与 System
本章将引入架构的最后两个核心概念:**Utility工具类** 和 **System系统**,完成我们的架构设计。
## 新需求
### 需求 1计数上限
计数器不能超过 **20**
### 需求 2阈值检查
- 当 Count > 10 时,输出提示信息
- 当 Count < -10 输出提示信息
## 方案一:在 Command 中实现(❌ 不推荐)
### 错误示范
```csharp
public class IncreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
var model = this.GetModel<ICounterModel>()!;
// ❌ 在 Command 里写业务规则
if (model.Count >= 20)
{
return; // 超过上限,不执行
}
model.Increment();
}
}
```
### 问题
1. **规则写死在 Command 里**
- 如果别的地方也要用"最大 20"的限制怎么办
- 规则变更如改成 100需要修改业务代码
2. **无法单独测试规则**
- 只能通过 Command 测试无法单独测试规则逻辑
3. **违反单一职责原则**
- Command 应该只负责"执行操作"
- 不应该负责"业务规则验证"
## 引入 Utility
### Utility 是什么?
**Utility工具类** 提供可复用的无状态逻辑负责
纯函数式的计算和验证
数据转换和格式化
业务规则的封装
Utility **不应该**
持有状态
依赖场景
直接修改 Model
### 特点
- **无状态**只提供计算方法
- **可复用**任何层都可以调用
- **可测试**纯函数易于测试
### 1. 定义 Utility 接口
`scripts/utility/` 创建 `ICounterUtility.cs`
```csharp
using GFramework.Core.Abstractions.Utility;
namespace MyGFrameworkGame.scripts.Utility;
/// <summary>
/// 计数器工具接口
/// </summary>
public interface ICounterUtility : IContextUtility
{
/// <summary>
/// 判断当前值是否可以增加
/// </summary>
bool CanIncrease(int current);
/// <summary>
/// 将值限制在有效范围内
/// </summary>
int Clamp(int value);
}
```
### 2. 实现 Utility 类
`scripts/utility/` 创建 `CounterUtility.cs`
```csharp
using System;
using GFramework.Core.Utility;
namespace MyGFrameworkGame.scripts.Utility;
/// <summary>
/// 计数器工具实现
/// </summary>
public class CounterUtility : AbstractContextUtility, ICounterUtility
{
private readonly int _maxCount;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="maxCount">最大值(默认 20</param>
public CounterUtility(int maxCount = 20)
{
_maxCount = maxCount;
}
/// <summary>
/// 初始化(可选)
/// </summary>
protected override void OnInit()
{
// 也可以通过上下文获取配置
}
/// <summary>
/// 判断是否可以继续增加
/// </summary>
public bool CanIncrease(int current)
{
return current < _maxCount;
}
/// <summary>
/// 将值限制在有效范围内
/// </summary>
public int Clamp(int value)
{
return Math.Clamp(value, 0, _maxCount);
}
}
```
::: tip 为什么用构造函数传参
这里使用构造函数传递 `maxCount` 是为了灵活性你也可以
- 从配置文件读取
- 通过 `architecture.Context.GetUtility()` 传入
- 硬编码在类内部
选择取决于项目需求
:::
### 3. 注册 Utility
编辑 `scripts/module/UtilityModule.cs`
```csharp
using GFramework.Core.Abstractions.Architecture;
using GFramework.Game.Architecture;
using MyGFrameworkGame.scripts.Utility;
namespace MyGFrameworkGame.scripts.module;
/// <summary>
/// 工具模块,负责注册所有的工具类
/// </summary>
public class UtilityModule : AbstractModule
{
public override void Install(IArchitecture architecture)
{
// 注册 CounterUtility最大值设为 20
architecture.RegisterUtility<ICounterUtility>(new CounterUtility(maxCount: 20));
}
}
```
### 4. 在 Command 中使用 Utility
编辑 `IncreaseCountCommand.cs`
```csharp
using GFramework.Core.Command;
using GFramework.Core.Extensions;
using MyGFrameworkGame.scripts.Model;
using MyGFrameworkGame.scripts.Utility;
namespace MyGFrameworkGame.scripts.Command;
public class IncreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
var model = this.GetModel<ICounterModel>()!;
var utility = this.GetUtility<ICounterUtility>()!;
// ✅ 使用 Utility 检查规则
if (!utility.CanIncrease(model.Count))
{
return; // 超过上限,不执行
}
model.Increment();
}
}
```
### 运行游戏
现在点击增加按钮计数器最多只能到 **20**
## Utility 的优势
### 1. 规则复用
假设多个地方需要检查上限
```csharp
// Command 中
if (!utility.CanIncrease(model.Count)) return;
// System 中
if (!utility.CanIncrease(value)) { /* ... */ }
// Controller 中
if (utility.CanIncrease(count)) { /* ... */ }
```
规则只写一次到处可用
### 2. 易于修改
需要改上限为 100
```csharp
// 只需修改注册时的参数
architecture.RegisterUtility<ICounterUtility>(new CounterUtility(maxCount: 100));
```
不需要改任何业务代码
### 3. 易于测试
```csharp
[Test]
public void CanIncrease_WhenAtMax_ReturnsFalse()
{
var utility = new CounterUtility(maxCount: 20);
Assert.IsFalse(utility.CanIncrease(20));
Assert.IsTrue(utility.CanIncrease(19));
}
```
纯函数测试简单
## 引入 System
### System 是什么?
现在有个新需求
- Count > 10 时,输出提示信息 "Count 超过 10"
- 当 Count < -10 输出提示信息 "Count 小于 -10"
这些逻辑应该写在哪里
** 不应该在 Command **
```csharp
protected override void OnExecute()
{
model.Increment();
// ❌ Command 不应该关心这些"连锁反应"
if (model.Count > 10) { /* ... */ }
if (model.Count < -10) { /* ... */ }
}
```
**为什么?**
- Command 只应该关心"行为"
- 不应该关心"系统状态带来的连锁反应"
** 应该在 System **
**System系统** 负责响应状态变化执行系统级逻辑
### System 的职责
监听状态变化
执行规则检查
触发系统级反应
协调多个 Model 的交互
System **不应该**
直接修改 Model应通过 Command
包含 UI 逻辑
### 1. 定义 System 接口
`scripts/system/` 创建 `ICounterThresholdSystem.cs`
```csharp
namespace MyGFrameworkGame.scripts.System;
/// <summary>
/// 计数器阈值检查系统接口
/// </summary>
public interface ICounterThresholdSystem
{
/// <summary>
/// 检查当前计数值是否超过阈值
/// </summary>
void CheckThreshold(int count);
}
```
### 2. 实现 System 类
`scripts/system/` 创建 `CounterThresholdSystem.cs`
```csharp
using GFramework.Core.Extensions;
using GFramework.Core.System;
using Godot;
using MyGFrameworkGame.scripts.Model;
namespace MyGFrameworkGame.scripts.System;
/// <summary>
/// 计数器阈值检查系统
/// </summary>
public class CounterThresholdSystem : AbstractSystem, ICounterThresholdSystem
{
/// <summary>
/// 初始化时注册事件监听
/// </summary>
protected override void OnInit()
{
// 监听计数变化事件
this.RegisterEvent<CounterModel.ChangedCountEvent>(e =>
{
CheckThreshold(e.Count);
});
}
/// <summary>
/// 检查阈值
/// </summary>
public void CheckThreshold(int count)
{
if (count > 10)
{
GD.Print("Count 超过 10");
}
if (count < -10)
{
GD.Print("Count 小于 -10");
}
}
}
```
::: tip System 的生命周期
`AbstractSystem` 会在注册时自动调用 `OnInit()`所以事件监听会在系统初始化时完成
:::
### 3. 注册 System
编辑 `scripts/module/SystemModule.cs`
```csharp
using GFramework.Core.Abstractions.Architecture;
using GFramework.Game.Architecture;
using MyGFrameworkGame.scripts.System;
namespace MyGFrameworkGame.scripts.module;
/// <summary>
/// 系统模块,负责注册所有的系统逻辑
/// </summary>
public class SystemModule : AbstractModule
{
public override void Install(IArchitecture architecture)
{
// 注册阈值检查系统
architecture.RegisterSystem<ICounterThresholdSystem>(new CounterThresholdSystem());
}
}
```
### 4. 运行游戏
启动游戏当计数超过 10 或小于 -10 输出面板会显示提示信息
![阈值提示](../assets/basic/image-20260211234946253.png)
## System 的优势
### 1. 关注点分离
**Command 负责"做什么"**
```csharp
model.Increment(); // 执行操作
```
**System 负责"响应状态"**
```csharp
if (count > 10) { /* 触发反应 */ }
```
### 2. 逻辑集中
假设将来需求变更
- 达到 5 播放音效
- 达到 15 触发警告
- 达到 20 锁定按钮
** 如果写在 Command**
```csharp
// 在 N 个 Command 里重复相同逻辑
if (count == 5) PlaySound();
if (count == 15) ShowWarning();
if (count == 20) LockButton();
```
** 集中在 System**
```csharp
public void CheckThreshold(int count)
{
if (count == 5) PlaySound();
if (count == 15) ShowWarning();
if (count == 20) LockButton();
}
```
只需修改一处
### 3. 支持复杂规则
System 可以协调多个 Model
```csharp
protected override void OnInit()
{
// 同时监听多个事件
this.RegisterEvent<CounterChangedEvent>(e => { /* ... */ });
this.RegisterEvent<ScoreChangedEvent>(e => { /* ... */ });
// 当两个条件同时满足时触发
if (counter > 10 && score > 100)
{
UnlockAchievement();
}
}
```
## 完整架构总览
现在我们的架构已经完整
```
┌─────────────┐
│ View │ Godot UI 节点
└──────┬──────┘
┌──────▼──────┐
│ Controller │ 将用户操作转为命令
└──────┬──────┘
┌──────▼──────┐
│ Command │ 封装业务逻辑
└──────┬──────┘
┌──────▼──────┐
│ Model │ 存储状态,发送事件
└──────┬──────┘
├─────────┐
│ │
┌──────▼──────┐ │
│ Event │ │
└──────┬──────┘ │
│ │
┌──────▼──────┐ │
│ System │ │ 响应状态变化
└──────┬──────┘ │
│ │
┌──────▼──────┐ │
│ Utility │◄─┘ 提供业务规则
└─────────────┘
```
### 数据流
```
用户点击按钮
Controller: SendCommand(IncreaseCommand)
Command: GetUtility().CanIncrease() → 检查规则
Command: GetModel().Increment() → 修改状态
Model: SendEvent(ChangedCountEvent) → 发送事件
├→ Controller: RegisterEvent → 更新 UI
└→ System: RegisterEvent → 检查阈值
```
## 各层职责总结
| 层级 | 职责 | 示例 |
|----------------|------------|-------------------|
| **View** | 呈现 UI接收输入 | LabelButton |
| **Controller** | 转发用户意图 | 将点击转为命令 |
| **Command** | 执行业务操作 | 增加计数 |
| **Model** | 存储状态发送事件 | 计数器的值 |
| **Event** | 通知状态变化 | ChangedCountEvent |
| **System** | 响应状态执行规则 | 阈值检查 |
| **Utility** | 提供业务规则 | 上限验证 |
## 设计原则
### 单向数据流
```
Action → Command → Model → Event → View/System
```
- 数据总是单向流动
- 没有循环依赖
- 易于理解和调试
### 关注点分离
```
Controller → "做什么"
Command → "怎么做"
Model → "状态是什么"
System → "状态变化后做什么"
Utility → "规则是什么"
```
### 依赖倒置
```
都依赖接口,不依赖具体实现
ICounterModel ← CounterModel
ICounterUtility ← CounterUtility
ICounterThresholdSystem ← CounterThresholdSystem
```
## 何时使用 Utility vs System
### Utility
**无状态的计算**
```csharp
utility.CanIncrease(count)
utility.Clamp(value)
utility.ValidateEmail(email)
```
**业务规则验证**
```csharp
if (!utility.CanPurchase(player, item)) return;
```
### System
**状态驱动的逻辑**
```csharp
if (count > 10) { /* 触发某些事情 */ }
```
**协调多个 Model**
```csharp
if (player.Level > 10 && achievement.Count > 5) { /* ... */ }
```
**系统级反应**
```csharp
PlaySound();
ShowNotification();
UpdateAchievement();
```
## 核心收获
通过本章我们学到了
| 概念 | 解释 |
|-------------|---------------|
| **Utility** | 无状态的业务规则和计算 |
| **System** | 响应状态变化执行系统逻辑 |
| **关注点分离** | 每一层专注自己的职责 |
| **单向数据流** | 数据流向清晰可控 |
| **事件驱动** | 通过事件解耦组件 |
## 完整代码回顾
### 项目结构
```
MyGFrameworkGame/
├── scripts/
│ ├── architecture/
│ │ └── GameArchitecture.cs
│ ├── module/
│ │ ├── ModelModule.cs
│ │ ├── SystemModule.cs
│ │ └── UtilityModule.cs
│ ├── model/
│ │ ├── ICounterModel.cs
│ │ └── CounterModel.cs
│ ├── command/
│ │ ├── IncreaseCountCommand.cs
│ │ └── DecreaseCountCommand.cs
│ ├── utility/
│ │ ├── ICounterUtility.cs
│ │ └── CounterUtility.cs
│ ├── system/
│ │ ├── ICounterThresholdSystem.cs
│ │ └── CounterThresholdSystem.cs
│ └── app/
│ └── App.cs
├── global/
│ └── GameEntryPoint.cs
└── scenes/
└── App.tscn
```
### 架构层次
```
GameEntryPoint (入口)
GameArchitecture (架构)
Module (模块)
├── ModelModule → CounterModel
├── SystemModule → CounterThresholdSystem
└── UtilityModule → CounterUtility
```
## 下一步
恭喜你已经完成了基础教程的核心内容掌握了 GFramework 的完整架构设计
在最后一章中我们将
- 回顾整个架构设计
- 总结最佳实践
- 解答常见问题
- 指引下一步学习方向
👉 [第 7 章:总结与最佳实践](./07-summary.md)
---
::: details 本章检查清单
- [ ] ICounterUtility CounterUtility 已创建
- [ ] Utility 已注册到 UtilityModule
- [ ] IncreaseCountCommand 使用 Utility 检查规则
- [ ] ICounterThresholdSystem CounterThresholdSystem 已创建
- [ ] System 已注册到 SystemModule
- [ ] 运行游戏计数最大为 20
- [ ] 超过阈值时能看到提示信息
- [ ] 理解了 Utility System 的职责区别
:::
::: tip 思考题
1. 如果需要在多个 System 之间通信应该怎么做
2. Utility System 哪个应该先注册为什么
3. 如何实现"计数为偶数时播放音效"的功能
这些问题会在进阶教程中探讨
:::