Compare commits

...

12 Commits

Author SHA1 Message Date
gewuyou
22f271e709
Merge pull request #251 from GeWuYou/feat/rich-text-effects-and-color-markers
feat(text): 添加富文本效果系统和颜色标记功能
2026-04-18 15:53:34 +08:00
GeWuYou
11515ff791 feat(godot): 添加富文本标签效果系统支持
- 新增 GfRichTextLabel 组件作为富文本标签宿主
- 实现 IRichTextEffectHost 接口用于效果控制器驱动
- 创建 RichTextEffectsController 处理效果装配逻辑
- 添加 RichTextProfile 配置资源类型
- 引入 RichTextEffectPlan 和 RichTextEffectPlanEntry 类型
- 在 CI 工作流中添加 GFramework.Godot.Tests 项目
- 优化 Godot 测试诊断条件判断逻辑
- 添加富文本效果控制器相关单元测试
2026-04-18 15:47:08 +08:00
GeWuYou
1145f455f3 feat(ci): 添加CI/CD工作流配置文件
- 配置pull request触发的构建和测试流程
- 集成代码质量检查和安全扫描功能
- 设置.NET多版本SDK环境支持
- 配置NuGet包和dotnet工具缓存优化
- 实现Node.js和Bun运行时环境搭建
- 添加配置工具依赖安装和测试执行
- 配置项目构建和单元测试执行流程
- 集成测试报告生成和发布功能
- 实现失败测试项目的错误处理机制
2026-04-18 15:18:37 +08:00
GeWuYou
5cb5a2270b fix(ci): 修复测试项目失败时的输出处理逻辑
- 使用here document格式正确输出失败项目列表到GITHUB_OUTPUT
- 添加EOF分隔符确保多行内容正确传递
- 在失败步骤中读取并显示具体的失败项目名称
- 保持原有的退出码设置确保工作流正确失败
2026-04-18 15:08:53 +08:00
GeWuYou
1665e72115 fix(ci): 修复PR报告权限配置问题
- 添加pull-requests写入权限以支持PR评论功能
- 修改pull-request-report条件避免跨仓库触发错误
2026-04-18 15:05:09 +08:00
GeWuYou
f3d50c6361 refactor(tests): 更新测试代码中的命名空间引用
- 将 RichTextEffectsControllerTests 中的 GFramework.Godot.Text 引用替换为 Godot 相关命名空间
- 使用 Godot.Collections.Array 替代系统数组类型
- 在 GlobalUsings 中添加 GFramework.Godot.Text 的全局引用
- 修复返回类型以使用新的 Array<RichTextEffect> 结构
2026-04-18 14:59:21 +08:00
GeWuYou
1c2a813a52 feat(ci): 添加CI/CD工作流配置和Godot文本效果控制器测试
- 配置GitHub Actions工作流用于PR构建和测试
- 实现代码质量检查和安全扫描功能
- 添加.NET多版本SDK支持和依赖缓存
- 集成MegaLinter和TruffleHog安全工具
- 创建RichTextEffectsController的单元测试覆盖各种场景
- 实现测试报告生成和发布功能
2026-04-18 14:55:31 +08:00
GeWuYou
22882f68c4 feat(text): 添加富文本效果系统和抖动效果实现
- 创建 RichTextEffectBase 基类提供统一的标签命名和环境参数读取逻辑
- 实现 RichTextJitterEffect 抖动效果类,支持振幅和速度参数调节
- 添加 DefaultRichTextEffectRegistry 默认效果注册表管理内置效果映射
- 创建 GfRichTextLabel 组合式富文本标签宿主,集成效果装配逻辑
- 定义 IRichTextEffectRegistry 接口实现效果注册表抽象
- 开发 RichTextEffectsController 装配控制器负责效果集合管理
- 实现 RichTextMarkup 工具类提供语义化富文本标签构建辅助方法
- 添加相关单元测试验证效果控制器和标记工具的功能正确性
2026-04-18 14:17:09 +08:00
GeWuYou
e5ad29314e feat(text): 添加富文本效果系统和颜色标记功能
- 实现 RichTextEffectBase 基类提供统一的标签命名和参数读取逻辑
- 添加 RichTextBlueEffect、RichTextGoldEffect、RichTextGreenEffect 和 RichTextRedEffect 颜色标记效果
- 添加 RichTextFadeInEffect、RichTextFlyInEffect、RichTextJitterEffect 和 RichTextSineEffect 动画效果
- 实现 DefaultRichTextEffectRegistry 默认效果注册表
- 创建 GfRichTextLabel 富文本标签宿主组件
- 添加 IRichTextEffectRegistry 效果注册表接口
- 实现 RichTextEffectEntry、RichTextEffectsController 和 RichTextProfile 配置管理类
- 添加 RichTextMarkup 语义化标签构建辅助方法
- 创建 RichTextMarkupTests 和 RichTextProfileTests 单元测试
2026-04-18 11:58:40 +08:00
gewuyou
2f4fccabf2
Merge pull request #250 from GeWuYou/docs/sdk-update-documentation
docs(sdk): 更新文档规范并添加VitePress配置
2026-04-18 10:28:21 +08:00
coderabbitai[bot]
8ddf1b6eef
fix: apply CodeRabbit auto-fixes
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-04-18 02:22:15 +00:00
GeWuYou
ebdc231c07 docs(sdk): 更新文档规范并添加VitePress配置
- 调整文档真实性原则,以源码和测试为首要证据源
- 新增模块README要求,规定所有用户包必须有说明文档
- 更新仓库文档规范,要求根README与文档站点分类一致
- 添加VitePress配置文件,支持中文搜索和泛型转义
- 创建入门指南文档,说明GFramework模块组成和接入路径
- 添加快速开始教程,演示Core模块最小使用示例
- 为Core模块添加详细README文档
- 为Core.Abstractions添加契约层说明文档
- 为Core.SourceGenerators添加源码生成器文档
- 为Game模块添加运行时层详细说明文档
2026-04-18 10:08:05 +08:00
39 changed files with 2797 additions and 835 deletions

View File

@ -8,6 +8,7 @@ on:
permissions: permissions:
contents: read contents: read
pull-requests: write
security-events: write security-events: write
jobs: jobs:
@ -145,11 +146,12 @@ jobs:
run: dotnet build GFramework.sln -c Release --no-restore run: dotnet build GFramework.sln -c Release --no-restore
# 运行单元测试输出TRX格式结果到TestResults目录 # 运行单元测试输出TRX格式结果到TestResults目录
# 在同一个 step 中并发执行所有测试以加快速度 # 顺序执行各测试项目,避免并发 dotnet test 进程导致“TRX 全绿但 step 仍返回失败”的假红状态
- name: Test All Projects - name: Test All Projects
id: test_all_projects id: test_all_projects
run: | run: |
set -euo pipefail set -euo pipefail
mkdir -p TestResults
test_projects=( test_projects=(
"GFramework.Core.Tests/GFramework.Core.Tests.csproj:core" "GFramework.Core.Tests/GFramework.Core.Tests.csproj:core"
@ -161,29 +163,49 @@ jobs:
"GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj:godot-sg" "GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj:godot-sg"
) )
pids=() failed=0
failed_projects=()
failed_log_paths=()
for entry in "${test_projects[@]}"; do for entry in "${test_projects[@]}"; do
project="${entry%%:*}" project="${entry%%:*}"
name="${entry##*:}" name="${entry##*:}"
log_path="TestResults/${name}.console.log"
dotnet test "$project" \ echo "::group::dotnet test $project"
if ! dotnet test "$project" \
-c Release \ -c Release \
--no-build \ --no-build \
--logger "trx;LogFileName=${name}-$RANDOM.trx" \ --logger "trx;LogFileName=${name}.trx" \
--results-directory TestResults & --results-directory TestResults \
2>&1 | tee "$log_path"; then
pids+=("$!")
done
failed=0
for pid in "${pids[@]}"; do
if ! wait "$pid"; then
failed=1 failed=1
failed_projects+=("$project")
failed_log_paths+=("$log_path")
echo "::error title=Test project failed::$project returned a non-zero exit code."
fi fi
echo "::endgroup::"
done done
echo "failed=$failed" >> "$GITHUB_OUTPUT" if [ "$failed" -eq 1 ]; then
printf 'Failed test projects:\n'
printf ' %s\n' "${failed_projects[@]}"
fi
{
echo "failed=$failed"
echo "failed_projects<<EOF"
if [ "${#failed_projects[@]}" -gt 0 ]; then
printf '%s\n' "${failed_projects[@]}"
fi
echo "EOF"
echo "failed_log_paths<<EOF"
if [ "${#failed_log_paths[@]}" -gt 0 ]; then
printf '%s\n' "${failed_log_paths[@]}"
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Generate CTRF report - name: Generate CTRF report
run: | run: |
mkdir -p ctrf mkdir -p ctrf
@ -198,6 +220,20 @@ jobs:
-d ctrf \ -d ctrf \
-f "$name.json" -f "$name.json"
done done
- name: Run GFramework.Godot.Tests Diagnostics
if: always() && contains(steps.test_all_projects.outputs.failed_projects, 'GFramework.Godot.Tests/GFramework.Godot.Tests.csproj')
continue-on-error: true
run: |
mkdir -p TestResults
dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj \
-c Release \
--no-build \
--blame-crash \
--diag TestResults/godot-testhost-diag.log \
--logger "trx;LogFileName=godot-diagnostic.trx" \
--results-directory TestResults \
2>&1 | tee TestResults/godot-diagnostic.console.log
# 生成并发布测试报告,无论测试成功或失败都会执行 # 生成并发布测试报告,无论测试成功或失败都会执行
@ -213,7 +249,7 @@ jobs:
with: with:
report-path: './ctrf/*.json' report-path: './ctrf/*.json'
github-report: true github-report: true
pull-request-report: true pull-request-report: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
summary-delta-report: true summary-delta-report: true
insights-report: true insights-report: true
flaky-rate-report: true flaky-rate-report: true
@ -227,4 +263,19 @@ jobs:
- name: Fail if any test project failed - name: Fail if any test project failed
if: always() && steps.test_all_projects.outputs.failed == '1' if: always() && steps.test_all_projects.outputs.failed == '1'
run: exit 1 env:
FAILED_PROJECTS: ${{ steps.test_all_projects.outputs.failed_projects }}
FAILED_LOG_PATHS: ${{ steps.test_all_projects.outputs.failed_log_paths }}
run: |
echo "The following test projects returned non-zero exit codes:"
printf '%s\n' "$FAILED_PROJECTS"
echo
echo "Captured dotnet test output:"
while IFS= read -r log_path; do
if [ -n "$log_path" ] && [ -f "$log_path" ]; then
echo "--- BEGIN $log_path ---"
cat "$log_path"
echo "--- END $log_path ---"
fi
done <<< "$FAILED_LOG_PATHS"
exit 1

View File

@ -237,23 +237,32 @@ bash scripts/validate-csharp-naming.sh
- If a framework abstraction changes meaning or intended usage, update the explanatory comments in code as part of the - If a framework abstraction changes meaning or intended usage, update the explanatory comments in code as part of the
same change. same change.
### Task Tracking ### Documentation Source Of Truth
- When working from a tracked implementation plan, contributors MUST update the corresponding tracking document under - Treat source code, `*.csproj`, tests, generated snapshots, and packaging metadata as the primary evidence for
`local-plan/todos/` in the same change. documentation updates.
- Tracking updates MUST reflect completed work, newly discovered issues, validation results, and the next recommended - Treat `CoreGrid` as a secondary evidence source for real project adoption patterns, directory layouts, and end-to-end
recovery point. usage examples.
- Completing code changes without updating the active tracking document is considered incomplete work. - Treat existing `README.md` files and `docs/zh-CN/` pages as editable outputs, not authoritative truth.
- For any multi-step refactor, migration, or cross-module task, contributors MUST create or adopt a dedicated recovery - If existing documentation conflicts with code or tests, update the documentation to match the implementation instead
document under `local-plan/todos/` before making substantive code changes. of preserving outdated wording.
- Recovery documents MUST record the current phase, the active recovery point identifier, known risks, and the next - Do not publish example code, setup steps, or package guidance that cannot be traced back to code, tests, or a
recommended resume step so another contributor or subagent can continue the work safely. verified consumer project.
- Contributors MUST maintain a matching execution trace under `local-plan/traces/` for complex work. The trace should
record the current date, key decisions, validation milestones, and the immediate next step. ### Module README Requirements
- When a task spans multiple commits or is likely to exceed a single agent context window, update both the recovery
document and the trace at each meaningful milestone before pausing or handing work off. - Every user-facing package or module directory that contains a `*.csproj` intended for direct consumption MUST have a
- If subagents are used on a complex task, the main agent MUST capture the delegated scope and any accepted findings in sibling `README.md`.
the active recovery document or trace before continuing implementation. - Use the canonical filename `README.md`. Do not introduce new `ReadMe.md` or other filename variants.
- A module README MUST describe:
- the module's purpose
- the relationship to adjacent runtime, abstractions, or generator packages
- the major subdirectories or subsystems the reader is expected to use
- the minimum adoption path
- the corresponding `docs/zh-CN/` entry points
- Adding a new top-level module directory without a `README.md` is considered incomplete work.
- If a module's responsibilities, setup, public API surface, generator inputs, or adoption path change, update that
module's `README.md` in the same change.
### Repository Documentation ### Repository Documentation
@ -273,6 +282,32 @@ bash scripts/validate-csharp-naming.sh
documentation is considered incomplete work. documentation is considered incomplete work.
- Do not rely on “the code is self-explanatory” for framework features that consumers need to adopt; write the - Do not rely on “the code is self-explanatory” for framework features that consumers need to adopt; write the
adoption path down so future users do not need to rediscover it from source. adoption path down so future users do not need to rediscover it from source.
- The repository root `README.md` MUST mirror the current top-level documentation taxonomy used by the docs site.
Do not maintain a second, differently named navigation system in the root README.
- Prefer linking the root `README.md` to section landing pages such as `index.md` instead of deep-linking to a single
article when the target is intended to be a documentation category.
- If a docs category appears in VitePress navigation or sidebar, it MUST have a real landing page or be removed from
navigation in the same change.
- When examples are rewritten, preserve only the parts that remain true. Delete or replace speculative examples instead
of lightly editing them into another inaccurate form.
### Task Tracking
- When working from a tracked implementation plan, contributors MUST update the corresponding tracking document under
`local-plan/todos/` in the same change.
- Tracking updates MUST reflect completed work, newly discovered issues, validation results, and the next recommended
recovery point.
- Completing code changes without updating the active tracking document is considered incomplete work.
- For any multi-step refactor, migration, or cross-module task, contributors MUST create or adopt a dedicated recovery
document under `local-plan/todos/` before making substantive code changes.
- Recovery documents MUST record the current phase, the active recovery point identifier, known risks, and the next
recommended resume step so another contributor or subagent can continue the work safely.
- Contributors MUST maintain a matching execution trace under `local-plan/traces/` for complex work. The trace should
record the current date, key decisions, validation milestones, and the immediate next step.
- When a task spans multiple commits or is likely to exceed a single agent context window, update both the recovery
document and the trace at each meaningful milestone before pausing or handing work off.
- If subagents are used on a complex task, the main agent MUST capture the delegated scope and any accepted findings in
the active recovery document or trace before continuing implementation.
### Documentation Preview ### Documentation Preview

View File

@ -1,27 +1,51 @@
# GFramework.Core.Abstractions # GFramework.Core.Abstractions
GFramework 框架的抽象层定义模块,包含所有核心组件的接口定义 `GFramework.Core.Abstractions` 承载 `Core` 运行时对应的接口、枚举和值对象,用来定义跨模块协作边界
## 主要内容 ## 什么时候单独依赖它
- 架构核心接口 (IArchitecture, IArchitectureContext等) - 你在做插件、适配层或扩展包,只想依赖契约,不想把完整运行时拉进来
- 数据模型接口 (IModel) - 你需要为测试、编辑器工具或生成器提供替身实现
- 业务系统接口 (ISystem) - 你在做多模块拆分,希望上层只面向接口编程
- 控制器接口 (IController)
- 命令与查询接口 (ICommand, IQuery)
- 事件系统接口 (IEvent, IEventBus)
- 依赖注入容器接口 (IIocContainer)
- 可绑定属性接口 (IBindableProperty)
- 状态管理接口 (IStore, IReducer, IStateSelector, IStoreBuilder)
- 日志系统接口 (ILogger)
## 设计原则 如果你只是直接使用框架功能,优先安装 `GFramework.Core`
- 接口隔离,每个接口职责单一 ## 包关系
- 依赖倒置,上层依赖抽象接口
- 类型安全,充分利用泛型系统
- 广泛兼容,基于 netstandard2.0
## 详细文档 - 契约层:`GFramework.Core.Abstractions`
- 实现层:`GFramework.Core`
- 相关扩展:
- `GFramework.Cqrs.Abstractions`
- `GFramework.Game.Abstractions`
参见 [docs/zh-CN/abstractions/](../docs/zh-CN/abstractions/) 目录下的详细文档。 ## 契约地图
| 目录 | 作用 |
| --- | --- |
| `Architectures/` | `IArchitecture`、模块、阶段监听与服务管理契约 |
| `Command/` / `Query/` | 旧版命令与查询执行器接口 |
| `Controller/` | `IController` |
| `Events/` | 事件契约、解绑接口与传播上下文 |
| `Model/` / `Systems/` / `Utility/` | 核心组件接口 |
| `State/` / `StateManagement/` | 状态机、Store、reducer、selector 契约 |
| `Property/` | `IBindableProperty` 与只读属性接口 |
| `Resource/` | 资源管理与释放策略契约 |
| `Localization/` | 本地化表、格式化与异常类型 |
| `Logging/` | logger、log entry、factory 相关契约 |
| `Ioc/` | `IIocContainer` |
| `Lifecycle/` | 初始化 / 销毁生命周期契约 |
| `Coroutine/` | 时间源、yield 指令与协程状态枚举 |
| `Pause/` | 暂停栈、token 与状态事件 |
| `Storage/` / `Serializer/` / `Versioning/` | 通用存储、序列化与版本化契约 |
## 采用建议
- 框架消费者通常同时安装 `GFramework.Core``GFramework.Core.Abstractions`
- 若你只需要对接口编程,可以仅引用本包,再在应用层自行提供实现
- 若你在写上层模块,优先把公共契约放在 `*.Abstractions`,实现放在对应 runtime 包
## 对应文档
- 抽象接口栏目:[`../docs/zh-CN/abstractions/index.md`](../docs/zh-CN/abstractions/index.md)
- Core 抽象页:[`../docs/zh-CN/abstractions/core-abstractions.md`](../docs/zh-CN/abstractions/core-abstractions.md)
- Core 运行时入口:[`../GFramework.Core/README.md`](../GFramework.Core/README.md)

View File

@ -1,63 +1,95 @@
# GFramework.SourceGenerators # GFramework.Core.SourceGenerators
Core 侧通用源码生成器模块 `GFramework.Core.SourceGenerators` 承载 Core 侧的通用源码生成器与分析器,用来减少样板代码并把部分约束前移到编译期
## Context Get 注入 ## 模块定位
当类本身是上下文感知类型时,可以通过字段特性生成一个手动调用的注入方法: 这个包属于编译期工具链,不是运行时库。
- `[GetService]` 当前仓库中的主要目录:
- `[GetServices]`
- `[GetSystem]` - `Architectures/`
- `[GetSystems]` - `Analyzers/`
- `Bases/`
- `Enums/`
- `Logging/`
- `Rule/`
- `Diagnostics/`
对应的生成器家族主要包括:
- 日志相关生成器
- `ContextAware` 及上下文注入辅助
- 枚举扩展生成器
- 优先级相关生成器
- 模块自动注册
- 注册可见性分析器
## 包关系
- 运行时:`GFramework.Core`
- 契约与特性:`GFramework.Core.SourceGenerators.Abstractions`
- 公共生成器支撑:`GFramework.SourceGenerators.Common`
如果你还需要游戏配置 schema 生成或 Godot 专用生成器,应分别安装:
- `GFramework.Game.SourceGenerators`
- `GFramework.Godot.SourceGenerators`
## 主要能力
### 上下文注入与 `ContextAware`
该包支持围绕上下文感知类型生成辅助代码,例如:
- `[ContextAware]`
- `[GetModel]` - `[GetModel]`
- `[GetModels]` - `[GetModels]`
- `[GetSystem]`
- `[GetSystems]`
- `[GetUtility]` - `[GetUtility]`
- `[GetUtilities]` - `[GetUtilities]`
- `[GetService]`
- `[GetServices]`
- `[GetAll]` - `[GetAll]`
上下文感知类满足以下任一条件即可: 这类生成器适合用于 View、Controller、Godot 节点包装或其他需要频繁访问架构上下文的类型。
- 类上带有 `[ContextAware]` ### 日志辅助
- 继承 `ContextAwareBase`
- 实现 `IContextAware`
生成器会生成 `__InjectContextBindings_Generated()`,需要在合适的生命周期中手动调用。在 Godot 中通常放在 `_Ready()` 支持通过生成器减少 `ILogger` 相关样板代码。
```csharp ### 注册分析器
using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] 包内还包含分析器,用来检查 `Model``System``Utility` 的使用点是否能在所属架构中找到静态可见注册,帮助尽早发现“代码可以编译、运行时却缺注册”的问题。
public partial class InventoryPanel
{
[GetModel]
private IInventoryModel _inventory = null!;
[GetServices] ## 最小接入路径
private IReadOnlyList<IInventoryStrategy> _strategies = null!;
public override void _Ready() ```xml
{ <ItemGroup>
__InjectContextBindings_Generated(); <PackageReference Include="GeWuYou.GFramework.Core.SourceGenerators"
} Version="x.y.z"
} PrivateAssets="all"
ExcludeAssets="runtime" />
</ItemGroup>
``` ```
`[GetAll]` 作用于类本身,会自动扫描字段并推断 `Model``System``Utility` 相关的 `GetX` 调用;已显式标记字段的优先级更高。 如果你想查看生成代码,可在消费者项目里启用:
`Service``Services` 绑定不会在 `[GetAll]` 下自动推断。对于普通引用类型字段,请显式使用 `[GetService]` ```xml
`[GetServices]`,避免将非上下文服务字段误判为服务依赖。 <PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
```
`[GetAll]` 会跳过 `const``static``readonly` 字段。若某个字段本来会被 `[GetAll]` 推断为 ## 适用场景
`Model``System``Utility` 绑定,但因为是不可赋值的 `static``readonly` 字段而被跳过,生成器会发出警告提示该字段不会参与生成。
## 注册分析器 - 你希望减少上下文绑定与日志相关样板代码
- 你需要在编译期发现部分注册可见性问题
- 你在做模块化架构,希望固定某些重复注册模式
包现在同时包含一个注册可见性分析器,用于检查 `Model``System``Utility` 的使用点是否能在所属架构中找到静态可见注册。 ## 对应文档
- 覆盖字段特性注入:`[GetModel]``[GetModels]``[GetSystem]``[GetSystems]``[GetUtility]``[GetUtilities]` - 源码生成器总览:[`../docs/zh-CN/source-generators/index.md`](../docs/zh-CN/source-generators/index.md)
- 覆盖手写调用:`GetModel<T>()``GetModels<T>()``GetSystem<T>()``GetSystems<T>()``GetUtility<T>()``GetUtilities<T>()` - Core 栏目:[`../docs/zh-CN/core/index.md`](../docs/zh-CN/core/index.md)
- 默认报告 `Warning`
- 当前只分析静态可见的注册路径,例如 `OnInitialize()``InstallModules()``InstallModule(new Module())`
对于反射、运行时条件分支、外部程序集动态注册等路径,分析器不会强行推断;当无法唯一确定组件所属架构时,也会选择不报,优先降低误报。

