From 7531762d3e9e40cf724f57226466df12dc2a4ae4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:45:39 +0800 Subject: [PATCH 1/5] =?UTF-8?q?docs(documentation):=20=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=E6=A0=8F=E7=9B=AE=E5=85=A5=E5=8F=A3=E9=A1=B5=E5=AF=BC=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 Core、Game 与 Source Generators 栏目 landing page,使其对齐当前模块定位、包关系与最小接入路径 - 修复 VitePress 对 docs 目录外 README 相对链接的 dead-link 校验问题,改为纯文本入口提示 - 补充 documentation-governance-and-refresh 主题的恢复点、验证结果与下一步专题页修订计划 --- ...ntation-governance-and-refresh-tracking.md | 26 +- ...umentation-governance-and-refresh-trace.md | 23 + docs/zh-CN/core/index.md | 719 +------- docs/zh-CN/game/index.md | 1458 +---------------- docs/zh-CN/source-generators/index.md | 1384 +--------------- 5 files changed, 272 insertions(+), 3338 deletions(-) diff --git a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md index fd3a2bdd..0447eb82 100644 --- a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md +++ b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md @@ -7,25 +7,28 @@ ## 当前恢复点 -- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-001` -- 当前阶段:`Phase 1` +- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-002` +- 当前阶段:`Phase 2` - 当前焦点: - - 已将当前工作树根目录的 legacy `local-plan/` 迁入 `ai-plan/public/documentation-governance-and-refresh/` - - 第一轮治理已完成 `AGENTS.md`、根 `README.md`、`getting-started` 与第一批高优先级模块 `README.md` - - 下一轮需要继续按栏目核对并重写 `docs/zh-CN/core/*`、`docs/zh-CN/game/*` 与 - `docs/zh-CN/source-generators/*` + - 已完成 `docs/zh-CN/core/index.md`、`docs/zh-CN/game/index.md` 与 + `docs/zh-CN/source-generators/index.md` 的 landing page 重写 + - 栏目入口已改为以模块定位、包关系、最小接入路径和继续阅读为主,不再沿用旧版失真教程结构 + - 下一轮需要继续核对并重写 `docs/zh-CN/core/*`、`docs/zh-CN/game/*` 与 + `docs/zh-CN/source-generators/*` 的专题页内容 ## 当前状态摘要 - 文档治理规则已收口到仓库规范,README、站点入口与采用链路不再依赖旧文档自证 -- 高优先级模块入口已补齐,首轮文档站构建校验已经通过 -- 当前主题仍是 active topic,因为核心栏目专题页仍可能包含与实现漂移的旧内容 +- 高优先级模块入口已补齐,栏目 landing page 已回到可作为默认导航入口的状态 +- 当前主题仍是 active topic,因为核心栏目下的专题页仍可能包含与实现漂移的旧内容 ## 当前活跃事实 - 旧 `local-plan/` 的详细 todo 与 trace 已迁入主题内 `archive/` - 当前分支 `docs/sdk-update-documentation` 已在 `ai-plan/public/README.md` 建立 topic 映射 - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 +- `core`、`game` 与 `source-generators` 三个栏目入口页现在都以模块 README 与当前包拆分为准 +- `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题 ## 当前风险 @@ -46,9 +49,10 @@ - 旧 `local-plan/` 的详细实施历史与文档站构建结果已迁入主题内归档 - active 跟踪文件已按 `ai-plan` 治理规则精简为当前恢复入口 +- `cd docs && bun run build` ## 下一步 -1. 继续按栏目核对 `docs/zh-CN/core/*`,列出仍失真的页面与示例 -2. 再推进 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` 的专题页重写 -3. 若下一轮重写完成且验证通过,将栏目级详细过程迁入本 topic 的 `archive/` +1. 先从 `docs/zh-CN/core/*` 开始,逐页核对架构、上下文、生命周期、命令、查询与 CQRS 的示例和术语 +2. 再推进 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面 +3. 若专题页批量重写完成且验证通过,将本轮 landing page 收口和下一轮专题页修订过程迁入本 topic 的 `archive/` diff --git a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md index a324a41d..6b9bb94e 100644 --- a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md +++ b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md @@ -29,3 +29,26 @@ 1. 后续继续该主题时,只从 `ai-plan/public/documentation-governance-and-refresh/` 进入,不再恢复 `local-plan/` 2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/` + +## 2026-04-21 + +### 阶段:栏目 landing page 收口(RP-002) + +- 依据 `ai-plan/public/README.md` 的 worktree 映射恢复 `documentation-governance-and-refresh` 主题,并确认该分支下一步应优先处理 `docs/zh-CN/core/*`、`game/*` 与 `source-generators/*` +- 复核 `docs/zh-CN/core/index.md`、`docs/zh-CN/game/index.md`、`docs/zh-CN/source-generators/index.md` 后确认:这三页仍保留旧版“大而全教程”结构,与当前模块 README、包拆分关系和推荐接入路径明显漂移 +- 对照 `GFramework.Core/README.md`、`GFramework.Game/README.md`、`GFramework.Core.SourceGenerators/README.md`、 + `GFramework.Game.SourceGenerators/README.md`、`GFramework.Cqrs.SourceGenerators/README.md` 与 + `GFramework.Godot.SourceGenerators/README.md`,重写三个栏目 landing page,使其回到“模块定位、包关系、最小接入路径、继续阅读”的可信入口形态 +- 首次执行 `cd docs && bun run build` 时发现 VitePress 会把跳到 `docs/` 目录外的相对链接判定为 dead link,因此将 landing page 末尾的模块 README 入口改为纯文本路径提示而非站内链接 +- 第二次执行 `cd docs && bun run build` 通过,说明当前 landing page 重写没有破坏站点构建 + +### 当前结论 + +- 当前默认导航入口已显著收敛,但专题页仍需逐页按源码与测试继续核对 +- 后续优先级应从 `core` 专题页开始,再向 `game` 与 `source-generators` 扩展 + +### 下一步 + +1. 审核 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md`、`cqrs.md` +2. 记录每页的失真点、真实 API 名称与应保留的最小示例 +3. 完成一轮专题页重写后再次执行 `cd docs && bun run build` diff --git a/docs/zh-CN/core/index.md b/docs/zh-CN/core/index.md index 5c4837bf..306dabf7 100644 --- a/docs/zh-CN/core/index.md +++ b/docs/zh-CN/core/index.md @@ -1,674 +1,107 @@ -# GFramework.Core 核心框架 +# Core -> 一个基于 CQRS、MVC 和事件驱动的轻量级游戏开发架构框架 +`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core` 与 `GFramework.Core.Abstractions`,以及与之直接相邻的旧版 +`Command` / `Query` 执行器和新版 `CQRS` 迁移入口。 -## 目录 +如果你第一次接入框架,建议先把这里当作“运行时底座说明”,再按需进入 `Game`、`Godot` 或 Source Generators 栏目。 -- [框架概述](#框架概述) -- [核心概念](#核心概念) -- [架构图](#架构图) -- [快速开始](#快速开始) -- [包说明](#包说明) -- [组件联动](#组件联动) -- [最佳实践](#最佳实践) -- [设计理念](#设计理念) +## 先理解包关系 -## 框架概述 +- `GeWuYou.GFramework.Core` + - 基础运行时实现,包含 `Architecture`、上下文、生命周期、事件、属性、状态、资源、日志、协程、IoC 等能力。 +- `GeWuYou.GFramework.Core.Abstractions` + - 对应的契约层,适合只依赖接口、做模块拆分或测试替身。 +- `GeWuYou.GFramework.Cqrs` + - 推荐给新功能使用的新请求模型运行时。 +- `GeWuYou.GFramework.Game` + - 在 `Core` 之上叠加游戏层配置、数据、设置、场景与 UI。 +- `GeWuYou.GFramework.Core.SourceGenerators` + - 在编译期补齐日志、上下文注入、模块自动注册等样板代码。 -本框架是一个与平台无关的轻量级架构,它结合了多种经典设计模式: +如果你只想先把架构跑起来,最小安装组合仍是: -- **MVC 架构模式** - 清晰的层次划分 -- **CQRS 模式** - 命令查询职责分离 -- **IoC/DI** - 依赖注入和控制反转 -- **事件驱动** - 松耦合的组件通信 -- **响应式编程** - 可绑定属性和数据流 -- **阶段式生命周期管理** - 精细化的架构状态控制 - -**重要说明**:GFramework.Core 是与平台无关的核心模块,不包含任何 Godot 特定代码。Godot 集成功能在 GFramework.Godot 包中实现。 - -### 核心特性 - -- **清晰的分层架构** - Model、View、Controller、System、Utility 各司其职 -- **类型安全** - 基于泛型的组件获取和事件系统 -- **松耦合** - 通过事件和接口实现组件解耦 -- **易于测试** - 依赖注入和纯函数设计 -- **可扩展** - 基于接口的规则体系 -- **生命周期管理** - 自动的注册和注销机制 -- **模块化** - 支持架构模块安装 -- **平台无关** - Core 模块可以在任何 .NET 环境中使用 - -## 核心概念 - -### 五层架构 - -``` -┌─────────────────────────────────────────┐ -│ View / UI │ UI 层:用户界面 -├─────────────────────────────────────────┤ -│ Controller │ 控制层:连接 UI 和业务逻辑 -├─────────────────────────────────────────┤ -│ System │ 逻辑层:业务逻辑 -├─────────────────────────────────────────┤ -│ Model │ 数据层:游戏状态 -├─────────────────────────────────────────┤ -│ Utility │ 工具层:无状态工具 -└─────────────────────────────────────────┘ +```bash +dotnet add package GeWuYou.GFramework.Core +dotnet add package GeWuYou.GFramework.Core.Abstractions ``` -### 横切关注点 +## 这个栏目应该回答什么 -``` -Command ──┐ -Query ──┼──→ 跨层操作(修改/查询数据) -Event ──┘ -``` +`Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织: -### 架构阶段 +- 架构与上下文 + - [architecture](./architecture.md) + - [context](./context.md) + - [lifecycle](./lifecycle.md) +- 旧版命令 / 查询执行器与迁移入口 + - [command](./command.md) + - [query](./query.md) + - [cqrs](./cqrs.md) +- 核心横切能力 + - [events](./events.md) + - [property](./property.md) + - [logging](./logging.md) + - [resource](./resource.md) + - [coroutine](./coroutine.md) + - [ioc](./ioc.md) +- 状态与扩展能力 + - [state-machine](./state-machine.md) + - [state-management](./state-management.md) + - [pause](./pause.md) + - [localization](./localization.md) + - [functional](./functional.md) + - [extensions](./extensions.md) -框架提供了精细化的生命周期管理,包含 11 个阶段: +## 最小接入路径 -``` -初始化流程: -None → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit → BeforeSystemInit → AfterSystemInit → Ready +当前版本的最小运行时入口只有三个关键动作: -销毁流程: -Ready → Destroying → Destroyed +1. 继承 `Architecture` +2. 在 `OnInitialize()` 中注册模型、系统、工具或模块 +3. 通过 `architecture.Context` 或 `ContextAwareBase` 的扩展方法访问上下文 -异常流程: -Any → FailedInitialization -``` - -每个阶段都会触发 `PhaseChanged` 事件,允许组件监听架构状态变化。 - -## 架构图 - -### 整体架构 - -从 v1.1.0 开始,Architecture 类采用模块化设计,将职责分离到专门的管理器中: - -``` - ┌──────────────────┐ - │ Architecture │ ← 核心协调器 - └────────┬─────────┘ - │ - ┌────────────────────┼────────────────────┐ - │ │ │ - ┌────▼────────┐ ┌──────▼──────┐ ┌────────▼────────┐ - │ Lifecycle │ │ Component │ │ Modules │ - │ Manager │ │ Registry │ │ Manager │ - └─────────────┘ └─────────────┘ └─────────────────┘ - │ │ │ - │ │ │ - 生命周期管理 组件注册管理 模块管理 - - 阶段转换 - System 注册 - 模块安装 - - 钩子管理 - Model 注册 - 行为注册 - - 初始化/销毁 - Utility 注册 -``` - -这种设计遵循单一职责原则,使代码更易维护和测试。 - -``` - ┌──────────────────┐ - │ Architecture │ ← 管理所有组件 - └────────┬─────────┘ - │ - ┌────────────────────┼────────────────────┐ - │ │ │ - ┌───▼────┐ ┌───▼────┐ ┌───▼─────┐ - │ Model │ │ System │ │ Utility │ - │ 层 │ │ 层 │ │ 层 │ - └───┬────┘ └───┬────┘ └────────┘ - │ │ - │ ┌─────────────┤ - │ │ │ - ┌───▼────▼───┐ ┌───▼──────┐ - │ Controller │ │ Command/ │ - │ 层 │ │ Query │ - └─────┬──────┘ └──────────┘ - │ - ┌─────▼─────┐ - │ View │ - │ UI │ - └───────────┘ -``` - -### 数据流向 - -``` -用户输入 → Controller → Command → System → Model → Event → Controller → View 更新 - -查询流程:Controller → Query → Model → 返回数据 -``` - -## 快速开始 - -本框架采用"约定优于配置"的设计理念,只需 4 步即可搭建完整的架构。 - -### 为什么需要这个框架? - -在传统开发中,我们经常遇到这些问题: - -- 代码耦合严重:UI 直接访问游戏逻辑,逻辑直接操作 UI -- 难以维护:修改一个功能需要改动多个文件 -- 难以测试:业务逻辑和 UI 混在一起无法独立测试 -- 难以复用:代码紧密耦合,无法在其他项目中复用 - -本框架通过清晰的分层解决这些问题。 - -### 1. 定义架构(Architecture) - -**作用**:Architecture 是整个应用的"中央调度器",负责管理所有组件的生命周期。 +最小示例: ```csharp -using GFramework.Core.Architecture; +using GFramework.Core.Architectures; -public class GameArchitecture : Architecture +public sealed class CounterArchitecture : Architecture { - protected override void Init() + protected override void OnInitialize() { - // 注册 Model - 游戏数据 - RegisterModel(new PlayerModel()); - - // 注册 System - 业务逻辑 - RegisterSystem(new CombatSystem()); - - // 注册 Utility - 工具类 - RegisterUtility(new StorageUtility()); + RegisterModel(new CounterModel()); + RegisterSystem(new CounterSystem()); } } ``` -**优势**: +对应的完整起步示例见: -- **依赖注入**:组件通过上下文获取架构引用 -- **集中管理**:所有组件注册在一处,一目了然 -- **生命周期管理**:自动初始化和销毁 -- **平台无关**:可以在任何 .NET 环境中使用 +- [快速开始](../getting-started/quick-start.md) -### 2. 定义 Model(数据层) +## 新项目如何选择能力 -**作用**:Model 是应用的"数据库",只负责存储和管理状态。 +- 只需要基础架构、事件、日志、资源、协程: + - 先停留在 `Core` +- 要写新的请求/通知处理流: + - 优先阅读 [cqrs](./cqrs.md) +- 要接入游戏内容配置、设置、数据仓库、Scene 或 UI: + - 转到 [Game](../game/index.md) +- 要接入 Godot 节点、场景和项目元数据生成: + - 转到 `Godot` 与 Source Generators 栏目 -```csharp -public class PlayerModel : AbstractModel -{ - // 使用 BindableProperty 实现响应式数据 - public BindableProperty Health { get; } = new(100); - public BindableProperty Gold { get; } = new(0); - - protected override void OnInit() - { - // Model 中可以监听自己的数据变化 - Health.Register(hp => - { - if (hp <= 0) this.SendEvent(new PlayerDiedEvent()); - }); - } -} +## 推荐阅读顺序 -// 也可以不使用 BindableProperty -public class PlayerModel : AbstractModel -{ - public int Health { get; private set; } - public int Gold { get; private set; } - - protected override void OnInit() - { - Health = 100; - Gold = 0; - } -} -``` +1. [快速开始](../getting-started/quick-start.md) +2. [architecture](./architecture.md) +3. [context](./context.md) +4. [lifecycle](./lifecycle.md) +5. [cqrs](./cqrs.md) -**优势**: +之后再按实际需要进入具体专题页,而不是把 `Core` 当成一次性读完的大杂烩。 -- **数据响应式**:BindableProperty 让数据变化自动通知监听者 -- **职责单一**:只存储数据,不包含复杂业务逻辑 -- **易于测试**:可以独立测试数据逻辑 +## 对应模块入口 -### 3. 定义 System(业务逻辑层) - -**作用**:System 是应用的"大脑",处理所有业务逻辑。 - -```csharp -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - // System 通过事件驱动,响应游戏中的各种事件 - this.RegisterEvent(OnEnemyAttack); - } - - private void OnEnemyAttack(EnemyAttackEvent e) - { - var playerModel = this.GetModel(); - - // 处理业务逻辑:计算伤害、更新数据 - playerModel.Health.Value -= e.Damage; - - // 发送事件通知其他组件 - this.SendEvent(new PlayerTookDamageEvent { Damage = e.Damage }); - } -} -``` - -**优势**: - -- **事件驱动**:通过事件解耦,不同 System 之间松耦合 -- **可组合**:多个 System 协同工作,每个专注自己的领域 -- **易于扩展**:新增功能只需添加新的 System 和事件监听 - -### 4. 定义 Controller(控制层) - -**作用**:Controller 是"桥梁",连接 UI 和业务逻辑。 - -```csharp -public class PlayerController : IController -{ - // 通过依赖注入获取架构 - private readonly IArchitecture _architecture; - - public PlayerController(IArchitecture architecture) - { - _architecture = architecture; - } - - // 监听模型变化 - public void Initialize() - { - var playerModel = _architecture.GetModel(); - - // 数据绑定:Model 数据变化自动更新 UI - playerModel.Health.RegisterWithInitValue(OnHealthChanged); - } - - private void OnHealthChanged(int hp) - { - // 更新 UI 显示 - UpdateHealthDisplay(hp); - } - - private void UpdateHealthDisplay(int hp) { /* UI 更新逻辑 */ } -} -``` - -**优势**: - -- **自动更新 UI**:通过 BindableProperty,数据变化自动反映到界面 -- **分离关注点**:UI 逻辑和业务逻辑完全分离 -- **易于测试**:可以通过依赖注入模拟架构进行测试 - -### 完成!现在你有了一个完整的架构 - -这 4 步完成后,你就拥有了: - -- **清晰的数据层**(Model) -- **独立的业务逻辑**(System) -- **灵活的控制层**(Controller) -- **统一的生命周期管理**(Architecture) - -### 下一步该做什么? - -1. **添加 Command**:封装用户操作(如购买物品、使用技能) -2. **添加 Query**:封装数据查询(如查询背包物品数量) -3. **添加更多 System**:如任务系统、背包系统、商店系统 -4. **使用 Utility**:添加工具类(如存档工具、数学工具) -5. **使用模块**:通过 IArchitectureModule 扩展架构功能 - -## 包说明 - -### Architecture 内部结构 (v1.1.0+) - -从 v1.1.0 开始,Architecture 类采用模块化设计,将原本 708 行的单一类拆分为多个职责清晰的协作者: - -#### 1. Architecture (核心协调器) - -**职责**: 提供统一的公共 API,协调各个管理器 - -**主要方法**: - -- `RegisterSystem()` - 注册系统 -- `RegisterModel()` - 注册模型 -- `RegisterUtility()` - 注册工具 -- `InstallModule()` - 安装模块 -- `InitializeAsync()` / `Initialize()` - 初始化架构 -- `DestroyAsync()` / `Destroy()` - 销毁架构 - -**事件**: - -- `PhaseChanged` - 阶段变更事件 - -#### 2. ArchitectureBootstrapper (初始化基础设施编排器) - -**职责**: 在用户 `OnInitialize()` 执行前准备环境、服务和上下文,并在组件初始化完成后执行初始化收尾 - -**核心功能**: - -- 初始化环境对象 -- 注册内置服务模块 -- 绑定架构上下文到 `GameContext` -- 执行服务钩子 -- 在 `InitializeAllComponentsAsync()` 完成后通过 `CompleteInitialization()` 冻结 IoC 容器 - -#### 3. ArchitectureLifecycle (生命周期管理器) - -**职责**: 管理架构的生命周期和阶段转换 - -**核心功能**: - -- 11 个架构阶段的管理和转换 -- 生命周期钩子 (IArchitectureLifecycleHook) 管理 -- 组件初始化 (按 Utility → Model → System 顺序) -- 组件销毁 (逆序销毁) -- 就绪状态管理 - -**关键方法**: - -- `EnterPhase()` - 进入指定阶段 -- `RegisterLifecycleHook()` - 注册生命周期钩子 -- `InitializeAllComponentsAsync()` - 初始化所有组件 -- `DestroyAsync()` - 异步销毁 - -#### 4. ArchitectureComponentRegistry (组件注册管理器) - -**职责**: 管理 System、Model、Utility 的注册 - -**核心功能**: - -- 组件注册和验证 -- 自动设置组件上下文 (IContextAware) -- 自动注册组件生命周期 (IInitializable、IDestroyable) -- 支持实例注册和类型注册 - -**关键方法**: - -- `RegisterSystem()` - 注册系统 -- `RegisterModel()` - 注册模型 -- `RegisterUtility()` - 注册工具 - -> 命名提醒: 公开的 `ArchitectureServices` 负责容器和基础服务,并不承担组件注册职责。 -> `ArchitectureComponentRegistry` 才是内部的 System / Model / Utility 注册器。 - -#### 5. ArchitectureModules (模块管理器) - -**职责**: 管理架构模块和 CQRS 管道行为 - -**核心功能**: - -- 模块安装 (IArchitectureModule) -- CQRS 管道行为注册(推荐 API 为 `RegisterCqrsPipelineBehavior`) - -**关键方法**: - -- `InstallModule()` - 安装模块 -- `RegisterCqrsPipelineBehavior()` - 注册 CQRS 管道行为 - -#### 设计优势 - -这种模块化设计带来以下优势: - -1. **单一职责**: 每个类只负责一个明确的功能 -2. **易于测试**: 可以独立测试每个管理器 -3. **易于维护**: 修改某个功能不影响其他功能 -4. **易于扩展**: 添加新功能更容易 -5. **代码安全**: 消除了 `null!` 断言,所有字段在构造后立即可用 - -详细的设计决策已在架构实现重构中落地。 - ---- - -## 包说明 - -| 包名 | 职责 | 文档 | -|----------------------|-----------------|--------------------------| -| **architecture** | 架构核心,管理所有组件生命周期 | [查看](./architecture) | -| **constants** | 框架常量定义 | 本文档 | -| **model** | 数据模型层,存储状态 | [查看](./model) | -| **system** | 业务逻辑层,处理业务规则 | [查看](./system) | -| **controller** | 控制器层,连接视图和逻辑 | (在 Abstractions 中) | -| **utility** | 工具类层,提供无状态工具 | [查看](./utility) | -| **command** | 命令模式,封装写操作 | [查看](./command) | -| **query** | 查询模式,封装读操作 | [查看](./query) | -| **events** | 事件系统,组件间通信 | [查看](./events) | -| **property** | 可绑定属性,响应式编程 | [查看](./property) | -| **state-management** | 集中式状态容器与选择器 | [查看](./state-management) | -| **ioc** | IoC 容器,依赖注入 | [查看](./ioc) | -| **rule** | 规则接口,定义组件约束 | [查看](./rule) | -| **extensions** | 扩展方法,简化 API 调用 | [查看](./extensions) | -| **logging** | 日志系统,记录运行日志 | [查看](./logging) | -| **environment** | 环境接口,提供运行环境信息 | [查看](./environment) | -| **localization** | 本地化系统,多语言支持 | [查看](./localization) | - -## 组件联动 - -### 1. 初始化流程 - -``` -创建 Architecture 实例 - └─> 构造函数 - ├─> 初始化 Logger - ├─> 创建 ArchitectureBootstrapper - ├─> 创建 ArchitectureLifecycle - ├─> 创建 ArchitectureComponentRegistry - └─> 创建 ArchitectureModules - └─> InitializeAsync() - ├─> Bootstrapper 准备环境/服务/上下文 - ├─> OnInitialize() (用户注册组件) - │ ├─> RegisterModel → Model.SetContext() - │ ├─> RegisterSystem → System.SetContext() - │ └─> RegisterUtility → 注册到容器 - ├─> InitializeAllComponentsAsync() - │ ├─> BeforeUtilityInit → Utility.Initialize() - │ ├─> BeforeModelInit → Model.Initialize() - │ └─> BeforeSystemInit → System.Initialize() - ├─> CompleteInitialization() → 冻结 IoC 容器 - └─> 进入 Ready -``` - -**重要变更 (v1.1.0)**: 管理器现在在构造函数中初始化,而不是在 InitializeAsync 中。这消除了 `null!` 断言,提高了代码安全性。 - -### 2. Command 执行流程 - -``` -Controller.SendCommand(command) - └─> command.Execute() - └─> command.OnDo() // 子类实现 - ├─> GetModel() // 获取数据 - ├─> 修改 Model 数据 - └─> SendEvent() // 发送事件 -``` - -### 3. Event 传播流程 - -``` -组件.SendEvent(event) - └─> TypeEventSystem.Send(event) - └─> 通知所有订阅者 - ├─> Controller 响应 → 更新 UI - ├─> System 响应 → 执行逻辑 - └─> Model 响应 → 更新状态 -``` - -### 4. BindableProperty 数据绑定 - -``` -Model: BindableProperty Health = new(100); -Controller: Health.RegisterWithInitValue(hp => UpdateUI(hp)) -修改值: Health.Value = 50 → 触发所有回调 → 更新 UI -``` - -## 最佳实践 - -### 1. 分层职责原则 - -每一层都有明确的职责边界,遵循这些原则能让代码更清晰、更易维护。 - -**Model 层**: - -```csharp -// 好:只存储数据 -public class PlayerModel : AbstractModel -{ - public BindableProperty Health { get; } = new(100); - protected override void OnInit() { } -} - -// 坏:包含业务逻辑 -public class PlayerModel : AbstractModel -{ - public void TakeDamage(int damage) // 业务逻辑应在 System - { - Health.Value -= damage; - if (Health.Value <= 0) Die(); - } -} -``` - -**System 层**: - -```csharp -// 好:处理业务逻辑 -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - this.RegisterEvent(OnAttack); - } - - private void OnAttack(AttackEvent e) - { - var target = this.GetModel(); - int finalDamage = CalculateDamage(e.BaseDamage, target); - target.Health.Value -= finalDamage; - } -} -``` - -### 2. 通信方式选择指南 - -| 通信方式 | 使用场景 | 优势 | -|----------------------|-----------|----------| -| **Command** | 用户操作、修改状态 | 可撤销、可记录 | -| **Query** | 查询数据、检查条件 | 明确只读意图 | -| **Event** | 通知其他组件 | 松耦合、可扩展 | -| **BindableProperty** | 数据变化通知 | 自动化、不会遗漏 | - -### 3. 生命周期管理 - -**为什么需要注销?** - -忘记注销监听器会导致: - -- **内存泄漏**:对象无法被 GC 回收 -- **逻辑错误**:已销毁的对象仍在响应事件 - -```csharp -// 使用 UnRegisterList 统一管理 -private IUnRegisterList _unregisterList = new UnRegisterList(); - -public void Initialize() -{ - this.RegisterEvent(OnEvent1) - .AddToUnregisterList(_unregisterList); - - model.Property.Register(OnPropertyChanged) - .AddToUnregisterList(_unregisterList); -} - -public void Cleanup() -{ - _unregisterList.UnRegisterAll(); -} -``` - -### 4. 性能优化技巧 - -```csharp -// 低效:每帧都查询 -var model = _architecture.GetModel(); // 频繁调用 - -// 高效:缓存引用 -private PlayerModel _playerModel; - -public void Initialize() -{ - _playerModel = _architecture.GetModel(); // 只查询一次 -} -``` - -## 设计理念 - -框架的设计遵循 SOLID 原则和经典设计模式。 - -### 1. 单一职责原则(SRP) - -- **Model**:只负责存储数据 -- **System**:只负责处理业务逻辑 -- **Controller**:只负责协调和输入处理 -- **Utility**:只负责提供工具方法 - -### 2. 开闭原则(OCP) - -- 通过**事件系统**添加新功能,无需修改现有代码 -- 新的 System 可以监听现有事件,插入自己的逻辑 - -### 3. 依赖倒置原则(DIP) - -- 所有组件通过接口交互 -- 通过 IoC 容器注入依赖 -- 易于替换实现和编写测试 - -### 4. 接口隔离原则(ISP) - -```csharp -// 小而专注的接口 -public interface ICanGetModel : IBelongToArchitecture { } -public interface ICanSendCommand : IBelongToArchitecture { } -public interface ICanRegisterEvent : IBelongToArchitecture { } - -// 组合需要的能力 -public interface IController : - ICanGetModel, - ICanSendCommand, - ICanRegisterEvent { } -``` - -### 5. 组合优于继承 - -通过接口组合获得能力,而不是通过继承。 - -### 框架核心设计模式 - -| 设计模式 | 应用位置 | 解决的问题 | 带来的好处 | -|-----------|------------|----------|--------| -| **工厂模式** | IoC 容器 | 组件的创建和管理 | 解耦创建逻辑 | -| **观察者模式** | Event 系统 | 组件间的通信 | 松耦合通信 | -| **命令模式** | Command | 封装操作请求 | 支持撤销重做 | -| **策略模式** | System | 不同的业务逻辑 | 易于切换策略 | -| **依赖注入** | 整体架构 | 组件间的依赖 | 自动管理依赖 | -| **模板方法** | Abstract 类 | 定义算法骨架 | 统一流程规范 | - -### 平台无关性 - -- **GFramework.Core**:纯 .NET 库,无任何平台特定代码 -- **GFramework.Godot**:Godot 特定实现,包含 Node 扩展、GodotLogger 等 -- 可以轻松将 Core 框架移植到其他平台(Unity、.NET MAUI 等) - ---- - -**版本**: 1.1.0 -**更新日期**: 2026-03-17 -**许可证**: Apache 2.0 - -## 更新日志 - -### v1.1.0 (2026-03-17) - -**重大重构**: - -- 拆分 Architecture 类为 4 个职责清晰的类 -- 消除 3 处 `null!` 强制断言,提高代码安全性 -- 在构造函数中初始化管理器,符合"构造即完整"原则 -- 添加 `PhaseChanged` 事件,支持阶段监听 - -**向后兼容**: 所有公共 API 保持不变,现有代码无需修改。 +- `GFramework.Core/README.md` +- `GFramework.Core.Abstractions/README.md` +- 仓库根 `README.md` diff --git a/docs/zh-CN/game/index.md b/docs/zh-CN/game/index.md index 37d8949a..7a0738dd 100644 --- a/docs/zh-CN/game/index.md +++ b/docs/zh-CN/game/index.md @@ -1,1423 +1,117 @@ -# GFramework.Game +# Game -> 游戏特定功能抽象 - 为游戏开发提供专门的工具和系统 +`Game` 栏目对应 `GFramework.Game` 与 `GFramework.Game.Abstractions` 这层游戏运行时能力。 -GFramework.Game 是 GFramework 框架的游戏特定功能模块,提供了游戏开发中常用的抽象和工具,包括资产管理、存储系统、序列化等核心功能。 +它建立在 `Core` 之上,负责把“能运行的基础架构”继续扩展成“可落地的游戏项目基础设施”,重点覆盖静态内容配置、数据与存档、设置、场景与 UI 路由、序列化和文件存储。 -## 📋 目录 +## 先理解包关系 -- [概述](#概述) -- [核心特性](#核心特性) -- [内容配置系统](#内容配置系统) -- [架构模块系统](#架构模块系统) -- [资产管理](#资产管理) -- [存储系统](#存储系统) -- [序列化系统](#序列化系统) -- [使用示例](#使用示例) -- [最佳实践](#最佳实践) -- [性能特性](#性能特性) +- `GeWuYou.GFramework.Game` + - 游戏层默认运行时实现。 +- `GeWuYou.GFramework.Game.Abstractions` + - 对应的契约层。 +- `GeWuYou.GFramework.Game.SourceGenerators` + - 面向 `schemas/**/*.schema.json` 的配置类型与表包装生成器。 +- `GeWuYou.GFramework.Core` + - 本层直接依赖的底座,提供架构、生命周期、事件、日志等通用能力。 -## 概述 +如果你的项目只想依赖契约,不想带入默认实现,请选择 `Game.Abstractions`。如果你需要可运行的默认基类与实现,请使用 +`Game`。 -GFramework.Game 为游戏开发提供了专门的功能模块,与 GFramework.Core 的平台无关特性完美结合,为游戏项目提供了一整套完整的解决方案。 +## 这个栏目应该回答什么 -### 核心设计理念 +`Game` 栏目聚焦“游戏项目该怎么接这些运行时能力”,不再保留与当前实现脱节的泛化模块示例。 -- **游戏导向**:专门针对游戏开发场景设计 -- **模块化架构**:可插拔的模块系统,按需组合 -- **数据持久化**:完善的存档和数据管理方案 -- **资源管理**:高效的资源加载和管理机制 +当前栏目主要入口: -## 核心特性 +- 配置与内容系统 + - [config-system](./config-system.md) +- 数据、设置、序列化与存储 + - [data](./data.md) + - [setting](./setting.md) + - [serialization](./serialization.md) + - [storage](./storage.md) +- 导航与界面 + - [scene](./scene.md) + - [ui](./ui.md) -### 🏗️ 模块化架构 +## 最小接入路径 -- **AbstractModule**:可重用的架构模块基类 -- **生命周期管理**:与框架生命周期深度集成 -- **依赖注入**:模块间的依赖自动管理 -- **配置驱动**:灵活的模块配置系统 +接入 `Game` 层前,先确保你已经完成 `Core` 的基础架构初始化。 -### 📦 资产管理 +最常见的四种接入目标如下: -- **统一资源目录**:集中化的资源注册和查询 -- **类型安全**:编译时类型检查和泛型支持 -- **重复检测**:自动检测资源重复注册 -- **映射支持**:灵活的资源映射和别名系统 - -### 💾 存储系统 - -- **分层存储**:命名空间支持的存储隔离 -- **多格式支持**:JSON、二进制等多种存储格式 -- **异步操作**:完整的异步存储 API -- **版本兼容**:存档版本管理和迁移支持 - -### 🔄 序列化系统 - -- **JSON 集成**:基于 Newtonsoft.Json 的序列化 -- **自定义序列化**:支持自定义序列化逻辑 -- **性能优化**:序列化缓存和优化策略 -- **类型安全**:强类型的序列化和反序列化 - -## 内容配置系统 - -`GFramework.Game` 当前包含面向静态游戏内容的 AI-First 配表能力,用于怪物、物品、技能、任务等只读内容数据。 - -这一能力的核心定位是: - -- 使用 `YAML` 作为配置源文件 -- 使用 `JSON Schema` 描述结构和约束 -- 在运行时以只读配置表形式暴露 -- 通过 Source Generator 生成配置类型和表包装 -- 配套 VS Code 工具提供浏览、校验和轻量编辑入口 - -如果你准备在游戏项目中接入这套系统,请先阅读: - -- [游戏内容配置系统](/zh-CN/game/config-system) - -该页面包含目录约定、运行时注册方式、schema 绑定方式、生成器接入约定和当前限制说明。 - -## 架构模块系统 - -### AbstractModule 基础使用 - -```csharp -using GFramework.Game.Architecture; - -public class AudioModule : AbstractModule -{ - public override void Install(IArchitecture architecture) - { - // 注册音频相关系统 - architecture.RegisterSystem(new AudioSystem()); - architecture.RegisterSystem(new MusicSystem()); - - // 注册音频工具 - architecture.RegisterUtility(new AudioUtility()); - architecture.RegisterUtility(new MusicUtility()); - } - - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.BeforeModelInit: - // 在模型初始化前准备音频资源 - PreloadAudioResources(); - break; - - case ArchitecturePhase.Ready: - // 架构准备就绪,开始播放背景音乐 - StartBackgroundMusic(); - break; - - case ArchitecturePhase.Destroying: - // 清理音频资源 - CleanupAudioResources(); - break; - } - } - - private void PreloadAudioResources() - { - // 预加载音频资源 - var audioUtility = architecture.GetUtility(); - audioUtility.PreloadAudio("background_music"); - audioUtility.PreloadAudio("shoot_sound"); - audioUtility.PreloadAudio("explosion_sound"); - } - - private void StartBackgroundMusic() - { - var musicSystem = architecture.GetSystem(); - musicSystem.PlayBackgroundMusic("background_music"); - } - - private void CleanupAudioResources() - { - var audioUtility = architecture.GetUtility(); - audioUtility.UnloadAllAudio(); - } -} -``` - -### 复杂模块示例 - -```csharp -public class SaveModule : AbstractModule -{ - private ISaveSystem _saveSystem; - private IDataMigrationManager _migrationManager; - - public override void Install(IArchitecture architecture) - { - // 注册存储相关组件 - _saveSystem = new SaveSystem(); - architecture.RegisterUtility(_saveSystem); - - _migrationManager = new DataMigrationManager(); - architecture.RegisterUtility(_migrationManager); - - // 注册数据版本管理 - architecture.RegisterSystem(new SaveDataVersionSystem()); - } - - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.AfterModelInit: - // 在模型初始化后设置数据迁移 - SetupDataMigrations(); - break; - - case ArchitecturePhase.Ready: - // 尝试自动加载存档 - TryAutoLoadSave(); - break; - } - } - - private void SetupDataMigrations() - { - // 设置数据版本迁移 - _migrationManager.RegisterMigration(1, 2, MigratePlayerDataV1ToV2); - _migrationManager.RegisterMigration(2, 3, MigratePlayerDataV2ToV3); - } - - private void TryAutoLoadSave() - { - if (_saveSystem.HasAutoSave()) - { - _saveSystem.LoadAutoSave(); - } - } - - private PlayerData MigratePlayerDataV1ToV2(PlayerData v1Data) - { - return new PlayerData - { - // 迁移逻辑 - Name = v1Data.Name, - Health = v1Data.Health, - // 新增字段 - MaxHealth = 100, - Level = 1 - }; - } - - private PlayerData MigratePlayerDataV2ToV3(PlayerData v2Data) - { - return new PlayerData - { - // 迁移逻辑 - Name = v2Data.Name, - Health = v2Data.Health, - MaxHealth = v2Data.MaxHealth, - Level = v2Data.Level, - // 新增字段 - Experience = 0, - Skills = new List() - }; - } -} -``` - -### 模块配置 - -```csharp -public class ModuleConfig -{ - public string SaveDirectory { get; set; } = "saves"; - public int AutoSaveInterval { get; set; } = 300; // 5分钟 - public bool EnableDataCompression { get; set; } = true; - public int MaxSaveSlots { get; set; } = 10; -} - -public class ConfigurableSaveModule : AbstractModule -{ - private ModuleConfig _config; - - public ConfigurableSaveModule(ModuleConfig config) - { - _config = config; - } - - public override void Install(IArchitecture architecture) - { - var saveSystem = new SaveSystem(_config); - architecture.RegisterUtility(saveSystem); - - // 配置自动保存 - if (_config.AutoSaveInterval > 0) - { - architecture.RegisterSystem(new AutoSaveSystem(_config.AutoSaveInterval)); - } - } -} -``` - -## 资产管理 - -### AbstractAssetCatalogUtility 基础使用 - -```csharp -using GFramework.Game.assets; - -public class GameAssetCatalog : AbstractAssetCatalogUtility -{ - public override void Initialize() - { - base.Initialize(); - - // 注册场景资产 - RegisterSceneUnit("Player", "res://scenes/Player.tscn"); - RegisterSceneUnit("Enemy", "res://scenes/Enemy.tscn"); - RegisterSceneUnit("Bullet", "res://scenes/Bullet.tscn"); - - // 注册场景页面 - RegisterScenePage("MainMenu", "res://ui/MainMenu.tscn"); - RegisterScenePage("GameUI", "res://ui/GameUI.tscn"); - RegisterScenePage("PauseMenu", "res://ui/PauseMenu.tscn"); - - // 注册通用资产 - RegisterAsset("PlayerTexture", "res://textures/player.png"); - RegisterAsset("EnemyTexture", "res://textures/enemy.png"); - RegisterAsset("ShootSound", "res://audio/shoot.wav"); - RegisterAsset("ExplosionSound", "res://audio/explosion.wav"); - } - - // 自定义资产验证 - protected override bool ValidateAsset(string key, string path) - { - if (!FileAccess.FileExists(path)) - { - GD.PrintErr($"Asset file not found: {path}"); - return false; - } - - return true; - } - - // 资产加载完成回调 - protected override void OnAssetLoaded(string key, object asset) - { - GD.Print($"Asset loaded: {key}"); - - // 对特定资产进行额外处理 - if (key == "PlayerTexture") - { - var texture = (Texture2D)asset; - // 预处理纹理... - } - } -} -``` - -### 资产映射系统 - -```csharp -public class AssetMapping -{ - public string Key { get; set; } - public string Path { get; set; } - public Type Type { get; set; } - public Dictionary Metadata { get; set; } = new(); -} - -public class AdvancedAssetCatalog : AbstractAssetCatalogUtility -{ - public override void Initialize() - { - base.Initialize(); - - // 使用映射对象注册资产 - RegisterAsset(new AssetMapping - { - Key = "Player", - Path = "res://scenes/Player.tscn", - Type = typeof(PackedScene), - Metadata = new Dictionary - { - ["category"] = "character", - ["tags"] = new[] { "player", "hero", "controlled" }, - ["health"] = 100, - ["speed"] = 5.0f - } - }); - - // 批量注册 - RegisterAssetsFromDirectory("res://textures/", "*.png", "texture"); - RegisterAssetsFromDirectory("res://audio/", "*.wav", "sound"); - } - - private void RegisterAssetsFromDirectory(string directory, string pattern, string prefix) - { - var dir = DirAccess.Open(directory); - if (dir == null) return; - - dir.ListDirBegin(); - var fileName = dir.GetNext(); - - while (!string.IsNullOrEmpty(fileName)) - { - if (fileName.EndsWith(pattern.Substring(1))) - { - var key = $"{prefix}{Path.GetFileNameWithoutExtension(fileName)}"; - var path = Path.Combine(directory, fileName); - - RegisterAsset(key, path); - } - - fileName = dir.GetNext(); - } - - dir.ListDirEnd(); - } -} -``` - -### 资产工厂模式 - -```csharp -public interface IAssetFactory -{ - T Create(string key); - bool CanCreate(string key); -} - -public class PlayerFactory : IAssetFactory -{ - private readonly AbstractAssetCatalogUtility _catalog; - - public PlayerFactory(AbstractAssetCatalogUtility catalog) - { - _catalog = catalog; - } - - public Player Create(string key) - { - var scene = _catalog.GetScene(key); - var player = scene.Instantiate(); - - // 配置玩家 - player.Health = GetPlayerHealth(key); - player.Speed = GetPlayerSpeed(key); - - return player; - } - - public bool CanCreate(string key) - { - return _catalog.HasScene(key) && key.StartsWith("Player"); - } - - private int GetPlayerHealth(string key) - { - var metadata = _catalog.GetAssetMetadata(key); - return metadata?.GetValueOrDefault("health", 100) ?? 100; - } - - private float GetPlayerSpeed(string key) - { - var metadata = _catalog.GetAssetMetadata(key); - return metadata?.GetValueOrDefault("speed", 5.0f) ?? 5.0f; - } -} -``` - -## 存储系统 - -### ScopedStorage 分层存储 +### 1. 只想先拿到文件存储 ```csharp +using GFramework.Core.Abstractions.Storage; +using GFramework.Game.Serializer; using GFramework.Game.Storage; -public class GameDataManager -{ - private readonly IStorage _rootStorage; - private readonly IStorage _playerStorage; - private readonly IStorage _saveStorage; - - public GameDataManager(IStorage rootStorage) - { - _rootStorage = rootStorage; - - // 创建分层存储 - _playerStorage = new ScopedStorage(rootStorage, "player"); - _saveStorage = new ScopedStorage(rootStorage, "saves"); - } - - public void SavePlayerData(string playerId, PlayerData data) - { - _playerStorage.Write($"{playerId}/profile", data); - _playerStorage.Write($"{playerId}/inventory", data.Inventory); - _playerStorage.Write($"{playerId}/stats", data.Stats); - } - - public PlayerData LoadPlayerData(string playerId) - { - var profile = _playerStorage.Read($"{playerId}/profile"); - var inventory = _playerStorage.Read($"{playerId}/inventory", new Inventory()); - var stats = _playerStorage.Read($"{playerId}/stats", new PlayerStats()); - - return new PlayerData - { - Profile = profile, - Inventory = inventory, - Stats = stats - }; - } - - public void SaveGame(int slotId, SaveData data) - { - _saveStorage.Write($"slot_{slotId}", data); - _saveStorage.Write($"slot_{slotId}/timestamp", DateTime.UtcNow); - _saveStorage.Write($"slot_{slotId}/version", data.Version); - } - - public SaveData LoadGame(int slotId) - { - return _saveStorage.Read($"slot_{slotId}"); - } - - public List GetSaveSlotInfos() - { - var infos = new List(); - - for (int i = 1; i <= 10; i++) - { - var timestamp = _saveStorage.Read($"slot_{i}/timestamp"); - var version = _saveStorage.Read($"slot_{i}/version"); - - if (timestamp != default) - { - infos.Add(new SaveSlotInfo - { - SlotId = i, - Timestamp = timestamp, - Version = version - }); - } - } - - return infos; - } -} +var serializer = new JsonSerializer(); +IStorage storage = new FileStorage("GameData", serializer); ``` -### 自定义存储实现 +### 2. 接入设置与存档 -```csharp -public class EncryptedStorage : IStorage -{ - private readonly IStorage _innerStorage; - private readonly IEncryptor _encryptor; - - public EncryptedStorage(IStorage innerStorage, IEncryptor encryptor) - { - _innerStorage = innerStorage; - _encryptor = encryptor; - } - - public void Write(string key, T data) - { - var json = JsonConvert.SerializeObject(data); - var encrypted = _encryptor.Encrypt(json); - _innerStorage.Write(key, encrypted); - } - - public T Read(string key, T defaultValue = default) - { - var encrypted = _innerStorage.Read(key); - if (string.IsNullOrEmpty(encrypted)) - return defaultValue; - - var json = _encryptor.Decrypt(encrypted); - return JsonConvert.DeserializeObject(json); - } - - public async Task WriteAsync(string key, T data) - { - var json = JsonConvert.SerializeObject(data); - var encrypted = _encryptor.Encrypt(json); - await _innerStorage.WriteAsync(key, encrypted); - } - - public async Task ReadAsync(string key, T defaultValue = default) - { - var encrypted = await _innerStorage.ReadAsync(key); - if (string.IsNullOrEmpty(encrypted)) - return defaultValue; - - var json = _encryptor.Decrypt(encrypted); - return JsonConvert.DeserializeObject(json); - } - - public bool Has(string key) - { - return _innerStorage.Has(key); - } - - public void Delete(string key) - { - _innerStorage.Delete(key); - } - - public void Clear() - { - _innerStorage.Clear(); - } -} -``` +典型顺序是: -### 存储缓存层 +1. 注册 `JsonSerializer` +2. 注册 `IStorage` +3. 注册 `ISettingsDataRepository` +4. 注册 `SettingsModel` +5. 注册 `SettingsSystem` +6. 注册 `ISaveRepository` -```csharp -public class CachedStorage : IStorage -{ - private readonly IStorage _innerStorage; - private readonly Dictionary _cache; - private readonly Dictionary _cacheTimestamps; - private readonly TimeSpan _cacheExpiry; - - public CachedStorage(IStorage innerStorage, TimeSpan cacheExpiry = default) - { - _innerStorage = innerStorage; - _cacheExpiry = cacheExpiry == default ? TimeSpan.FromMinutes(5) : cacheExpiry; - _cache = new Dictionary(); - _cacheTimestamps = new Dictionary(); - } - - public T Read(string key, T defaultValue = default) - { - if (_cache.TryGetValue(key, out var cachedValue) && - !IsCacheExpired(key)) - { - return (T)cachedValue; - } - - var value = _innerStorage.Read(key, defaultValue); - UpdateCache(key, value); - - return value; - } - - public void Write(string key, T data) - { - _innerStorage.Write(key, data); - UpdateCache(key, data); - } - - public async Task ReadAsync(string key, T defaultValue = default) - { - if (_cache.TryGetValue(key, out var cachedValue) && - !IsCacheExpired(key)) - { - return (T)cachedValue; - } - - var value = await _innerStorage.ReadAsync(key, defaultValue); - UpdateCache(key, value); - - return value; - } - - public async Task WriteAsync(string key, T data) - { - await _innerStorage.WriteAsync(key, data); - UpdateCache(key, data); - } - - public bool Has(string key) - { - return _cache.ContainsKey(key) || _innerStorage.Has(key); - } - - public void Delete(string key) - { - _cache.Remove(key); - _cacheTimestamps.Remove(key); - _innerStorage.Delete(key); - } - - public void Clear() - { - _cache.Clear(); - _cacheTimestamps.Clear(); - _innerStorage.Clear(); - } - - public void ClearCache() - { - _cache.Clear(); - _cacheTimestamps.Clear(); - } - - private bool IsCacheExpired(string key) - { - if (!_cacheTimestamps.TryGetValue(key, out var timestamp)) - return true; - - return DateTime.UtcNow - timestamp > _cacheExpiry; - } - - private void UpdateCache(string key, T value) - { - _cache[key] = value; - _cacheTimestamps[key] = DateTime.UtcNow; - } -} -``` +具体组合方式见: -## 序列化系统 +- [setting](./setting.md) +- [data](./data.md) +- [storage](./storage.md) -### JsonSerializer 使用 +### 3. 接入静态 YAML 配置 -```csharp -using GFramework.Game.Serializer; -using Newtonsoft.Json; +当你要维护怪物、物品、技能、任务等只读内容数据时,优先使用: -public class GameDataSerializer -{ - private readonly JsonSerializer _serializer; - - public GameDataSerializer() - { - // 在构造阶段完成全部 JsonSerializerSettings / Converter 配置, - // 后续把 _serializer 视为共享只读实例。 - _serializer = new JsonSerializer(new JsonSerializerSettings - { - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Populate, - TypeNameHandling = TypeNameHandling.None - }); - - // 自定义转换器 - _serializer.Converters.Add(new Vector2JsonConverter()); - _serializer.Converters.Add(new ColorJsonConverter()); - _serializer.Converters.Add(new GodotResourceJsonConverter()); - } - - public string Serialize(T data) - { - return _serializer.Serialize(data); - } - - public T Deserialize(string json) - { - return _serializer.Deserialize(json); - } - - public void SerializeToFile(string path, T data) - { - var json = Serialize(data); - FileAccess.Open(path, FileAccess.ModeFlags.Write).StoreString(json); - } - - public T DeserializeFromFile(string path) - { - if (!FileAccess.FileExists(path)) - return default(T); - - var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); - var json = file.GetAsText(); - file.Close(); - - return Deserialize(json); - } -} -``` +- `GFramework.Game` 运行时 +- `GFramework.Game.SourceGenerators` +- `schemas/**/*.schema.json` + `config/**/*.yaml` -对于玩家可直接编辑的存档文件,默认应保持 `TypeNameHandling.None`。只有确实需要多态反序列化时,才应配合 -白名单 `SerializationBinder` 显式限制允许的类型集合。 +完整约定见: -### 自定义 JSON 转换器 +- [config-system](./config-system.md) -```csharp -public class Vector2JsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer) - { - writer.WriteStartObject(); - writer.WritePropertyName("x"); - writer.WriteValue(value.X); - writer.WritePropertyName("y"); - writer.WriteValue(value.Y); - writer.WriteEndObject(); - } - - public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return Vector2.Zero; - - var obj = serializer.Deserialize>(reader); - return new Vector2(obj["x"], obj["y"]); - } -} +### 4. 接入 Scene / UI 路由 -public class ColorJsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer) - { - writer.WriteValue(value.ToHtml()); - } - - public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return Colors.White; - - var html = reader.Value.ToString(); - return Color.FromHtml(html); - } -} +`SceneRouterBase` 与 `UiRouterBase` 提供的是可复用基类,不直接绑定具体引擎。工厂、root、注册表通常由引擎适配层或项目自身提供。 -public class GodotResourceJsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, Resource value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - writer.WriteStartObject(); - writer.WritePropertyName("$type"); - writer.WriteValue(value.GetType().Name); - writer.WritePropertyName("path"); - writer.WriteValue(value.ResourcePath); - writer.WriteEndObject(); - } - - public override Resource ReadJson(JsonReader reader, Type objectType, Resource existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return null; - - var obj = serializer.Deserialize>(reader); - var path = obj["path"]; - - return GD.Load(path); - } -} -``` +入口见: -### 版本化序列化 +- [scene](./scene.md) +- [ui](./ui.md) -```csharp -public class VersionedData -{ - public int Version { get; set; } - public Dictionary Data { get; set; } = new(); -} +## 推荐阅读顺序 -public class VersionedSerializer -{ - private readonly Dictionary _typeVersions = new(); - private readonly Dictionary _migrations = new(); - - public void RegisterVersion(int version) - { - _typeVersions[typeof(T)] = version; - } - - public void RegisterMigration(int fromVersion, int toVersion, IDataMigration migration) - { - var key = GetMigrationKey(typeof(T), fromVersion, toVersion); - _migrations[key] = migration; - } - - public string Serialize(T data) - { - var versioned = new VersionedData - { - Version = _typeVersions.GetValueOrDefault(typeof(T), 1), - Data = new Dictionary - { - ["type"] = typeof(T).Name, - ["data"] = JsonConvert.SerializeObject(data) - } - }; - - return JsonConvert.SerializeObject(versioned); - } - - public T Deserialize(string json) - { - var versioned = JsonConvert.DeserializeObject(json); - var currentVersion = _typeVersions.GetValueOrDefault(typeof(T), 1); - - // 迁移数据到当前版本 - var dataJson = MigrateData(versioned.Data["data"] as string, versioned.Version, currentVersion); - - return JsonConvert.DeserializeObject(dataJson); - } - - private string MigrateData(string dataJson, int fromVersion, int toVersion) - { - var currentData = dataJson; - var currentVersion = fromVersion; - - while (currentVersion < toVersion) - { - var migrationKey = GetMigrationKey(typeof(T), currentVersion, currentVersion + 1); - - if (_migrations.TryGetValue(migrationKey, out var migration)) - { - currentData = migration.Migrate(currentData); - currentVersion++; - } - else - { - throw new InvalidOperationException($"No migration found from version {currentVersion} to {currentVersion + 1}"); - } - } - - return currentData; - } - - private int GetMigrationKey(Type type, int fromVersion, int toVersion) - { - return HashCode.Combine(type.Name, fromVersion, toVersion); - } -} +1. [Core 入口](../core/index.md) +2. [config-system](./config-system.md) +3. [data](./data.md) +4. [setting](./setting.md) +5. [scene](./scene.md) 或 [ui](./ui.md) -public interface IDataMigration -{ - string Migrate(string data); -} +## 与真实接法的关系 -public interface IDataMigration : IDataMigration -{ - T Migrate(T data); -} -``` +这个栏目以源码、`*.csproj`、模块 `README.md` 与 `CoreGrid` 中已验证的接法为准。 -## 使用示例 +例如当前仓库与 `CoreGrid` 的共同事实包括: -### 完整的游戏数据管理系统 +- 配置系统采用 `YAML + JSON Schema + Source Generator` +- 设置持久化通常通过 `UnifiedSettingsDataRepository` +- 场景与 UI 路由依赖项目自己的 factory / root,而不是框架替你绑定引擎对象 -```csharp -// 1. 定义数据模型 -public class GameProfile -{ - public string PlayerName { get; set; } - public DateTime LastPlayed { get; set; } - public int TotalPlayTime { get; set; } - public List UnlockedAchievements { get; set; } = new(); -} +如果某个旧页面与这些事实冲突,应以源码和模块 README 为准,并在同一轮里修正文档。 -public class GameSettings -{ - public float MasterVolume { get; set; } = 1.0f; - public float MusicVolume { get; set; } = 0.8f; - public float SFXVolume { get; set; } = 0.9f; - public GraphicsSettings Graphics { get; set; } = new(); - public InputSettings Input { get; set; } = new(); -} +## 对应模块入口 -// 2. 创建游戏管理器 -[ContextAware] -[Log] -public partial class GameManager : Node, IController -{ - private GameAssetCatalog _assetCatalog; - private GameDataManager _dataManager; - private GameDataSerializer _serializer; - - protected override void OnInit() - { - // 初始化资产目录 - _assetCatalog = new GameAssetCatalog(); - _assetCatalog.Initialize(); - - // 初始化存储系统 - var rootStorage = new FileStorage("user://data/"); - var cachedStorage = new CachedStorage(rootStorage); - _dataManager = new GameDataManager(cachedStorage); - - // 初始化序列化器 - _serializer = new GameDataSerializer(); - - Logger.Info("Game manager initialized"); - } - - public void StartNewGame(string playerName) - { - Logger.Info($"Starting new game for player: {playerName}"); - - // 创建新的游戏档案 - var profile = new GameProfile - { - PlayerName = playerName, - LastPlayed = DateTime.UtcNow, - TotalPlayTime = 0 - }; - - _dataManager.SavePlayerData("current", profile); - - // 加载初始场景 - LoadInitialScene(); - - this.SendEvent(new NewGameStartedEvent { PlayerName = playerName }); - } - - public void LoadGame(int slotId) - { - Logger.Info($"Loading game from slot {slotId}"); - - try - { - var saveData = _dataManager.LoadGame(slotId); - if (saveData != null) - { - // 恢复游戏状态 - RestoreGameState(saveData); - - this.SendEvent(new GameLoadedEvent { SlotId = slotId }); - Logger.Info("Game loaded successfully"); - } - else - { - Logger.Warning($"No save data found in slot {slotId}"); - this.SendEvent(new GameLoadFailedEvent { SlotId = slotId }); - } - } - catch (Exception ex) - { - Logger.Error($"Failed to load game: {ex.Message}"); - this.SendEvent(new GameLoadFailedEvent { SlotId = slotId, Error = ex.Message }); - } - } - - public void SaveGame(int slotId) - { - Logger.Info($"Saving game to slot {slotId}"); - - try - { - var saveData = CreateSaveData(); - _dataManager.SaveGame(slotId, saveData); - - this.SendEvent(new GameSavedEvent { SlotId = slotId }); - Logger.Info("Game saved successfully"); - } - catch (Exception ex) - { - Logger.Error($"Failed to save game: {ex.Message}"); - this.SendEvent(new GameSaveFailedEvent { SlotId = slotId, Error = ex.Message }); - } - } - - private void LoadInitialScene() - { - var playerScene = _assetCatalog.GetScene("Player"); - var player = playerScene.Instantiate(); - - var gameWorldScene = _assetCatalog.GetScene("GameWorld"); - var gameWorld = gameWorldScene.Instantiate(); - - AddChild(gameWorld); - gameWorld.AddChild(player); - } - - private void RestoreGameState(SaveData saveData) - { - // 恢复玩家位置 - var playerScene = _assetCatalog.GetScene("Player"); - var player = playerScene.Instantiate(); - player.Position = saveData.PlayerPosition; - - // 恢复游戏世界 - var gameWorldScene = _assetCatalog.GetScene("GameWorld"); - var gameWorld = gameWorldScene.Instantiate(); - - AddChild(gameWorld); - gameWorld.AddChild(player); - - // 恢复其他游戏状态 - this.GetModel().Health.Value = saveData.PlayerHealth; - this.GetModel().CurrentLevel.Value = saveData.CurrentLevel; - this.GetModel().LoadFromData(saveData.Inventory); - } - - private SaveData CreateSaveData() - { - var player = GetTree().CurrentScene.FindChild("Player"); - - return new SaveData - { - PlayerPosition = player?.Position ?? Vector2.Zero, - PlayerHealth = this.GetModel().Health.Value, - CurrentLevel = this.GetModel().CurrentLevel.Value, - Inventory = this.GetModel().GetData(), - Timestamp = DateTime.UtcNow, - Version = 1 - }; - } -} -``` - -### 自动保存系统 - -```csharp -public class AutoSaveSystem : AbstractSystem -{ - private Timer _autoSaveTimer; - private int _currentSaveSlot; - private float _autoSaveInterval; - - public AutoSaveSystem(float intervalMinutes = 5.0f) - { - _autoSaveInterval = intervalMinutes * 60.0f; - } - - protected override void OnInit() - { - _autoSaveTimer = new Timer(); - _autoSaveTimer.WaitTime = _autoSaveInterval; - _autoSaveTimer.Timeout += OnAutoSave; - _autoSaveTimer.Autostart = true; - - AddChild(_autoSaveTimer); - - // 监听重要事件,立即自动保存 - this.RegisterEvent(OnImportantEvent); - this.RegisterEvent(OnImportantEvent); - this.RegisterEvent(OnImportantEvent); - - Logger.Info("Auto-save system initialized"); - } - - protected override void OnDestroy() - { - _autoSaveTimer?.Stop(); - - // 最后一次自动保存 - PerformAutoSave(); - } - - private void OnAutoSave() - { - PerformAutoSave(); - Logger.Debug("Periodic auto-save completed"); - } - - private void OnImportantEvent(IEvent e) - { - Logger.Info($"Important event {e.GetType().Name}, triggering auto-save"); - PerformAutoSave(); - } - - private void PerformAutoSave() - { - try - { - var saveData = CreateAutoSaveData(); - - // 保存到自动存档槽 - var storage = this.GetUtility(); - storage.Write("autosave", saveData); - storage.Write("autosave/timestamp", DateTime.UtcNow); - - Logger.Debug("Auto-save completed successfully"); - } - catch (Exception ex) - { - Logger.Error($"Auto-save failed: {ex.Message}"); - } - } - - private SaveData CreateAutoSaveData() - { - return new SaveData - { - // ... 创建保存数据 - }; - } -} -``` - -## 最佳实践 - -### 🏗️ 数据模型设计 - -#### 1. 版本化数据结构 - -```csharp -// 好的做法:版本化数据模型 -[Serializable] -public class PlayerDataV2 -{ - public int Version { get; set; } = 2; - public string Name { get; set; } - public int Health { get; set; } - public int MaxHealth { get; set; } = 100; // V2 新增 - public List Skills { get; set; } = new(); // V2 新增 -} - -// 避免:没有版本控制 -[Serializable] -public class PlayerData -{ - public string Name { get; set; } - public int Health { get; set; } - // 未来新增字段会导致兼容性问题 -} -``` - -#### 2. 数据验证 - -```csharp -public class PlayerData -{ - private string _name; - - public string Name - { - get => _name; - set - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("Player name cannot be empty"); - _name = value; - } - } - - public int Health - { - get => _health; - set => _health = Math.Max(0, value); // 确保不为负数 - } - - public void Validate() - { - if (string.IsNullOrWhiteSpace(Name)) - throw new ValidationException("Player name is required"); - - if (Health < 0) - throw new ValidationException("Health cannot be negative"); - } -} -``` - -### 💾 存储策略 - -#### 1. 分层存储命名 - -```csharp -// 好的做法:有意义的分层结构 -var playerStorage = new ScopedStorage(rootStorage, "players"); -var saveStorage = new ScopedStorage(rootStorage, "saves"); -var settingsStorage = new ScopedStorage(rootStorage, "settings"); -var tempStorage = new ScopedStorage(rootStorage, "temp"); - -// 避免的混乱命名 -var storage1 = new ScopedStorage(rootStorage, "data1"); -var storage2 = new ScopedStorage(rootStorage, "data2"); -``` - -#### 2. 存储性能优化 - -```csharp -// 好的做法:批量操作和缓存 -public class OptimizedDataManager -{ - private readonly IStorage _storage; - private readonly Dictionary _writeBuffer = new(); - - public void QueueWrite(string key, T data) - { - _writeBuffer[key] = data; - } - - public async Task FlushWritesAsync() - { - var tasks = _writeBuffer.Select(kvp => _storage.WriteAsync(kvp.Key, kvp.Value)); - await Task.WhenAll(tasks); - _writeBuffer.Clear(); - } -} - -// 避免:频繁的小写入 -public class InefficientDataManager -{ - public void UpdatePlayerStat(string stat, int value) - { - _storage.Write($"player/stats/{stat}", value); // 每次都写入磁盘 - } -} -``` - -### 🔄 序列化优化 - -#### 1. 选择合适的序列化格式 - -```csharp -// 好的做法:根据需求选择格式 -public class GameSerializer -{ - // JSON:可读性好,调试方便 - public string SerializeForDebug(object data) => JsonConvert.SerializeObject(data, Formatting.Indented); - - // 二进制:体积小,性能好 - public byte[] SerializeForStorage(object data) => MessagePackSerializer.Serialize(data); - - // 压缩:减少存储空间 - public byte[] SerializeWithCompression(object data) - { - var json = JsonConvert.SerializeObject(data); - return Compress(Encoding.UTF8.GetBytes(json)); - } -} -``` - -#### 2. 自定义序列化逻辑 - -```csharp -public class PlayerInventory -{ - public Dictionary Items { get; set; } = new(); - - [JsonIgnore] // 排除不需要序列化的属性 - public int TotalWeight => Items.Sum(kvp => GetItemWeight(kvp.Key) * kvp.Value); - - [JsonProperty("total_items")] // 自定义序列化名称 - public int TotalItems => Items.Values.Sum(); - - public bool ShouldSerializeItems() // 条件序列化 - { - return Items.Count > 0; - } - - [OnDeserialized] // 反序列化后处理 - private void OnDeserialized(StreamingContext context) - { - // 初始化默认值或执行验证 - Items ??= new Dictionary(); - } -} -``` - -### 🏭 模块设计模式 - -#### 1. 单一职责模块 - -```csharp -// 好的做法:模块职责单一 -public class AudioModule : AbstractModule -{ - // 只负责音频相关功能 -} - -public class SaveModule : AbstractModule -{ - // 只负责存档相关功能 -} - -// 避免:功能过于庞大 -public class GameModule : AbstractModule -{ - // 音频、存档、UI、输入全部混在一起 -} -``` - -#### 2. 模块间通信 - -```csharp -public class SaveModule : AbstractModule -{ - public override void Install(IArchitecture architecture) - { - architecture.RegisterUtility(new SaveUtility()); - } -} - -public class PlayerModule : AbstractModule -{ - public override void Install(IArchitecture architecture) - { - architecture.RegisterSystem(new PlayerSystem()); - } - - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - if (phase == ArchitecturePhase.Ready) - { - // 通过事件进行模块间通信 - this.RegisterEvent(OnPlayerDeath); - } - } - - private void OnPlayerDeath(PlayerDeathEvent e) - { - // 触发保存模块的事件 - this.SendEvent(new RequestAutoSaveEvent { Reason = "Player Death" }); - } -} -``` - -## 性能特性 - -### 📊 内存管理 - -- **缓存策略**:多层缓存减少磁盘 I/O -- **延迟加载**:按需加载资源,减少内存占用 -- **对象池化**:重用对象,减少 GC 压力 -- **内存映射**:大文件使用内存映射技术 - -### ⚡ 存储性能 - -```csharp -// 性能对比示例 -public class StoragePerformanceTest -{ - // 同步操作:简单直接 - public void SyncWrite(IStorage storage, string key, object data) - { - storage.Write(key, data); // ~1-5ms - } - - // 异步操作:非阻塞 - public async Task AsyncWrite(IStorage storage, string key, object data) - { - await storage.WriteAsync(key, data); // ~0.1-1ms (不阻塞主线程) - } - - // 批量操作:高吞吐量 - public async Task BatchWrite(IStorage storage, Dictionary data) - { - var tasks = data.Select(kvp => storage.WriteAsync(kvp.Key, kvp.Value)); - await Task.WhenAll(tasks); // ~10-50ms for 100 items - } -} -``` - -### 🔄 序列化优化 - -- **格式选择**:JSON(可读性)vs 二进制(性能) -- **压缩技术**:减少存储空间和网络传输 -- **增量序列化**:只序列化变化的部分 -- **版本控制**:向后兼容的数据迁移 - ---- - -## 依赖关系 - -```mermaid -graph TD - A[GFramework.Game] --> B[GFramework.Core] - A --> C[GFramework.Core.Abstractions] - A --> D[Newtonsoft.Json] - A --> E[System.Text.Json] -``` - -## 版本兼容性 - -- **.NET**: 6.0+ -- **Newtonsoft.Json**: 13.0.3+ -- **GFramework.Core**: 与 Core 模块版本保持同步 ---- +- `GFramework.Game/README.md` +- `GFramework.Game.Abstractions/README.md` +- 仓库根 `README.md` diff --git a/docs/zh-CN/source-generators/index.md b/docs/zh-CN/source-generators/index.md index 7d11b400..cb07dba2 100644 --- a/docs/zh-CN/source-generators/index.md +++ b/docs/zh-CN/source-generators/index.md @@ -1,1320 +1,100 @@ # Source Generators -> 编译时代码生成 - 零运行时开销的代码增强工具 +`Source Generators` 栏目对应 `GFramework` 当前按模块拆分发布的编译期工具链。 -GFramework 当前按模块提供一组 Source Generators,通过编译时分析自动生成样板代码,显著提升开发效率并减少运行时开销。 +这里的重点不是“存在一个统一的大生成器包”,而是帮助你确认应该安装哪个生成器包、它服务哪个运行时模块,以及继续去看哪一类专题页。 -## 📋 目录 +## 当前包拆分 -- [概述](#概述) -- [核心特性](#核心特性) -- [安装配置](#安装配置) -- [Log 属性生成器](#log-属性生成器) -- [Config Schema 生成器](#config-schema-生成器) -- [CQRS Handler Registry 生成器](#cqrs-handler-registry-生成器) -- [ContextAware 属性生成器](#contextaware-属性生成器) -- [GenerateEnumExtensions 属性生成器](#generateenumextensions-属性生成器) -- [Priority 属性生成器](#priority-属性生成器) -- [Context Get 注入生成器](#context-get-注入生成器) -- [AutoRegisterModule 生成器](#autoregistermodule-生成器) -- [Godot 项目元数据生成](#godot-项目元数据生成) -- [GetNode 生成器 (Godot)](#getnode-生成器) -- [BindNodeSignal 生成器 (Godot)](#bindnodesignal-生成器) -- [AutoUiPage 生成器 (Godot)](#autouipage-生成器) -- [AutoScene 生成器 (Godot)](#autoscene-生成器) -- [AutoRegisterExportedCollections 生成器 (Godot)](#autoregisterexportedcollections-生成器) -- [诊断信息](#诊断信息) -- [性能优势](#性能优势) -- [使用示例](#使用示例) -- [最佳实践](#最佳实践) -- [常见问题](#常见问题) - -## 概述 - -GFramework 的 Source Generators 利用 Roslyn 源代码生成器技术,在编译时分析你的代码并自动生成常用的样板代码,让开发者专注于业务逻辑而不是重复的模板代码。 - -当前 NuGet 发布按模块拆分为: +GFramework 当前发布的生成器包是: - `GeWuYou.GFramework.Core.SourceGenerators` - `GeWuYou.GFramework.Game.SourceGenerators` -- `GeWuYou.GFramework.Godot.SourceGenerators` - `GeWuYou.GFramework.Cqrs.SourceGenerators` +- `GeWuYou.GFramework.Godot.SourceGenerators` -不存在 `GeWuYou.GFramework.SourceGenerators` 或 `GeWuYou.GFramework.SourceGenerators.Attributes` 这类聚合包。 +不存在 `GeWuYou.GFramework.SourceGenerators` 或 `*.SourceGenerators.Attributes` 这类聚合包。 -### 核心设计理念 +## 先按场景选包 -- **零运行时开销**:代码在编译时生成,无反射或动态调用 -- **类型安全**:编译时类型检查,避免运行时错误 -- **开发效率**:自动生成样板代码,减少重复工作 -- **可配置性**:支持多种配置选项满足不同需求 +- 想减少日志、上下文注入、模块自动注册等 Core 侧样板代码: + - 选择 `GeWuYou.GFramework.Core.SourceGenerators` +- 想把 `schemas/**/*.schema.json` 生成成配置类型和表包装: + - 选择 `GeWuYou.GFramework.Game.SourceGenerators` +- 想让 CQRS handler registry 在编译期生成,缩小运行时反射扫描范围: + - 选择 `GeWuYou.GFramework.Cqrs.SourceGenerators` +- 想在 Godot 项目里生成 AutoLoad / Input Action 入口,或减少节点与信号样板代码: + - 选择 `GeWuYou.GFramework.Godot.SourceGenerators` -## 核心特性 +## 与运行时的关系 -### 🎯 主要生成器 +这些包都是编译期工具链,不是运行时库。 -- **[Log] 属性**:自动生成 ILogger 字段和日志方法 -- **Config Schema 生成器**:根据 `*.schema.json` 生成配置类型和表包装 -- **CQRS Handler Registry 生成器**:为 CQRS handlers 生成程序集级注册表并缩小运行时反射范围 -- **[ContextAware] 属性**:自动实现 IContextAware 接口 -- **[GenerateEnumExtensions] 属性**:自动生成枚举扩展方法 -- **[Priority] 属性**:自动实现 IPrioritized 接口,为类添加优先级标记 -- **Context Get 注入特性**:自动注入架构组件(GetModel/GetSystem/GetUtility/GetService/GetAll) -- **[AutoRegisterModule] 属性**:为模块类自动生成固定顺序的 `Install(IArchitecture)` 注册代码 +对应关系如下: + +| 生成器包 | 主要服务的运行时 | +| --- | --- | +| `GFramework.Core.SourceGenerators` | `GFramework.Core` | +| `GFramework.Game.SourceGenerators` | `GFramework.Game` | +| `GFramework.Cqrs.SourceGenerators` | `GFramework.Cqrs` | +| `GFramework.Godot.SourceGenerators` | `GFramework.Godot` | + +安装时通常保持生成器包与对应运行时包版本一致,并将生成器声明为: + +```xml + +``` + +其他生成器包的安装模式相同。 + +## 这个栏目怎么读 + +### Core 侧通用生成器 + +- [logging-generator](./logging-generator.md) +- [context-aware-generator](./context-aware-generator.md) +- [context-get-generator](./context-get-generator.md) +- [enum-generator](./enum-generator.md) +- [priority-generator](./priority-generator.md) +- [auto-register-module-generator](./auto-register-module-generator.md) + +### Game / CQRS 相关生成器 + +- 配置 schema 生成与运行时接法: + - [../game/config-system.md](../game/config-system.md) +- CQRS registry 生成入口: + - [../core/cqrs.md](../core/cqrs.md) ### Godot 专用生成器 -- **Godot 项目元数据生成 (Godot)**:从 `project.godot` 生成 AutoLoad 与 Input Action 的强类型访问入口 -- **[GetNode] 属性 (Godot)**:自动获取 Godot 节点引用,支持多种查找模式 -- **[BindNodeSignal] 属性 (Godot)**:自动生成 Godot 节点信号绑定与解绑逻辑 -- **[AutoUiPage] 属性 (Godot)**:自动生成 UI 页面行为包装与页面 Key -- **[AutoScene] 属性 (Godot)**:自动生成场景行为包装与场景 Key -- **[AutoRegisterExportedCollections] 属性 (Godot)**:自动生成导出集合的批量注册方法 - -### 🔧 高级特性 - -- **智能诊断**:生成器包含详细的错误诊断信息 -- **增量编译**:只生成修改过的代码,提高编译速度 -- **命名空间控制**:灵活控制生成代码的命名空间 -- **可访问性控制**:支持不同的访问修饰符 -- **智能推断注入**:[GetAll] 自动推断字段类型并注入架构组件 -- **优先级排序建议**:分析器自动建议使用 GetAllByPriority 确保正确排序 - -## 安装配置 - -### NuGet 包安装 - -```xml - - - net6.0 - - - - - - - -``` - -如果你只使用 Godot 生成器或 CQRS 处理器注册生成器,请把上面的包替换为对应的 -`GeWuYou.GFramework.Godot.SourceGenerators` 或 `GeWuYou.GFramework.Cqrs.SourceGenerators`。 -这些拆分包会同时带上各自需要的 abstractions 程序集,不需要再额外安装单独的 `*.Attributes` 包。 -实际接入时请替换为当前发布版本,或与项目中其余 `GeWuYou.GFramework.*` 包保持同一版本。 - -### Config Schema 文件约定 - -当项目引用 `GeWuYou.GFramework.Game.SourceGenerators` 的打包产物时,生成器会默认从 `schemas/**/*.schema.json` 收集配置 -schema -文件并作为 `AdditionalFiles` 输入。 - -这意味着消费者项目通常只需要维护如下结构: - -```text -GameProject/ -├─ config/ -│ └─ monster/ -│ └─ slime.yaml -└─ schemas/ - └─ monster.schema.json -``` - -如果你需要完整接入运行时加载、schema 校验和 VS Code 工具链,请继续阅读: - -- [游戏内容配置系统](/zh-CN/game/config-system) - -## Config Schema 生成器 - -Config Schema 生成器会扫描 `*.schema.json` 文件,并生成: - -- 配置数据类型 -- 与 `IConfigTable` 对齐的表包装类型 - -这一生成器适合与 `GFramework.Game.Config.YamlConfigLoader` 配合使用,让 schema、运行时和工具链共享同一份结构约定。 - -当前支持的 schema 子集以内容配置系统文档中的说明为准,重点覆盖: - -- `object` 根节点 -- `required` -- `integer` -- `number` -- `boolean` -- `string` -- `array` - -### 项目文件配置 - -```xml - - - - net6.0 - true - Generated - - - - - - -``` - -## CQRS Handler Registry 生成器 - -`GeWuYou.GFramework.Cqrs.SourceGenerators` 会在编译期分析当前业务程序集中的 CQRS handlers,并生成: - -- `ICqrsHandlerRegistry` 实现,用于在启动时直接注册可安全引用的 handlers -- 程序集级 `CqrsHandlerRegistryAttribute` 元数据,供运行时优先走生成注册路径 -- 必要时的 `CqrsReflectionFallbackAttribute`,让运行时只补扫生成代码无法合法引用的 handlers - -### 接入包 - -如果你的项目已经使用 GFramework 架构层,请在现有 Core 依赖基础上补齐 CQRS runtime 与 generator: - -```xml - - - - - -``` - -如果当前项目还没有接入架构运行时,请同时保持 `GeWuYou.GFramework.Core` / -`GeWuYou.GFramework.Core.Abstractions` 与 CQRS 包版本一致。 - -### 最小示例 - -下面的最小示例展示了“安装 runtime + source generator 后,正常注册程序集”的接入方式。运行时会优先使用生成的 -handler registry;如果某个 handler 无法被生成代码直接引用,则自动补走定向反射回退。 - -```csharp -using GFramework.Core.Architectures; -using GFramework.Cqrs.Abstractions.Cqrs.Command; -using GFramework.Cqrs.Cqrs.Command; - -public sealed record CreatePlayerCommand(string Name) : ICommand; - -public sealed class CreatePlayerCommandHandler : AbstractCommandHandler -{ - public override ValueTask Handle(CreatePlayerCommand command, CancellationToken cancellationToken) - { - return ValueTask.FromResult(command.Name.Length); - } -} - -public sealed class GameArchitecture : Architecture -{ - protected override void OnInitialize() - { - RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly); - } -} -``` - -### 兼容性与迁移说明 - -- 不安装 `GeWuYou.GFramework.Cqrs.SourceGenerators` 也可以正常运行;此时 CQRS runtime 会继续使用反射扫描注册 - handlers。 -- 安装生成器后,不需要额外改写 `RegisterCqrsHandlersFromAssembly(...)` / - `RegisterCqrsHandlersFromAssemblies(...)` 调用点;运行时会自动优先使用生成注册表。 -- CQRS 消息基类位于 `GFramework.Cqrs.Command` / `Query` / `Notification`,而处理器基类位于 - `GFramework.Cqrs.Cqrs.*` 命名空间。文档示例需要分别引用两组命名空间。 - -## Log 属性生成器 - -[Log] 属性自动为标记的类生成日志记录功能,包括 ILogger 字段和便捷的日志方法。 - -### 基础使用 - -```csharp -using GFramework.Core.SourceGenerators.Abstractions.Logging; - -[Log] -public partial class PlayerController -{ - public void DoSomething() - { - Logger.Info("Doing something"); // 自动生成的 Logger 字段 - Logger.Debug("Debug information"); - Logger.Warning("Warning message"); - Logger.Error("Error occurred"); - } -} -``` - -### 生成的代码 - -编译器会自动生成如下代码: - -```csharp -// -public partial class PlayerController -{ - private static readonly ILogger Logger = - LoggerFactoryResolver.Provider.CreateLogger("YourNamespace.PlayerController"); -} -``` - -**注意**:生成器只生成 ILogger 字段,不生成日志方法。日志方法(Info、Debug、Error 等)来自 ILogger 接口本身。 - -### 高级配置 - -```csharp -[Log( - Name = "Custom.PlayerLogger", // 自定义日志分类名称 - FieldName = "CustomLogger", // 自定义字段名 - IsStatic = false, // 是否为静态字段 - AccessModifier = "public" // 访问修饰符 -)] -public partial class CustomLoggerExample -{ - public void LogSomething() - { - CustomLogger.Info("Custom logger message"); - } -} -``` - -### 配置选项说明 - -| 参数 | 类型 | 默认值 | 说明 | -|----------------|---------|-----------|---------------------------------| -| Name | string? | null | 日志分类名称(默认使用类名) | -| FieldName | string | "Logger" | 生成的字段名称 | -| IsStatic | bool | true | 是否生成静态字段 | -| AccessModifier | string | "private" | 访问修饰符(private/protected/public) | - -### 静态类支持 - -```csharp -[Log] -public static partial class MathHelper -{ - public static int Add(int a, int b) - { - Logger.Debug($"Adding {a} and {b}"); - return a + b; - } -} -``` - -## ContextAware 属性生成器 - -[ContextAware] 属性自动实现 IContextAware 接口,提供便捷的架构上下文访问能力。 - -### 基础使用 - -```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class PlayerController : IController -{ - public void Initialize() - { - // 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口) - var playerModel = this.GetModel(); - var combatSystem = this.GetSystem(); - - this.SendEvent(new PlayerInitializedEvent()); - } -} -``` - -### 生成的代码 - -编译器会自动生成如下代码: - -```csharp -// -#nullable enable - -namespace YourNamespace; - -partial class PlayerController : global::GFramework.Core.Abstractions.Rule.IContextAware -{ - private global::GFramework.Core.Abstractions.Architecture.IArchitectureContext? _context; - private static global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider? _contextProvider; - - /// - /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider) - /// - protected global::GFramework.Core.Abstractions.Architecture.IArchitectureContext Context - { - get - { - if (_context == null) - { - _contextProvider ??= new global::GFramework.Core.Architecture.GameContextProvider(); - _context = _contextProvider.GetContext(); - } - - return _context; - } - } - - /// - /// 配置上下文提供者(用于测试或多架构场景) - /// - public static void SetContextProvider(global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider provider) - { - _contextProvider = provider; - } - - /// - /// 重置上下文提供者为默认值(用于测试清理) - /// - public static void ResetContextProvider() - { - _contextProvider = null; - } - - void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architecture.IArchitectureContext context) - { - _context = context; - } - - global::GFramework.Core.Abstractions.Architecture.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext() - { - return Context; - } -} -``` - -### 测试场景配置 - -在单元测试中,可以配置自定义的上下文提供者: - -```csharp -[Test] -public async Task TestPlayerController() -{ - var testArchitecture = new TestArchitecture(); - await testArchitecture.InitAsync(); - - // 配置测试上下文提供者 - PlayerController.SetContextProvider(new TestContextProvider(testArchitecture)); - - try - { - var controller = new PlayerController(); - controller.Initialize(); - // 测试逻辑... - } - finally - { - // 清理:重置上下文提供者 - PlayerController.ResetContextProvider(); - } -} -``` - -### 与其他属性组合 - -```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Logging; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[Log] -[ContextAware] -public partial class AdvancedController : IController -{ - public void ProcessRequest() - { - Logger.Info("Processing request"); - - var model = this.GetModel(); - Logger.Info($"Player health: {model.Health}"); - - this.SendCommand(new ProcessCommand()); - Logger.Debug("Command sent"); - } -} -``` - -## GenerateEnumExtensions 属性生成器 - -[GenerateEnumExtensions] 属性为枚举类型生成便捷的扩展方法,提高代码可读性和类型安全性。 - -### 基础使用 - -```csharp -using GFramework.Core.SourceGenerators.Abstractions.Enums; - -[GenerateEnumExtensions] -public enum GameState -{ - Playing, - Paused, - GameOver, - Menu -} - -// 自动生成的扩展方法: -public static class GameStateExtensions -{ - public static bool IsPlaying(this GameState state) => state == GameState.Playing; - public static bool IsPaused(this GameState state) => state == GameState.Paused; - public static bool IsGameOver(this GameState state) => state == GameState.GameOver; - public static bool IsMenu(this GameState state) => state == GameState.Menu; - - public static bool IsIn(this GameState state, params GameState[] values) - { - if (values == null) return false; - foreach (var v in values) if (state == v) return true; - return false; - } -} - -// 使用示例 -public class GameManager -{ - private GameState _currentState = GameState.Menu; - - public bool CanProcessInput() - { - return _currentState.IsPlaying() || _currentState.IsMenu(); - } - - public bool IsGameOver() - { - return _currentState.IsGameOver(); - } - - public bool IsActiveState() - { - return _currentState.IsIn(GameState.Playing, GameState.Menu); - } -} -``` - -### 配置选项 - -```csharp -[GenerateEnumExtensions( - GenerateIsMethods = true, // 是否生成 IsX 方法(默认 true) - GenerateIsInMethod = true // 是否生成 IsIn 方法(默认 true) -)] -public enum PlayerState -{ - Idle, - Walking, - Running, - Jumping, - Attacking -} -``` - -### 配置选项说明 - -| 参数 | 类型 | 默认值 | 说明 | -|--------------------|------|------|-------------------| -| GenerateIsMethods | bool | true | 是否为每个枚举值生成 IsX 方法 | -| GenerateIsInMethod | bool | true | 是否生成 IsIn 方法 | - -## Godot 项目元数据生成 - -Godot 项目元数据生成器会读取 `project.godot`,把项目级配置转换为稳定的编译期 API。 - -当前生成两个统一入口: - -- `GFramework.Godot.Generated.AutoLoads` -- `GFramework.Godot.Generated.InputActions` - -默认情况下,NuGet 包引用会自动把项目根目录下的 `project.godot` 加入 `AdditionalFiles`。如果你是在仓库内通过 analyzer -形式直接引用生成器,则需要手动加入: - -```xml - - - -``` - -### AutoLoad 映射 - -当某个 AutoLoad 不能仅靠类名唯一推断到 C# 节点类型时,可以使用 `[AutoLoad]` 指定映射: - -```csharp -using GFramework.Godot.SourceGenerators.Abstractions; -using Godot; - -[AutoLoad("GameServices")] -public partial class GameServices : Node -{ -} -``` - -生成后可以直接使用: - -```csharp -using GFramework.Godot.Generated; - -var services = AutoLoads.GameServices; - -if (AutoLoads.TryGetAudioBus(out var audioBus)) -{ -} -``` - -### Input Action 常量 - -`[input]` 段会被转换为强类型常量: - -```csharp -using GFramework.Godot.Generated; - -if (Input.IsActionPressed(InputActions.MoveUp)) -{ -} -``` - -完整说明请见:[Godot 项目元数据生成器](./godot-project-generator) - -## GetNode 生成器 - -GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode()` 方法。 - -### 主要功能 - -- **自动节点获取**:根据路径或字段名自动获取 Godot 节点 -- **多种查找模式**:支持唯一名(`%Name`)、相对路径、绝对路径查找 -- **可选节点支持**:可以标记节点为可选,获取失败时返回 null -- **_Ready 钩子**:自动生成 `_Ready()` 方法注入节点获取逻辑 - -### 基础示例 - -```csharp -using GFramework.Godot.SourceGenerators.Abstractions; -using Godot; - -public partial class PlayerHud : Control -{ - [GetNode] - private Label _healthLabel = null!; - - [GetNode("HUD/ScoreValue")] - private Label _scoreLabel = null!; - - public override void _Ready() - { - __InjectGetNodes_Generated(); - _healthLabel.Text = "100"; - } -} -``` - -**完整文档**:[GetNode 生成器](./get-node-generator) - -## AutoRegisterModule 生成器 - -AutoRegisterModule 生成器面向 GFramework 模块安装场景,为类上的注册声明自动生成 `Install(IArchitecture)`。 - -### 主要功能 - -- **声明式模块注册**:用 Attribute 声明 `Model` / `System` / `Utility` -- **稳定顺序输出**:按 Attribute 书写顺序生成注册代码 -- **零反射开销**:编译期生成 `new T()` 与 `architecture.RegisterXxx(...)` - -### 基础示例 - -```csharp -using GFramework.Core.SourceGenerators.Abstractions.Architectures; - -[AutoRegisterModule] -[RegisterModel(typeof(RunStateModel))] -[RegisterSystem(typeof(BuildSystem))] -[RegisterUtility(typeof(AudioUtility))] -public partial class GameplayModule -{ -} -``` - -**完整文档**:[AutoRegisterModule 生成器](./auto-register-module-generator) - -## BindNodeSignal 生成器 - -BindNodeSignal 生成器为标记了 `[BindNodeSignal]` 特性的方法自动生成节点事件绑定和解绑代码。 - -### 主要功能 - -- **自动事件绑定**:在 `_Ready()` 中自动订阅节点事件 -- **自动事件解绑**:在 `_ExitTree()` 中自动取消订阅 -- **多事件绑定**:一个方法可以绑定到多个节点事件 -- **类型安全检查**:编译时验证方法签名与事件委托的兼容性 - -### 基础示例 - -```csharp -using GFramework.Godot.SourceGenerators.Abstractions; -using Godot; - -public partial class MainMenu : Control -{ - private Button _startButton = null!; - - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - private void OnStartButtonPressed() - { - StartGame(); - } - - public override void _Ready() - { - __BindNodeSignals_Generated(); - } - - public override void _ExitTree() - { - __UnbindNodeSignals_Generated(); - } -} -``` - -**完整文档**:[BindNodeSignal 生成器](./bind-node-signal-generator) - -## AutoUiPage 生成器 - -AutoUiPage 生成器为 Godot 页面节点自动生成 `UiKeyStr`、缓存的 `IUiPageBehavior` 和 `GetPage()`。 - -### 主要功能 - -- **页面键统一声明**:用一个 Attribute 同时声明页面 Key 与 `UiLayer` -- **行为缓存**:延迟创建并缓存 `IUiPageBehavior` -- **减少页面样板**:不再手写重复的页面工厂包装 - -### 基础示例 - -```csharp -using GFramework.Godot.SourceGenerators.Abstractions.UI; -using GFramework.Game.Abstractions.Enums; -using Godot; - -[AutoUiPage(nameof(UiKey.MainMenu), nameof(UiLayer.Page))] -public partial class MainMenu : Control -{ -} -``` - -**完整文档**:[AutoUiPage 生成器](./auto-ui-page-generator) - -## AutoScene 生成器 - -AutoScene 生成器为场景根节点自动生成 `SceneKeyStr`、缓存的 `ISceneBehavior` 和 `GetScene()`。 - -### 主要功能 - -- **场景键统一声明**:避免散落的场景 key 字符串 -- **行为缓存**:延迟创建并复用场景行为对象 -- **路由接入更直接**:场景节点可直接暴露标准场景行为入口 - -### 基础示例 - -```csharp -using GFramework.Godot.SourceGenerators.Abstractions.UI; -using GFramework.Game.Abstractions.Enums; -using Godot; - -[AutoScene(nameof(SceneKey.Gameplay))] -public partial class GameplayRoot : Node2D -{ -} -``` - -**完整文档**:[AutoScene 生成器](./auto-scene-generator) - -## AutoRegisterExportedCollections 生成器 - -AutoRegisterExportedCollections 生成器为 Godot 导出集合自动生成批量注册方法,适合启动入口和资源映射注册。 - -### 主要功能 - -- **批量注册样板收敛**:统一 `foreach + registry.Register(item)` 模式 -- **编译期校验**:验证集合、注册器成员和方法签名 -- **支持接口注册器**:兼容从基类和继承接口解析注册方法 - -### 基础示例 - -```csharp -using GFramework.Godot.SourceGenerators.Abstractions.UI; -using Godot; - -[AutoRegisterExportedCollections] -public partial class GameEntryPoint : Node -{ - [RegisterExportedCollection(nameof(_textureRegistry), "Registry")] - private Godot.Collections.Array? _textureConfigs; -} -``` - -**完整文档**:[AutoRegisterExportedCollections 生成器](./auto-register-exported-collections-generator) - -## 诊断信息 - -这一组 Source Generators 提供详细的编译时诊断信息,帮助开发者快速定位和解决问题。 - -### GF_Logging_001 - 日志字段名冲突 - -```csharp -[Log(fieldName = "Logger")] -public partial class ClassWithLogger -{ - private readonly ILogger Logger; // ❌ 冲突! -} -``` - -**错误信息**: `GF_Logging_001: Logger field name 'Logger' conflicts with existing field` - -**解决方案**: 更改字段名或移除冲突字段 - -```csharp -[Log(fieldName = "CustomLogger")] -public partial class ClassWithLogger -{ - private readonly ILogger Logger; // ✅ 不冲突 - private static readonly ILogger CustomLogger; // ✅ 生成器使用 CustomLogger -} -``` - -### GF_Rule_001 - ContextAware 接口冲突 - -```csharp -[ContextAware] -public partial class AlreadyContextAware : IContextAware // ❌ 冲突! -{ - // 已实现 IContextAware -} -``` - -**错误信息**: `GF_Rule_001: Type already implements IContextAware interface` - -**解决方案**: 移除 [ContextAware] 属性或移除手动实现 - -```csharp -// 方案1:移除属性 -public partial class AlreadyContextAware : IContextAware -{ - // 手动实现 -} - -// 方案2:移除手动实现,使用生成器 -[ContextAware] -public partial class AlreadyContextAware -{ - // 生成器自动实现 -} -``` - -### GF_Enum_001 - 枚举成员命名冲突 - -```csharp -[GenerateEnumExtensions] -public enum ConflictEnum -{ - IsPlaying, // ❌ 冲突!会生成 IsIsPlaying() - HasJump // ❌ 冲突!会生成 HasHasJump() -} -``` - -**错误信息**: `GF_Enum_001: Enum member name conflicts with generated method` - -**解决方案**: 重命名枚举成员或自定义前缀 - -```csharp -[GenerateEnumExtensions(customPrefix = "IsState")] -public enum ConflictEnum -{ - Playing, // ✅ 生成 IsStatePlaying() - Jump // ✅ 生成 IsStateJump() -} -``` - -### Priority 相关诊断 - -`Priority` 相关规则以专用文档为权威来源: - -- 完整生成器诊断见 [Priority 生成器](./priority-generator.md#诊断信息) -- 排序分析器规则见 [与 PriorityUsageAnalyzer 集成](./priority-generator.md#与-priorityusageanalyzer-集成) - -| 诊断 ID | 含义 | 常见修复方向 | -|-------------------------|-------------------------------|------------------------------| -| `GF_Priority_001` | `[Priority]` 只能应用于类 | 仅在 `partial class` 上使用特性 | -| `GF_Priority_002` | 类型已手动实现 `IPrioritized` | 删除特性或删除手写接口实现 | -| `GF_Priority_003` | 标记类型未声明为 `partial` | 为类型添加 `partial` | -| `GF_Priority_004` | 优先级参数无效 | 提供有效的整数优先级值 | -| `GF_Priority_005` | 嵌套类不支持生成 | 将目标类型提取为顶层类 | -| `GF_Priority_Usage_001` | 应优先使用 `GetAllByPriority()` | 对实现 `IPrioritized` 的类型改用排序获取 | - -### Context Get 相关诊断 - -`Context Get` 相关规则以专用文档为权威来源: - -- 完整诊断见 [Context Get 注入生成器](./context-get-generator.md#诊断信息) -- 调用时机建议见 [推荐调用时机与模式](./context-get-generator.md#推荐调用时机与模式) - -| 诊断 ID | 含义 | 常见修复方向 | -|---------------------|--------------------|---------------------------------------------------------------| -| `GF_ContextGet_001` | 嵌套类不支持生成注入 | 将目标类型提取为顶层类 | -| `GF_ContextGet_002` | 注入字段不能为 `static` | 改为实例字段 | -| `GF_ContextGet_003` | 注入字段不能为 `readonly` | 移除 `readonly` | -| `GF_ContextGet_004` | 字段类型与注入特性不匹配 | 使用符合特性约束的字段类型 | -| `GF_ContextGet_005` | 目标类型必须具备上下文访问能力 | 添加 `[ContextAware]`、实现 `IContextAware` 或继承 `ContextAwareBase` | -| `GF_ContextGet_006` | 同一字段不能声明多个注入特性 | 每个字段只保留一个注入特性 | -| `GF_ContextRegistration_001` | `Model` 使用点没有静态可见注册 | 在所属架构的初始化链路中显式注册对应 `Model` | -| `GF_ContextRegistration_002` | `System` 使用点没有静态可见注册 | 在所属架构的初始化链路中显式注册对应 `System` | -| `GF_ContextRegistration_003` | `Utility` 使用点没有静态可见注册 | 在所属架构的初始化链路中显式注册对应 `Utility` | - -## 性能优势 - -### 编译时 vs 运行时对比 - -| 特性 | 手动实现 | 反射实现 | 源码生成器 | -|-----------|------|------|-------| -| **运行时性能** | 最优 | 最差 | 最优 | -| **内存开销** | 最小 | 最大 | 最小 | -| **类型安全** | 编译时 | 运行时 | 编译时 | -| **开发效率** | 低 | 中 | 高 | -| **调试友好** | 好 | 差 | 好 | - -### 基准测试结果 - -```csharp -// 日志性能对比 (100,000 次调用) -// 手动实现: 0.23ms -// 反射实现: 45.67ms -// 源码生成器: 0.24ms (几乎无差异) - -// 上下文访问性能对比 (1,000,000 次访问) -// 手动实现: 0.12ms -// 反射实现: 23.45ms -// 源码生成器: 0.13ms (几乎无差异) -``` - -### 内存分配分析 - -```csharp -// 使用 source generators 的内存分配 -[Log] -[ContextAware] -public partial class EfficientController : IController -{ - public void Process() - { - Logger.Info("Processing"); // 0 分配 - var model = this.GetModel(); // 0 分配 - } -} - -// 对比反射实现的内存分配 -public class InefficientController : IController -{ - public void Process() - { - var logger = GetLoggerViaReflection(); // 每次分配 - var model = GetModelViaReflection(); // 每次分配 - } -} -``` - -## 使用示例 - -### 完整的游戏控制器示例 - -```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Logging; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[Log] -[ContextAware] -public partial class GameController : Node, IController -{ - private PlayerModel _playerModel; - private CombatSystem _combatSystem; - - public override void _Ready() - { - // 初始化模型和系统引用 - _playerModel = this.GetModel(); - _combatSystem = this.GetSystem(); - - // 监听事件 - this.RegisterEvent(OnPlayerInput) - .UnRegisterWhenNodeExitTree(this); - - Logger.Info("Game controller initialized"); - } - - private void OnPlayerInput(PlayerInputEvent e) - { - Logger.Debug($"Processing player input: {e.Action}"); - - switch (e.Action) - { - case "attack": - HandleAttack(); - break; - case "defend": - HandleDefend(); - break; - } - } - - private void HandleAttack() - { - if (_playerModel.CanAttack()) - { - Logger.Info("Player attacks"); - _combatSystem.ProcessAttack(); - this.SendEvent(new AttackEvent()); - } - else - { - Logger.Warning("Player cannot attack - cooldown"); - } - } - - private void HandleDefend() - { - if (_playerModel.CanDefend()) - { - Logger.Info("Player defends"); - _playerModel.IsDefending.Value = true; - this.SendEvent(new DefendEvent()); - } - else - { - Logger.Warning("Player cannot defend"); - } - } -} -``` - -### 枚举状态管理示例 - -```csharp -[GenerateEnumExtensions( - generateIsMethods = true, - generateHasMethod = true, - generateInMethod = true, - includeToString = true -)] -public enum CharacterState -{ - Idle, - Walking, - Running, - Jumping, - Falling, - Attacking, - Hurt, - Dead -} - -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Logging; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[Log] -[ContextAware] -public partial class CharacterController : Node, IController -{ - private CharacterModel _characterModel; - - public override void _Ready() - { - _characterModel = this.GetModel(); - - // 监听状态变化 - _characterModel.State.Register(OnStateChanged); - } - - private void OnStateChanged(CharacterState newState) - { - Logger.Info($"Character state changed to: {newState.ToDisplayString()}"); - - // 使用生成的扩展方法 - if (newState.IsDead()) - { - HandleDeath(); - } - else if (newState.IsHurt()) - { - HandleHurt(); - } - else if (newState.In(CharacterState.Walking, CharacterState.Running)) - { - StartMovementEffects(); - } - - // 检查是否可以接受输入 - if (newState.In(CharacterState.Idle, CharacterState.Walking, CharacterState.Running)) - { - EnableInput(); - } - else - { - DisableInput(); - } - } - - private bool CanAttack() - { - var state = _characterModel.State.Value; - return state.In(CharacterState.Idle, CharacterState.Walking, CharacterState.Running); - } - - private void HandleDeath() - { - Logger.Info("Character died"); - DisableInput(); - PlayDeathAnimation(); - this.SendEvent(new CharacterDeathEvent()); - } -} -``` - -## 最佳实践 - -### 🎯 属性使用策略 - -#### 1. 合理的属性组合 - -```csharp -// 好的做法:相关功能组合使用 -[Log] -[ContextAware] -public partial class BusinessLogicComponent : IComponent -{ - // 既有日志记录又有上下文访问 -} - -// 避免:不必要的属性 -[Log] // ❌ 静态工具类通常不需要日志 -public static class MathHelper -{ - public static int Add(int a, int b) => a + b; -} -``` - -#### 2. 命名约定 - -```csharp -// 好的做法:一致的命名 -[Log(fieldName = "Logger")] -public partial class PlayerController { } - -[Log(fieldName = "Logger")] -public partial class EnemyController { } - -// 避免:不一致的命名 -[Log(fieldName = "Logger")] -public partial class PlayerController { } - -[Log(fieldName = "CustomLogger")] -public partial class EnemyController { } -``` - -### 🏗️ 项目组织 - -#### 1. 分离生成器和业务逻辑 - -```csharp -// 好的做法:部分类分离 -// PlayerController.Logic.cs - 业务逻辑 -public partial class PlayerController : IController -{ - public void Move(Vector2 direction) - { - if (CanMove()) - { - UpdatePosition(direction); - Logger.Debug($"Player moved to {direction}"); - } - } -} - -// PlayerController.Generated.cs - 生成代码所在 -// 不需要手动维护,由生成器处理 -``` - -#### 2. 枚举设计 - -```csharp -// 好的做法:有意义的枚举设计 -[GenerateEnumExtensions] -public enum GameState -{ - MainMenu, // 主菜单 - Playing, // 游戏中 - Paused, // 暂停 - GameOver, // 游戏结束 - Victory // 胜利 -} - -// 避免:含义不明确的枚举值 -[GenerateEnumExtensions] -public enum State -{ - State1, - State2, - State3 -} -``` - -### 🔧 性能优化 - -#### 1. 避免过度日志 - -```csharp -// 好的做法:合理的日志级别 -[Log] -public partial class PerformanceCriticalComponent -{ - public void Update() - { - // 只在必要时记录日志 - if (_performanceCounter % 1000 == 0) - { - Logger.Debug($"Performance: {_performanceCounter} ticks"); - } - } -} - -// 避免:过度日志记录 -[Log] -public partial class NoisyComponent -{ - public void Update() - { - Logger.Debug($"Frame: {Engine.GetProcessFrames()}"); // 太频繁 - } -} -``` - -### 🛡️ 错误处理 - -```csharp -[Log] -[ContextAware] -public partial class RobustComponent : IComponent -{ - public void RiskyOperation() - { - try - { - var model = this.GetModel(); - model.PerformRiskyOperation(); - Logger.Info("Operation completed successfully"); - } - catch (Exception ex) - { - Logger.Error($"Operation failed: {ex.Message}"); - this.SendEvent(new OperationFailedEvent { Error = ex.Message }); - } - } -} -``` - -## 常见问题 - -### Q: 为什么需要标记类为 `partial`? - -**A**: 源代码生成器需要向现有类添加代码,`partial` 关键字允许一个类的定义分散在多个文件中。生成器会在编译时创建另一个部分类文件,包含生成的代码。 - -```csharp -[Log] -public partial class MyClass { } // ✅ 需要 partial - -[Log] -public class MyClass { } // ❌ 编译错误,无法添加生成代码 -``` - -### Q: 生成的代码在哪里? - -**A**: 生成的代码在编译过程中创建,默认位置在 `obj/Debug/net6.0/generated/` 目录下。可以在项目文件中配置输出位置: - -```xml - - - Generated - -``` - -### Q: 如何调试生成器问题? - -**A**: 生成器提供了详细的诊断信息: - -1. **查看错误列表**:编译错误会显示在 IDE 中 -2. **查看生成文件**:检查生成的代码文件 -3. **启用详细日志**:在项目文件中添加: - -```xml - - - true - -``` - -### Q: 可以自定义生成器吗? - -**A**: 当前版本的生成器支持有限的配置。如需完全自定义,可以创建自己的源代码生成器项目。 - -### Q: 性能影响如何? - -**A**: 源代码生成器对运行时性能的影响几乎为零: - -- **编译时**:可能会增加编译时间(通常几秒) -- **运行时**:与手写代码性能相同 -- **内存使用**:与手写代码内存使用相同 - -### Q: 与依赖注入框架兼容吗? - -**A**: 完全兼容。生成器创建的是标准代码,可以与任何依赖注入框架配合使用: - -```csharp -[Log] -[ContextAware] -public partial class ServiceComponent : IService -{ - // 可以通过构造函数注入依赖 - private readonly IDependency _dependency; - - public ServiceComponent(IDependency dependency) - { - _dependency = dependency; - Logger.Info("Service initialized with dependency injection"); - } -} -``` - ---- - -## 依赖关系 - -```mermaid -graph TD - A[GeWuYou.GFramework.Core.SourceGenerators] --> B[GFramework.Core.SourceGenerators.Abstractions] - A --> C[GFramework.SourceGenerators.Common] - A --> D[GFramework.Core.Abstractions] - A --> E[Microsoft.CodeAnalysis.CSharp] - A --> F[Microsoft.CodeAnalysis.Analyzers] - G[GeWuYou.GFramework.Game.SourceGenerators] --> C - H[GeWuYou.GFramework.Godot.SourceGenerators] --> C - I[GeWuYou.GFramework.Cqrs.SourceGenerators] --> C -``` - -## 版本兼容性 - -- **.NET**: 6.0+ -- **Visual Studio**: 2022 17.0+ -- **Rider**: 2022.3+ -- **Roslyn**: 4.0+ +- [godot-project-generator](./godot-project-generator.md) +- [get-node-generator](./get-node-generator.md) +- [bind-node-signal-generator](./bind-node-signal-generator.md) +- [auto-ui-page-generator](./auto-ui-page-generator.md) +- [auto-scene-generator](./auto-scene-generator.md) +- [auto-register-exported-collections-generator](./auto-register-exported-collections-generator.md) + +## 推荐接入顺序 + +1. 先确认你已经选定运行时层,而不是先装生成器 +2. 再按运行时模块补充对应的生成器包 +3. 只在确实需要的项目里安装生成器,避免为了“可能以后会用”而把所有包一起引入 + +例如: + +- 新项目只需要 Core 上下文注入和日志辅助: + - 安装 `Core` + `Core.SourceGenerators` +- 需要静态 YAML 配置: + - 安装 `Game` + `Game.SourceGenerators` +- 需要 CQRS 生成注册表: + - 安装 `Cqrs` + `Cqrs.SourceGenerators` + +## 对应模块入口 + +- `GFramework.Core.SourceGenerators/README.md` +- `GFramework.Game.SourceGenerators/README.md` +- `GFramework.Cqrs.SourceGenerators/README.md` +- `GFramework.Godot.SourceGenerators/README.md` From dfeb40ba15a12c07946124aa5bc14b4175e2eccd Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:27:14 +0800 Subject: [PATCH 2/5] =?UTF-8?q?docs(documentation):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20ai-libs=20=E5=8F=82=E8=80=83=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 AGENTS、Game README 与游戏栏目入口中的参考表述,统一改为 ai-libs 下的只读参考实现 - 移除活跃文档入口中的旧外部项目命名,避免继续暴露特定参考仓库线索 - 补充 documentation-governance-and-refresh 主题的 tracking 与 trace,记录本轮引用迁移和后续约束 --- AGENTS.md | 4 +-- GFramework.Game.Abstractions/README.md | 9 ++++--- GFramework.Game/README.md | 25 ++++++++++--------- ...ntation-governance-and-refresh-tracking.md | 2 +- ...umentation-governance-and-refresh-trace.md | 10 ++++++++ docs/zh-CN/game/index.md | 4 +-- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 00311302..21170139 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -269,8 +269,8 @@ bash scripts/validate-csharp-naming.sh - Treat source code, `*.csproj`, tests, generated snapshots, and packaging metadata as the primary evidence for documentation updates. -- Treat `CoreGrid` as a secondary evidence source for real project adoption patterns, directory layouts, and end-to-end - usage examples. +- Treat verified reference implementations under `ai-libs/` as a secondary evidence source for real project adoption + patterns, directory layouts, and end-to-end usage examples. - Treat existing `README.md` files and `docs/zh-CN/` pages as editable outputs, not authoritative truth. - If existing documentation conflicts with code or tests, update the documentation to match the implementation instead of preserving outdated wording. diff --git a/GFramework.Game.Abstractions/README.md b/GFramework.Game.Abstractions/README.md index eb2031c7..91fa5f32 100644 --- a/GFramework.Game.Abstractions/README.md +++ b/GFramework.Game.Abstractions/README.md @@ -25,7 +25,8 @@ - `FileStorage`、`ScopedStorage`、`JsonSerializer`、`SettingsModel`、`SaveRepository`、`SceneRouterBase`、`UiRouterBase`、`YamlConfigLoader` 等都在实现这里的契约。 - 引擎适配包或项目代码 - `IUiFactory`、`ISceneFactory`、`IUiRoot`、`ISceneRoot`、资源注册表等通常由引擎适配层或游戏项目自己实现。 - - CoreGrid 的真实结构也是这样:页面/场景 factory、root、registry 在项目层,运行时基类和契约来自 `GFramework.Game` 与本包。 + - 仓库内 `ai-libs/` 下的只读参考实现通常也是这样组织:页面 / 场景 factory、root、registry 在项目层, + 运行时基类和契约来自 `GFramework.Game` 与本包。 ## 子系统地图 @@ -195,9 +196,9 @@ public sealed class ContinueGameCommandHandler 也就是说,本包回答的是“项目各层如何约定”,`GFramework.Game` 回答的是“这些约定默认怎么跑起来”。 -## CoreGrid 里的真实用法线索 +## `ai-libs/` 里的参考接入线索 -CoreGrid 对本包的使用方式,能比较清楚地说明它的职责边界: +`ai-libs/` 下的只读参考实现对本包的使用方式,能比较清楚地说明它的职责边界: - 公共脚本广泛引用: - `IUiRouter` @@ -213,7 +214,7 @@ CoreGrid 对本包的使用方式,能比较清楚地说明它的职责边界 - 真正的实现和装配则放在: - `GFramework.Game` - `GFramework.Godot.*` - - CoreGrid 自己的模块、factory、root、registry + - 项目自己的模块、factory、root、registry 这正是本包的设计目标:让业务层依赖稳定契约,而不是依赖具体运行时细节。 diff --git a/GFramework.Game/README.md b/GFramework.Game/README.md index 2b0cf825..60336e82 100644 --- a/GFramework.Game/README.md +++ b/GFramework.Game/README.md @@ -31,7 +31,8 @@ - 引擎适配包或项目内适配层 - 本包提供的是“引擎无关”的核心逻辑和基类。 - 真正和 Godot、Unity、MonoGame 等引擎对象打交道的工厂、根节点、资源注册表,通常在相邻引擎包或游戏项目内实现。 - - CoreGrid 的真实接法就是这样:配置文件 IO 由 `GFramework.Godot.Config` 适配,UI/Scene factory 与 root 由项目自己提供。 + - 仓库内 `ai-libs/` 下的只读参考实现通常也是这样接入:配置文件 IO 由 `GFramework.Godot.Config` 适配, + UI / Scene factory 与 root 由项目自己提供。 ## 子系统地图 @@ -72,7 +73,7 @@ - `SaveConfiguration` - 槽位目录、文件名、前缀等约定 -CoreGrid 的真实用法: +`ai-libs/` 下已验证参考实现的常见接法: - 设置持久化使用 `UnifiedSettingsDataRepository` - 存档使用 `SaveRepository` @@ -95,7 +96,7 @@ CoreGrid 的真实用法: - `Setting/Events/*` - 设置初始化、应用、保存、重置相关事件 -CoreGrid 的真实用法: +`ai-libs/` 下已验证参考实现的常见接法: - 在模型模块中创建 `SettingsModel` - 注册多个 applicator @@ -148,7 +149,7 @@ CoreGrid 的真实用法: - `Scene/Handler/*`、`UI/Handler/*` - 默认转换处理器基类与日志处理器 -CoreGrid 的真实用法: +`ai-libs/` 下已验证参考实现的常见接法: - 项目自定义 `SceneRouter : SceneRouterBase` - 项目自定义 `UiRouter : UiRouterBase` @@ -253,7 +254,7 @@ await settingsSystem.ApplyAll(); await settingsSystem.SaveAll(); ``` -CoreGrid 目前就是按这个思路接入,只是底层存储换成了 Godot 适配实现。 +`ai-libs/` 下的只读参考实现目前也是按这个思路接入,只是底层存储换成了 Godot 适配实现。 ### 3. 接入静态 YAML 配置 @@ -311,18 +312,18 @@ public sealed class MyUiRouter : UiRouterBase 这类 router 适合作为你的项目层或引擎适配层代码,而不是直接修改本包。 -## CoreGrid 里的真实用法线索 +## `ai-libs/` 里的参考接入线索 -当前仓库内,CoreGrid 对本包的使用大致分成三层: +当前仓库内的只读参考实现,对本包的使用大致分成三层: - 配置 - - `CoreGridConfigHost` 使用生成表元数据与 YAML loader 完成配置注册 + - 项目级配置宿主类型使用生成表元数据与 YAML loader 完成配置注册 - 设置与存档 - - `UtilityModule` 注册序列化器、底层存储、`UnifiedSettingsDataRepository`、`SaveRepository` - - `ModelModule` 创建 `SettingsModel` 并注册 applicator + - 项目层 utility 模块注册序列化器、底层存储、`UnifiedSettingsDataRepository`、 + `SaveRepository` + - 项目层 model 模块创建 `SettingsModel` 并注册 applicator - 路由 - - `SceneRouter` 继承 `SceneRouterBase` - - `UiRouter` 继承 `UiRouterBase` + - 项目自定义 `SceneRouterBase` / `UiRouterBase` 的派生类型 这说明本包更适合做“游戏基础设施层”,而不是把所有引擎对象耦死在包内部。 diff --git a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md index 0447eb82..c12904e4 100644 --- a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md +++ b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md @@ -34,7 +34,7 @@ - 旧专题页示例失真风险:`docs/zh-CN/core/*`、`game/*` 与 `source-generators/*` 中仍可能保留看似合理但与 真实实现不一致的示例 - - 缓解措施:继续按源码、测试、`*.csproj` 与 `CoreGrid` 真实接法核对,不把旧文档当事实来源 + - 缓解措施:继续按源码、测试、`*.csproj` 与 `ai-libs/` 下已验证参考实现核对,不把旧文档当事实来源 - 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择 - 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring - Active 入口回膨胀风险:后续若把栏目级重写过程直接追加到 active 文档,会再次拖慢恢复 diff --git a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md index 6b9bb94e..f0164e42 100644 --- a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md +++ b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md @@ -52,3 +52,13 @@ 1. 审核 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md`、`cqrs.md` 2. 记录每页的失真点、真实 API 名称与应保留的最小示例 3. 完成一轮专题页重写后再次执行 `cd docs && bun run build` + +### 补充:2026-04-21 内容引用迁移 + +- 按当前文档治理主题,继续清理活跃规范与面向读者的内容入口中的旧参考仓库命名 +- `AGENTS.md` 已把“secondary evidence source”从特定项目名收口为 `ai-libs/` 下的已验证只读参考实现 +- `GFramework.Game/README.md`、`GFramework.Game.Abstractions/README.md` 与 + `docs/zh-CN/game/index.md` 已同步改为 `ai-libs/` 参考表述,并去掉特定参考项目名称与项目内类型名线索 +- `documentation-governance-and-refresh` active tracking 已同步把风险缓解中的参考来源更新为 + `ai-libs/` 下已验证参考实现 +- 下一次专题页重写时,继续沿用同一表述,不再把特定参考项目名写入新的活跃文档入口 diff --git a/docs/zh-CN/game/index.md b/docs/zh-CN/game/index.md index 7a0738dd..59a00443 100644 --- a/docs/zh-CN/game/index.md +++ b/docs/zh-CN/game/index.md @@ -100,9 +100,9 @@ IStorage storage = new FileStorage("GameData", serializer); ## 与真实接法的关系 -这个栏目以源码、`*.csproj`、模块 `README.md` 与 `CoreGrid` 中已验证的接法为准。 +这个栏目以源码、`*.csproj`、模块 `README.md` 与 `ai-libs/` 下已验证的参考接法为准。 -例如当前仓库与 `CoreGrid` 的共同事实包括: +例如当前文档应优先和以下已验证事实保持一致: - 配置系统采用 `YAML + JSON Schema + Source Generator` - 设置持久化通常通过 `UnifiedSettingsDataRepository` From 60faf8eaff0068f20e82418555fc11ea7c8ef240 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:14:06 +0800 Subject: [PATCH 3/5] =?UTF-8?q?docs(core):=20=E9=87=8D=E5=86=99=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E4=B8=93=E9=A2=98=E9=A1=B5=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 architecture、context、lifecycle、command、query 与 cqrs 页面,使其对齐当前公开 API 与初始化语义 - 移除 Init、属性式总线、旧输入赋值示例和已移除的 Mediator 兼容入口等过时说明 - 补充 documentation-governance-and-refresh 主题的恢复点、验证结果与下一步专题页计划 --- ...ntation-governance-and-refresh-tracking.md | 26 +- ...umentation-governance-and-refresh-trace.md | 20 + docs/zh-CN/core/architecture.md | 328 +++----- docs/zh-CN/core/command.md | 487 ++---------- docs/zh-CN/core/context.md | 549 +++----------- docs/zh-CN/core/cqrs.md | 703 +++--------------- docs/zh-CN/core/lifecycle.md | 513 +++---------- docs/zh-CN/core/query.md | 553 ++------------ 8 files changed, 612 insertions(+), 2567 deletions(-) diff --git a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md index c12904e4..a3f984b8 100644 --- a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md +++ b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md @@ -7,20 +7,21 @@ ## 当前恢复点 -- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-002` -- 当前阶段:`Phase 2` +- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-003` +- 当前阶段:`Phase 3` - 当前焦点: - - 已完成 `docs/zh-CN/core/index.md`、`docs/zh-CN/game/index.md` 与 - `docs/zh-CN/source-generators/index.md` 的 landing page 重写 - - 栏目入口已改为以模块定位、包关系、最小接入路径和继续阅读为主,不再沿用旧版失真教程结构 - - 下一轮需要继续核对并重写 `docs/zh-CN/core/*`、`docs/zh-CN/game/*` 与 - `docs/zh-CN/source-generators/*` 的专题页内容 + - 已完成 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md` 与 + `cqrs.md` 的专题页重写 + - `core` 关键专题页已改回当前 `Architecture`、`ArchitectureContext`、旧 Command/Query 兼容层与新 CQRS + runtime 的真实入口语义 + - 下一轮需要继续推进 `docs/zh-CN/core/*` 余下专题页,以及 `docs/zh-CN/game/*`、 + `docs/zh-CN/source-generators/*` 的专题页核对 ## 当前状态摘要 - 文档治理规则已收口到仓库规范,README、站点入口与采用链路不再依赖旧文档自证 -- 高优先级模块入口已补齐,栏目 landing page 已回到可作为默认导航入口的状态 -- 当前主题仍是 active topic,因为核心栏目下的专题页仍可能包含与实现漂移的旧内容 +- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态 +- 当前主题仍是 active topic,因为 `core` 其余专题页及 `game`、`source-generators` 栏目下仍可能包含与实现漂移的旧内容 ## 当前活跃事实 @@ -29,6 +30,8 @@ - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 - `core`、`game` 与 `source-generators` 三个栏目入口页现在都以模块 README 与当前包拆分为准 - `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题 +- `core` 关键专题页已移除 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的 + `RegisterMediatorBehavior` 等过时说明 ## 当前风险 @@ -53,6 +56,7 @@ ## 下一步 -1. 先从 `docs/zh-CN/core/*` 开始,逐页核对架构、上下文、生命周期、命令、查询与 CQRS 的示例和术语 +1. 继续核对 `docs/zh-CN/core/*` 余下专题页,优先处理 `events`、`property`、`state-management`、`coroutine` + 与 `logging` 2. 再推进 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面 -3. 若专题页批量重写完成且验证通过,将本轮 landing page 收口和下一轮专题页修订过程迁入本 topic 的 `archive/` +3. 若专题页批量重写完成且验证通过,将本轮 `core` 专题页收口和后续修订过程迁入本 topic 的 `archive/` diff --git a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md index f0164e42..bea860e5 100644 --- a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md +++ b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md @@ -62,3 +62,23 @@ - `documentation-governance-and-refresh` active tracking 已同步把风险缓解中的参考来源更新为 `ai-libs/` 下已验证参考实现 - 下一次专题页重写时,继续沿用同一表述,不再把特定参考项目名写入新的活跃文档入口 + +### 补充:2026-04-21 Core 专题页收口(RP-003) + +- 复核 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md` 与 `cqrs.md` + 后确认:这些页面仍大量保留旧 API 叙述,例如 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` + 赋值式命令/查询示例,以及已移除的 `RegisterMediatorBehavior` +- 对照 `Architecture`、`ArchitectureContext`、`IArchitectureContext`、`ContextAwareBase`、旧 + `AbstractCommand` / `AbstractQuery` 基类和 `GFramework.Cqrs/README.md` 后,重写上述六个页面 +- 新版专题页将结构统一为“当前角色、真实公开入口、最小示例、兼容边界、迁移方向”,避免继续复刻旧版大而全教程 +- `core/context.md` 已明确把 `GameContext` 收束为兼容回退路径,而不是新代码的推荐接法 +- `core/command.md` 与 `core/query.md` 已明确旧体系仍可用,但新功能应优先走 `GFramework.Cqrs` +- `core/cqrs.md` 已与当前 runtime / generator / handler 注册语义对齐,并明确 `RegisterCqrsPipelineBehavior()` + 是公开入口 +- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页重写没有破坏文档站构建 + +### 下一步 + +1. 继续处理 `docs/zh-CN/core/events.md`、`property.md`、`state-management.md`、`coroutine.md`、`logging.md` +2. 保持同样的证据顺序:源码、`*.csproj`、模块 README、`ai-libs/` 参考实现 +3. 完成下一批专题页重写后再次执行 `cd docs && bun run build` diff --git a/docs/zh-CN/core/architecture.md b/docs/zh-CN/core/architecture.md index 671e847a..e1900382 100644 --- a/docs/zh-CN/core/architecture.md +++ b/docs/zh-CN/core/architecture.md @@ -1,239 +1,147 @@ -# Architecture 架构详解 +# Architecture -> 深入了解 GFramework 的核心架构设计和实现 +`Architecture` 是 `GFramework.Core` 的运行时入口。它负责三件事: -## 目录 +- 组织初始化与销毁阶段 +- 接入模型、系统、工具和模块 +- 暴露 `ArchitectureContext` 作为统一上下文入口 -- [概述](#概述) -- [架构设计](#架构设计) -- [生命周期管理](#生命周期管理) -- [组件注册](#组件注册) -- [模块系统](#模块系统) -- [最佳实践](#最佳实践) -- [API 参考](#api-参考) +当前版本的 `Architecture` 已经是协调器外观。对外仍保留稳定的注册与生命周期 API,但内部职责已经拆给专门协作者处理。 -## 概述 +## 你真正会用到的公开入口 -Architecture 是 GFramework 的核心类,负责管理整个应用的生命周期、组件注册和模块管理。从 v1.1.0 开始,Architecture -采用模块化设计,将职责分离到专门的协作者中。 +最常见的成员只有这些: -> 命名约定: -> - `ArchitectureServices` 是公开的基础服务入口,负责容器、事件总线、命令执行器、查询执行器和服务模块管理 -> - `ArchitectureComponentRegistry` 是内部组件注册器,专门负责 System / Model / Utility 的注册与生命周期接入 -> - 两者不是同一层职责,不要混用 +- `OnInitialize()` + - 子类唯一必须实现的入口,用来注册模型、系统、工具、模块和额外的 CQRS 行为 +- `RegisterModel(...)` / `RegisterSystem(...)` / `RegisterUtility(...)` + - 注册运行时组件 +- `InstallModule(...)` + - 安装实现了 `IArchitectureModule` 的模块 +- `RegisterLifecycleHook(...)` + - 注册阶段钩子 +- `RegisterCqrsPipelineBehavior()` + - 注册 CQRS pipeline 行为 +- `RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` + - 显式接入其他程序集中的 CQRS handlers +- `InitializeAsync()` / `WaitUntilReadyAsync()` + - 启动架构并等待进入 `Ready` +- `DestroyAsync()` + - 逆序销毁所有已接入组件 -### 设计目标 - -- **单一职责**: 每个管理器只负责一个明确的功能 -- **类型安全**: 基于泛型的组件获取和注册 -- **生命周期管理**: 自动的初始化和销毁机制 -- **可扩展性**: 支持模块和钩子扩展 -- **向后兼容**: 保持公共 API 稳定 - -### 核心组件 - -``` -Architecture (核心协调器) - ├── ArchitectureBootstrapper (初始化基础设施编排) - ├── ArchitectureLifecycle (生命周期管理) - ├── ArchitectureComponentRegistry (组件注册) - └── ArchitectureModules (模块管理) -``` - -## 架构设计 - -### 设计模式 - -Architecture 采用以下设计模式: - -1. **组合模式 (Composition)**: Architecture 组合多个内部协作者 -2. **委托模式 (Delegation)**: 方法调用委托给专门的管理器 -3. **协调器模式 (Coordinator)**: Architecture 作为协调器统一对外接口 - -### 类图 - -``` -┌─────────────────────────────────────────────────────┐ -│ Architecture │ -│ - _bootstrapper: ArchitectureBootstrapper │ -│ - _lifecycle: ArchitectureLifecycle │ -│ - _componentRegistry: ArchitectureComponentRegistry│ -│ - _modules: ArchitectureModules │ -│ - _logger: ILogger │ -│ │ -│ + RegisterSystem() │ -│ + RegisterModel() │ -│ + RegisterUtility() │ -│ + InstallModule() │ -│ + InitializeAsync() │ -│ + DestroyAsync() │ -│ + event PhaseChanged │ -└─────────────────────────────────────────────────────┘ - │ │ │ │ - │ │ │ │ - ▼ ▼ ▼ ▼ -┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐ -│ Bootstrapper │ │ Lifecycle │ │ComponentReg. │ │ Modules │ -│ │ │ │ │ │ │ │ -│ - 环境初始化 │ │ - 阶段管理 │ │ - System 注册│ │ - 模块安装 │ -│ - 服务准备 │ │ - 钩子管理 │ │ - Model 注册 │ │ - 行为注册 │ -│ - 上下文绑定 │ │ - 组件初始化 │ │ - Utility 注册│ │ │ -│ - 容器冻结 │ │ - 就绪/销毁协调 │ │ - 生命周期接入│ │ │ -└──────────────┘ └──────────────────┘ └──────────────┘ └──────────────┘ -``` - -### 构造函数初始化 - -从 v1.1.0 开始,所有管理器在构造函数中初始化: +## 最小示例 ```csharp -protected Architecture( - IArchitectureConfiguration? configuration = null, - IEnvironment? environment = null, - IArchitectureServices? services = null, - IArchitectureContext? context = null) -{ - var resolvedConfiguration = configuration ?? new ArchitectureConfiguration(); - var resolvedEnvironment = environment ?? new DefaultEnvironment(); - var resolvedServices = services ?? new ArchitectureServices(); - _context = context; +using GFramework.Core.Architectures; - // 初始化 Logger - LoggerFactoryResolver.Provider = resolvedConfiguration.LoggerProperties.LoggerFactoryProvider; - _logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name); - - // 初始化协作者 - _bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger); - _lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger); - _componentRegistry = new ArchitectureComponentRegistry(this, resolvedConfiguration, resolvedServices, _lifecycle, _logger); - _modules = new ArchitectureModules(this, resolvedServices, _logger); -} -``` - -**优势**: - -- 消除 `null!` 断言,提高代码安全性 -- 对象在构造后立即可用 -- 符合"构造即完整"原则 -- 可以在 InitializeAsync 之前访问事件 - -## 生命周期管理 - -### 架构阶段 - -Architecture 定义了 11 个生命周期阶段: - -| 阶段 | 说明 | 触发时机 | -|------------------------|--------------|------------------| -| `None` | 初始状态 | 构造函数完成后 | -| `BeforeUtilityInit` | Utility 初始化前 | 开始初始化 Utility | -| `AfterUtilityInit` | Utility 初始化后 | 所有 Utility 初始化完成 | -| `BeforeModelInit` | Model 初始化前 | 开始初始化 Model | -| `AfterModelInit` | Model 初始化后 | 所有 Model 初始化完成 | -| `BeforeSystemInit` | System 初始化前 | 开始初始化 System | -| `AfterSystemInit` | System 初始化后 | 所有 System 初始化完成 | -| `Ready` | 就绪状态 | 所有组件初始化完成 | -| `Destroying` | 销毁中 | 开始销毁 | -| `Destroyed` | 已销毁 | 销毁完成 | -| `FailedInitialization` | 初始化失败 | 初始化过程中发生异常 | - -### 阶段转换 - -``` -正常流程: -None → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit - → BeforeSystemInit → AfterSystemInit → Ready → Destroying → Destroyed - -异常流程: -Any → FailedInitialization -``` - -### 阶段事件 - -可以通过 `PhaseChanged` 事件监听阶段变化: - -```csharp -public class MyArchitecture : Architecture +public sealed class GameArchitecture : Architecture { protected override void OnInitialize() { - // 监听阶段变化 - PhaseChanged += phase => - { - Console.WriteLine($"Phase changed to: {phase}"); - }; + RegisterModel(new PlayerModel()); + RegisterSystem(new CombatSystem()); + RegisterUtility(new SaveUtility()); } } ``` -### 生命周期钩子 - -实现 `IArchitectureLifecycleHook` 接口可以在阶段变化时执行自定义逻辑: +启动方式: ```csharp -public class MyLifecycleHook : IArchitectureLifecycleHook +var architecture = new GameArchitecture(); +await architecture.InitializeAsync(); + +await architecture.WaitUntilReadyAsync(); +``` + +## 初始化时机 + +当前版本不再使用旧文档里的 `Init()` 入口。注册逻辑必须写在: + +```csharp +protected override void OnInitialize() +{ +} +``` + +框架会在 `InitializeAsync()` 内完成: + +1. 基础设施准备 +2. 创建并绑定 `ArchitectureContext` +3. 调用用户的 `OnInitialize()` +4. 按阶段初始化 `Utility -> Model -> System` +5. 进入 `Ready` + +如果你还看到旧示例里写 `protected override void Init()`,那就是过时内容。 + +## 组件注册顺序 + +`Architecture` 仍然维持清晰的组件边界: + +- `Model` + - 承载状态 +- `System` + - 承载业务流程 +- `Utility` + - 承载无状态或基础设施型能力 + +初始化顺序固定为: + +1. `Utility` +2. `Model` +3. `System` + +销毁时会按逆序处理,并优先调用异步销毁接口。 + +## 模块与 CQRS 接入 + +如果你的功能以模块形式组织,优先通过 `InstallModule(...)` 接入,而不是把所有注册逻辑都堆进一个超大的 `OnInitialize()`。 + +如果 handlers 不只在当前架构程序集里,需要显式追加程序集: + +```csharp +protected override void OnInitialize() +{ + RegisterCqrsPipelineBehavior>(); + + RegisterCqrsHandlersFromAssemblies( + [ + typeof(InventoryCqrsMarker).Assembly, + typeof(BattleCqrsMarker).Assembly + ]); +} +``` + +默认运行时会优先尝试消费端程序集上的生成注册表;缺失或不适用时回退到反射扫描。 + +## 阶段与钩子 + +`Architecture` 公开: + +- `CurrentPhase` +- `IsReady` +- `PhaseChanged` +- `RegisterLifecycleHook(...)` + +如果你需要在 `Ready`、`Destroying` 等阶段执行横切逻辑,比起把这类逻辑塞进某个具体 `System`,更适合单独实现 +`IArchitectureLifecycleHook`。 + +```csharp +public sealed class MetricsHook : IArchitectureLifecycleHook { public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) { - switch (phase) + if (phase == ArchitecturePhase.Ready) { - case ArchitecturePhase.Ready: - Console.WriteLine("Architecture is ready!"); - break; - case ArchitecturePhase.Destroying: - Console.WriteLine("Architecture is being destroyed!"); - break; + Console.WriteLine("Architecture ready."); } } } - -// 注册钩子 -architecture.RegisterLifecycleHook(new MyLifecycleHook()); ``` -### 初始化流程 +## 什么时候看别的页面 -``` -1. 创建 Architecture 实例 - └─> 构造函数初始化管理器 - -2. 调用 InitializeAsync() 或 Initialize() - ├─> ArchitectureBootstrapper 准备基础设施 - │ ├─> 初始化环境 (Environment.Initialize()) - │ ├─> 注册内置服务模块 - │ ├─> 初始化架构上下文并绑定 GameContext - │ ├─> 执行服务钩子 - │ └─> 初始化服务模块 - ├─> 调用 OnInitialize() (用户注册组件) - ├─> 初始化所有组件 - │ ├─> BeforeUtilityInit → 初始化 Utility → AfterUtilityInit - │ ├─> BeforeModelInit → 初始化 Model → AfterModelInit - │ └─> BeforeSystemInit → 初始化 System → AfterSystemInit - ├─> CompleteInitialization() 冻结 IoC 容器 - └─> 进入 Ready 阶段 - -3. 等待就绪 (可选) - └─> await architecture.WaitUntilReadyAsync() -``` - -### 销毁流程 - -``` -1. 调用 DestroyAsync() 或 Destroy() - ├─> 检查当前阶段 (如果是 None 或已销毁则直接返回) - ├─> 进入 Destroying 阶段 - ├─> 逆序销毁所有组件 - │ ├─> 优先调用 IAsyncDestroyable.DestroyAsync() - │ └─> 否则调用 IDestroyable.Destroy() - ├─> 销毁服务模块 - ├─> 进入 Destroyed 阶段 - └─> 清空 IoC 容器 -``` - ---- - -**版本**: 1.1.0 -**更新日期**: 2026-03-17 -**相关文档**: - -- [核心框架概述](./index.md) +- 想看上下文 API:转到 [context](./context.md) +- 想看阶段和销毁语义:转到 [lifecycle](./lifecycle.md) +- 想看旧命令 / 查询兼容层:转到 [command](./command.md) 和 [query](./query.md) +- 想看推荐的新请求模型:转到 [cqrs](./cqrs.md) diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md index d18b5513..9372b980 100644 --- a/docs/zh-CN/core/command.md +++ b/docs/zh-CN/core/command.md @@ -1,51 +1,29 @@ -# Command 包使用说明 +# Command -## 概述 +本页只说明 `GFramework.Core.Command` 里的旧命令体系。 -Command 包实现了命令模式(Command Pattern),用于封装用户操作和业务逻辑。通过命令模式,可以将请求封装为对象,实现操作的参数化、队列化、日志记录、撤销等功能。 +它仍然被保留,用来兼容存量代码;但如果你在写新功能,优先使用 [cqrs](./cqrs.md) 里的新请求模型。 -命令系统是 GFramework CQRS 架构的重要组成部分,与事件系统和查询系统协同工作,实现完整的业务逻辑处理流程。 +## 当前仍然可用的基类 -## 核心接口 +旧命令体系当前最常见的三个基类是: -### ICommand +- `AbstractCommand` + - 无输入、无返回值 +- `AbstractCommand` + - 有输入、无返回值 +- `AbstractCommand` + - 有输入、有返回值 -无返回值命令接口,定义了命令的基本契约。 +注意一个和旧文档不同的点:泛型命令现在通过构造函数接收输入,而不是依赖 `Input` 可写属性。 -**核心方法:** +## 无输入命令 ```csharp -void Execute(); // 执行命令 -``` +using GFramework.Core.Command; +using GFramework.Core.Extensions; -### ICommand`` - -带返回值的命令接口,用于需要返回执行结果的命令。 - -**核心方法:** - -```csharp -TResult Execute(); // 执行命令并返回结果 -``` - -## 核心类 - -### AbstractCommand - -无返回值命令的抽象基类,提供了命令的基础实现。它继承自 ContextAwareBase,具有上下文感知能力。 - -**核心方法:** - -```csharp -void ICommand.Execute(); // 实现 ICommand 接口 -protected abstract void OnExecute(); // 抽象执行方法,由子类实现 -``` - -**使用示例:** - -```csharp -// 定义一个无返回值的基础命令 -public class SimpleCommand : AbstractCommand +public sealed class RestoreHealthCommand : AbstractCommand { protected override void OnExecute() { @@ -54,422 +32,93 @@ public class SimpleCommand : AbstractCommand this.SendEvent(new PlayerHealthRestoredEvent()); } } - -// 使用命令 -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class GameController : IController -{ - public void OnRestoreHealthButtonClicked() - { - this.SendCommand(new SimpleCommand()); - } -} ``` -### AbstractCommand`` - -无输入参数但带返回值的命令基类。 - -**核心方法:** +发送方式: ```csharp -TResult ICommand.Execute(); // 实现 ICommand 接口 -protected abstract TResult OnExecute(); // 抽象执行方法,由子类实现 +this.SendCommand(new RestoreHealthCommand()); ``` -**使用示例:** +## 带输入命令 + +旧命令输入类型现在直接复用 CQRS 抽象层里的 `ICommandInput`: ```csharp -// 定义一个无输入但有返回值的命令 -public class GetPlayerHealthQuery : AbstractCommand +using GFramework.Core.Command; +using GFramework.Core.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs.Command; + +public sealed record DamagePlayerInput(int Amount) : ICommandInput; + +public sealed class DamagePlayerCommand(DamagePlayerInput input) + : AbstractCommand(input) { - protected override int OnExecute() + protected override void OnExecute(DamagePlayerInput input) { var playerModel = this.GetModel(); - return playerModel.Health.Value; - } -} - -// 使用命令 -public class UISystem : AbstractSystem -{ - protected override void OnInit() - { - this.RegisterEvent(OnUpdateUI); - } - - private void OnUpdateUI(UpdateUIEvent e) - { - var health = this.SendCommand(new GetPlayerHealthQuery()); - Console.WriteLine($"Player health: {health}"); + playerModel.Health.Value -= input.Amount; } } ``` -## 命令的生命周期 - -1. **创建命令**:实例化命令对象,传入必要的参数 -2. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()` -3. **返回结果**:对于带返回值的命令,返回执行结果 -4. **命令销毁**:命令执行完毕后可以被垃圾回收 - -**注意事项:** - -- 命令应该是无状态的,执行完即可丢弃 -- 避免在命令中保存长期引用 -- 命令执行应该是原子操作 - -### 与 Store 配合使用 - -当某个 Model 内部使用 `Store` 管理复杂聚合状态时,Command 依然是推荐的写入口。 +发送方式: ```csharp -public sealed class DamagePlayerCommand(int amount) : AbstractCommand -{ - protected override void OnExecute() - { - var model = this.GetModel(); - model.Store.Dispatch(new DamagePlayerAction(amount)); - } -} +this.SendCommand(new DamagePlayerCommand(new DamagePlayerInput(10))); ``` -这样可以保持现有职责边界不变: - -- Controller 发送命令 -- Command 执行操作 -- Model 承载状态 -- Store 负责统一归约状态变化 - -完整示例见 [`state-management`](./state-management)。 - -## CommandBus - 命令总线 - -### 功能说明 - -`CommandBus` 是命令执行的核心组件,负责发送和执行命令。 - -**主要方法:** +## 带返回值命令 ```csharp -void Send(ICommand command); // 发送无返回值命令 -TResult Send(ICommand command); // 发送带返回值命令 +using GFramework.Core.Command; +using GFramework.Core.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs.Command; + +public sealed record GetGoldRewardInput(int EnemyLevel) : ICommandInput; + +public sealed class GetGoldRewardCommand(GetGoldRewardInput input) + : AbstractCommand(input) +{ + protected override int OnExecute(GetGoldRewardInput input) + { + return input.EnemyLevel * 10; + } +} ``` -**特点:** - -- 统一的命令执行入口 -- 支持同步命令执行 -- 与架构上下文集成 - -### 使用示例 - ```csharp -// 通过架构获取命令总线 -var commandBus = architecture.Context.CommandBus; - -// 发送无返回值命令 -commandBus.Send(new StartGameCommand(1, "Player1")); - -// 发送带返回值命令 -var damage = commandBus.Send(new CalculateDamageCommand(100, 50)); +var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3))); ``` -## 命令基类变体 +## 发送入口 -框架提供了多种命令基类以满足不同需求: +旧命令由 `IArchitectureContext` 的兼容入口执行: -### AbstractCommand`` +- `SendCommand(ICommand)` +- `SendCommand(ICommand)` +- `SendCommandAsync(IAsyncCommand)` +- `SendCommandAsync(IAsyncCommand)` -带输入参数的无返回值命令类。通过 `ICommandInput` 接口传递参数。 - -**核心方法:** +在 `IContextAware` 对象内,通常直接通过扩展使用: ```csharp -void ICommand.Execute(); // 实现 ICommand 接口 -protected abstract void OnExecute(TInput input); // 抽象执行方法,接收输入参数 +using GFramework.Core.Extensions; ``` -**使用示例:** +## 什么时候还应该用旧命令 -```csharp -// 定义输入对象 -public class StartGameInput : ICommandInput -{ - public int LevelId { get; set; } - public string PlayerName { get; set; } -} +- 你在维护既有 `Core.Command` 代码 +- 你的调用链已经依赖旧 `CommandExecutor` +- 当前改动目标是局部修复,不值得同时做 CQRS 迁移 -// 定义命令 -public class StartGameCommand : AbstractCommand -{ - protected override void OnExecute(StartGameInput input) - { - var playerModel = this.GetModel(); - var gameModel = this.GetModel(); +## 什么时候该切到 CQRS - playerModel.PlayerName.Value = input.PlayerName; - gameModel.CurrentLevel.Value = input.LevelId; - gameModel.GameState.Value = GameState.Playing; +下面这些场景更适合新 CQRS runtime: - this.SendEvent(new GameStartedEvent()); - } -} +- 需要 request / notification / stream 的统一模型 +- 需要 pipeline behaviors +- 需要 handler registry 生成器 +- 你正在写新的业务模块,而不是维护历史命令代码 -// 使用命令 -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class GameController : IController -{ - public void OnStartButtonClicked() - { - var input = new StartGameInput { LevelId = 1, PlayerName = "Player1" }; - this.SendCommand(new StartGameCommand { Input = input }); - } -} -``` - -### AbstractCommand`` - -既带输入参数又带返回值的命令类。 - -**核心方法:** - -```csharp -TResult ICommand.Execute(); // 实现 ICommand 接口 -protected abstract TResult OnExecute(TInput input); // 抽象执行方法,接收输入参数 -``` - -**使用示例:** - -```csharp -// 定义输入对象 -public class CalculateDamageInput : ICommandInput -{ - public int AttackerAttackPower { get; set; } - public int DefenderDefense { get; set; } -} - -// 定义命令 -public class CalculateDamageCommand : AbstractCommand -{ - protected override int OnExecute(CalculateDamageInput input) - { - var config = this.GetModel(); - var baseDamage = input.AttackerAttackPower - input.DefenderDefense; - var finalDamage = Math.Max(1, baseDamage * config.DamageMultiplier); - return (int)finalDamage; - } -} - -// 使用命令 -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() { } - - public void Attack(Character attacker, Character defender) - { - var input = new CalculateDamageInput - { - AttackerAttackPower = attacker.AttackPower, - DefenderDefense = defender.Defense - }; - - var damage = this.SendCommand(new CalculateDamageCommand { Input = input }); - defender.Health -= damage; - this.SendEvent(new DamageDealtEvent(attacker, defender, damage)); - } -} -``` - -### AbstractAsyncCommand`` - -支持异步执行的带输入参数的无返回值命令基类。 - -**核心方法:** - -```csharp -Task IAsyncCommand.ExecuteAsync(); // 实现异步命令接口 -protected abstract Task OnExecuteAsync(TInput input); // 抽象异步执行方法 -``` - -### AbstractAsyncCommand`` - -支持异步执行的既带输入参数又带返回值的命令基类。 - -**核心方法:** - -```csharp -Task IAsyncCommand.ExecuteAsync(); // 实现异步命令接口 -protected abstract Task OnExecuteAsync(TInput input); // 抽象异步执行方法 -``` - -**使用示例:** - -```csharp -// 定义输入对象 -public class LoadSaveDataInput : ICommandInput -{ - public string SaveSlot { get; set; } -} - -// 定义异步命令 -public class LoadSaveDataCommand : AbstractAsyncCommand -{ - protected override async Task OnExecuteAsync(LoadSaveDataInput input) - { - var storage = this.GetUtility(); - return await storage.LoadSaveDataAsync(input.SaveSlot); - } -} - -// 使用异步命令 -public class SaveSystem : AbstractSystem -{ - protected override void OnInit() - { - this.RegisterEvent(OnLoadGameRequest); - } - - private async void OnLoadGameRequest(LoadGameRequestEvent e) - { - var input = new LoadSaveDataInput { SaveSlot = e.SaveSlot }; - var saveData = await this.SendCommandAsync(new LoadSaveDataCommand { Input = input }); - - if (saveData != null) - { - this.SendEvent(new GameLoadedEvent { SaveData = saveData }); - } - } -} -``` - -## 命令处理器执行 - -所有发送给命令总线的命令最终都会通过 `CommandExecutor` 来执行: - -```csharp -public class CommandExecutor -{ - public static void Execute(ICommand command) - { - command.Execute(); - } - - public static TResult Execute(ICommand command) - { - return command.Execute(); - } -} -``` - -**特点:** - -- 提供统一的命令执行机制 -- 支持同步和异步命令执行 -- 可以扩展添加中间件逻辑 - -## 使用场景 - -### 1. 用户交互操作 - -```csharp -public class SaveGameCommand : AbstractCommand -{ - private readonly string _saveSlot; - - public SaveGameCommand(string saveSlot) - { - _saveSlot = saveSlot; - } - - protected override void OnExecute() - { - var saveSystem = this.GetSystem(); - var playerModel = this.GetModel(); - - saveSystem.SavePlayerData(playerModel, _saveSlot); - this.SendEvent(new GameSavedEvent(_saveSlot)); - } -} -``` - -### 2. 业务流程控制 - -```csharp -public class LoadLevelCommand : AbstractCommand -{ - private readonly int _levelId; - - public LoadLevelCommand(int levelId) - { - _levelId = levelId; - } - - protected override void OnExecute() - { - var levelSystem = this.GetSystem(); - var uiSystem = this.GetSystem(); - - // 显示加载界面 - uiSystem.ShowLoadingScreen(); - - // 加载关卡 - levelSystem.LoadLevel(_levelId); - - // 发送事件 - this.SendEvent(new LevelLoadedEvent(_levelId)); - } -} -``` - -## 最佳实践 - -1. **保持命令原子性**:一个命令应该完成一个完整的业务操作 -2. **命令无状态**:命令不应该保存长期状态,执行完即可丢弃 -3. **参数通过构造函数传递**:命令需要的参数应在创建时传入 -4. **避免命令嵌套**:命令内部尽量不要发送其他命令,使用事件通信 -5. **合理使用返回值**:只在确实需要返回结果时使用带返回值的命令 -6. **命令命名规范**:使用动词+名词形式,如 `StartGameCommand`、`SavePlayerCommand` -7. **单一职责原则**:每个命令只负责一个特定的业务操作 -8. **使用异步命令**:对于需要长时间执行的操作,使用异步命令避免阻塞 -9. **命令验证**:在命令执行前验证输入参数的有效性 -10. **错误处理**:在命令中适当处理异常情况 - -## 命令模式优势 - -### 1. 可扩展性 - -- 命令可以被序列化和存储 -- 支持命令队列和批处理 -- 便于实现撤销/重做功能 - -### 2. 可测试性 - -- 命令逻辑独立,易于单元测试 -- 可以模拟命令执行结果 -- 支持行为驱动开发 - -### 3. 可维护性 - -- 业务逻辑集中管理 -- 降低组件间耦合度 -- 便于重构和扩展 - -## 相关包 - -- [`architecture`](./architecture.md) - 架构核心,负责命令的分发和执行 -- [`extensions`](./extensions.md) - 提供 `SendCommand()` 扩展方法 -- [`query`](./query.md) - 查询模式,用于数据查询 -- [`events`](./events.md) - 事件系统,命令执行后的通知机制 -- [`system`](./system.md) - 业务系统,命令的主要执行者 -- [`model`](./model.md) - 数据模型,命令操作的数据 - ---- - -**许可证**:Apache 2.0 +迁移后常见写法见:[cqrs](./cqrs.md) diff --git a/docs/zh-CN/core/context.md b/docs/zh-CN/core/context.md index 8866f121..f905e2be 100644 --- a/docs/zh-CN/core/context.md +++ b/docs/zh-CN/core/context.md @@ -1,490 +1,163 @@ -# Context 上下文指南 +# Context -## 概述 +`IArchitectureContext` 是框架的统一上下文入口。 -Context(上下文)是 GFramework 中的核心概念,提供了对架构服务的统一访问入口。通过 Context,组件可以访问事件总线、命令总线、查询总线、IoC -容器等核心服务。 +当前版本的上下文不再以“公开属性总线”作为主要模型,而是以一组明确的方法同时承载: -## 核心接口 +- 组件获取 +- 事件系统 +- 旧 Command / Query 兼容入口 +- 新 CQRS runtime 入口 -### IArchitectureContext +默认实现类型是 `ArchitectureContext`。 -架构上下文接口,定义了对架构服务的访问契约。 +## 先记住一个事实 -**核心属性:** +如果你还在找旧文档里的这些属性: + +- `CommandBus` +- `QueryBus` +- `EventBus` +- `Container` + +那说明你看到的是旧写法。当前推荐入口是方法,不是这些属性式总线。 + +## 组件访问 + +`IArchitectureContext` 直接提供按类型获取组件的方法: ```csharp -IEventBus EventBus { get; } // 事件总线 -ICommandBus CommandBus { get; } // 命令总线 -IQueryBus QueryBus { get; } // 查询总线 -IIocContainer Container { get; } // IoC 容器 -IEnvironment Environment { get; } // 环境配置 -IArchitectureConfiguration Configuration { get; } // 架构配置 -ILogger Logger { get; } // 日志系统 -``` - -## 核心类 - -### ArchitectureContext - -架构上下文的完整实现。 - -**使用示例:** - -```csharp -// 通过架构获取上下文 var context = architecture.Context; -// 访问各个服务 -var eventBus = context.EventBus; -var commandBus = context.CommandBus; -var queryBus = context.QueryBus; -var container = context.Container; -var environment = context.Environment; -var logger = context.Logger; +var model = context.GetModel(); +var system = context.GetSystem(); +var utility = context.GetUtility(); +var service = context.GetService(); ``` -### GameContext +也支持批量获取和按优先级获取: -游戏上下文类,管理架构类型与上下文实例的映射关系。 +- `GetModels()` +- `GetSystems()` +- `GetUtilities()` +- `GetServices()` +- `GetModelsByPriority()` +- `GetSystemsByPriority()` +- `GetUtilitiesByPriority()` +- `GetServicesByPriority()` -**核心方法:** +## 在 `IContextAware` 对象里怎么用 + +大多数业务代码不会手动把 `architecture.Context` 传来传去,而是通过 `IContextAware` 扩展方法访问上下文: ```csharp -// 绑定架构类型到上下文 -static void Bind(IArchitectureContext context) - where TArchitecture : IArchitecture; +using GFramework.Core.Extensions; -// 获取架构类型对应的上下文 -static IArchitectureContext GetContext() - where TArchitecture : IArchitecture; - -// 解绑架构类型 -static void Unbind() - where TArchitecture : IArchitecture; -``` - -## 在组件中使用 Context - -### 在 Model 中使用 - -```csharp -public class PlayerModel : AbstractModel -{ - public BindableProperty Health { get; } = new(100); - - protected override void OnInit() - { - // 通过 Context 访问事件总线 - var context = this.GetContext(); - var eventBus = context.EventBus; - - // 监听生命值变化 - Health.Register(hp => - { - if (hp <= 0) - { - // 发送事件 - eventBus.Send(new PlayerDiedEvent()); - } - }); - } -} -``` - -### 在 System 中使用 - -```csharp -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - // 通过 Context 访问各个服务 - var context = this.GetContext(); - var eventBus = context.EventBus; - var commandBus = context.CommandBus; - var container = context.Container; - - // 注册事件监听 - eventBus.Register(OnEnemyAttack); - } - - private void OnEnemyAttack(EnemyAttackEvent e) - { - var context = this.GetContext(); - var playerModel = context.Container.Get(); - - // 处理伤害 - playerModel.Health.Value -= e.Damage; - } -} -``` - -### 在 Command 中使用 - -```csharp -public class StartGameCommand : AbstractCommand +public sealed class DamagePlayerCommand : AbstractCommand { protected override void OnExecute() { - // 通过 Context 访问服务 - var context = this.GetContext(); - var container = context.Container; - var eventBus = context.EventBus; - - var playerModel = container.Get(); - playerModel.Health.Value = playerModel.MaxHealth.Value; - - eventBus.Send(new GameStartedEvent()); + var playerModel = this.GetModel(); + playerModel.Health.Value -= 10; } } ``` -### 在 Query 中使用 +常用扩展包括: + +- `GetModel()` +- `GetSystem()` +- `GetUtility()` +- `GetService()` +- `SendEvent(...)` +- `RegisterEvent(...)` +- `SendCommand(...)` +- `SendQuery(...)` + +## 事件入口 + +框架事件系统仍然由上下文统一暴露: ```csharp -public class GetPlayerHealthQuery : AbstractQuery +context.SendEvent(new PlayerDiedEvent()); + +var unRegister = context.RegisterEvent(static e => { - protected override int OnDo() - { - // 通过 Context 访问容器 - var context = this.GetContext(); - var playerModel = context.Container.Get(); - - return playerModel.Health.Value; - } -} -``` - -## GameContext 的使用 - -### 绑定架构到 GameContext - -```csharp -public class GameArchitecture : Architecture -{ - protected override void Init() - { - // 注册组件 - RegisterModel(new PlayerModel()); - RegisterSystem(new CombatSystem()); - } -} - -// 在应用启动时绑定 -var architecture = new GameArchitecture(); -await architecture.InitializeAsync(); - -// 绑定架构到 GameContext -GameContext.Bind(architecture.Context); -``` - -### 从 GameContext 获取上下文 - -```csharp -// 在任何地方获取架构上下文 -var context = GameContext.GetContext(); - -// 访问服务 -var playerModel = context.Container.Get(); -var eventBus = context.EventBus; -``` - -### 使用 GameContext 的扩展方法 - -```csharp -// 通过扩展方法简化访问 -public static class GameContextExtensions -{ - public static T GetModel(this IArchitectureContext context) - where T : class, IModel - { - return context.Container.Get(); - } - - public static T GetSystem(this IArchitectureContext context) - where T : class, ISystem - { - return context.Container.Get(); - } -} - -// 使用 -var context = GameContext.GetContext(); -var playerModel = context.GetModel(); -var combatSystem = context.GetSystem(); -``` - -## Context 中的服务 - -### EventBus - 事件总线 - -```csharp -var context = architecture.Context; -var eventBus = context.EventBus; - -// 注册事件 -eventBus.Register(e => -{ - Console.WriteLine("Player died!"); + Console.WriteLine("Player died."); }); - -// 发送事件 -eventBus.Send(new PlayerDiedEvent()); ``` -### CommandBus - 命令总线 +在 `IContextAware` 对象里也可以直接用扩展: ```csharp -var context = architecture.Context; -var commandBus = context.CommandBus; - -// 发送命令 -commandBus.Send(new StartGameCommand()); - -// 发送带返回值的命令 -var damage = commandBus.Send(new CalculateDamageCommand { Input = input }); +this.SendEvent(new PlayerDiedEvent()); ``` -### QueryBus - 查询总线 +## 旧 Command / Query 兼容入口 + +当前上下文仍保留旧命令 / 查询体系: + +- `SendCommand(ICommand)` +- `SendCommand(ICommand)` +- `SendCommandAsync(IAsyncCommand)` +- `SendCommandAsync(IAsyncCommand)` +- `SendQuery(IQuery)` +- `SendQueryAsync(IAsyncQuery)` + +这部分入口主要用于兼容存量代码。新功能优先看 [cqrs](./cqrs.md)。 + +## 新 CQRS 入口 + +`IArchitectureContext` 也是当前 CQRS runtime 的主入口。最重要的方法是: + +- `SendRequestAsync(...)` +- `SendRequest(...)` +- `SendAsync(...)` +- `PublishAsync(...)` +- `CreateStream(...)` +- `SendCommandAsync(...)` / `SendQueryAsync(...)` 的 CQRS 重载 + +示例: ```csharp -var context = architecture.Context; -var queryBus = context.QueryBus; - -// 发送查询 -var health = queryBus.Send(new GetPlayerHealthQuery { Input = new EmptyQueryInput() }); +var playerId = await architecture.Context.SendRequestAsync( + new CreatePlayerCommand(new CreatePlayerInput("Alice"))); ``` -### Container - IoC 容器 +如果你在 `IContextAware` 对象内部,通常直接用 `GFramework.Cqrs.Extensions` 里的扩展: ```csharp -var context = architecture.Context; -var container = context.Container; +using GFramework.Cqrs.Extensions; -// 获取已注册的组件 -var playerModel = container.Get(); -var combatSystem = container.Get(); - -// 获取所有实现某接口的组件 -var allSystems = container.GetAll(); +var playerId = await this.SendAsync( + new CreatePlayerCommand(new CreatePlayerInput("Alice"))); ``` -### Environment - 环境配置 +## `GameContext` 现在是什么角色 -```csharp -var context = architecture.Context; -var environment = context.Environment; +`GameContext` 仍然存在,但已经退到兼容和回退路径。 -// 获取环境值 -var gameMode = environment.Get("GameMode"); -var maxPlayers = environment.Get("MaxPlayers"); +`ContextAwareBase` 在实例未显式注入上下文时,会回退到 `GameContext.GetFirstArchitectureContext()`。这能保证部分旧代码继续工作,但它不是新代码的首选接法。 -// 安全获取值 -if (environment.TryGet("ServerAddress", out var address)) -{ - Console.WriteLine($"Server: {address}"); -} -``` +新代码更推荐: -### Logger - 日志系统 +- 让对象通过框架流程注入 `IArchitectureContext` +- 或使用 `[ContextAware]` 生成路径 +- 或显式从 `architecture.Context` 启动调用链 -```csharp -var context = architecture.Context; -var logger = context.Logger; +## 什么时候需要手动拿 `architecture.Context` -// 记录日志 -logger.Log("Game started"); -logger.LogWarning("Low memory"); -logger.LogError("Failed to load resource"); -``` +以下场景适合直接使用 `architecture.Context`: -## Context 的生命周期 +- 组合根或启动代码 +- 非 `IContextAware` 对象 +- 测试中显式驱动请求和事件 +- 你要清楚地区分“旧 Command / Query 兼容入口”和“新 CQRS 入口” -### 创建 +## 继续阅读 -Context 在架构初始化时自动创建: - -```csharp -var architecture = new GameArchitecture(); -// Context 在这里被创建 -var context = architecture.Context; -``` - -### 使用 - -Context 在架构的整个生命周期中可用: - -```csharp -// 初始化期间 -await architecture.InitializeAsync(); - -// Ready 阶段 -var context = architecture.Context; -var playerModel = context.Container.Get(); - -// 销毁前 -architecture.Destroy(); -``` - -### 销毁 - -Context 随着架构的销毁而销毁: - -```csharp -architecture.Destroy(); -// Context 不再可用 -``` - -## 最佳实践 - -### 1. 通过扩展方法简化访问 - -```csharp -public static class ContextExtensions -{ - public static T GetModel(this IArchitectureContext context) - where T : class, IModel - { - return context.Container.Get(); - } - - public static T GetSystem(this IArchitectureContext context) - where T : class, ISystem - { - return context.Container.Get(); - } - - public static void SendCommand(this IArchitectureContext context, ICommand command) - { - context.CommandBus.Send(command); - } - - public static TResult SendQuery(this IArchitectureContext context, IQuery query) - { - return context.QueryBus.Send(query); - } -} - -// 使用 -var context = architecture.Context; -var playerModel = context.GetModel(); -context.SendCommand(new StartGameCommand()); -``` - -### 2. 缓存 Context 引用 - -```csharp -public class GameSystem : AbstractSystem -{ - private IArchitectureContext _context; - - protected override void OnInit() - { - // 缓存 Context 引用 - _context = this.GetContext(); - - // 后续使用缓存的引用 - _context.EventBus.Register(OnGameStarted); - } - - private void OnGameStarted(GameStartedEvent e) - { - var playerModel = _context.Container.Get(); - } -} -``` - -### 3. 使用 GameContext 实现全局访问 - -```csharp -// 在应用启动时绑定 -public class GameBootstrapper -{ - public async Task StartAsync() - { - var architecture = new GameArchitecture(); - await architecture.InitializeAsync(); - - // 绑定到 GameContext - GameContext.Bind(architecture.Context); - } -} - -// 在任何地方访问 -public class UIController -{ - public void UpdateHealthDisplay() - { - var context = GameContext.GetContext(); - var playerModel = context.Container.Get(); - - // 更新 UI - healthText.text = playerModel.Health.Value.ToString(); - } -} -``` - -### 4. 处理 Context 不可用的情况 - -```csharp -public class SafeGameSystem : AbstractSystem -{ - protected override void OnInit() - { - try - { - var context = this.GetContext(); - if (context == null) - { - Console.WriteLine("Context not available"); - return; - } - - var playerModel = context.Container.Get(); - } - catch (Exception ex) - { - Console.WriteLine($"Error accessing context: {ex.Message}"); - } - } -} -``` - -## Context vs Architecture - -### Architecture - -- **职责**:管理组件的生命周期 -- **作用**:注册、初始化、销毁组件 -- **访问**:通过 `GetArchitecture()` 获取 - -### Context - -- **职责**:提供对架构服务的访问 -- **作用**:访问事件总线、命令总线、查询总线等 -- **访问**:通过 `GetContext()` 获取 - -```csharp -// Architecture 用于管理 -var architecture = GameArchitecture.Interface; -architecture.RegisterModel(new PlayerModel()); - -// Context 用于访问服务 -var context = architecture.Context; -var playerModel = context.Container.Get(); -``` - -## 相关包 - -- [`architecture`](./architecture.md) - 架构核心,创建和管理 Context -- [`ioc`](./ioc.md) - IoC 容器,通过 Context 访问 -- [`events`](./events.md) - 事件总线,通过 Context 访问 -- [`command`](./command.md) - 命令总线,通过 Context 访问 -- [`query`](./query.md) - 查询总线,通过 Context 访问 -- [`environment`](./environment.md) - 环境配置,通过 Context 访问 -- [`logging`](./logging.md) - 日志系统,通过 Context 访问 - ---- - -**许可证**:Apache 2.0 +- 架构入口:[architecture](./architecture.md) +- 生命周期:[lifecycle](./lifecycle.md) +- 旧命令系统:[command](./command.md) +- 旧查询系统:[query](./query.md) +- 新 CQRS runtime:[cqrs](./cqrs.md) diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 0c248a96..8d9158ec 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -1,656 +1,171 @@ --- title: CQRS -description: GFramework 内建 CQRS runtime,用统一请求分发、通知发布和流式处理组织业务逻辑。 +description: 当前推荐的新请求模型,统一覆盖 command、query、notification、stream request 和 pipeline behaviors。 --- # CQRS -## 概述 +`GFramework.Cqrs` 是当前推荐的新请求模型 runtime。 -CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式,将数据的读取(Query)和修改(Command)操作分离。GFramework -当前内建自有 CQRS runtime,通过统一的请求分发器、通知发布和流式请求管道提供类型安全、解耦的业务逻辑处理方式。 +如果你在写新功能,优先使用这套模型,而不是继续扩展 `GFramework.Core.Command` / `Query` 的兼容层。 -通过 CQRS,你可以将复杂的业务逻辑拆分为独立的命令和查询处理器,每个处理器只负责单一职责,使代码更易于测试和维护。 - -**主要特性**: - -- 命令查询职责分离 -- 内建请求分发与解耦设计 -- 支持管道行为(Behaviors) -- 异步处理支持 -- 与架构系统深度集成 -- 支持流式处理 - -## 接入包 - -按模块安装 CQRS runtime;如果希望在编译期生成 handler 注册表,再额外安装对应的 source generator: +## 安装方式 ```bash dotnet add package GeWuYou.GFramework.Cqrs dotnet add package GeWuYou.GFramework.Cqrs.Abstractions +``` -# 可选:编译期生成 handler registry,减少冷启动反射扫描 +如果你希望消费端程序集在编译期生成 handler registry,再额外安装: + +```bash dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators ``` -## 核心概念 +## 先理解分层 -### Command(命令) +- `GFramework.Cqrs.Abstractions` + - 纯契约层,定义请求、处理器、行为等接口 +- `GFramework.Cqrs` + - 默认 runtime、dispatcher、处理器基类和上下文扩展 +- `GFramework.Cqrs.SourceGenerators` + - 可选生成器,为消费端程序集生成 `ICqrsHandlerRegistry` -命令表示修改系统状态的操作,如创建、更新、删除: +## 最小示例 -```csharp -using GFramework.Cqrs.Command; -using GFramework.Cqrs.Abstractions.Cqrs.Command; +消息基类和处理器基类在不同命名空间: -// 定义命令输入 -public class CreatePlayerInput : ICommandInput -{ - public string Name { get; set; } - public int Level { get; set; } -} +- 消息基类:`GFramework.Cqrs.Command` / `Query` / `Notification` +- 处理器基类:`GFramework.Cqrs.Cqrs.Command` / `Query` / `Notification` -// 定义命令 -public class CreatePlayerCommand : CommandBase -{ - public CreatePlayerCommand(CreatePlayerInput input) : base(input) { } -} -``` - -### Query(查询) - -查询表示读取系统状态的操作,不修改数据: - -```csharp -using GFramework.Cqrs.Query; -using GFramework.Cqrs.Abstractions.Cqrs.Query; - -// 定义查询输入 -public class GetPlayerInput : IQueryInput -{ - public int PlayerId { get; set; } -} - -// 定义查询 -public class GetPlayerQuery : QueryBase -{ - public GetPlayerQuery(GetPlayerInput input) : base(input) { } -} -``` - -### Handler(处理器) - -处理器负责执行命令或查询的具体逻辑: +示例: ```csharp using GFramework.Cqrs.Command; using GFramework.Cqrs.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Command; -// 命令处理器 -public class CreatePlayerCommandHandler : AbstractCommandHandler +public sealed record CreatePlayerInput(string Name) : ICommandInput; + +public sealed class CreatePlayerCommand(CreatePlayerInput input) + : CommandBase(input) { - public override async ValueTask Handle( +} + +public sealed class CreatePlayerCommandHandler + : AbstractCommandHandler +{ + public override ValueTask Handle( CreatePlayerCommand command, CancellationToken cancellationToken) { - var input = command.Input; - var playerModel = this.GetModel(); - - // 创建玩家 - var playerId = playerModel.CreatePlayer(input.Name, input.Level); - - return playerId; + var playerModel = Context.GetModel(); + var playerId = playerModel.Create(command.Input.Name); + return ValueTask.FromResult(playerId); } } ``` -> 说明:消息基类位于 `GFramework.Cqrs.Command` / `Query` / `Notification` 命名空间,而处理器基类位于 -> `GFramework.Cqrs.Cqrs.*` 命名空间。编写最小示例时需要同时引用对应的消息与 handler 命名空间。 +## 发送请求 -### Dispatcher(请求分发器) - -架构上下文会负责将命令、查询和通知路由到对应的处理器: +如果你在 `IContextAware` 对象内部: ```csharp -// 通过架构上下文发送命令 -var command = new CreatePlayerCommand(new CreatePlayerInput -{ - Name = "Player1", - Level = 1 -}); +using GFramework.Cqrs.Extensions; -var playerId = await this.SendAsync(command); +var playerId = await this.SendAsync( + new CreatePlayerCommand(new CreatePlayerInput("Alice"))); ``` -## 基本用法 - -### 定义和发送命令 +如果你在组合根或测试里: ```csharp -// 1. 定义命令输入 -public class SaveGameInput : ICommandInput +var playerId = await architecture.Context.SendRequestAsync( + new CreatePlayerCommand(new CreatePlayerInput("Alice"))); +``` + +最常用的上下文入口有: + +- `SendRequestAsync(...)` +- `SendAsync(...)` +- `SendQueryAsync(...)` +- `PublishAsync(...)` +- `CreateStream(...)` + +## 查询、通知和流 + +这套 runtime 不只处理 command,也统一处理: + +- Query + - 读路径请求 +- Notification + - 一对多广播 +- Stream Request + - 返回 `IAsyncEnumerable` + +也就是说,新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。 + +## 注册处理器 + +在标准 `Architecture` 启动路径中,CQRS runtime 会自动接入基础设施。你通常只需要在 `OnInitialize()` 里追加行为或额外程序集: + +```csharp +protected override void OnInitialize() { - public int SlotId { get; set; } - public GameData Data { get; set; } -} + RegisterCqrsPipelineBehavior>(); + RegisterCqrsPipelineBehavior>(); -// 2. 定义命令 -public class SaveGameCommand : CommandBase -{ - public SaveGameCommand(SaveGameInput input) : base(input) { } -} - -// 3. 实现命令处理器 -public class SaveGameCommandHandler : AbstractCommandHandler -{ - public override async ValueTask Handle( - SaveGameCommand command, - CancellationToken cancellationToken) - { - var input = command.Input; - var saveSystem = this.GetSystem(); - - // 保存游戏 - await saveSystem.SaveAsync(input.SlotId, input.Data); - - // 发送事件 - this.SendEvent(new GameSavedEvent { SlotId = input.SlotId }); - - return Unit.Value; - } -} - -// 4. 发送命令 -public async Task SaveGame() -{ - var command = new SaveGameCommand(new SaveGameInput - { - SlotId = 1, - Data = currentGameData - }); - - await this.SendAsync(command); + RegisterCqrsHandlersFromAssemblies( + [ + typeof(InventoryCqrsMarker).Assembly, + typeof(BattleCqrsMarker).Assembly + ]); } ``` -### 定义和发送查询 +默认逻辑会: + +1. 优先使用消费端程序集上的生成注册器 +2. 生成注册器不可用时回退到反射扫描 +3. 对同一程序集去重,避免重复注册 + +## Pipeline Behavior + +如果你需要围绕请求处理流程插入横切逻辑,使用: ```csharp -// 1. 定义查询输入 -public class GetHighScoresInput : IQueryInput -{ - public int Count { get; set; } = 10; -} - -// 2. 定义查询 -public class GetHighScoresQuery : QueryBase> -{ - public GetHighScoresQuery(GetHighScoresInput input) : base(input) { } -} - -// 3. 实现查询处理器 -public class GetHighScoresQueryHandler : AbstractQueryHandler> -{ - public override async ValueTask> Handle( - GetHighScoresQuery query, - CancellationToken cancellationToken) - { - var input = query.Input; - var scoreModel = this.GetModel(); - - // 查询高分榜 - var scores = await scoreModel.GetTopScoresAsync(input.Count); - - return scores; - } -} - -// 4. 发送查询 -public async Task> GetHighScores() -{ - var query = new GetHighScoresQuery(new GetHighScoresInput - { - Count = 10 - }); - - var scores = await this.SendQueryAsync(query); - return scores; -} -``` - -### 注册处理器 - -在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: - -```csharp -public class GameArchitecture : Architecture -{ - protected override void OnInitialize() - { - // 注册通用开放泛型行为 - RegisterCqrsPipelineBehavior>(); - RegisterCqrsPipelineBehavior>(); - - // 默认只自动扫描当前架构程序集和 GFramework.Core 程序集中的处理器 - } -} -``` - -当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器; -如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。 -`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。 - -如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围: - -```csharp -public class GameArchitecture : Architecture -{ - protected override void OnInitialize() - { - RegisterCqrsPipelineBehavior>(); - - RegisterCqrsHandlersFromAssemblies( - [ - typeof(InventoryCqrsMarker).Assembly, - typeof(BattleCqrsMarker).Assembly - ]); - } -} -``` - -`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 会复用与默认启动路径相同的注册逻辑: -优先使用程序集级生成注册器,失败时自动回退到反射扫描;如果同一程序集已经由默认路径或其他模块接入,框架会自动去重,避免重复注册 -handler。 - -`RegisterCqrsPipelineBehavior()` 是唯一保留的公开入口;旧的 `Mediator` 兼容别名与扩展已移除,不再继续维护。 -如果你正在从旧版本迁移,只需要直接改用 `RegisterCqrsPipelineBehavior()`; -旧 `RegisterMediatorBehavior()` 已移除,不再保留兼容入口。 -当前接口支持两种形式: - -- 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求 -- 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior` - -## 高级用法 - -### Request(请求) - -Request 是更通用的消息类型,可以用于任何场景: - -```csharp -using GFramework.Cqrs.Request; -using GFramework.Cqrs.Abstractions.Cqrs.Request; - -// 定义请求输入 -public class ValidatePlayerInput : IRequestInput -{ - public string PlayerName { get; set; } -} - -// 定义请求 -public class ValidatePlayerRequest : RequestBase -{ - public ValidatePlayerRequest(ValidatePlayerInput input) : base(input) { } -} - -// 实现请求处理器 -public class ValidatePlayerRequestHandler : AbstractRequestHandler -{ - public override async ValueTask Handle( - ValidatePlayerRequest request, - CancellationToken cancellationToken) - { - var input = request.Input; - var playerModel = this.GetModel(); - - // 验证玩家名称 - var isValid = await playerModel.IsNameValidAsync(input.PlayerName); - - return isValid; - } -} -``` - -### Notification(通知) - -Notification 用于一对多的消息广播: - -```csharp -using GFramework.Cqrs.Notification; -using GFramework.Cqrs.Abstractions.Cqrs.Notification; - -// 定义通知输入 -public class PlayerLevelUpInput : INotificationInput -{ - public int PlayerId { get; set; } - public int NewLevel { get; set; } -} - -// 定义通知 -public class PlayerLevelUpNotification : NotificationBase -{ - public PlayerLevelUpNotification(PlayerLevelUpInput input) : base(input) { } -} - -// 实现通知处理器 1 -public class AchievementNotificationHandler : AbstractNotificationHandler -{ - public override async ValueTask Handle( - PlayerLevelUpNotification notification, - CancellationToken cancellationToken) - { - var input = notification.Input; - // 检查成就 - CheckLevelAchievements(input.PlayerId, input.NewLevel); - await Task.CompletedTask; - } -} - -// 实现通知处理器 2 -public class RewardNotificationHandler : AbstractNotificationHandler -{ - public override async ValueTask Handle( - PlayerLevelUpNotification notification, - CancellationToken cancellationToken) - { - var input = notification.Input; - // 发放奖励 - GiveRewards(input.PlayerId, input.NewLevel); - await Task.CompletedTask; - } -} - -// 发布通知(所有处理器都会收到) -var notification = new PlayerLevelUpNotification(new PlayerLevelUpInput -{ - PlayerId = 1, - NewLevel = 10 -}); - -await this.PublishAsync(notification); -``` - -### Pipeline Behaviors(管道行为) - -Behaviors 可以在处理器执行前后添加横切关注点: - -```csharp -using GFramework.Core.Abstractions.Cqrs; - -// 日志行为 -public class LoggingBehavior : IPipelineBehavior - where TMessage : IRequest -{ - public async ValueTask Handle( - TMessage message, - MessageHandlerDelegate next, - CancellationToken cancellationToken) - { - var messageName = message.GetType().Name; - Console.WriteLine($"[开始] {messageName}"); - - var response = await next(message, cancellationToken); - - Console.WriteLine($"[完成] {messageName}"); - - return response; - } -} - -// 性能监控行为 -public class PerformanceBehavior : IPipelineBehavior - where TMessage : IRequest -{ - public async ValueTask Handle( - TMessage message, - MessageHandlerDelegate next, - CancellationToken cancellationToken) - { - var stopwatch = Stopwatch.StartNew(); - - var response = await next(message, cancellationToken); - - stopwatch.Stop(); - var elapsed = stopwatch.ElapsedMilliseconds; - - if (elapsed > 100) - { - Console.WriteLine($"警告: {message.GetType().Name} 耗时 {elapsed}ms"); - } - - return response; - } -} - -// 注册行为 RegisterCqrsPipelineBehavior>(); -RegisterCqrsPipelineBehavior>(); ``` -### 验证行为 +适合的场景包括: -```csharp -public class ValidationBehavior : IPipelineBehavior - where TMessage : IRequest -{ - public async ValueTask Handle( - TMessage message, - MessageHandlerDelegate next, - CancellationToken cancellationToken) - { - // 验证输入 - if (message is IValidatable validatable) - { - var errors = validatable.Validate(); - if (errors.Any()) - { - throw new ValidationException(errors); - } - } +- 日志 +- 性能统计 +- 校验 +- 审计 +- 重试或统一异常封装 - return await next(message, cancellationToken); - } -} -``` +旧的 `Mediator` 兼容别名入口已经移除;当前公开入口只有 `RegisterCqrsPipelineBehavior()`。 -### 流式处理 +## 和旧 Command / Query 的关系 -处理大量数据时使用流式处理: +当前仓库同时存在两套路径: -```csharp -// 流式查询 -public class GetAllPlayersStreamQuery : QueryBase> -{ - public GetAllPlayersStreamQuery() : base(new EmptyInput()) { } -} +- 旧路径 + - `GFramework.Core.Command` + - `GFramework.Core.Query` +- 新路径 + - `GFramework.Cqrs` -// 流式查询处理器 -public class GetAllPlayersStreamQueryHandler : AbstractStreamQueryHandler -{ - public override async IAsyncEnumerable Handle( - GetAllPlayersStreamQuery query, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var playerModel = this.GetModel(); +`IArchitectureContext` 仍然会兼容旧入口,但新代码应优先使用 CQRS runtime。 - await foreach (var player in playerModel.GetAllPlayersAsync(cancellationToken)) - { - yield return player; - } - } -} +一个简单判断规则: -// 使用流式查询 -var query = new GetAllPlayersStreamQuery(); -var stream = this.CreateStream(query); +- 在维护历史代码:允许继续使用旧 Command / Query +- 在写新功能或新模块:优先使用 CQRS -await foreach (var player in stream) -{ - Console.WriteLine($"玩家: {player.Name}"); -} -``` +## 继续阅读 -## 最佳实践 - -1. **命令和查询分离**:严格区分修改和读取操作 - ```csharp - ✓ CreatePlayerCommand, GetPlayerQuery // 职责清晰 - ✗ PlayerCommand // 职责不明确 - ``` - -2. **使用有意义的命名**:命令用动词,查询用 Get - ```csharp - ✓ CreatePlayerCommand, UpdateScoreCommand, GetHighScoresQuery - ✗ PlayerCommand, ScoreCommand, ScoresQuery - ``` - -3. **输入验证**:在处理器中验证输入 - ```csharp - public override async ValueTask Handle(...) - { - if (string.IsNullOrEmpty(command.Input.Name)) - throw new ArgumentException("Name is required"); - - // 处理逻辑 - } - ``` - -4. **使用 Behaviors 处理横切关注点**:日志、性能、验证等 - ```csharp - RegisterCqrsPipelineBehavior>(); - RegisterCqrsPipelineBehavior>(); - ``` - -5. **保持处理器简单**:一个处理器只做一件事 - ```csharp - ✓ 处理器只负责业务逻辑,通过架构组件访问数据 - ✗ 处理器中包含复杂的数据访问和业务逻辑 - ``` - -6. **使用 CancellationToken**:支持操作取消 - ```csharp - public override async ValueTask Handle(..., CancellationToken cancellationToken) - { - await someAsyncOperation(cancellationToken); - } - ``` - -## 常见问题 - -### 问题:Command 和 Query 有什么区别? - -**解答**: - -- **Command**:修改系统状态,可能有副作用,通常返回 void 或简单结果 -- **Query**:只读取数据,无副作用,返回查询结果 - -```csharp -// Command: 修改状态 -CreatePlayerCommand -> 创建玩家 -UpdateScoreCommand -> 更新分数 - -// Query: 读取数据 -GetPlayerQuery -> 获取玩家信息 -GetHighScoresQuery -> 获取高分榜 -``` - -### 问题:什么时候使用 Request? - -**解答**: -Request 是更通用的消息类型,当操作既不是纯命令也不是纯查询时使用: - -```csharp -// 验证操作:读取数据并返回结果,但不修改状态 -ValidatePlayerRequest - -// 计算操作:基于输入计算结果 -CalculateDamageRequest -``` - -### 问题:Notification 和 Event 有什么区别? - -**解答**: - -- **Notification**:通过内建 CQRS runtime 发送,处理器在同一请求上下文中执行 -- **Event**:通过 EventBus 发送,监听器异步执行 - -```csharp -// Notification: 同步处理 -await this.PublishAsync(notification); // 等待所有处理器完成 - -// Event: 异步处理 -this.SendEvent(event); // 立即返回,监听器异步执行 -``` - -### 问题:如何处理命令失败? - -**解答**: -使用异常或返回 Result 类型: - -```csharp -// 方式 1: 抛出异常 -public override async ValueTask Handle(...) -{ - if (!IsValid()) - throw new InvalidOperationException("Invalid operation"); - - return Unit.Value; -} - -// 方式 2: 返回 Result -public override async ValueTask Handle(...) -{ - if (!IsValid()) - return Result.Failure("Invalid operation"); - - return Result.Success(); -} -``` - -### 问题:处理器可以调用其他处理器吗? - -**解答**: -可以,通过架构上下文继续发送新的命令或查询: - -```csharp -public override async ValueTask Handle(...) -{ - // 调用其他命令 - await this.SendAsync(new AnotherCommand(...)); - - return Unit.Value; -} -``` - -### 问题:如何测试处理器? - -**解答**: -处理器是独立的类,易于单元测试: - -```csharp -[Test] -public async Task CreatePlayer_ShouldReturnPlayerId() -{ - // Arrange - var handler = new CreatePlayerCommandHandler(); - handler.SetContext(mockContext); - - var command = new CreatePlayerCommand(new CreatePlayerInput - { - Name = "Test", - Level = 1 - }); - - // Act - var playerId = await handler.Handle(command, CancellationToken.None); - - // Assert - Assert.That(playerId, Is.GreaterThan(0)); -} -``` - -## 相关文档 - -- [命令系统](/zh-CN/core/command) - 传统命令模式 -- [查询系统](/zh-CN/core/query) - 传统查询模式 -- [事件系统](/zh-CN/core/events) - 事件驱动架构 -- [协程系统](/zh-CN/core/coroutine) - 在协程中使用 CQRS +- 架构入口:[architecture](./architecture.md) +- 上下文入口:[context](./context.md) +- 模块 README:`GFramework.Cqrs/README.md` diff --git a/docs/zh-CN/core/lifecycle.md b/docs/zh-CN/core/lifecycle.md index 7e13a204..a546070d 100644 --- a/docs/zh-CN/core/lifecycle.md +++ b/docs/zh-CN/core/lifecycle.md @@ -1,461 +1,166 @@ --- title: 生命周期管理 -description: 生命周期管理提供了标准化的组件初始化和销毁机制,确保资源的正确管理和释放。 +description: 当前版本的架构生命周期由阶段模型、初始化顺序、逆序销毁和生命周期钩子共同组成。 --- # 生命周期管理 -## 概述 +`GFramework.Core` 的生命周期由 `Architecture` 统一编排,而不是让每个组件各自决定初始化时机。 -生命周期管理是 GFramework 中用于管理组件初始化和销毁的核心机制。通过实现标准的生命周期接口,组件可以在适当的时机执行初始化逻辑和资源清理,确保系统的稳定性和资源的有效管理。 +你真正需要关注的是: -GFramework 提供了同步和异步两套生命周期接口,适用于不同的使用场景。架构会自动管理所有注册组件的生命周期,开发者只需实现相应的接口即可。 +- 阶段枚举 `ArchitecturePhase` +- 组件初始化顺序 +- 逆序销毁语义 +- `IArchitectureLifecycleHook` -**主要特性**: +## 阶段模型 -- 标准化的初始化和销毁流程 -- 支持同步和异步操作 -- 自动生命周期管理 -- 按注册顺序初始化,按逆序销毁 -- 与架构系统深度集成 +当前公开阶段如下: -## 核心概念 +| 阶段 | 含义 | +| --- | --- | +| `None` | 尚未开始初始化 | +| `BeforeUtilityInit` | 即将初始化工具 | +| `AfterUtilityInit` | 工具初始化完成 | +| `BeforeModelInit` | 即将初始化模型 | +| `AfterModelInit` | 模型初始化完成 | +| `BeforeSystemInit` | 即将初始化系统 | +| `AfterSystemInit` | 系统初始化完成 | +| `Ready` | 架构已完成初始化并可供稳定使用 | +| `Destroying` | 正在销毁 | +| `Destroyed` | 已销毁 | +| `FailedInitialization` | 初始化流程失败 | -### 生命周期接口层次 +正常路径: -GFramework 提供了一套完整的生命周期接口: +```text +None +-> BeforeUtilityInit +-> AfterUtilityInit +-> BeforeModelInit +-> AfterModelInit +-> BeforeSystemInit +-> AfterSystemInit +-> Ready +-> Destroying +-> Destroyed +``` + +## 初始化顺序 + +注册顺序和初始化顺序不是一回事。当前框架会按组件类别统一推进: + +1. `Utility` +2. `Model` +3. `System` + +这保证了大多数系统在初始化时,可以安全依赖已经就绪的工具与模型。 + +启动方式: ```csharp -// 同步接口 -public interface IInitializable -{ - void Initialize(); -} +var architecture = new GameArchitecture(); +await architecture.InitializeAsync(); +await architecture.WaitUntilReadyAsync(); +``` -public interface IDestroyable -{ - void Destroy(); -} +注册逻辑仍然写在 `OnInitialize()`: -public interface ILifecycle : IInitializable, IDestroyable -{ -} - -// 异步接口 -public interface IAsyncInitializable -{ - Task InitializeAsync(); -} - -public interface IAsyncDestroyable -{ - ValueTask DestroyAsync(); -} - -public interface IAsyncLifecycle : IAsyncInitializable, IAsyncDestroyable +```csharp +protected override void OnInitialize() { + RegisterUtility(new SaveUtility()); + RegisterModel(new PlayerModel()); + RegisterSystem(new CombatSystem()); } ``` -### 初始化阶段 +## 销毁语义 -组件在注册到架构后会自动进行初始化: +销毁由 `DestroyAsync()` 统一触发,框架会按逆序回收组件。 + +如果组件实现了异步销毁接口,框架会优先走异步路径。也就是说,新代码应优先实现: + +- `IAsyncDestroyable` +- 或其他已有的异步销毁基类路径 + +同步 `Destroy()` 主要是兼容入口。 + +## 组件自己的生命周期 + +大多数组件不需要手写 `Initialize()`;继承框架基类即可: ```csharp -public class PlayerModel : AbstractModel +public sealed class PlayerModel : AbstractModel { protected override void OnInit() { - // 初始化逻辑 - Console.WriteLine("PlayerModel 初始化"); } } ``` -### 销毁阶段 - -当架构销毁时,所有实现了 `IDestroyable` 的组件会按注册的逆序被销毁: - ```csharp -public class GameSystem : AbstractSystem -{ - public void Destroy() - { - // 清理资源 - Console.WriteLine("GameSystem 销毁"); - } -} -``` - -## 基本用法 - -### 实现同步生命周期 - -最常见的方式是继承框架提供的抽象基类: - -```csharp -using GFramework.Core.Model; - -public class InventoryModel : AbstractModel -{ - private List _items = new(); - - protected override void OnInit() - { - // 初始化库存 - _items = new List(); - Console.WriteLine("库存系统已初始化"); - } -} -``` - -### 实现销毁逻辑 - -对于需要清理资源的组件,实现 `IDestroyable` 接口: - -```csharp -using GFramework.Core.Abstractions.System; -using GFramework.Core.Abstractions.Lifecycle; - -public class AudioSystem : ISystem, IDestroyable -{ - private AudioEngine _engine; - - public void Initialize() - { - _engine = new AudioEngine(); - _engine.Start(); - } - - public void Destroy() - { - // 清理音频资源 - _engine?.Stop(); - _engine?.Dispose(); - _engine = null; - } -} -``` - -### 在架构中注册 - -组件注册后,架构会自动管理其生命周期: - -```csharp -public class GameArchitecture : Architecture -{ - protected override void Init() - { - // 注册顺序:Model -> System -> Utility - RegisterModel(new PlayerModel()); // 1. 初始化 - RegisterModel(new InventoryModel()); // 2. 初始化 - RegisterSystem(new AudioSystem()); // 3. 初始化 - - // 销毁顺序会自动反转: - // AudioSystem -> InventoryModel -> PlayerModel - } -} -``` - -## 高级用法 - -### 异步初始化 - -对于需要异步操作的组件(如加载配置、连接数据库),使用异步生命周期: - -```csharp -using GFramework.Core.Abstractions.Lifecycle; -using GFramework.Core.Abstractions.System; - -public class ConfigurationSystem : ISystem, IAsyncInitializable -{ - private Configuration _config; - - public async Task InitializeAsync() - { - // 异步加载配置文件 - _config = await LoadConfigurationAsync(); - Console.WriteLine("配置已加载"); - } - - private async Task LoadConfigurationAsync() - { - await Task.Delay(100); // 模拟异步操作 - return new Configuration(); - } -} -``` - -### 异步销毁 - -对于需要异步清理的资源(如关闭网络连接、保存数据): - -```csharp -using GFramework.Core.Abstractions.Lifecycle; - -public class NetworkSystem : ISystem, IAsyncDestroyable -{ - private NetworkClient _client; - - public void Initialize() - { - _client = new NetworkClient(); - } - - public async ValueTask DestroyAsync() - { - // 异步关闭连接 - if (_client != null) - { - await _client.DisconnectAsync(); - await _client.DisposeAsync(); - } - Console.WriteLine("网络连接已关闭"); - } -} -``` - -### 完整异步生命周期 - -同时实现异步初始化和销毁: - -```csharp -public class DatabaseSystem : ISystem, IAsyncLifecycle -{ - private DatabaseConnection _connection; - - public async Task InitializeAsync() - { - // 异步连接数据库 - _connection = new DatabaseConnection(); - await _connection.ConnectAsync("connection-string"); - Console.WriteLine("数据库已连接"); - } - - public async ValueTask DestroyAsync() - { - // 异步关闭数据库连接 - if (_connection != null) - { - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } - Console.WriteLine("数据库连接已关闭"); - } -} -``` - -### 生命周期钩子 - -监听架构的生命周期阶段: - -```csharp -using GFramework.Core.Abstractions.Enums; - -public class AnalyticsSystem : AbstractSystem +public sealed class CombatSystem : AbstractSystem { protected override void OnInit() { - Console.WriteLine("分析系统初始化"); - } - - public override void OnArchitecturePhase(ArchitecturePhase phase) - { - switch (phase) - { - case ArchitecturePhase.Initializing: - Console.WriteLine("架构正在初始化"); - break; - case ArchitecturePhase.Ready: - Console.WriteLine("架构已就绪"); - StartTracking(); - break; - case ArchitecturePhase.Destroying: - Console.WriteLine("架构正在销毁"); - StopTracking(); - break; - } - } - - private void StartTracking() { } - private void StopTracking() { } -} -``` - -## 最佳实践 - -1. **优先使用抽象基类**:继承 `AbstractModel`、`AbstractSystem` 等基类,它们已经实现了生命周期接口 - ```csharp - ✓ public class MyModel : AbstractModel { } - ✗ public class MyModel : IModel, IInitializable { } - ``` - -2. **初始化顺序很重要**:按依赖关系注册组件,被依赖的组件先注册 - ```csharp - protected override void Init() - { - RegisterModel(new ConfigModel()); // 先注册配置 - RegisterModel(new PlayerModel()); // 再注册依赖配置的模型 - RegisterSystem(new GameplaySystem()); // 最后注册系统 - } - ``` - -3. **销毁时释放资源**:实现 `Destroy()` 方法清理非托管资源 - ```csharp - public void Destroy() - { - // 释放事件订阅 - _eventBus.Unsubscribe(OnGameEvent); - - // 释放非托管资源 - _nativeHandle?.Dispose(); - - // 清空引用 - _cache?.Clear(); - } - ``` - -4. **异步操作使用异步接口**:避免在同步方法中阻塞异步操作 - ```csharp - ✓ public async Task InitializeAsync() { await LoadDataAsync(); } - ✗ public void Initialize() { LoadDataAsync().Wait(); } // 可能死锁 - ``` - -5. **避免在初始化中访问其他组件**:初始化顺序可能导致组件尚未就绪 - ```csharp - ✗ protected override void OnInit() - { - var system = this.GetSystem(); // 可能尚未初始化 - } - - ✓ public override void OnArchitecturePhase(ArchitecturePhase phase) - { - if (phase == ArchitecturePhase.Ready) - { - var system = this.GetSystem(); // 安全 - } - } - ``` - -6. **使用 OnArchitecturePhase 处理跨组件依赖**:在 Ready 阶段访问其他组件 - ```csharp - public override void OnArchitecturePhase(ArchitecturePhase phase) - { - if (phase == ArchitecturePhase.Ready) - { - // 此时所有组件都已初始化完成 - var config = this.GetModel(); - ApplyConfiguration(config); - } - } - ``` - -## 常见问题 - -### 问题:什么时候使用同步 vs 异步生命周期? - -**解答**: - -- **同步**:简单的初始化逻辑,如创建对象、设置默认值 -- **异步**:需要 I/O 操作的场景,如加载文件、网络请求、数据库连接 - -```csharp -// 同步:简单初始化 -public class ScoreModel : AbstractModel -{ - protected override void OnInit() - { - Score = 0; // 简单赋值 - } -} - -// 异步:需要 I/O -public class SaveSystem : ISystem, IAsyncInitializable -{ - public async Task InitializeAsync() - { - await LoadSaveDataAsync(); // 文件 I/O } } ``` -### 问题:组件的初始化和销毁顺序是什么? +如果你的组件需要真正的异步初始化或销毁,再补对应接口。 -**解答**: +## 生命周期钩子 -- **初始化顺序**:按注册顺序(先注册先初始化) -- **销毁顺序**:按注册的逆序(后注册先销毁) +当你需要做横切阶段逻辑时,优先实现 `IArchitectureLifecycleHook`,而不是把这些逻辑分散到某个具体 `System` 里。 ```csharp -protected override void Init() +public sealed class MetricsHook : IArchitectureLifecycleHook { - RegisterModel(new A()); // 1. 初始化,3. 销毁 - RegisterModel(new B()); // 2. 初始化,2. 销毁 - RegisterSystem(new C()); // 3. 初始化,1. 销毁 -} -``` - -### 问题:如何在初始化时访问其他组件? - -**解答**: -不要在 `OnInit()` 中访问其他组件,使用 `OnArchitecturePhase()` 在 Ready 阶段访问: - -```csharp -public class DependentSystem : AbstractSystem -{ - protected override void OnInit() - { - // ✗ 不要在这里访问其他组件 - } - - public override void OnArchitecturePhase(ArchitecturePhase phase) + public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) { if (phase == ArchitecturePhase.Ready) { - // ✓ 在这里安全访问其他组件 - var config = this.GetModel(); + Console.WriteLine("Architecture ready."); } } } ``` -### 问题:Destroy() 方法一定会被调用吗? - -**解答**: -只有在正常销毁架构时才会调用。如果应用程序崩溃或被强制终止,`Destroy()` 可能不会被调用。因此: - -- 不要依赖 `Destroy()` 保存关键数据 -- 使用自动保存机制保护重要数据 -- 非托管资源应该实现 `IDisposable` 模式 - -### 问题:可以在 Destroy() 中访问其他组件吗? - -**解答**: -不推荐。销毁时其他组件可能已经被销毁。如果必须访问,确保检查组件是否仍然可用: +注册方式: ```csharp -public void Destroy() -{ - // ✗ 不安全 - var system = this.GetSystem(); - system.DoSomething(); - - // ✓ 安全 - try - { - var system = this.GetSystem(); - system?.DoSomething(); - } - catch - { - // 组件可能已销毁 - } -} +architecture.RegisterLifecycleHook(new MetricsHook()); ``` -## 相关文档 +## 阶段监听 -- [架构组件](/zh-CN/core/architecture) - 架构基础和组件注册 -- [Model 层](/zh-CN/core/model) - 数据模型的生命周期 -- [System 层](/zh-CN/core/system) - 业务系统的生命周期 -- [异步初始化](/zh-CN/core/async-initialization) - 异步架构初始化详解 +如果你只需要观察阶段变化,也可以直接订阅: + +```csharp +architecture.PhaseChanged += phase => +{ + Console.WriteLine($"Phase changed: {phase}"); +}; +``` + +## 什么时候会进入 `FailedInitialization` + +如果初始化流程中抛出异常,架构会切到 `FailedInitialization`。这意味着: + +- `Ready` 不会被触发 +- 后续诊断应先回到启动路径 +- 文档示例不应假设“只要 new 了 Architecture 就一定能跑到 Ready” + +## 推荐做法 + +- 新代码优先使用 `InitializeAsync()` / `DestroyAsync()` +- 把注册逻辑放在 `OnInitialize()`,不要沿用旧文档里的 `Init()` +- 让 `Utility` 承载底层能力,让 `Model` 承载状态,再让 `System` 消费两者 +- 跨组件阶段逻辑优先写成 `IArchitectureLifecycleHook` + +## 继续阅读 + +- 架构入口:[architecture](./architecture.md) +- 上下文入口:[context](./context.md) diff --git a/docs/zh-CN/core/query.md b/docs/zh-CN/core/query.md index f618bcf0..43e31ab7 100644 --- a/docs/zh-CN/core/query.md +++ b/docs/zh-CN/core/query.md @@ -1,532 +1,103 @@ -# Query 包使用说明 +# Query -## 概述 +本页说明 `GFramework.Core.Query` 里的旧查询体系。 -Query 包实现了 CQRS(命令查询职责分离)模式中的查询部分。Query 用于封装数据查询逻辑,与 Command 不同的是,Query -有返回值且不应该修改系统状态。 +和旧命令系统一样,它仍然保留用于兼容存量代码;新功能优先使用 [cqrs](./cqrs.md) 中的新查询模型。 -查询系统是 GFramework CQRS 架构的重要组成部分,专门负责数据读取操作,与命令系统和事件系统协同工作。 +## 当前仍然可用的基类 -## 核心接口 +旧查询体系最常见的两个基类是: -### IQuery`` +- `AbstractQuery` + - 无输入查询 +- `AbstractQuery` + - 带输入查询 -查询接口,定义了查询的基本契约。 +与旧文档不同,带输入查询现在通过构造函数接收输入,不再依赖 `Input` 属性赋值。 -**核心成员:** +## 无输入查询 ```csharp -TResult Do(); // 执行查询并返回结果 -``` +using GFramework.Core.Extensions; +using GFramework.Core.Query; -## 核心类 - -### AbstractQuery`` - -抽象查询基类,提供了查询的基础实现。通过 `IQueryInput` 接口传递参数。 - -**核心方法:** - -```csharp -TResult IQuery.Do(); // 实现 IQuery 接口 -protected abstract TResult OnDo(TInput input); // 抽象查询方法,接收输入参数 -``` - -**使用方式:** - -```csharp -public abstract class AbstractQuery : ContextAwareBase, IQuery - where TInput : IQueryInput +public sealed class GetPlayerHealthQuery : AbstractQuery { - public TResult Do() => OnDo(Input); // 执行查询 - public TInput Input { get; set; } // 输入参数 - protected abstract TResult OnDo(TInput input); // 子类实现查询逻辑 -} -``` - -### AbstractAsyncQuery`` - -支持异步执行的查询基类。 - -**核心方法:** - -```csharp -Task IAsyncQuery.DoAsync(); // 实现异步查询接口 -protected abstract Task OnDoAsync(TInput input); // 抽象异步查询方法 -``` - -### EmptyQueryInput - -空查询输入类,用于表示不需要任何输入参数的查询操作。 - -**使用方式:** - -```csharp -public sealed class EmptyQueryInput : IQueryInput -{ - // 作为占位符使用,适用于那些不需要额外输入参数的查询场景 -} -``` - -### QueryBus - -查询总线实现,负责执行查询并返回结果。 - -**核心方法:** - -```csharp -TResult Send(IQuery query); // 发送并执行查询 -``` - -**使用方式:** - -```csharp -public sealed class QueryBus : IQueryBus -{ - public TResult Send(IQuery query) + protected override int OnDo() { - ArgumentNullException.ThrowIfNull(query); - return query.Do(); + return this.GetModel().Health.Value; } } ``` -## 基本使用 - -### 1. 定义查询 +发送方式: ```csharp -// 定义查询输入 -public class GetPlayerGoldInput : IQueryInput { } +var health = this.SendQuery(new GetPlayerHealthQuery()); +``` -// 查询玩家金币数量 -public class GetPlayerGoldQuery : AbstractQuery -{ - protected override int OnDo(GetPlayerGoldInput input) - { - return this.GetModel().Gold.Value; - } -} +## 带输入查询 -// 定义查询输入 -public class GetItemCountInput : IQueryInput -{ - public string ItemId { get; set; } -} +旧查询输入类型现在直接复用 CQRS 抽象层里的 `IQueryInput`: -// 查询背包中指定物品的数量 -public class GetItemCountQuery : AbstractQuery +```csharp +using GFramework.Core.Extensions; +using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; + +public sealed record GetItemCountInput(string ItemId) : IQueryInput; + +public sealed class GetItemCountQuery(GetItemCountInput input) + : AbstractQuery(input) { protected override int OnDo(GetItemCountInput input) { - var inventory = this.GetModel(); - return inventory.GetItemCount(input.ItemId); - } -} - -// 定义异步查询输入 -public class LoadPlayerDataInput : IQueryInput -{ - public string PlayerId { get; set; } -} - -// 异步查询玩家数据 -public class LoadPlayerDataQuery : AbstractAsyncQuery -{ - protected override async Task OnDoAsync(LoadPlayerDataInput input) - { - var storage = this.GetUtility(); - return await storage.LoadPlayerDataAsync(input.PlayerId); + var inventoryModel = this.GetModel(); + return inventoryModel.GetItemCount(input.ItemId); } } ``` -### 2. 发送查询 - ```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class ShopUI : IController -{ - [Export] private Button _buyButton; - [Export] private int _itemPrice = 100; - - public void OnReady() - { - _buyButton.Pressed += OnBuyButtonPressed; - } - - private void OnBuyButtonPressed() - { - // 查询玩家金币 - var query = new GetPlayerGoldQuery { Input = new GetPlayerGoldInput() }; - int playerGold = this.SendQuery(query); - - if (playerGold >= _itemPrice) - { - // 发送购买命令 - this.SendCommand(new BuyItemCommand { Input = new BuyItemInput { ItemId = "sword_01" } }); - } - else - { - Console.WriteLine("金币不足!"); - } - } -} +var count = this.SendQuery( + new GetItemCountQuery(new GetItemCountInput("potion"))); ``` -### 3. 在 System 中使用 +## 异步查询 + +上下文仍然保留旧异步查询执行入口: + +- `SendQueryAsync(IAsyncQuery)` + +这主要面向兼容旧 `AsyncQueryExecutor` 路径。文档不再推荐围绕旧 `QueryBus` 设计新功能。 + +## 发送入口 + +旧查询的执行入口是: + +- `SendQuery(IQuery)` +- `SendQueryAsync(IAsyncQuery)` + +在 `IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展: ```csharp -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - // 注册事件监听 - this.RegisterEvent(OnEnemyAttack); - } - - private void OnEnemyAttack(EnemyAttackEvent e) - { - // 查询玩家是否已经死亡 - var query = new IsPlayerDeadQuery { Input = new EmptyQueryInput() }; - bool isDead = this.SendQuery(query); - - if (!isDead) - { - // 执行伤害逻辑 - this.SendCommand(new TakeDamageCommand { Input = new TakeDamageInput { Damage = e.Damage } }); - } - } -} - -public class IsPlayerDeadQuery : AbstractQuery -{ - protected override bool OnDo(EmptyQueryInput input) - { - return this.GetModel().Health.Value <= 0; - } -} -``` +using GFramework.Core.Extensions; ``` -## 高级用法 +## 什么时候继续保留旧查询 -### 1. 带参数的复杂查询 +- 你在维护现有 `Core.Query` 代码 +- 当前代码已经建立在旧查询执行器之上 +- 你只想修正局部行为,不想顺手迁移整条调用链 -```csharp -// 定义查询输入 -public class GetEnemiesInRangeInput : IQueryInput -{ - public Vector3 Center { get; set; } - public float Radius { get; set; } -} +## 什么时候改用 CQRS 查询 -// 查询指定范围内的敌人列表 -public class GetEnemiesInRangeQuery : AbstractQuery> -{ - protected override List OnDo(GetEnemiesInRangeInput input) - { - var enemySystem = this.GetSystem(); - return enemySystem.GetEnemiesInRange(input.Center, input.Radius); - } -} +如果你正在写新的读取路径,优先考虑: -// 使用 -var input = new GetEnemiesInRangeInput { Center = playerPosition, Radius = 10.0f }; -var query = new GetEnemiesInRangeQuery { Input = input }; -var enemies = this.SendQuery(query); -``` +- `GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery` +- `AbstractQueryHandler` +- `architecture.Context.SendQueryAsync(...)` -### 2. 组合查询 +原因很简单:新查询路径和命令、通知、流式请求共享同一 dispatcher 与行为管道。 -```csharp -// 定义查询输入 -public class CanUseSkillInput : IQueryInput -{ - public string SkillId { get; set; } -} - -// 查询玩家是否可以使用技能 -public class CanUseSkillQuery : AbstractQuery -{ - protected override bool OnDo(CanUseSkillInput input) - { - var playerModel = this.GetModel(); - - // 查询技能消耗 - var skillCostQuery = new GetSkillCostQuery { Input = new GetSkillCostInput { SkillId = input.SkillId } }; - var skillCost = this.SendQuery(skillCostQuery); - - // 检查是否满足条件 - return playerModel.Mana.Value >= skillCost.ManaCost - && !this.SendQuery(new IsSkillOnCooldownQuery { Input = new IsSkillOnCooldownInput { SkillId = input.SkillId } }); - } -} - -public class GetSkillCostInput : IQueryInput -{ - public string SkillId { get; set; } -} - -public class GetSkillCostQuery : AbstractQuery -{ - protected override SkillCost OnDo(GetSkillCostInput input) - { - return this.GetModel().GetSkillCost(input.SkillId); - } -} - -public class IsSkillOnCooldownInput : IQueryInput -{ - public string SkillId { get; set; } -} - -public class IsSkillOnCooldownQuery : AbstractQuery -{ - protected override bool OnDo(IsSkillOnCooldownInput input) - { - return this.GetModel().IsOnCooldown(input.SkillId); - } -} -``` - -### 3. 聚合数据查询 - -```csharp -// 查询玩家战斗力 -public class GetPlayerPowerQuery : AbstractQuery -{ - protected override int OnDo(EmptyQueryInput input) - { - var playerModel = this.GetModel(); - var equipmentModel = this.GetModel(); - - int basePower = playerModel.Level.Value * 10; - int equipmentPower = equipmentModel.GetTotalPower(); - int buffPower = this.SendQuery(new GetBuffPowerQuery { Input = new EmptyQueryInput() }); - - return basePower + equipmentPower + buffPower; - } -} - -// 查询玩家详细信息(用于UI显示) -public class GetPlayerInfoQuery : AbstractQuery -{ - protected override PlayerInfo OnDo(EmptyQueryInput input) - { - var playerModel = this.GetModel(); - - return new PlayerInfo - { - Name = playerModel.Name.Value, - Level = playerModel.Level.Value, - Health = playerModel.Health.Value, - MaxHealth = playerModel.MaxHealth.Value, - Gold = this.SendQuery(new GetPlayerGoldQuery { Input = new GetPlayerGoldInput() }), - Power = this.SendQuery(new GetPlayerPowerQuery { Input = new EmptyQueryInput() }) - }; - } -} -``` - -### 4. 跨 System 查询 - -```csharp -// 在 AI System 中查询玩家状态 -public class EnemyAISystem : AbstractSystem -{ - protected override void OnInit() { } - - public void UpdateEnemyBehavior(Enemy enemy) - { - // 查询玩家位置 - var playerPosQuery = new GetPlayerPositionQuery { Input = new EmptyQueryInput() }; - var playerPos = this.SendQuery(playerPosQuery); - - // 查询玩家是否在攻击范围内 - var inRangeInput = new IsPlayerInRangeInput { Position = enemy.Position, Range = enemy.AttackRange }; - bool inRange = this.SendQuery(new IsPlayerInRangeQuery { Input = inRangeInput }); - - if (inRange) - { - // 查询是否可以攻击 - var canAttackInput = new CanEnemyAttackInput { EnemyId = enemy.Id }; - bool canAttack = this.SendQuery(new CanEnemyAttackQuery { Input = canAttackInput }); - - if (canAttack) - { - this.SendCommand(new EnemyAttackCommand { Input = new EnemyAttackInput { EnemyId = enemy.Id } }); - } - } - } -} -``` - -## Query 的执行机制 - -所有发送给查询总线的查询最终都会通过 `QueryExecutor` 来执行: - -```csharp -public class QueryExecutor -{ - public static TResult Execute(IQuery query) - { - return query.Do(); - } -} -``` - -**特点:** - -- 提供统一的查询执行机制 -- 支持同步查询执行 -- 与架构上下文集成 - -## Command vs Query - -### Command(命令) - -- **用途**:修改系统状态 -- **返回值**:无返回值(void)或有返回值 -- **示例**:购买物品、造成伤害、升级角色 - -### Query(查询) - -- **用途**:读取数据,不修改状态 -- **返回值**:必须有返回值 -- **示例**:获取金币数量、检查技能冷却、查询玩家位置 - -```csharp -// ❌ 错误:在 Query 中修改状态 -public class BadQuery : AbstractQuery -{ - protected override int OnDo() - { - var model = this.GetModel(); - model.Gold.Value += 100; // 不应该在 Query 中修改数据! - return model.Gold.Value; - } -} - -// ✅ 正确:Query 只读取数据 -public class GoodQuery : AbstractQuery -{ - protected override int OnDo() - { - return this.GetModel().Gold.Value; - } -} - -// ✅ 修改数据应该使用 Command -public class AddGoldCommand : AbstractCommand -{ - private readonly int _amount; - - public AddGoldCommand(int amount) - { - _amount = amount; - } - - protected override void OnExecute() - { - var model = this.GetModel(); - model.Gold.Value += _amount; - } -} -``` - -## 最佳实践 - -1. **查询只读取,不修改** - 保持 Query 的纯粹性 -2. **小而专注** - 每个 Query 只负责一个具体的查询任务 -3. **可组合** - 复杂查询可以通过组合简单查询实现 -4. **避免过度查询** - 如果需要频繁查询,考虑使用 BindableProperty -5. **命名清晰** - Query 名称应该清楚表达查询意图(Get、Is、Can、Has等前缀) -6. **参数通过构造函数传递** - 查询需要的参数应在创建时传入 -7. **查询无状态** - 查询不应该保存长期状态,执行完即可丢弃 -8. **合理使用缓存** - 对于复杂计算,可以在 Model 中缓存结果 - -## 性能优化 - -### 1. 缓存查询结果 - -```csharp -// 在 Model 中缓存复杂计算 -public class PlayerModel : AbstractModel -{ - private int? _cachedPower; - - public int GetPower() - { - if (_cachedPower == null) - { - _cachedPower = CalculatePower(); - } - return _cachedPower.Value; - } - - private int CalculatePower() - { - // 复杂计算... - return 100; - } - - public void InvalidatePowerCache() - { - _cachedPower = null; - } -} -``` - -### 2. 批量查询 - -```csharp -// 一次查询多个数据,而不是多次单独查询 -public class GetMultipleItemCountsQuery : AbstractQuery> -{ - private readonly List _itemIds; - - public GetMultipleItemCountsQuery(List itemIds) - { - _itemIds = itemIds; - } - - protected override Dictionary OnDo() - { - var inventory = this.GetModel(); - return _itemIds.ToDictionary(id => id, id => inventory.GetItemCount(id)); - } -} -``` - -## 查询模式优势 - -### 1. 职责分离 - -- 读写操作明确分离 -- 便于优化读写性能 -- 降低系统复杂度 - -### 2. 可扩展性 - -- 读写可以独立扩展 -- 支持不同的数据存储策略 -- 便于实现读写分离 - -### 3. 可维护性 - -- 查询逻辑集中管理 -- 便于重构和优化 -- 降低组件间耦合 - -## 相关包 - -- [`command`](./command.md) - CQRS 的命令部分 -- [`model`](./model.md) - Query 主要从 Model 获取数据 -- [`system`](./system.md) - System 中可以发送 Query -- **Controller** - Controller 中可以发送 Query(接口定义在 Core.Abstractions 中) -- [`extensions`](./extensions.md) - 提供 SendQuery 扩展方法 -- [`architecture`](./architecture.md) - 架构核心,负责查询的分发和执行 \ No newline at end of file +继续阅读:[cqrs](./cqrs.md) From 26d5d84d267c0e3ad1cce1e7d7d96a3a0134c8e0 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:30:53 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(pr-review):=20=E4=BF=AE=E5=A4=8D=20Code?= =?UTF-8?q?Rabbit=20=E9=9D=9E=E7=A9=BA=E8=AF=84=E5=AE=A1=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复最新 head commit 上空 APPROVED review 覆盖非空 COMMENTED review 的选择逻辑 - 保持最新 review 元数据输出不变,并新增用于结构化解析的非空 CodeRabbit review 选择 - 验证当前分支 PR 可重新提取 Nitpick comments --- .../scripts/fetch_current_pr_review.py | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py b/.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py index 017f2424..b7e3d467 100644 --- a/.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py +++ b/.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py @@ -538,6 +538,27 @@ def build_latest_commit_review_threads(comments: list[dict[str, Any]]) -> list[d return sorted(threads, key=lambda item: (item["path"], item["line"] or 0, item["thread_id"])) +def select_latest_submitted_review( + reviews: list[dict[str, Any]], + *, + required_user: str | None = None, + prefer_non_empty_body: bool = False, +) -> dict[str, Any] | None: + filtered_reviews = [review for review in reviews if review.get("submitted_at")] + if required_user is not None: + filtered_reviews = [review for review in filtered_reviews if review.get("user", {}).get("login") == required_user] + + if not filtered_reviews: + return None + + if prefer_non_empty_body: + non_empty_body_reviews = [review for review in filtered_reviews if str(review.get("body") or "").strip()] + if non_empty_body_reviews: + filtered_reviews = non_empty_body_reviews + + return max(filtered_reviews, key=lambda review: review.get("submitted_at", "")) + + def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]: api_base = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}" commits = fetch_paged_json(f"{api_base}/commits?per_page=100") @@ -558,10 +579,11 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]: review for review in reviews if review.get("commit_id") == latest_commit_sha and review.get("submitted_at") ] candidate_reviews = latest_commit_reviews or [review for review in reviews if review.get("submitted_at")] - latest_review = ( - max(candidate_reviews, key=lambda review: review.get("submitted_at", "")) - if candidate_reviews - else None + latest_review = select_latest_submitted_review(candidate_reviews) + latest_coderabbit_review_with_body = select_latest_submitted_review( + candidate_reviews, + required_user=CODERABBIT_LOGIN, + prefer_non_empty_body=True, ) latest_commit_comments = [comment for comment in comments if comment.get("commit_id") == latest_commit_sha] @@ -581,6 +603,18 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]: "user": latest_review.get("user", {}).get("login") if latest_review else "", "body": latest_review.get("body") if latest_review else "", }, + "latest_coderabbit_review_with_body": { + "id": latest_coderabbit_review_with_body.get("id") if latest_coderabbit_review_with_body else None, + "state": latest_coderabbit_review_with_body.get("state") if latest_coderabbit_review_with_body else "", + "submitted_at": ( + latest_coderabbit_review_with_body.get("submitted_at") if latest_coderabbit_review_with_body else "" + ), + "commit_id": latest_coderabbit_review_with_body.get("commit_id") if latest_coderabbit_review_with_body else "", + "user": latest_coderabbit_review_with_body.get("user", {}).get("login") + if latest_coderabbit_review_with_body + else "", + "body": latest_coderabbit_review_with_body.get("body") if latest_coderabbit_review_with_body else "", + }, "threads": threads, "open_threads": open_threads, } @@ -621,7 +655,7 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]: coderabbit_review: dict[str, Any] = {} try: latest_commit_review = fetch_latest_commit_review(pr_number) - latest_review = latest_commit_review.get("latest_review", {}) + latest_review = latest_commit_review.get("latest_coderabbit_review_with_body", {}) latest_review_body = str(latest_review.get("body") or "") if latest_review.get("user") == CODERABBIT_LOGIN and latest_review_body: coderabbit_review = parse_latest_review_body(latest_review_body) From 233195df91abc3c1b4d4bdbdce1409da1c5d29cf Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:34:37 +0800 Subject: [PATCH 5/5] =?UTF-8?q?docs(core):=20=E8=A1=A5=E9=BD=90=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E9=93=BE=E6=8E=A5=E5=B9=B6=E6=94=B6=E5=8F=A3=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E5=91=8A=E8=AD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 core landing page 的 Godot 与 Source Generators 导航入口为可点击链接 - 修复 documentation-governance-and-refresh active trace 的重复标题并消除 MD024 告警 - 补充 tracking 与 trace 的恢复点、验证记录和 PR review 跟进结论 --- ...ntation-governance-and-refresh-tracking.md | 12 ++++++-- ...umentation-governance-and-refresh-trace.md | 29 +++++++++++++++++-- docs/zh-CN/core/index.md | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md index a3f984b8..4dd0f89c 100644 --- a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md +++ b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md @@ -7,7 +7,7 @@ ## 当前恢复点 -- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-003` +- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-004` - 当前阶段:`Phase 3` - 当前焦点: - 已完成 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md` 与 @@ -32,6 +32,10 @@ - `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题 - `core` 关键专题页已移除 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的 `RegisterMediatorBehavior` 等过时说明 +- `core/index.md` 已把 `Godot` 与 `Source Generators` 栏目入口改成可点击链接,补齐 landing page 导航一致性 +- `documentation-governance-and-refresh` active trace 已把重复的 `### 下一步` 标题改成带恢复点标识的唯一标题,消除 + `MD024/no-duplicate-heading` 告警 +- `gframework-pr-review` 脚本已修复“空 `APPROVED` review 覆盖非空 CodeRabbit review body”的解析路径,当前分支可重新提取 Nitpick comments ## 当前风险 @@ -42,6 +46,9 @@ - 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring - Active 入口回膨胀风险:后续若把栏目级重写过程直接追加到 active 文档,会再次拖慢恢复 - 缓解措施:阶段完成并验证后,继续把细节迁入本 topic 的 `archive/` +- review 跟进遗漏风险:如果 PR review 抓取继续优先选中空 review body,会漏掉 CodeRabbit 的 Nitpick 和 + linter 跟进项 + - 缓解措施:保持当前“最新提交 + 最新非空 CodeRabbit review body”解析策略,并在有疑点时以 API 实抓结果复核 ## 活跃文档 @@ -53,10 +60,11 @@ - 旧 `local-plan/` 的详细实施历史与文档站构建结果已迁入主题内归档 - active 跟踪文件已按 `ai-plan` 治理规则精简为当前恢复入口 - `cd docs && bun run build` +- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json` ## 下一步 1. 继续核对 `docs/zh-CN/core/*` 余下专题页,优先处理 `events`、`property`、`state-management`、`coroutine` 与 `logging` 2. 再推进 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面 -3. 若专题页批量重写完成且验证通过,将本轮 `core` 专题页收口和后续修订过程迁入本 topic 的 `archive/` +3. 若 active trace 再积累新的已完成阶段,按恢复点粒度迁入 `archive/traces/`,避免默认启动入口再次膨胀 diff --git a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md index bea860e5..c8f4130d 100644 --- a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md +++ b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md @@ -25,7 +25,7 @@ - 历史 trace 归档: - `ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-history-through-2026-04-18.md` -### 下一步 +### 下一步(RP-001) 1. 后续继续该主题时,只从 `ai-plan/public/documentation-governance-and-refresh/` 进入,不再恢复 `local-plan/` 2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/` @@ -47,7 +47,7 @@ - 当前默认导航入口已显著收敛,但专题页仍需逐页按源码与测试继续核对 - 后续优先级应从 `core` 专题页开始,再向 `game` 与 `source-generators` 扩展 -### 下一步 +### 下一步(RP-002) 1. 审核 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md`、`cqrs.md` 2. 记录每页的失真点、真实 API 名称与应保留的最小示例 @@ -77,7 +77,30 @@ 是公开入口 - 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页重写没有破坏文档站构建 -### 下一步 +### 下一步(RP-003) + +### 补充:2026-04-21 PR review 跟进收口(RP-004) + +- 通过 `gframework-pr-review` 复查当前分支 PR 时发现:脚本把同一 head commit 上空 body 的 `APPROVED` + review 误当成“最新 review body”,导致 `Nitpick comments` 未被结构化提取 +- 对照 GitHub API 的 review 列表后,确认真正包含 `Nitpick comments (2)` 的是更早 3 秒提交的 + `COMMENTED` review;因此调整脚本为“保持最新 review 元数据输出不变,但解析时优先选择同一提交上的最新非空 + CodeRabbit review body” +- 根据重新提取的 Nitpick 内容,补齐 `docs/zh-CN/core/index.md` 里 `Godot` 与 `Source Generators` + 栏目的可点击链接 +- 顺手修正 active trace 中重复的 `### 下一步` 标题,消除 `MD024/no-duplicate-heading` 告警,避免后续 PR + review 再次把文档治理入口本身标成噪音 + +### 验证(RP-004) + +- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json` +- `cd docs && bun run build` + +### 下一步(RP-004) + +1. 继续处理 `docs/zh-CN/core/events.md`、`property.md`、`state-management.md`、`coroutine.md`、`logging.md` +2. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节 +3. 保持 PR review 跟进时优先验证最新未解决线程、非空 CodeRabbit review body 与 MegaLinter 明确告警 1. 继续处理 `docs/zh-CN/core/events.md`、`property.md`、`state-management.md`、`coroutine.md`、`logging.md` 2. 保持同样的证据顺序:源码、`*.csproj`、模块 README、`ai-libs/` 参考实现 diff --git a/docs/zh-CN/core/index.md b/docs/zh-CN/core/index.md index 306dabf7..d8a600a6 100644 --- a/docs/zh-CN/core/index.md +++ b/docs/zh-CN/core/index.md @@ -88,7 +88,7 @@ public sealed class CounterArchitecture : Architecture - 要接入游戏内容配置、设置、数据仓库、Scene 或 UI: - 转到 [Game](../game/index.md) - 要接入 Godot 节点、场景和项目元数据生成: - - 转到 `Godot` 与 Source Generators 栏目 + - 转到 [Godot](../godot/index.md) 与 [Source Generators](../source-generators/index.md) 栏目 ## 推荐阅读顺序