docs(tutorials): 更新基础教程导航结构并添加完整教程内容
- 重构教程导航,将基础教程拆分为多个子章节 - 添加第1章:环境准备,包含.NET SDK和Godot引擎安装指南 - 添加第2章:项目创建与初始化,介绍GFramework项目结构搭建 - 添加第3章:基础计数器实现,演示传统MVC模式及问题分析 - 添加第4章:引入Model重构,展示GFramework的Model层设计 - 配置教程间的前后导航链接 - 更新导航菜单结构,支持折叠展开功能
@ -139,8 +139,21 @@ export default defineConfig({
|
||||
{
|
||||
text: '教程',
|
||||
items: [
|
||||
{ text: '入门教程', link: '/zh-CN/tutorials/getting-started' },
|
||||
{ text: '基础教程', link: '/zh-CN/tutorials/basic-tutorial' },
|
||||
{
|
||||
text: '基础教程',
|
||||
link: '/zh-CN/tutorials/basic/',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '教程概览', link: '/zh-CN/tutorials/basic/' },
|
||||
{ text: '1. 环境准备', link: '/zh-CN/tutorials/basic/01-environment' },
|
||||
{ text: '2. 项目创建与初始化', link: '/zh-CN/tutorials/basic/02-project-setup' },
|
||||
{ text: '3. 基础计数器实现', link: '/zh-CN/tutorials/basic/03-counter-basic' },
|
||||
{ text: '4. 引入 Model 重构', link: '/zh-CN/tutorials/basic/04-model-refactor' },
|
||||
{ text: '5. 命令系统优化', link: '/zh-CN/tutorials/basic/05-command-system' },
|
||||
{ text: '6. Utility 与 System', link: '/zh-CN/tutorials/basic/06-utility-system' },
|
||||
{ text: '7. 总结与最佳实践', link: '/zh-CN/tutorials/basic/07-summary' }
|
||||
]
|
||||
},
|
||||
{ text: 'Godot集成', link: '/zh-CN/tutorials/godot-integration' },
|
||||
{ text: '高级模式', link: '/zh-CN/tutorials/advanced-patterns' }
|
||||
]
|
||||
|
||||
BIN
docs/zh-CN/tutorials/assets/basic/image-20260211210657387.png
Normal file
|
After Width: | Height: | Size: 690 KiB |
BIN
docs/zh-CN/tutorials/assets/basic/image-20260211211756993.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
docs/zh-CN/tutorials/assets/basic/image-20260211214905664.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
docs/zh-CN/tutorials/assets/basic/image-20260211215031192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/zh-CN/tutorials/assets/basic/image-20260211215626940.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
docs/zh-CN/tutorials/assets/basic/image-20260211222402064.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/zh-CN/tutorials/assets/basic/image-20260211223654625.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
docs/zh-CN/tutorials/assets/basic/image-20260211234946253.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
@ -1,733 +0,0 @@
|
||||
# GFramework 基础教程
|
||||
|
||||
这是一个循序渐进的教程,将带你从零开始学习 GFramework,通过构建一个计数器应用来理解框架的核心概念和最佳实践。
|
||||
|
||||
## 目录
|
||||
|
||||
### 第一部分:入门准备
|
||||
|
||||
- [环境准备](#环境准备)
|
||||
- [GFramework 核心概念](#gframework-核心概念)
|
||||
|
||||
### 第二部分:基础实践
|
||||
|
||||
- [创建第一个项目](#创建第一个项目)
|
||||
- [实现基础计数器](#实现基础计数器)
|
||||
|
||||
### 第三部分:架构深化
|
||||
|
||||
- [Model层设计](#model层设计)
|
||||
|
||||
### 第四部分:架构完善
|
||||
|
||||
- [System层实现](#system层实现)
|
||||
- [Utility层设计](#utility层设计)
|
||||
|
||||
### 第五部分:架构整合
|
||||
|
||||
- [完整架构集成](#完整架构集成)
|
||||
|
||||
### 第六部分:进阶理解
|
||||
|
||||
- [架构原理深入](#架构原理深入)
|
||||
- [最佳实践总结](#最佳实践总结)
|
||||
|
||||
---
|
||||
|
||||
## 环境准备
|
||||
|
||||
### 系统要求
|
||||
|
||||
- **操作系统**: Windows 10+, macOS 10.15+, 或 Linux
|
||||
- **.NET SDK**: 6.0 或更高版本
|
||||
- **Godot 引擎**: 4.5.1 或更高版本
|
||||
- **IDE**: Visual Studio 2022+, JetBrains Rider, 或 VS Code
|
||||
|
||||
### 安装 .NET SDK
|
||||
|
||||
1. 访问 [.NET 官网](https://dotnet.microsoft.com/download)
|
||||
2. 下载并安装 .NET 6.0 SDK
|
||||
3. 验证安装:
|
||||
|
||||
```bash
|
||||
dotnet --version
|
||||
# 应该显示 6.0.x 或更高版本
|
||||
```
|
||||
|
||||
### 安装 Godot
|
||||
|
||||
1. 访问 [Godot 官网](https://godotengine.org/download)
|
||||
2. 下载 Godot (Mono版)
|
||||
3. 解压到合适的位置并启动
|
||||
4. 在编辑器设置中确认 .NET 支持
|
||||
|
||||
### 验证环境
|
||||
|
||||
创建一个测试项目验证环境:
|
||||
|
||||
```bash
|
||||
# 创建测试项目
|
||||
dotnet new console -n TestProject
|
||||
cd TestProject
|
||||
|
||||
# 添加 GFramework 引用
|
||||
dotnet add package GeWuYou.GFramework.Core
|
||||
dotnet add package GeWuYou.GFramework.Godot
|
||||
|
||||
# 编译测试
|
||||
dotnet build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GFramework 核心概念
|
||||
|
||||
### MVC 架构模式简介
|
||||
|
||||
在传统的 MVC(Model-View-Controller)架构中:
|
||||
|
||||
- **Model(模型)**: 管理应用程序的数据和业务逻辑
|
||||
- **View(视图)**: 负责数据的展示和用户界面
|
||||
- **Controller(控制器)**: 处理用户输入,协调 Model 和 View
|
||||
|
||||
### GFramework 架构组件概述
|
||||
|
||||
GFramework 在传统 MVC 基础上进行了扩展,包含以下核心组件:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[GFramework Architecture] --> B[Model]
|
||||
A --> C[View]
|
||||
A --> D[Controller]
|
||||
A --> E[System]
|
||||
A --> F[Utility]
|
||||
A --> G[EventBus]
|
||||
|
||||
B --> B1[数据模型层]
|
||||
C --> C1[视图层]
|
||||
D --> D1[控制器层]
|
||||
E --> E1[系统业务逻辑层]
|
||||
F --> F1[工具服务层]
|
||||
G --> G1[事件总线]
|
||||
```
|
||||
|
||||
### 为什么选择事件驱动架构
|
||||
|
||||
传统的直接调用方式存在耦合问题:
|
||||
|
||||
```csharp
|
||||
// 传统方式 - 强耦合
|
||||
Model.Increment();
|
||||
View.UpdateDisplay(Model.Count);
|
||||
```
|
||||
|
||||
事件驱动架构的优势:
|
||||
|
||||
```csharp
|
||||
// 事件驱动 - 松耦合
|
||||
Model.Increment(); // 只关心业务逻辑
|
||||
// View 通过事件订阅自动更新
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 创建第一个项目
|
||||
|
||||
### 项目结构规划
|
||||
|
||||
创建以下文件夹结构:
|
||||
|
||||
```text
|
||||
MyGFrameworkGame/
|
||||
├── scripts/ # 脚本代码
|
||||
│ ├── architecture/ # 架构定义
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── system/ # 业务系统
|
||||
│ ├── utility/ # 工具服务
|
||||
│ ├── module/ # 模块定义
|
||||
│ └── app/ # 应用入口
|
||||
├── scenes/ # 场景文件
|
||||
├── assets/ # 游戏资源
|
||||
└── global/ # 全局类
|
||||
```
|
||||
|
||||
### 包管理配置
|
||||
|
||||
推荐使用以下包配置:
|
||||
|
||||
```bash
|
||||
# 核心框架
|
||||
dotnet add package GeWuYou.GFramework
|
||||
|
||||
# Godot 集成
|
||||
dotnet add package GeWuYou.GFramework.Godot
|
||||
|
||||
# 源码生成器(可选但推荐)
|
||||
dotnet add package GeWuYou.GFramework.SourceGenerators
|
||||
```
|
||||
|
||||
### 基础场景搭建
|
||||
|
||||
创建一个简单的 UI 场景:
|
||||
|
||||
```text
|
||||
Control (App)
|
||||
├── VBoxContainer
|
||||
│ ├── Label ("%Label")
|
||||
│ ├── HBoxContainer
|
||||
│ │ ├── Button ("%AddButton")
|
||||
│ │ └── Button ("%SubButton")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现基础计数器
|
||||
|
||||
### 传统 MVC 实现方式
|
||||
|
||||
让我们先用传统方式实现一个简单的计数器:
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
|
||||
public partial class App : Control
|
||||
{
|
||||
private Button _addButton => GetNode<Button>("%AddButton");
|
||||
private Button _subButton => GetNode<Button>("%SubButton");
|
||||
private Label _label => GetNode<Label>("%Label");
|
||||
|
||||
private int _count = 0;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_addButton.Pressed += () => {
|
||||
_count++;
|
||||
UpdateView();
|
||||
};
|
||||
|
||||
_subButton.Pressed += () => {
|
||||
_count--;
|
||||
UpdateView();
|
||||
};
|
||||
|
||||
UpdateView();
|
||||
}
|
||||
|
||||
private void UpdateView()
|
||||
{
|
||||
_label.Text = $"Count: {_count}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 识别设计问题
|
||||
|
||||
上述实现存在以下问题:
|
||||
|
||||
1. **View 与 Controller 耦合过紧**
|
||||
- UI 控件和业务逻辑直接耦合
|
||||
- 难以维护和扩展
|
||||
|
||||
2. **数据状态没有抽象**
|
||||
- 状态直接存在控制器中
|
||||
- 无法复用和独立测试
|
||||
|
||||
3. **缺乏统一上下文管理**
|
||||
- 状态管理分散
|
||||
- 跨组件通信困难
|
||||
|
||||
4. **可测试性低**
|
||||
- 业务逻辑与 Godot 节点耦合
|
||||
- 难以进行单元测试
|
||||
|
||||
### 引入 GFramework 改进
|
||||
|
||||
让我们用 GFramework 来解决这些问题:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.SourceGenerators.Abstractions.logging;
|
||||
using GFramework.SourceGenerators.Abstractions.rule;
|
||||
using Godot;
|
||||
|
||||
[ContextAware]
|
||||
[Log]
|
||||
public partial class App : Control, IController
|
||||
{
|
||||
private Button AddButton => GetNode<Button>("%AddButton");
|
||||
private Button SubButton => GetNode<Button>("%SubButton");
|
||||
private Label Label => GetNode<Label>("%Label");
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 事件驱动的视图更新
|
||||
this.RegisterEvent<CounterModel.ChangedCountEvent>(e => {
|
||||
UpdateView(e.Count);
|
||||
});
|
||||
|
||||
// 命令驱动的用户交互
|
||||
AddButton.Pressed += () => {
|
||||
this.SendCommand(new IncreaseCountCommand());
|
||||
};
|
||||
|
||||
SubButton.Pressed += () => {
|
||||
this.SendCommand(new DecreaseCountCommand());
|
||||
};
|
||||
|
||||
UpdateView();
|
||||
}
|
||||
|
||||
private void UpdateView(int count = 0)
|
||||
{
|
||||
Label.Text = $"Count: {count}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model层设计
|
||||
|
||||
### 抽象模型接口
|
||||
|
||||
首先定义计数器模型接口:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.model;
|
||||
|
||||
public interface ICounterModel : IModel
|
||||
{
|
||||
int Count { get; }
|
||||
void Increment();
|
||||
void Decrement();
|
||||
}
|
||||
```
|
||||
|
||||
### 具体模型实现
|
||||
|
||||
实现具体的计数器模型:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.Core.model;
|
||||
|
||||
public class CounterModel : AbstractModel, ICounterModel
|
||||
{
|
||||
public int Count { get; private set; }
|
||||
|
||||
protected override void OnInit() { }
|
||||
|
||||
public sealed record ChangedCountEvent
|
||||
{
|
||||
public int Count { get; init; }
|
||||
}
|
||||
|
||||
public void Increment()
|
||||
{
|
||||
Count++;
|
||||
this.SendEvent(new ChangedCountEvent { Count = Count });
|
||||
}
|
||||
|
||||
public void Decrement()
|
||||
{
|
||||
Count--;
|
||||
this.SendEvent(new ChangedCountEvent { Count = Count });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 模型注册与使用
|
||||
|
||||
在模块中注册模型:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.architecture;
|
||||
|
||||
public class ModelModule : AbstractModule
|
||||
{
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterModel(new CounterModel());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System层实现
|
||||
|
||||
### 系统模块概念
|
||||
|
||||
System 层负责处理业务逻辑和规则:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.architecture;
|
||||
|
||||
public class SystemModule : AbstractModule
|
||||
{
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 注册业务系统
|
||||
architecture.RegisterSystem(new CounterBusinessSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务逻辑封装
|
||||
|
||||
创建业务逻辑系统:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
|
||||
public class CounterBusinessSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 系统初始化逻辑
|
||||
}
|
||||
|
||||
public void ValidateCount(int count)
|
||||
{
|
||||
// 业务验证逻辑
|
||||
if (count < 0)
|
||||
throw new InvalidOperationException("计数不能为负数");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 命令模式应用
|
||||
|
||||
创建计数命令:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.command;
|
||||
using GFramework.Core.extensions;
|
||||
|
||||
public class IncreaseCountCommand : AbstractCommand
|
||||
{
|
||||
protected override void OnExecute()
|
||||
{
|
||||
var model = this.GetModel<ICounterModel>();
|
||||
model.Increment();
|
||||
}
|
||||
}
|
||||
|
||||
public class DecreaseCountCommand : AbstractCommand
|
||||
{
|
||||
protected override void OnExecute()
|
||||
{
|
||||
var model = this.GetModel<ICounterModel>();
|
||||
model.Decrement();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utility层设计
|
||||
|
||||
### 工具类职责
|
||||
|
||||
Utility 层提供无状态的辅助功能:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.architecture;
|
||||
|
||||
public class UtilityModule : AbstractModule
|
||||
{
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 注册工具服务
|
||||
architecture.RegisterUtility(new MathUtility());
|
||||
architecture.RegisterUtility(new StringUtility());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖注入管理
|
||||
|
||||
工具类的依赖注入示例:
|
||||
|
||||
```csharp
|
||||
public class MathUtility
|
||||
{
|
||||
public int Clamp(int value, int min, int max)
|
||||
{
|
||||
return Math.Max(min, Math.Min(max, value));
|
||||
}
|
||||
|
||||
public float Lerp(float a, float b, float t)
|
||||
{
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 通用功能封装
|
||||
|
||||
常用的工具方法:
|
||||
|
||||
```csharp
|
||||
public class StringUtility
|
||||
{
|
||||
public string FormatCount(int count)
|
||||
{
|
||||
return count >= 1000
|
||||
? $"{count / 1000.0:F1}K"
|
||||
: count.ToString();
|
||||
}
|
||||
|
||||
public bool IsValidName(string name)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(name) && name.Length <= 20;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整架构集成
|
||||
|
||||
### 模块化架构设计
|
||||
|
||||
整合所有模块:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
InstallModule(new ModelModule());
|
||||
InstallModule(new SystemModule());
|
||||
InstallModule(new UtilityModule());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖关系管理
|
||||
|
||||
清晰的依赖层次:
|
||||
|
||||
```text
|
||||
GameArchitecture
|
||||
├── ModelModule
|
||||
│ └── CounterModel
|
||||
├── SystemModule
|
||||
│ └── CounterBusinessSystem
|
||||
└── UtilityModule
|
||||
├── MathUtility
|
||||
└── StringUtility
|
||||
```
|
||||
|
||||
### 初始化流程
|
||||
|
||||
游戏入口点:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using Godot;
|
||||
|
||||
public partial class GameEntryPoint : Node
|
||||
{
|
||||
public static IArchitecture Architecture { get; private set; }
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Architecture = new GameArchitecture();
|
||||
Architecture.Initialize();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 上下文管理
|
||||
|
||||
使用上下文感知特性:
|
||||
|
||||
```csharp
|
||||
[ContextAware]
|
||||
public partial class App : Control, IController
|
||||
{
|
||||
// 框架自动注入上下文
|
||||
// 可以直接使用 this.GetModel<T>() 等方法
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 架构原理深入
|
||||
|
||||
### 事件驱动架构优势
|
||||
|
||||
事件驱动的核心价值:
|
||||
|
||||
```csharp
|
||||
// 发布事件
|
||||
this.SendEvent(new GameEvent.LevelCompleted { Level = 1 });
|
||||
|
||||
// 订阅事件(多个订阅者)
|
||||
this.RegisterEvent<GameEvent.LevelCompleted>(e => {
|
||||
// 更新UI
|
||||
});
|
||||
|
||||
this.RegisterEvent<GameEvent.LevelCompleted>(e => {
|
||||
// 保存进度
|
||||
});
|
||||
|
||||
this.RegisterEvent<GameEvent.LevelCompleted>(e => {
|
||||
// 播放音效
|
||||
});
|
||||
```
|
||||
|
||||
### 解耦设计思想
|
||||
|
||||
组件间的松耦合关系:
|
||||
|
||||
```text
|
||||
Model ←→ EventBus ←→ View
|
||||
↑ ↑
|
||||
└───────System────────┘
|
||||
```
|
||||
|
||||
### 可测试性提升
|
||||
|
||||
各层组件的可测试性:
|
||||
|
||||
```csharp
|
||||
// Model 层测试
|
||||
[Test]
|
||||
public void CounterModel_Increment_ShouldIncreaseCount()
|
||||
{
|
||||
var model = new CounterModel();
|
||||
model.Initialize();
|
||||
|
||||
model.Increment();
|
||||
|
||||
Assert.Equal(1, model.Count);
|
||||
}
|
||||
|
||||
// System 层测试
|
||||
[Test]
|
||||
public void CounterBusinessSystem_ValidateCount_ShouldThrowForNegative()
|
||||
{
|
||||
var system = new CounterBusinessSystem();
|
||||
system.Initialize();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
system.ValidateCount(-1));
|
||||
}
|
||||
```
|
||||
|
||||
### 扩展性考虑
|
||||
|
||||
易于扩展的架构设计:
|
||||
|
||||
```csharp
|
||||
// 添加新功能只需扩展相应层
|
||||
public class SaveSystem : AbstractSystem
|
||||
{
|
||||
public void SaveGame()
|
||||
{
|
||||
var counterModel = this.GetModel<ICounterModel>();
|
||||
// 保存逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 在模块中注册
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterSystem(new SaveSystem());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践总结
|
||||
|
||||
### 代码组织规范
|
||||
|
||||
推荐的项目结构:
|
||||
|
||||
```text
|
||||
Project/
|
||||
├── scripts/
|
||||
│ ├── architecture/
|
||||
│ │ ├── GameArchitecture.cs
|
||||
│ │ └── GameModule.cs
|
||||
│ ├── model/
|
||||
│ │ ├── ICounterModel.cs
|
||||
│ │ └── CounterModel.cs
|
||||
│ ├── system/
|
||||
│ │ ├── CounterBusinessSystem.cs
|
||||
│ │ └── SaveSystem.cs
|
||||
│ ├── utility/
|
||||
│ │ ├── MathUtility.cs
|
||||
│ │ └── StringUtility.cs
|
||||
│ ├── command/
|
||||
│ │ ├── IncreaseCountCommand.cs
|
||||
│ │ └── DecreaseCountCommand.cs
|
||||
│ └── app/
|
||||
│ └── App.cs
|
||||
└── scenes/
|
||||
```
|
||||
|
||||
### 设计模式应用
|
||||
|
||||
核心设计模式:
|
||||
|
||||
1. **观察者模式**: 事件系统
|
||||
2. **命令模式**: 业务操作封装
|
||||
3. **工厂模式**: 对象创建
|
||||
4. **单例模式**: 架构上下文
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
```csharp
|
||||
// 避免频繁事件发送
|
||||
public void BatchUpdate()
|
||||
{
|
||||
// 批量处理逻辑
|
||||
this.SendEvent(new BatchUpdateEvent { Data = batchData });
|
||||
}
|
||||
|
||||
// 合理使用事件过滤
|
||||
this.RegisterEvent<CounterModel.ChangedCountEvent>(
|
||||
e => UpdateView(e.Count),
|
||||
filter: e => e.Count % 10 == 0 // 每10次更新一次
|
||||
);
|
||||
```
|
||||
|
||||
### 常见问题解答
|
||||
|
||||
**Q: 什么时候使用 Model vs System?**
|
||||
A: Model 管理状态数据,System 处理业务逻辑
|
||||
|
||||
**Q: 如何处理异步操作?**
|
||||
A: 使用 AbstractAsyncCommand 和协程调度器
|
||||
|
||||
**Q: 如何进行模块间通信?**
|
||||
A: 通过事件总线,避免直接依赖
|
||||
|
||||
**Q: 如何管理复杂的依赖关系?**
|
||||
A: 使用模块化设计,明确依赖方向
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
通过本教程,你已经学习了:
|
||||
|
||||
1. **基础概念**: MVC 架构和事件驱动设计
|
||||
2. **实践应用**: 从传统实现到框架改进
|
||||
3. **架构分层**: Model、System、Utility 各层职责
|
||||
4. **核心原理**: 解耦设计和可测试性
|
||||
5. **最佳实践**: 代码组织和设计模式应用
|
||||
|
||||
现在你已经具备了使用 GFramework 构建游戏应用的基础知识,可以开始创建更复杂的游戏功能了!
|
||||
352
docs/zh-CN/tutorials/basic/01-environment.md
Normal file
@ -0,0 +1,352 @@
|
||||
---
|
||||
prev:
|
||||
text: '教程首页'
|
||||
link: './index'
|
||||
next:
|
||||
text: '项目创建与初始化'
|
||||
link: './02-project-setup'
|
||||
---
|
||||
|
||||
# 第 1 章:环境准备
|
||||
|
||||
在开始使用 GFramework 之前,我们需要配置好开发环境。本章将指导你完成所有必要工具的安装和验证。
|
||||
|
||||
## 系统要求
|
||||
|
||||
### 操作系统
|
||||
|
||||
- **Windows**:Windows 10 或更高版本(推荐 Windows 11)
|
||||
- **macOS**:macOS 10.15 Catalina 或更高版本
|
||||
- **Linux**:主流发行版(Ubuntu 20.04+, Fedora 35+, Arch Linux 等)
|
||||
|
||||
### 硬件要求
|
||||
|
||||
- **CPU**:双核或更高(推荐四核)
|
||||
- **内存**:8 GB RAM(推荐 16 GB)
|
||||
- **存储**:至少 5 GB 可用空间
|
||||
- **显卡**:支持 OpenGL 3.3 或更高
|
||||
|
||||
## 安装 .NET SDK
|
||||
|
||||
GFramework 基于 .NET 6.0+,需要先安装 .NET SDK。
|
||||
|
||||
### Windows
|
||||
|
||||
1. 访问 [.NET 官方下载页](https://dotnet.microsoft.com/download)
|
||||
2. 下载 **.NET 6.0 SDK** 或更高版本
|
||||
3. 运行安装程序,按照提示完成安装
|
||||
4. 安装完成后,打开命令提示符或 PowerShell
|
||||
|
||||
### macOS
|
||||
|
||||
**方式一:使用官方安装包**
|
||||
|
||||
```bash
|
||||
# 访问官网下载 .pkg 安装包
|
||||
# https://dotnet.microsoft.com/download
|
||||
```
|
||||
|
||||
**方式二:使用 Homebrew**
|
||||
|
||||
```bash
|
||||
brew install --cask dotnet-sdk
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
**Ubuntu/Debian**
|
||||
|
||||
```bash
|
||||
# 添加 Microsoft 包仓库
|
||||
wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
rm packages-microsoft-prod.deb
|
||||
|
||||
# 安装 .NET SDK
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dotnet-sdk-6.0
|
||||
```
|
||||
|
||||
**Fedora**
|
||||
|
||||
```bash
|
||||
sudo dnf install dotnet-sdk-6.0
|
||||
```
|
||||
|
||||
**Arch Linux**
|
||||
|
||||
```bash
|
||||
sudo pacman -S dotnet-sdk
|
||||
```
|
||||
|
||||
### 验证 .NET 安装
|
||||
|
||||
打开终端或命令提示符,运行:
|
||||
|
||||
```bash
|
||||
dotnet --version
|
||||
```
|
||||
|
||||
你应该看到类似以下输出:
|
||||
|
||||
```
|
||||
6.0.428
|
||||
```
|
||||
|
||||
或更高版本(如 7.0.x, 8.0.x)。
|
||||
|
||||
::: tip 提示
|
||||
建议安装最新的 LTS(长期支持)版本。本教程兼容 .NET 6.0 及以上所有版本。
|
||||
:::
|
||||
|
||||
## 安装 Godot 引擎
|
||||
|
||||
GFramework 的 Godot 集成需要 **Godot 4.5.1** 或更高版本,并且必须是 **.NET 版本**(不是标准版)。
|
||||
|
||||
### 下载 Godot
|
||||
|
||||
1. 访问 [Godot 官方下载页](https://godotengine.org/download)
|
||||
2. 选择 **Godot 4.5.x - .NET** 版本
|
||||
- **Windows**: 下载 `Godot_v4.5.x-stable_mono_win64.zip`
|
||||
- **macOS**: 下载 `Godot_v4.5.x-stable_mono_macos.universal.zip`
|
||||
- **Linux**: 下载 `Godot_v4.5.x-stable_mono_linux_x86_64.zip`
|
||||
|
||||
### 安装 Godot
|
||||
|
||||
**Windows**
|
||||
|
||||
1. 解压下载的 ZIP 文件
|
||||
2. 将文件夹移动到合适的位置(如 `C:\Godot\`)
|
||||
3. 双击 `Godot_v4.5.x-stable_mono_win64.exe` 启动
|
||||
|
||||
**macOS**
|
||||
|
||||
1. 解压下载的 ZIP 文件
|
||||
2. 将 `Godot.app` 拖动到 `应用程序` 文件夹
|
||||
3. 首次运行时,右键点击 → 打开(绕过安全检查)
|
||||
|
||||
**Linux**
|
||||
|
||||
1. 解压下载的 ZIP 文件
|
||||
2. 添加可执行权限:
|
||||
|
||||
```bash
|
||||
chmod +x Godot_v4.5.x-stable_mono_linux.x86_64
|
||||
```
|
||||
|
||||
3. 运行:
|
||||
|
||||
```bash
|
||||
./Godot_v4.5.x-stable_mono_linux.x86_64
|
||||
```
|
||||
|
||||
### 配置 Godot .NET 支持
|
||||
|
||||
首次启动 Godot 时,编辑器会自动检测 .NET SDK。
|
||||
|
||||
1. 打开 Godot 编辑器
|
||||
2. 点击菜单 **编辑器 → 编辑器设置**
|
||||
3. 在左侧导航中找到 **Mono → Builds**
|
||||
4. 确认 **Build Tool** 设置为 `dotnet CLI`
|
||||
|
||||
如果 Godot 提示找不到 .NET SDK,确保:
|
||||
|
||||
- .NET SDK 已正确安装
|
||||
- 终端中能运行 `dotnet --version`
|
||||
- 重启 Godot 编辑器
|
||||
|
||||
::: warning 注意
|
||||
必须下载 **.NET (Mono) 版本** 的 Godot,标准版不支持 C# 开发。文件名中应包含 `mono` 字样。
|
||||
:::
|
||||
|
||||
## 安装 IDE(可选但推荐)
|
||||
|
||||
虽然可以使用任何文本编辑器,但专业的 IDE 能大幅提升开发效率。
|
||||
|
||||
### Visual Studio 2022(推荐 Windows 用户)
|
||||
|
||||
1. 下载 [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)(免费)
|
||||
2. 安装时选择以下工作负载:
|
||||
- **.NET 桌面开发**
|
||||
- **游戏开发 → 使用 Unity 的游戏开发**(可选,提供更好的游戏开发工具)
|
||||
3. 确保勾选 **.NET 6.0 SDK**
|
||||
|
||||
### JetBrains Rider(推荐跨平台用户)
|
||||
|
||||
1. 下载 [JetBrains Rider](https://www.jetbrains.com/rider/)
|
||||
2. 安装并激活(提供 30 天试用,或使用社区许可证)
|
||||
3. 首次启动时,Rider 会自动检测 .NET SDK
|
||||
|
||||
**优势**:
|
||||
|
||||
- 跨平台支持(Windows, macOS, Linux)
|
||||
- 出色的代码补全和重构工具
|
||||
- 原生支持 Godot 项目
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
1. 下载 [Visual Studio Code](https://code.visualstudio.com/)
|
||||
2. 安装以下扩展:
|
||||
- **C# (Microsoft)**
|
||||
- **C# Dev Kit**
|
||||
- **godot-tools**
|
||||
|
||||
::: tip IDE 选择建议
|
||||
|
||||
- **Windows 用户**:Visual Studio 2022 或 Rider
|
||||
- **macOS/Linux 用户**:Rider 或 VS Code
|
||||
- **轻量级需求**:VS Code
|
||||
:::
|
||||
|
||||
## 环境验证
|
||||
|
||||
完成所有安装后,让我们创建一个测试项目来验证环境。
|
||||
|
||||
### 1. 创建测试控制台项目
|
||||
|
||||
打开终端,运行:
|
||||
|
||||
```bash
|
||||
# 创建新的控制台项目
|
||||
dotnet new console -n GFrameworkTest
|
||||
cd GFrameworkTest
|
||||
|
||||
# 添加 GFramework 核心包
|
||||
dotnet add package GeWuYou.GFramework.Core
|
||||
|
||||
# 编译测试
|
||||
dotnet build
|
||||
```
|
||||
|
||||
如果编译成功,说明 .NET 环境配置正确。
|
||||
|
||||
### 2. 创建测试 Godot 项目
|
||||
|
||||
1. 打开 Godot 编辑器
|
||||
2. 点击 **新建项目**
|
||||
3. 选择项目路径,命名为 `GodotTest`
|
||||
4. **渲染器** 选择 **Forward+** 或 **Mobile**
|
||||
5. 点击 **创建并编辑**
|
||||
|
||||
项目创建后,Godot 会提示初始化 C# 项目:
|
||||
|
||||

|
||||
|
||||
点击 **创建 C# 解决方案**,Godot 会自动生成 `.csproj` 文件。
|
||||
|
||||
### 3. 验证 Godot C# 支持
|
||||
|
||||
在 Godot 编辑器中:
|
||||
|
||||
1. 右键点击 **文件系统** 面板的根目录
|
||||
2. 选择 **创建新脚本**
|
||||
3. **语言** 选择 **C#**
|
||||
4. 脚本名称输入 `TestScript.cs`
|
||||
5. 点击 **创建**
|
||||
|
||||
如果脚本成功创建并打开外部编辑器,说明 Godot 和 .NET 集成正常。
|
||||
|
||||
### 4. 完整验证脚本
|
||||
|
||||
将以下代码粘贴到 `TestScript.cs`:
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using System;
|
||||
|
||||
public partial class TestScript : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
GD.Print("✅ Godot + .NET 环境配置成功!");
|
||||
GD.Print($"✅ .NET 版本: {Environment.Version}");
|
||||
GD.Print("✅ 准备开始使用 GFramework!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在场景中添加一个 Node 节点,附加这个脚本,然后点击 **运行场景**(F6)。
|
||||
|
||||
如果在输出面板看到:
|
||||
|
||||
```
|
||||
✅ Godot + .NET 环境配置成功!
|
||||
✅ .NET 版本: 6.0.x
|
||||
✅ 准备开始使用 GFramework!
|
||||
```
|
||||
|
||||
恭喜!你的环境已经完全配置好了!
|
||||
|
||||
## 常见问题排查
|
||||
|
||||
### Godot 找不到 .NET SDK
|
||||
|
||||
**症状**:Godot 提示"无法找到 .NET SDK"
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 确认终端中能运行 `dotnet --version`
|
||||
2. 重启 Godot 编辑器
|
||||
3. 检查 **编辑器设置 → Mono → Builds** 中的 .NET 路径
|
||||
4. 尝试手动指定 .NET SDK 路径
|
||||
|
||||
### MSBuild 错误
|
||||
|
||||
**症状**:编译时提示 MSBuild 相关错误
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 清理并重新生成项目
|
||||
dotnet clean
|
||||
dotnet build
|
||||
```
|
||||
|
||||
### macOS 安全限制
|
||||
|
||||
**症状**:macOS 阻止 Godot 运行
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 右键点击 `Godot.app`
|
||||
2. 选择 **打开**(不是双击)
|
||||
3. 在弹出的对话框中点击 **打开**
|
||||
4. 后续可以正常双击启动
|
||||
|
||||
### Linux 缺少依赖
|
||||
|
||||
**症状**:Linux 下 Godot 无法启动
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install libxcursor1 libxinerama1 libxi6 libxrandr2 libgl1
|
||||
|
||||
# Fedora
|
||||
sudo dnf install libXcursor libXinerama libXi libXrandr mesa-libGL
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
环境配置完成!现在你已经准备好开始使用 GFramework 了。
|
||||
|
||||
在下一章中,我们将:
|
||||
|
||||
- 创建一个新的 Godot 项目
|
||||
- 引入 GFramework NuGet 包
|
||||
- 搭建基础的项目架构
|
||||
- 创建游戏入口点
|
||||
|
||||
👉 [第 2 章:项目创建与初始化](./02-project-setup.md)
|
||||
|
||||
---
|
||||
|
||||
::: details 本章检查清单
|
||||
|
||||
- [ ] .NET SDK 已安装并可以运行 `dotnet --version`
|
||||
- [ ] Godot 4.5.1+ .NET 版本已安装
|
||||
- [ ] IDE 已安装(Visual Studio / Rider / VS Code)
|
||||
- [ ] 测试项目成功编译
|
||||
- [ ] Godot 能够创建和运行 C# 脚本
|
||||
:::
|
||||
484
docs/zh-CN/tutorials/basic/02-project-setup.md
Normal file
@ -0,0 +1,484 @@
|
||||
---
|
||||
prev:
|
||||
text: '环境准备'
|
||||
link: './01-environment'
|
||||
next:
|
||||
text: '基础计数器实现'
|
||||
link: './03-counter-basic'
|
||||
---
|
||||
|
||||
# 第 2 章:项目创建与初始化
|
||||
|
||||
本章将指导你创建一个新的 Godot 项目,引入 GFramework,并搭建基础的架构结构。
|
||||
|
||||
## 创建 Godot 项目
|
||||
|
||||
### 1. 新建项目
|
||||
|
||||
1. 打开 Godot 编辑器
|
||||
2. 点击 **新建项目**
|
||||
3. 配置项目信息:
|
||||
- **项目名称**:`MyGFrameworkGame`
|
||||
- **项目路径**:选择合适的文件夹
|
||||
- **渲染器**:Forward+ 或 Mobile(根据需求选择)
|
||||
4. 点击 **创建并编辑**
|
||||
|
||||
### 2. 初始化 C# 项目
|
||||
|
||||
项目创建后,Godot 会提示初始化 C# 支持:
|
||||
|
||||

|
||||
|
||||
点击 **创建 C# 解决方案**,Godot 会自动生成:
|
||||
|
||||
- `MyGFrameworkGame.csproj` - C# 项目文件
|
||||
- `MyGFrameworkGame.sln` - 解决方案文件
|
||||
|
||||
### 3. 配置项目结构
|
||||
|
||||
在项目根目录创建以下文件夹:
|
||||
|
||||
```
|
||||
MyGFrameworkGame/
|
||||
├── scripts/ # C# 脚本代码
|
||||
│ ├── architecture/ # 架构相关
|
||||
│ ├── module/ # 模块定义
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── system/ # 系统逻辑
|
||||
│ ├── utility/ # 工具类
|
||||
│ └── app/ # 应用逻辑
|
||||
├── scenes/ # Godot 场景文件
|
||||
├── assets/ # 游戏资源(图片、音频等)
|
||||
├── global/ # 全局脚本
|
||||
└── project.godot # Godot 项目配置
|
||||
```
|
||||
|
||||
::: tip 文件夹组织建议
|
||||
这只是推荐的结构,你可以根据项目需求调整。核心原则是:
|
||||
|
||||
- **按功能分类**(如 model、system)
|
||||
- **保持清晰**(避免过深的嵌套)
|
||||
- **易于导航**(团队成员能快速找到文件)
|
||||
:::
|
||||
|
||||
## 引入 GFramework
|
||||
|
||||
### 方式一:使用命令行(推荐)
|
||||
|
||||
打开项目根目录的终端,运行:
|
||||
|
||||
```bash
|
||||
# 核心能力 + 游戏扩展(一键安装)
|
||||
dotnet add package GeWuYou.GFramework
|
||||
|
||||
# Godot 集成
|
||||
dotnet add package GeWuYou.GFramework.Godot
|
||||
|
||||
# 源码生成器(可选,但推荐)
|
||||
dotnet add package GeWuYou.GFramework.SourceGenerators
|
||||
```
|
||||
|
||||
::: details 分包安装(了解即可)
|
||||
如果需要更细粒度的控制,也可以单独安装各个包:
|
||||
|
||||
```bash
|
||||
# 核心能力
|
||||
dotnet add package GeWuYou.GFramework.Core
|
||||
dotnet add package GeWuYou.GFramework.Core.Abstractions
|
||||
|
||||
# 游戏扩展
|
||||
dotnet add package GeWuYou.GFramework.Game
|
||||
dotnet add package GeWuYou.GFramework.Game.Abstractions
|
||||
|
||||
# Godot 集成
|
||||
dotnet add package GeWuYou.GFramework.Godot
|
||||
|
||||
# 源码生成器
|
||||
dotnet add package GeWuYou.GFramework.SourceGenerators
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 方式二:使用 IDE
|
||||
|
||||
**Visual Studio / Rider**
|
||||
|
||||
1. 右键点击项目 → **管理 NuGet 包**
|
||||
2. 搜索 `GeWuYou.GFramework`
|
||||
3. 安装以下包:
|
||||
- `GeWuYou.GFramework`
|
||||
- `GeWuYou.GFramework.Godot`
|
||||
- `GeWuYou.GFramework.SourceGenerators`
|
||||
|
||||

|
||||
|
||||
### 验证安装
|
||||
|
||||
安装完成后,运行:
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
|
||||
如果编译成功,说明 GFramework 已正确引入。
|
||||
|
||||
## 搭建基础架构
|
||||
|
||||
### 1. 创建游戏架构类
|
||||
|
||||
在 `scripts/architecture/` 创建 `GameArchitecture.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.architecture;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏架构类,负责管理整个应用的模块和依赖
|
||||
/// </summary>
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
/// <summary>
|
||||
/// 安装游戏所需的各个功能模块
|
||||
/// </summary>
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 稍后在这里注册模块
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip 架构类的作用
|
||||
`GameArchitecture` 是整个应用的核心入口点,它负责:
|
||||
|
||||
- 注册和管理所有模块
|
||||
- 提供依赖注入容器
|
||||
- 管理应用生命周期
|
||||
:::
|
||||
|
||||
### 2. 创建模型模块
|
||||
|
||||
在 `scripts/module/` 创建 `ModelModule.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.architecture;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.module;
|
||||
|
||||
/// <summary>
|
||||
/// 模型模块,负责注册所有的数据模型
|
||||
/// </summary>
|
||||
public class ModelModule : AbstractModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 安装模型到架构中
|
||||
/// </summary>
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 稍后在这里注册 Model
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip Model 是什么?
|
||||
**Model(模型)** 表示游戏的状态和数据。它是简单的数据容器,负责:
|
||||
|
||||
- 存储应用状态(如计数器的值)
|
||||
- 提供数据访问接口
|
||||
- 发送状态变化事件
|
||||
|
||||
Model **不应该**包含业务逻辑或 UI 逻辑。
|
||||
:::
|
||||
|
||||
### 3. 创建系统模块
|
||||
|
||||
在 `scripts/module/` 创建 `SystemModule.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.architecture;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.module;
|
||||
|
||||
/// <summary>
|
||||
/// 系统模块,负责注册所有的系统逻辑
|
||||
/// </summary>
|
||||
public class SystemModule : AbstractModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 安装系统到架构中
|
||||
/// </summary>
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 稍后在这里注册 System
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip System 是什么?
|
||||
**System(系统)** 包含游戏的业务逻辑和规则。它负责:
|
||||
|
||||
- 响应状态变化
|
||||
- 执行游戏规则
|
||||
- 协调多个 Model 的交互
|
||||
|
||||
示例:设置系统、UI 路由系统、场景管理系统等。
|
||||
:::
|
||||
|
||||
### 4. 创建工具模块
|
||||
|
||||
在 `scripts/module/` 创建 `UtilityModule.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.architecture;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.module;
|
||||
|
||||
/// <summary>
|
||||
/// 工具模块,负责注册所有的工具类
|
||||
/// </summary>
|
||||
public class UtilityModule : AbstractModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 安装工具类到架构中
|
||||
/// </summary>
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 稍后在这里注册 Utility
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip Utility 是什么?
|
||||
**Utility(工具类)** 提供可复用的辅助函数,负责:
|
||||
|
||||
- 无状态的计算和验证
|
||||
- 数据转换和格式化
|
||||
- 纯函数式的业务规则
|
||||
|
||||
示例:数学计算、数据校验、文件操作等。
|
||||
:::
|
||||
|
||||
### 5. 注册模块到架构
|
||||
|
||||
回到 `GameArchitecture.cs`,注册刚创建的模块:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using MyGFrameworkGame.scripts.module;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.architecture;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏架构类,负责管理整个应用的模块和依赖
|
||||
/// </summary>
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
/// <summary>
|
||||
/// 安装游戏所需的各个功能模块
|
||||
/// </summary>
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 按顺序安装模块
|
||||
InstallModule(new ModelModule()); // 数据模型
|
||||
InstallModule(new SystemModule()); // 系统逻辑
|
||||
InstallModule(new UtilityModule()); // 工具类
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 模块安装顺序
|
||||
模块的安装顺序很重要!如果 System 依赖 Model,那么 Model 必须先安装。
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. Utility(无依赖)
|
||||
2. Model(可能依赖 Utility)
|
||||
3. System(可能依赖 Model 和 Utility)
|
||||
:::
|
||||
|
||||
## 创建游戏入口点
|
||||
|
||||
### 1. 创建全局类
|
||||
|
||||
在 Godot 编辑器中:
|
||||
|
||||
1. 点击 **项目 → 项目设置**
|
||||
2. 选择 **自动加载** 标签
|
||||
3. 点击文件夹图标,选择 **新建脚本**
|
||||
4. 配置脚本:
|
||||
- **语言**:C#
|
||||
- **模板**:Node
|
||||
- **类名**:`GameEntryPoint`
|
||||
- **路径**:`global/GameEntryPoint.cs`
|
||||
5. 点击 **创建**
|
||||
6. 将脚本添加为自动加载单例
|
||||
|
||||

|
||||
|
||||
### 2. 实现入口点逻辑
|
||||
|
||||
编辑 `global/GameEntryPoint.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using Godot;
|
||||
using MyGFrameworkGame.scripts.architecture;
|
||||
|
||||
namespace MyGFrameworkGame.global;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏入口点,在游戏启动时初始化架构
|
||||
/// </summary>
|
||||
public partial class GameEntryPoint : Node
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取游戏架构的静态属性
|
||||
/// </summary>
|
||||
public static IArchitecture Architecture { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 当节点首次进入场景树时调用
|
||||
/// </summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
// 创建游戏架构实例
|
||||
Architecture = new GameArchitecture();
|
||||
|
||||
// 初始化游戏架构(会自动调用 InstallModules)
|
||||
Architecture.Initialize();
|
||||
|
||||
GD.Print("✅ GFramework 架构初始化完成!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 验证架构初始化
|
||||
|
||||
运行游戏(F5),在输出面板应该看到:
|
||||
|
||||
```
|
||||
✅ GFramework 架构初始化完成!
|
||||
```
|
||||
|
||||
如果看到这条消息,说明架构初始化成功!
|
||||
|
||||
## 项目结构总览
|
||||
|
||||
此时,你的项目结构应该如下:
|
||||
|
||||
```
|
||||
MyGFrameworkGame/
|
||||
├── scripts/
|
||||
│ ├── architecture/
|
||||
│ │ └── GameArchitecture.cs ✅ 游戏架构
|
||||
│ └── module/
|
||||
│ ├── ModelModule.cs ✅ 模型模块
|
||||
│ ├── SystemModule.cs ✅ 系统模块
|
||||
│ └── UtilityModule.cs ✅ 工具模块
|
||||
├── global/
|
||||
│ └── GameEntryPoint.cs ✅ 入口点
|
||||
├── scenes/
|
||||
├── assets/
|
||||
├── project.godot
|
||||
├── MyGFrameworkGame.csproj ✅ C# 项目文件
|
||||
└── MyGFrameworkGame.sln ✅ 解决方案文件
|
||||
```
|
||||
|
||||
## 理解架构设计
|
||||
|
||||
让我们回顾一下刚才搭建的架构:
|
||||
|
||||
```
|
||||
GameEntryPoint (入口)
|
||||
↓
|
||||
GameArchitecture (架构核心)
|
||||
↓
|
||||
InstallModules (安装模块)
|
||||
├── ModelModule → 注册 Model
|
||||
├── SystemModule → 注册 System
|
||||
└── UtilityModule → 注册 Utility
|
||||
```
|
||||
|
||||
### 为什么要分模块?
|
||||
|
||||
1. **关注点分离**:Model、System、Utility 各司其职
|
||||
2. **依赖管理**:通过模块控制初始化顺序
|
||||
3. **可维护性**:相关功能集中在一个模块中
|
||||
4. **可扩展性**:新增功能只需添加新模块
|
||||
|
||||
### 依赖注入的优势
|
||||
|
||||
通过 `architecture.Register*()`,GFramework 提供了依赖注入能力:
|
||||
|
||||
```csharp
|
||||
// 注册时
|
||||
architecture.RegisterModel(new CounterModel());
|
||||
|
||||
// 使用时(在 Controller 或 Command 中)
|
||||
var model = this.GetModel<ICounterModel>();
|
||||
```
|
||||
|
||||
优势:
|
||||
|
||||
- **无需手动管理单例**
|
||||
- **自动处理依赖关系**
|
||||
- **便于单元测试**(可以 mock 依赖)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 编译错误:"找不到类型或命名空间"
|
||||
|
||||
**原因**:NuGet 包未正确安装
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
dotnet restore
|
||||
dotnet build
|
||||
```
|
||||
|
||||
### GameEntryPoint 未自动执行
|
||||
|
||||
**原因**:未添加为自动加载
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 打开 **项目 → 项目设置 → 自动加载**
|
||||
2. 确认 `GameEntryPoint` 在列表中
|
||||
3. 确认 **启用** 复选框已勾选
|
||||
|
||||
### 模块注册顺序错误
|
||||
|
||||
**症状**:System 初始化时找不到 Model
|
||||
|
||||
**解决方案**:
|
||||
在 `InstallModules()` 中调整顺序,确保被依赖的模块先安装。
|
||||
|
||||
## 下一步
|
||||
|
||||
基础架构搭建完成!虽然现在还没有具体功能,但我们已经建立了一个坚实的基础。
|
||||
|
||||
在下一章中,我们将:
|
||||
|
||||
- 创建一个简单的计数器 UI
|
||||
- 实现基础的增减功能
|
||||
- 分析传统实现的问题
|
||||
- 为后续的架构优化做准备
|
||||
|
||||
👉 [第 3 章:基础计数器实现](./03-counter-basic.md)
|
||||
|
||||
---
|
||||
|
||||
::: details 本章检查清单
|
||||
|
||||
- [ ] Godot 项目已创建
|
||||
- [ ] C# 解决方案已初始化
|
||||
- [ ] GFramework NuGet 包已安装
|
||||
- [ ] GameArchitecture 类已创建
|
||||
- [ ] 三大模块(Model/System/Utility)已创建
|
||||
- [ ] GameEntryPoint 入口点已配置
|
||||
- [ ] 运行游戏能看到"架构初始化完成"消息
|
||||
:::
|
||||
417
docs/zh-CN/tutorials/basic/03-counter-basic.md
Normal file
@ -0,0 +1,417 @@
|
||||
---
|
||||
prev:
|
||||
text: '项目创建与初始化'
|
||||
link: './02-project-setup'
|
||||
next:
|
||||
text: '引入 Model 重构'
|
||||
link: './04-model-refactor'
|
||||
---
|
||||
|
||||
# 第 3 章:基础计数器实现
|
||||
|
||||
在本章中,我们将创建一个简单的计数器应用。首先用最直接的方式实现功能,然后分析这种实现方式的问题,为后续的架构优化做准备。
|
||||
|
||||
## 创建 UI 场景
|
||||
|
||||
### 1. 创建主场景
|
||||
|
||||
1. 在 Godot 编辑器中,点击 **场景 → 新建场景**
|
||||
2. 选择 **用户界面** 作为根节点类型(Control)
|
||||
3. 将根节点重命名为 `App`
|
||||
4. 保存场景为 `scenes/App.tscn`
|
||||
|
||||
### 2. 搭建 UI 布局
|
||||
|
||||
添加以下节点结构:
|
||||
|
||||
```
|
||||
App (Control)
|
||||
├── CenterContainer (CenterContainer)
|
||||
│ └── VBoxContainer (VBoxContainer)
|
||||
│ ├── Label (Label) - 唯一名称:%Label
|
||||
│ ├── HBoxContainer (HBoxContainer)
|
||||
│ │ ├── SubButton (Button) - 唯一名称:%SubButton
|
||||
│ │ └── AddButton (Button) - 唯一名称:%AddButton
|
||||
```
|
||||
|
||||
::: tip 唯一名称的作用
|
||||
在 Godot 中,带 `%` 前缀的唯一名称可以让我们在代码中更方便地访问节点:
|
||||
|
||||
```csharp
|
||||
GetNode("%Label") // 无论节点在哪个层级都能找到
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 3. 配置节点属性
|
||||
|
||||
**App (Control)**
|
||||
|
||||
- **布局**:Anchor Preset → Full Rect
|
||||
|
||||
**CenterContainer**
|
||||
|
||||
- **布局**:Anchor Preset → Full Rect
|
||||
|
||||
**VBoxContainer**
|
||||
|
||||
- **主题覆盖 → 常量 → Separation**:20
|
||||
|
||||
**Label (%Label)**
|
||||
|
||||
- **文本**:Count: 0
|
||||
- **水平对齐**:Center
|
||||
- **主题覆盖 → 字体大小**:32
|
||||
|
||||
**SubButton (%SubButton)**
|
||||
|
||||
- **文本**:-
|
||||
- **最小尺寸**:宽度 60, 高度 40
|
||||
|
||||
**AddButton (%AddButton)**
|
||||
|
||||
- **文本**:+
|
||||
- **最小尺寸**:宽度 60, 高度 40
|
||||
|
||||
### 4. 最终效果
|
||||
|
||||
完成后的界面应该如下:
|
||||
|
||||

|
||||
|
||||
## 实现基础功能
|
||||
|
||||
### 1. 创建 App 脚本
|
||||
|
||||
右键点击 `App` 节点,选择 **附加脚本**:
|
||||
|
||||
- **语言**:C#
|
||||
- **模板**:Node: Basic Script
|
||||
- **路径**:`scripts/app/App.cs`
|
||||
|
||||
点击 **创建**。
|
||||
|
||||
### 2. 编写代码
|
||||
|
||||
编辑 `App.cs`,实现基础功能:
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.app;
|
||||
|
||||
/// <summary>
|
||||
/// 计数器应用的主控制器
|
||||
/// </summary>
|
||||
public partial class App : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取 Add 按钮节点
|
||||
/// </summary>
|
||||
private Button AddButton => GetNode<Button>("%AddButton");
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Sub 按钮节点
|
||||
/// </summary>
|
||||
private Button SubButton => GetNode<Button>("%SubButton");
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Label 节点
|
||||
/// </summary>
|
||||
private Label Label => GetNode<Label>("%Label");
|
||||
|
||||
/// <summary>
|
||||
/// 计数器的当前值
|
||||
/// </summary>
|
||||
private int _count;
|
||||
|
||||
/// <summary>
|
||||
/// 节点准备就绪时调用
|
||||
/// </summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
// 绑定按钮点击事件
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_count++;
|
||||
UpdateView();
|
||||
};
|
||||
|
||||
SubButton.Pressed += () =>
|
||||
{
|
||||
_count--;
|
||||
UpdateView();
|
||||
};
|
||||
|
||||
// 初始化界面
|
||||
UpdateView();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新界面显示
|
||||
/// </summary>
|
||||
private void UpdateView()
|
||||
{
|
||||
Label.Text = $"Count: {_count}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 设置为主场景
|
||||
|
||||
1. 点击 **项目 → 项目设置 → 应用 → 运行**
|
||||
2. **主场景** 选择 `res://scenes/App.tscn`
|
||||
3. 点击 **关闭**
|
||||
|
||||
### 4. 运行游戏
|
||||
|
||||
按 **F5** 运行游戏,点击 + 和 - 按钮,观察计数变化:
|
||||
|
||||

|
||||
|
||||
功能正常工作!🎉
|
||||
|
||||
## 代码分析
|
||||
|
||||
虽然功能实现了,但让我们分析一下这段代码的结构。
|
||||
|
||||
### 识别 MVC 组成部分
|
||||
|
||||
#### View(视图)
|
||||
|
||||
```csharp
|
||||
// 节点引用
|
||||
private Button AddButton => GetNode<Button>("%AddButton");
|
||||
private Button SubButton => GetNode<Button>("%SubButton");
|
||||
private Label Label => GetNode<Label>("%Label");
|
||||
|
||||
// 渲染逻辑
|
||||
private void UpdateView()
|
||||
{
|
||||
Label.Text = $"Count: {_count}";
|
||||
}
|
||||
```
|
||||
|
||||
**职责**:呈现 UI,接收用户输入
|
||||
|
||||
#### Model(数据)
|
||||
|
||||
```csharp
|
||||
// 状态数据
|
||||
private int _count;
|
||||
```
|
||||
|
||||
**职责**:保存应用状态
|
||||
|
||||
#### Controller(控制器)
|
||||
|
||||
```csharp
|
||||
// 事件绑定和业务逻辑
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_count++; // 修改 Model
|
||||
UpdateView(); // 通知 View
|
||||
};
|
||||
|
||||
SubButton.Pressed += () =>
|
||||
{
|
||||
_count--;
|
||||
UpdateView();
|
||||
};
|
||||
```
|
||||
|
||||
**职责**:响应事件,协调 Model 和 View
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
用户点击按钮
|
||||
↓
|
||||
Pressed 事件触发
|
||||
↓
|
||||
修改 _count (Model)
|
||||
↓
|
||||
调用 UpdateView() (View)
|
||||
↓
|
||||
界面更新
|
||||
```
|
||||
|
||||
## 设计问题分析
|
||||
|
||||
虽然代码简洁明了,但存在以下问题:
|
||||
|
||||
### 问题 1:View 与 Controller 耦合过紧
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_count++; // ← 业务逻辑
|
||||
UpdateView(); // ← UI 更新
|
||||
};
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- UI 控件和业务逻辑直接耦合
|
||||
- 修改逻辑可能影响界面,反之亦然
|
||||
- 当功能复杂化时,代码难以维护
|
||||
|
||||
**示例**:如果将来需要:
|
||||
|
||||
- 多个按钮触发相同逻辑
|
||||
- 多个 Label 显示相同数据
|
||||
- 支持键盘快捷键
|
||||
|
||||
当前实现会导致代码重复和混乱。
|
||||
|
||||
### 问题 2:数据状态没有抽象
|
||||
|
||||
```csharp
|
||||
private int _count;
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- 计数器直接存在 Controller 里
|
||||
- 没有独立的 Model 层
|
||||
- 无法在不同界面或场景复用数据
|
||||
- 无法单独测试逻辑
|
||||
|
||||
**示例**:如果需要:
|
||||
|
||||
- 在多个场景共享计数器状态
|
||||
- 保存和加载计数器数据
|
||||
- 实现撤销/重做功能
|
||||
|
||||
当前实现无法满足这些需求。
|
||||
|
||||
### 问题 3:缺乏统一的上下文管理
|
||||
|
||||
```csharp
|
||||
public override void _Ready()
|
||||
{
|
||||
// 事件绑定和视图更新直接写在这里
|
||||
AddButton.Pressed += ...
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- 当应用复杂化,涉及异步操作、多场景时
|
||||
- 容易出现状态不一致或上下文丢失
|
||||
- 无法在多个 Controller 间共享状态
|
||||
|
||||
**示例**:如果计数器需要:
|
||||
|
||||
- 在多个 Service 之间共享
|
||||
- 跨场景保持状态
|
||||
- 支持网络同步
|
||||
|
||||
当前实现会非常麻烦。
|
||||
|
||||
### 问题 4:可测试性低
|
||||
|
||||
```csharp
|
||||
_count++; // ← 逻辑和按钮点击事件耦合
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- `_count` 逻辑和按钮点击事件紧密耦合
|
||||
- 无法在单元测试中独立测试计数器逻辑
|
||||
- 只能在运行场景里手动点击按钮验证
|
||||
|
||||
**示例**:无法写出这样的单元测试:
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public void Increment_ShouldIncreaseCount()
|
||||
{
|
||||
var counter = new CounterModel();
|
||||
counter.Increment();
|
||||
Assert.AreEqual(1, counter.Count);
|
||||
}
|
||||
```
|
||||
|
||||
## 问题总结
|
||||
|
||||
| 问题 | 后果 | 示例场景 |
|
||||
|--------------------|-----------|-----------------|
|
||||
| View-Controller 耦合 | 代码重复,难以维护 | 多个界面显示相同数据 |
|
||||
| Model 未抽象 | 无法复用,难以测试 | 跨场景共享状态 |
|
||||
| 缺乏上下文管理 | 状态混乱,扩展困难 | 多 Controller 协作 |
|
||||
| 可测试性低 | 只能手动测试 | 业务逻辑复杂化 |
|
||||
|
||||
## 重构的必要性
|
||||
|
||||
随着项目规模增长,这些问题会被放大:
|
||||
|
||||
**场景 1:新增功能**
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_count++;
|
||||
if (_count > 10) { /* 做某事 */ }
|
||||
if (_count < -10) { /* 做别的事 */ }
|
||||
SaveToFile();
|
||||
PlaySound();
|
||||
UpdateAchievement();
|
||||
UpdateView();
|
||||
};
|
||||
```
|
||||
|
||||
→ **Controller 迅速膨胀**
|
||||
|
||||
**场景 2:跨场景共享**
|
||||
|
||||
```csharp
|
||||
// 场景 A
|
||||
private int _count;
|
||||
|
||||
// 场景 B
|
||||
private int _count; // ← 重复的状态,如何同步?
|
||||
```
|
||||
|
||||
→ **状态同步困难**
|
||||
|
||||
**场景 3:单元测试**
|
||||
|
||||
```csharp
|
||||
// 如何测试这个逻辑?
|
||||
AddButton.Pressed += () => { _count++; };
|
||||
```
|
||||
|
||||
→ **无法测试**
|
||||
|
||||
## 下一步
|
||||
|
||||
我们已经实现了基础功能,并分析了当前实现的问题。在下一章中,我们将:
|
||||
|
||||
1. **引入 Model 层**:将状态抽离到独立的 Model
|
||||
2. **使用事件系统**:实现数据驱动的 UI 更新
|
||||
3. **解耦 View 和 Controller**:让代码更清晰、可测试
|
||||
|
||||
通过这些重构,你将看到 GFramework 如何帮助我们解决这些设计问题。
|
||||
|
||||
👉 [第 4 章:引入 Model 重构](./04-model-refactor.md)
|
||||
|
||||
---
|
||||
|
||||
::: details 本章检查清单
|
||||
|
||||
- [ ] UI 场景已创建(App.tscn)
|
||||
- [ ] 节点结构正确(Label, AddButton, SubButton)
|
||||
- [ ] App.cs 脚本已创建并附加到根节点
|
||||
- [ ] 运行游戏,点击按钮能正常增减计数
|
||||
- [ ] 理解了当前实现的 4 个主要问题
|
||||
:::
|
||||
|
||||
::: tip 思考题
|
||||
|
||||
1. 如果要在另一个场景也显示这个计数器,你会怎么做?
|
||||
2. 如何实现"计数超过 10 时播放音效"的功能?
|
||||
3. 能否在不运行游戏的情况下测试计数逻辑?
|
||||
|
||||
这些问题在下一章中都会得到解答!
|
||||
:::
|
||||
552
docs/zh-CN/tutorials/basic/04-model-refactor.md
Normal file
@ -0,0 +1,552 @@
|
||||
---
|
||||
prev:
|
||||
text: '基础计数器实现'
|
||||
link: './03-counter-basic'
|
||||
next:
|
||||
text: '命令系统优化'
|
||||
link: './05-command-system'
|
||||
---
|
||||
|
||||
# 第 4 章:引入 Model 重构
|
||||
|
||||
在上一章中,我们实现了基础功能但也发现了一些设计问题。本章将通过引入 **Model 层** 和 **事件系统**,重构我们的计数器应用。
|
||||
|
||||
## 理解 Model 的作用
|
||||
|
||||
在 GFramework 中,**Model(模型)** 负责:
|
||||
|
||||
✅ **存储应用状态**:保存数据,如计数器的值
|
||||
✅ **提供数据访问接口**:通过方法暴露数据操作
|
||||
✅ **发送状态变化事件**:当数据改变时通知监听者
|
||||
|
||||
Model **不应该**:
|
||||
|
||||
❌ 包含 UI 逻辑(如更新 Label)
|
||||
❌ 直接调用 Controller 方法
|
||||
❌ 知道谁在使用它
|
||||
|
||||
## 创建 Model
|
||||
|
||||
### 1. 定义 Model 接口
|
||||
|
||||
在 `scripts/model/` 创建 `ICounterModel.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.model;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.model;
|
||||
|
||||
/// <summary>
|
||||
/// 计数器模型接口,定义计数器的基本操作
|
||||
/// </summary>
|
||||
public interface ICounterModel : IModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前计数器的值
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 将计数器的值增加 1
|
||||
/// </summary>
|
||||
void Increment();
|
||||
|
||||
/// <summary>
|
||||
/// 将计数器的值减少 1
|
||||
/// </summary>
|
||||
void Decrement();
|
||||
}
|
||||
```
|
||||
|
||||
::: tip 为什么要定义接口?
|
||||
使用接口的好处:
|
||||
|
||||
- **依赖倒置**:依赖抽象而不是具体实现
|
||||
- **易于测试**:可以创建 Mock 实现
|
||||
- **灵活替换**:可以切换不同的实现
|
||||
|
||||
这是 SOLID 原则中的 **依赖倒置原则(DIP)**。
|
||||
:::
|
||||
|
||||
### 2. 实现 Model 类
|
||||
|
||||
在 `scripts/model/` 创建 `CounterModel.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.Core.model;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.model;
|
||||
|
||||
/// <summary>
|
||||
/// 计数器模型实现
|
||||
/// </summary>
|
||||
public class CounterModel : AbstractModel, ICounterModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前计数器的值
|
||||
/// </summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 Model(可选)
|
||||
/// </summary>
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 可以在这里初始化默认值或加载数据
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计数变化事件
|
||||
/// </summary>
|
||||
public sealed record ChangedCountEvent
|
||||
{
|
||||
public int Count { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数器的值
|
||||
/// </summary>
|
||||
public void Increment()
|
||||
{
|
||||
Count++;
|
||||
// 发送事件通知监听者
|
||||
this.SendEvent(new ChangedCountEvent { Count = Count });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 减少计数器的值
|
||||
/// </summary>
|
||||
public void Decrement()
|
||||
{
|
||||
Count--;
|
||||
// 发送事件通知监听者
|
||||
this.SendEvent(new ChangedCountEvent { Count = Count });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip 事件驱动设计
|
||||
注意 `Increment` 和 `Decrement` 方法的最后一行:
|
||||
|
||||
```csharp
|
||||
this.SendEvent(new ChangedCountEvent { Count = Count });
|
||||
```
|
||||
|
||||
这是 **事件驱动架构** 的核心:
|
||||
|
||||
- Model 只负责改变状态并发送事件
|
||||
- **不关心谁在监听**
|
||||
- **不直接调用 UI 更新**
|
||||
|
||||
这实现了完全解耦!
|
||||
:::
|
||||
|
||||
### 3. 注册 Model
|
||||
|
||||
编辑 `scripts/module/ModelModule.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.architecture;
|
||||
using MyGFrameworkGame.scripts.model;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.module;
|
||||
|
||||
/// <summary>
|
||||
/// 模型模块,负责注册所有的数据模型
|
||||
/// </summary>
|
||||
public class ModelModule : AbstractModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 安装模型到架构中
|
||||
/// </summary>
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 注册 CounterModel 实例
|
||||
architecture.RegisterModel<ICounterModel>(new CounterModel());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 注意泛型参数
|
||||
|
||||
```csharp
|
||||
architecture.RegisterModel<ICounterModel>(new CounterModel());
|
||||
```
|
||||
|
||||
第一个泛型参数 `ICounterModel` 是接口类型,用于后续获取时指定类型。
|
||||
:::
|
||||
|
||||
## 重构 Controller
|
||||
|
||||
### 1. 启用上下文感知
|
||||
|
||||
编辑 `App.cs`,添加 `IController` 接口和特性:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.SourceGenerators.Abstractions.rule;
|
||||
using Godot;
|
||||
using MyGFrameworkGame.scripts.model;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.app;
|
||||
|
||||
/// <summary>
|
||||
/// 计数器应用的主控制器
|
||||
/// </summary>
|
||||
[ContextAware] // ← 启用上下文感知(由源码生成器处理)
|
||||
public partial class App : Control, IController // ← 实现 IController 接口
|
||||
{
|
||||
// ... 其他代码
|
||||
}
|
||||
```
|
||||
|
||||
::: tip ContextAware 特性
|
||||
`[ContextAware]` 特性由 **源码生成器** 处理,它会自动生成:
|
||||
|
||||
- `IArchitecture Architecture { get; }` 属性
|
||||
- 依赖注入相关的辅助代码
|
||||
|
||||
这使得我们可以使用 `this.GetModel()` 等扩展方法。
|
||||
:::
|
||||
|
||||
### 2. 获取 Model 并监听事件
|
||||
|
||||
修改 `_Ready` 方法:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.SourceGenerators.Abstractions.rule;
|
||||
using Godot;
|
||||
using MyGFrameworkGame.scripts.model;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.app;
|
||||
|
||||
[ContextAware]
|
||||
public partial class App : Control, IController
|
||||
{
|
||||
private Button AddButton => GetNode<Button>("%AddButton");
|
||||
private Button SubButton => GetNode<Button>("%SubButton");
|
||||
private Label Label => GetNode<Label>("%Label");
|
||||
|
||||
private ICounterModel _counterModel = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 从架构中获取 Model
|
||||
_counterModel = this.GetModel<ICounterModel>()!;
|
||||
|
||||
// 监听 Model 的事件
|
||||
this.RegisterEvent<CounterModel.ChangedCountEvent>(e =>
|
||||
{
|
||||
UpdateView(e.Count);
|
||||
});
|
||||
|
||||
// 按钮点击时调用 Model 的方法
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_counterModel.Increment();
|
||||
};
|
||||
|
||||
SubButton.Pressed += () =>
|
||||
{
|
||||
_counterModel.Decrement();
|
||||
};
|
||||
|
||||
// 初始化界面
|
||||
UpdateView(_counterModel.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新界面显示
|
||||
/// </summary>
|
||||
private void UpdateView(int count)
|
||||
{
|
||||
Label.Text = $"Count: {count}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 运行游戏
|
||||
|
||||
按 **F5** 运行游戏,测试功能:
|
||||
|
||||

|
||||
|
||||
功能依然正常!但现在架构更清晰了。
|
||||
|
||||
## 对比重构前后
|
||||
|
||||
### 重构前
|
||||
|
||||
```csharp
|
||||
// ❌ 状态在 Controller 里
|
||||
private int _count;
|
||||
|
||||
// ❌ UI 直接修改状态
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_count++; // 修改状态
|
||||
UpdateView(); // 更新 UI
|
||||
};
|
||||
|
||||
// ❌ 状态和 UI 耦合
|
||||
private void UpdateView()
|
||||
{
|
||||
Label.Text = $"Count: {_count}";
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- 状态、逻辑、UI 混在一起
|
||||
- 无法复用状态
|
||||
- 无法单独测试
|
||||
|
||||
### 重构后
|
||||
|
||||
```csharp
|
||||
// ✅ 状态在 Model 里
|
||||
private ICounterModel _counterModel;
|
||||
|
||||
// ✅ UI 只发起操作
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_counterModel.Increment(); // 调用 Model 方法
|
||||
};
|
||||
|
||||
// ✅ 通过事件更新 UI
|
||||
this.RegisterEvent<CounterModel.ChangedCountEvent>(e =>
|
||||
{
|
||||
UpdateView(e.Count); // 响应事件
|
||||
});
|
||||
```
|
||||
|
||||
**优势**:
|
||||
|
||||
- 关注点分离
|
||||
- Model 可复用、可测试
|
||||
- UI 只负责展示
|
||||
|
||||
## 理解事件驱动架构
|
||||
|
||||
### 数据流向
|
||||
|
||||
```
|
||||
用户点击按钮
|
||||
↓
|
||||
Controller: _counterModel.Increment()
|
||||
↓
|
||||
Model: Count++
|
||||
↓
|
||||
Model: SendEvent(ChangedCountEvent)
|
||||
↓
|
||||
Controller: RegisterEvent<ChangedCountEvent>
|
||||
↓
|
||||
Controller: UpdateView(e.Count)
|
||||
↓
|
||||
界面更新
|
||||
```
|
||||
|
||||
### 为什么不直接调用 UpdateView?
|
||||
|
||||
**方式一:直接调用(❌ 不推荐)**
|
||||
|
||||
```csharp
|
||||
public void Increment()
|
||||
{
|
||||
Count++;
|
||||
_view.UpdateView(Count); // ← Model 依赖 View
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- Model 依赖 View,强耦合
|
||||
- Model 无法复用
|
||||
- 无法扩展(如果有多个监听者怎么办?)
|
||||
|
||||
**方式二:事件驱动(✅ 推荐)**
|
||||
|
||||
```csharp
|
||||
public void Increment()
|
||||
{
|
||||
Count++;
|
||||
this.SendEvent(new ChangedCountEvent(Count)); // ← 发送事件
|
||||
}
|
||||
|
||||
// View 订阅事件
|
||||
this.RegisterEvent<ChangedCountEvent>(e => UpdateView(e.Count));
|
||||
```
|
||||
|
||||
**优势**:
|
||||
|
||||
- Model 不知道 View 的存在
|
||||
- 可以有多个订阅者
|
||||
- 完全解耦
|
||||
|
||||
### 事件系统的威力
|
||||
|
||||
假设将来需要:
|
||||
|
||||
**需求 1:多个界面显示计数器**
|
||||
|
||||
```csharp
|
||||
// Scene A
|
||||
this.RegisterEvent<ChangedCountEvent>(e => LabelA.Text = $"Count: {e.Count}");
|
||||
|
||||
// Scene B
|
||||
this.RegisterEvent<ChangedCountEvent>(e => LabelB.Text = $"Count: {e.Count}");
|
||||
```
|
||||
|
||||
无需修改 Model!
|
||||
|
||||
**需求 2:计数变化时播放音效**
|
||||
|
||||
```csharp
|
||||
this.RegisterEvent<ChangedCountEvent>(e =>
|
||||
{
|
||||
if (e.Count > 0) PlaySound("increment.wav");
|
||||
else PlaySound("decrement.wav");
|
||||
});
|
||||
```
|
||||
|
||||
只需添加新的监听者!
|
||||
|
||||
## 解决的问题
|
||||
|
||||
回顾第 3 章提出的问题:
|
||||
|
||||
### ✅ 问题 1:View-Controller 耦合
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```csharp
|
||||
// 之前:UI 直接修改状态
|
||||
_count++;
|
||||
UpdateView();
|
||||
|
||||
// 现在:UI 通过 Model 操作
|
||||
_counterModel.Increment();
|
||||
|
||||
// UI 通过事件更新
|
||||
this.RegisterEvent<ChangedCountEvent>(e => UpdateView(e.Count));
|
||||
```
|
||||
|
||||
现在的流程:`Button → Model → Event → View`,这是典型的 **单向数据流**。
|
||||
|
||||
### ✅ 问题 2:Model 未抽象
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```csharp
|
||||
// 之前:状态在 Controller 里
|
||||
private int _count;
|
||||
|
||||
// 现在:状态在独立的 Model
|
||||
private ICounterModel _counterModel;
|
||||
_counterModel = this.GetModel<ICounterModel>();
|
||||
```
|
||||
|
||||
现在:
|
||||
|
||||
- Model 可复用
|
||||
- Model 可替换(依赖接口)
|
||||
- Controller 不关心具体实现
|
||||
- **可以单独测试 Model**
|
||||
|
||||
### ✅ 问题 3:缺乏上下文管理
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```csharp
|
||||
[ContextAware]
|
||||
this.GetModel<ICounterModel>()
|
||||
this.RegisterEvent<ChangedCountEvent>(...)
|
||||
```
|
||||
|
||||
现在:
|
||||
|
||||
- Model 由 Context 提供
|
||||
- 事件系统由框架管理
|
||||
- 生命周期由框架管理
|
||||
- 不需要手动 new Model
|
||||
- 不会出现跨 Controller 状态错乱
|
||||
|
||||
### ⚠️ 问题 4:可测试性(部分改善)
|
||||
|
||||
**Model 层**:
|
||||
|
||||
```csharp
|
||||
// ✅ Model 现在完全可测试
|
||||
var model = new CounterModel();
|
||||
model.Increment();
|
||||
Assert.AreEqual(1, model.Count);
|
||||
```
|
||||
|
||||
**Controller 层**:
|
||||
|
||||
```csharp
|
||||
// ⚠️ Controller 仍然依赖 Godot 节点
|
||||
App : Control
|
||||
```
|
||||
|
||||
Controller 仍然不能在纯单元测试中实例化,但这是合理的:
|
||||
|
||||
- **View 本来就不应该被单元测试**
|
||||
- **业务逻辑已经在 Model,可以测试**
|
||||
- **Controller 只是桥梁**
|
||||
|
||||
## 核心收获
|
||||
|
||||
通过这次重构,我们学到了:
|
||||
|
||||
| 概念 | 解释 |
|
||||
|---------------|---------------------------------------|
|
||||
| **Model 的职责** | 存储状态,提供接口,发送事件 |
|
||||
| **事件驱动架构** | Model 不关心谁在监听,完全解耦 |
|
||||
| **依赖注入** | 通过 `GetModel` 获取依赖,而不是 `new` |
|
||||
| **单向数据流** | Button → Model → Event → View |
|
||||
| **关注点分离** | 数据(Model)、逻辑(Controller)、展示(View)各司其职 |
|
||||
|
||||
## 下一步
|
||||
|
||||
虽然我们引入了 Model,但 Controller 仍然承担着 **交互逻辑**:
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_counterModel.Increment(); // ← 这是交互逻辑
|
||||
};
|
||||
```
|
||||
|
||||
当功能复杂化时(如需要保存、验证、异步操作),这些逻辑会让 Controller 变得臃肿。
|
||||
|
||||
在下一章中,我们将引入 **Command(命令)模式**,进一步解耦 Controller。
|
||||
|
||||
👉 [第 5 章:命令系统优化](./05-command-system.md)
|
||||
|
||||
---
|
||||
|
||||
::: details 本章检查清单
|
||||
|
||||
- [ ] ICounterModel 接口已创建
|
||||
- [ ] CounterModel 实现已创建
|
||||
- [ ] Model 已注册到 ModelModule
|
||||
- [ ] App.cs 添加了 [ContextAware] 和 IController
|
||||
- [ ] 通过事件系统更新 UI
|
||||
- [ ] 运行游戏,功能正常
|
||||
- [ ] 理解了事件驱动架构的优势
|
||||
:::
|
||||
|
||||
::: tip 思考题
|
||||
|
||||
1. 如果要在 Model 中添加"最大值限制"功能,应该怎么做?
|
||||
2. 如果需要持久化计数器(保存到文件),应该在哪一层实现?
|
||||
3. 事件驱动和直接调用的主要区别是什么?
|
||||
|
||||
这些问题会在后续章节中继续探讨!
|
||||
:::
|
||||
535
docs/zh-CN/tutorials/basic/05-command-system.md
Normal file
@ -0,0 +1,535 @@
|
||||
---
|
||||
prev:
|
||||
text: '引入 Model 重构'
|
||||
link: './04-model-refactor'
|
||||
next:
|
||||
text: 'Utility 与 System'
|
||||
link: './06-utility-system'
|
||||
---
|
||||
|
||||
# 第 5 章:命令系统优化
|
||||
|
||||
在上一章中,我们通过 Model 和事件系统实现了数据驱动的架构。但 Controller 仍然承担着交互逻辑,本章将引入 **Command(命令)模式
|
||||
** 进一步优化。
|
||||
|
||||
## Controller 的职责问题
|
||||
|
||||
### 当前代码
|
||||
|
||||
```csharp
|
||||
public override void _Ready()
|
||||
{
|
||||
_counterModel = this.GetModel<ICounterModel>()!;
|
||||
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_counterModel.Increment(); // ← 交互逻辑
|
||||
};
|
||||
|
||||
SubButton.Pressed += () =>
|
||||
{
|
||||
_counterModel.Decrement(); // ← 交互逻辑
|
||||
};
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
看起来很简洁,但这段代码同时承担着:
|
||||
|
||||
- **表现逻辑**(View Binding):`AddButton.Pressed +=`
|
||||
- **交互逻辑**(Interaction Logic):`_counterModel.Increment()`
|
||||
|
||||
### 为什么这是问题?
|
||||
|
||||
现在只是简单的增减,但如果功能变复杂:
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += async () =>
|
||||
{
|
||||
// 1. 验证状态
|
||||
if (!CanIncrement()) return;
|
||||
|
||||
// 2. 执行业务逻辑
|
||||
await DoSomethingAsync();
|
||||
_counterModel.Increment();
|
||||
|
||||
// 3. 保存数据
|
||||
await SaveToFileAsync();
|
||||
|
||||
// 4. 播放音效
|
||||
PlaySound("increment.wav");
|
||||
|
||||
// 5. 统计埋点
|
||||
LogAnalytics("counter_incremented");
|
||||
|
||||
// 6. 更新成就
|
||||
UpdateAchievement();
|
||||
};
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- Controller 迅速膨胀
|
||||
- 逻辑难以复用(如果键盘快捷键也要增加计数?)
|
||||
- 难以测试(需要 mock 按钮)
|
||||
- 违反单一职责原则
|
||||
|
||||
## 理解 Command 模式
|
||||
|
||||
### Command 的作用
|
||||
|
||||
**Command(命令)** 是一种设计模式,它将"请求"封装成对象:
|
||||
|
||||
```
|
||||
用户操作 → Command → Model
|
||||
```
|
||||
|
||||
优势:
|
||||
|
||||
- **解耦**:Controller 不关心如何增加计数,只负责"发送命令"
|
||||
- **复用**:同一个命令可以被多个地方调用
|
||||
- **扩展**:新增逻辑只需修改命令,不影响 Controller
|
||||
- **可测试**:可以独立测试命令逻辑
|
||||
|
||||
### 职责划分
|
||||
|
||||
| 层级 | 职责 |
|
||||
|----------------|------------|
|
||||
| **Controller** | 将用户操作转换为命令 |
|
||||
| **Command** | 封装具体的业务逻辑 |
|
||||
| **Model** | 存储状态,发送事件 |
|
||||
|
||||
## 创建 Command
|
||||
|
||||
### 1. 创建增加命令
|
||||
|
||||
在 `scripts/command/` 创建 `IncreaseCountCommand.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.command;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGFrameworkGame.scripts.model;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.command;
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数器值的命令
|
||||
/// </summary>
|
||||
public class IncreaseCountCommand : AbstractCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行命令的核心逻辑
|
||||
/// </summary>
|
||||
protected override void OnExecute()
|
||||
{
|
||||
// 获取 Model 并调用方法
|
||||
var model = this.GetModel<ICounterModel>()!;
|
||||
model.Increment();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip AbstractCommand
|
||||
`AbstractCommand` 是 GFramework 提供的基类,它:
|
||||
|
||||
- 自动注入 `Architecture` 上下文
|
||||
- 提供 `GetModel`、`GetSystem`、`GetUtility` 等方法
|
||||
- 管理命令的生命周期
|
||||
:::
|
||||
|
||||
### 2. 创建减少命令
|
||||
|
||||
在 `scripts/command/` 创建 `DecreaseCountCommand.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.command;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGFrameworkGame.scripts.model;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.command;
|
||||
|
||||
/// <summary>
|
||||
/// 减少计数器值的命令
|
||||
/// </summary>
|
||||
public class DecreaseCountCommand : AbstractCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行命令的核心逻辑
|
||||
/// </summary>
|
||||
protected override void OnExecute()
|
||||
{
|
||||
var model = this.GetModel<ICounterModel>()!;
|
||||
model.Decrement();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 重构 Controller
|
||||
|
||||
### 使用命令替换直接调用
|
||||
|
||||
编辑 `App.cs`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.SourceGenerators.Abstractions.rule;
|
||||
using Godot;
|
||||
using MyGFrameworkGame.scripts.command;
|
||||
using MyGFrameworkGame.scripts.model;
|
||||
|
||||
namespace MyGFrameworkGame.scripts.app;
|
||||
|
||||
[ContextAware]
|
||||
public partial class App : Control, IController
|
||||
{
|
||||
private Button AddButton => GetNode<Button>("%AddButton");
|
||||
private Button SubButton => GetNode<Button>("%SubButton");
|
||||
private Label Label => GetNode<Label>("%Label");
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 监听事件
|
||||
this.RegisterEvent<CounterModel.ChangedCountEvent>(e =>
|
||||
{
|
||||
UpdateView(e.Count);
|
||||
});
|
||||
|
||||
// 使用命令替换直接调用
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
this.SendCommand(new IncreaseCountCommand());
|
||||
};
|
||||
|
||||
SubButton.Pressed += () =>
|
||||
{
|
||||
this.SendCommand(new DecreaseCountCommand());
|
||||
};
|
||||
|
||||
// 初始化界面
|
||||
UpdateView();
|
||||
}
|
||||
|
||||
private void UpdateView(int count = 0)
|
||||
{
|
||||
Label.Text = $"Count: {count}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 运行游戏
|
||||
|
||||
按 **F5** 运行游戏,功能依然正常!
|
||||
|
||||
## 对比重构前后
|
||||
|
||||
### 重构前(使用 Model)
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_counterModel.Increment(); // ← 直接调用 Model
|
||||
};
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- Controller 知道如何增加计数
|
||||
- 如果逻辑复杂化,Controller 会变臃肿
|
||||
|
||||
### 重构后(使用 Command)
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
this.SendCommand(new IncreaseCountCommand()); // ← 发送命令
|
||||
};
|
||||
```
|
||||
|
||||
**优势**:
|
||||
|
||||
- Controller 不关心如何增加计数
|
||||
- 逻辑封装在 Command 中
|
||||
- Controller 只负责"转发用户意图"
|
||||
|
||||
## Command 的优势
|
||||
|
||||
### 1. 解耦 Controller
|
||||
|
||||
**之前**:
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
if (!CanIncrement()) return;
|
||||
await SaveData();
|
||||
_counterModel.Increment();
|
||||
PlaySound();
|
||||
LogAnalytics();
|
||||
};
|
||||
```
|
||||
|
||||
Controller 必须知道所有细节。
|
||||
|
||||
**现在**:
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
this.SendCommand(new IncreaseCountCommand());
|
||||
};
|
||||
```
|
||||
|
||||
所有逻辑在 Command 中:
|
||||
|
||||
```csharp
|
||||
protected override void OnExecute()
|
||||
{
|
||||
if (!CanIncrement()) return;
|
||||
await SaveData();
|
||||
this.GetModel<ICounterModel>()!.Increment();
|
||||
PlaySound();
|
||||
LogAnalytics();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 逻辑复用
|
||||
|
||||
假设需要通过键盘快捷键增加计数:
|
||||
|
||||
**之前**:
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () => { /* 逻辑 */ };
|
||||
Input.IsActionPressed("increment") => { /* 复制相同逻辑 */ };
|
||||
```
|
||||
|
||||
代码重复!
|
||||
|
||||
**现在**:
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () => this.SendCommand(new IncreaseCountCommand());
|
||||
Input.IsActionPressed("increment") => this.SendCommand(new IncreaseCountCommand());
|
||||
```
|
||||
|
||||
逻辑只写一次!
|
||||
|
||||
### 3. 易于测试
|
||||
|
||||
**之前**:
|
||||
|
||||
```csharp
|
||||
// 无法测试,必须 mock 按钮
|
||||
AddButton.Pressed += () => { /* 逻辑 */ };
|
||||
```
|
||||
|
||||
**现在**:
|
||||
|
||||
```csharp
|
||||
// 可以直接测试命令
|
||||
[Test]
|
||||
public void IncreaseCommand_ShouldIncrementCount()
|
||||
{
|
||||
var model = new CounterModel();
|
||||
var command = new IncreaseCountCommand();
|
||||
|
||||
command.Execute();
|
||||
|
||||
Assert.AreEqual(1, model.Count);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 支持撤销/重做(扩展)
|
||||
|
||||
Command 模式天然支持撤销功能:
|
||||
|
||||
```csharp
|
||||
public class IncreaseCountCommand : AbstractCommand
|
||||
{
|
||||
protected override void OnExecute()
|
||||
{
|
||||
// 执行
|
||||
this.GetModel<ICounterModel>()!.Increment();
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
// 撤销
|
||||
this.GetModel<ICounterModel>()!.Decrement();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Command 的实际应用
|
||||
|
||||
让我们看一个更复杂的例子:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 更改语言命令
|
||||
/// </summary>
|
||||
public class ChangeLanguageCommand : AbstractAsyncCommand<ChangeLanguageInput>
|
||||
{
|
||||
protected override async Task OnExecuteAsync(ChangeLanguageInput input)
|
||||
{
|
||||
// 1. 获取设置 Model
|
||||
var settingsModel = this.GetModel<ISettingsModel>()!;
|
||||
|
||||
// 2. 获取设置数据
|
||||
var settings = settingsModel.GetData();
|
||||
|
||||
// 3. 修改语言配置
|
||||
settings.Language = input.Language;
|
||||
|
||||
// 4. 应用设置(通过 System)
|
||||
await this.GetSystem<ISettingsSystem>()!.Apply();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果这些逻辑都写在 Controller:
|
||||
|
||||
```csharp
|
||||
LanguageButton.Pressed += async () =>
|
||||
{
|
||||
var settingsModel = this.GetModel<ISettingsModel>()!;
|
||||
var settings = settingsModel.GetData();
|
||||
settings.Language = newLanguage;
|
||||
await this.GetSystem<ISettingsSystem>()!.Apply();
|
||||
};
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- Controller 臃肿
|
||||
- 逻辑分散
|
||||
- 难以复用
|
||||
|
||||
## 理解职责边界
|
||||
|
||||
### Controller vs Command
|
||||
|
||||
| 层级 | 职责 | 示例 |
|
||||
|----------------|------------|-------------|
|
||||
| **Controller** | 将用户操作转换为意图 | "用户点击了增加按钮" |
|
||||
| **Command** | 封装业务逻辑 | "如何增加计数" |
|
||||
| **Model** | 存储和管理状态 | "计数的值是多少" |
|
||||
|
||||
**类比**:
|
||||
|
||||
- **Controller**:服务员(接收顾客点单)
|
||||
- **Command**:厨师(制作菜品)
|
||||
- **Model**:菜单(菜品信息)
|
||||
|
||||
### 何时使用 Command?
|
||||
|
||||
✅ **应该使用 Command**:
|
||||
|
||||
- 逻辑超过 3 行
|
||||
- 需要复用的操作
|
||||
- 涉及多个 Model/System 的协作
|
||||
- 需要异步操作
|
||||
- 需要撤销/重做
|
||||
|
||||
❌ **不需要 Command**:
|
||||
|
||||
- 极简单的操作(如 `model.GetData()`)
|
||||
- 纯 UI 逻辑(如切换界面状态)
|
||||
|
||||
## 核心收获
|
||||
|
||||
通过这次重构,我们学到了:
|
||||
|
||||
| 概念 | 解释 |
|
||||
|----------------|------------------------------|
|
||||
| **Command 模式** | 将请求封装成对象 |
|
||||
| **职责分离** | Controller 负责转发,Command 负责执行 |
|
||||
| **逻辑复用** | 同一命令可被多处调用 |
|
||||
| **可测试性** | 命令可独立测试 |
|
||||
| **单一职责** | 每个 Command 只做一件事 |
|
||||
|
||||
## 对比三个阶段
|
||||
|
||||
### 阶段 1:基础实现
|
||||
|
||||
```csharp
|
||||
private int _count;
|
||||
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_count++;
|
||||
UpdateView();
|
||||
};
|
||||
```
|
||||
|
||||
**问题**:状态、逻辑、UI 混在一起
|
||||
|
||||
### 阶段 2:引入 Model
|
||||
|
||||
```csharp
|
||||
private ICounterModel _counterModel;
|
||||
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
_counterModel.Increment();
|
||||
};
|
||||
```
|
||||
|
||||
**改进**:状态抽离到 Model,但交互逻辑仍在 Controller
|
||||
|
||||
### 阶段 3:引入 Command
|
||||
|
||||
```csharp
|
||||
AddButton.Pressed += () =>
|
||||
{
|
||||
this.SendCommand(new IncreaseCountCommand());
|
||||
};
|
||||
```
|
||||
|
||||
**完善**:Controller 不再关心"如何",只负责"转发"
|
||||
|
||||
## 下一步
|
||||
|
||||
现在我们的架构已经很清晰了:
|
||||
|
||||
```
|
||||
View → Controller → Command → Model → Event → View
|
||||
```
|
||||
|
||||
但还有两个问题:
|
||||
|
||||
1. **业务规则**:如何实现"计数不能超过 20"?
|
||||
2. **状态响应**:如何实现"计数超过 10 时触发某个逻辑"?
|
||||
|
||||
这些问题需要 **Utility** 和 **System** 来解决。
|
||||
|
||||
在下一章中,我们将:
|
||||
|
||||
- 引入 **Utility** 处理业务规则
|
||||
- 引入 **System** 响应状态变化
|
||||
- 完成完整的架构设计
|
||||
|
||||
👉 [第 6 章:Utility 与 System](./06-utility-system.md)
|
||||
|
||||
---
|
||||
|
||||
::: details 本章检查清单
|
||||
|
||||
- [ ] IncreaseCountCommand 已创建
|
||||
- [ ] DecreaseCountCommand 已创建
|
||||
- [ ] App.cs 使用 SendCommand 替换了直接调用
|
||||
- [ ] 运行游戏,功能正常
|
||||
- [ ] 理解了 Command 的职责和优势
|
||||
- [ ] 理解了 Controller、Command、Model 的职责边界
|
||||
:::
|
||||
|
||||
::: tip 思考题
|
||||
|
||||
1. 如果需要实现"撤销"功能,应该如何修改 Command?
|
||||
2. 异步命令(如网络请求)应该如何实现?
|
||||
3. 多个 Command 需要按顺序执行时,应该怎么做?
|
||||
|
||||
这些高级用法可以在后续深入学习!
|
||||
:::
|
||||
712
docs/zh-CN/tutorials/basic/06-utility-system.md
Normal file
@ -0,0 +1,712 @@
|
||||
---
|
||||
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 时,输出面板会显示提示信息:
|
||||
|
||||

|
||||
|
||||
## 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,接收输入 | Label、Button |
|
||||
| **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. 如何实现"计数为偶数时播放音效"的功能?
|
||||
|
||||
这些问题会在进阶教程中探讨!
|
||||
:::
|
||||
751
docs/zh-CN/tutorials/basic/07-summary.md
Normal file
@ -0,0 +1,751 @@
|
||||
---
|
||||
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 创造出精彩的项目!
|
||||
192
docs/zh-CN/tutorials/basic/index.md
Normal file
@ -0,0 +1,192 @@
|
||||
# 基础教程:从零开始使用 GFramework
|
||||
|
||||
欢迎来到 GFramework 的基础教程!本教程将带你从零开始,创建一个完整的计数器应用,逐步掌握 GFramework 的核心概念和最佳实践。
|
||||
|
||||
## 📚 教程概述
|
||||
|
||||
通过本教程,你将:
|
||||
|
||||
- 搭建完整的开发环境
|
||||
- 创建一个基于 Godot 的项目并集成 GFramework
|
||||
- 从基础实现到架构优化,体验完整的重构过程
|
||||
- 理解 Model、Command、System、Utility 等核心概念
|
||||
- 掌握事件驱动架构的设计思想
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- ✅ 搭建 GFramework 开发环境
|
||||
- ✅ 使用 GFramework 构建清晰的应用架构
|
||||
- ✅ 理解并应用 MVC 架构模式
|
||||
- ✅ 使用命令系统解耦业务逻辑
|
||||
- ✅ 通过事件系统实现组件通信
|
||||
- ✅ 合理使用 Utility 和 System 层
|
||||
|
||||
## ⏱️ 预计学习时间
|
||||
|
||||
- **快速浏览**:30 分钟
|
||||
- **完整实践**:2-3 小时
|
||||
- **深入理解**:4-6 小时
|
||||
|
||||
## 📋 前置知识要求
|
||||
|
||||
### 必需
|
||||
|
||||
- C# 基础语法
|
||||
- 面向对象编程概念
|
||||
- 基本的命令行操作
|
||||
|
||||
### 推荐
|
||||
|
||||
- Godot 引擎基础(了解节点系统即可)
|
||||
- MVC 架构基础概念
|
||||
- 依赖注入的基本理解
|
||||
|
||||
## 📖 教程章节
|
||||
|
||||
### [1. 环境准备](./01-environment.md)
|
||||
|
||||
搭建 GFramework 开发环境,包括 .NET SDK 和 Godot 引擎的安装配置。
|
||||
|
||||
**关键内容**:
|
||||
|
||||
- 系统要求检查
|
||||
- .NET SDK 安装
|
||||
- Godot 引擎配置
|
||||
- 环境验证
|
||||
|
||||
**预计时间**:15-30 分钟
|
||||
|
||||
---
|
||||
|
||||
### [2. 项目创建与初始化](./02-project-setup.md)
|
||||
|
||||
创建 Godot 项目,引入 GFramework,并搭建基础架构。
|
||||
|
||||
**关键内容**:
|
||||
|
||||
- 创建 Godot 项目
|
||||
- 引入 GFramework NuGet 包
|
||||
- 定义游戏架构类
|
||||
- 创建三大模块(Model、System、Utility)
|
||||
- 配置游戏入口点
|
||||
|
||||
**预计时间**:30-45 分钟
|
||||
|
||||
---
|
||||
|
||||
### [3. 基础计数器实现](./03-counter-basic.md)
|
||||
|
||||
实现一个简单的计数器功能,了解传统实现方式的问题。
|
||||
|
||||
**关键内容**:
|
||||
|
||||
- 搭建 UI 场景
|
||||
- 实现基础计数器功能
|
||||
- 分析代码中的耦合问题
|
||||
- 理解为什么需要架构优化
|
||||
|
||||
**预计时间**:20-30 分钟
|
||||
|
||||
---
|
||||
|
||||
### [4. 引入 Model 重构](./04-model-refactor.md)
|
||||
|
||||
将数据状态抽离到 Model 层,引入事件系统实现数据驱动。
|
||||
|
||||
**关键内容**:
|
||||
|
||||
- 创建 Model 接口和实现
|
||||
- 注册 Model 到架构
|
||||
- 使用事件系统更新 UI
|
||||
- 对比重构前后的改进
|
||||
|
||||
**预计时间**:30-40 分钟
|
||||
|
||||
---
|
||||
|
||||
### [5. 命令系统优化](./05-command-system.md)
|
||||
|
||||
使用 Command 模式解耦交互逻辑,让 Controller 更加清晰。
|
||||
|
||||
**关键内容**:
|
||||
|
||||
- 创建增减计数命令
|
||||
- 重构 Controller 使用命令
|
||||
- 理解 Command 的职责边界
|
||||
- 对比直接调用的优缺点
|
||||
|
||||
**预计时间**:30-40 分钟
|
||||
|
||||
---
|
||||
|
||||
### [6. Utility 与 System](./06-utility-system.md)
|
||||
|
||||
引入 Utility 处理业务规则,使用 System 响应状态变化。
|
||||
|
||||
**关键内容**:
|
||||
|
||||
- 实现计数限制 Utility
|
||||
- 创建阈值检查 System
|
||||
- 理解各层的职责分工
|
||||
- 完整架构总结
|
||||
|
||||
**预计时间**:40-50 分钟
|
||||
|
||||
---
|
||||
|
||||
### [7. 总结与最佳实践](./07-summary.md)
|
||||
|
||||
回顾整个架构设计,总结最佳实践和常见问题。
|
||||
|
||||
**关键内容**:
|
||||
|
||||
- 各层职责对照表
|
||||
- 设计原则总结
|
||||
- 常见问题 FAQ
|
||||
- 下一步学习建议
|
||||
|
||||
**预计时间**:15-20 分钟
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
如果你已经熟悉基础概念,可以:
|
||||
|
||||
1. **只想看最终效果**:直接跳到 [第 6 章](./06-utility-system.md) 查看完整架构
|
||||
2. **已有 Godot 项目**:从 [第 2 章](./02-project-setup.md) 开始集成 GFramework
|
||||
3. **想快速上手**:按顺序完成 [第 2-4 章](./02-project-setup.md),掌握核心用法
|
||||
|
||||
## 💡 学习建议
|
||||
|
||||
- **边学边做**:每个章节都包含完整代码,建议跟着实践
|
||||
- **理解原理**:不要只复制代码,思考每一步的设计意图
|
||||
- **对比差异**:重构前后的代码对比是理解架构价值的关键
|
||||
- **举一反三**:将学到的模式应用到自己的项目中
|
||||
|
||||
## 🔗 相关资源
|
||||
|
||||
- [GFramework 文档首页](../../)
|
||||
- [Core 核心框架](../../core/)
|
||||
- [Godot 集成](../../godot/)
|
||||
- [源码生成器](../../source-generators/)
|
||||
|
||||
## ❓ 遇到问题?
|
||||
|
||||
- 查看 [常见问题 FAQ](./07-summary.md#常见问题-faq)
|
||||
- 访问 [GitHub Issues](https://github.com/GeWuYou/GFramework/issues)
|
||||
- 加入社区讨论
|
||||
|
||||
---
|
||||
|
||||
::: tip 提示
|
||||
本教程采用循序渐进的方式,每一步都基于前一步的代码。建议按顺序学习,这样能更好地理解架构演进的过程。
|
||||
:::
|
||||
|
||||
::: warning 注意
|
||||
教程中的代码示例使用 Godot 4.5.1+ 和 .NET 6.0+。如果你使用不同版本,可能需要适当调整。
|
||||
:::
|
||||
|
||||
让我们开始吧!👉 [第 1 章:环境准备](./01-environment.md)
|
||||