View File

@ -1,32 +1,84 @@
# GFramework.Core # GFramework.Core
GFramework 框架的核心模块提供MVC架构的基础设施 `GFramework.Core` 是框架的基础运行时,负责架构生命周期、组件注册、上下文访问,以及不依赖具体引擎的通用能力
## 主要功能 如果你只想先把框架跑起来,应先从这个模块开始。
- **Architecture** - 应用程序架构管理,支持依赖注入、生命周期管理和模块化扩展 ## 模块定位
- **Model** - 数据模型层,管理应用状态和数据
- **System** - 业务逻辑层,处理核心业务逻辑和事件响应
- **Controller** - 控制器层处理用户输入和UI协调
- **Command** - 命令模式实现,封装用户操作
- **Query** - 查询模式实现支持CQRS架构
- **Events** - 事件系统,实现组件间松耦合通信
- **IoC** - 轻量级依赖注入容器
- **Property** - 可绑定属性,支持数据绑定和响应式编程
- **StateManagement** - 集中式状态容器,支持状态归约、选择器和诊断
- **Utility** - 无状态工具类
- **Pool** - 对象池系统减少GC压力
- **Extensions** - 框架扩展方法
- **Logging** - 日志系统
- **Environment** - 环境配置管理
## 设计原则 这一层提供:
- 与平台解耦,不依赖特定游戏引擎 - `Architecture``ArchitectureContext`
- 接口隔离,职责单一 - `Model` / `System` / `Utility` 运行时
- 依赖倒置,面向接口编程 - 旧版 `Command` / `Query` 执行器
- 组合优于继承 - 事件、属性、状态机、状态管理
- 资源、日志、协程、并发、环境与本地化
## 详细文档 它不负责:
参见 [docs/zh-CN/core/](../docs/zh-CN/core/) 目录下的详细文档。 - 游戏内容配置、Scene / UI / Storage 等游戏层能力
- Godot 节点与场景集成
- 新版 CQRS 请求模型的消息契约定义
## 包关系
- 直接依赖:
- `GFramework.Cqrs`
- `GFramework.Cqrs.Abstractions`
- `GFramework.Core.Abstractions`
- 常见上层模块:
- `GFramework.Game`
- `GFramework.Godot`
如果你只需要契约,不需要实现层,改为依赖 [`../GFramework.Core.Abstractions/README.md`](../GFramework.Core.Abstractions/README.md)。
## 子系统地图
| 目录 | 作用 |
| --- | --- |
| `Architectures/` | 架构入口、上下文、生命周期、模块安装与组件注册 |
| `Command/` | 旧版命令执行器与同步 / 异步命令基类 |
| `Query/` | 旧版查询执行器与同步 / 异步查询基类 |
| `Events/` | 事件总线、事件作用域、统计与过滤 |
| `Property/` | `BindableProperty<T>` 与相关解绑对象 |
| `State/` | 状态机与状态切换事件 |
| `StateManagement/` | Store、selector、middleware 与状态诊断 |
| `Coroutine/` | 协程调度、快照、统计与优先级 |
| `Resource/` | 资源缓存、句柄和释放策略 |
| `Logging/` | logger、factory、配置与组合日志器 |
| `Ioc/` | 基于 `Microsoft.Extensions.DependencyInjection` 的容器适配 |
| `Concurrency/` | 键控异步锁与统计 |
| `Pause/` | 暂停栈和暂停范围 |
| `Localization/` | 本地化表与格式化入口 |
| `Functional/` | `Option``Result` 等轻量函数式工具 |
| `Extensions/` | 上下文与集合等扩展方法 |
## 最小接入路径
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
最小入口:
1. 继承 `Architecture`
2. 在 `OnInitialize()` 中注册模型、系统、工具或模块
3. 通过 `architecture.Context``ContextAwareBase` 的扩展方法访问上下文
最小示例见:
- [`../docs/zh-CN/getting-started/quick-start.md`](../docs/zh-CN/getting-started/quick-start.md)
## 什么时候继续接别的包
- 需要推荐的新请求模型:加 `GFramework.Cqrs`
- 需要游戏层路由、设置、配置和存储:加 `GFramework.Game`
- 需要 Godot 节点与场景适配:加 `GFramework.Godot`
- 需要编译期生成日志、上下文注入或模块注册:加 `GFramework.Core.SourceGenerators`
## 对应文档
- Core 栏目:[`../docs/zh-CN/core/index.md`](../docs/zh-CN/core/index.md)
- CQRS[`../docs/zh-CN/core/cqrs.md`](../docs/zh-CN/core/cqrs.md)
- 入门指南:[`../docs/zh-CN/getting-started/index.md`](../docs/zh-CN/getting-started/index.md)

View File

@ -0,0 +1,101 @@
# GFramework.Cqrs.Abstractions
`GFramework.Cqrs.Abstractions` 提供 GFramework CQRS 的最小契约层。它只包含消息接口、处理器接口、运行时 seam 和管道契约,不包含默认 dispatcher、处理器扫描或任何 `GFramework.Core` 运行时实现。适合以下场景:
- 你的业务程序集只需要声明 Command、Query、Notification、Stream Request 或处理器接口。
- 你希望把消息契约放在更稳定的基础层,避免直接依赖默认 runtime 实现。
- 你要为其他运行时、测试环境或自定义容器实现兼容的 CQRS 接口。
## 模块定位
- 这是 CQRS 的“协议层”。
- 目标框架为 `netstandard2.1`,用于被更上层模块稳定引用。
- 当前包不负责处理器自动注册,也不负责请求分发。
如果你需要默认消息基类、处理器基类、上下文扩展方法和运行时实现,请使用 `GeWuYou.GFramework.Cqrs`
## 包关系
推荐按职责引用:
- `GeWuYou.GFramework.Cqrs.Abstractions`
- 提供 `IRequest<TResponse>``INotification``IStreamRequest<TResponse>``IRequestHandler<,>``INotificationHandler<>``IPipelineBehavior<,>``ICqrsRuntime``ICqrsContext``Unit` 等基础契约。
- `GeWuYou.GFramework.Cqrs`
- 引用本包,并提供默认 runtime、处理器注册、消息基类、处理器基类、上下文扩展方法。
- `GeWuYou.GFramework.Cqrs.SourceGenerators`
- 可选。面向消费端程序集生成 `ICqrsHandlerRegistry` 注册表,减少冷启动反射扫描;未生成或不适用时,运行时仍会回退到反射注册。
## 子系统地图
本包当前可以分为几类契约:
- 消息契约
- `Cqrs/IRequest.cs`
- `Cqrs/INotification.cs`
- `Cqrs/IStreamRequest.cs`
- `Cqrs/Command/ICommand.cs`
- `Cqrs/Query/IQuery.cs`
- `Cqrs/Request/IRequestInput.cs`
- `Cqrs/Command/ICommandInput.cs`
- `Cqrs/Query/IQueryInput.cs`
- `Cqrs/Notification/INotificationInput.cs`
- 处理器契约
- `Cqrs/IRequestHandler.cs`
- `Cqrs/INotificationHandler.cs`
- `Cqrs/IStreamRequestHandler.cs`
- 运行时 seam
- `Cqrs/ICqrsRuntime.cs`
- `Cqrs/ICqrsContext.cs`
- `Cqrs/ICqrsHandlerRegistrar.cs`
- 管道与辅助类型
- `Cqrs/IPipelineBehavior.cs`
- `Cqrs/MessageHandlerDelegate.cs`
- `Cqrs/Unit.cs`
## 最小接入路径
如果你只想在基础层定义一个可被上层 runtime 消费的 Query可以只依赖本包
```bash
dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
```
```csharp
using GFramework.Cqrs.Abstractions.Cqrs.Query;
public sealed record GetPlayerProfileInput(int PlayerId) : IQueryInput;
public sealed class GetPlayerProfileQuery : IQuery<PlayerProfileDto>
{
public GetPlayerProfileQuery(GetPlayerProfileInput input)
{
Input = input;
}
public GetPlayerProfileInput Input { get; }
}
public sealed class GetPlayerProfileHandler
: IRequestHandler<GetPlayerProfileQuery, PlayerProfileDto>
{
public ValueTask<PlayerProfileDto> Handle(
GetPlayerProfileQuery request,
CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
```
这条路径适合“只声明契约”的场景。真正执行分发时,仍需要上层提供 `ICqrsRuntime` 和处理器注册流程,通常由 `GeWuYou.GFramework.Cqrs``GFramework.Core` 完成。
## 使用边界
- 只引用本包时,没有 `CommandBase<TInput, TResponse>``QueryBase<TInput, TResponse>``NotificationBase<TInput>` 等消息基类。
- 只引用本包时,没有 `AbstractCommandHandler``AbstractQueryHandler``AbstractNotificationHandler` 等处理器基类。
- `ICqrsContext` 当前是轻量 marker seam默认 runtime 在需要向 `IContextAware` 处理器注入上下文时,仍要求传入的上下文同时实现 `IArchitectureContext`
## 文档入口
- 运行时与整体接入说明:`docs/zh-CN/core/cqrs.md`
- 如果你需要默认实现而不是契约层,请看 `GFramework.Cqrs/README.md`

View File

@ -0,0 +1,67 @@
# GFramework.Cqrs.SourceGenerators
`GFramework.Cqrs.SourceGenerators` 用于在编译期为当前业务程序集生成 CQRS handler registry减少运行时程序集扫描与反射注册成本。
## 模块定位
这个包是编译期生成器,不是运行时消息或处理器库。
生成器会分析当前业务程序集中的:
- `IRequestHandler<,>`
- `INotificationHandler<>`
- `IStreamRequestHandler<,>`
并生成:
- `ICqrsHandlerRegistry` 实现
- 程序集级 `CqrsHandlerRegistryAttribute`
- 必要时的 `CqrsReflectionFallbackAttribute` 元数据
## 包关系
- 运行时:`GFramework.Cqrs`
- 契约层:`GFramework.Cqrs.Abstractions`
- 生成器:`GFramework.Cqrs.SourceGenerators`
不安装这个包也可以正常使用 CQRS区别只在于运行时会更多依赖反射扫描注册 handlers。
## 当前代码入口
仓库内该包的主要实现位于:
- `Cqrs/CqrsHandlerRegistryGenerator.cs`
它会在可以安全生成静态注册器时前移注册工作;对无法由生成代码直接引用的 handler则通过 reflection fallback 元数据让运行时做定向补扫,而不是整程序集盲扫。
## 最小接入路径
```xml
<ItemGroup>
<PackageReference Include="GeWuYou.GFramework.Cqrs" Version="x.y.z" />
<PackageReference Include="GeWuYou.GFramework.Cqrs.Abstractions" Version="x.y.z" />
<PackageReference Include="GeWuYou.GFramework.Cqrs.SourceGenerators"
Version="x.y.z"
PrivateAssets="all"
ExcludeAssets="runtime" />
</ItemGroup>
```
运行时侧仍按正常方式注册程序集:
```csharp
RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly);
```
安装生成器后,运行时会优先走生成的 registry无法静态表达的部分再走定向回退。
## 什么时候值得安装
- 你的业务程序集里 handler 数量较多
- 你希望缩小冷启动时的反射扫描范围
- 你需要把 handler 注册路径收束到编译期并保持可诊断
## 对应文档
- CQRS 栏目:[`../docs/zh-CN/core/cqrs.md`](../docs/zh-CN/core/cqrs.md)
- 源码生成器总览:[`../docs/zh-CN/source-generators/index.md`](../docs/zh-CN/source-generators/index.md)

154
GFramework.Cqrs/README.md Normal file
View File

@ -0,0 +1,154 @@
# GFramework.Cqrs
`GFramework.Cqrs` 是 GFramework 的默认 CQRS runtime 包。它在 `GFramework.Cqrs.Abstractions` 之上提供请求分发、通知发布、流式请求处理、处理器注册、上下文扩展方法,以及消息/处理器基类,面向直接使用 GFramework CQRS 的业务模块。
## 模块定位
- 这是 CQRS 的“默认实现层”。
- 包内同时承载运行时基础设施和面向业务代码的便捷基类。
- 它依赖 `GFramework.Cqrs.Abstractions``GFramework.Core.Abstractions`
- 在标准 GFramework 架构启动路径中,`GFramework.Core``CqrsRuntimeModule` 会把默认 runtime、处理器注册器与注册服务自动接入容器。
如果你只需要声明跨模块共享的消息契约,而不想依赖默认 runtime请改为引用 `GeWuYou.GFramework.Cqrs.Abstractions`
## 包关系
推荐的依赖关系如下:
- `GeWuYou.GFramework.Cqrs.Abstractions`
- 最小 CQRS 契约层。
- `GeWuYou.GFramework.Cqrs`
- 默认 runtime 与业务侧常用基类。
- `GeWuYou.GFramework.Cqrs.SourceGenerators`
- 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;缺失或不适用时,回退到反射扫描。
- `GFramework.Core`
- 架构上下文中实际调用 `ICqrsRuntime`,并在模块初始化时注册 CQRS 基础设施。
## 子系统地图
本包当前主要由以下子系统组成:
- 消息基类
- `Command/CommandBase.cs`
- `Query/QueryBase.cs`
- `Request/RequestBase.cs`
- `Notification/NotificationBase.cs`
- 处理器基类
- `Cqrs/CqrsContextAwareHandlerBase.cs`
- `Cqrs/Command/AbstractCommandHandler.cs`
- `Cqrs/Query/AbstractQueryHandler.cs`
- `Cqrs/Notification/AbstractNotificationHandler.cs`
- `Cqrs/Request/AbstractRequestHandler.cs`
- `Cqrs/Request/AbstractStreamRequestHandler.cs`
- `Cqrs/Command/AbstractStreamCommandHandler.cs`
- `Cqrs/Query/AbstractStreamQueryHandler.cs`
- 请求管道
- `Cqrs/Behaviors/LoggingBehavior.cs`
- `Cqrs/Behaviors/PerformanceBehavior.cs`
- 管道契约定义在 `GFramework.Cqrs.Abstractions``IPipelineBehavior<,>`
- 默认 runtime 与注册入口
- `CqrsRuntimeFactory.cs`
- `Internal/CqrsDispatcher.cs`
- `Internal/CqrsHandlerRegistrar.cs`
- `Internal/DefaultCqrsHandlerRegistrar.cs`
- `Internal/DefaultCqrsRegistrationService.cs`
- 生成注册表协作接口
- `ICqrsHandlerRegistry.cs`
- `CqrsHandlerRegistryAttribute.cs`
- `CqrsReflectionFallbackAttribute.cs`
- 业务侧扩展入口
- `Extensions/ContextAwareCqrsExtensions.cs`
- `Extensions/ContextAwareCqrsCommandExtensions.cs`
- `Extensions/ContextAwareCqrsQueryExtensions.cs`
## 最小接入路径
在标准 GFramework 架构中,最小接入通常是“安装包 + 定义消息 + 定义处理器 + 通过上下文发送”:
```bash
dotnet add package GeWuYou.GFramework.Cqrs
dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
```
如果你希望减少处理器注册时的反射扫描,再额外安装:
```bash
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
```
示例:
```csharp
using GFramework.Cqrs.Command;
using GFramework.Cqrs.Cqrs.Command;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
public sealed record CreatePlayerInput(string Name) : ICommandInput;
public sealed class CreatePlayerCommand : CommandBase<CreatePlayerInput, int>
{
public CreatePlayerCommand(CreatePlayerInput input) : base(input)
{
}
}
public sealed class CreatePlayerCommandHandler
: AbstractCommandHandler<CreatePlayerCommand, int>
{
public override ValueTask<int> Handle(
CreatePlayerCommand command,
CancellationToken cancellationToken)
{
var playerModel = Context.GetModel<PlayerModel>();
var playerId = playerModel.Create(command.Input.Name);
return ValueTask.FromResult(playerId);
}
}
```
`IContextAware` 对象内发送命令:
```csharp
using GFramework.Cqrs.Extensions;
var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInput("Alice")));
```
`ArchitectureContext` 上也可以直接使用统一 CQRS 入口,例如 `SendRequestAsync``SendQueryAsync``PublishAsync``CreateStream`
## 运行时行为
- 请求分发
- `CqrsDispatcher` 按请求实际类型解析 `IRequestHandler<,>`,未找到处理器会抛出异常。
- 通知分发
- 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。
- 流式请求
- 通过 `IStreamRequest<TResponse>``IStreamRequestHandler<,>` 返回 `IAsyncEnumerable<TResponse>`
- 上下文注入
- 处理器基类继承 `CqrsContextAwareHandlerBase`runtime 会在分发前注入当前 `IArchitectureContext`
- 如果处理器或行为需要上下文注入,而当前 `ICqrsContext` 不是 `IArchitectureContext`,默认实现会抛出异常。
- 管道行为
- 所有已注册 `IPipelineBehavior<TRequest, TResponse>` 会包裹请求处理器执行。
- 当前包内提供了 `LoggingBehavior``PerformanceBehavior` 两个可复用行为。
## 处理器注册与程序集接入
默认注册流程由 `ICqrsRegistrationService.RegisterHandlers(IEnumerable<Assembly>)` 协调,语义是:
- 同一程序集按稳定键去重,避免重复注册。
- 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。
- 生成注册器不可用,或声明了 `CqrsReflectionFallbackAttribute` 时,回退到反射扫描。
- 处理器以 transient 方式注册,避免上下文感知处理器在并发请求间共享可变上下文。
如果你走标准 `GFramework.Core` 架构初始化路径,这些步骤通常由框架自动完成;裸容器或测试环境则需要显式补齐 runtime 与注册入口。
## 适用边界
- 这个包是默认实现,不是“纯契约包”。
- 处理器基类依赖 runtime 在分发前注入上下文,不适合脱离 dispatcher 直接手动实例化后调用。
- README 中的消息基类和 handler 基类位于 `GFramework.Cqrs`,接口契约位于 `GFramework.Cqrs.Abstractions`;最小示例通常需要同时引入这两个命名空间层级。
## 文档入口
- 总体文档:`docs/zh-CN/core/cqrs.md`
- 契约层说明:`GFramework.Cqrs.Abstractions/README.md`

View File

@ -1,13 +1,240 @@
# GFramework.Game.Abstractions # GFramework.Game.Abstractions
GFramework.Game 模块的抽象层,提供游戏业务相关的接口定义 `GFramework.Game.Abstractions``GFramework` 游戏层的契约包
## 主要内容 它建立在 `GFramework.Core.Abstractions` 之上,只定义接口、枚举、路由上下文、事件契约和少量可直接复用的数据类型,不提供完整运行时实现。它的主要用途是把游戏业务层、引擎适配层、公共 feature 包与具体实现解耦。
- 游戏业务常用抽象接口 如果你需要可直接运行的默认实现,请改为依赖 `GFramework.Game`
- 与 GFramework.Core.Abstractions 配合使用的契约
## 使用建议 ## 包定位
- 若需直接使用完整游戏扩展能力,优先使用 GFramework.Game - 为游戏相关子系统提供稳定契约,而不是默认实现。
- 若在做模块拆分或需要解耦,可单独依赖本包 - 适合被多个程序集共同引用,避免公共业务层直接依赖具体运行时。
- 典型使用场景:
- 定义 `IScene``IUiPage``ISettingsData``IData` 等业务对象
- 让 feature 包只感知 `IConfigRegistry``ISaveRepository<T>``ISettingsModel``IUiRouter``ISceneRouter`
- 在引擎适配层之外共享设置、场景参数、UI 参数、存档数据类型
## 与相邻包的关系
- `GFramework.Core.Abstractions`
- 本包直接依赖它。
- 提供 `ISystem``IModel``IUtility`、上下文 utility 等底层抽象。
- `GFramework.Game`
- 本包的主要实现层。
- `FileStorage``ScopedStorage``JsonSerializer``SettingsModel<TRepository>``SaveRepository<TSaveData>``SceneRouterBase``UiRouterBase``YamlConfigLoader` 等都在实现这里的契约。
- 引擎适配包或项目代码
- `IUiFactory``ISceneFactory``IUiRoot``ISceneRoot`、资源注册表等通常由引擎适配层或游戏项目自己实现。
- CoreGrid 的真实结构也是这样:页面/场景 factory、root、registry 在项目层,运行时基类和契约来自 `GFramework.Game` 与本包。
## 子系统地图
### `Config/`
静态内容配置的读取侧契约。
- `IConfigLoader`
- `IConfigRegistry`
- `IConfigTable`
- `ConfigLoadException`
- `ConfigLoadDiagnostic`
- `ConfigLoadFailureKind`
这一层只描述“如何注册与访问配置表”,不关心底层来自 YAML、二进制还是远程源。
### `Data/`
可写数据与存档契约。
- `IData`
- `IVersionedData`
- `IDataLocation`
- `IDataLocationProvider`
- `IDataRepository`
- `ISettingsDataRepository`
- `ISaveRepository<TSaveData>`
- `ISaveMigration<TSaveData>`
- `DataRepositoryOptions`
- `Data/Events/*`
这一层让业务代码不需要知道数据最终按“单文件一项”还是“统一文件多 section”持久化。
### `Setting/`
设置系统契约。
- `ISettingsData`
- `IResetApplyAbleSettings`
- `ISettingsModel`
- `ISettingsSystem`
- `ISettingsMigration`
- `ISettingsChangedEvent`
- `Setting/Data/*`
- 内置了 `AudioSettings``GraphicsSettings``LocalizationSettings` 三类常见设置数据
这里定义的是“设置生命周期和应用语义”,不限定具体引擎。
### `Scene/`
场景导航契约。
- `IScene`
- `ISceneBehavior`
- `ISceneFactory`
- `ISceneRoot`
- `ISceneRouter`
- `ISceneTransitionHandler`
- `ISceneAroundTransitionHandler`
- `ISceneRouteGuard`
- `IGameSceneRegistry<T>`
如果你想把场景定义放在公共业务层,通常依赖本包就够了。
### `UI/`
UI 页面与路由契约。
- `IUiPage`
- `IUiPageBehavior`
- `IUiFactory`
- `IUiRoot`
- `IUiRouter`
- `IUiTransitionHandler`
- `IUiAroundTransitionHandler`
- `IUiRouteGuard`
- `UiHandle`
- `UiTransitionHandlerOptions`
- `UiInteractionProfile`
`IUiRouter` 不只覆盖页面栈,还覆盖 Overlay / Modal / Toast / Topmost 等层级 UI 语义。
### `Routing/`
- `IRoute`
- `IRouteContext`
- `IRouteGuard<TRoute>`
Scene 与 UI 路由共享这套基础约定。
### `Storage/`
- `IFileStorage`
- `IScopedStorage`
它们继承自核心层的 `IStorage`,用于表达“文件存储实现”和“带作用域前缀的存储实现”这两个角色。
### `Asset/``Enums/`
- `Asset/`
- 资源注册表契约,如 `IAssetRegistry<T>`
- `Enums/`
- UI/Scene 转场、UI 层级、输入动作、存储类型等公共枚举
## 最小接入路径
### 1. 只想在公共业务层声明游戏对象
直接依赖本包,定义业务数据和交互参数:
```csharp
using GFramework.Game.Abstractions.Data;
using GFramework.Game.Abstractions.Scene;
using GFramework.Game.Abstractions.UI;
public sealed class GameSaveData : IVersionedData
{
public int Version { get; set; } = 1;
public DateTime LastModified { get; set; } = DateTime.UtcNow;
}
public sealed class GameplayEnterParam : ISceneEnterParam
{
public required string Seed { get; init; }
}
public sealed class PauseMenuParam : IUiPageEnterParam
{
public bool AllowRestart { get; init; }
}
```
这个阶段不需要把 `GFramework.Game` 一起带进来。
### 2. 只想让 feature 包依赖抽象,不绑定具体实现
直接面向接口编程:
```csharp
public sealed class ContinueGameCommandHandler
{
private readonly ISaveRepository<GameSaveData> _saveRepository;
private readonly ISceneRouter _sceneRouter;
public ContinueGameCommandHandler(
ISaveRepository<GameSaveData> saveRepository,
ISceneRouter sceneRouter)
{
_saveRepository = saveRepository;
_sceneRouter = sceneRouter;
}
}
```
这样 feature 包不必知道底层到底是 `SaveRepository<TSaveData>`、Godot 适配层,还是你自己的实现。
### 3. 什么时候要升级到 `GFramework.Game`
一旦你需要下面任一项,就不该只停留在本包:
- 默认的 JSON 序列化实现
- 文件系统存储实现
- 设置模型与系统实现
- 槽位存档仓库实现
- YAML 配置加载器
- Scene / UI 路由基类
也就是说,本包回答的是“项目各层如何约定”,`GFramework.Game` 回答的是“这些约定默认怎么跑起来”。
## CoreGrid 里的真实用法线索
CoreGrid 对本包的使用方式,能比较清楚地说明它的职责边界:
- 公共脚本广泛引用:
- `IUiRouter`
- `ISceneRouter`
- `ISettingsModel`
- `ISettingsSystem`
- `ISaveRepository<GameSaveData>`
- `IConfigRegistry`
- 业务数据和参数实现引用:
- `IData` / `IVersionedData`
- `ISceneEnterParam`
- `IUiPageEnterParam`
- 真正的实现和装配则放在:
- `GFramework.Game`
- `GFramework.Godot.*`
- CoreGrid 自己的模块、factory、root、registry
这正是本包的设计目标:让业务层依赖稳定契约,而不是依赖具体运行时细节。
## 对应文档入口
虽然大部分详细文档写在 `GFramework.Game` 侧,但阅读顺序仍然适用于本包:
- 游戏模块总览:[docs/zh-CN/game/index.md](../docs/zh-CN/game/index.md)
- 内容配置系统:[docs/zh-CN/game/config-system.md](../docs/zh-CN/game/config-system.md)
- 数据与存档:[docs/zh-CN/game/data.md](../docs/zh-CN/game/data.md)
- 设置系统:[docs/zh-CN/game/setting.md](../docs/zh-CN/game/setting.md)
- 存储系统:[docs/zh-CN/game/storage.md](../docs/zh-CN/game/storage.md)
- 序列化系统:[docs/zh-CN/game/serialization.md](../docs/zh-CN/game/serialization.md)
- 场景系统:[docs/zh-CN/game/scene.md](../docs/zh-CN/game/scene.md)
- UI 系统:[docs/zh-CN/game/ui.md](../docs/zh-CN/game/ui.md)
## 选择建议
- 选 `GFramework.Game.Abstractions`
- 你在写共享业务层、公共 feature 包、纯契约层
- 选 `GFramework.Game`
- 你需要默认实现、基础设施拼装、运行时启动入口
- 两者一起用
- 最常见。公共层依赖 abstractions应用层或引擎层依赖 runtime

View File

@ -0,0 +1,70 @@
# GFramework.Game.SourceGenerators
`GFramework.Game.SourceGenerators` 负责把 `schemas/**/*.schema.json` 转成游戏内容配置类型和表包装代码。
它服务的核心场景是:让 `YAML` 配置、`JSON Schema`、运行时加载器和工具链共享一套结构定义。
## 模块定位
这个包是编译期生成器,不是运行时库。
它会在编译期读取 schema并生成
- 配置数据类型
- 对应的表包装类型
- 与 `GFramework.Game.Config` 运行时协作的访问辅助代码
## 包关系
- 运行时:`GFramework.Game`
- 生成器:`GFramework.Game.SourceGenerators`
- 公共生成器支撑:`GFramework.SourceGenerators.Common`
如果你的项目还会使用 `[Log]``[ContextAware]` 或 Core 侧上下文注入特性,还需要同时安装
`GFramework.Core.SourceGenerators`
## 目录与输入约定
当前项目结构显示该生成器主要围绕以下代码组织:
- `Config/SchemaConfigGenerator.cs`
- `Diagnostics/ConfigSchemaDiagnostics.cs`
- `GeWuYou.GFramework.Game.SourceGenerators.targets`
消费者项目的推荐目录约定:
```text
GameProject/
├─ config/
│ └─ monster/
│ └─ slime.yaml
└─ schemas/
└─ monster.schema.json
```
默认情况下,打包产物会通过 `targets``schemas/**/*.schema.json` 纳入 `AdditionalFiles`
## 最小接入路径
```xml
<ItemGroup>
<PackageReference Include="GeWuYou.GFramework.Game" Version="x.y.z" />
<PackageReference Include="GeWuYou.GFramework.Game.SourceGenerators"
Version="x.y.z"
PrivateAssets="all"
ExcludeAssets="runtime" />
</ItemGroup>
```
如果你在仓库内用 `ProjectReference` 调试,仍需要把对应 `targets` 接进消费者项目。
## 什么时候使用它
- 你想把静态游戏内容维护成 `YAML`
- 你希望在编译期拿到强类型配置访问入口
- 你希望运行时加载、schema 校验和编辑工具链共用同一份结构定义
## 对应文档
- 配置系统:[`../docs/zh-CN/game/config-system.md`](../docs/zh-CN/game/config-system.md)
- 源码生成器总览:[`../docs/zh-CN/source-generators/index.md`](../docs/zh-CN/source-generators/index.md)

View File

@ -1,16 +1,344 @@
# GFramework.Game # GFramework.Game
GFramework 框架的游戏通用模块,提供游戏开发常用的功能 `GFramework.Game``GFramework` 面向游戏项目的运行时层
## 主要功能 它建立在 `GFramework.Core``GFramework.Game.Abstractions` 之上提供可直接落地的实现与基类覆盖静态内容配置、数据与存档、设置、场景路由、UI 路由、序列化、文件存储、状态机扩展等常见游戏运行时需求。
- **Settings** - 游戏设置系统,支持设置分类和配置应用 如果你的项目只需要“契约”而不想带入实现,请依赖 `GFramework.Game.Abstractions`。如果你的项目需要真正可运行的默认实现或基类,请依赖本包。
## 依赖关系 ## 包定位
- 依赖 GFramework.Core - 面向使用者的默认运行时实现层,不是纯接口包。
- 依赖 GFramework.Core.Abstractions - 适合作为游戏项目启动层、基础设施层、引擎适配层之上的通用运行时底座。
- 当前最成熟、最明确的能力集中在:
- 静态内容配置:`Config/`
- 数据与存档:`Data/`
- 设置系统:`Setting/`
- 场景与 UI 路由基类:`Scene/``UI/`
- 序列化与文件存储:`Serializer/``Storage/`
## 详细文档 ## 与相邻包的关系
参见 [docs/zh-CN/game/](../docs/zh-CN/game/) 目录下的详细文档。 - `GFramework.Core`
- 本包直接依赖它。
- 提供架构、上下文注入、事件、日志、系统/模型/utility 生命周期等底层能力。
- `GFramework.Game.Abstractions`
- 本包的上游契约层。
- 本包中的 `FileStorage``SettingsModel<TRepository>``SaveRepository<TSaveData>``YamlConfigLoader``SceneRouterBase``UiRouterBase` 等都在实现这里定义的接口。
- `GFramework.Game.SourceGenerators`
- 主要与配置系统配合使用。
- 当你需要 schema 驱动的 YAML 配表时,运行时包是 `GFramework.Game`,生成时代码由 source generators 补齐。
- 引擎适配包或项目内适配层
- 本包提供的是“引擎无关”的核心逻辑和基类。
- 真正和 Godot、Unity、MonoGame 等引擎对象打交道的工厂、根节点、资源注册表,通常在相邻引擎包或游戏项目内实现。
- CoreGrid 的真实接法就是这样:配置文件 IO 由 `GFramework.Godot.Config` 适配UI/Scene factory 与 root 由项目自己提供。
## 子系统地图
### `Config/`
面向静态游戏内容的只读配置系统。
- `YamlConfigLoader`
- 从文件系统根目录加载 YAML 配置表,并注册到 `IConfigRegistry`
- `ConfigRegistry`
- 运行时配置表注册表
- `GameConfigBootstrap`
- 非 `Architecture` 场景下的官方配置启动入口
- `GameConfigModule`
- `Architecture` 场景下的官方配置接入模块
- `InMemoryConfigTable<TKey, TValue>`
- 配置表默认只读承载
- `YamlConfigHotReloadOptions`
- 开发期热重载控制
这个子系统通常与 `GFramework.Game.SourceGenerators` 一起使用,而不是手写大量注册代码。
对应文档:
- [docs/zh-CN/game/config-system.md](../docs/zh-CN/game/config-system.md)
- [docs/zh-CN/game/index.md](../docs/zh-CN/game/index.md)
### `Data/`
面向可写业务数据、设置持久化与存档槽位的仓库实现。
- `DataRepository`
- 一条 `IDataLocation` 对应一份持久化对象
- `UnifiedSettingsDataRepository`
- 把多个设置 section 聚合到单一文件中
- `SaveRepository<TSaveData>`
- 面向槽位存档,支持版本迁移链
- `SaveConfiguration`
- 槽位目录、文件名、前缀等约定
CoreGrid 的真实用法:
- 设置持久化使用 `UnifiedSettingsDataRepository`
- 存档使用 `SaveRepository<GameSaveData>`
- 两者共用同一个底层存储 utility
对应文档:
- [docs/zh-CN/game/data.md](../docs/zh-CN/game/data.md)
- [docs/zh-CN/game/setting.md](../docs/zh-CN/game/setting.md)
### `Setting/`
设置生命周期编排层。
- `SettingsModel<TRepository>`
- 管理 `ISettingsData` 实例、迁移、加载、保存、重置
- 编排 applicator 的 `Apply`
- `SettingsSystem`
- 面向业务代码暴露更直接的系统级入口
- `Setting/Events/*`
- 设置初始化、应用、保存、重置相关事件
CoreGrid 的真实用法:
- 在模型模块中创建 `SettingsModel<ISettingsDataRepository>`
- 注册多个 applicator
- 启动时先 `InitializeAsync()`,再 `ApplyAll()`
- 退出时统一 `SaveAll()`
对应文档:
- [docs/zh-CN/game/setting.md](../docs/zh-CN/game/setting.md)
### `Storage/`
面向本地文件系统的基础存储。
- `FileStorage`
- 基于目录与文件的 `IStorage` 实现
- 负责路径清洗、细粒度锁、原子写入、层级 key 到目录结构的映射
- `ScopedStorage`
- 为底层存储增加前缀作用域
这部分能力经常被 `DataRepository``SaveRepository<TSaveData>``UnifiedSettingsDataRepository` 复用。
对应文档:
- [docs/zh-CN/game/storage.md](../docs/zh-CN/game/storage.md)
- [GFramework.Game/Storage/ReadMe.md](./Storage/ReadMe.md)
### `Serializer/`
- `JsonSerializer`
- 当前默认序列化实现
- 同时可作为 `ISerializer``IRuntimeTypeSerializer`
它通常先于存储和数据仓库被注册。
对应文档:
- [docs/zh-CN/game/serialization.md](../docs/zh-CN/game/serialization.md)
### `Scene/``UI/`
面向游戏导航的可复用基类,不直接绑定具体引擎。
- `SceneRouterBase`
- 依赖 `ISceneFactory``ISceneRoot`
- 提供栈式场景路由与转换处理管道
- `UiRouterBase`
- 依赖 `IUiFactory``IUiRoot`
- 提供页面栈、Overlay/Modal/Toast 等层级 UI、输入动作分发、暂停联动
- `Scene/Handler/*``UI/Handler/*`
- 默认转换处理器基类与日志处理器
CoreGrid 的真实用法:
- 项目自定义 `SceneRouter : SceneRouterBase`
- 项目自定义 `UiRouter : UiRouterBase`
- 工厂、注册表、root 都由项目或引擎适配层提供
对应文档:
- [docs/zh-CN/game/scene.md](../docs/zh-CN/game/scene.md)
- [docs/zh-CN/game/ui.md](../docs/zh-CN/game/ui.md)
### `Routing/``State/`
- `Routing/RouterBase<TRoute, TContext>`
- Scene/UI 路由共享基类
- `State/GameStateMachineSystem`
- 对核心状态机系统的游戏向封装
这两部分一般被上层子系统消费,不是多数项目的第一接入点。
## 最小接入路径
下面按最常见的四种接入目标给出最短路径。
### 1. 只想先拿到文件存储
```csharp
using GFramework.Core.Abstractions.Serializer;
using GFramework.Core.Abstractions.Storage;
using GFramework.Game.Serializer;
using GFramework.Game.Storage;
ISerializer serializer = new JsonSerializer();
IStorage storage = new FileStorage("GameData", serializer);
await storage.WriteAsync("player/profile", new { Name = "Alice", Level = 3 });
```
如果你需要逻辑隔离,再包一层 `ScopedStorage`
```csharp
var settingsStorage = new ScopedStorage(storage, "settings");
```
### 2. 接入设置和存档
运行时最小拼装顺序通常是:
1. 注册 `JsonSerializer`
2. 注册一个 `IStorage` 实现
3. 注册 `ISettingsDataRepository`
4. 创建并注册 `SettingsModel<ISettingsDataRepository>`
5. 注册 applicator
6. 注册 `SettingsSystem`
7. 注册 `ISaveRepository<TSaveData>`
示意代码:
```csharp
var serializer = new JsonSerializer();
var storage = new FileStorage("GameData", serializer);
architecture.RegisterUtility(serializer);
architecture.RegisterUtility<IStorage>(storage);
architecture.RegisterUtility<ISettingsDataRepository>(
new UnifiedSettingsDataRepository(
storage,
serializer,
new DataRepositoryOptions { BasePath = "settings", AutoBackup = true }));
architecture.RegisterModel(
new SettingsModel<ISettingsDataRepository>(
new MySettingsLocationProvider(),
architecture.Context.GetUtility<ISettingsDataRepository>())
.RegisterApplicator(new MyAudioSettingsApplicator()));
architecture.RegisterSystem<ISettingsSystem>(new SettingsSystem());
architecture.RegisterUtility<ISaveRepository<MySaveData>>(
new SaveRepository<MySaveData>(
storage,
new SaveConfiguration
{
SaveRoot = "saves",
SaveSlotPrefix = "slot_",
SaveFileName = "save.json"
}));
```
启动时:
```csharp
await settingsModel.InitializeAsync();
await settingsSystem.ApplyAll();
```
退出前:
```csharp
await settingsSystem.SaveAll();
```
CoreGrid 目前就是按这个思路接入,只是底层存储换成了 Godot 适配实现。
### 3. 接入静态 YAML 配置
如果你不走 `Architecture` 生命周期,直接使用 `GameConfigBootstrap`
```csharp
var bootstrap = new GameConfigBootstrap(
new GameConfigBootstrapOptions
{
RootPath = contentRootPath,
ConfigureLoader = static loader =>
{
loader.RegisterAllGeneratedConfigTables();
}
});
await bootstrap.InitializeAsync();
var registry = bootstrap.Registry;
```
如果你走 `Architecture`,优先使用 `GameConfigModule`,并在较早阶段安装。
这一能力几乎总是与 source generators 绑定使用。目录、schema、生成器与热重载约定请直接看
- [docs/zh-CN/game/config-system.md](../docs/zh-CN/game/config-system.md)
### 4. 接入 Scene / UI 路由
这里的最小前提不是“直接 new 一个 router”而是先补齐运行时依赖
- `ISceneFactory` / `IUiFactory`
- `ISceneRoot` / `IUiRoot`
- 具体页面或场景行为实现
然后让项目自己的 router 继承基类:
```csharp
public sealed class MySceneRouter : SceneRouterBase
{
protected override void RegisterHandlers()
{
RegisterHandler(new LoggingTransitionHandler());
}
}
public sealed class MyUiRouter : UiRouterBase
{
protected override void RegisterHandlers()
{
RegisterHandler(new GFramework.Game.UI.Handler.LoggingTransitionHandler());
}
}
```
这类 router 适合作为你的项目层或引擎适配层代码,而不是直接修改本包。
## CoreGrid 里的真实用法线索
当前仓库内CoreGrid 对本包的使用大致分成三层:
- 配置
- `CoreGridConfigHost` 使用生成表元数据与 YAML loader 完成配置注册
- 设置与存档
- `UtilityModule` 注册序列化器、底层存储、`UnifiedSettingsDataRepository``SaveRepository<GameSaveData>`
- `ModelModule` 创建 `SettingsModel<ISettingsDataRepository>` 并注册 applicator
- 路由
- `SceneRouter` 继承 `SceneRouterBase`
- `UiRouter` 继承 `UiRouterBase`
这说明本包更适合做“游戏基础设施层”,而不是把所有引擎对象耦死在包内部。
## 文档入口
- 游戏模块总览:[docs/zh-CN/game/index.md](../docs/zh-CN/game/index.md)
- 内容配置系统:[docs/zh-CN/game/config-system.md](../docs/zh-CN/game/config-system.md)
- 数据与存档:[docs/zh-CN/game/data.md](../docs/zh-CN/game/data.md)
- 设置系统:[docs/zh-CN/game/setting.md](../docs/zh-CN/game/setting.md)
- 存储系统:[docs/zh-CN/game/storage.md](../docs/zh-CN/game/storage.md)
- 序列化系统:[docs/zh-CN/game/serialization.md](../docs/zh-CN/game/serialization.md)
- 场景系统:[docs/zh-CN/game/scene.md](../docs/zh-CN/game/scene.md)
- UI 系统:[docs/zh-CN/game/ui.md](../docs/zh-CN/game/ui.md)
## 什么时候不该直接依赖本包
以下场景优先考虑只依赖 `GFramework.Game.Abstractions`
- 你在做纯领域层、协议层或可复用 feature 包,只想引用接口和数据契约
- 你已经有自己的配置、存储、路由实现,只想复用统一契约
- 你不希望业务程序集带入 `Newtonsoft.Json``YamlDotNet` 等运行时依赖

View File

@ -22,3 +22,4 @@ global using System.Globalization;
global using System.IO; global using System.IO;
global using System.Text; global using System.Text;
global using System.Text.Json; global using System.Text.Json;
global using GFramework.Godot.Text;

View File

@ -0,0 +1,29 @@
namespace GFramework.Godot.Tests.Text;
/// <summary>
/// <see cref="RichTextEffectPlan" /> 的纯托管测试。
/// </summary>
[TestFixture]
public sealed class RichTextEffectPlanTests
{
/// <summary>
/// 验证默认内置计划会暴露完整的第一阶段效果键集合。
/// </summary>
[Test]
public void CreateBuiltInDefault_Should_Contain_The_First_Phase_Effect_Keys()
{
var plan = RichTextEffectPlan.CreateBuiltInDefault();
Assert.That(plan.Effects.Select(static entry => entry.Key), Is.EqualTo(new[]
{
"green",
"red",
"gold",
"blue",
"fade_in",
"sine",
"jitter",
"fly_in"
}));
}
}

View File

@ -0,0 +1,129 @@
namespace GFramework.Godot.Tests.Text;
/// <summary>
/// <see cref="RichTextEffectsController" /> 的纯托管行为测试。
/// </summary>
[TestFixture]
public sealed class RichTextEffectsControllerTests
{
/// <summary>
/// 验证启用框架效果时会开启宿主 BBCode并在 Profile 为空时回退到内置默认配置。
/// </summary>
[Test]
public void RefreshEffects_Should_Enable_Bbcode_And_Use_BuiltIn_Default_Profile()
{
var host = new FakeRichTextEffectHost();
var controller = new RichTextEffectsController(
host,
() => null,
() => true,
() => false);
controller.RefreshEffects();
Assert.That(host.BbcodeEnabled, Is.True);
Assert.That(host.CapturedAnimatedEffectsEnabled, Has.Count.EqualTo(1));
Assert.That(host.CapturedAnimatedEffectsEnabled[0], Is.False);
Assert.That(host.CapturedProfiles, Has.Count.EqualTo(1));
Assert.That(host.CapturedProfiles[0].Effects.Select(static entry => entry.Key), Is.EqualTo(new[]
{
"green",
"red",
"gold",
"blue",
"fade_in",
"sine",
"jitter",
"fly_in"
}));
}
/// <summary>
/// 验证关闭框架效果时不会触发新的效果安装,并会清空宿主的自定义效果集合。
/// </summary>
[Test]
public void RefreshEffects_Should_Clear_CustomEffects_When_Framework_Effects_Are_Disabled()
{
var host = new FakeRichTextEffectHost
{
BbcodeEnabled = true
};
host.SimulateInstalledEffects();
var controller = new RichTextEffectsController(
host,
() => RichTextEffectPlan.CreateBuiltInDefault(),
() => false,
() => true);
controller.RefreshEffects();
Assert.That(host.BbcodeEnabled, Is.True);
Assert.That(host.CustomEffectsInstalled, Is.False);
Assert.That(host.ClearCustomEffectsCallCount, Is.EqualTo(1));
Assert.That(host.CapturedProfiles, Is.Empty);
}
/// <summary>
/// 验证控制器会在每次刷新时读取最新的配置访问器结果,避免缓存旧配置。
/// </summary>
[Test]
public void RefreshEffects_Should_Use_The_Current_Profile_From_Accessor()
{
var host = new FakeRichTextEffectHost();
var firstProfile = new RichTextEffectPlan(
[
new RichTextEffectPlanEntry("green")
]);
var secondProfile = new RichTextEffectPlan(
[
new RichTextEffectPlanEntry("gold")
]);
RichTextEffectPlan? currentProfile = firstProfile;
var controller = new RichTextEffectsController(
host,
() => currentProfile,
() => true,
() => true);
controller.RefreshEffects();
currentProfile = secondProfile;
controller.RefreshEffects();
Assert.That(host.CapturedProfiles, Has.Count.EqualTo(2));
Assert.That(host.CapturedProfiles[0], Is.SameAs(firstProfile));
Assert.That(host.CapturedProfiles[1], Is.SameAs(secondProfile));
}
private sealed class FakeRichTextEffectHost : IRichTextEffectHost
{
public List<RichTextEffectPlan> CapturedProfiles { get; } = [];
public List<bool> CapturedAnimatedEffectsEnabled { get; } = [];
public bool CustomEffectsInstalled { get; private set; }
public int ClearCustomEffectsCallCount { get; private set; }
public bool BbcodeEnabled { get; set; }
public void ApplyEffects(RichTextEffectPlan profile, bool animatedEffectsEnabled)
{
ArgumentNullException.ThrowIfNull(profile);
CapturedProfiles.Add(profile);
CapturedAnimatedEffectsEnabled.Add(animatedEffectsEnabled);
CustomEffectsInstalled = true;
}
public void ClearCustomEffects()
{
ClearCustomEffectsCallCount++;
CustomEffectsInstalled = false;
}
public void SimulateInstalledEffects()
{
CustomEffectsInstalled = true;
}
}
}

View File

@ -0,0 +1,65 @@
using GFramework.Godot.Text;
namespace GFramework.Godot.Tests.Text;
/// <summary>
/// <see cref="RichTextMarkup" /> 的测试。
/// </summary>
[TestFixture]
public sealed class RichTextMarkupTests
{
/// <summary>
/// 验证颜色快捷方法会输出预期标签。
/// </summary>
[Test]
public void Green_Should_Wrap_Text_With_Green_Tag()
{
var result = RichTextMarkup.Green("Ready");
Assert.That(result, Is.EqualTo("[green]Ready[/green]"));
}
/// <summary>
/// 验证效果方法会按稳定顺序拼接环境参数。
/// </summary>
[Test]
public void Effect_Should_Sort_Environment_Parameters_By_Key()
{
var env = new Dictionary<string, object?>
{
["tick"] = 0.1f,
["speed"] = 4
};
var result = RichTextMarkup.Effect("Hello", "fade_in", env);
Assert.That(result, Is.EqualTo("[fade_in speed=4 tick=0.1]Hello[/fade_in]"));
}
/// <summary>
/// 验证非法标签 token 会被拒绝,避免生成损坏的 BBCode。
/// </summary>
[Test]
public void Effect_Should_Reject_Invalid_Tag_Tokens()
{
var exception = Assert.Throws<ArgumentException>(() => RichTextMarkup.Effect("Hello", "fade=in"));
Assert.That(exception!.ParamName, Is.EqualTo("tag"));
}
/// <summary>
/// 验证非法环境参数键会被拒绝,避免注入无效的 BBCode token。
/// </summary>
[Test]
public void Effect_Should_Reject_Invalid_Environment_Key_Tokens()
{
var env = new Dictionary<string, object?>
{
["bad key"] = 1
};
var exception = Assert.Throws<ArgumentException>(() => RichTextMarkup.Effect("Hello", "fade_in", env));
Assert.That(exception!.ParamName, Is.EqualTo("env"));
}
}

View File

@ -0,0 +1,68 @@
using GFramework.Godot.Text.Effects;
namespace GFramework.Godot.Text;
/// <summary>
/// 默认的富文本效果注册表。
/// 该实现仅负责内置效果键的解析,不处理业务层文本构建或配置持久化。
/// </summary>
public sealed class DefaultRichTextEffectRegistry : IRichTextEffectRegistry
{
/// <summary>
/// 创建当前配置对应的全部效果实例。
/// </summary>
/// <param name="profile">效果组合配置。</param>
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
/// <returns>内置效果实例集合。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="profile" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public IReadOnlyList<RichTextEffect> CreateEffects(RichTextProfile profile, bool animatedEffectsEnabled)
{
ArgumentNullException.ThrowIfNull(profile);
var effects = new List<RichTextEffect>(profile.Effects.Length);
foreach (var entry in profile.Effects)
{
if (entry is null || !entry.Enabled || string.IsNullOrWhiteSpace(entry.Key))
{
continue;
}
var effect = CreateEffect(entry.Key, animatedEffectsEnabled);
if (effect is not null)
{
effects.Add(effect);
}
}
return effects;
}
/// <summary>
/// 根据效果键创建单个效果实例。
/// </summary>
/// <param name="key">效果键。</param>
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
/// <returns>解析成功时返回效果实例;否则返回 <see langword="null" />。</returns>
public RichTextEffect? CreateEffect(string key, bool animatedEffectsEnabled)
{
if (string.IsNullOrWhiteSpace(key))
{
return null;
}
return key.Trim().ToLowerInvariant() switch
{
"green" => new RichTextGreenEffect(),
"red" => new RichTextRedEffect(),
"gold" => new RichTextGoldEffect(),
"blue" => new RichTextBlueEffect(),
"fade_in" => new RichTextFadeInEffect(animatedEffectsEnabled),
"sine" => new RichTextSineEffect(animatedEffectsEnabled),
"jitter" => new RichTextJitterEffect(animatedEffectsEnabled),
"fly_in" => new RichTextFlyInEffect(animatedEffectsEnabled),
_ => null
};
}
}

View File

@ -0,0 +1,27 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 为文本应用蓝色语义高亮。
/// </summary>
[GlobalClass]
[Tool]
public partial class RichTextBlueEffect : RichTextEffectBase
{
private static readonly Color BlueColor = new(0.44f, 0.72f, 0.98f, 1.0f);
/// <summary>
/// 获取标签名。
/// </summary>
protected override string TagName => "blue";
/// <summary>
/// 应用蓝色颜色效果。
/// </summary>
/// <param name="charFx">当前字符上下文。</param>
/// <returns>始终返回 <see langword="true" />。</returns>
public override bool _ProcessCustomFX(CharFXTransform charFx)
{
charFx.Color = BlueColor;
return true;
}
}

View File

@ -0,0 +1,87 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 富文本效果基类,提供统一的标签命名和环境参数读取辅助逻辑。
/// 该基类只负责 Godot 适配细节,不承载业务语义分层。
/// </summary>
[Tool]
public abstract partial class RichTextEffectBase : RichTextEffect
{
/// <summary>
/// 获取当前效果对应的 BBCode 标签名。
/// </summary>
protected abstract string TagName { get; }
/// <summary>
/// 获取 Godot 识别当前效果所需的 `bbcode` 属性。
/// 属性名使用小写是 Godot `RichTextEffect` 的约定,不是框架对公共成员命名的放宽。
/// </summary>
public string bbcode => TagName;
/// <summary>
/// 尝试从字符环境参数中读取布尔值。
/// </summary>
/// <param name="transform">当前字符变换上下文。</param>
/// <param name="key">参数键。</param>
/// <param name="defaultValue">读取失败时使用的默认值。</param>
/// <returns>最终布尔值;当环境参数不存在或类型不是 <see cref="Variant.Type.Bool" /> 时返回默认值。</returns>
protected bool GetBool(CharFXTransform transform, string key, bool defaultValue = false)
{
if (transform.Env.TryGetValue(Variant.From(key), out var value) &&
value.VariantType == Variant.Type.Bool)
{
return value.AsBool();
}
return defaultValue;
}
/// <summary>
/// 尝试从字符环境参数中读取浮点值。
/// </summary>
/// <param name="transform">当前字符变换上下文。</param>
/// <param name="key">参数键。</param>
/// <param name="defaultValue">读取失败时使用的默认值。</param>
/// <returns>
/// 最终浮点值;当环境参数不存在,或类型既不是 <see cref="Variant.Type.Float" /> 也不是
/// <see cref="Variant.Type.Int" /> 时返回默认值。
/// </returns>
protected float GetFloat(CharFXTransform transform, string key, float defaultValue)
{
if (transform.Env.TryGetValue(Variant.From(key), out var value) &&
(value.VariantType == Variant.Type.Float || value.VariantType == Variant.Type.Int))
{
return (float)value.AsDouble();
}
return defaultValue;
}
/// <summary>
/// 尝试从字符环境参数中读取颜色值。
/// </summary>
/// <param name="transform">当前字符变换上下文。</param>
/// <param name="key">参数键。</param>
/// <param name="defaultValue">读取失败时使用的默认值。</param>
/// <returns>最终颜色值。</returns>
protected Color GetColor(CharFXTransform transform, string key, Color defaultValue)
{
if (transform.Env.TryGetValue(Variant.From(key), out var value) &&
value.VariantType == Variant.Type.Color)
{
return (Color)value;
}
return defaultValue;
}
/// <summary>
/// 从字符环境参数中应用可见性开关。
/// </summary>
/// <param name="transform">当前字符变换上下文。</param>
/// <param name="defaultValue">默认可见性。</param>
protected void ApplyVisibility(CharFXTransform transform, bool defaultValue = true)
{
transform.Visible = GetBool(transform, "visible", defaultValue);
}
}

View File

@ -0,0 +1,47 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 为文本提供逐字符淡入效果。
/// </summary>
[GlobalClass]
[Tool]
public partial class RichTextFadeInEffect : RichTextEffectBase
{
private readonly bool _animatedEffectsEnabled;
/// <summary>
/// 初始化淡入效果。
/// </summary>
/// <param name="animatedEffectsEnabled">是否允许动态效果实际生效。</param>
public RichTextFadeInEffect(bool animatedEffectsEnabled = true)
{
_animatedEffectsEnabled = animatedEffectsEnabled;
}
/// <summary>
/// 获取标签名。
/// </summary>
protected override string TagName => "fade_in";
/// <summary>
/// 应用淡入动画。
/// </summary>
/// <param name="charFx">当前字符上下文。</param>
/// <returns>始终返回 <see langword="true" />。</returns>
public override bool _ProcessCustomFX(CharFXTransform charFx)
{
if (!_animatedEffectsEnabled)
{
return true;
}
var speed = GetFloat(charFx, "speed", 4.0f);
var tick = GetFloat(charFx, "tick", 0.01f);
var progress = (float)(charFx.ElapsedTime * speed - charFx.RelativeIndex * tick);
var color = charFx.Color;
color.A = Mathf.Clamp(progress, 0f, 1f);
charFx.Color = color;
ApplyVisibility(charFx);
return true;
}
}

View File

@ -0,0 +1,64 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 为文本提供逐字符飞入效果。
/// </summary>
[GlobalClass]
[Tool]
public partial class RichTextFlyInEffect : RichTextEffectBase
{
private readonly bool _animatedEffectsEnabled;
/// <summary>
/// 初始化飞入效果。
/// </summary>
/// <param name="animatedEffectsEnabled">是否允许动态效果实际生效。</param>
public RichTextFlyInEffect(bool animatedEffectsEnabled = true)
{
_animatedEffectsEnabled = animatedEffectsEnabled;
}
/// <summary>
/// 获取标签名。
/// </summary>
protected override string TagName => "fly_in";
/// <summary>
/// 应用飞入动画。
/// </summary>
/// <param name="charFx">当前字符上下文。</param>
/// <returns>始终返回 <see langword="true" />。</returns>
public override bool _ProcessCustomFX(CharFXTransform charFx)
{
if (!_animatedEffectsEnabled)
{
return true;
}
var startOffset = new Vector2(
GetFloat(charFx, "offset_x", 12f),
GetFloat(charFx, "offset_y", 0f));
var speed = GetFloat(charFx, "speed", 3.0f);
var tick = GetFloat(charFx, "tick", 0.015f);
var progress = Mathf.Clamp((float)(charFx.ElapsedTime * speed - charFx.RelativeIndex * tick), 0f, 1f);
var eased = EaseOutQuad(progress);
charFx.Offset += startOffset * (1f - eased);
var color = charFx.Color;
color.A = eased;
charFx.Color = color;
ApplyVisibility(charFx);
return true;
}
/// <summary>
/// 计算二次缓出值。
/// </summary>
/// <param name="value">归一化进度。</param>
/// <returns>缓出后的进度。</returns>
private static float EaseOutQuad(float value)
{
return 1f - (1f - value) * (1f - value);
}
}

View File

@ -0,0 +1,27 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 为文本应用金色语义高亮。
/// </summary>
[GlobalClass]
[Tool]
public partial class RichTextGoldEffect : RichTextEffectBase
{
private static readonly Color GoldColor = new(0.96f, 0.79f, 0.34f, 1.0f);
/// <summary>
/// 获取标签名。
/// </summary>
protected override string TagName => "gold";
/// <summary>
/// 应用金色颜色效果。
/// </summary>
/// <param name="charFx">当前字符上下文。</param>
/// <returns>始终返回 <see langword="true" />。</returns>
public override bool _ProcessCustomFX(CharFXTransform charFx)
{
charFx.Color = GoldColor;
return true;
}
}

View File

@ -0,0 +1,27 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 为文本应用绿色语义高亮。
/// </summary>
[GlobalClass]
[Tool]
public partial class RichTextGreenEffect : RichTextEffectBase
{
private static readonly Color GreenColor = new(0.46f, 0.91f, 0.49f, 1.0f);
/// <summary>
/// 获取标签名。
/// </summary>
protected override string TagName => "green";
/// <summary>
/// 应用绿色颜色效果。
/// </summary>
/// <param name="charFx">当前字符上下文。</param>
/// <returns>始终返回 <see langword="true" />。</returns>
public override bool _ProcessCustomFX(CharFXTransform charFx)
{
charFx.Color = GreenColor;
return true;
}
}

View File

@ -0,0 +1,62 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 为文本提供抖动效果。
/// </summary>
/// <remarks>
/// 默认注册表会在每次宿主刷新时为该效果创建独立实例。
/// <see cref="_ProcessCustomFX" /> 内部会复用并修改 <see cref="_noise" /> 的 Seed因此该类型假定 Godot 在主线程顺序
/// 执行字符效果,不支持跨多个 <see cref="RichTextLabel" /> 共享同一实例,也不保证并发调用下的线程安全。
/// </remarks>
[GlobalClass]
[Tool]
public partial class RichTextJitterEffect : RichTextEffectBase
{
private readonly bool _animatedEffectsEnabled;
private readonly FastNoiseLite _noise;
/// <summary>
/// 初始化抖动效果。
/// </summary>
/// <param name="animatedEffectsEnabled">是否允许动态效果实际生效。</param>
public RichTextJitterEffect(bool animatedEffectsEnabled = true)
{
_animatedEffectsEnabled = animatedEffectsEnabled;
_noise = new FastNoiseLite
{
NoiseType = FastNoiseLite.NoiseTypeEnum.Perlin,
FractalOctaves = 8,
FractalGain = 0.8f
};
}
/// <summary>
/// 获取标签名。
/// </summary>
protected override string TagName => "jitter";
/// <summary>
/// 应用抖动位移。
/// </summary>
/// <param name="charFx">当前字符上下文。</param>
/// <returns>始终返回 <see langword="true" />。</returns>
public override bool _ProcessCustomFX(CharFXTransform charFx)
{
if (!_animatedEffectsEnabled)
{
return true;
}
var amplitude = GetFloat(charFx, "amplitude", 3.0f);
var speed = GetFloat(charFx, "speed", 600.0f);
_noise.Seed = (charFx.RelativeIndex + 1) * 131;
var x = _noise.GetNoise1D((float)charFx.ElapsedTime * speed);
_noise.Seed = (charFx.RelativeIndex + 1) * 737;
var y = _noise.GetNoise1D((float)charFx.ElapsedTime * speed);
charFx.Offset += new Vector2(x, y) * amplitude;
ApplyVisibility(charFx);
return true;
}
}

View File

@ -0,0 +1,27 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 为文本应用红色语义高亮。
/// </summary>
[GlobalClass]
[Tool]
public partial class RichTextRedEffect : RichTextEffectBase
{
private static readonly Color RedColor = new(0.96f, 0.35f, 0.35f, 1.0f);
/// <summary>
/// 获取标签名。
/// </summary>
protected override string TagName => "red";
/// <summary>
/// 应用红色颜色效果。
/// </summary>
/// <param name="charFx">当前字符上下文。</param>
/// <returns>始终返回 <see langword="true" />。</returns>
public override bool _ProcessCustomFX(CharFXTransform charFx)
{
charFx.Color = RedColor;
return true;
}
}

View File

@ -0,0 +1,47 @@
namespace GFramework.Godot.Text.Effects;
/// <summary>
/// 为文本提供正弦波形的上下漂浮效果。
/// </summary>
[GlobalClass]
[Tool]
public partial class RichTextSineEffect : RichTextEffectBase
{
private readonly bool _animatedEffectsEnabled;
/// <summary>
/// 初始化正弦效果。
/// </summary>
/// <param name="animatedEffectsEnabled">是否允许动态效果实际生效。</param>
public RichTextSineEffect(bool animatedEffectsEnabled = true)
{
_animatedEffectsEnabled = animatedEffectsEnabled;
}
/// <summary>
/// 获取标签名。
/// </summary>
protected override string TagName => "sine";
/// <summary>
/// 应用正弦位移。
/// </summary>
/// <param name="charFx">当前字符上下文。</param>
/// <returns>始终返回 <see langword="true" />。</returns>
public override bool _ProcessCustomFX(CharFXTransform charFx)
{
if (!_animatedEffectsEnabled)
{
return true;
}
var amplitude = GetFloat(charFx, "amplitude", 0.8f);
var frequency = GetFloat(charFx, "frequency", 0.5f);
var speed = GetFloat(charFx, "speed", 1.5f);
var phase = (float)(charFx.ElapsedTime * speed + charFx.RelativeIndex * 0.1f);
var offsetY = amplitude * Mathf.Sin(phase * Mathf.Pi * 2f * frequency);
charFx.Offset += new Vector2(0f, offsetY);
ApplyVisibility(charFx);
return true;
}
}

View File

@ -0,0 +1,106 @@
namespace GFramework.Godot.Text;
/// <summary>
/// GFramework 提供的组合式富文本标签宿主。
/// 该类型只负责桥接 Godot 的 <see cref="RichTextLabel" /> 与框架的效果装配逻辑,不承载具体效果实现。
/// </summary>
[GlobalClass]
[Tool]
public partial class GfRichTextLabel : RichTextLabel, IRichTextEffectHost
{
private IRichTextEffectRegistry? _effectRegistry;
private RichTextEffectsController? _effectsController;
/// <summary>
/// 获取或设置当前标签使用的效果配置。
/// 为空时将回退到内置默认配置。
/// </summary>
[Export]
public RichTextProfile? Profile { get; set; }
/// <summary>
/// 获取或设置是否启用框架管理的富文本效果装配。
/// 关闭后只会停止框架效果安装,不会覆盖调用方手动维护的其他 BBCode 解析状态。
/// </summary>
[Export]
public bool EnableFrameworkEffects { get; set; } = true;
/// <summary>
/// 获取或设置是否允许字符级动态效果实际生效。
/// 关闭后仍然会安装对应标签,使富文本内容保持可解析。
/// </summary>
[Export]
public bool AnimatedEffectsEnabled { get; set; } = true;
/// <summary>
/// 获取当前使用的效果注册表。
/// </summary>
/// <exception cref="ArgumentNullException">
/// 当设置值为 <see langword="null" /> 时抛出。
/// </exception>
internal IRichTextEffectRegistry EffectRegistry
{
get => _effectRegistry ??= new DefaultRichTextEffectRegistry();
set => _effectRegistry = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// 根据控制器提供的配置参数在适配层实例化 Godot 原生效果,并写回标签宿主。
/// 这样控制器与测试替身不需要直接触碰 <see cref="RichTextEffect" /> 或
/// <see cref="global::Godot.Collections.Array" />,而真正依赖 Godot runtime 的工作只发生在节点边界上。
/// </summary>
/// <param name="profile">需要安装的纯托管效果计划。</param>
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
void IRichTextEffectHost.ApplyEffects(RichTextEffectPlan profile, bool animatedEffectsEnabled)
{
ArgumentNullException.ThrowIfNull(profile);
var registry = EffectRegistry;
var effects = registry.CreateEffects(RichTextProfile.FromPlan(profile), animatedEffectsEnabled);
var customEffects = new global::Godot.Collections.Array();
foreach (var effect in effects)
{
customEffects.Add(effect);
}
CustomEffects = customEffects;
}
/// <summary>
/// 清空标签当前持有的自定义效果集合。
/// </summary>
void IRichTextEffectHost.ClearCustomEffects()
{
CustomEffects = new global::Godot.Collections.Array();
}
/// <summary>
/// 节点就绪时初始化控制器并安装效果集合。
/// </summary>
public override void _Ready()
{
EnsureController().Initialize();
}
/// <summary>
/// 手动刷新框架效果集合。
/// 当调用方在运行时替换配置或切换动画开关时,可通过该方法同步宿主状态。
/// </summary>
public void RefreshFrameworkEffects()
{
EnsureController().RefreshEffects();
}
/// <summary>
/// 获取或创建控制器实例。
/// </summary>
/// <returns>组合式装配控制器。</returns>
private RichTextEffectsController EnsureController()
{
return _effectsController ??= new RichTextEffectsController(
this,
() => Profile is null ? null : RichTextEffectPlan.FromProfile(Profile),
() => EnableFrameworkEffects,
() => AnimatedEffectsEnabled);
}
}

View File

@ -0,0 +1,28 @@
namespace GFramework.Godot.Text;
/// <summary>
/// 抽象可被富文本效果控制器驱动的宿主。
/// 该接口把装配决策从 Godot 原生 <see cref="RichTextLabel" /> 生命周期中解耦出来,便于在纯托管测试中验证开关、
/// 配置回退和注册表替换行为。
/// </summary>
internal interface IRichTextEffectHost
{
/// <summary>
/// 获取或设置宿主是否启用 BBCode 解析。
/// </summary>
bool BbcodeEnabled { get; set; }
/// <summary>
/// 使用给定的配置和动画开关重建宿主上的自定义富文本效果。
/// 纯托管控制器只负责组合刷新参数,适配层负责在真正需要时解析注册表、实例化 Godot 效果对象并写回宿主。
/// </summary>
/// <param name="profile">需要安装的纯托管效果计划。</param>
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
void ApplyEffects(RichTextEffectPlan profile, bool animatedEffectsEnabled);
/// <summary>
/// 清空当前安装到宿主上的自定义富文本效果集合。
/// 关闭框架效果时,控制器会通过该方法显式撤销之前安装的效果。
/// </summary>
void ClearCustomEffects();
}

View File

@ -0,0 +1,36 @@
namespace GFramework.Godot.Text;
/// <summary>
/// 富文本效果注册表,负责把配置中的效果键解析为可安装的 <see cref="RichTextEffect" /> 实例。
/// </summary>
/// <remarks>
/// <see cref="RichTextEffectsController" /> 会在 <see cref="GfRichTextLabel" /> 就绪或显式刷新时调用该注册表,重建
/// <see cref="RichTextLabel.CustomEffects" />。
/// 该抽象存在的目的,是把“配置里声明了哪些效果”与“这些效果如何实例化”解耦,使宿主节点、配置资源和测试替身都不必
/// 直接依赖具体内置效果类型。
/// 当项目只需要组合现有标签时,应优先使用 <see cref="RichTextProfile" />;当项目需要替换内置映射、注入自定义
/// <see cref="RichTextEffect" />,或按“动态效果是否启用”的边界切换效果实现时,应实现该接口。
/// </remarks>
public interface IRichTextEffectRegistry
{
/// <summary>
/// 根据指定配置创建完整的效果实例集合。
/// 该方法会在每次宿主刷新时执行,因此实现应保持可重复、确定且与宿主生命周期兼容。
/// </summary>
/// <param name="profile">效果组合配置。</param>
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
/// <returns>可直接写入 <see cref="RichTextLabel.CustomEffects" /> 的效果实例集合。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="profile" /> 为 <see langword="null" /> 且实现不接受空配置时抛出。
/// </exception>
IReadOnlyList<RichTextEffect> CreateEffects(RichTextProfile profile, bool animatedEffectsEnabled);
/// <summary>
/// 根据单个效果键创建对应效果实例。
/// 该方法主要用于将配置声明的 key 映射为宿主可安装的单个效果对象。
/// </summary>
/// <param name="key">效果键。</param>
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
/// <returns>解析成功时返回效果实例;否则返回 <see langword="null" />。</returns>
RichTextEffect? CreateEffect(string key, bool animatedEffectsEnabled);
}

View File

@ -0,0 +1,22 @@
namespace GFramework.Godot.Text;
/// <summary>
/// 描述一条富文本效果配置项。
/// 该资源只负责声明需要启用的效果键与开关状态,不承担实例创建逻辑。
/// </summary>
[GlobalClass]
public partial class RichTextEffectEntry : Resource
{
/// <summary>
/// 获取或设置效果键。
/// 键值由 <see cref="IRichTextEffectRegistry" /> 解析为具体的 <see cref="RichTextEffect" /> 实例。
/// </summary>
[Export]
public string Key { get; set; } = string.Empty;
/// <summary>
/// 获取或设置该配置项是否启用。
/// </summary>
[Export]
public bool Enabled { get; set; } = true;
}

View File

@ -0,0 +1,70 @@
namespace GFramework.Godot.Text;
/// <summary>
/// 描述一次富文本效果安装所需的纯托管计划。
/// 该类型用于把控制器与测试替身隔离在 Godot runtime 之外,使刷新决策可以在普通 .NET 测试进程中验证。
/// </summary>
internal sealed class RichTextEffectPlan
{
/// <summary>
/// 初始化一个富文本效果计划。
/// </summary>
/// <param name="effects">计划中声明的效果条目集合。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="effects" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public RichTextEffectPlan(IReadOnlyList<RichTextEffectPlanEntry> effects)
{
ArgumentNullException.ThrowIfNull(effects);
Effects = effects.ToArray();
}
/// <summary>
/// 获取当前计划启用的效果条目集合。
/// </summary>
public IReadOnlyList<RichTextEffectPlanEntry> Effects { get; }
/// <summary>
/// 从 Godot 资源配置转换为纯托管计划。
/// </summary>
/// <param name="profile">待转换的资源配置。</param>
/// <returns>与资源配置等价的纯托管计划。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="profile" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public static RichTextEffectPlan FromProfile(RichTextProfile profile)
{
ArgumentNullException.ThrowIfNull(profile);
var effects = new RichTextEffectPlanEntry[profile.Effects.Length];
for (var index = 0; index < profile.Effects.Length; index++)
{
var entry = profile.Effects[index];
effects[index] = entry is null
? default
: new RichTextEffectPlanEntry(entry.Key, entry.Enabled);
}
return new RichTextEffectPlan(effects);
}
/// <summary>
/// 创建包含全部内置效果的默认计划。
/// </summary>
/// <returns>包含第一阶段全部内置效果键的默认计划。</returns>
public static RichTextEffectPlan CreateBuiltInDefault()
{
return new RichTextEffectPlan(
[
new RichTextEffectPlanEntry("green"),
new RichTextEffectPlanEntry("red"),
new RichTextEffectPlanEntry("gold"),
new RichTextEffectPlanEntry("blue"),
new RichTextEffectPlanEntry("fade_in"),
new RichTextEffectPlanEntry("sine"),
new RichTextEffectPlanEntry("jitter"),
new RichTextEffectPlanEntry("fly_in")
]);
}
}

View File

@ -0,0 +1,9 @@
namespace GFramework.Godot.Text;
/// <summary>
/// 描述一条纯托管的富文本效果计划项。
/// 控制器与测试替身只关心效果键和启用状态,不需要依赖 Godot 资源对象本身。
/// </summary>
/// <param name="Key">效果键。</param>
/// <param name="Enabled">该效果项是否启用。</param>
internal readonly record struct RichTextEffectPlanEntry(string Key, bool Enabled = true);

View File

@ -0,0 +1,68 @@
namespace GFramework.Godot.Text;
/// <summary>
/// 负责把纯托管效果计划和开关装配为宿主标签的实际效果集合。
/// 该控制器是组合式扩展的装配中心,使 <see cref="GfRichTextLabel" /> 保持轻量。
/// </summary>
internal sealed class RichTextEffectsController
{
private readonly Func<bool> _animatedEffectsEnabledAccessor;
private readonly Func<bool> _frameworkEffectsEnabledAccessor;
private readonly IRichTextEffectHost _host;
private readonly Func<RichTextEffectPlan?> _profileAccessor;
/// <summary>
/// 初始化控制器实例。
/// </summary>
/// <param name="host">目标富文本标签。</param>
/// <param name="profileAccessor">当前纯托管效果计划访问器。</param>
/// <param name="frameworkEffectsEnabledAccessor">框架效果总开关访问器。</param>
/// <param name="animatedEffectsEnabledAccessor">字符动画开关访问器。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="host" />、<paramref name="profileAccessor" />、
/// <paramref name="frameworkEffectsEnabledAccessor" /> 或 <paramref name="animatedEffectsEnabledAccessor" />
/// 为 <see langword="null" /> 时抛出。
/// </exception>
public RichTextEffectsController(
IRichTextEffectHost host,
Func<RichTextEffectPlan?> profileAccessor,
Func<bool> frameworkEffectsEnabledAccessor,
Func<bool> animatedEffectsEnabledAccessor)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
_profileAccessor = profileAccessor ?? throw new ArgumentNullException(nameof(profileAccessor));
_frameworkEffectsEnabledAccessor = frameworkEffectsEnabledAccessor
?? throw new ArgumentNullException(nameof(frameworkEffectsEnabledAccessor));
_animatedEffectsEnabledAccessor = animatedEffectsEnabledAccessor
?? throw new ArgumentNullException(nameof(animatedEffectsEnabledAccessor));
}
/// <summary>
/// 初始化并立即刷新宿主标签的效果集合。
/// </summary>
public void Initialize()
{
RefreshEffects();
}
/// <summary>
/// 根据当前配置和开关重建宿主标签上的 <see cref="RichTextLabel.CustomEffects" />。
/// </summary>
public void RefreshEffects()
{
var frameworkEffectsEnabled = _frameworkEffectsEnabledAccessor();
if (frameworkEffectsEnabled && !_host.BbcodeEnabled)
{
_host.BbcodeEnabled = true;
}
if (!frameworkEffectsEnabled)
{
_host.ClearCustomEffects();
return;
}
var profile = _profileAccessor() ?? RichTextEffectPlan.CreateBuiltInDefault();
_host.ApplyEffects(profile, _animatedEffectsEnabledAccessor());
}
}

View File

@ -0,0 +1,182 @@
using System.Globalization;
using System.Text;
namespace GFramework.Godot.Text;
/// <summary>
/// 提供语义化的富文本标签构建辅助方法。
/// 该工具层用于减少业务代码直接手写原始 BBCode 字符串的重复工作。
/// </summary>
public static class RichTextMarkup
{
/// <summary>
/// 使用指定标签包裹文本。
/// </summary>
/// <param name="text">原始文本。</param>
/// <param name="tag">标签名。</param>
/// <returns>包裹后的 BBCode 文本。</returns>
/// <exception cref="ArgumentException">
/// 当 <paramref name="tag" /> 为空、仅包含空白字符,或包含 BBCode token 不允许的控制字符时抛出。
/// </exception>
public static string Color(string text, string tag)
{
return Wrap(text, tag);
}
/// <summary>
/// 使用 `green` 标签包裹文本。
/// </summary>
/// <param name="text">原始文本。</param>
/// <returns>包裹后的 BBCode 文本。</returns>
public static string Green(string text)
{
return Wrap(text, "green");
}
/// <summary>
/// 使用 `red` 标签包裹文本。
/// </summary>
/// <param name="text">原始文本。</param>
/// <returns>包裹后的 BBCode 文本。</returns>
public static string Red(string text)
{
return Wrap(text, "red");
}
/// <summary>
/// 使用 `gold` 标签包裹文本。
/// </summary>
/// <param name="text">原始文本。</param>
/// <returns>包裹后的 BBCode 文本。</returns>
public static string Gold(string text)
{
return Wrap(text, "gold");
}
/// <summary>
/// 使用 `blue` 标签包裹文本。
/// </summary>
/// <param name="text">原始文本。</param>
/// <returns>包裹后的 BBCode 文本。</returns>
public static string Blue(string text)
{
return Wrap(text, "blue");
}
/// <summary>
/// 使用指定效果标签包裹文本,并可附带参数环境。
/// 环境参数会按键名进行稳定排序,避免不同字典实现导致输出顺序漂移。
/// </summary>
/// <param name="text">原始文本。</param>
/// <param name="tag">标签名。</param>
/// <param name="env">可选的标签参数集合。</param>
/// <returns>包裹后的 BBCode 文本。</returns>
/// <exception cref="ArgumentException">
/// 当 <paramref name="tag" /> 为空、仅包含空白字符,包含 BBCode token 不允许的控制字符,
/// 或 <paramref name="env" /> 中存在包含非法控制字符的参数键时抛出。
/// </exception>
public static string Effect(string text, string tag, IReadOnlyDictionary<string, object?>? env = null)
{
ValidateToken(tag, nameof(tag));
var builder = new StringBuilder();
builder.Append('[');
builder.Append(tag);
if (env is not null)
{
foreach (var pair in CollectEnvironmentPairs(env))
{
builder.Append(' ');
builder.Append(pair.Key);
builder.Append('=');
builder.Append(FormatValue(pair.Value));
}
}
builder.Append(']');
builder.Append(text ?? string.Empty);
builder.Append("[/");
builder.Append(tag);
builder.Append(']');
return builder.ToString();
}
/// <summary>
/// 使用指定标签包裹文本。
/// </summary>
/// <param name="text">原始文本。</param>
/// <param name="tag">标签名。</param>
/// <returns>包裹后的 BBCode 文本。</returns>
private static string Wrap(string text, string tag)
{
ValidateToken(tag, nameof(tag));
return $"[{tag}]{text ?? string.Empty}[/{tag}]";
}
/// <summary>
/// 收集并排序可写入 BBCode 的环境参数。
/// </summary>
/// <param name="env">原始环境参数。</param>
/// <returns>按键名稳定排序后的参数集合。</returns>
/// <exception cref="ArgumentException">
/// 当参数键包含 BBCode token 不允许的控制字符时抛出。
/// </exception>
private static IReadOnlyList<KeyValuePair<string, object>> CollectEnvironmentPairs(
IReadOnlyDictionary<string, object?> env)
{
var pairs = new List<KeyValuePair<string, object>>(env.Count);
foreach (var pair in env)
{
if (pair.Value is null || string.IsNullOrWhiteSpace(pair.Key))
{
continue;
}
ValidateToken(pair.Key, nameof(env));
pairs.Add(new KeyValuePair<string, object>(pair.Key, pair.Value));
}
pairs.Sort(static (left, right) => StringComparer.Ordinal.Compare(left.Key, right.Key));
return pairs;
}
/// <summary>
/// 验证 BBCode 标签或参数键是否满足 token 约束。
/// </summary>
/// <param name="token">待验证的 token。</param>
/// <param name="paramName">异常参数名。</param>
/// <exception cref="ArgumentException">
/// 当 token 为空、仅包含空白字符,或包含 BBCode token 不允许的控制字符时抛出。
/// </exception>
private static void ValidateToken(string token, string paramName)
{
if (string.IsNullOrWhiteSpace(token))
{
throw new ArgumentException("BBCode token cannot be null, empty, or whitespace.", paramName);
}
foreach (var character in token)
{
if (char.IsWhiteSpace(character) || character is '[' or ']' or '=')
{
throw new ArgumentException("BBCode token contains invalid control characters.", paramName);
}
}
}
/// <summary>
/// 将标签参数值格式化为稳定的 BBCode 字符串表示。
/// </summary>
/// <param name="value">待格式化的值。</param>
/// <returns>适用于 BBCode 参数的字符串。</returns>
private static string FormatValue(object value)
{
return value switch
{
string text => text,
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString() ?? string.Empty
};
}
}

View File

@ -0,0 +1,50 @@
namespace GFramework.Godot.Text;
/// <summary>
/// 描述一个富文本效果组合配置。
/// 该资源是 Godot 编辑器与场景系统使用的配置载体;运行时控制器会先把它转换为
/// <see cref="RichTextEffectPlan" />,再在纯托管边界内完成刷新决策。
/// </summary>
[GlobalClass]
public partial class RichTextProfile : Resource
{
/// <summary>
/// 获取或设置当前配置启用的效果条目集合。
/// </summary>
[Export]
public RichTextEffectEntry[] Effects { get; set; } = [];
/// <summary>
/// 创建包含全部内置效果的默认配置。
/// 该方法为第一阶段提供零配置可用的回退组合。
/// </summary>
/// <returns>包含全部内置效果键的默认配置。</returns>
public static RichTextProfile CreateBuiltInDefault()
{
return FromPlan(RichTextEffectPlan.CreateBuiltInDefault());
}
/// <summary>
/// 从纯托管效果计划创建对应的 Godot 资源配置。
/// 该转换只应发生在真正需要与 Godot 宿主或公开注册表交互的适配层边界上。
/// </summary>
/// <param name="plan">待转换的纯托管效果计划。</param>
/// <returns>与计划等价的 Godot 资源配置。</returns>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="plan" /> 为 <see langword="null" /> 时抛出。
/// </exception>
internal static RichTextProfile FromPlan(RichTextEffectPlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
var profile = new RichTextProfile();
profile.Effects = plan.Effects
.Select(static entry => new RichTextEffectEntry
{
Key = entry.Key,
Enabled = entry.Enabled
})
.ToArray();
return profile;
}
}

140
README.md
View File

@ -1,6 +1,6 @@
# GFramework # GFramework
> 面向游戏开发场景的模块化 C# 框架,核心能力与具体引擎解耦,可按需组合 Core / Game / Godot / Source Generators > 面向游戏开发场景的模块化 C# 框架,按运行时、抽象层、引擎集成和源码生成器拆分能力
[![NuGet Core](https://img.shields.io/badge/NuGet-GeWuYou.GFramework.Core-2C7BE5)](https://www.nuget.org/packages/GeWuYou.GFramework.Core) [![NuGet Core](https://img.shields.io/badge/NuGet-GeWuYou.GFramework.Core-2C7BE5)](https://www.nuget.org/packages/GeWuYou.GFramework.Core)
[![NuGet Meta](https://img.shields.io/badge/NuGet-GeWuYou.GFramework-1F9D55)](https://www.nuget.org/packages/GeWuYou.GFramework) [![NuGet Meta](https://img.shields.io/badge/NuGet-GeWuYou.GFramework-1F9D55)](https://www.nuget.org/packages/GeWuYou.GFramework)
@ -8,84 +8,87 @@
[![.NET](https://img.shields.io/badge/.NET-8.0+-purple)](https://dotnet.microsoft.com/) [![.NET](https://img.shields.io/badge/.NET-8.0+-purple)](https://dotnet.microsoft.com/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue)](LICENSE) [![License](https://img.shields.io/badge/License-Apache%202.0-blue)](LICENSE)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat-square&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS5zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/GeWuYou/GFramework) [![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat-square&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS5zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/GeWuYou/GFramework)
---
## 项目简介 ## 从哪里开始
GFramework 采用清晰分层与模块化设计,强调: - 第一次接触框架:[`docs/zh-CN/getting-started/index.md`](docs/zh-CN/getting-started/index.md)
- 想先跑一个最小例子:[`docs/zh-CN/getting-started/quick-start.md`](docs/zh-CN/getting-started/quick-start.md)
- 已经知道要用哪个模块:直接进入对应模块目录下的 `README.md`
- **架构分层Architecture / Model / System / Utility** ## 模块地图
- **命令与查询分离CQRS**
- **类型安全事件机制**
- **可绑定属性与响应式数据流**
- **可扩展的 IOC/生命周期管理**
- **基于 Roslyn 的源码生成能力**
项目灵感参考自 [QFramework](https://github.com/liangxiegame/QFramework),并在模块边界、工程组织和可扩展性方面进行了持续重构。 | 模块 | 作用 | 入口 |
| --- | --- | --- |
## 功能模块 | `GFramework.Core` | 架构、命令、查询、事件、状态、日志、资源、协程等基础运行时 | [README](GFramework.Core/README.md) |
| `GFramework.Core.Abstractions` | `Core` 对应的契约层,适合面向接口开发或做模块拆分 | [README](GFramework.Core.Abstractions/README.md) |
| 模块 | 说明 | 文档 | | `GFramework.Cqrs` | 新版 CQRS runtime提供 request dispatcher、notification publish 与 handler 注册 | [README](GFramework.Cqrs/README.md) |
|-------------------------------------------------------------------------|-----------------------------------------|---------------------------------------------------| | `GFramework.Cqrs.Abstractions` | CQRS 消息、处理器、pipeline 行为等契约 | [README](GFramework.Cqrs.Abstractions/README.md) |
| `GFramework.Core` | 平台无关的核心架构能力架构、命令、查询、事件、属性、IOC、日志等 | [查看](GFramework.Core/README.md) | | `GFramework.Game` | 面向游戏项目的配置、数据、路由、场景、UI、设置和存储运行时 | [README](GFramework.Game/README.md) |
| `GFramework.Core.Abstractions` | Core 对应的抽象接口定义 | [查看](GFramework.Core.Abstractions/README.md) | | `GFramework.Game.Abstractions` | `Game` 对应的契约层 | [README](GFramework.Game.Abstractions/README.md) |
| `GFramework.Game` | 游戏业务侧扩展状态、配置、存储、UI 等) | [查看](GFramework.Game/README.md) | | `GFramework.Godot` | Godot 集成层负责把框架能力接入节点、场景、UI、设置与存储 | [README](GFramework.Godot/README.md) |
| `GFramework.Game.Abstractions` | Game 模块抽象接口定义 | [查看](GFramework.Game.Abstractions/README.md) | | `GFramework.Ecs.Arch` | Arch ECS 集成 | [README](GFramework.Ecs.Arch/README.md) |
| `GFramework.Godot` | Godot 集成层(节点扩展、场景/设置/存储适配等) | [查看](GFramework.Godot/README.md) | | `GFramework.Core.SourceGenerators` | Core 侧通用源码生成器与分析器 | [README](GFramework.Core.SourceGenerators/README.md) |
| `GFramework.Cqrs` / `GFramework.Cqrs.Abstractions` | CQRS runtime、契约与 handler 注册基础设施 | [查看](docs/zh-CN/core/cqrs.md) | | `GFramework.Game.SourceGenerators` | 游戏内容配置 schema 生成器 | [README](GFramework.Game.SourceGenerators/README.md) |
| `GFramework.Core.SourceGenerators` | Core 侧源码生成器(日志、枚举扩展、规则、模块注册等) | [查看](GFramework.Core.SourceGenerators/README.md) | | `GFramework.Cqrs.SourceGenerators` | CQRS handler registry 生成器 | [README](GFramework.Cqrs.SourceGenerators/README.md) |
| `GFramework.Game.SourceGenerators` / `GFramework.Cqrs.SourceGenerators` | 游戏配置 schema 与 CQRS handler registry 生成器 | [查看](docs/zh-CN/source-generators/index.md) | | `GFramework.Godot.SourceGenerators` | Godot 场景专用源码生成器 | [README](GFramework.Godot.SourceGenerators/README.md) |
| `GFramework.Godot.SourceGenerators` | Godot 场景下的源码生成器扩展 | [查看](GFramework.Godot.SourceGenerators/README.md) |
## 文档导航 ## 文档导航
- 入门教程:[`docs/zh-CN/tutorials/getting-started.md`](docs/zh-CN/tutorials/getting-started.md) 仓库根 README 与文档站点保持同一套栏目命名:
- Godot 集成:[`docs/zh-CN/godot/index.md`](docs/zh-CN/godot/index.md)
- 进阶模式:[`docs/zh-CN/core/index.md`](docs/zh-CN/core/index.md)
- 最佳实践:[`docs/zh-CN/best-practices/architecture-patterns.md`](docs/zh-CN/best-practices/architecture-patterns.md)
- API 参考:[`docs/zh-CN/api-reference/`](docs/zh-CN/api-reference/)
> 如果你更偏好按模块阅读,建议从各子项目 `README.md` 开始,再回到 `docs/` 查阅专题文档。 - 入门指南:[`docs/zh-CN/getting-started/index.md`](docs/zh-CN/getting-started/index.md)
- Core[`docs/zh-CN/core/index.md`](docs/zh-CN/core/index.md)
- Game[`docs/zh-CN/game/index.md`](docs/zh-CN/game/index.md)
- Godot[`docs/zh-CN/godot/index.md`](docs/zh-CN/godot/index.md)
- 教程:[`docs/zh-CN/tutorials/index.md`](docs/zh-CN/tutorials/index.md)
- 源码生成器:[`docs/zh-CN/source-generators/index.md`](docs/zh-CN/source-generators/index.md)
- ECS[`docs/zh-CN/ecs/index.md`](docs/zh-CN/ecs/index.md)
- 抽象接口:[`docs/zh-CN/abstractions/index.md`](docs/zh-CN/abstractions/index.md)
- 最佳实践:[`docs/zh-CN/best-practices/index.md`](docs/zh-CN/best-practices/index.md)
- API 参考:[`docs/zh-CN/api-reference/index.md`](docs/zh-CN/api-reference/index.md)
- FAQ[`docs/zh-CN/faq.md`](docs/zh-CN/faq.md)
- 故障排查:[`docs/zh-CN/troubleshooting.md`](docs/zh-CN/troubleshooting.md)
- 贡献:[`docs/zh-CN/contributing.md`](docs/zh-CN/contributing.md)
## 包选择说明(避免混淆) ## 包选择
- **`GeWuYou.GFramework`**聚合元包Meta Package用于一键引入常用能力集合适合快速试用或原型阶段。 - `GeWuYou.GFramework`
- **`GeWuYou.GFramework.Core`**:核心起步包,适合希望按模块精细控制依赖的项目(推荐生产项目从此起步)。 当前是聚合元包,只聚合 `GFramework.Core``GFramework.Game` 这两层运行时,适合快速试用。
- `GeWuYou.GFramework.Core`
推荐的最小起步包。先从核心运行时开始,再按需叠加 `Cqrs``Game``Godot` 和 Source Generators。
- `GeWuYou.GFramework.*.Abstractions`
适合需要单独依赖契约层、插件化、测试替身或多模块解耦的场景。
- `GeWuYou.GFramework.*.SourceGenerators`
只在需要编译期生成代码时安装,版本应与运行时包保持一致。
如果你已明确技术栈建议优先按模块安装Core / Cqrs / Game / Godot / Source Generators避免不必要依赖。 ## 最小安装组合
## 快速安装
按实际需求选择依赖:
```bash ```bash
# 核心能力(推荐最小起步 # 最小起步
dotnet add package GeWuYou.GFramework.Core dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions dotnet add package GeWuYou.GFramework.Core.Abstractions
# CQRS # 新版 CQRS
dotnet add package GeWuYou.GFramework.Cqrs dotnet add package GeWuYou.GFramework.Cqrs
dotnet add package GeWuYou.GFramework.Cqrs.Abstractions dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
# 游戏扩展 # 游戏层运行时
dotnet add package GeWuYou.GFramework.Game dotnet add package GeWuYou.GFramework.Game
dotnet add package GeWuYou.GFramework.Game.Abstractions dotnet add package GeWuYou.GFramework.Game.Abstractions
# Godot 集成(仅 Godot 项目需要) # Godot 集成
dotnet add package GeWuYou.GFramework.Godot dotnet add package GeWuYou.GFramework.Godot
# 按场景选择源码生成器(可选,但推荐) # 按需安装源码生成器
dotnet add package GeWuYou.GFramework.Core.SourceGenerators dotnet add package GeWuYou.GFramework.Core.SourceGenerators
dotnet add package GeWuYou.GFramework.Game.SourceGenerators dotnet add package GeWuYou.GFramework.Game.SourceGenerators
dotnet add package GeWuYou.GFramework.Godot.SourceGenerators
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
dotnet add package GeWuYou.GFramework.Godot.SourceGenerators
``` ```
## 可选模块导入 ## 可选全局 using
发布后的运行时包支持可选的模块级自动导入,但默认关闭,避免在普通项目里无意污染命名空间。 NuGet 消费项目可显式开启模块级自动导入:
在 NuGet 消费项目中显式开启:
```xml ```xml
<PropertyGroup> <PropertyGroup>
@ -93,9 +96,7 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
</PropertyGroup> </PropertyGroup>
``` ```
启用后,项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入其推荐命名空间。 如果只想排除部分命名空间:
如果某几个命名空间不想导入,可以局部排除:
```xml ```xml
<ItemGroup> <ItemGroup>
@ -104,45 +105,36 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
</ItemGroup> </ItemGroup>
``` ```
> 该能力面向 NuGet 包消费场景。若你在本地解决方案中直接使用 `ProjectReference`,仍建议保留自己的 `GlobalUsings.cs` 或手写 > 该能力主要面向 NuGet 消费场景。仓库内 `ProjectReference` 方式仍建议显式维护自己的 `GlobalUsings.cs`
`using`
## 仓库结构 ## 仓库结构
```text ```text
GFramework.sln GFramework.sln
├─ GFramework.Cqrs/
├─ GFramework.Cqrs.Abstractions/
├─ GFramework.Core/ ├─ GFramework.Core/
├─ GFramework.Core.Abstractions/ ├─ GFramework.Core.Abstractions/
├─ GFramework.Core.SourceGenerators/ ├─ GFramework.Cqrs/
├─ GFramework.Core.SourceGenerators.Abstractions/ ├─ GFramework.Cqrs.Abstractions/
├─ GFramework.Game/ ├─ GFramework.Game/
├─ GFramework.Game.Abstractions/ ├─ GFramework.Game.Abstractions/
├─ GFramework.Game.SourceGenerators/
├─ GFramework.Godot/ ├─ GFramework.Godot/
├─ GFramework.Godot.SourceGenerators/ ├─ GFramework.Ecs.Arch/
├─ GFramework.Core.SourceGenerators/
├─ GFramework.Game.SourceGenerators/
├─ GFramework.Cqrs.SourceGenerators/ ├─ GFramework.Cqrs.SourceGenerators/
├─ GFramework.Godot.SourceGenerators/
├─ GFramework.SourceGenerators.Common/ ├─ GFramework.SourceGenerators.Common/
├─ docs/ └─ docs/
└─ docfx/
``` ```
## 兼容性
- **运行时/工具链**:基于 .NET 生态,具体以各项目 `*.csproj``TargetFramework` 为准。
- **引擎集成**:当前提供 Godot 集成模块Core 层可迁移至其他 .NET 场景。
## 贡献 ## 贡献
欢迎提交 Issue 与 Pull Request 提交功能或行为变更时,请把代码、测试和文档一起更新:
1. 提交 Issue 时请优先选择对应模板:`Bug Report / 缺陷报告``Feature Request / 功能建议``Documentation / 文档改进``Question / 使用咨询` 1. 先阅读对应模块目录下的 `README.md`
2. 提交前先搜索现有 Issues并阅读相关 README、文档或排障页面 2. 如果改动影响采用路径、安装方式、公共 API 或目录结构,同时更新 `docs/zh-CN/`
3. Fork 本仓库并创建特性分支 3. 对跨模块或多阶段任务,维护 `local-plan/todos/``local-plan/traces/`
4. 补充必要的测试或文档更新
5. 提交 PR描述变更背景、方案与验证结果
## 许可证 ## 许可证
项目采用 [Apache License 2.0](LICENSE)。 仓库当前采用 [Apache License 2.0](LICENSE)。

View File

@ -116,6 +116,7 @@ export default defineConfig({
text: '更多', text: '更多',
items: [ items: [
{ text: 'ECS', link: '/zh-CN/ecs/' }, { text: 'ECS', link: '/zh-CN/ecs/' },
{ text: '抽象接口', link: '/zh-CN/abstractions/' },
{ text: '源码生成器', link: '/zh-CN/source-generators' }, { text: '源码生成器', link: '/zh-CN/source-generators' },
{ text: '最佳实践', link: '/zh-CN/best-practices/' }, { text: '最佳实践', link: '/zh-CN/best-practices/' },
{ text: 'API 参考', link: '/zh-CN/api-reference' }, { text: 'API 参考', link: '/zh-CN/api-reference' },

View File

@ -0,0 +1,20 @@
# 抽象接口
`GFramework.*.Abstractions` 用来承载跨模块协作所需的契约,而不是运行时实现。
适合阅读这部分内容的场景:
- 你要做模块拆分,只想依赖接口,不想直接引用完整运行时
- 你要为测试、编辑器工具或插件提供替身实现
- 你在维护生成器、适配层或二次封装,需要先理解契约边界
## 阅读顺序
- Core 抽象层:[`core-abstractions.md`](./core-abstractions.md)
- Game 抽象层:[`game-abstractions.md`](./game-abstractions.md)
## 使用建议
- 如果你只是想直接使用框架功能,优先从对应运行时模块的 `README.md` 和栏目页开始。
- 只有在明确需要“契约层而非实现层”时,才单独依赖 `*.Abstractions` 包。
- 抽象层页面会解释接口分组与职责;实际安装与接入路径,仍应以运行时模块 README 与 `getting-started` 为主。

View File

@ -1,359 +1,107 @@
# 架构概览 # 入门指南
GFramework 采用经典的五层架构模式,结合 CQRS 和事件驱动设计,为游戏开发提供清晰、可维护的架构基础。 这一部分只回答三个问题:
## 核心架构模式 1. `GFramework` 由哪些模块组成
2. 第一次接入应该从哪个包开始
### 五层架构 3. 最小可运行路径是什么
``` 如果你还没决定具体用法,先阅读本栏目;如果你已经明确要用某个模块,直接进入对应模块目录下的 `README.md` 会更快。
┌─────────────────────────────────────────┐
│ View / UI │ ← 用户界面层 ## 推荐起步路径
├─────────────────────────────────────────┤
│ Controller │ ← 控制层 ### 只想先把架构跑起来
├─────────────────────────────────────────┤
│ System │ ← 业务逻辑层 `Core` 开始:
├─────────────────────────────────────────┤
│ Model │ ← 数据层 - `GeWuYou.GFramework.Core`
├─────────────────────────────────────────┤ - `GeWuYou.GFramework.Core.Abstractions`
│ Utility │ ← 工具层
└─────────────────────────────────────────┘ 这组包提供:
```
- `Architecture`
### 跨层操作机制 - `Model` / `System` / `Utility`
- 旧版 `Command` / `Query` 执行器
``` - 事件、属性、状态机、状态管理、资源、日志、协程等基础设施
Command ──┐
Query ──┼──→ 跨层操作(修改/查询数据) 对应文档:
Event ──┘
``` - [`../core/index.md`](../core/index.md)
- [`quick-start.md`](./quick-start.md)
### 生命周期阶段
### 想用新版 CQRS
```
初始化Init → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit → BeforeSystemInit → AfterSystemInit → Ready `Core` 基础上补:
销毁Destroy → Destroying → Destroyed
``` - `GeWuYou.GFramework.Cqrs`
- `GeWuYou.GFramework.Cqrs.Abstractions`
## 核心组件详解
这组包提供:
### 1. Architecture架构
- 统一 request dispatcher
应用的中央调度器,负责管理所有组件的生命周期。 - notification publish
- pipeline behaviors
```csharp - handler 注册与反射回退机制
public class GameArchitecture : Architecture
{ 对应文档:
protected override void Init()
{ - [`../core/cqrs.md`](../core/cqrs.md)
// 注册所有组件 - 仓库内模块入口:`GFramework.Cqrs/README.md`
RegisterModel(new PlayerModel());
RegisterSystem(new CombatSystem()); ### 想做游戏运行时
RegisterUtility(new StorageUtility());
} `Core` 基础上按需补:
}
``` - `GeWuYou.GFramework.Game`
- `GeWuYou.GFramework.Game.Abstractions`
**主要职责:**
这组包提供:
- 组件注册和管理
- 生命周期协调 - 内容配置系统
- 依赖注入 - 数据存取与设置
- 跨组件通信协调 - Scene / UI / Routing 抽象与运行时
- 文件存储和序列化
### 2. Model数据模型
对应文档:
应用的状态存储层,只负责数据的存储和管理。
- [`../game/index.md`](../game/index.md)
```csharp - 仓库内模块入口:`GFramework.Game/README.md`
public class PlayerModel : AbstractModel
{ ### 想接入 Godot
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<string> Name { get; } = new("Player"); 继续叠加:
protected override void OnInit() - `GeWuYou.GFramework.Godot`
{
// 监听自身数据变化 对应文档:
Health.Register(OnHealthChanged);
} - [`../godot/index.md`](../godot/index.md)
- 仓库内模块入口:`GFramework.Godot/README.md`
private void OnHealthChanged(int newHealth)
{ ## Source Generators 什么时候装
if (newHealth <= 0)
this.SendEvent(new PlayerDiedEvent()); 只在需要编译期生成代码时再装:
}
} - `GeWuYou.GFramework.Core.SourceGenerators`
``` - `GeWuYou.GFramework.Game.SourceGenerators`
- `GeWuYou.GFramework.Cqrs.SourceGenerators`
**设计原则:** - `GeWuYou.GFramework.Godot.SourceGenerators`
- 只存储数据,不包含业务逻辑 典型场景:
- 使用 BindableProperty 实现响应式数据
- 通过事件通知数据变化 - 自动生成日志、上下文绑定、模块注册代码
- 从 `schema` 生成游戏配置类型
### 3. System业务系统 - 为 CQRS handlers 生成注册表
- 生成 Godot 节点、场景和 UI 包装代码
应用的业务逻辑处理层。
## 建议阅读顺序
```csharp
public class CombatSystem : AbstractSystem 1. [`quick-start.md`](./quick-start.md)
{ 2. 你准备使用的模块 README
protected override void OnInit() 3. 对应栏目页,例如 `core/``game/``godot/`
{ 4. 需要更完整示例时,再进入 `tutorials/`
// 订阅相关事件
this.GetEvent<AttackEvent>().Register(OnAttack); ## 注意
}
- 旧文档里有一些早期示例已经和当前 API 漂移。本栏目以后只保留经过代码或测试核对的最小路径。
private void OnAttack(AttackEvent e) - 若根 README、模块 README 与某篇专题页冲突,以模块 README 和当前代码为准。
{
var attacker = e.Attacker;
var target = e.Target;
// 计算伤害
var damage = CalculateDamage(attacker, target);
// 更新目标生命值
target.Health.Value -= damage;
// 发送伤害事件
this.SendEvent(new DamageEvent(target, damage));
}
private int CalculateDamage(Entity attacker, Entity target)
{
return Mathf.Max(1, attacker.Attack.Value - target.Defense.Value);
}
}
```
**设计原则:**
- 处理业务逻辑,不直接存储数据
- 通过事件与其他组件通信
- 从 Model 获取数据,向 Model 发送更新
### 4. Controller控制器
连接 UI 和业务逻辑的桥梁。
```csharp
public class PlayerController : IController
{
private IArchitecture _architecture;
private PlayerModel _playerModel;
public PlayerController(IArchitecture architecture)
{
_architecture = architecture;
_playerModel = architecture.GetModel<PlayerModel>();
// 监听模型变化并更新 UI
_playerModel.Health.RegisterWithInitValue(UpdateHealthDisplay);
}
public void OnPlayerInput(Vector2 direction)
{
// 将用户输入转换为命令
_architecture.SendCommand(new MovePlayerCommand { Direction = direction });
}
private void UpdateHealthDisplay(int health)
{
// 更新 UI 显示
Console.WriteLine($"Player Health: {health}");
}
}
```
**核心功能:**
- 接收用户输入
- 发送命令到系统
- 监听模型变化更新 UI
- 协调 UI 和业务逻辑
### 5. Utility工具类
提供无状态的辅助功能。
```csharp
public class StorageUtility : IUtility
{
public void SaveData<T>(string key, T data)
{
// 实现数据保存逻辑
}
public T LoadData<T>(string key, T defaultValue = default)
{
// 实现数据加载逻辑
return defaultValue;
}
}
```
**使用场景:**
- 数据存储和读取
- 数学计算工具
- 字符串处理
- 网络通信辅助
## 通信机制
### 1. Command命令
用于修改应用状态的操作:
```csharp
public class MovePlayerCommand : AbstractCommand
{
public Vector2 Direction { get; set; }
protected override void OnDo()
{
// 执行移动逻辑
this.SendEvent(new PlayerMovedEvent { Position = CalculateNewPosition() });
}
}
```
### 2. Query查询
用于查询应用状态:`
```csharp
public class GetPlayerHealthQuery : AbstractQuery<int>
{
protected override int OnDo()
{
var playerModel = this.GetModel<PlayerModel>();
return playerModel.Health.Value;
}
}
```
### 3. Event事件
组件间通信的主要机制:`
```
// 发送事件
this.SendEvent(new PlayerDiedEvent());
// 监听事件
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
```
## 响应式编程
### BindableProperty
```csharp
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<string> Name { get; } = new("Player");
}
// 使用方式
playerModel.Health.Value = 50; // 自动触发所有监听器
playerModel.Health.Register(newValue => {
Console.WriteLine($"Health changed to: {newValue}");
});
```
### 数据绑定优势
- **自动更新**:数据变化自动通知监听者
- **内存安全**:自动管理监听器生命周期
- **类型安全**:编译时类型检查
- **性能优化**:只在值真正改变时触发
## 最佳实践
### 1. 分层职责明确
```csharp
// ✅ 正确Model 只存储数据
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
}
// ❌ 错误Model 包含业务逻辑
public class PlayerModel : AbstractModel
{
public void TakeDamage(int damage) // 业务逻辑应该在 System 中
{
Health.Value -= damage;
}
}
```
### 2. 事件驱动设计
```csharp
// ✅ 正确:使用事件解耦
public class CombatSystem : AbstractSystem
{
private void OnPlayerAttack(PlayerAttackEvent e)
{
// 处理攻击逻辑
this.SendEvent(new EnemyDamagedEvent { Damage = CalculateDamage() });
}
}
// ❌ 错误:直接调用其他组件
public class CombatSystem : AbstractSystem
{
private void OnPlayerAttack(PlayerAttackEvent e)
{
var enemySystem = this.GetSystem<EnemySystem>(); // 紧耦合
enemySystem.TakeDamage(CalculateDamage());
}
}
```
### 3. 命令查询分离
```csharp
// ✅ 正确:明确区分命令和查询
public class MovePlayerCommand : AbstractCommand { } // 修改状态
public class GetPlayerPositionQuery : AbstractQuery<Vector2> { } // 查询状态
// ❌ 错误:混合读写操作
public class PlayerManager
{
public void MoveAndGetPosition(Vector2 direction, out Vector2 position) // 职责不清
{
// ...
}
}
```
## 架构优势
### 1. 可维护性
- 清晰的职责分离
- 松耦合的组件设计
- 易于定位和修复问题
### 2. 可测试性
- 组件可独立测试
- 依赖可轻松模拟
- 支持单元测试和集成测试
### 3. 可扩展性
- 新功能通过添加组件实现
- 现有组件无需修改
- 支持插件化架构
### 4. 团队协作
- 统一的架构规范
- 易于新人上手
- 减少代码冲突

View File

@ -1,323 +1,105 @@
# 快速开始 # 快速开始
指南将帮助您快速构建第一个基于 GFramework 的应用程序 页给出一个只依赖 `Core` 的最小路径,用来确认你已经成功接入 `Architecture``Model``System` 与旧版命令执行器
## 1. 创建项目架构 > 说明:当前仓库同时存在旧版 `Command` / `Query` 执行器与新版 `CQRS` runtime。本页故意先用最短路径说明基础架构如何跑起来如果你要写新功能随后应继续阅读 [`../core/cqrs.md`](../core/cqrs.md)。
首先定义您的应用架构: ## 1. 安装最小依赖
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
## 2. 定义一个最小架构
```csharp ```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()
{ {
// 注册模型 - 存储应用状态 RegisterModel(new CounterModel());
RegisterModel(new PlayerModel()); RegisterSystem(new CounterSystem());
RegisterModel(new GameStateModel());
// 注册系统 - 处理业务逻辑
RegisterSystem(new PlayerSystem());
RegisterSystem(new GameLogicSystem());
// 注册工具类 - 提供辅助功能
RegisterUtility(new StorageUtility());
} }
} }
``` ```
## 2. 定义数据模型 这里要点只有两个:
创建您的数据模型: - 架构入口是 `Architecture`
- 当前版本使用 `protected override void OnInitialize()` 注册模型、系统和工具
## 3. 定义一个模型
```csharp ```csharp
public class PlayerModel : AbstractModel using GFramework.Core.Model;
using GFramework.Core.Property;
public sealed class CounterModel : AbstractModel
{ {
// 使用可绑定属性实现响应式数据 public BindableProperty<int> Count { get; } = new(0);
public BindableProperty<string> Name { get; } = new("Player");
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> Score { get; } = new(0);
protected override void OnInit() protected override void OnInit()
{ {
// 监听健康值变化
Health.Register(OnHealthChanged);
} }
private void OnHealthChanged(int newHealth)
{
if (newHealth <= 0)
{
this.SendEvent(new PlayerDiedEvent());
}
}
}
public class GameStateModel : AbstractModel
{
public BindableProperty<bool> IsGameRunning { get; } = new(false);
public BindableProperty<int> CurrentLevel { get; } = new(1);
} }
``` ```
## 3. 实现业务逻辑 `BindableProperty<T>` 适合承载可观察状态;如果你只需要一个最小例子,保持 `OnInit()` 为空即可。
创建处理业务逻辑的系统: ## 4. 定义一个系统
```csharp ```csharp
public class PlayerSystem : AbstractSystem using GFramework.Core.Systems;
public sealed class CounterSystem : AbstractSystem
{ {
protected override void OnInit() protected override void OnInit()
{ {
// 监听玩家输入事件
this.RegisterEvent<PlayerMoveEvent>(OnPlayerMove);
this.RegisterEvent<PlayerAttackEvent>(OnPlayerAttack);
}
private void OnPlayerMove(PlayerMoveEvent e)
{
var playerModel = this.GetModel<PlayerModel>();
// 处理移动逻辑
Console.WriteLine($"Player moved to {e.Direction}");
}
private void OnPlayerAttack(PlayerAttackEvent e)
{
var playerModel = this.GetModel<PlayerModel>();
// 处理攻击逻辑
playerModel.Score.Value += 10;
this.SendEvent(new EnemyDamagedEvent { Damage = 25 });
}
}
public class GameLogicSystem : AbstractSystem
{
protected override void OnInit()
{
this.RegisterEvent<EnemyDamagedEvent>(OnEnemyDamaged);
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
}
private void OnEnemyDamaged(EnemyDamagedEvent e)
{
Console.WriteLine($"Enemy took {e.Damage} damage");
// 检查是否需要升级关卡
CheckLevelProgress();
}
private void OnPlayerDied(PlayerDiedEvent e)
{
var gameState = this.GetModel<GameStateModel>();
gameState.IsGameRunning.Value = false;
Console.WriteLine("Game Over!");
}
private void CheckLevelProgress()
{
// 实现关卡进度检查逻辑
} }
} }
``` ```
## 4. 定义事件 ## 5. 定义一个命令
创建应用中使用的事件:
```csharp ```csharp
public class PlayerMoveEvent : IEvent using GFramework.Core.Command;
{
public Vector2 Direction { get; set; }
}
public class PlayerAttackEvent : IEvent public sealed class IncreaseCountCommand : AbstractCommand
{ {
public Vector2 TargetPosition { get; set; } protected override void OnExecute()
} {
var model = GetModel<CounterModel>();
public class PlayerDiedEvent : IEvent model.Count.Value += 1;
{ }
// 玩家死亡事件
}
public class EnemyDamagedEvent : IEvent
{
public int Damage { get; set; }
} }
``` ```
## 5. 创建控制器 `AbstractCommand` 继承自 `ContextAwareBase`,所以命令内部可以直接访问 `GetModel<T>()``GetSystem<T>()` 等上下文方法。
实现控制器来连接 UI 和业务逻辑: ## 6. 初始化并执行
```csharp ```csharp
using GFramework.Core.Abstractions.Controller; var architecture = new CounterArchitecture();
using GFramework.SourceGenerators.Abstractions.Rule; architecture.Initialize();
[ContextAware] architecture.Context.SendCommand(new IncreaseCountCommand());
public partial class GameController : IController
{
private PlayerModel _playerModel;
private GameStateModel _gameStateModel;
public void Initialize() var count = architecture.Context.GetModel<CounterModel>().Count.Value;
{ Console.WriteLine(count); // 1
_playerModel = this.GetModel<PlayerModel>();
_gameStateModel = this.GetModel<GameStateModel>();
// 初始化事件监听
InitializeEventListeners();
}
private void InitializeEventListeners()
{
// 监听模型变化并更新 UI
_playerModel.Health.RegisterWithInitValue(OnHealthChanged);
_playerModel.Score.RegisterWithInitValue(OnScoreChanged);
_gameStateModel.IsGameRunning.Register(OnGameStateChanged);
}
public void StartGame()
{
_gameStateModel.IsGameRunning.Value = true;
this.SendEvent(new GameStartEvent());
Console.WriteLine("Game started!");
}
public void MovePlayer(Vector2 direction)
{
this.SendCommand(new MovePlayerCommand { Direction = direction });
}
public void PlayerAttack(Vector2 target)
{
this.SendCommand(new AttackCommand { TargetPosition = target });
}
// UI 更新回调
private void OnHealthChanged(int health)
{
UpdateHealthDisplay(health);
}
private void OnScoreChanged(int score)
{
UpdateScoreDisplay(score);
}
private void OnGameStateChanged(bool isRunning)
{
UpdateGameStatusDisplay(isRunning);
}
private void UpdateHealthDisplay(int health)
{
// 更新血条 UI
Console.WriteLine($"Health: {health}");
}
private void UpdateScoreDisplay(int score)
{
// 更新分数显示
Console.WriteLine($"Score: {score}");
}
private void UpdateGameStatusDisplay(bool isRunning)
{
// 更新游戏状态显示
Console.WriteLine($"Game running: {isRunning}");
}
}
``` ```
## 6. 定义命令 如果你能走通这一步,说明以下链路已经成立:
创建命令来封装用户操作: - 架构初始化
- 模型 / 系统注册
```csharp - 上下文访问
public class MovePlayerCommand : AbstractCommand - 旧版命令执行
{
public Vector2 Direction { get; set; }
protected override void OnDo()
{
// 发送移动事件
this.SendEvent(new PlayerMoveEvent { Direction = Direction });
}
}
public class AttackCommand : AbstractCommand
{
public Vector2 TargetPosition { get; set; }
protected override void OnDo()
{
// 发送攻击事件
this.SendEvent(new PlayerAttackEvent { TargetPosition = TargetPosition });
}
}
```
## 7. 运行应用
现在让我们运行这个简单的应用:
```csharp
class Program
{
static void Main(string[] args)
{
// 创建并初始化架构
var architecture = new GameArchitecture();
architecture.Initialize();
// 创建控制器
var gameController = new GameController();
gameController.Initialize();
// 开始游戏
gameController.StartGame();
// 模拟玩家操作
gameController.MovePlayer(new Vector2(1, 0));
gameController.PlayerAttack(new Vector2(5, 5));
// 模拟玩家受伤
var playerModel = architecture.GetModel<PlayerModel>();
playerModel.Health.Value = 50;
// 模拟玩家死亡
playerModel.Health.Value = 0;
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
```
## 8. 运行结果
执行程序后,您应该看到类似以下输出:
```
Game started!
Game running: True
Player moved to (1, 0)
Player took 25 damage
Score: 10
Health: 50
Health: 0
Player died
Game Over!
Game running: False
Press any key to exit...
```
## 下一步 ## 下一步
这个简单的示例展示了 GFramework 的核心概念: - 想切到推荐的新请求模型:看 [`../core/cqrs.md`](../core/cqrs.md)
- 想进入游戏层能力:看 [`../game/index.md`](../game/index.md)
1. **架构模式** - 清晰的分层结构 - 想看模块入口而不是栏目页:回到对应模块目录下的 `README.md`
2. **响应式数据** - BindableProperty 自动更新
3. **事件驱动** - 松耦合的组件通信
4. **命令模式** - 封装用户操作