Compare commits

...

20 Commits

Author SHA1 Message Date
gewuyou
a1dbed3c8d docs(cqrs): 刷新 Cqrs 文档入口
- 更新 Cqrs family landing、API 参考与 source-generators 导航

- 新增 CQRS handler registry 专题页并补充 XML inventory

- 补充 runtime 与 generator 内部类型 XML 注释

- 记录 RP-004 验证结果与后续恢复点
2026-04-22 18:51:05 +08:00
GeWuYou
da0ae700a3 docs(ecs): 重写 Ecs.Arch 文档入口
- 重写 Ecs.Arch README 与 ecs 栏目页面,使采用路径对齐当前源码和集成测试

- 补充 Ecs.Arch.Abstractions 的 XML inventory 与抽象页阅读链路

- 更新 documentation-full-coverage-governance 的恢复点、验证结果和下一波次计划
2026-04-22 17:19:33 +08:00
GeWuYou
af21f16c09 docs(core): 补齐 Core XML 覆盖基线清单
- 补充 Core 与 Core.Abstractions README 的类型族级 XML 覆盖基线入口

- 更新 Core 与 Core.Abstractions landing page 的 XML inventory、代表类型和阅读重点

- 同步刷新 documentation-full-coverage-governance 的 tracking 与 trace 恢复点
2026-04-22 16:06:44 +08:00
gewuyou
704fdaa2c8 docs(core): 对齐 Core 模块文档入口
- 更新 Core 与 Core.Abstractions README 的目录映射和 XML 阅读入口

- 重写 Core Abstractions 页面,改为契约边界与最小接入路径说明

- 补充 Core landing page、API 参考入口和 ai-plan 跟踪记录
2026-04-22 15:32:22 +08:00
GeWuYou
cc49b8638f docs(governance): 建立长期文档治理入口
- 新增 documentation-full-coverage-governance active topic 与首轮 inventory、trace 入口
- 补充 GFramework.Ecs.Arch.Abstractions README、抽象接口页面与导航映射
- 更新 API 参考页与根 README,明确内部支撑模块 owner 和阅读链路
2026-04-22 15:07:15 +08:00
gewuyou
45a87c6988 docs(ai-plan): 归档文档治理主题
- 迁移 documentation-governance-and-refresh 主题到 public archive 目录

- 更新 public README 的 active topic 与 worktree 映射

- 收口归档内 tracking 和 trace 的完成状态与恢复说明
2026-04-22 14:31:29 +08:00
gewuyou
310aeafa57 docs(ai-plan): 收口文档治理恢复入口
- 归档 documentation-governance-and-refresh 截至 2026-04-22 的跟踪与 trace 历史

- 更新 active tracking 与 trace,只保留恢复点、风险、验证结果和下一步

- 补充 Godot 栏目稳定性复核后的归档决策与 PR #268 follow-up 入口
2026-04-22 14:23:15 +08:00
gewuyou
b2a5555c75
Merge pull request #270 from GeWuYou/docs/sdk-update-documentation
Docs/sdk update documentation
2026-04-22 14:14:54 +08:00
GeWuYou
76e7f68544 docs(godot): 收口日志系统文档
- 重写 docs/zh-CN/godot/logging.md,按当前 provider、控制台输出语义与 [Log] 边界整理采用路径

- 更新 documentation-governance-and-refresh 的 tracking 与 trace,推进恢复点到 RP-017 并记录验证结果
2026-04-22 13:33:35 +08:00
GeWuYou
3ba1e3f202 docs(godot): 收口 signal 与 extensions 文档
- 更新 godot signal 页面,明确 Signal(...)、SignalBuilder 与 [BindNodeSignal] 的分工

- 更新 godot extensions 页面,收敛到当前存在的扩展成员与生命周期边界

- 补充 documentation-governance-and-refresh 跟踪与 trace,记录 RP-016 和验证结果
2026-04-22 13:20:18 +08:00
gewuyou
03ecbe5989 docs(godot): 重写场景与UI接入文档
- 更新 Godot 场景文档,按当前 factory、registry、root、provider 与 AutoScene 接线收口采用路径
- 更新 Godot UI 文档,明确 layer 语义、provider 要求、root 接线与 AutoUiPage 用法
- 同步 documentation-governance-and-refresh 跟踪与 trace 到 RP-015,并把下一步切到 signal/extensions
2026-04-22 13:03:14 +08:00
gewuyou
5d436694f8 docs(godot): 收口 Godot 入口与架构文档
- 更新 Godot landing page,使包关系、最小接入路径与运行时边界回到源码与测试契约

- 重写 Godot architecture 页面,明确锚点生命周期、模块挂接顺序与 IGodotModule 契约边界

- 更新 documentation-governance topic 的 RP-014 跟踪与验证记录,标记下一轮收口目标
2026-04-22 12:50:05 +08:00
GeWuYou
a77f79b3e2 docs(tutorials): 重写 Godot 集成教程
- 重写 Godot 集成教程,按当前源码与 CoreGrid 采用路径收口 project.godot、GetNode 与 BindNodeSignal 的接线说明

- 更新 tutorials 入口摘要与 documentation-governance-and-refresh 跟踪,记录 RP-013、验证结果和下一恢复点
2026-04-22 12:33:53 +08:00
gewuyou
aec1931c74 docs(source-generators): 收口 AutoRegister 文档语义
- 更新 auto-register-exported-collections 专题页,补齐 frontmatter 并按当前源码与测试收口成员形状、匹配规则、null-skip 行为与诊断边界

- 补充 documentation-governance-and-refresh 的 RP-012 恢复点,记录 godot-integration tutorial 仍残留旧 API 的跟进风险
2026-04-22 11:39:35 +08:00
gewuyou
214f52b6c2 docs(source-generators): 刷新 Godot 生成器文档
- 更新 Godot 项目元数据、GetNode 与 BindNodeSignal 专题页,按当前源码与测试收口最小接入路径、生成语义与诊断边界

- 补充 documentation-governance-and-refresh 的 RP-011 恢复点、验证结果与下一步建议
2026-04-22 11:25:49 +08:00
gewuyou
3425b299f0
Merge pull request #268 from GeWuYou/docs/sdk-update-documentation
docs(game): 收口场景与UI专题文档
2026-04-22 11:15:34 +08:00
gewuyou
a980a042ae
Merge pull request #267 from GeWuYou/fix/analyzer-warning-reduction-batch
fix(core): 统一事件签名并清理MA0046告警
2026-04-21 18:21:38 +08:00
GeWuYou
a9f86348ff fix(core): 修复 AsyncLogAppender 刷新竞态
- 修复 AsyncLogAppender 在队列已被后台线程提前清空时 Flush 仍可能超时失败的问题
- 新增 AsyncLogAppender 已处理队列场景的稳定回归测试并重新验证 GFramework.Core.Tests
- 更新 analyzer-warning-reduction 的 tracking 与 trace 记录 PR267 failed-test follow-up
2026-04-21 17:32:50 +08:00
GeWuYou
685897f2de fix(core): 收口 PR267 事件契约遗留问题
- 修复 AsyncLogAppender 接口刷新路径重复触发完成事件,并补充单次通知回归测试
- 补充 Architecture、CoroutineExceptionEventArgs 与阶段协调器的事件契约注释
- 更新 PhaseChanged 迁移文档与 analyzer-warning-reduction recovery 记录
2026-04-21 16:50:56 +08:00
GeWuYou
8831cb42a8 fix(core): 统一事件签名并清理MA0046告警
hBc
2026-04-21 16:15:36 +08:00
60 changed files with 4002 additions and 8187 deletions

View File

@ -0,0 +1,24 @@
using GFramework.Core.Abstractions.Enums;
namespace GFramework.Core.Abstractions.Architectures;
/// <summary>
/// 表示架构阶段变化事件的数据。
/// 该类型用于向事件订阅者传递当前已进入的阶段值。
/// </summary>
public sealed class ArchitecturePhaseChangedEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="ArchitecturePhaseChangedEventArgs" /> 的新实例。
/// </summary>
/// <param name="phase">当前已进入的架构阶段。</param>
public ArchitecturePhaseChangedEventArgs(ArchitecturePhase phase)
{
Phase = phase;
}
/// <summary>
/// 获取当前已进入的架构阶段。
/// </summary>
public ArchitecturePhase Phase { get; }
}

View File

@ -0,0 +1,26 @@
namespace GFramework.Core.Abstractions.Logging;
/// <summary>
/// 表示异步日志刷新完成事件的数据。
/// 该类型用于告知订阅者本次刷新是否在超时时间内成功完成。
/// </summary>
public sealed class AsyncLogFlushCompletedEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="AsyncLogFlushCompletedEventArgs" /> 的新实例。
/// </summary>
/// <param name="success">
/// 刷新是否成功完成。
/// 为 <see langword="true" /> 表示所有待处理日志都已在超时前落地;
/// 为 <see langword="false" /> 表示刷新超时或输出器已不可用。
/// </param>
public AsyncLogFlushCompletedEventArgs(bool success)
{
Success = success;
}
/// <summary>
/// 获取刷新是否成功完成。
/// </summary>
public bool Success { get; }
}

View File

@ -2,6 +2,8 @@
`GFramework.Core.Abstractions` 承载 `Core` 运行时对应的接口、枚举和值对象,用来定义跨模块协作边界。 `GFramework.Core.Abstractions` 承载 `Core` 运行时对应的接口、枚举和值对象,用来定义跨模块协作边界。
它只描述契约,不提供默认的架构、事件、状态、资源或 IoC 实现;这些实现都在 `GFramework.Core` 中。
## 什么时候单独依赖它 ## 什么时候单独依赖它
- 你在做插件、适配层或扩展包,只想依赖契约,不想把完整运行时拉进来 - 你在做插件、适配层或扩展包,只想依赖契约,不想把完整运行时拉进来
@ -20,23 +22,34 @@
## 契约地图 ## 契约地图
| 目录 | 作用 | | 目录 | 作用 |
| --- | --- | | --- | --- |
| `Architectures/` | `IArchitecture`、模块、阶段监听与服务管理契约 | | `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、上下文、模块、服务模块、阶段监听、注册表基类与生命周期契约 |
| `Command/` / `Query/` | 旧版命令与查询执行器接口 | | `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` | 组件角色接口、优先级 / key 值对象、上下文感知约束与扩展边界 |
| `Controller/` | `IController` | | `Command/` `Query/` `Cqrs/` | 旧版命令 / 查询执行器接口,以及 `ICqrsRuntime` 这类新请求模型接线契约 |
| `Events/` | 事件契约、解绑接口与传播上下文 | | `Events/` `Property/` `State/` `StateManagement/` | 事件总线、解绑对象、可绑定属性、状态机、Store / reducer / middleware 契约 |
| `Model/` / `Systems/` / `Utility/` | 核心组件接口 | | `Coroutine/` `Time/` `Pause/` `Concurrency/` | 协程状态、时间源、暂停栈、键控异步锁和统计对象 |
| `State/` / `StateManagement/` | 状态机、Store、reducer、selector 契约 | | `Resource/` `Pool/` `Logging/` `Localization/` | 资源句柄、对象池、日志、日志工厂、本地化表与格式化契约 |
| `Property/` | `IBindableProperty` 与只读属性接口 | | `Configuration/` `Environment/` | 配置管理器、环境对象与运行时环境访问契约 |
| `Resource/` | 资源管理与释放策略契约 | | `Data/` `Serializer/` `Storage/` `Versioning/` | 数据装载、序列化、存储与版本化契约 |
| `Localization/` | 本地化表、格式化与异常类型 | | `Enums/` `Properties/` | 架构阶段枚举,以及架构 / logger 相关属性键 |
| `Logging/` | logger、log entry、factory 相关契约 |
| `Ioc/` | `IIocContainer` | ## XML 覆盖基线
| `Lifecycle/` | 初始化 / 销毁生命周期契约 |
| `Coroutine/` | 时间源、yield 指令与协程状态枚举 | 截至 `2026-04-22`,已按顶层目录对 `GFramework.Core.Abstractions` 的公开 / 内部类型声明做过一轮轻量盘点;当前契约目录族的类型声明都已带
| `Pause/` | 暂停栈、token 与状态事件 | XML 注释。这里记录的是类型族级基线,成员级契约细节仍需要在后续波次继续审计。
| `Storage/` / `Serializer/` / `Versioning/` | 通用存储、序列化与版本化契约 |
| 类型族 | 基线状态 | 代表类型 |
| --- | --- | --- |
| `Architectures/` `Lifecycle/` `Registries/` | `20/20` 个类型声明已带 XML 注释 | `IArchitecture``IArchitectureContext``IServiceModule``KeyValueRegistryBase<TKey, TValue>` |
| `Command/` `Query/` `Cqrs/` | `10/10` 个类型声明已带 XML 注释 | `ICommandExecutor``IAsyncQueryExecutor``ICqrsRuntime` |
| `Events/` `Property/` `State/` `StateManagement/` | `25/25` 个类型声明已带 XML 注释 | `IEventBus``IBindableProperty<T>``IStateMachine``IStore<TState>` |
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `17/17` 个类型声明已带 XML 注释 | `IYieldInstruction``ITimeProvider``IPauseStackManager``IAsyncKeyLockManager` |
| `Resource/` `Pool/` `Logging/` `Localization/` | `27/27` 个类型声明已带 XML 注释 | `IResourceManager``IObjectPoolSystem``ILogger``ILocalizationManager` |
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `7/7` 个类型声明已带 XML 注释 | `IConfigurationManager``IEnvironment``ILoadableFrom<T>``ISerializer``IStorage` |
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `19/19` 个类型声明已带 XML 注释 | `IPrioritized``IController``IModel``ISystem``IContextUtility``ArchitecturePhase` |
完整 inventory 与阅读顺序见 `docs/zh-CN/abstractions/core-abstractions.md`
## 采用建议 ## 采用建议
@ -44,8 +57,18 @@
- 若你只需要对接口编程,可以仅引用本包,再在应用层自行提供实现 - 若你只需要对接口编程,可以仅引用本包,再在应用层自行提供实现
- 若你在写上层模块,优先把公共契约放在 `*.Abstractions`,实现放在对应 runtime 包 - 若你在写上层模块,优先把公共契约放在 `*.Abstractions`,实现放在对应 runtime 包
## 重点 XML 关注点
如果你在做契约审计、模块拆分或测试替身,优先看这些类型族的 XML 文档:
- 架构与模块入口:`IArchitecture``IArchitectureContext``IServiceModule`
- 运行时基础设施:`IIocContainer``ILogger``IResourceManager``IConfigurationManager`
- 状态与并发能力:`IStateMachine``IStore``IAsyncKeyLockManager``ITimeProvider`
- 迁移与组合边界:`ICommandExecutor``IQueryExecutor``ICqrsRuntime`
## 对应文档 ## 对应文档
- 抽象接口栏目:[`../docs/zh-CN/abstractions/index.md`](../docs/zh-CN/abstractions/index.md) - 抽象接口栏目:[`../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 抽象页:[`../docs/zh-CN/abstractions/core-abstractions.md`](../docs/zh-CN/abstractions/core-abstractions.md)
- Core 运行时入口:[`../GFramework.Core/README.md`](../GFramework.Core/README.md) - Core 运行时入口:[`../GFramework.Core/README.md`](../GFramework.Core/README.md)
- API 参考入口:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)

View File

@ -62,6 +62,35 @@ public class ArchitectureLifecycleBehaviorTests
await architecture.DestroyAsync(); await architecture.DestroyAsync();
} }
/// <summary>
/// 验证阶段变更事件会以架构实例作为 sender并通过事件参数暴露阶段值。
/// </summary>
[Test]
public async Task InitializeAsync_Should_Raise_PhaseChanged_With_Sender_And_EventArgs()
{
var architecture = new PhaseTrackingArchitecture();
var observations = new List<(object? Sender, ArchitecturePhase Phase)>();
architecture.PhaseChanged += (sender, eventArgs) => observations.Add((sender, eventArgs.Phase));
await architecture.InitializeAsync();
Assert.That(observations, Is.Not.Empty);
Assert.That(observations.All(item => ReferenceEquals(item.Sender, architecture)), Is.True);
Assert.That(observations.Select(static item => item.Phase), Is.EqualTo(new[]
{
ArchitecturePhase.BeforeUtilityInit,
ArchitecturePhase.AfterUtilityInit,
ArchitecturePhase.BeforeModelInit,
ArchitecturePhase.AfterModelInit,
ArchitecturePhase.BeforeSystemInit,
ArchitecturePhase.AfterSystemInit,
ArchitecturePhase.Ready
}));
await architecture.DestroyAsync();
}
/// <summary> /// <summary>
/// 验证用户初始化失败时,等待 Ready 的任务会失败并进入 FailedInitialization 阶段。 /// 验证用户初始化失败时,等待 Ready 的任务会失败并进入 FailedInitialization 阶段。
/// </summary> /// </summary>
@ -183,7 +212,7 @@ public class ArchitectureLifecycleBehaviorTests
public PhaseTrackingArchitecture(Action? onInitializeAction = null) public PhaseTrackingArchitecture(Action? onInitializeAction = null)
{ {
_onInitializeAction = onInitializeAction; _onInitializeAction = onInitializeAction;
PhaseChanged += phase => PhaseHistory.Add(phase); PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
} }
/// <summary> /// <summary>
@ -214,7 +243,7 @@ public class ArchitectureLifecycleBehaviorTests
public DestroyOrderArchitecture(List<string> destroyOrder) public DestroyOrderArchitecture(List<string> destroyOrder)
{ {
_destroyOrder = destroyOrder; _destroyOrder = destroyOrder;
PhaseChanged += phase => PhaseHistory.Add(phase); PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
} }
/// <summary> /// <summary>
@ -247,7 +276,7 @@ public class ArchitectureLifecycleBehaviorTests
public FailingInitializationArchitecture(List<string> destroyOrder) public FailingInitializationArchitecture(List<string> destroyOrder)
{ {
_destroyOrder = destroyOrder; _destroyOrder = destroyOrder;
PhaseChanged += phase => PhaseHistory.Add(phase); PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
} }
/// <summary> /// <summary>

View File

@ -43,6 +43,6 @@ public abstract class TestArchitectureBase : Architecture
_postRegistrationHook?.Invoke(this); _postRegistrationHook?.Invoke(this);
// 订阅阶段变更事件以记录历史 // 订阅阶段变更事件以记录历史
PhaseChanged += phase => PhaseHistory.Add(phase); PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
} }
} }

View File

@ -331,6 +331,63 @@ public class CoroutineSchedulerTests
Assert.That(_scheduler.ActiveCoroutineCount, Is.EqualTo(0)); Assert.That(_scheduler.ActiveCoroutineCount, Is.EqualTo(0));
} }
/// <summary>
/// 验证完成事件会把调度器实例、句柄和完成结果暴露给订阅者。
/// </summary>
[Test]
public void Run_Should_Raise_OnCoroutineFinished_With_EventArgs()
{
object? observedSender = null;
CoroutineFinishedEventArgs? observedArgs = null;
_scheduler.OnCoroutineFinished += (sender, eventArgs) =>
{
observedSender = sender;
observedArgs = eventArgs;
};
var handle = _scheduler.Run(CreateSimpleCoroutine());
_scheduler.Update();
Assert.Multiple(() =>
{
Assert.That(observedSender, Is.SameAs(_scheduler));
Assert.That(observedArgs, Is.Not.Null);
Assert.That(observedArgs!.Handle, Is.EqualTo(handle));
Assert.That(observedArgs.CompletionStatus, Is.EqualTo(CoroutineCompletionStatus.Completed));
Assert.That(observedArgs.Exception, Is.Null);
});
}
/// <summary>
/// 验证异常事件会把调度器实例、失败句柄和异常对象暴露给订阅者。
/// </summary>
[Test]
public async Task Scheduler_Should_Raise_OnCoroutineException_With_EventArgs()
{
var exceptionSource =
new TaskCompletionSource<(object? Sender, CoroutineExceptionEventArgs EventArgs)>(
TaskCreationOptions.RunContinuationsAsynchronously);
_scheduler.OnCoroutineException += (sender, eventArgs) =>
{
exceptionSource.TrySetResult((sender, eventArgs));
};
var handle = _scheduler.Run(CreateExceptionCoroutine());
_scheduler.Update();
var observation = await exceptionSource.Task.WaitAsync(TimeSpan.FromSeconds(3));
Assert.Multiple(() =>
{
Assert.That(observation.Sender, Is.SameAs(_scheduler));
Assert.That(observation.EventArgs.Handle, Is.EqualTo(handle));
Assert.That(observation.EventArgs.Exception, Is.TypeOf<InvalidOperationException>());
Assert.That(observation.EventArgs.Exception.Message, Is.EqualTo("Test exception"));
});
}
/// <summary> /// <summary>
/// 验证协程调度器应该扩展容量当槽位已满 /// 验证协程调度器应该扩展容量当槽位已满
/// </summary> /// </summary>

View File

@ -77,6 +77,73 @@ public class AsyncLogAppenderTests
Assert.That(innerAppender.Entries.Count, Is.EqualTo(100)); Assert.That(innerAppender.Entries.Count, Is.EqualTo(100));
} }
[Test]
public void Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result()
{
var innerAppender = new TestAppender();
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10);
object? observedSender = null;
AsyncLogFlushCompletedEventArgs? observedArgs = null;
asyncAppender.Append(new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Flush check", null, null));
asyncAppender.OnFlushCompleted += (sender, eventArgs) =>
{
observedSender = sender;
observedArgs = eventArgs;
};
var result = asyncAppender.Flush(TimeSpan.FromSeconds(1));
Assert.Multiple(() =>
{
Assert.That(observedSender, Is.SameAs(asyncAppender));
Assert.That(observedArgs, Is.Not.Null);
Assert.That(observedArgs!.Success, Is.EqualTo(result));
});
}
[Test]
public void ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once()
{
var innerAppender = new TestAppender();
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10);
ILogAppender logAppender = asyncAppender;
var observedResults = new List<bool>();
asyncAppender.Append(new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Interface flush check", null, null));
asyncAppender.OnFlushCompleted += (_, eventArgs) => observedResults.Add(eventArgs.Success);
logAppender.Flush();
Assert.That(observedResults, Has.Count.EqualTo(1));
Assert.That(observedResults, Has.All.True);
}
[Test]
public void Flush_WhenEntriesAlreadyProcessed_Should_Still_ReportSuccess()
{
using var appendCompleted = new ManualResetEventSlim();
var innerAppender = new SignalingAppender(appendCompleted);
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 10);
var observedResults = new List<bool>();
asyncAppender.Append(new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Already processed", null, null));
Assert.That(appendCompleted.Wait(TimeSpan.FromSeconds(1)), Is.True);
asyncAppender.OnFlushCompleted += (_, eventArgs) => observedResults.Add(eventArgs.Success);
var result = asyncAppender.Flush(TimeSpan.FromSeconds(1));
Assert.Multiple(() =>
{
Assert.That(result, Is.True);
Assert.That(observedResults, Has.Count.EqualTo(1));
Assert.That(observedResults, Has.All.True);
Assert.That(innerAppender.FlushCount, Is.EqualTo(1));
});
}
[Test] [Test]
public void Dispose_ShouldProcessRemainingEntries() public void Dispose_ShouldProcessRemainingEntries()
{ {
@ -265,6 +332,32 @@ public class AsyncLogAppenderTests
} }
} }
private sealed class SignalingAppender : ILogAppender
{
private readonly ManualResetEventSlim _appendCompleted;
public SignalingAppender(ManualResetEventSlim appendCompleted)
{
_appendCompleted = appendCompleted;
}
public int FlushCount { get; private set; }
public void Append(LogEntry entry)
{
_appendCompleted.Set();
}
public void Flush()
{
FlushCount++;
}
public void Dispose()
{
}
}
private class ThrowingAppender : ILogAppender private class ThrowingAppender : ILogAppender
{ {
public void Append(LogEntry entry) public void Append(LogEntry entry)
@ -296,4 +389,4 @@ public class AsyncLogAppenderTests
{ {
} }
} }
} }

View File

@ -49,6 +49,7 @@ public abstract class Architecture : IArchitecture
// 初始化管理器 // 初始化管理器
_bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger); _bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger);
_lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger); _lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger);
_lifecycle.PhaseChanged += HandleLifecyclePhaseChanged;
_componentRegistry = new ArchitectureComponentRegistry( _componentRegistry = new ArchitectureComponentRegistry(
this, this,
resolvedConfiguration, resolvedConfiguration,
@ -98,13 +99,17 @@ public abstract class Architecture : IArchitecture
public virtual Action<IServiceCollection>? Configurator => null; public virtual Action<IServiceCollection>? Configurator => null;
/// <summary> /// <summary>
/// 阶段变更事件(用于测试和扩展) /// 在架构生命周期阶段发生变化时触发。
/// </summary> /// </summary>
public event Action<ArchitecturePhase>? PhaseChanged /// <remarks>
{ /// <para>
add => _lifecycle.PhaseChanged += value; /// 订阅者应通过 <see cref="ArchitecturePhaseChangedEventArgs.Phase" /> 读取当前阶段,而不是依赖内部生命周期对象。
remove => _lifecycle.PhaseChanged -= value; /// </para>
} /// <para>
/// 事件委托中的 <c>sender</c> 始终为当前 <see cref="Architecture" /> 实例,便于测试与外部扩展保持稳定的发布者契约。
/// </para>
/// </remarks>
public event EventHandler<ArchitecturePhaseChangedEventArgs>? PhaseChanged;
#endregion #endregion
@ -142,6 +147,21 @@ public abstract class Architecture : IArchitecture
#endregion #endregion
#region Event Relays
/// <summary>
/// 把生命周期协作者的阶段广播重新映射到当前架构实例,
/// 以便公开事件的 sender 始终反映真实的架构发布者。
/// </summary>
/// <param name="sender">生命周期协作者实例。</param>
/// <param name="eventArgs">阶段变化事件数据。</param>
private void HandleLifecyclePhaseChanged(object? sender, ArchitecturePhaseChangedEventArgs eventArgs)
{
PhaseChanged?.Invoke(this, eventArgs);
}
#endregion
#region Module Management #region Module Management
/// <summary> /// <summary>

View File

@ -71,6 +71,7 @@ internal sealed class ArchitectureLifecycle(
public void EnterPhase(ArchitecturePhase next) public void EnterPhase(ArchitecturePhase next)
{ {
_phaseCoordinator.EnterPhase(next); _phaseCoordinator.EnterPhase(next);
PhaseChanged?.Invoke(this, new ArchitecturePhaseChangedEventArgs(next));
} }
#endregion #endregion
@ -127,11 +128,7 @@ internal sealed class ArchitectureLifecycle(
/// <summary> /// <summary>
/// 阶段变更事件(用于测试和扩展) /// 阶段变更事件(用于测试和扩展)
/// </summary> /// </summary>
public event Action<ArchitecturePhase>? PhaseChanged public event EventHandler<ArchitecturePhaseChangedEventArgs>? PhaseChanged;
{
add => _phaseCoordinator.PhaseChanged += value;
remove => _phaseCoordinator.PhaseChanged -= value;
}
#endregion #endregion

View File

@ -22,12 +22,6 @@ internal sealed class ArchitecturePhaseCoordinator(
/// </summary> /// </summary>
public ArchitecturePhase CurrentPhase { get; private set; } public ArchitecturePhase CurrentPhase { get; private set; }
/// <summary>
/// 在架构阶段变更时触发。
/// 该事件用于测试和扩展场景,保持现有公共行为不变。
/// </summary>
public event Action<ArchitecturePhase>? PhaseChanged;
/// <summary> /// <summary>
/// 注册一个生命周期钩子。 /// 注册一个生命周期钩子。
/// 就绪后是否允许追加注册由架构配置控制,以保证阶段回调的一致性。 /// 就绪后是否允许追加注册由架构配置控制,以保证阶段回调的一致性。
@ -45,8 +39,8 @@ internal sealed class ArchitecturePhaseCoordinator(
/// <summary> /// <summary>
/// 进入指定阶段并广播给所有阶段消费者。 /// 进入指定阶段并广播给所有阶段消费者。
/// 顺序保持为“更新阶段值 → 生命周期钩子 → 容器中的阶段监听器 → 外部事件”, /// 顺序保持为“更新阶段值 → 生命周期钩子 → 容器中的阶段监听器”,
/// 以兼容既有调用约定 /// 以保证框架扩展与运行时组件看到一致的阶段视图
/// </summary> /// </summary>
/// <param name="next">目标阶段。</param> /// <param name="next">目标阶段。</param>
public void EnterPhase(ArchitecturePhase next) public void EnterPhase(ArchitecturePhase next)
@ -61,7 +55,6 @@ internal sealed class ArchitecturePhaseCoordinator(
NotifyLifecycleHooks(next); NotifyLifecycleHooks(next);
NotifyPhaseListeners(next); NotifyPhaseListeners(next);
PhaseChanged?.Invoke(next);
} }
/// <summary> /// <summary>
@ -113,4 +106,4 @@ internal sealed class ArchitecturePhaseCoordinator(
listener.OnArchitecturePhase(phase); listener.OnArchitecturePhase(phase);
} }
} }
} }

View File

@ -0,0 +1,30 @@
namespace GFramework.Core.Coroutine;
/// <summary>
/// 表示协程异常事件的数据。
/// 该类型用于把失败协程的句柄与实际异常一起传递给订阅者。
/// </summary>
public sealed class CoroutineExceptionEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="CoroutineExceptionEventArgs" /> 的新实例。
/// </summary>
/// <param name="handle">发生异常的协程句柄。</param>
/// <param name="exception">协程执行过程中抛出的异常。</param>
/// <exception cref="ArgumentNullException"><paramref name="exception" /> 为 <see langword="null" />。</exception>
public CoroutineExceptionEventArgs(CoroutineHandle handle, Exception exception)
{
Handle = handle;
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
}
/// <summary>
/// 获取发生异常的协程句柄。
/// </summary>
public CoroutineHandle Handle { get; }
/// <summary>
/// 获取协程执行过程中抛出的异常。
/// </summary>
public Exception Exception { get; }
}

View File

@ -0,0 +1,42 @@
using GFramework.Core.Abstractions.Coroutine;
namespace GFramework.Core.Coroutine;
/// <summary>
/// 表示协程结束事件的数据。
/// 该类型统一描述协程完成、取消或失败后的最终结果。
/// </summary>
public sealed class CoroutineFinishedEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="CoroutineFinishedEventArgs" /> 的新实例。
/// </summary>
/// <param name="handle">已结束的协程句柄。</param>
/// <param name="completionStatus">协程最终结果。</param>
/// <param name="exception">若协程以失败结束,则为对应异常;否则为 <see langword="null" />。</param>
public CoroutineFinishedEventArgs(
CoroutineHandle handle,
CoroutineCompletionStatus completionStatus,
Exception? exception)
{
Handle = handle;
CompletionStatus = completionStatus;
Exception = exception;
}
/// <summary>
/// 获取已结束的协程句柄。
/// </summary>
public CoroutineHandle Handle { get; }
/// <summary>
/// 获取协程最终结果。
/// </summary>
public CoroutineCompletionStatus CompletionStatus { get; }
/// <summary>
/// 获取协程失败时对应的异常对象。
/// 对于完成或取消结果,该值为 <see langword="null" />。
/// </summary>
public Exception? Exception { get; }
}

View File

@ -91,7 +91,7 @@ public sealed class CoroutineScheduler(
/// 为了避免阻塞调度器主循环,该事件会被派发到线程池回调中执行。 /// 为了避免阻塞调度器主循环,该事件会被派发到线程池回调中执行。
/// 如果调用方需要与宿主线程保持一致,请同时订阅 <see cref="OnCoroutineFinished" />。 /// 如果调用方需要与宿主线程保持一致,请同时订阅 <see cref="OnCoroutineFinished" />。
/// </remarks> /// </remarks>
public event Action<CoroutineHandle, Exception>? OnCoroutineException; public event EventHandler<CoroutineExceptionEventArgs>? OnCoroutineException;
/// <summary> /// <summary>
/// 当协程以完成、取消或失败任一结果结束时触发。 /// 当协程以完成、取消或失败任一结果结束时触发。
@ -99,7 +99,7 @@ public sealed class CoroutineScheduler(
/// <remarks> /// <remarks>
/// 该事件在调度器所在的驱动线程中同步触发,适合与宿主生命周期管理逻辑集成。 /// 该事件在调度器所在的驱动线程中同步触发,适合与宿主生命周期管理逻辑集成。
/// </remarks> /// </remarks>
public event Action<CoroutineHandle, CoroutineCompletionStatus, Exception?>? OnCoroutineFinished; public event EventHandler<CoroutineFinishedEventArgs>? OnCoroutineFinished;
/// <summary> /// <summary>
/// 检查指定协程句柄是否仍然处于活跃状态。 /// 检查指定协程句柄是否仍然处于活跃状态。
@ -622,7 +622,7 @@ public sealed class CoroutineScheduler(
UpdateCompletionMetadata(handle, completionStatus); UpdateCompletionMetadata(handle, completionStatus);
ReleaseCompletedCoroutine(slotIndex, slot, handle); ReleaseCompletedCoroutine(slotIndex, slot, handle);
CompleteCoroutineLifecycle(handle, completionStatus); CompleteCoroutineLifecycle(handle, completionStatus);
OnCoroutineFinished?.Invoke(handle, completionStatus, exception); OnCoroutineFinished?.Invoke(this, new CoroutineFinishedEventArgs(handle, completionStatus, exception));
} }
/// <summary> /// <summary>
@ -642,7 +642,7 @@ public sealed class CoroutineScheduler(
{ {
try try
{ {
handler(handle, ex); handler(this, new CoroutineExceptionEventArgs(handle, ex));
} }
catch (Exception callbackEx) catch (Exception callbackEx)
{ {

View File

@ -23,6 +23,7 @@ public sealed class AsyncLogAppender : ILogAppender
private readonly Action<Exception>? _processingErrorHandler; private readonly Action<Exception>? _processingErrorHandler;
private readonly Task _processingTask; private readonly Task _processingTask;
private bool _disposed; private bool _disposed;
private int _isProcessingEntry;
private volatile bool _flushRequested; private volatile bool _flushRequested;
/// <summary> /// <summary>
@ -117,14 +118,14 @@ public sealed class AsyncLogAppender : ILogAppender
/// </summary> /// </summary>
void ILogAppender.Flush() void ILogAppender.Flush()
{ {
var success = Flush(); Flush();
OnFlushCompleted?.Invoke(success);
} }
/// <summary> /// <summary>
/// Flush 操作完成事件参数指示是否成功true或超时false /// Flush 操作完成事件。
/// 事件数据通过 <see cref="AsyncLogFlushCompletedEventArgs" /> 提供。
/// </summary> /// </summary>
public event Action<bool>? OnFlushCompleted; public event EventHandler<AsyncLogFlushCompletedEventArgs>? OnFlushCompleted;
/// <summary> /// <summary>
/// 刷新缓冲区,等待所有日志写入完成 /// 刷新缓冲区,等待所有日志写入完成
@ -140,12 +141,13 @@ public sealed class AsyncLogAppender : ILogAppender
// 请求刷新 // 请求刷新
_flushRequested = true; _flushRequested = true;
TrySignalFlushCompletion();
try try
{ {
// 等待处理任务发出完成信号 // 等待处理任务发出完成信号
var success = _flushSemaphore.Wait(actualTimeout); var success = _flushSemaphore.Wait(actualTimeout);
OnFlushCompleted?.Invoke(success); OnFlushCompleted?.Invoke(this, new AsyncLogFlushCompletedEventArgs(success));
return success; return success;
} }
finally finally
@ -166,6 +168,7 @@ public sealed class AsyncLogAppender : ILogAppender
{ {
try try
{ {
Volatile.Write(ref _isProcessingEntry, 1);
_innerAppender.Append(entry); _innerAppender.Append(entry);
} }
catch (Exception ex) catch (Exception ex)
@ -173,18 +176,12 @@ public sealed class AsyncLogAppender : ILogAppender
// 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。 // 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。
ReportProcessingError(ex); ReportProcessingError(ex);
} }
finally
// 检查是否有刷新请求且通道已空
if (_flushRequested && _channel.Reader.Count == 0)
{ {
_innerAppender.Flush(); Volatile.Write(ref _isProcessingEntry, 0);
// 发出完成信号
if (_flushSemaphore.CurrentCount == 0)
{
_flushSemaphore.Release();
}
} }
TrySignalFlushCompletion();
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@ -209,6 +206,29 @@ public sealed class AsyncLogAppender : ILogAppender
} }
} }
/// <summary>
/// 在后台消费者已经处理完当前条目且队列为空时完成挂起的 Flush 请求。
/// </summary>
private void TrySignalFlushCompletion()
{
if (!_flushRequested)
{
return;
}
if (Volatile.Read(ref _isProcessingEntry) != 0 || _channel.Reader.Count != 0)
{
return;
}
_innerAppender.Flush();
if (_flushSemaphore.CurrentCount == 0)
{
_flushSemaphore.Release();
}
}
/// <summary> /// <summary>
/// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。 /// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。
/// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。 /// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。

View File

@ -10,9 +10,10 @@
- `Architecture``ArchitectureContext` - `Architecture``ArchitectureContext`
- `Model` / `System` / `Utility` 运行时 - `Model` / `System` / `Utility` 运行时
- 旧版 `Command` / `Query` 执行器 - 旧版 `Command` / `Query` 执行器,以及与新版 `CQRS` runtime 的接线入口
- 事件、属性、状态机、状态管理 - 事件、属性、状态机、状态管理、规则与上下文扩展
- 资源、日志、协程、并发、环境与本地化 - 资源、对象池、日志、协程、并发、环境、配置与本地化
- 服务模块管理、时间提供器与默认的 IoC 容器适配
它不负责: 它不负责:
@ -37,6 +38,7 @@
| 目录 | 作用 | | 目录 | 作用 |
| --- | --- | | --- | --- |
| `Architectures/` | 架构入口、上下文、生命周期、模块安装与组件注册 | | `Architectures/` | 架构入口、上下文、生命周期、模块安装与组件注册 |
| `Services/` | 服务模块注册、生命周期协调与模块管理 |
| `Command/` | 旧版命令执行器与同步 / 异步命令基类 | | `Command/` | 旧版命令执行器与同步 / 异步命令基类 |
| `Query/` | 旧版查询执行器与同步 / 异步查询基类 | | `Query/` | 旧版查询执行器与同步 / 异步查询基类 |
| `Events/` | 事件总线、事件作用域、统计与过滤 | | `Events/` | 事件总线、事件作用域、统计与过滤 |
@ -44,15 +46,37 @@
| `State/` | 状态机与状态切换事件 | | `State/` | 状态机与状态切换事件 |
| `StateManagement/` | Store、selector、middleware 与状态诊断 | | `StateManagement/` | Store、selector、middleware 与状态诊断 |
| `Coroutine/` | 协程调度、快照、统计与优先级 | | `Coroutine/` | 协程调度、快照、统计与优先级 |
| `Time/` | 默认时间提供器与协程时间源 |
| `Resource/` | 资源缓存、句柄和释放策略 | | `Resource/` | 资源缓存、句柄和释放策略 |
| `Pool/` | 对象池系统与常用池化辅助实现 |
| `Logging/` | logger、factory、配置与组合日志器 | | `Logging/` | logger、factory、配置与组合日志器 |
| `Ioc/` | 基于 `Microsoft.Extensions.DependencyInjection` 的容器适配 | | `Ioc/` | 基于 `Microsoft.Extensions.DependencyInjection` 的容器适配 |
| `Concurrency/` | 键控异步锁与统计 | | `Concurrency/` | 键控异步锁与统计 |
| `Configuration/` | 配置管理器与配置监听解绑对象 |
| `Environment/` | 运行环境对象与上下文环境扩展 |
| `Pause/` | 暂停栈和暂停范围 | | `Pause/` | 暂停栈和暂停范围 |
| `Localization/` | 本地化表与格式化入口 | | `Localization/` | 本地化表与格式化入口 |
| `Rule/` | `ContextAwareBase` 等上下文感知基类 |
| `Functional/` | `Option``Result` 等轻量函数式工具 | | `Functional/` | `Option``Result` 等轻量函数式工具 |
| `Extensions/` | 上下文与集合等扩展方法 | | `Extensions/` | 上下文与集合等扩展方法 |
## XML 覆盖基线
截至 `2026-04-22`,已按顶层目录对 `GFramework.Core` 的公开 / 内部类型声明做过一轮轻量盘点;当前主目录族的类型声明都已带
XML 注释。这里先保留阅读基线,成员级 ``<param>`` / ``<returns>`` / 生命周期语义审计仍属于后续治理项。
| 类型族 | 基线状态 | 代表类型 |
| --- | --- | --- |
| `Architectures/` `Services/` | `22/22` 个类型声明已带 XML 注释 | `Architecture``ArchitectureContext``ArchitectureLifecycle``ServiceModuleManager` |
| `Command/` `Query/` | `15/15` 个类型声明已带 XML 注释 | `CommandExecutor``AsyncQueryExecutor``AbstractCommand<TInput>``AbstractQuery<TResult>` |
| `Events/` `Property/` `State/` `StateManagement/` | `29/29` 个类型声明已带 XML 注释 | `EventBus``BindableProperty<T>``StateMachine``Store<TState>` |
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `43/43` 个类型声明已带 XML 注释 | `CoroutineScheduler``CoroutineHandle``PauseStackManager``AsyncKeyLockManager` |
| `Resource/` `Pool/` | `8/8` 个类型声明已带 XML 注释 | `ResourceManager``AutoReleaseStrategy``AbstractObjectPoolSystem<TKey, TObject>` |
| `Logging/` `Localization/` `Configuration/` `Environment/` `Ioc/` | `31/31` 个类型声明已带 XML 注释 | `ConsoleLogger``LocalizationManager``ConfigurationManager``DefaultEnvironment``MicrosoftDiContainer` |
| `Model/` `Systems/` `Utility/` `Rule/` `Extensions/` `Functional/` | `34/34` 个类型声明已带 XML 注释 | `AbstractModel``AbstractSystem``NumericDisplayFormatter``ContextAwareBase``Result<T>` |
完整的模块化阅读顺序和 inventory 说明见 `docs/zh-CN/core/index.md`
## 最小接入路径 ## 最小接入路径
```bash ```bash
@ -80,5 +104,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
## 对应文档 ## 对应文档
- Core 栏目:[`../docs/zh-CN/core/index.md`](../docs/zh-CN/core/index.md) - Core 栏目:[`../docs/zh-CN/core/index.md`](../docs/zh-CN/core/index.md)
- Core 抽象层:[`../docs/zh-CN/abstractions/core-abstractions.md`](../docs/zh-CN/abstractions/core-abstractions.md)
- API 参考入口:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)
- CQRS[`../docs/zh-CN/core/cqrs.md`](../docs/zh-CN/core/cqrs.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) - 入门指南:[`../docs/zh-CN/getting-started/index.md`](../docs/zh-CN/getting-started/index.md)

View File

@ -1144,6 +1144,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
string HandlerInterfaceDisplayName, string HandlerInterfaceDisplayName,
string HandlerInterfaceLogName); string HandlerInterfaceLogName);
/// <summary>
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
/// </summary>
/// <remarks>
/// 该枚举只服务于输出排序与代码分支选择,用来保证生成注册器在“直接注册”
/// “反射实现类型查找”和“精确运行时类型解析”之间保持稳定顺序。
/// </remarks>
private enum OrderedRegistrationKind private enum OrderedRegistrationKind
{ {
Direct, Direct,
@ -1151,6 +1158,14 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
PreciseReflected PreciseReflected
} }
/// <summary>
/// 描述生成注册器中某个运行时类型引用的构造方式。
/// </summary>
/// <remarks>
/// 某些 handler 服务类型可以直接以 <c>typeof(...)</c> 输出,某些则需要在运行时补充
/// 反射查找、数组/指针封装或泛型实参重建。该记录把这些差异收敛为统一的递归结构,
/// 供源码输出阶段生成稳定的类型解析语句。
/// </remarks>
private sealed record RuntimeTypeReferenceSpec( private sealed record RuntimeTypeReferenceSpec(
string? TypeDisplayName, string? TypeDisplayName,
string? ReflectionTypeMetadataName, string? ReflectionTypeMetadataName,
@ -1161,18 +1176,27 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, RuntimeTypeReferenceSpec? GenericTypeDefinitionReference,
ImmutableArray<RuntimeTypeReferenceSpec> GenericTypeArguments) ImmutableArray<RuntimeTypeReferenceSpec> GenericTypeArguments)
{ {
/// <summary>
/// 创建一个可直接通过 <c>typeof(...)</c> 表达的类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName) public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName)
{ {
return new RuntimeTypeReferenceSpec(typeDisplayName, null, null, null, 0, null, null, return new RuntimeTypeReferenceSpec(typeDisplayName, null, null, null, 0, null, null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty); ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
} }
/// <summary>
/// 创建一个需要从当前消费端程序集反射解析的类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName) public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName)
{ {
return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, null, null, 0, null, null, return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, null, null, 0, null, null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty); ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
} }
/// <summary>
/// 创建一个需要从被引用程序集反射解析的类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromExternalReflectionLookup( public static RuntimeTypeReferenceSpec FromExternalReflectionLookup(
string reflectionAssemblyName, string reflectionAssemblyName,
string reflectionTypeMetadataName) string reflectionTypeMetadataName)
@ -1182,18 +1206,27 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
ImmutableArray<RuntimeTypeReferenceSpec>.Empty); ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
} }
/// <summary>
/// 创建一个数组类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank) public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank)
{ {
return new RuntimeTypeReferenceSpec(null, null, null, elementTypeReference, arrayRank, null, null, return new RuntimeTypeReferenceSpec(null, null, null, elementTypeReference, arrayRank, null, null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty); ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
} }
/// <summary>
/// 创建一个指针类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromPointer(RuntimeTypeReferenceSpec pointedAtTypeReference) public static RuntimeTypeReferenceSpec FromPointer(RuntimeTypeReferenceSpec pointedAtTypeReference)
{ {
return new RuntimeTypeReferenceSpec(null, null, null, null, 0, pointedAtTypeReference, null, return new RuntimeTypeReferenceSpec(null, null, null, null, 0, pointedAtTypeReference, null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty); ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
} }
/// <summary>
/// 创建一个封闭泛型类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromConstructedGeneric( public static RuntimeTypeReferenceSpec FromConstructedGeneric(
RuntimeTypeReferenceSpec genericTypeDefinitionReference, RuntimeTypeReferenceSpec genericTypeDefinitionReference,
ImmutableArray<RuntimeTypeReferenceSpec> genericTypeArguments) ImmutableArray<RuntimeTypeReferenceSpec> genericTypeArguments)

View File

@ -651,32 +651,70 @@ internal static class CqrsHandlerRegistrar
} }
} }
/// <summary>
/// 描述某个程序集在生成注册器之后仍需运行时补扫的 handler 元数据。
/// </summary>
/// <remarks>
/// 该对象把“是否存在精确 fallback 类型列表”与“是否只能回退到整程序集扫描”收敛为同一份内部状态,
/// 供注册流水线后续阶段统一判断。
/// </remarks>
private sealed class ReflectionFallbackMetadata(IReadOnlyList<Type> types) private sealed class ReflectionFallbackMetadata(IReadOnlyList<Type> types)
{ {
/// <summary>
/// 获取需要通过运行时反射补充注册的 handler 类型集合。
/// </summary>
public IReadOnlyList<Type> Types { get; } = types ?? throw new ArgumentNullException(nameof(types)); public IReadOnlyList<Type> Types { get; } = types ?? throw new ArgumentNullException(nameof(types));
/// <summary>
/// 获取当前是否持有精确的 fallback 类型清单。
/// </summary>
public bool HasExplicitTypes => Types.Count > 0; public bool HasExplicitTypes => Types.Count > 0;
} }
/// <summary>
/// 描述单个程序集在注册阶段提取到的 generated registry 与 reflection fallback 元数据。
/// </summary>
private sealed class AssemblyRegistrationMetadata( private sealed class AssemblyRegistrationMetadata(
IReadOnlyList<Type> registryTypes, IReadOnlyList<Type> registryTypes,
ReflectionFallbackMetadata? reflectionFallbackMetadata) ReflectionFallbackMetadata? reflectionFallbackMetadata)
{ {
/// <summary>
/// 获取程序集上声明的 generated registry 类型集合。
/// </summary>
public IReadOnlyList<Type> RegistryTypes { get; } = public IReadOnlyList<Type> RegistryTypes { get; } =
registryTypes ?? throw new ArgumentNullException(nameof(registryTypes)); registryTypes ?? throw new ArgumentNullException(nameof(registryTypes));
/// <summary>
/// 获取该程序集是否还要求运行时补充 reflection fallback。
/// </summary>
public ReflectionFallbackMetadata? ReflectionFallbackMetadata { get; } = reflectionFallbackMetadata; public ReflectionFallbackMetadata? ReflectionFallbackMetadata { get; } = reflectionFallbackMetadata;
} }
/// <summary>
/// 缓存 generated registry 激活所需的类型判定结果与工厂委托。
/// </summary>
/// <remarks>
/// 该缓存把“是否实现契约”“是否为抽象类型”“是否已构建激活委托”封装为不可变快照,
/// 避免对同一 registry 类型重复执行反射分析。
/// </remarks>
private sealed class RegistryActivationMetadata( private sealed class RegistryActivationMetadata(
bool implementsRegistryContract, bool implementsRegistryContract,
bool isAbstract, bool isAbstract,
Func<ICqrsHandlerRegistry>? factory) Func<ICqrsHandlerRegistry>? factory)
{ {
/// <summary>
/// 获取目标类型是否实现了 <see cref="ICqrsHandlerRegistry" />。
/// </summary>
public bool ImplementsRegistryContract { get; } = implementsRegistryContract; public bool ImplementsRegistryContract { get; } = implementsRegistryContract;
/// <summary>
/// 获取目标类型是否为抽象类型。
/// </summary>
public bool IsAbstract { get; } = isAbstract; public bool IsAbstract { get; } = isAbstract;
/// <summary>
/// 获取可用于实例化 registry 的工厂委托。
/// </summary>
public Func<ICqrsHandlerRegistry>? Factory { get; } = factory; public Func<ICqrsHandlerRegistry>? Factory { get; } = factory;
} }
} }

View File

@ -0,0 +1,103 @@
# GFramework.Ecs.Arch.Abstractions
`GFramework.Ecs.Arch.Abstractions` 承载 Arch ECS 集成层的最小契约,用来让共享业务层、宿主循环或扩展模块在不依赖
`GFramework.Ecs.Arch` 默认实现的前提下,仍然可以约定 ECS 模块边界。
如果你需要的是 `UseArch(...)` 扩展、`ArchSystemAdapter<T>` 基类、`World` 注册和默认模块实现,请改为依赖
`GFramework.Ecs.Arch`
## 包定位
- 这是 `Ecs.Arch` 的契约层,不是默认实现层。
- 适合让上层模块只面向 `IArchEcsModule``IArchSystemAdapter<T>``ArchOptions` 编程。
- 常见场景:
- 共享宿主循环只依赖更新契约,不直接引用 Arch runtime 实现
- 多程序集之间需要共享 ECS 配置对象或接口边界
- 测试替身、编辑器工具或外部适配层希望复用契约,但自行决定底层实现
## 与相邻包的关系
- `GFramework.Core.Abstractions`
- 本包直接依赖它,并复用 `IServiceModule``ISystem` 等基础契约。
- `GFramework.Ecs.Arch.Abstractions`
- 只定义 Arch ECS 集成相关的最小契约和配置对象。
- `GFramework.Ecs.Arch`
- 本包的默认实现层。
- 负责 `UseArch(...)` 扩展、默认模块注册、Arch `World` 装配,以及系统适配器基类。
## 契约地图
| 文件 | 作用 |
| --- | --- |
| `IArchEcsModule.cs` | ECS 模块服务契约,负责统一驱动系统更新 |
| `IArchSystemAdapter.cs` | 让 ECS 系统适配到 GFramework `ISystem` 生命周期的接口 |
| `ArchOptions.cs` | `WorldCapacity``EnableStatistics``Priority` 等配置对象 |
## XML 阅读基线
下表记录当前契约包的类型声明级 XML 基线,方便把 README、站内抽象页与源码阅读顺序对齐。
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
| --- | --- | --- | --- |
| 模块契约 | `IArchEcsModule` | 已覆盖 | 宿主循环如何统一驱动 ECS 更新 |
| 系统桥接契约 | `IArchSystemAdapter<T>` | 已覆盖 | 外部模块怎样只依赖更新接口而不绑定默认实现 |
| 配置对象 | `ArchOptions` | 已覆盖 | 跨程序集共享 ECS 配置边界 |
## 最小接入路径
### 1. 只想约定宿主循环与 ECS 模块边界
```csharp
using GFramework.Ecs.Arch.Abstractions;
public sealed class EcsUpdateLoop
{
private readonly IArchEcsModule _ecsModule;
public EcsUpdateLoop(IArchEcsModule ecsModule)
{
_ecsModule = ecsModule;
}
public void Tick(float deltaTime)
{
_ecsModule.Update(deltaTime);
}
}
```
### 2. 只想共享配置对象
```csharp
using GFramework.Ecs.Arch.Abstractions;
var options = new ArchOptions
{
WorldCapacity = 2048,
EnableStatistics = true,
Priority = 40
};
```
### 3. 什么时候要升级到 `GFramework.Ecs.Arch`
一旦你需要下面任一项,就不该只停留在本包:
- `UseArch(...)` 或其他 runtime 装配入口
- `ArchSystemAdapter<T>` 等默认基类
- Arch `World` 的创建、注册和查询能力
- 与 `GFramework` 架构生命周期绑定的默认模块实现
## 边界说明
- 本包不提供 Arch `World` 的默认构造与注册逻辑。
- 本包不提供系统基类、扩展方法或默认服务实现。
- 它回答的是“外部模块怎样与 Arch ECS 集成层约定边界”不是“Arch ECS 默认怎么接入到项目里”。
## 对应文档入口
- 抽象接口总览:[`../docs/zh-CN/abstractions/index.md`](../docs/zh-CN/abstractions/index.md)
- Ecs.Arch 抽象层说明:[`../docs/zh-CN/abstractions/ecs-arch-abstractions.md`](../docs/zh-CN/abstractions/ecs-arch-abstractions.md)
- ECS 模块入口:[`../docs/zh-CN/ecs/index.md`](../docs/zh-CN/ecs/index.md)
- Arch ECS 集成:[`../docs/zh-CN/ecs/arch.md`](../docs/zh-CN/ecs/arch.md)
- 运行时实现入口:[`../GFramework.Ecs.Arch/README.md`](../GFramework.Ecs.Arch/README.md)

View File

@ -1,16 +1,30 @@
# GFramework.Ecs.Arch # GFramework.Ecs.Arch
GFramework 的 Arch ECS 集成包,提供开箱即用的 ECSEntity Component System支持 `GFramework.Ecs.Arch``GFramework` 当前 Arch ECS family 的默认运行时实现包
## 特性 它负责把 Arch `World`、GFramework 的服务模块生命周期,以及 `ArchSystemAdapter<T>` 系统桥接到同一条采用路径中。
如果你需要的只是共享契约,请改为依赖 `GFramework.Ecs.Arch.Abstractions`
- 🎯 **显式集成** - 符合 .NET 生态习惯的显式注册方式 ## 包定位
- 🔌 **零依赖** - 不使用时Core 包无 Arch 依赖
- 🎯 **类型安全** - 完整的类型系统和编译时检查
- ⚡ **高性能** - 基于 Arch ECS 的高性能实现
- 🔧 **易扩展** - 简单的系统适配器模式
## 快速开始 - 这是运行时实现层,不是纯契约层。
- 适合需要 `UseArch(...)``World` 自动注册、默认模块生命周期和系统桥接基类的项目。
- 常见场景:
- 在架构实例上显式接入 Arch ECS
- 让 `World` 由默认模块创建并放入容器
- 让 ECS 系统复用 `ArchSystemAdapter<float>` 生命周期桥接
- 通过 `IArchEcsModule.Update(deltaTime)` 统一驱动 ECS 帧更新
## 与相邻包的关系
- `GFramework.Core`
- 提供架构、容器、生命周期和系统注册基础设施。
- `GFramework.Ecs.Arch.Abstractions`
- 提供 `IArchEcsModule``IArchSystemAdapter<T>` 和契约层 `ArchOptions`
- `GFramework.Ecs.Arch`
- 提供 `UseArch(...)`、默认 `ArchEcsModule``World` 注册,以及系统适配器基类与示例类型。
## 最小接入路径
### 1. 安装包 ### 1. 安装包
@ -18,53 +32,43 @@ GFramework 的 Arch ECS 集成包,提供开箱即用的 ECSEntity Component
dotnet add package GeWuYou.GFramework.Ecs.Arch dotnet add package GeWuYou.GFramework.Ecs.Arch
``` ```
### 2. 注册 ECS 模块 ### 2. 在 `Initialize()` 之前显式接入 Arch runtime
按当前实现,`UseArch(...)` 会把 `ArchEcsModule` 提前登记到 `ArchitectureModuleRegistry`,因此调用时机应早于
`Initialize()`
```csharp ```csharp
// 在架构初始化时添加 Arch ECS 支持 using GFramework.Core.Architectures;
var architecture = new GameArchitecture(config) using GFramework.Ecs.Arch.Extensions;
.UseArch(); // 添加 ECS 支持
architecture.Initialize(); public sealed class GameArchitecture : Architecture
``` {
public GameArchitecture() : base(new ArchitectureConfiguration())
{
}
### 3. 带配置的注册 protected override void OnInitialize()
{
RegisterSystem<MovementSystem>();
}
}
```csharp var architecture = new GameArchitecture()
var architecture = new GameArchitecture(config)
.UseArch(options => .UseArch(options =>
{ {
options.WorldCapacity = 2000; options.WorldCapacity = 2048;
options.EnableStatistics = true;
options.Priority = 50; options.Priority = 50;
}); });
architecture.Initialize(); architecture.Initialize();
``` ```
```csharp ### 3. 编写并注册系统
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Position(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
[StructLayout(LayoutKind.Sequential)]
public struct Velocity(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
```
### 5. 创建系统
```csharp ```csharp
using Arch.Core; using Arch.Core;
using GFramework.Ecs.Arch; using GFramework.Ecs.Arch;
using GFramework.Ecs.Arch.Components;
public sealed class MovementSystem : ArchSystemAdapter<float> public sealed class MovementSystem : ArchSystemAdapter<float>
{ {
@ -78,115 +82,57 @@ public sealed class MovementSystem : ArchSystemAdapter<float>
protected override void OnUpdate(in float deltaTime) protected override void OnUpdate(in float deltaTime)
{ {
World.Query(in _query, (ref Position pos, ref Velocity vel) => var frameDelta = deltaTime;
World.Query(in _query, (ref Position position, ref Velocity velocity) =>
{ {
pos.X += vel.X * deltaTime; position.X += velocity.X * frameDelta;
pos.Y += vel.Y * deltaTime; position.Y += velocity.Y * frameDelta;
}); });
} }
} }
``` ```
### 6. 注册系统 ### 4. 初始化后获取 `World` 与 ECS 模块
```csharp ```csharp
public class MyArchitecture : Architecture using Arch.Core;
{ using GFramework.Ecs.Arch.Abstractions;
protected override void OnRegisterSystem(IIocContainer container)
{ var world = architecture.Context.GetService<World>();
container.Register<MovementSystem>(); var ecsModule = architecture.Context.GetService<IArchEcsModule>();
}
}
``` ```
### 7. 创建实体 ### 5. 由宿主循环驱动更新
```csharp ```csharp
var world = this.GetService<World>();
var entity = world.Create(
new Position(0, 0),
new Velocity(1, 1)
);
```
### 8. 更新系统
```csharp
var ecsModule = this.GetService<IArchEcsModule>();
ecsModule.Update(deltaTime); ecsModule.Update(deltaTime);
``` ```
## 配置选项 ## 运行时职责地图
### 代码配置 | 文件 | 作用 |
| --- | --- |
| `Extensions/ArchExtensions.cs` | 通过 `UseArch(...)` 把默认模块注册到 `ArchitectureModuleRegistry` |
| `ArchEcsModule.cs` | 创建并注册 `World`,按优先级收集 `ArchSystemAdapter<float>`,负责初始化、销毁和逐帧更新 |
| `ArchSystemAdapter.cs` | 把 GFramework 系统生命周期桥接到 Arch `ISystem<T>` 生命周期 |
| `ArchOptions.cs` | 承载 `WorldCapacity``EnableStatistics``Priority` 这组运行时配置 |
| `Components/*.cs``Systems/*.cs` | 提供最小组件与系统示例,帮助对照查询写法和更新模式 |
```csharp ## XML 阅读基线
var architecture = new GameArchitecture(config)
.UseArch(options =>
{
options.WorldCapacity = 2000;
options.EnableStatistics = true;
options.Priority = 50;
});
```
### 配置说明 下表记录当前模块 README 与源码可对照的类型声明级 XML 基线。
- `WorldCapacity` - World 初始容量默认1000 | 类型族 | 代表类型 | XML 状态 | 阅读重点 |
- `EnableStatistics` - 是否启用统计信息默认false | --- | --- | --- | --- |
- `Priority` - 模块优先级默认50 | 装配入口 | `ArchExtensions` | 已覆盖 | `UseArch(...)` 的时机与返回值 |
| 运行时模块 | `ArchEcsModule` | 已覆盖 | `World` 注册、系统排序、销毁顺序 |
| 系统桥接层 | `ArchSystemAdapter<T>` | 已覆盖 | `OnArchInitialize``OnUpdate``OnArchDispose` |
| 示例类型 | `Position``Velocity``MovementSystem` | 已覆盖 | 组件布局、查询写法、最小示例 |
## 架构说明 ## 对应文档入口
### 显式注册模式 - ECS 总览:[`../docs/zh-CN/ecs/index.md`](../docs/zh-CN/ecs/index.md)
- Arch ECS 集成:[`../docs/zh-CN/ecs/arch.md`](../docs/zh-CN/ecs/arch.md)
本包采用 .NET 生态标准的显式注册模式,基于架构实例: - 抽象契约页:[`../docs/zh-CN/abstractions/ecs-arch-abstractions.md`](../docs/zh-CN/abstractions/ecs-arch-abstractions.md)
- 统一 API / XML 导航:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)
**优点:**
- ✅ 符合 .NET 生态习惯
- ✅ 显式、可控
- ✅ 易于测试和调试
- ✅ 支持配置
- ✅ 支持链式调用
- ✅ 避免"魔法"行为
**使用方式:**
```csharp
// 在架构初始化时添加
var architecture = new GameArchitecture(config)
.UseArch(); // 显式注册
architecture.Initialize();
```
详见:[INTEGRATION_PATTERN.md](INTEGRATION_PATTERN.md)
### 系统适配器
`ArchSystemAdapter<T>` 桥接 Arch.System.ISystem<T> 到 GFramework 架构:
- 自动获取 World 实例
- 集成到框架生命周期
- 支持上下文感知Context-Aware
### 生命周期
1. **注册阶段** - 模块自动注册到架构
2. **初始化阶段** - 创建 World初始化系统
3. **运行阶段** - 每帧调用 Update
4. **销毁阶段** - 清理资源,销毁 World
## 示例
完整示例请参考 `GFramework.Ecs.Arch.Tests` 项目。
## 依赖
- GFramework.Core >= 1.0.0
- Arch >= 2.1.0
- Arch.System >= 1.1.0
## 许可证
MIT License

View File

@ -781,15 +781,13 @@ public partial class Timing : Node
/// <summary> /// <summary>
/// 在协程结束时解除节点归属回调并清理索引。 /// 在协程结束时解除节点归属回调并清理索引。
/// </summary> /// </summary>
/// <param name="handle">已结束的协程句柄。</param> /// <param name="sender">触发事件的协程调度器。</param>
/// <param name="status">协程最终状态。</param> /// <param name="eventArgs">协程结束事件数据。</param>
/// <param name="exception">若失败则为异常对象。</param>
private void HandleCoroutineFinished( private void HandleCoroutineFinished(
CoroutineHandle handle, object? sender,
CoroutineCompletionStatus status, CoroutineFinishedEventArgs eventArgs)
Exception? exception)
{ {
CleanupOwnedCoroutineRegistration(handle); CleanupOwnedCoroutineRegistration(eventArgs.Handle);
} }
/// <summary> /// <summary>

View File

@ -27,11 +27,22 @@
| `GFramework.Game.Abstractions` | `Game` 对应的契约层 | [README](GFramework.Game.Abstractions/README.md) | | `GFramework.Game.Abstractions` | `Game` 对应的契约层 | [README](GFramework.Game.Abstractions/README.md) |
| `GFramework.Godot` | Godot 集成层负责把框架能力接入节点、场景、UI、设置与存储 | [README](GFramework.Godot/README.md) | | `GFramework.Godot` | Godot 集成层负责把框架能力接入节点、场景、UI、设置与存储 | [README](GFramework.Godot/README.md) |
| `GFramework.Ecs.Arch` | Arch ECS 集成 | [README](GFramework.Ecs.Arch/README.md) | | `GFramework.Ecs.Arch` | Arch ECS 集成 | [README](GFramework.Ecs.Arch/README.md) |
| `GFramework.Ecs.Arch.Abstractions` | Arch ECS 集成对应的契约层,适合共享宿主循环与 ECS 模块边界 | [README](GFramework.Ecs.Arch.Abstractions/README.md) |
| `GFramework.Core.SourceGenerators` | Core 侧通用源码生成器与分析器 | [README](GFramework.Core.SourceGenerators/README.md) | | `GFramework.Core.SourceGenerators` | Core 侧通用源码生成器与分析器 | [README](GFramework.Core.SourceGenerators/README.md) |
| `GFramework.Game.SourceGenerators` | 游戏内容配置 schema 生成器 | [README](GFramework.Game.SourceGenerators/README.md) | | `GFramework.Game.SourceGenerators` | 游戏内容配置 schema 生成器 | [README](GFramework.Game.SourceGenerators/README.md) |
| `GFramework.Cqrs.SourceGenerators` | CQRS handler registry 生成器 | [README](GFramework.Cqrs.SourceGenerators/README.md) | | `GFramework.Cqrs.SourceGenerators` | CQRS handler registry 生成器 | [README](GFramework.Cqrs.SourceGenerators/README.md) |
| `GFramework.Godot.SourceGenerators` | Godot 场景专用源码生成器 | [README](GFramework.Godot.SourceGenerators/README.md) | | `GFramework.Godot.SourceGenerators` | Godot 场景专用源码生成器 | [README](GFramework.Godot.SourceGenerators/README.md) |
## 内部支撑模块
以下目录目前不是独立采用入口,而是跟随所属模块维护的内部支撑组件:
| 目录 | 定位 | 跟随入口 |
| --- | --- | --- |
| `GFramework.Core.SourceGenerators.Abstractions` | `Core.SourceGenerators` 的内部契约层 | [GFramework.Core.SourceGenerators/README.md](GFramework.Core.SourceGenerators/README.md) |
| `GFramework.Godot.SourceGenerators.Abstractions` | `Godot.SourceGenerators` 的内部契约层 | [GFramework.Godot.SourceGenerators/README.md](GFramework.Godot.SourceGenerators/README.md) |
| `GFramework.SourceGenerators.Common` | 生成器家族共享的公共支撑代码 | [docs/zh-CN/source-generators/index.md](docs/zh-CN/source-generators/index.md) |
## 文档导航 ## 文档导航
仓库根 README 与文档站点保持同一套栏目命名: 仓库根 README 与文档站点保持同一套栏目命名:
@ -119,10 +130,13 @@ GFramework.sln
├─ GFramework.Game.Abstractions/ ├─ GFramework.Game.Abstractions/
├─ GFramework.Godot/ ├─ GFramework.Godot/
├─ GFramework.Ecs.Arch/ ├─ GFramework.Ecs.Arch/
├─ GFramework.Ecs.Arch.Abstractions/
├─ GFramework.Core.SourceGenerators/ ├─ GFramework.Core.SourceGenerators/
├─ GFramework.Core.SourceGenerators.Abstractions/
├─ GFramework.Game.SourceGenerators/ ├─ GFramework.Game.SourceGenerators/
├─ GFramework.Cqrs.SourceGenerators/ ├─ GFramework.Cqrs.SourceGenerators/
├─ GFramework.Godot.SourceGenerators/ ├─ GFramework.Godot.SourceGenerators/
├─ GFramework.Godot.SourceGenerators.Abstractions/
├─ GFramework.SourceGenerators.Common/ ├─ GFramework.SourceGenerators.Common/
└─ docs/ └─ docs/
``` ```

View File

@ -25,6 +25,11 @@ help the current worktree land on the right recovery documents without scanning
- Purpose: continue the AI-First config runtime, generator, and consumer DX work for `GFramework.Game`. - Purpose: continue the AI-First config runtime, generator, and consumer DX work for `GFramework.Game`.
- Tracking: `ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md` - Tracking: `ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md`
- Trace: `ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md` - Trace: `ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md`
- `documentation-full-coverage-governance`
- Purpose: govern full-coverage documentation inventory, module-wave remediation, and the README / docs / XML /
API-reference alignment baseline.
- Tracking: `ai-plan/public/documentation-full-coverage-governance/todos/documentation-full-coverage-governance-tracking.md`
- Trace: `ai-plan/public/documentation-full-coverage-governance/traces/documentation-full-coverage-governance-trace.md`
- `coroutine-optimization` - `coroutine-optimization`
- Purpose: continue the coroutine semantics, host integration, observability, regression coverage, and migration-doc - Purpose: continue the coroutine semantics, host integration, observability, regression coverage, and migration-doc
follow-up work. follow-up work.
@ -38,10 +43,6 @@ help the current worktree land on the right recovery documents without scanning
- Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog. - Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog.
- Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md` - Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md`
- Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md` - Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md`
- `documentation-governance-and-refresh`
- Purpose: continue the documentation governance, README hardening, and `docs/zh-CN` accuracy refresh work.
- Tracking: `ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md`
- Trace: `ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md`
## Worktree To Active Topic Map ## Worktree To Active Topic Map
@ -64,10 +65,12 @@ help the current worktree land on the right recovery documents without scanning
- Priority 1: `data-repository-persistence` - Priority 1: `data-repository-persistence`
- Branch: `docs/sdk-update-documentation` - Branch: `docs/sdk-update-documentation`
- Worktree hint: `GFramework-update-documentation` - Worktree hint: `GFramework-update-documentation`
- Priority 1: `documentation-governance-and-refresh` - Priority 1: `documentation-full-coverage-governance`
## Archived Topics ## Archived Topics
- `cqrs-cache-docs-hardening` - `cqrs-cache-docs-hardening`
- Archive root: `ai-plan/public/archive/cqrs-cache-docs-hardening/` - Archive root: `ai-plan/public/archive/cqrs-cache-docs-hardening/`
- Note: archived topics stay outside the default `boot` context until a user explicitly requests historical review. - Note: archived topics stay outside the default `boot` context until a user explicitly requests historical review.
- `documentation-governance-and-refresh`
- Archive root: `ai-plan/public/archive/documentation-governance-and-refresh/`
- Note: PR #268 已合并;文档治理与 Godot 栏目刷新阶段已完成,后续仅作为历史恢复材料保留。

View File

@ -7,10 +7,14 @@
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-012` - 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-015`
- 当前阶段:`Phase 12` - 当前阶段:`Phase 15`
- 当前焦点: - 当前焦点:
- 当前 PR review workflow 已补强到支持 JSON 落盘与按 section/path 收窄输出;下一轮恢复到 `MA0046` 主批次 - 当前分支 PR #267 的失败测试已通过 `$gframework-pr-review` 与本地整包测试完成复核
- 已确认并修复 `AsyncLogAppender.Flush()` 在“后台线程先清空队列”场景下可能超时返回 `false` 的竞态
- 已补上稳定回归测试,避免只在整包 `GFramework.Core.Tests` 里偶发暴露的刷新完成信号问题再次回归
- 下一轮默认恢复到 `MA0016``MA0002` 低风险批次;`MA0015``MA0077` 继续作为尾项顺手吸收
- `GFramework.Godot``Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进 - 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015``MA0077` - 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015``MA0077`
只是当前最明显的低数量示例,不构成限定 只是当前最明显的低数量示例,不构成限定
@ -24,8 +28,13 @@
- 已完成当前 PR #265 review follow-up修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚 - 已完成当前 PR #265 review follow-up修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚
- 已继续完成当前 PR #265 review follow-up修复 `Event<T>``Event<T, TK>` 监听器计数的 off-by-one并补充回归测试 - 已继续完成当前 PR #265 review follow-up修复 `Event<T>``Event<T, TK>` 监听器计数的 off-by-one并补充回归测试
- 已增强 `gframework-pr-review` 脚本与 skill 文档,降低超长 JSON 直出导致的 review 信号漏看风险 - 已增强 `gframework-pr-review` 脚本与 skill 文档,降低超长 JSON 直出导致的 review 信号漏看风险
- 当前 `PauseStackManager``Store``CoroutineScheduler``GFramework.Core``MA0048` - 已完成 `GFramework.Core` 当前 `MA0046` 批次:将阶段、协程与异步日志事件统一迁移到 `EventHandler<TEventArgs>` 形状,
文件/类型命名冲突已从 active 入口移除;主题内剩余 warning 主要集中在 `MA0046` delegate 形状、 并同步更新 `GFramework.Godot` 订阅点、定向测试与 `docs/zh-CN` 示例
- 已完成当前 PR #267 review follow-up修复 `AsyncLogAppender``ILogAppender.Flush()` 双重完成通知,并补齐
`PhaseChanged` / `CoroutineExceptionEventArgs` XML 文档、`PhaseChanged` 迁移说明和 `ai-plan` 基线注释
- 已完成当前 PR #267 failed-test follow-up修复 `AsyncLogAppender.Flush()` 在队列已被后台线程提前清空时仍可能
等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `9` 条;剩余 warning 集中在
`MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项 `MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项
## 当前活跃事实 ## 当前活跃事实
@ -51,16 +60,24 @@
委托导致的 `GetListenerCount()` off-by-one并以定向事件测试验证注册、注销和计数语义 委托导致的 `GetListenerCount()` off-by-one并以定向事件测试验证注册、注销和计数语义
- `RP-012``gframework-pr-review` 增加 `--json-output``--section``--path` 与文本截断能力,并更新 skill 推荐用法, - `RP-012``gframework-pr-review` 增加 `--json-output``--section``--path` 与文本截断能力,并更新 skill 推荐用法,
让“先落盘、再定向抽取”成为默认可操作路径 让“先落盘、再定向抽取”成为默认可操作路径
- `RP-013` 已完成 `GFramework.Core` 当前 `MA0046` 批次,并以新的事件参数类型替换阶段、协程和异步日志事件的
非标准签名;`GFramework.Core` `net8.0` warnings-only 基线由 `15` 降至 `9`
- `RP-014` 使用 `gframework-pr-review` 复核当前分支 PR #267 的 latest head review threads、outside-diff comment 与
nitpick comment 后,确认 8 条高信号项中仍成立的是 1 个行为 bug 与 7 个文档/测试/跟踪缺口,并按最小改动收口
- `RP-015` 使用 `$gframework-pr-review` 复核 PR #267 的 CTRF 失败测试评论后,确认 `AsyncLogAppender` 仍存在
“队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险 ## 当前风险
- 公共契约兼容风险:剩余 `MA0046` / `MA0016` 若直接改公开委托或集合类型,可能波及用户代码 - 公共契约兼容风险:剩余 `MA0016` 若直接改公开集合类型,可能波及用户代码
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试 - 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定 - 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证 - 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数 - 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数 - 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构 - 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
- 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership主代理负责合并验证 - 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership主代理负责合并验证
@ -121,11 +138,32 @@
- 结果:通过;`--json-output``--section``--path``--max-description-length` 已出现在 CLI 帮助中 - 结果:通过;`--json-output``--section``--path``--max-description-length` 已出现在 CLI 帮助中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo` - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)` - 结果:`0 Warning(s)``0 Error(s)`
- `RP-013` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;相对 `RP-009` / `RP-011` 的 warnings-only 基线 `15 Warning(s)` 已降到 `9 Warning(s)`
当前 `GFramework.Core` `net8.0` 输出中已不再出现 `MA0046`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~CoroutineSchedulerTests|FullyQualifiedName~AsyncLogAppenderTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`50 Passed``0 Failed`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -nologo`
- 结果:失败;当前 worktree 的 Godot restore 资产仍引用 Windows fallback package folder尚未完成独立项目编译验证
- `RP-014` 的定向验证结果:
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果通过host Windows `dotnet` 首次验证前补齐了缺失的 `Meziantou.Analyzer 3.0.48`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`9 Warning(s)``0 Error(s)``AsyncLogAppender` 行为修复与 XML / 文档补充未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CoroutineSchedulerTests.Scheduler_Should_Raise_OnCoroutineException_With_EventArgs|FullyQualifiedName~AsyncLogAppenderTests.Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result|FullyQualifiedName~AsyncLogAppenderTests.ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once|FullyQualifiedName~ArchitectureLifecycleBehaviorTests.InitializeAsync_Should_Raise_PhaseChanged_With_Sender_And_EventArgs" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`4 Passed``0 Failed`
- `RP-015` 的验证结果:
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers --filter "FullyQualifiedName~AsyncLogAppenderTests"`
- 结果:`15 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
- 结果:`1607 Passed``0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步 ## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录 1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 下一轮优先以 `MA0046` 为主批次启动,先从 `Architecture*``CoroutineScheduler` 的低风险 delegate 形状修正中选一个切入点 2. 下一轮优先在 `MA0016``MA0002` 之间选择低风险批次继续推进,默认先看 `LoggingConfiguration` /
3. 若 `MA0046` 的文件 ownership 可以清晰切分,允许使用不同模型的 subagent 并行处理互不冲突的目录或类型簇 `FilterConfiguration``CollectionExtensions`
3. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/` 4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,104 @@
# Analyzer Warning Reduction 追踪 # Analyzer Warning Reduction 追踪
## 2026-04-21 — RP-015
### 阶段PR #267 failed-test follow-up 收口RP-015
- 触发背景:
- 用户指出“测试好像挂了”,按 `$gframework-pr-review` 重新抓取当前分支 PR #267 的 review / checks / CTRF 评论
- PR 评论里同时存在一次 `2143 passed / 0 failed` 与一次 `1 failed` 的 CTRF 报告;失败用例为
`AsyncLogAppenderTests.ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once`
- 复核过程:
- 先跑定向单测时该用例可以单独通过,因此继续核对 PR head commit 与本地整包测试,避免把旧评论误判成当前状态
- 在 `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
下成功复现相同失败,确认问题仍存在于当前代码,而不是单纯的 PR 评论残留
- 同时发现当前沙箱内如果用 shell 循环反复启动 `dotnet test`,会触发 `MSBuild` named pipe `Permission denied`
的环境噪音;后续验证改为单次命令并显式加 `--disable-build-servers`
- 根因结论:
- `AsyncLogAppender.Flush()` 只依赖后台消费循环在处理完某个条目后检查 `_flushRequested`
- 当调用方执行 `Flush()` 前,后台线程已经把最后一个条目消费完并离开检查点时,`Flush()` 会一直等到默认超时,
最终通过 `OnFlushCompleted` 发出一次 `Success=false` 的错误完成通知
- 实施修复:
- 为 `AsyncLogAppender` 增加“当前是否仍有条目在途处理”的状态跟踪
- 抽出 `TrySignalFlushCompletion()`,让 `Flush()` 在请求发出后先做一次即时完成判定;后台循环在每次处理结束后也复用
这条判定路径
- 在 `AsyncLogAppenderTests` 中新增 `Flush_WhenEntriesAlreadyProcessed_Should_Still_ReportSuccess`,稳定覆盖
“调用 Flush 前队列已被后台线程清空”的场景
- 验证结果:
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers --filter "FullyQualifiedName~AsyncLogAppenderTests"`
- 结果:`15 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
- 结果:`1607 Passed``0 Failed`
- 当前结论:
- PR #267 的 failed-test 信号不是纯粹的历史评论噪音,而是当前实现里仍存在的时序竞态
- 修复后该竞态已被稳定回归测试覆盖,当前 `GFramework.Core.Tests` 整包通过
- 下一步建议:
- 若继续 analyzer warning reduction 主题,恢复到 `MA0016` / `MA0002` 低风险批次
## 2026-04-21 — RP-014
### 阶段PR #267 review follow-up 收口RP-014
- 使用 `gframework-pr-review` 抓取当前分支 PR #267 的 latest head review threads、outside-diff comment、nitpick comment、
MegaLinter 摘要与测试报告,并确认本轮除了 6 条 open thread 之外,还存在 1 条 outside-diff 与 1 条 nitpick 需要一并复核
- 本地复核后确认仍成立的项:
- `AsyncLogAppender` 的显式接口实现 `ILogAppender.Flush()` 会在调用 `Flush()` 后再次手动触发 `OnFlushCompleted`
导致接口路径重复通知
- `Architecture.PhaseChanged``CoroutineExceptionEventArgs``ArchitecturePhaseCoordinator.EnterPhase` 的 XML/注释契约仍未完全同步
- `CoroutineSchedulerTests` 的异常事件测试缺少测试级超时
- `docs/zh-CN/core/architecture.md``docs/zh-CN/core/lifecycle.md` 仍缺少明确的 `PhaseChanged` 迁移说明
- `ai-plan` active tracking 中 `RP-013``9 Warning(s)` 需要明确是相对 `RP-009` / `RP-011` 的 warnings-only 基线收敛
- 实施最小修复:
- 删除 `ILogAppender.Flush()` 中重复的完成事件触发,只保留 `Flush(TimeSpan?)` 内的单一通知源
- 为接口调用路径补充单次完成通知回归测试,并为协程异常事件测试增加 `WaitAsync(TimeSpan.FromSeconds(3))`
- 补齐 `Architecture.PhaseChanged``CoroutineExceptionEventArgs``ArchitecturePhaseCoordinator.EnterPhase` 的契约文档
- 在 `docs/zh-CN/core/architecture.md``docs/zh-CN/core/lifecycle.md` 中加入 `phase => ...` 迁移到 `(_, args) => ...` 的说明
- 更新 `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md` 的恢复点、基线描述与验证结果
- 验证结果:
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果通过host Windows `dotnet` 首次验证前补齐了缺失的 `Meziantou.Analyzer 3.0.48`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`9 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CoroutineSchedulerTests.Scheduler_Should_Raise_OnCoroutineException_With_EventArgs|FullyQualifiedName~AsyncLogAppenderTests.Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result|FullyQualifiedName~AsyncLogAppenderTests.ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once|FullyQualifiedName~ArchitectureLifecycleBehaviorTests.InitializeAsync_Should_Raise_PhaseChanged_With_Sender_And_EventArgs" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`4 Passed``0 Failed`
- 当前结论:
- PR #267 里当前仍成立的 CodeRabbit 高信号项已在本地收口
- 修复内容没有改变 `EventHandler<TEventArgs>` 迁移方向,只是补齐行为、文档与恢复信息
- 下一步建议:
- 恢复到 `MA0016` / `MA0002` 主批次,默认先看 `LoggingConfiguration``FilterConfiguration``CollectionExtensions`
## 2026-04-21 — RP-013
### 阶段:`MA0046` 事件签名批次收口RP-013
- 依据 `RP-012` 的下一步建议,本轮恢复到 `GFramework.Core``MA0046` 主批次,而不是继续停留在 PR review workflow 优化
- 本地 warnings-only 基线确认当前 `GFramework.Core` `net8.0` 仍有 `6``MA0046`
- `Architecture.cs`
- `ArchitectureLifecycle.cs`
- `ArchitecturePhaseCoordinator.cs`
- `AsyncLogAppender.cs`
- `CoroutineScheduler.cs` 两处事件
- 方案选择:
- 不再保留 `Action<...>` 事件签名,统一改为标准 `EventHandler<TEventArgs>`
- 为 `Architecture``AsyncLogAppender` 新增放在 `GFramework.Core.Abstractions` 的事件参数类型
- 为 `CoroutineScheduler` 新增放在 `GFramework.Core` 的事件参数类型,因为 `CoroutineHandle` 定义在 runtime 层,不适合反向放入 Abstractions
- `Architecture` 相关事件采用 `Coordinator -> Lifecycle -> Architecture` relay而不是直接透传底层事件确保公开事件的 sender 始终是实际发布者,并避免引入新的 `MA0091`
- 同步适配:
- 更新 `GFramework.Godot/Coroutine/Timing.cs``OnCoroutineFinished` 订阅签名
- 更新 `ArchitectureLifecycleBehaviorTests``CoroutineSchedulerTests``AsyncLogAppenderTests` 以覆盖 sender / event args 契约
- 更新 `docs/zh-CN/core/architecture.md``docs/zh-CN/core/lifecycle.md``PhaseChanged` 示例
- 验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` 输出中已无 `MA0046`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~CoroutineSchedulerTests|FullyQualifiedName~AsyncLogAppenderTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`50 Passed``0 Failed`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -nologo`
- 结果:失败;当前 worktree 的 `project.assets.json` 仍引用 Windows fallback package folder尚未完成 Godot 独立编译验证
- 当前结论:
- `MA0046` 已从 active 批次中移除
- 剩余 `GFramework.Core` `net8.0` warning 分布更新为:`MA0016=5``MA0002=2``MA0015=1``MA0077=1`
- 若继续本主题,下一步默认转入 `MA0016` 批次;若继续触达 Godot再先修复该项目 restore 资产
## 2026-04-21 — RP-012 ## 2026-04-21 — RP-012
### 阶段PR review workflow 输出收窄增强RP-012 ### 阶段PR review workflow 输出收窄增强RP-012

View File

@ -7,19 +7,26 @@
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-010` - 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-017`
- 当前阶段:`Phase 3` - 当前阶段:`Phase 3`
- 当前焦点: - 当前焦点:
- 已建立统一公开 skill`.agents/skills/gframework-doc-refresh/` - 已建立统一公开 skill`.agents/skills/gframework-doc-refresh/`
- 文档重构入口已从“按 guide/tutorial/api 类型拆 skill”收口为“按源码模块驱动文档刷新” - 文档重构入口已从“按 guide/tutorial/api 类型拆 skill”收口为“按源码模块驱动文档刷新”
- PR #268 的当前未解决 review 线程已进入收口Scene/UI 标题层级修正、共享脚本 review 修复、`gframework-pr-review` 多 AI reviewer 支持补齐 - `docs/zh-CN/godot/index.md` 已改成源码优先的模块 landing page不再把 `GetNodeX``CreateSignalBuilder``InstallGodotModule(...)` 写成默认入口
- 下一轮需要用统一 skill 推进 Godot 相关生成器页面核对 - `docs/zh-CN/godot/architecture.md` 已改成当前锚点生命周期、模块挂接顺序和接口边界说明,不再沿用旧版 `.Wait()` 叙述
- `docs/zh-CN/godot/scene.md``docs/zh-CN/godot/ui.md` 已按当前 factory / registry / root / source-generator wiring 重写完成
- `docs/zh-CN/godot/signal.md` 已按当前 `Signal(...)` / `SignalBuilder` / `[BindNodeSignal]` 分工重写完成
- `docs/zh-CN/godot/extensions.md` 已按当前 `GodotPathExtensions``NodeExtensions``SignalFluentExtensions``UnRegisterExtension` 重写完成
- `docs/zh-CN/godot/logging.md` 已按当前 provider / factory / logger 结构、Godot 控制台输出语义与 CoreGrid 架构接线重写完成
- 下一轮高优先级工作转为评估 Godot 栏目当前 active 恢复点是否可以收口并迁入 archive
## 当前状态摘要 ## 当前状态摘要
- 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证 - 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口 - 高优先级模块入口、`core` 关键专题页与 `tutorials/godot-integration.md` 已回到“以源码 / 测试 / README 为准”的状态
- 当前主题仍是 active topic因为 `source-generators` 栏目下的 Godot 相关页面仍可能包含与实现漂移的旧内容,且统一 skill 还需要在该场景上继续落地使用 - `docs/zh-CN/godot/index.md``architecture.md``scene.md``ui.md` 已完成当前实现收口
- 当前主题仍是 active topic因为 Godot 栏目本轮已完成 `logging.md` 收口,但仍需确认是否可以把当前阶段历史迁入
`archive/`,并在下一次推送后跟进 PR #268 的 review 线程收敛情况
## 当前活跃事实 ## 当前活跃事实
@ -57,6 +64,41 @@
- `docs/zh-CN/source-generators/priority-generator.md` 已改成“生成 `IPrioritized`、priority-aware 检索 API、动态优先级边界与诊断”的结构 - `docs/zh-CN/source-generators/priority-generator.md` 已改成“生成 `IPrioritized`、priority-aware 检索 API、动态优先级边界与诊断”的结构
不再把 `GetAllByPriority<T>()` / `system.Init()` 当作所有场景的默认示例 不再把 `GetAllByPriority<T>()` / `system.Init()` 当作所有场景的默认示例
- 本轮重写后再次执行 `cd docs && bun run build` 通过,当前 `source-generators` 栏目改动没有破坏站点构建 - 本轮重写后再次执行 `cd docs && bun run build` 通过,当前 `source-generators` 栏目改动没有破坏站点构建
- `docs/zh-CN/source-generators/godot-project-generator.md` 已改成“包关系、最小接入路径、AutoLoad / InputActions 生成语义、`project.godot` 文件约束与诊断边界”的结构,
明确 `GFrameworkGodotProjectFile` 只能改相对路径、不能改文件名
- `docs/zh-CN/source-generators/get-node-generator.md` 已改成“字段注入职责、路径推断、`Required` / `Lookup` 语义、`_Ready()` 自动补齐边界与冲突诊断”的结构,
明确只有缺少 `_Ready()` 时才会生成 `OnGetNodeReadyGenerated()`
- `docs/zh-CN/source-generators/bind-node-signal-generator.md` 已改成“CLR event 绑定职责、生命周期接线要求、与 `[GetNode]` 的调用顺序、签名约束与命名冲突”的结构,
明确当前不会自动生成 `_Ready()` / `_ExitTree()`
- `docs/zh-CN/source-generators/auto-register-exported-collections-generator.md` 已补齐 frontmatter并改成“成员形状、registry 匹配规则、null-skip 行为、编译期诊断与 CoreGrid 真实采用路径”的结构,
明确生成器依赖的是实例可读集合成员与可读 registry 成员,不要求成员必须带 `[Export]`
- `docs/zh-CN/tutorials/godot-integration.md` 已改成“包关系、`project.godot` 接线、`[GetNode]` / `[BindNodeSignal]` 协作顺序、运行时扩展边界、迁移提醒”的结构,
不再把 `GetNodeX``CreateSignalBuilder``AbstractGodotModule` 默认化叙述为当前推荐路径
- `docs/zh-CN/tutorials/index.md` 中 Godot 教程入口摘要已同步改成“项目级配置 + 生成器协作 + 生命周期边界”,不再继续宣传对象池 / 性能优化式旧范围
- `docs/zh-CN/godot/index.md` 已改成“模块定位、包关系、最小接入路径、关键入口、当前边界”的 landing page 结构,并明确把
`[GetNode]``[BindNodeSignal]``AutoLoads``InputActions` 归到 `GFramework.Godot.SourceGenerators`
- `docs/zh-CN/godot/architecture.md` 已改成“何时继承 `AbstractArchitecture`、何时使用 `InstallGodotModule(...)`、锚点生命周期、
`IGodotModule` 契约边界”的结构,不再把 `OnPhase(...)` / `OnArchitecturePhase(...)` 写成稳定自动广播
- 本轮再次执行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh` 校验 `godot/index.md`
`godot/architecture.md`,并执行 `cd docs && bun run build`,站点构建继续通过
- `docs/zh-CN/godot/scene.md` 已改成“公开入口、factory 实际行为、项目侧 router/root wiring、`[AutoScene]` 最小接入路径、
当前边界”的结构,明确当前没有 `GodotSceneRouter`,且 `GodotSceneFactory` 会在 provider 缺失时回退到
`SceneBehaviorFactory`
- `docs/zh-CN/godot/ui.md` 已改成“公开入口、layer behavior 语义、项目侧 router/root wiring、`[AutoUiPage]` 最小接入路径、
输入与暂停边界”的结构,明确当前没有 `GodotUiRouter`,且 `GodotUiFactory` 仍强制要求 `IUiPageBehaviorProvider`
- 本轮已执行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
`bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`,两页聚焦校验通过
- `docs/zh-CN/godot/signal.md` 已改成“当前公开入口、动态绑定最小接入路径、与 `[BindNodeSignal]` 的分工、当前边界”的结构,
不再沿用旧 `CreateSignalBuilder(...)` / builder-pattern 教程式长篇叙述
- `docs/zh-CN/godot/extensions.md` 已改成“真实扩展分组、Node 辅助成员表、`UnRegisterWhenNodeExitTree(...)` 生命周期边界、
当前边界”的结构,不再把扩展层写成覆盖所有 Godot 开发动作的万能工具箱
- 本轮已执行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
`bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`,两页聚焦校验通过
- 本轮再次执行 `cd docs && bun run build` 通过,当前 Godot signal / extensions 页面改动没有破坏站点构建
- `docs/zh-CN/godot/logging.md` 已改成“当前公开入口、最小接入路径、Godot 控制台输出语义、`[Log]` 协作边界、当前限制”的结构,
不再把直接改写 `LoggerFactoryResolver.Provider``AbstractGodotModule` 或 Godot 专用日志 API 写成默认接入模型
- 本轮已执行 `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
`cd docs && bun run build`logging 页面聚焦校验与站点构建继续通过
- `.agents/skills/gframework-doc-refresh/SKILL.md` 已改成标准 YAML frontmatter skill并明确支持模块输入、证据顺序、输出优先级与验证步骤 - `.agents/skills/gframework-doc-refresh/SKILL.md` 已改成标准 YAML frontmatter skill并明确支持模块输入、证据顺序、输出优先级与验证步骤
- `.agents/skills/gframework-doc-refresh/SKILL.md``description` 已加引号,修复 `Recommended command:` 中冒号导致的 - `.agents/skills/gframework-doc-refresh/SKILL.md``description` 已加引号,修复 `Recommended command:` 中冒号导致的
invalid YAML skill 加载警告 invalid YAML skill 加载警告
@ -74,7 +116,12 @@
- 旧专题页示例失真风险:`docs/zh-CN/game/*``source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例 - 旧专题页示例失真风险:`docs/zh-CN/game/*``source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例
- 缓解措施:`game/scene.md``ui.md``source-generators/context-aware-generator.md``priority-generator.md` 已完成收口; - 缓解措施:`game/scene.md``ui.md``source-generators/context-aware-generator.md``priority-generator.md` 已完成收口;
`godot-project-generator.md``get-node-generator.md``bind-node-signal-generator.md``auto-register-exported-collections-generator.md`
已完成收口;
继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对剩余 Godot 相关页面,不把旧文档当事实来源 继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对剩余 Godot 相关页面,不把旧文档当事实来源
- Godot 栏目归档过早风险:虽然 `logging.md` 已完成收口,但如果在推送前就把当前阶段过早归档,后续 review 跟进会缺少
清晰的 active 恢复入口
- 缓解措施:先保留当前 topic 为 active待确认本轮页面集与 PR #268 的 review 跟进节奏后,再决定是否迁入 `archive/`
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择 - 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring - 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
- 模块映射不全风险:统一 skill 若遗漏模块别名、测试项目或 docs 栏目映射,会让后续扫描阶段直接失焦 - 模块映射不全风险:统一 skill 若遗漏模块别名、测试项目或 docs 栏目映射,会让后续扫描阶段直接失焦
@ -115,10 +162,25 @@
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Core` - `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Core`
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot.SourceGenerators` - `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot.SourceGenerators`
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Cqrs` - `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Cqrs`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/godot-project-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/get-node-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/bind-node-signal-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/auto-register-exported-collections-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- `rg -n "GetNodeX|CreateSignalBuilder|GodotGameArchitecture|AbstractGodotModule|InstallGodotModule\(|GFramework\.Godot\.Pool" docs/zh-CN/godot docs/zh-CN/tutorials -S`
- `cd docs && bun run build`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
- `rg -n "GodotSceneRouter|GodotUiRouter|CreateSignalBuilder|GetNodeX|InstallGodotModule\(" docs/zh-CN/godot -S`
- `cd docs && bun run build`
## 下一步 ## 下一步
1. 继续核对 Godot 相关生成器页面,优先处理 `godot-project-generator.md``get-node-generator.md` 1. 评估当前 Godot 栏目页面集是否已足够稳定,决定是否把本阶段 active 恢复点收口并迁入 `archive/`
`bind-node-signal-generator.md`,优先用 `gframework-doc-refresh` 的模块扫描结果驱动判断 2. 如需继续保持 active优先精简 tracking / trace只保留归档决策、当前风险与下一次 PR follow-up 入口
2. 下一次推送后先重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛 3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛
3. 再继续确认 `project.godot``AutoLoad` / `InputActions``GetNode` / `BindNodeSignal` 示例仍与当前包关系和生成器入口一致

View File

@ -0,0 +1,103 @@
# Documentation Governance And Refresh Trace
## 2026-04-22
### 当前恢复点RP-017
- 本轮从 PR #268 的最新 review 数据恢复未发现失败检查CTRF 报告显示 2139 个测试全部通过
- 本轮复核确认当前 PR 的 latest-head open thread 同时来自 `coderabbitai[bot]``greptile-apps[bot]`
- 已本地修复仍然成立的 review
- `docs/zh-CN/game/scene.md` 把“推荐目录与文件约定(项目侧)”降为“最小接入路径”下的子节
- `docs/zh-CN/game/ui.md` 为“最小接入路径”补充导语,并修复同级标题错位
- `.agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh` 改成 opening / closing fence 状态机
- `.agents/skills/_shared/module-config.sh` 补齐缺失模块映射,并让未映射模块返回非零退出码
- `gframework-pr-review` 已从文案和输出模型两侧补齐多 reviewer 支持:当前 JSON 会单独给出 `review_agents`
以及 `open_thread_counts_by_user`,文本输出会显式列出 CodeRabbit / Greptile
- `fetch_current_pr_review.py` 的本地函数 docstring 覆盖率已补到 `44/44`
- 已闭环 RP-001 到 RP-008 的执行细节已归档到
`ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-rp-001-through-rp-008.md`
- 本轮按 `gframework-doc-refresh` 的模块扫描结果,重写了 `Godot.SourceGenerators` 的 3 个高风险专题页:
- `godot-project-generator.md`
- `get-node-generator.md`
- `bind-node-signal-generator.md`
- 新页面统一收口到“包关系、最小接入路径、真实生成语义、生命周期边界、诊断约束”,不再沿用旧教程式长篇 API 罗列
- 本轮额外复核了 `ai-libs/CoreGrid` 的真实采用方式,确认 `[GetNode]` / `[BindNodeSignal]` 组合使用时应先注入节点再绑定事件
- 本轮继续收口 `auto-register-exported-collections-generator.md`,补齐 frontmatter并把“导出集合”纠正为“实例可读集合成员 + registry 成员 + 单参数实例方法”的真实契约
- 本轮已重写 `docs/zh-CN/tutorials/godot-integration.md`,把内容收口为“包关系、`project.godot` 接线、`[GetNode]` /
`[BindNodeSignal]` 协作顺序、运行时扩展边界、迁移提醒”,不再把旧 Godot API 列表当事实来源
- `docs/zh-CN/tutorials/index.md` 的 Godot 教程入口摘要已同步改成当前采用路径,避免入口页继续把教程描述成对象池 / 性能优化总览
- 本轮已重写 `docs/zh-CN/godot/index.md`,改成“模块定位、包关系、最小接入路径、关键入口、当前边界”的 landing page 结构,
明确把 `[GetNode]``[BindNodeSignal]``AutoLoads``InputActions` 收口到 `GFramework.Godot.SourceGenerators`
- 本轮已重写 `docs/zh-CN/godot/architecture.md`,改成“锚点生命周期、`InstallGodotModule(...)` 执行顺序、`IGodotModule`
契约边界”的结构,不再沿用旧版 `.Wait()` 和自动阶段广播叙述
- 本轮已重写 `docs/zh-CN/godot/scene.md`把内容收口为“公开入口、factory 真实行为、项目侧 router/root wiring、
`ISceneBehaviorProvider``[AutoScene]` 的真实关系、当前边界”,不再继续虚构 `GodotSceneRouter`
- 本轮已重写 `docs/zh-CN/godot/ui.md`把内容收口为“公开入口、layer behavior 语义、项目侧 router/root wiring、
`IUiPageBehaviorProvider``[AutoUiPage]` 的真实关系、输入与暂停边界”,不再继续虚构 `GodotUiRouter`
- 本轮额外确认 Godot Scene / UI 的关键差异:`GodotSceneFactory` 在 provider 缺失时会回退到 `SceneBehaviorFactory`
`GodotUiFactory` 仍会在缺失 `IUiPageBehaviorProvider` 时直接抛异常;这已写入两页文档,避免继续把两者描述成同一种接入模型
- 本轮已重写 `docs/zh-CN/godot/signal.md`,把内容收口为“当前公开入口、动态绑定最小接入路径、与 `[BindNodeSignal]`
的分工、当前边界”,明确当前入口是 `Signal(...)` 而不是旧 `CreateSignalBuilder(...)`
- 本轮已重写 `docs/zh-CN/godot/extensions.md`,把内容收口为“真实扩展分组、`NodeExtensions` 实际成员、`UnRegisterWhenNodeExitTree(...)`
生命周期边界、当前边界”,不再继续宣称存在覆盖所有 Godot 场景的万能扩展层
- 本轮复核 `ai-libs/CoreGrid` 的动态绑定用法后,明确把 fluent API 定位为“动态对象 / 动态 signal 的运行时连接”,而把静态控件绑定继续归到
`[BindNodeSignal]` 生成器链路
- 本轮已重写 `docs/zh-CN/godot/logging.md`,把内容收口为“当前 provider / factory / logger 结构、最小接入路径、
Godot 控制台输出语义、`[Log]` 协作边界、当前限制”,不再把直接改全局 provider 或 `AbstractGodotModule` 写成默认采用路径
- 本轮额外复核 `GFramework.Godot/Logging/*.cs``GFramework.Core.Abstractions/Logging/LoggerFactoryResolver.cs`
`GFramework.Core/Logging/CachedLoggerFactory.cs``ai-libs/CoreGrid/global/GameEntryPoint.cs`,确认当前推荐接法应以
`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider` 为主,而不是先写 `LoggerFactoryResolver.Provider = ...`
### 当前决策
- active trace 只保留当前恢复点、关键事实、验证和下一步;完成阶段继续进入 `archive/traces/`
- `scene.md``ui.md` 的集成说明除目录布局外,也要保证标题层级能真实反映采用路径语义
- `gframework-pr-review` 继续以 latest-head unresolved thread 为主信号,同时显式声明支持的 AI reviewer 名单,避免 skill
声明与实际抓取能力再次漂移
- `Godot.SourceGenerators` 专题页继续采用“源码 / 测试 / README 优先,`ai-libs/` 只补消费者 wiring”的证据顺序
- `BindNodeSignal` 页面明确记录“当前不自动生成 `_Ready()` / `_ExitTree()`”,避免继续把它写成自动生命周期织入器
- `auto-register-exported-collections` 页面明确区分“运行时 null 时跳过注册”和“配置错误时编译期报错”,避免旧文档把两类边界混为一谈
- `godot-integration.md` 已重新成为可用的采用路径入口;后续 Godot 文档收口应优先处理 `godot/index.md``godot/architecture.md`
- `godot/index.md``godot/architecture.md` 现在都必须维持“运行时包与生成器包分边界”的写法,不能再把场景注入和项目元数据生成写回
`GFramework.Godot` 运行时契约
- `scene.md` 已明确记录“项目侧 router + Godot factory/registry/root”这一分工后续不要再把 router 包装回
`GFramework.Godot` 运行时
- `ui.md` 已明确记录 `Page` 必须走 `PushAsync` / `ReplaceAsync``Show(..., UiLayer.Page)` 在当前实现中会抛异常;
后续不要再把所有 UI 入口重新写回统一 `Show(...)`
- `signal.md` 已明确为 `Signal(...)` / `SignalBuilder` 的轻量 fluent 包装说明页,不再继续混入生成器职责
- `extensions.md` 已明确限制在 `GodotPathExtensions``NodeExtensions``SignalFluentExtensions``UnRegisterExtension`
这四组当前存在的扩展
- `logging.md` 已完成收口;下一轮优先级转为评估当前 Godot 栏目恢复点是否可以迁入 `archive/`,并保留 PR review follow-up 入口
### 验证
- `python3 -B .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch docs/sdk-update-documentation --format json --json-output /tmp/current-pr-review.json`
- `python3 -B .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch docs/sdk-update-documentation --section open-threads`
- `python3 -B -c "import ast, pathlib; path=pathlib.Path('.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py'); tree=ast.parse(path.read_text(encoding='utf-8')); funcs=[node for node in ast.walk(tree) if isinstance(node,(ast.FunctionDef, ast.AsyncFunctionDef))]; documented=sum(1 for node in funcs if ast.get_docstring(node)); print(f'functions={len(funcs)} documented={documented} coverage={documented/len(funcs):.2%}')"`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh docs/zh-CN/game/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh docs/zh-CN/game/ui.md`
- `bash -lc 'source .agents/skills/_shared/module-config.sh && get_readme_paths Core.SourceGenerators.Abstractions && if get_readme_paths Not.Real.Module; then exit 1; else echo unmapped-ok; fi'`
- `cd docs && bun run build`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/godot-project-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/get-node-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/bind-node-signal-generator.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/auto-register-exported-collections-generator.md`
- `cd docs && bun run build`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- `cd docs && bun run build`
- `rg -n "GetNodeX|CreateSignalBuilder|GodotGameArchitecture|AbstractGodotModule|InstallGodotModule\(|GFramework\\.Godot\\.Pool" docs/zh-CN/godot docs/zh-CN/tutorials -S`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
- `rg -n "GodotSceneRouter|GodotUiRouter|CreateSignalBuilder|GetNodeX|InstallGodotModule\(" docs/zh-CN/godot -S`
- `cd docs && bun run build`
### 下一步
1. 评估当前 Godot 栏目页面集是否已足够稳定,决定是否把当前恢复点收口并迁入 `archive/`
2. 如暂不归档,先把 active tracking / trace 进一步压缩到归档决策、当前风险与 PR 跟进入口
3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否关闭或减少

View File

@ -0,0 +1,62 @@
# Documentation Governance And Refresh 跟踪
## 目标
继续以“文档必须可追溯到源码、测试与真实接入方式”为原则,维护 `GFramework` 的仓库入口、模块入口与
`docs/zh-CN` 采用链路,避免 README、专题页与教程再次偏离当前实现。
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-019`
- 当前阶段:`Completed`
- 当前焦点:
- 用户已确认 PR #268 合并,本 topic 对应的文档治理收口工作完成
- 当前目录将在本轮迁入 `ai-plan/public/archive/documentation-governance-and-refresh/`
- 后续若需历史回溯,应从 archive 中恢复,而不是继续把该 topic 作为 active 默认入口
## 当前状态摘要
- `docs/zh-CN/godot/` 当前高优先级页面集与 `docs/zh-CN/tutorials/godot-integration.md` 已完成源码优先收口
- PR #268 已合并,上一轮保留 active 的唯一原因已经解除
- 本 topic 已达到归档条件实现完成、校验完成、PR 生命周期结束
## 当前活跃事实
- 当前 worktree 下未发现 `ai-plan/private/` 恢复目录,本主题一直以 public artifacts 作为唯一恢复入口
- 已存在的阶段归档:
- `ai-plan/public/documentation-governance-and-refresh/archive/todos/documentation-governance-and-refresh-history-through-2026-04-22.md`
- `ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-history-through-2026-04-22.md`
- 2026-04-22 之前的长篇历史已存在于 2026-04-18 与 RP-001 through RP-008 的归档文件中
- 当前待处理事项已清零;后续只保留历史查询价值
## 当前风险
- 后续历史定位风险:如果不把 topic 从 active 列表中移除,`boot` 会继续把已经完成的文档治理主题当作默认入口
- 缓解措施:本轮同步更新 `ai-plan/public/README.md` 并把整个 topic 目录迁入 `ai-plan/public/archive/`
- 文档回漂风险:未来若有新的 README / `docs/zh-CN` 变更,仍可能重新引入与源码不一致的表述
- 缓解措施:新任务应创建或复用新的 active topic而不是重启当前已完成主题
## 活跃文档
- 当前 trace[documentation-governance-and-refresh-trace.md](../traces/documentation-governance-and-refresh-trace.md)
- 2026-04-22 跟踪归档:[documentation-governance-and-refresh-history-through-2026-04-22.md](../archive/todos/documentation-governance-and-refresh-history-through-2026-04-22.md)
- 2026-04-22 trace 归档:[documentation-governance-and-refresh-history-through-2026-04-22.md](../archive/traces/documentation-governance-and-refresh-history-through-2026-04-22.md)
- 2026-04-18 历史归档:[documentation-governance-and-refresh-history-through-2026-04-18.md](../archive/todos/documentation-governance-and-refresh-history-through-2026-04-18.md)
- RP-001 到 RP-008 trace 归档:[documentation-governance-and-refresh-rp-001-through-rp-008.md](../archive/traces/documentation-governance-and-refresh-rp-001-through-rp-008.md)
## 验证说明
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- `cd docs && bun run build`
## 下一步
1. 将整个 `documentation-governance-and-refresh` 目录迁入 `ai-plan/public/archive/`
2. 从 `ai-plan/public/README.md` 删除该 topic 的 active 声明与 worktree 映射

View File

@ -0,0 +1,25 @@
# Documentation Governance And Refresh Trace
## 2026-04-22
### 当前恢复点RP-019
- 本轮按 `boot` 恢复 `documentation-governance-and-refresh` 主题
- 用户明确说明 PR #268 已合并,因此该主题不再需要保持 active 以等待 review follow-up
- 当前主题满足完成条件:文档页已完成校验、`docs` 站点先前已构建通过、PR 生命周期结束
- 本轮将把整个主题目录迁入 `ai-plan/public/archive/documentation-governance-and-refresh/`
- `ai-plan/public/README.md` 也将在本轮删除该 topic 的 active 声明与 worktree 映射
### 当前决策
- 当前主题正式归档,不再作为 `boot` 默认入口
- 若未来出现新的文档治理任务,应创建新的 active topic 或挂到新的现役主题,而不是恢复本目录
- 现有 tracking / trace 留在 archive 中作为历史恢复材料
### 验证
- `cd docs && bun run build`
### 下一步
1. 若需回看本阶段历史,从 `ai-plan/public/archive/documentation-governance-and-refresh/` 读取归档材料

View File

@ -0,0 +1,146 @@
# Documentation Full Coverage Governance 跟踪
## 目标
建立一个长期 active topic持续治理 `GFramework` 的 README、`docs/zh-CN`、站点导航、XML 文档和 API
参考链路,避免历史上的阶段性刷新完成后再次回漂。
- 用源码、测试、`*.csproj` 和必要的 `ai-libs/` 证据校正文档
- 以模块族为单位闭环 README、landing page、专题页、教程入口和 API 参考链路
- 明确哪些目录是可直接消费模块,哪些只是内部支撑模块
- 把 XML 文档缺口纳入治理范围,而不是只刷新 Markdown
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-004`
- 当前阶段:`Phase 3 - Cqrs Docs Refresh`
- 当前焦点:
- 收口 `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` 的 landing / generator topic / API 入口
- 延续 `README / landing / API reference / XML inventory` 的同一治理模板
- 为下一波 `Game` family 审计保留统一的恢复模板与验证口径
## 当前状态摘要
- 已归档的 `documentation-governance-and-refresh` 仅保留为历史证据,不再作为默认 `boot` 入口
- 本轮已确认的消费属性结论:
- `GFramework.Ecs.Arch.Abstractions`:可打包直接消费模块,需要 README 和文档入口
- `GFramework.Core.SourceGenerators.Abstractions``IsPackable=false`,按内部支撑模块处理
- `GFramework.Godot.SourceGenerators.Abstractions``IsPackable=false`,按内部支撑模块处理
- `GFramework.SourceGenerators.Common``IsPackable=false`,按内部支撑模块处理
- 本轮已完成的治理动作:
- 新建 `GFramework.Ecs.Arch.Abstractions/README.md`
- 在根 `README.md` 中补齐 `GFramework.Ecs.Arch.Abstractions` 入口,并声明内部支撑模块 owner
- 为抽象接口栏目补齐 `Ecs.Arch.Abstractions` 页面与 sidebar 入口
- 将 `docs/zh-CN/api-reference/index.md` 重写为模块到 XML / README / 教程的阅读链路入口
- 为 `GFramework.Core/README.md` 补齐 `Services``Configuration``Environment``Pool``Rule``Time` 等当前目录映射
- 为 `GFramework.Core.Abstractions/README.md` 补齐契约族地图与 XML 阅读重点
- 将 `docs/zh-CN/abstractions/core-abstractions.md` 从过时的接口摘录页重写为契约边界 / 包关系 / 最小接入路径页面
- 为 `docs/zh-CN/core/index.md` 补齐 frontmatter、能力域导航和 API / XML 阅读入口
- 为 `GFramework.Core/README.md``GFramework.Core.Abstractions/README.md` 补齐类型族级 XML 覆盖基线入口
- 为 `docs/zh-CN/core/index.md``docs/zh-CN/abstractions/core-abstractions.md` 增加“类型族 -> XML 覆盖状态 -> 代表类型”的 inventory
- 基于顶层目录轻量盘点确认:`Core` / `Core.Abstractions` 当前公开 / 内部类型声明都已带 XML 注释,成员级审计留待后续波次
- 重写 `docs/zh-CN/ecs/index.md`,收敛当前 ECS family 的包边界、采用顺序和 XML inventory
- 重写 `docs/zh-CN/ecs/arch.md`,明确 `UseArch(...)` 需早于 `Initialize()` 的真实接入时机
- 刷新 `GFramework.Ecs.Arch/README.md`,使运行时 README 与源码 / 测试一致
- 为 `GFramework.Ecs.Arch.Abstractions/README.md``docs/zh-CN/abstractions/ecs-arch-abstractions.md` 补齐类型族级 XML inventory
- 重写 `docs/zh-CN/core/cqrs.md`,将其收敛为 `Cqrs` family landing并补齐运行时 / 契约层 / 生成器的 XML inventory
- 新建 `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`,为 `Cqrs.SourceGenerators` 补齐站内专题入口
- 更新 `docs/zh-CN/source-generators/index.md``docs/zh-CN/api-reference/index.md` 与 VitePress sidebar使 `Cqrs` family 的 generator 入口可导航
- 为 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs``GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中缺失的内部类型补齐 XML 注释,使本轮轻量 inventory 达到声明级闭环
## Inventory第一版
| 模块族 | 当前状态 | 当前证据 | 下一动作 |
| --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | `README / landing / 类型族级 XML inventory 已收口,成员级审计待补齐` | 根 README、模块 README、`docs/zh-CN/core/**``docs/zh-CN/abstractions/core-abstractions.md` 已对齐当前目录与类型族基线 | 进入巡检;如有新 API 变更,再追加成员级 XML 审计 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | `README / landing / generator topic / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Cqrs/README.md``GFramework.Cqrs.Abstractions/README.md``GFramework.Cqrs.SourceGenerators/README.md``docs/zh-CN/core/cqrs.md``docs/zh-CN/source-generators/cqrs-handler-registry-generator.md``docs/zh-CN/api-reference/index.md` 已对齐当前源码与测试 | 转入巡检;下一波切到 `Game` family 的 XML / 教程链路审计 |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `已验证` | 根 README、模块 README、`docs/zh-CN/game/**` 和 abstractions 页已存在 | 后续波次补 XML / 教程链路审计 |
| `Godot` / `Godot.SourceGenerators` | `已验证` | 上一轮归档 topic 已完成核心 landing / topic / tutorial 校验 | 进入巡检周期,重点看回漂 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | `README / landing / abstractions / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Ecs.Arch/README.md``GFramework.Ecs.Arch.Abstractions/README.md``docs/zh-CN/ecs/**``docs/zh-CN/abstractions/ecs-arch-abstractions.md` 已对齐当前源码与测试 | 转入巡检;后续仅在运行时公共 API 变动时补成员级 XML 细审 |
| `SourceGenerators.Common``*.SourceGenerators.Abstractions` | `已判定为内部支撑` | `*.csproj` 明确 `IsPackable=false` | 由所属模块 README 与生成器栏目说明 owner不建独立采用页 |
## 缺口分级
- `P0`
- 错误采用路径、错误包关系、错误 API / 生命周期语义
- 站点导航死链、空 landing page、明显错误的模块 owner
- `P1`
- 直接消费模块缺 README 或缺对应 docs 入口
- README / docs 示例与源码实现不一致
- 教程仍引用已经过时的默认接线方式
- `P2`
- 结构重复、交叉链接不足、API 参考链路过薄
- 站内页面存在事实正确但组织方式不利于定位的内容
## 当前风险
- 当前 `Core` / `Core.Abstractions` 只完成了类型族级 XML 基线,不等于成员级契约全审计
- 缓解措施:后续只在共享抽象或高风险生命周期接口发生改动时补成员级细审,不在本轮扩张范围
- 其他模块族尚未全部建立同粒度的 XML inventory
- 缓解措施:按 `Ecs``Cqrs``Game` 的波次顺序继续推广同一模板
- 新功能分支若修改 README / docs / 公共 API 却不挂文档 topic仍可能回漂
- 缓解措施:将本 topic 作为长期 active topic 保留,并在后续巡检中记录回漂来源
- VitePress 页面不能直接链接到 `docs/` 目录之外的模块 `README.md`
- 缓解措施:站内页面用模块路径文本或站内 API 入口表达,仓库级 README 仍保留仓库文件链接
- `GFramework.Cqrs` 在当前 WSL / dotnet 环境下,本地 build 仍会读取失效的 fallback package folder 配置,导致无法完成该项目的标准编译验证
- 缓解措施:本轮先以 `GFramework.Cqrs.SourceGenerators` 编译通过和 docs site build 通过作为有效验证,并在后续环境治理或构建脚本清理时单独处理 `RestoreFallbackFolders` / 资产文件问题
## 验证说明
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/index.md`
- 结果:通过
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
- 结果:通过
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
- 结果:通过
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
- 结果:通过
- 备注:`2026-04-22` 在补充 Core XML inventory 后重新验证
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/core-abstractions.md`
- 结果:通过
- 备注:`2026-04-22` 在补充 Core.Abstractions XML inventory 后重新验证
- `cd docs && bun run build`
- 结果:通过
- 备注:`2026-04-22` 重新构建通过;仅保留 VitePress 大 chunk warning无构建失败
- `dotnet build GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`
- 结果:通过
- 备注:`0 Warning(s) / 0 Error(s)`
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`
- 结果:通过
- 备注:`0 Warning(s) / 0 Error(s)`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/index.md`
- 结果:通过
- 备注:`2026-04-22` 在重写 ECS landing 后重新验证
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
- 结果:通过
- 备注:`2026-04-22` 在重写 Arch ECS 专题页后重新验证
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
- 结果:通过
- 备注:`2026-04-22` 在补充抽象页 XML inventory 后重新验证
- `cd docs && bun run build`
- 结果:通过
- 备注:`2026-04-22` 在 Ecs 波次重写后重新构建通过;仅保留 VitePress 大 chunk warning无构建失败
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
- 结果:通过
- 备注:`2026-04-22` 在重写 `Cqrs` family landing 后重新验证
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- 结果:通过
- 备注:`2026-04-22` 在新增 `Cqrs.SourceGenerators` 专题页后验证通过
- `python3` 轻量 XML inventory 扫描
- 结果:通过
- 备注:`2026-04-22` 确认 `GFramework.Cqrs``Internal/``14/14``GFramework.Cqrs.SourceGenerators/Cqrs/``3/3``GFramework.Cqrs.Abstractions/Cqrs/``20/20`
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -p:RestoreFallbackFolders=`
- 结果:通过
- 备注:保留既有 `NU1900``MA0051` warnings无新增编译错误
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:失败
- 备注:当前环境会命中失效的 Windows fallback package folder并在多目标 inner build 阶段触发 `MSB4276` / `MSB4018`;失败原因已记录为环境阻塞,不属于本轮文档改动回归
- `cd docs && bun run build`
- 结果:通过
- 备注:`2026-04-22``Cqrs` 波次文档刷新后重新构建通过;仅保留 VitePress 大 chunk warning无构建失败
## 下一步
1. 切换到 `Game` / `Game.Abstractions` / `Game.SourceGenerators` 波次,按 `Cqrs` 模板核对 README / landing / tutorials / API reference / XML 链路
2. 评估 `Game` family 当前是否已经具备类型族级 XML inventory还是仍停留在“README / 页面存在但不可审计”
3. 在后续环境治理任务中单独处理 `GFramework.Cqrs` 本地 build 的 fallback package folder 阻塞,避免影响后续代码类验证

View File

@ -0,0 +1,143 @@
# Documentation Full Coverage Governance Trace
## 2026-04-22
### 当前恢复点RP-001
- 按长期治理计划新建 active topic `documentation-full-coverage-governance`
- 在 `ai-plan/public/README.md` 中将当前分支 `docs/sdk-update-documentation` 映射到该 topic
- 复核已知缺口模块的 `*.csproj` 后确认:
- `GFramework.Ecs.Arch.Abstractions` 是可打包消费模块,需要独立 README
- `GFramework.Core.SourceGenerators.Abstractions``GFramework.Godot.SourceGenerators.Abstractions`
`GFramework.SourceGenerators.Common` 都是 `IsPackable=false` 的内部支撑模块
- 基于该结论,本轮没有为内部支撑模块新增独立 README而是在根 README 与 abstractions / API 入口中明确其 owner
### 当前决策
- 新主题的完成条件采用长期治理口径:`P0` 清零、无 README 缺失、无导航死链,并完成连续两轮稳定巡检
- 本轮先做治理基础设施与 inventory不把整个长期计划伪装成单轮完成
- `api-reference` 页面改为“模块 -> README / docs / XML / tutorial”的阅读链路入口避免继续维护失真的伪签名列表
- `Ecs.Arch` family 被列为高优先 backlog抽象层入口已补齐但 runtime docs 仍需按源码重写
- `Core` / `Core.Abstractions` 波次先收口 README、landing page 和 abstractions 页的目录映射,再补显式 XML 覆盖 inventory
- VitePress 站内页面不直接链接仓库根模块 `README.md`;站内仅保留可构建的 docs 链接,模块 README 以文本路径或仓库 README 承接
### 当前恢复点RP-002
- 完成 `Core` / `Core.Abstractions` 的类型族级 XML inventory
- `GFramework.Core/README.md`
- `GFramework.Core.Abstractions/README.md`
- `docs/zh-CN/core/index.md`
- `docs/zh-CN/abstractions/core-abstractions.md`
- 通过顶层目录轻量盘点确认:
- `GFramework.Core` 当前各目录族的公开 / 内部类型声明都已带 XML 注释
- `GFramework.Core.Abstractions` 当前各契约目录族的公开 / 内部类型声明都已带 XML 注释
- 这轮 inventory 明确限定为“类型声明级基线”,不把结果表述成成员级 XML 合规审计
### 当前决策RP-002
- XML inventory 同时落在模块 README 和站内 landing page
- README 提供仓库侧入口,方便从包目录直接恢复上下文
- docs landing 提供更细的类型族 / 代表类型 / 阅读重点表格,方便站内导航
- `Core` 波次在补齐基线后转入巡检,不继续在本轮展开成员级 ``<param>`` / ``<returns>`` 审计
- 下一恢复点切换到 `Ecs` 波次,优先处理仍明显失真的 runtime docs
### 当前验证
- 文档校验:
- `validate-all.sh docs/zh-CN/abstractions/index.md`:通过
- `validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`:通过
- `validate-all.sh docs/zh-CN/api-reference/index.md`:通过
- `validate-all.sh docs/zh-CN/core/index.md`:通过
- `validate-all.sh docs/zh-CN/abstractions/core-abstractions.md`:通过
- 构建校验:
- `cd docs && bun run build`:通过
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`:通过,`0 Warning(s) / 0 Error(s)`
- `dotnet build GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`:通过,`0 Warning(s) / 0 Error(s)`
### 当前验证RP-002
- 文档校验:
- `validate-all.sh docs/zh-CN/core/index.md`:通过
- `validate-all.sh docs/zh-CN/abstractions/core-abstractions.md`:通过
- 构建校验:
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning无构建失败
### 当前恢复点RP-003
- 完成 `Ecs.Arch` 波次的运行时文档刷新:
- `docs/zh-CN/ecs/index.md`
- `docs/zh-CN/ecs/arch.md`
- `GFramework.Ecs.Arch/README.md`
- 为 `Ecs.Arch.Abstractions` 补齐与运行时页同粒度的 XML inventory
- `GFramework.Ecs.Arch.Abstractions/README.md`
- `docs/zh-CN/abstractions/ecs-arch-abstractions.md`
- 明确记录一个关键采用事实:
- `UseArch(...)` 必须早于 `Initialize()` 调用
- 该结论以 `ArchExtensions` 的模块注册方式和 `ExplicitRegistrationTests` 为证据
- 将 `Ecs.Arch` family 从“入口存在但失真”推进到“README / landing / abstractions / XML inventory 已对齐源码与测试”
### 当前决策RP-003
- `Ecs` 波次继续采用与 `Core` 相同的治理粒度:
- 模块 README 承担仓库入口
- `docs/zh-CN/ecs/index.md` 承担模块族 landing
- `docs/zh-CN/ecs/arch.md` 承担运行时默认实现专题页
- `docs/zh-CN/abstractions/ecs-arch-abstractions.md` 承担契约边界专题页
- `EnableStatistics` 当前仅保留在公开配置面上;文档不再把它写成已验证的运行时行为
- 下一恢复点切换到 `Cqrs` 波次,优先解决入口分散和 API / XML 阅读链路不统一的问题
### 当前验证RP-003
- 文档校验:
- `validate-all.sh docs/zh-CN/ecs/index.md`:通过
- `validate-all.sh docs/zh-CN/ecs/arch.md`:通过
- `validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`:通过
- 构建校验:
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning无构建失败
### 下一步
1. 在 `Cqrs` 波次核对模块 README、`docs/zh-CN/core/cqrs.md``docs/zh-CN/source-generators/**` 的真实 owner
2. 决定 `Cqrs` family 是补 dedicated landing 还是拆分现有入口页
### 当前恢复点RP-004
- 完成 `Cqrs` 波次的模块族入口刷新:
- 重写 `docs/zh-CN/core/cqrs.md`
- 新建 `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- 更新 `docs/zh-CN/source-generators/index.md`
- 更新 `docs/zh-CN/api-reference/index.md`
- 更新 `docs/.vitepress/config.mts`
- 将 `Cqrs` family 从“README 已存在但 generator 入口分散”推进到“runtime / abstractions / source generator 都有明确站内入口”
- 为 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`
`GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中缺失的内部类型补齐 XML 注释
- 基于轻量扫描确认:
- `GFramework.Cqrs.Abstractions/Cqrs/` 当前类型声明级 XML 覆盖为 `20/20`
- `GFramework.Cqrs` 根入口与 `Internal/` 已补到 `19/19`
- `GFramework.Cqrs.SourceGenerators/Cqrs/` 当前类型声明级 XML 覆盖为 `3/3`
### 当前决策RP-004
- `docs/zh-CN/core/cqrs.md` 继续保留在 `Core` 栏目,但其角色调整为 `Cqrs` family landing而不再只是 runtime 简介页
- `Cqrs.SourceGenerators` 不单独新建一级导航栏目,而是在 `source-generators` 栏目内补一个专用专题页,保持站点 taxonomy 稳定
- generator 入口以“专题页 + API reference 链接 + sidebar”三点联动而不是只在 `source-generators/index.md` 留一个段落链接
- XML inventory 仍维持“类型声明级基线”口径,不在本轮扩展成成员级 `param/returns/exception` 细审
### 当前验证RP-004
- 文档校验:
- `validate-all.sh docs/zh-CN/core/cqrs.md`:通过
- `validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`:通过
- 轻量 XML inventory
- `GFramework.Cqrs/Internal/``14/14`
- `GFramework.Cqrs.Abstractions/Cqrs/``20/20`
- `GFramework.Cqrs.SourceGenerators/Cqrs/``3/3`
- 构建校验:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -p:RestoreFallbackFolders=`:通过
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning无构建失败
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`:失败;当前 WSL / dotnet 环境仍引用失效的 Windows fallback package folder并在多目标 inner build 阶段触发 `MSB4276` / `MSB4018`
### 下一步
1. 切换到 `Game` family 波次,按 `Core` / `Ecs` / `Cqrs` 已验证模板继续补 XML inventory 与教程链路
2. 把 `GFramework.Cqrs` 的本地构建阻塞留给后续环境治理或构建脚本清理,不在本 topic 内扩张为环境修复任务

View File

@ -1,41 +0,0 @@
# Documentation Governance And Refresh Trace
## 2026-04-22
### 当前恢复点RP-010
- 本轮从 PR #268 的最新 review 数据恢复未发现失败检查CTRF 报告显示 2139 个测试全部通过
- 本轮复核确认当前 PR 的 latest-head open thread 同时来自 `coderabbitai[bot]``greptile-apps[bot]`
- 已本地修复仍然成立的 review
- `docs/zh-CN/game/scene.md` 把“推荐目录与文件约定(项目侧)”降为“最小接入路径”下的子节
- `docs/zh-CN/game/ui.md` 为“最小接入路径”补充导语,并修复同级标题错位
- `.agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh` 改成 opening / closing fence 状态机
- `.agents/skills/_shared/module-config.sh` 补齐缺失模块映射,并让未映射模块返回非零退出码
- `gframework-pr-review` 已从文案和输出模型两侧补齐多 reviewer 支持:当前 JSON 会单独给出 `review_agents`
以及 `open_thread_counts_by_user`,文本输出会显式列出 CodeRabbit / Greptile
- `fetch_current_pr_review.py` 的本地函数 docstring 覆盖率已补到 `44/44`
- 已闭环 RP-001 到 RP-008 的执行细节已归档到
`ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-rp-001-through-rp-008.md`
### 当前决策
- active trace 只保留当前恢复点、关键事实、验证和下一步;完成阶段继续进入 `archive/traces/`
- `scene.md``ui.md` 的集成说明除目录布局外,也要保证标题层级能真实反映采用路径语义
- `gframework-pr-review` 继续以 latest-head unresolved thread 为主信号,同时显式声明支持的 AI reviewer 名单,避免 skill
声明与实际抓取能力再次漂移
### 验证
- `python3 -B .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch docs/sdk-update-documentation --format json --json-output /tmp/current-pr-review.json`
- `python3 -B .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch docs/sdk-update-documentation --section open-threads`
- `python3 -B -c "import ast, pathlib; path=pathlib.Path('.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py'); tree=ast.parse(path.read_text(encoding='utf-8')); funcs=[node for node in ast.walk(tree) if isinstance(node,(ast.FunctionDef, ast.AsyncFunctionDef))]; documented=sum(1 for node in funcs if ast.get_docstring(node)); print(f'functions={len(funcs)} documented={documented} coverage={documented/len(funcs):.2%}')"`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh docs/zh-CN/game/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh docs/zh-CN/game/ui.md`
- `bash -lc 'source .agents/skills/_shared/module-config.sh && get_readme_paths Core.SourceGenerators.Abstractions && if get_readme_paths Not.Real.Module; then exit 1; else echo unmapped-ok; fi'`
- `cd docs && bun run build`
### 下一步
1. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否关闭或减少
2. 继续使用 `gframework-doc-refresh``Godot.SourceGenerators` 做真实模块扫描
3. 优先刷新 `godot-project-generator.md``get-node-generator.md``bind-node-signal-generator.md`

View File

@ -252,6 +252,7 @@ export default defineConfig({
{ text: 'ContextAware 生成器', link: '/zh-CN/source-generators/context-aware-generator' }, { text: 'ContextAware 生成器', link: '/zh-CN/source-generators/context-aware-generator' },
{ text: 'Priority 生成器', link: '/zh-CN/source-generators/priority-generator' }, { text: 'Priority 生成器', link: '/zh-CN/source-generators/priority-generator' },
{ text: 'Context Get 注入', link: '/zh-CN/source-generators/context-get-generator' }, { text: 'Context Get 注入', link: '/zh-CN/source-generators/context-get-generator' },
{ text: 'CQRS Handler Registry', link: '/zh-CN/source-generators/cqrs-handler-registry-generator' },
{ text: 'Godot 项目元数据', link: '/zh-CN/source-generators/godot-project-generator' }, { text: 'Godot 项目元数据', link: '/zh-CN/source-generators/godot-project-generator' },
{ text: 'GetNode 生成器 (Godot)', link: '/zh-CN/source-generators/get-node-generator' }, { text: 'GetNode 生成器 (Godot)', link: '/zh-CN/source-generators/get-node-generator' },
{ text: 'BindNodeSignal 生成器 (Godot)', link: '/zh-CN/source-generators/bind-node-signal-generator' } { text: 'BindNodeSignal 生成器 (Godot)', link: '/zh-CN/source-generators/bind-node-signal-generator' }
@ -264,7 +265,8 @@ export default defineConfig({
text: '抽象接口', text: '抽象接口',
items: [ items: [
{ text: 'Core Abstractions', link: '/zh-CN/abstractions/core-abstractions' }, { text: 'Core Abstractions', link: '/zh-CN/abstractions/core-abstractions' },
{ text: 'Game Abstractions', link: '/zh-CN/abstractions/game-abstractions' } { text: 'Game Abstractions', link: '/zh-CN/abstractions/game-abstractions' },
{ text: 'Ecs.Arch Abstractions', link: '/zh-CN/abstractions/ecs-arch-abstractions' }
] ]
} }
], ],

View File

@ -1,192 +1,104 @@
---
title: Core Abstractions
description: GFramework.Core.Abstractions 的契约边界、包关系与 XML 阅读重点。
---
# Core Abstractions # Core Abstractions
> GFramework.Core.Abstractions 核心抽象接口定义 `GFramework.Core.Abstractions``Core` 运行时的契约包。
## 概述 它负责定义架构、生命周期、事件、状态、资源、日志、配置、并发和持久化相关的接口、枚举和值对象,用来建立跨模块协作边界;
默认实现、基类、容器适配和运行时装配则在 `GFramework.Core` 中。
GFramework.Core.Abstractions 包含了框架的所有核心接口定义,这些接口定义了组件之间的契约,实现了依赖倒置和面向接口编程 如果你要开箱即用地使用框架能力,应依赖 `GFramework.Core`;如果你在做扩展包、测试替身、工具层或多模块拆分,才单独消费本包
## 核心接口 ## 什么时候单独依赖它
### IArchitecture - 你在写插件、模块扩展或测试替身,只想依赖接口而不拉入默认运行时
- 你需要让多个程序集共享架构、状态、资源或日志契约
- 你希望把公共边界放进 `*.Abstractions`,而把具体实现留在应用层或宿主层
应用程序架构接口: ## 包关系
- 契约层:`GFramework.Core.Abstractions`
- 运行时实现:`GFramework.Core`
- 常见相邻契约:`GFramework.Cqrs.Abstractions``GFramework.Game.Abstractions`
## 契约地图
| 契约族 | 作用 |
| --- | --- |
| `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、上下文、模块、服务模块、阶段监听、注册表与初始化 / 销毁生命周期契约 |
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` | 组件角色接口、优先级 / key 值对象、上下文感知边界 |
| `Command/` `Query/` `Cqrs/` | 旧版命令 / 查询执行器接口,以及与新版请求模型衔接的运行时契约 |
| `Events/` `Property/` `State/` `StateManagement/` | 事件总线、解绑对象、可绑定属性、状态机、Store / reducer / middleware 契约 |
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | 协程状态、时间源、暂停栈、键控异步锁与统计对象 |
| `Resource/` `Pool/` `Logging/` `Localization/` | 资源句柄、对象池、日志、logger factory、本地化表与格式化契约 |
| `Configuration/` `Environment/` | 配置管理器、环境对象与运行时环境访问契约 |
| `Data/` `Serializer/` `Storage/` `Versioning/` | 数据装载、序列化、存储与版本化契约 |
| `Enums/` `Properties/` | 架构阶段枚举,以及架构 / logger 相关属性键 |
## 最小接入路径
### 1. 只面向契约编程
```csharp ```csharp
public interface IArchitecture using GFramework.Core.Abstractions.Architectures;
{ using GFramework.Core.Abstractions.Logging;
void Initialize();
void Destroy();
T GetModel<T>() where T : IModel;
T GetSystem<T>() where T : ISystem;
T GetUtility<T>() where T : IUtility;
void RegisterModel(IModel model);
void RegisterSystem(ISystem system);
void RegisterUtility(IUtility utility);
}
```
### IModel public sealed class DiagnosticsFeature
数据模型接口:
```csharp
public interface IModel
{
void Init();
void Dispose();
IArchitecture Architecture { get; }
}
```
### ISystem
业务逻辑系统接口:
```csharp
public interface ISystem
{
void Init();
void Dispose();
IArchitecture Architecture { get; }
}
```
### IController
控制器接口:
```csharp
public interface IController : IBelongToArchitecture
{
void Init();
void Dispose();
}
```
### IUtility
工具类接口:
```csharp
public interface IUtility
{
}
```
## 事件接口
### IEvent
事件基接口:
```csharp
public interface IEvent
{
}
```
### IEventHandler
事件处理器接口:
```csharp
public interface IEventHandler<TEvent> where TEvent : IEvent
{
void Handle(TEvent @event);
}
```
## 命令查询接口
### ICommand
命令接口:
```csharp
public interface ICommand
{
void Execute();
}
```
### IQuery
查询接口:
```csharp
public interface IQuery<TResult>
{
TResult Execute();
}
```
## 依赖注入接口
### IIocContainer
IoC 容器接口:
```csharp
public interface IIocContainer
{
void Register<TInterface, TImplementation>() where TImplementation : TInterface;
void Register<TInterface>(TInterface instance);
TInterface Resolve<TInterface>();
bool IsRegistered<TInterface>();
}
```
## 生命周期接口
### ILifecycle
组件生命周期接口:
```csharp
public interface ILifecycle
{
void OnInit();
void OnDestroy();
}
```
## 使用示例
### 通过接口实现依赖注入
```csharp
public class MyService : IMyService
{ {
private readonly IArchitecture _architecture; private readonly IArchitecture _architecture;
private readonly ILogger _logger;
public MyService(IArchitecture architecture)
public DiagnosticsFeature(IArchitecture architecture, ILogger logger)
{ {
_architecture = architecture; _architecture = architecture;
_logger = logger;
} }
} }
``` ```
### 自定义事件 ### 2. 什么时候切到运行时包
```csharp 下面这些需求都属于 `GFramework.Core` 的职责,而不是本包:
public class PlayerDiedEvent : IEvent
{
public int PlayerId { get; set; }
public Vector2 Position { get; set; }
}
```
--- - 继承 `Architecture` 并完成默认初始化流程
- 使用 `ContextAwareBase``AbstractModel``AbstractSystem` 等默认基类
- 使用默认的 `CommandExecutor``QueryExecutor``BindableProperty<T>``StateMachine`
- 直接启用默认的 `Microsoft.Extensions.DependencyInjection` 容器适配或资源 / 协程 / 日志实现
**相关文档** ## XML 阅读重点
- [Core 概述](../core/index.md) 如果你在做契约审计、采用设计或扩展适配,优先核对这些类型族的 XML 文档:
- [Architecture](../core/architecture)
- [Events](../core/events) - 架构与模块入口:`IArchitecture``IArchitectureContext``IServiceModule`
- [Command](../core/command) - 运行时基础设施:`IIocContainer``ILogger``IResourceManager``IConfigurationManager`
- [Query](../core/query) - 状态与并发能力:`IStateMachine``IStore``IAsyncKeyLockManager``ITimeProvider`
- 迁移与组合边界:`ICommandExecutor``IQueryExecutor``ICqrsRuntime`
## XML 覆盖基线
下面这份 inventory 记录的是 `2026-04-22``GFramework.Core.Abstractions` 做的一轮轻量 XML 盘点结果:只统计公开 /
内部类型声明是否带 XML 注释,用来建立契约层阅读入口;成员级参数、返回值、异常与生命周期说明仍需要后续波次继续细化。
| 契约族 | 基线状态 | 代表类型 | 阅读重点 |
| --- | --- | --- | --- |
| `Architectures/` | `12/12` 个类型声明已带 XML 注释 | `IArchitecture``IArchitectureContext``IArchitectureServices``IServiceModule` | 看架构上下文、服务访问面与模块安装 / 生命周期约束 |
| `Lifecycle/` `Registries/` | `8/8` 个类型声明已带 XML 注释 | `ILifecycle``IAsyncInitializable``IRegistry<T, TR>``KeyValueRegistryBase<TKey, TValue>` | 看初始化 / 销毁阶段和注册表抽象边界 |
| `Command/` `Query/` `Cqrs/` | `10/10` 个类型声明已带 XML 注释 | `ICommandExecutor``IAsyncCommand<TResult>``IQueryExecutor``ICqrsRuntime` | 看旧命令 / 查询接口与新请求模型之间的兼容和迁移边界 |
| `Events/` `Property/` | `10/10` 个类型声明已带 XML 注释 | `IEventBus``IEventFilter<T>``IBindableProperty<T>``IReadonlyBindableProperty<T>` | 看事件传播、过滤、解绑对象和属性订阅语义 |
| `State/` `StateManagement/` | `15/15` 个类型声明已带 XML 注释 | `IStateMachine``IAsyncState``IStore<TState>``IStoreMiddleware<TState>` | 看状态机契约与 Store 的 reducer / middleware / diagnostics 边界 |
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `17/17` 个类型声明已带 XML 注释 | `IYieldInstruction``ICoroutineStatistics``ITimeProvider``IPauseStackManager``IAsyncKeyLockManager` | 看调度模型、时间源、暂停栈和异步锁契约 |
| `Resource/` `Pool/` `Logging/` `Localization/` | `27/27` 个类型声明已带 XML 注释 | `IResourceManager``IObjectPoolSystem``ILogger``IStructuredLogger``ILocalizationManager` | 看资源 / 池化 / 日志 / 本地化这些基础设施的宿主责任 |
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `7/7` 个类型声明已带 XML 注释 | `IConfigurationManager``IEnvironment``ILoadableFrom<T>``ISerializer``IStorage``IVersioned` | 看配置、环境、序列化和持久化边界,以及谁负责具体实现 |
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `19/19` 个类型声明已带 XML 注释 | `IPrioritized``IController``IModel``ISystem``IContextUtility``ArchitecturePhase` | 看基础角色接口、辅助值对象和架构属性键的复用方式 |
## 阅读顺序
1. 先读本页,确认你是否真的只需要契约层
2. 再看 [`../core/index.md`](../core/index.md) 了解默认运行时怎么组织这些契约
3. 回到模块 README
- `GFramework.Core.Abstractions/README.md`
- `GFramework.Core/README.md`
4. 需要统一导航时,再看 [`../api-reference/index.md`](../api-reference/index.md)

View File

@ -0,0 +1,103 @@
---
title: Ecs.Arch Abstractions
description: GFramework.Ecs.Arch.Abstractions 的契约边界、包关系和最小接入路径。
---
# Ecs.Arch Abstractions
`GFramework.Ecs.Arch.Abstractions` 是 Arch ECS 集成层的契约包。
它建立在 `GFramework.Core.Abstractions` 之上,只定义 ECS 模块更新、系统适配和配置对象,不负责默认的 Arch
`World` 装配、扩展方法或系统基类。
如果你需要开箱即用的集成实现,请改为依赖 `GFramework.Ecs.Arch`
## 什么时候单独依赖它
- 你在做共享宿主循环、工具层或 feature 包,只需要 `IArchEcsModule`
- 你想让不同程序集共享 `ArchOptions` 或系统适配契约,但不直接绑定默认 runtime
- 你需要为测试或外部适配层提供替身实现
## 包关系
- 契约层:`GFramework.Ecs.Arch.Abstractions`
- 运行时实现:`GFramework.Ecs.Arch`
- 底层基础契约:`GFramework.Core.Abstractions`
## 契约地图
| 类型 | 作用 |
| --- | --- |
| `IArchEcsModule` | 统一更新 ECS 系统的服务模块契约 |
| `IArchSystemAdapter<T>` | 让 ECS 系统适配到 `ISystem` 生命周期 |
| `ArchOptions` | 承载 `WorldCapacity``EnableStatistics``Priority` 等配置 |
## 类型族级 XML Inventory
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
| --- | --- | --- | --- |
| 模块契约 | `IArchEcsModule` | 已覆盖 | 统一更新入口、宿主循环边界 |
| 系统契约 | `IArchSystemAdapter<T>` | 已覆盖 | 只依赖更新接口而不绑定默认 runtime |
| 配置对象 | `ArchOptions` | 已覆盖 | 共享配置字段与跨程序集采用边界 |
## 最小接入路径
### 1. 共享模块只依赖更新契约
```csharp
using GFramework.Ecs.Arch.Abstractions;
public sealed class GameplayHost
{
private readonly IArchEcsModule _ecsModule;
public GameplayHost(IArchEcsModule ecsModule)
{
_ecsModule = ecsModule;
}
public void Tick(float deltaTime)
{
_ecsModule.Update(deltaTime);
}
}
```
### 2. 共享配置对象
```csharp
using GFramework.Ecs.Arch.Abstractions;
var options = new ArchOptions
{
WorldCapacity = 2048,
EnableStatistics = true,
Priority = 40
};
```
### 3. 什么时候切到运行时包
下面这些需求都属于 `GFramework.Ecs.Arch` 的职责,而不是本包:
- 通过 `UseArch(...)` 把模块挂进架构
- 使用默认的 `ArchSystemAdapter<T>` 基类
- 访问 Arch `World` 与查询 API
- 使用默认的模块装配和生命周期实现
## 阅读顺序
1. 先读本页,确认你是否真的只需要契约层
2. 如果需要默认实现,再看 [`../ecs/arch.md`](../ecs/arch.md)
3. 回到对应模块 README
- `GFramework.Ecs.Arch.Abstractions/README.md`
- `GFramework.Ecs.Arch/README.md`
## 边界提醒
- `GFramework.Core.SourceGenerators.Abstractions`
- `GFramework.Godot.SourceGenerators.Abstractions`
- `GFramework.SourceGenerators.Common`
这些目录当前都不是独立消费模块,而是源码生成器家族的内部支撑组件。它们不属于抽象接口栏目里的独立采用入口,
应分别跟随 `Core.SourceGenerators``Godot.SourceGenerators` 或其他生成器模块的 README 与专题页维护。

View File

@ -1,3 +1,8 @@
---
title: 抽象接口
description: GFramework 各抽象层模块的阅读入口与使用边界。
---
# 抽象接口 # 抽象接口
`GFramework.*.Abstractions` 用来承载跨模块协作所需的契约,而不是运行时实现。 `GFramework.*.Abstractions` 用来承载跨模块协作所需的契约,而不是运行时实现。
@ -12,9 +17,19 @@
- Core 抽象层:[`core-abstractions.md`](./core-abstractions.md) - Core 抽象层:[`core-abstractions.md`](./core-abstractions.md)
- Game 抽象层:[`game-abstractions.md`](./game-abstractions.md) - Game 抽象层:[`game-abstractions.md`](./game-abstractions.md)
- Ecs.Arch 抽象层:[`ecs-arch-abstractions.md`](./ecs-arch-abstractions.md)
## 使用建议 ## 使用建议
- 如果你只是想直接使用框架功能,优先从对应运行时模块的 `README.md` 和栏目页开始。 - 如果你只是想直接使用框架功能,优先从对应运行时模块的 `README.md` 和栏目页开始。
- 只有在明确需要“契约层而非实现层”时,才单独依赖 `*.Abstractions` 包。 - 只有在明确需要“契约层而非实现层”时,才单独依赖 `*.Abstractions` 包。
- 抽象层页面会解释接口分组与职责;实际安装与接入路径,仍应以运行时模块 README 与 `getting-started` 为主。 - 抽象层页面会解释接口分组与职责;实际安装与接入路径,仍应以运行时模块 README 与 `getting-started` 为主。
## 当前边界
- `GFramework.Core.SourceGenerators.Abstractions`
- `GFramework.Godot.SourceGenerators.Abstractions`
- `GFramework.SourceGenerators.Common`
这些目录当前不作为独立抽象接口栏目维护,而是作为源码生成器家族的内部支撑模块,分别跟随所属模块 README 和
`source-generators` 栏目维护。

View File

@ -1,571 +1,81 @@
# API 参考文档 ---
title: API 参考
本文档提供 GFramework 各模块的完整 API 参考。 description: GFramework 的 API 阅读入口,按模块映射 README、专题页、XML 文档和教程链路。
## 核心命名空间
### GFramework.Core.architecture
核心架构命名空间,包含所有基础组件。
#### 主要类型
| 类型 | 说明 |
|--------------------|--------|
| `Architecture` | 应用架构基类 |
| `AbstractModel` | 数据模型基类 |
| `AbstractSystem` | 业务系统基类 |
| `AbstractCommand` | 命令基类 |
| `AbstractQuery<T>` | 查询基类 |
| `IController` | 控制器接口 |
| `IUtility` | 工具类接口 |
### GFramework.Core.events
事件系统命名空间。
#### 主要类型
| 类型 | 说明 |
|-------------------|----------|
| `IEvent` | 事件接口 |
| `IEventSystem` | 事件系统接口 |
| `TypeEventSystem` | 类型安全事件系统 |
### GFramework.Core.property
属性系统命名空间。
#### 主要类型
| 类型 | 说明 |
|-----------------------|--------|
| `BindableProperty<T>` | 可绑定属性 |
| `IUnRegister` | 注销接口 |
| `IUnRegisterList` | 注销列表接口 |
### GFramework.Core.ioc
IoC 容器命名空间。
#### 主要类型
| 类型 | 说明 |
|--------------|------|
| `IContainer` | 容器接口 |
| `Container` | 容器实现 |
### GFramework.Core.pool
对象池命名空间。
#### 主要类型
| 类型 | 说明 |
|------------------|-------|
| `IObjectPool<T>` | 对象池接口 |
| `ObjectPool<T>` | 对象池实现 |
### GFramework.Core.Localization
本地化系统命名空间。
#### 主要类型
| 类型 | 说明 |
|--------------------------|----------|
| `ILocalizationManager` | 本地化管理器接口 |
| `ILocalizationTable` | 本地化表接口 |
| `ILocalizationString` | 本地化字符串接口 |
| `ILocalizationFormatter` | 格式化器接口 |
| `LocalizationConfig` | 本地化配置类 |
| `LocalizationManager` | 本地化管理器实现 |
| `LocalizationTable` | 本地化表实现 |
| `LocalizationString` | 本地化字符串实现 |
## 常用 API
### Architecture
```csharp
public abstract class Architecture : IBelongToArchitecture
{
// 初始化架构
public void Initialize();
// 销毁架构
public void Destroy();
// 注册模型
public void RegisterModel<T>(T model) where T : IModel;
// 获取模型
public T GetModel<T>() where T : IModel;
// 注册系统
public void RegisterSystem<T>(T system) where T : ISystem;
// 获取系统
public T GetSystem<T>() where T : ISystem;
// 注册工具
public void RegisterUtility<T>(T utility) where T : IUtility;
// 获取工具
public T GetUt>() where T : IUtility;
// 发送命令
public void SendCommand<T>(T command) where T : ICommand;
// 发送查询
public TResult SendQuery<TQuery, TResult>(TQuery query)
where TQuery : IQuery<TResult>;
// 发送事件
public void SendEvent<T>(T e) where T : IEvent;
}
```
### AbstractModel
```csharp
public abstract class AbstractModel : IBelongToArchitecture
{
// 初始化模型
protected abstract void OnInit();
// 销毁模型
protected virtual void OnDestroy();
// 获取架构
public IArchitecture GetArchitecture();
// 发送事件
protected void SendEvent<T>(T e) where T : IEvent;
// 获取模型
protected T GetModel<T>() where T : IModel;
// 获取系统
protected T GetSystem<T>() where T : ISystem;
// 获取工具
protected T GetUtility<T>() where T : IUtility;
}
```
### AbstractSystem
```csharp
public abstract class AbstractSystem : IBelongToArchitecture
{
// 初始化系统
protected abstract void OnInit();
// 销毁系统
protected virtual void OnDestroy();
// 获取架构
public IArchitecture GetArchitecture();
// 发送事件
protected void SendEvent<T>(T e) where T : IEvent;
// 注册事件
protected IUnRegister RegisterEvent<T>(Action<T> onEvent)
where T : IEvent;
// 获取模型
protected T GetModel<T>() where T : IModel;
// 获取系统
protected T GetSystem<T>() where T : ISystem;
// 获取工具
protected T GetUtility<T>() where T : IUtility;
}
```
### AbstractCommand
```csharp
public abstract class AbstractCommand : IBelongToArchitecture
{
// 执行命令
public void Execute();
// 命令实现
protected abstract void OnDo();
// 获取架构
public IArchitecture GetArchitecture();
// 发送事件
protected void SendEvent<T>(T e) where T : IEvent;
// 获取模型
protected T GetModel<T>() where T : IModel;
// 获取系统
protected T GetSystem<T>() where T : ISystem;
// 获取工具
protected T GetUtility<T>() where T : IUtility;
}
```
### AbstractQuery`<T>`
```csharp
public abstract class AbstractQuery<T> : IBelongToArchitecture
{
// 执行查询
public T Do();
// 查询实现
protected abstract T OnDo();
// 获取架构
public IArchitecture GetArchitecture();
// 获取模型
protected T GetModel<T>() where T : IModel;
// 获取系统
protected T GetSystem<T>() where T : ISystem;
// 获取工具
protected T GetUtility<T>() where T : IUtility;
}
```
### BindableProperty`<T>`
```csharp
public class BindableProperty<T>
{
// 构造函数
public BindableProperty(T initialValue = default);
// 获取或设置值
public T Value { get; set; }
// 注册监听器
public IUnRegister Register(Action<T> onValueChanged);
// 注册监听器(包含初始值)
public IUnRegister RegisterWithInitValue(Action<T> onValueChanged);
// 获取当前值
public T GetValue();
// 设置值
public void SetValue(T newValue);
}
```
### ILocalizationManager
```csharp
public interface ILocalizationManager : ISystem
{
// 获取当前语言代码
string CurrentLanguage { get; }
// 获取当前文化信息
CultureInfo CurrentCulture { get; }
// 获取可用语言列表
IReadOnlyList<string> AvailableLanguages { get; }
// 设置当前语言
void SetLanguage(string languageCode);
// 获取本地化表
ILocalizationTable GetTable(string tableName);
// 获取本地化文本
string GetText(string table, string key);
// 获取本地化字符串(支持变量)
ILocalizationString GetString(string table, string key);
// 尝试获取本地化文本
bool TryGetText(string table, string key, out string text);
// 注册格式化器
void RegisterFormatter(string name, ILocalizationFormatter formatter);
// 订阅语言变化事件
void SubscribeToLanguageChange(Action<string> callback);
// 取消订阅语言变化事件
void UnsubscribeFromLanguageChange(Action<string> callback);
}
```
### ILocalizationString
```csharp
public interface ILocalizationString
{
// 获取表名
string Table { get; }
// 获取键名
string Key { get; }
// 添加变量
ILocalizationString WithVariable(string name, object value);
// 批量添加变量
ILocalizationString WithVariables(params (string name, object value)[] variables);
// 格式化并返回文本
string Format();
// 获取原始文本
string GetRaw();
// 检查键是否存在
bool Exists();
}
```
### LocalizationConfig
```csharp
public class LocalizationConfig
{
// 默认语言代码
public string DefaultLanguage { get; set; } = "eng";
// 回退语言代码
public string FallbackLanguage { get; set; } = "eng";
// 本地化文件路径
public string LocalizationPath { get; set; } = "res://localization";
// 用户覆盖路径
public string OverridePath { get; set; } = "user://localization_override";
// 是否启用热重载
public bool EnableHotReload { get; set; } = true;
// 是否在加载时验证
public bool ValidateOnLoad { get; set; } = true;
}
```
## 扩展方法
### 架构扩展
```csharp
// 发送命令
public static void SendCommand<T>(this IBelongToArchitecture self, T command)
where T : ICommand;
// 发送查询
public static TResult SendQuery<TQuery, TResult>(
this IBelongToArchitecture self, TQuery query)
where TQuery : IQuery<TResult>;
// 发送事件
public static void SendEvent<T>(this IBelongToArchitecture self, T e)
where T : IEvent;
// 获取模型
public static T GetModel<T>(this IBelongToArchitecture self)
where T : IModel;
// 获取系统
public static T GetSystem<T>(this IBelongToArchitecture self)
where T : ISystem;
// 获取工具
public static T GetUtility<T>(this IBelongToArchitecture self)
where T : IUtility;
// 注册事件
public static IUnRegister RegisterEvent<T>(
this IBelongToArchitecture self, Action<T> onEvent)
where T : IEvent;
```
### 属性扩展
```csharp
// 添加到注销列表
public static IUnRegister AddToUnregisterList(
this IUnRegister self, IUnRegisterList list);
// 注销所有
public static void UnRegisterAll(this IUnRegisterList self);
```
## 游戏模块 API
### GFramework.Game
游戏业务扩展模块。
#### 主要类型
| 类型 | 说明 |
|---------------|--------|
| `GameSetting` | 游戏设置 |
| `GameState` | 游戏状态 |
| `IGameModule` | 游戏模块接口 |
## Godot 集成 API
### GFramework.Godot
Godot 引擎集成模块。
#### 主要类型
| 类型 | 说明 |
|------------------|------------|
| `GodotNode` | Godot 节点扩展 |
| `GodotCoroutine` | Godot 协程 |
| `GodotSignal` | Godot 信号 |
## 源码生成器
### Source Generators 家族
自动代码生成工具按模块拆分为 `GFramework.Core.SourceGenerators``GFramework.Game.SourceGenerators`
`GFramework.Godot.SourceGenerators``GFramework.Cqrs.SourceGenerators`。面向业务代码声明的 Attribute
主要来自 `GFramework.Core.SourceGenerators.Abstractions.*` 与对应模块的 runtime/generator 包。
#### 支持的生成器
| 生成器 | 说明 |
|--------------------------------------------|-------------|
| `LoggingGenerator` | 日志生成器 |
| `EnumGenerator` | 枚举扩展生成器 |
| `RuleGenerator` | 规则生成器 |
| `AutoRegisterModuleGenerator` | 架构模块注册生成器 |
| `AutoUiPageGenerator` | UI 页面行为生成器 |
| `AutoSceneGenerator` | 场景行为生成器 |
| `AutoRegisterExportedCollectionsGenerator` | 导出集合批量注册生成器 |
#### 常用 Attribute
| Attribute | 说明 | 文档 |
|--------------------------------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| `AutoRegisterModuleAttribute` | 为模块类生成 `Install(IArchitecture)` | [AutoRegisterModule 生成器](../source-generators/auto-register-module-generator.md) |
| `RegisterModelAttribute` | 声明模块内自动注册的 `IModel` 类型 | [AutoRegisterModule 生成器](../source-generators/auto-register-module-generator.md) |
| `RegisterSystemAttribute` | 声明模块内自动注册的 `ISystem` 类型 | [AutoRegisterModule 生成器](../source-generators/auto-register-module-generator.md) |
| `RegisterUtilityAttribute` | 声明模块内自动注册的 `IUtility` 类型 | [AutoRegisterModule 生成器](../source-generators/auto-register-module-generator.md) |
| `AutoUiPageAttribute` | 为 `CanvasItem` 页面节点生成 `GetPage()` | [AutoUiPage 生成器](../source-generators/auto-ui-page-generator.md) |
| `AutoSceneAttribute` | 为场景根节点生成 `GetScene()` | [AutoScene 生成器](../source-generators/auto-scene-generator.md) |
| `AutoLoadAttribute` | 显式声明 `project.godot` AutoLoad 与 C# 节点类型映射 | [Godot 项目元数据生成器](../source-generators/godot-project-generator.md) |
| `AutoRegisterExportedCollectionsAttribute` | 为宿主类开启导出集合批量注册生成 | [AutoRegisterExportedCollections 生成器](../source-generators/auto-register-exported-collections-generator.md) |
| `RegisterExportedCollectionAttribute` | 指定集合与注册器成员的映射关系 | [AutoRegisterExportedCollections 生成器](../source-generators/auto-register-exported-collections-generator.md) |
## 常见用法示例
### 创建架构
```csharp
public class MyArchitecture : Architecture
{
protected override void Init()
{
RegisterModel(new PlayerModel());
RegisterSystem(new PlayerSystem());
RegisterUtility(new StorageUtility());
}
}
// 使用
var arch = new MyArchitecture();
arch.Initialize();
```
### 发送命令
```csharp
public class AttackCommand : AbstractCommand
{
public int Damage { get; set; }
protected override void OnDo()
{
var player = this.GetModel<PlayerModel>();
this.SendEvent(new AttackEvent { Damage = Damage });
}
}
// 使用
arch.SendCommand(new AttackCommand { Damage = 10 });
```
### 发送查询
```csharp
public class GetPlayerHealthQuery : AbstractQuery<int>
{
protected override int OnDo()
{
return this.GetModel<PlayerModel>().Health.Value;
}
}
// 使用
var health = arch.SendQuery(new GetPlayerHealthQuery());
```
### 监听事件
```csharp
public class PlayerSystem : AbstractSystem
{
protected override void OnInit()
{
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
}
private void OnPlayerDied(PlayerDiedEvent e)
{
Console.WriteLine("Player died!");
}
}
```
### 使用本地化
```csharp
// 初始化本地化管理器
var config = new LocalizationConfig
{
DefaultLanguage = "eng",
LocalizationPath = "res://localization"
};
var locManager = new LocalizationManager(config);
locManager.Initialize();
// 获取简单文本
string title = locManager.GetText("common", "game.title");
// 使用变量
var message = locManager.GetString("common", "ui.message.welcome")
.WithVariable("playerName", "Alice")
.Format();
// 切换语言
locManager.SetLanguage("zhs");
// 监听语言变化
locManager.SubscribeToLanguageChange(language =>
{
Console.WriteLine($"Language changed to: {language}");
});
```
--- ---
更多详情请查看各模块的详细文档。 # API 参考
这里不再维护一份脱离源码演化的“伪 API 列表”。
当前 `GFramework` 的 API 参考链路以四类证据协同为准:
1. 模块 README说明包关系、最小接入路径和目录边界
2. `docs/zh-CN` 专题页:说明采用顺序、生命周期和使用建议
3. 代码中的 XML 文档:说明公开 / 内部类型和关键成员的契约
4. 教程页:说明这些 API 在真实接入路径中的组合方式
## 阅读顺序
### 想确认“该装哪个包、先看哪类 API”
先读模块 README再读对应 landing page
- 入门入口:[`../getting-started/index.md`](../getting-started/index.md)
- 根模块地图:仓库根 `README.md`
### 想确认“这个功能属于哪个模块”
按下面的模块映射进入对应入口:
| 模块族 | 模块 README | 站内入口 | XML 文档关注点 |
| --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | `GFramework.Core/README.md``GFramework.Core.Abstractions/README.md` | [`../core/index.md`](../core/index.md)、[`../abstractions/core-abstractions.md`](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | `GFramework.Cqrs/README.md``GFramework.Cqrs.Abstractions/README.md``GFramework.Cqrs.SourceGenerators/README.md` | [`../core/cqrs.md`](../core/cqrs.md)、[`../source-generators/cqrs-handler-registry-generator.md`](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `GFramework.Game/README.md``GFramework.Game.Abstractions/README.md``GFramework.Game.SourceGenerators/README.md` | [`../game/index.md`](../game/index.md)、[`../abstractions/game-abstractions.md`](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | `GFramework.Godot/README.md``GFramework.Godot.SourceGenerators/README.md` | [`../godot/index.md`](../godot/index.md)、[`../source-generators/index.md`](../source-generators/index.md) | 节点扩展、场景 / UI 适配、资源 / 存储 / 日志接入 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | `GFramework.Ecs.Arch/README.md``GFramework.Ecs.Arch.Abstractions/README.md` | [`../ecs/index.md`](../ecs/index.md)、[`../ecs/arch.md`](../ecs/arch.md)、[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
## 先看 XML还是先看教程
### 先看 XML 文档的情况
- 你在确认公开类型的约束、线程 / 生命周期语义、参数和返回值契约
- 你需要区分“抽象层保证了什么”和“默认实现额外提供了什么”
- 你在做多模块拆分、测试替身或扩展适配层
优先关注这些类型族:
- 架构 / 模块 / 服务入口
- 生命周期、注册、路由、工厂、provider 契约
- Source Generator 的 attribute、diagnostic 和 generated contract
### 先看教程和专题页的情况
- 你要的是最小接入路径,而不是逐个类型审计
- 你想确认模块组合方式、目录约定和推荐接线顺序
- 你在做从旧入口迁移到新入口的采用决策
优先入口:
- 教程概览:[`../tutorials/index.md`](../tutorials/index.md)
- 最佳实践:[`../best-practices/index.md`](../best-practices/index.md)
- 故障排查:[`../troubleshooting.md`](../troubleshooting.md)
## 当前边界
- `GFramework.Core.SourceGenerators.Abstractions`
- `GFramework.Godot.SourceGenerators.Abstractions`
- `GFramework.SourceGenerators.Common`
这些目录当前不是独立消费模块,因此不单独维护站内 API 参考入口。它们的公开说明跟随所属模块 README 和
`source-generators` 栏目维护。
## 使用方式
把本页当成“API 阅读导航”而不是“签名快照”:
- 先选模块
- 再进 README 和专题页确认采用路径
- 最后回到代码里的 XML 文档核对具体契约
当 README、专题页和 XML 文档出现冲突时,以源码和测试所反映的当前实现为准。

View File

@ -123,10 +123,24 @@ protected override void OnInitialize()
- `PhaseChanged` - `PhaseChanged`
- `RegisterLifecycleHook(...)` - `RegisterLifecycleHook(...)`
其中 `PhaseChanged` 现在遵循标准 `EventHandler<ArchitecturePhaseChangedEventArgs>` 约定,
阶段值通过 `args.Phase` 读取。
如果你正在从旧版本迁移,需要把单参数写法 `phase => ...` 改成 `(_, args) => ...`
并通过 `ArchitecturePhaseChangedEventArgs.Phase` 读取阶段值。
如果你需要在 `Ready``Destroying` 等阶段执行横切逻辑,比起把这类逻辑塞进某个具体 `System`,更适合单独实现 如果你需要在 `Ready``Destroying` 等阶段执行横切逻辑,比起把这类逻辑塞进某个具体 `System`,更适合单独实现
`IArchitectureLifecycleHook` `IArchitectureLifecycleHook`
```csharp ```csharp
architecture.PhaseChanged += (_, args) =>
{
if (args.Phase == ArchitecturePhase.Ready)
{
Console.WriteLine("Architecture ready from event.");
}
};
public sealed class MetricsHook : IArchitectureLifecycleHook public sealed class MetricsHook : IArchitectureLifecycleHook
{ {
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)

View File

@ -1,15 +1,29 @@
--- ---
title: CQRS title: CQRS
description: 当前推荐的新请求模型,统一覆盖 command、query、notification、stream request 和 pipeline behaviors description: Cqrs 模块族的运行时、契约层、生成器入口,以及 XML / API 阅读链路
--- ---
# CQRS # CQRS
`GFramework.Cqrs` 是当前推荐的新请求模型 runtime。 `Cqrs` 栏目对应三个直接相关的消费模块:
如果你在写新功能,优先使用这套模型,而不是继续扩展 `GFramework.Core.Command` / `Query` 的兼容层。 - `GFramework.Cqrs`
- `GFramework.Cqrs.Abstractions`
- `GFramework.Cqrs.SourceGenerators`
## 安装方式 如果你在写新功能,优先使用这套请求模型,而不是继续扩展 `GFramework.Core.Command` / `Query` 的兼容层。
## 模块族边界
| 模块 | 角色 | 何时安装 |
| --- | --- | --- |
| `GeWuYou.GFramework.Cqrs.Abstractions` | 纯契约层,定义 request、notification、stream、handler、pipeline、runtime seam | 需要把消息契约放到更稳定的共享层,或只依赖接口做解耦 |
| `GeWuYou.GFramework.Cqrs` | 默认 runtime提供 dispatcher、handler 基类、上下文扩展和程序集注册流程 | 大多数直接消费 CQRS 的业务模块 |
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`,缩小运行时反射扫描范围 | handler 较多,想把注册映射前移到编译期 |
## 最小接入路径
最小安装组合是:
```bash ```bash
dotnet add package GeWuYou.GFramework.Cqrs dotnet add package GeWuYou.GFramework.Cqrs
@ -22,15 +36,6 @@ dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
``` ```
## 先理解分层
- `GFramework.Cqrs.Abstractions`
- 纯契约层,定义请求、处理器、行为等接口
- `GFramework.Cqrs`
- 默认 runtime、dispatcher、处理器基类和上下文扩展
- `GFramework.Cqrs.SourceGenerators`
- 可选生成器,为消费端程序集生成 `ICqrsHandlerRegistry`
## 最小示例 ## 最小示例
消息基类和处理器基类在不同命名空间: 消息基类和处理器基类在不同命名空间:
@ -38,12 +43,10 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
- 消息基类:`GFramework.Cqrs.Command` / `Query` / `Notification` - 消息基类:`GFramework.Cqrs.Command` / `Query` / `Notification`
- 处理器基类:`GFramework.Cqrs.Cqrs.Command` / `Query` / `Notification` - 处理器基类:`GFramework.Cqrs.Cqrs.Command` / `Query` / `Notification`
示例:
```csharp ```csharp
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using GFramework.Cqrs.Command; using GFramework.Cqrs.Command;
using GFramework.Cqrs.Cqrs.Command; using GFramework.Cqrs.Cqrs.Command;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
public sealed record CreatePlayerInput(string Name) : ICommandInput; public sealed record CreatePlayerInput(string Name) : ICommandInput;
@ -66,9 +69,7 @@ public sealed class CreatePlayerCommandHandler
} }
``` ```
## 发送请求 如果你在 `IContextAware` 对象内部发送请求:
如果你在 `IContextAware` 对象内部:
```csharp ```csharp
using GFramework.Cqrs.Extensions; using GFramework.Cqrs.Extensions;
@ -77,7 +78,7 @@ var playerId = await this.SendAsync(
new CreatePlayerCommand(new CreatePlayerInput("Alice"))); new CreatePlayerCommand(new CreatePlayerInput("Alice")));
``` ```
如果你在组合根或测试里: 如果你在组合根或测试里发送请求
```csharp ```csharp
var playerId = await architecture.Context.SendRequestAsync( var playerId = await architecture.Context.SendRequestAsync(
@ -92,7 +93,7 @@ var playerId = await architecture.Context.SendRequestAsync(
- `PublishAsync(...)` - `PublishAsync(...)`
- `CreateStream(...)` - `CreateStream(...)`
## 查询、通知和流 ## 统一请求模型
这套 runtime 不只处理 command也统一处理 这套 runtime 不只处理 command也统一处理
@ -103,9 +104,9 @@ var playerId = await architecture.Context.SendRequestAsync(
- Stream Request - Stream Request
- 返回 `IAsyncEnumerable<T>` - 返回 `IAsyncEnumerable<T>`
也就是说,新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。 新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。
## 注册处理器 ## 处理器注册与生成器协作
在标准 `Architecture` 启动路径中CQRS runtime 会自动接入基础设施。你通常只需要在 `OnInitialize()` 里追加行为或额外程序集: 在标准 `Architecture` 启动路径中CQRS runtime 会自动接入基础设施。你通常只需要在 `OnInitialize()` 里追加行为或额外程序集:
@ -123,11 +124,15 @@ protected override void OnInitialize()
} }
``` ```
默认逻辑会 默认注册流程当前遵循这些语义
1. 优先使用消费端程序集上的生成注册器 1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute`
2. 生成注册器不可用时回退到反射扫描 2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry`
3. 对同一程序集去重,避免重复注册 3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径
4. 如果程序集带有 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
5. 同一程序集按稳定键去重,避免重复注册
`Cqrs.SourceGenerators` 的专题入口见 [../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)。
## Pipeline Behavior ## Pipeline Behavior
@ -145,7 +150,7 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
- 审计 - 审计
- 重试或统一异常封装 - 重试或统一异常封装
旧的 `Mediator` 兼容别名入口已经移除;当前公开入口只有 `RegisterCqrsPipelineBehavior<TBehavior>()` 当前公开入口只有 `RegisterCqrsPipelineBehavior<TBehavior>()`
## 和旧 Command / Query 的关系 ## 和旧 Command / Query 的关系
@ -157,15 +162,28 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
- 新路径 - 新路径
- `GFramework.Cqrs` - `GFramework.Cqrs`
`IArchitectureContext` 仍然兼容旧入口,但新代码应优先使用 CQRS runtime。 `IArchitectureContext` 仍然兼容旧入口,但新代码应优先使用 CQRS runtime。
一个简单判断规则: 一个简单判断规则:
- 在维护历史代码:允许继续使用旧 Command / Query - 在维护历史代码:允许继续使用旧 Command / Query
- 在写新功能或新模块:优先使用 CQRS - 在写新功能或新模块:优先使用 CQRS
## XML 覆盖基线
下面这份 inventory 记录的是 `2026-04-22``Cqrs` 家族做的一轮轻量 XML 盘点结果:只统计当前运行时、契约层和生成器入口中的类型声明级 XML 覆盖,用来校对 README、landing page 与 API 入口,不把它表述成成员级契约全审计。
| 类型族 | 基线状态 | 代表类型 | 阅读重点 |
| --- | --- | --- | --- |
| `GFramework.Cqrs.Abstractions/Cqrs/` | `20/20` 个类型声明已带 XML 注释 | `ICqrsRuntime``ICqrsHandlerRegistrar``IPipelineBehavior<,>``IRequestHandler<,>``Unit` | 先看请求、处理器和 runtime seam 的最小契约 |
| `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `7/7` 个类型声明已带 XML 注释 | `CommandBase<TInput, TResponse>``QueryBase<TInput, TResponse>``NotificationBase<TInput>``ContextAwareCqrsExtensions` | 看业务侧常用基类和上下文发送入口 |
| `GFramework.Cqrs/Cqrs/` | `12/12` 个类型声明已带 XML 注释 | `AbstractCommandHandler<,>``AbstractQueryHandler<,>``AbstractNotificationHandler<>``LoggingBehavior<,>` | 看默认处理器基类、上下文注入与行为管道 |
| `GFramework.Cqrs` 根入口与 `Internal/` | `19/19` 个类型声明已带 XML 注释 | `CqrsRuntimeFactory``ICqrsHandlerRegistry``CqrsHandlerRegistryAttribute``CqrsReflectionFallbackAttribute``DefaultCqrsRegistrationService` | 看 runtime 创建入口、registry 协议、fallback 语义和程序集去重规则 |
| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `3/3` 个类型声明已带 XML 注释 | `CqrsHandlerRegistryGenerator``RuntimeTypeReferenceSpec``OrderedRegistrationKind` | 看生成注册器、精确 type lookup 和 fallback 诊断边界 |
## 继续阅读 ## 继续阅读
- 架构入口:[architecture](./architecture.md) - 架构入口:[architecture](./architecture.md)
- 上下文入口:[context](./context.md) - 上下文入口:[context](./context.md)
- 生成器专题:[../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)
- 模块 README`GFramework.Cqrs/README.md` - 模块 README`GFramework.Cqrs/README.md`

View File

@ -1,3 +1,8 @@
---
title: Core
description: GFramework.Core 与 GFramework.Core.Abstractions 的运行时入口、采用顺序和 XML 阅读导航。
---
# Core # Core
`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core``GFramework.Core.Abstractions`,以及与之直接相邻的旧版 `Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core``GFramework.Core.Abstractions`,以及与之直接相邻的旧版
@ -29,28 +34,70 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
`Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织: `Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织:
- 架构与上下文 - 架构与生命周期
- [architecture](./architecture.md) - [architecture](./architecture.md)
- [context](./context.md) - [context](./context.md)
- [lifecycle](./lifecycle.md) - [lifecycle](./lifecycle.md)
- [async-initialization](./async-initialization.md)
- 组件角色与运行时接入
- [model](./model.md)
- [system](./system.md)
- [utility](./utility.md)
- [environment](./environment.md)
- [extensions](./extensions.md)
- 旧版命令 / 查询执行器与迁移入口 - 旧版命令 / 查询执行器与迁移入口
- [command](./command.md) - [command](./command.md)
- [query](./query.md) - [query](./query.md)
- [cqrs](./cqrs.md) - [cqrs](./cqrs.md)
- 核心横切能力 - 状态、事件与规则
- [events](./events.md) - [events](./events.md)
- [property](./property.md) - [property](./property.md)
- [rule](./rule.md)
- [logging](./logging.md) - [logging](./logging.md)
- [resource](./resource.md)
- [coroutine](./coroutine.md)
- [ioc](./ioc.md)
- 状态与扩展能力
- [state-machine](./state-machine.md) - [state-machine](./state-machine.md)
- [state-management](./state-management.md) - [state-management](./state-management.md)
- 运行时支撑能力
- [resource](./resource.md)
- [pool](./pool.md)
- [coroutine](./coroutine.md)
- [pause](./pause.md) - [pause](./pause.md)
- [localization](./localization.md) - [localization](./localization.md)
- [configuration](./configuration.md)
- [ioc](./ioc.md)
- 通用辅助能力
- [functional](./functional.md) - [functional](./functional.md)
- [extensions](./extensions.md)
## XML 与 API 阅读入口
如果你已经知道模块归属,但想确认公开类型的契约边界,建议按下面顺序阅读:
1. 先看模块 README `GFramework.Core/README.md`,确认包关系和目录边界
2. 再看本栏目对应专题页,确认采用顺序、生命周期与推荐接线方式
3. 最后回到源码中的 XML 文档,重点核对这些类型族:
- `Architecture` / `IArchitectureContext`
- `CommandExecutor` / `QueryExecutor`
- `ILogger` / `ILoggerFactory`
- `IResourceManager` / `IConfigurationManager`
- `IAsyncKeyLockManager` / `ITimeProvider`
统一入口见 [`../api-reference/index.md`](../api-reference/index.md)。
## XML 覆盖基线
下面这份 inventory 记录的是 `2026-04-22``GFramework.Core` 做的一轮轻量 XML 盘点结果:只统计顶层目录中的公开 /
内部类型声明是否带 XML 注释,用来确认阅读入口和治理优先级;成员级 ``<param>``、``<returns>``、异常语义与线程说明仍需要继续细审。
| 类型族 | 基线状态 | 代表类型 | 阅读重点 |
| --- | --- | --- | --- |
| `Architectures/` | `16/16` 个类型声明已带 XML 注释 | `Architecture``ArchitectureContext``ArchitectureLifecycle``ArchitecturePhaseCoordinator` | 看架构启动、模块安装、阶段切换和上下文暴露边界 |
| `Services/` | `6/6` 个类型声明已带 XML 注释 | `ServiceModuleManager``CommandExecutorModule``CqrsRuntimeModule` | 看服务模块的注册顺序、销毁语义和默认接线 |
| `Command/` `Query/` | `15/15` 个类型声明已带 XML 注释 | `CommandExecutor``AsyncQueryExecutor``AbstractCommand<TInput>``AbstractQuery<TResult>` | 看旧入口兼容面与向 `CQRS` 迁移时还保留了哪些执行契约 |
| `Events/` `Property/` | `19/19` 个类型声明已带 XML 注释 | `EventBus``EnhancedEventBus``BindableProperty<T>``OrEvent<T>` | 看事件传播、解绑约束和可绑定属性的订阅语义 |
| `State/` `StateManagement/` | `10/10` 个类型声明已带 XML 注释 | `StateMachine``StateMachineSystem``Store<TState>``StoreBuilder<TState>` | 看状态切换、selector / middleware / dispatch 的单向流边界 |
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `43/43` 个类型声明已带 XML 注释 | `CoroutineScheduler``CoroutineHandle``WaitForSecondsRealtime``PauseStackManager``AsyncKeyLockManager` | 看调度阶段、等待指令、时间源和暂停 / 锁的线程语义 |
| `Resource/` `Pool/` | `8/8` 个类型声明已带 XML 注释 | `ResourceManager``AutoReleaseStrategy``ManualReleaseStrategy``AbstractObjectPoolSystem<TKey, TObject>` | 看资源句柄释放策略与对象池复用约束 |
| `Logging/` `Localization/` `Configuration/` `Environment/` `Ioc/` | `31/31` 个类型声明已带 XML 注释 | `ConsoleLogger``CompositeLogger``LocalizationManager``ConfigurationManager``MicrosoftDiContainer` | 看日志组装、格式化 / filter、配置监听、环境对象与容器适配 |
| `Model/` `Systems/` `Utility/` `Rule/` `Extensions/` `Functional/` | `34/34` 个类型声明已带 XML 注释 | `AbstractModel``AbstractSystem``NumericDisplayFormatter``ContextAwareBase``Result<T>` | 看默认基类、上下文感知 helper、数值格式化和通用扩展的使用边界 |
## 最小接入路径 ## 最小接入路径
@ -104,4 +151,5 @@ public sealed class CounterArchitecture : Architecture
- `GFramework.Core/README.md` - `GFramework.Core/README.md`
- `GFramework.Core.Abstractions/README.md` - `GFramework.Core.Abstractions/README.md`
- `docs/zh-CN/api-reference/index.md`
- 仓库根 `README.md` - 仓库根 `README.md`

View File

@ -138,10 +138,13 @@ architecture.RegisterLifecycleHook(new MetricsHook());
如果你只需要观察阶段变化,也可以直接订阅: 如果你只需要观察阶段变化,也可以直接订阅:
如果你从旧版本的 `PhaseChanged` 迁移过来,需要把旧写法 `phase => ...` 改成 `(_, args) => ...`
并通过 `ArchitecturePhaseChangedEventArgs.Phase` 读取阶段值。
```csharp ```csharp
architecture.PhaseChanged += phase => architecture.PhaseChanged += (_, args) =>
{ {
Console.WriteLine($"Phase changed: {phase}"); Console.WriteLine($"Phase changed: {args.Phase}");
}; };
``` ```

View File

@ -1,46 +1,43 @@
--- ---
title: Arch ECS 集成 title: Arch ECS 集成
description: GFramework 的 Arch ECS 集成包使用指南,提供高性能的实体组件系统支持 description: GFramework.Ecs.Arch 的默认运行时装配路径、系统桥接方式与 XML 阅读入口
--- ---
# Arch ECS 集成 # Arch ECS 集成
## 概述 `GFramework.Ecs.Arch` 是当前仓库里负责 Arch ECS 默认接入路径的运行时包。它把 Arch `World`、GFramework 的
`IServiceModule` 生命周期,以及 `AbstractSystem` / `ISystem` 体系桥接到同一条初始化与更新链路中。
`GFramework.Ecs.Arch` 是 GFramework 的 Arch ECS 集成包,提供开箱即用的 ECSEntity Component ## 什么时候依赖它
System支持。基于 [Arch.Core](https://github.com/genaray/Arch) 实现,具有极致的性能和简洁的 API。
**主要特性** 当你需要下面任一能力时,应直接依赖 `GeWuYou.GFramework.Ecs.Arch`
- 🎯 **显式集成** - 符合 .NET 生态习惯的显式注册方式 - 在架构实例上调用 `UseArch(...)`
- 🔌 **零依赖** - 不使用时Core 包无 Arch 依赖 - 让 `World` 在服务模块注册阶段自动创建并注入容器
- 🎯 **类型安全** - 完整的类型系统和编译时检查 - 让 ECS 系统继承 `ArchSystemAdapter<T>`
- ⚡ **高性能** - 基于 Arch ECS 的高性能实现 - 使用仓库自带的 `Position``Velocity``MovementSystem` 最小示例
- 🔧 **易扩展** - 简单的系统适配器模式
- 📊 **优先级支持** - 系统按优先级顺序执行
**性能特点** 如果你只想保留共享边界,而不依赖默认实现,请改看
[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)。
- 10,000 个实体更新 < 100ms ## 最小接入路径
- 1,000 个实体创建 < 50ms
- 基于 Archetype 的高效内存布局
- 零 GC 分配的组件访问
## 安装 ### 1. 安装包
```bash ```bash
dotnet add package GeWuYou.GFramework.Ecs.Arch dotnet add package GeWuYou.GFramework.Ecs.Arch
``` ```
## 快速开始 ### 2. 在 `Initialize()` 之前调用 `UseArch(...)`
### 1. 注册 ECS 模块 当前实现通过 `ArchitectureModuleRegistry.Register(...)` 提前登记 `ArchEcsModule`。这意味着调用时机应位于
`Initialize()` 之前,而不是放进 `OnInitialize()` 里。
```csharp ```csharp
using GFramework.Core.Architecture; using GFramework.Core.Architectures;
using GFramework.Ecs.Arch.Extensions; using GFramework.Ecs.Arch.Extensions;
public class GameArchitecture : Architecture public sealed class GameArchitecture : Architecture
{ {
public GameArchitecture() : base(new ArchitectureConfiguration()) public GameArchitecture() : base(new ArchitectureConfiguration())
{ {
@ -48,698 +45,100 @@ public class GameArchitecture : Architecture
protected override void OnInitialize() protected override void OnInitialize()
{ {
// 显式注册 Arch ECS 模块 RegisterSystem<MovementSystem>();
this.UseArch();
} }
} }
// 初始化架构 var architecture = new GameArchitecture()
var architecture = new GameArchitecture(); .UseArch(options =>
{
options.WorldCapacity = 2048;
options.Priority = 50;
});
architecture.Initialize(); architecture.Initialize();
``` ```
### 2. 带配置的注册 ### 3. 用 `ArchSystemAdapter<float>` 编写系统
```csharp `ArchSystemAdapter<T>``OnInit()` 中从当前上下文解析 `World`,再把 Arch 的 `Initialize / BeforeUpdate /
public class GameArchitecture : Architecture AfterUpdate / Dispose` 钩子桥接到可重写方法。
{
protected override void OnInitialize()
{
// 带配置的注册
this.UseArch(options =>
{
options.WorldCapacity = 2000; // World 初始容量
options.EnableStatistics = true; // 启用统计信息
options.Priority = 50; // 模块优先级
});
}
}
```
### 3. 定义组件
组件是纯数据结构,使用 `struct` 定义:
```csharp
using System.Runtime.InteropServices;
namespace MyGame.Components;
[StructLayout(LayoutKind.Sequential)]
public struct Position(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
[StructLayout(LayoutKind.Sequential)]
public struct Velocity(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
[StructLayout(LayoutKind.Sequential)]
public struct Health(float current, float max)
{
public float Current { get; set; } = current;
public float Max { get; set; } = max;
}
```
### 4. 创建系统
系统继承自 `ArchSystemAdapter&lt;T&gt;`
```csharp ```csharp
using Arch.Core; using Arch.Core;
using GFramework.Ecs.Arch; using GFramework.Ecs.Arch;
using MyGame.Components; using GFramework.Ecs.Arch.Components;
namespace MyGame.Systems;
/// <summary>
/// 移动系统 - 更新实体位置
/// </summary>
public sealed class MovementSystem : ArchSystemAdapter<float> public sealed class MovementSystem : ArchSystemAdapter<float>
{ {
private QueryDescription _query; private QueryDescription _query;
protected override void OnArchInitialize() protected override void OnArchInitialize()
{ {
// 创建查询:查找所有同时拥有 Position 和 Velocity 组件的实体
_query = new QueryDescription() _query = new QueryDescription()
.WithAll<Position, Velocity>(); .WithAll<Position, Velocity>();
} }
protected override void OnUpdate(in float deltaTime) protected override void OnUpdate(in float deltaTime)
{ {
// 查询并更新所有符合条件的实体 var frameDelta = deltaTime;
World.Query(in _query, (ref Position pos, ref Velocity vel) =>
World.Query(in _query, (ref Position position, ref Velocity velocity) =>
{ {
pos.X += vel.X * deltaTime; position.X += velocity.X * frameDelta;
pos.Y += vel.Y * deltaTime; position.Y += velocity.Y * frameDelta;
}); });
} }
} }
``` ```
### 5. 注册系统 ### 4. 初始化后解析 `World` 与模块服务
```csharp
public class GameArchitecture : Architecture
{
protected override void OnInitialize()
{
this.UseArch();
// 注册 ECS 系统
RegisterSystem<MovementSystem>();
}
}
```
### 6. 创建实体
```csharp ```csharp
using Arch.Core; using Arch.Core;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using MyGame.Components;
[ContextAware]
public partial class GameController
{
public void Start()
{
// 获取 World
var world = this.GetService<World>();
// 创建实体
var player = world.Create(
new Position(0, 0),
new Velocity(0, 0),
new Health(100, 100)
);
var enemy = world.Create(
new Position(10, 10),
new Velocity(-1, 0),
new Health(50, 50)
);
}
}
```
### 7. 更新系统
```csharp
using GFramework.Ecs.Arch.Abstractions; using GFramework.Ecs.Arch.Abstractions;
public class GameLoop var world = architecture.Context.GetService<World>();
{ var ecsModule = architecture.Context.GetService<IArchEcsModule>();
private IArchEcsModule _ecsModule;
public void Initialize()
{
// 获取 ECS 模块
_ecsModule = architecture.Context.GetService<IArchEcsModule>();
}
public void Update(float deltaTime)
{
// 更新所有 ECS 系统
_ecsModule.Update(deltaTime);
}
}
``` ```
## 配置选项 ### 5. 由宿主循环显式调用 `Update`
### ArchOptions
```csharp ```csharp
public sealed class ArchOptions ecsModule.Update(deltaTime);
{
/// <summary>
/// World 初始容量默认1000
/// </summary>
public int WorldCapacity { get; set; } = 1000;
/// <summary>
/// 是否启用统计信息默认false
/// </summary>
public bool EnableStatistics { get; set; } = false;
/// <summary>
/// 模块优先级默认50
/// </summary>
public int Priority { get; set; } = 50;
}
``` ```
### 配置示例 这一步在 `GFramework.Ecs.Arch.Tests/Ecs/*.cs` 中也采用同样的驱动方式。
```csharp ## 运行时职责
this.UseArch(options =>
{
// 设置 World 初始容量
// 根据预期实体数量设置,避免频繁扩容
options.WorldCapacity = 2000;
// 启用统计信息(开发/调试时使用) | 类型 | 责任 | 证据文件 |
options.EnableStatistics = true; | --- | --- | --- |
| `ArchExtensions` | 把 `ArchEcsModule` 注册到 `ArchitectureModuleRegistry` | `GFramework.Ecs.Arch/Extensions/ArchExtensions.cs` |
| `ArchEcsModule` | 创建并注册 `World`,按优先级收集 `ArchSystemAdapter<float>`,负责初始化、销毁和逐帧更新 | `GFramework.Ecs.Arch/ArchEcsModule.cs` |
| `ArchSystemAdapter<T>` | 从 GFramework 系统生命周期桥接到 Arch `ISystem<T>` 生命周期 | `GFramework.Ecs.Arch/ArchSystemAdapter.cs` |
| `ArchOptions` | 暴露 `WorldCapacity``EnableStatistics``Priority` 这组运行时配置对象 | `GFramework.Ecs.Arch/ArchOptions.cs` |
// 设置模块优先级 ## 配置对象阅读提示
// 数值越小,优先级越高
options.Priority = 50;
});
```
## 核心概念 当前公开配置对象是 `GFramework.Ecs.Arch.ArchOptions`。从源码可直接确认:
### Entity实体 - `WorldCapacity` 用于 `World.Create(...)` 的容量参数
- `Priority` 影响 `ArchEcsModule` 作为服务模块的排序
- `EnableStatistics` 目前保留在公开配置面上;采用时应以源码 XML 注释和实现行为为准,而不是依赖旧文档推断
实体是游戏世界中的基本对象,本质上是一个唯一标识符: ## 类型族级 XML Inventory
```csharp | 类型族 | 代表类型 | XML 状态 | 阅读重点 |
// 创建空实体 | --- | --- | --- | --- |
var entity = world.Create(); | 装配入口 | `ArchExtensions` | 已覆盖 | `UseArch(...)` 的时机、链式调用返回值 |
| 服务模块 | `ArchEcsModule` | 已覆盖 | `World` 注册、系统收集、模块销毁顺序 |
| 系统桥接层 | `ArchSystemAdapter<T>` | 已覆盖 | `OnArchInitialize` / `OnUpdate` / `OnArchDispose` |
| 示例类型 | `Position``Velocity``MovementSystem` | 已覆盖 | 组件布局、查询写法、最小集成样例 |
// 创建带组件的实体 ## 相关入口
var entity = world.Create(
new Position(0, 0),
new Velocity(1, 1)
);
// 销毁实体 - ECS 模块总览:[`index.md`](./index.md)
world.Destroy(entity); - 抽象契约页:[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)
``` - 仓库模块 README`GFramework.Ecs.Arch/README.md`
- 统一 API / XML 导航:[`../api-reference/index.md`](../api-reference/index.md)
### Component组件
组件是纯数据结构,用于存储实体的状态:
```csharp
// 添加组件
world.Add(entity, new Position(0, 0));
// 检查组件
if (world.Has<Position>(entity))
{
// 获取组件引用(零 GC 分配)
ref var pos = ref world.Get<Position>(entity);
pos.X += 10;
}
// 设置组件(替换现有值)
world.Set(entity, new Position(100, 100));
// 移除组件
world.Remove<Velocity>(entity);
```
### System系统
系统包含游戏逻辑,处理具有特定组件组合的实体:
```csharp
public sealed class DamageSystem : ArchSystemAdapter<float>
{
private QueryDescription _query;
protected override void OnArchInitialize()
{
// 初始化查询
_query = new QueryDescription()
.WithAll<Health, Damage>();
}
protected override void OnUpdate(in float deltaTime)
{
// 处理伤害
World.Query(in _query, (Entity entity, ref Health health, ref Damage damage) =>
{
health.Current -= damage.Value * deltaTime;
if (health.Current <= 0)
{
health.Current = 0;
World.Remove<Damage>(entity);
}
});
}
}
```
### World世界
World 是 ECS 的核心容器,管理所有实体和组件:
```csharp
// World 由 ArchEcsModule 自动创建和注册
var world = this.GetService<World>();
// 获取实体数量
var entityCount = world.Size;
// 清空所有实体
world.Clear();
```
## 系统适配器
### ArchSystemAdapter&lt;T&gt;
`ArchSystemAdapter&lt;T&gt;` 桥接 Arch.System.ISystem&lt;T&gt; 到 GFramework 架构:
```csharp
public sealed class MySystem : ArchSystemAdapter<float>
{
// Arch 系统初始化
protected override void OnArchInitialize()
{
// 创建查询、初始化资源
}
// 更新前调用
protected override void OnBeforeUpdate(in float deltaTime)
{
// 预处理逻辑
}
// 主更新逻辑
protected override void OnUpdate(in float deltaTime)
{
// 处理实体
}
// 更新后调用
protected override void OnAfterUpdate(in float deltaTime)
{
// 后处理逻辑
}
// 资源清理
protected override void OnArchDispose()
{
// 清理资源
}
}
```
### 访问 World
在系统中可以直接访问 `World` 属性:
```csharp
public sealed class MySystem : ArchSystemAdapter<float>
{
protected override void OnUpdate(in float deltaTime)
{
// 访问 World
var entityCount = World.Size;
// 创建实体
var entity = World.Create(new Position(0, 0));
// 查询实体
var query = new QueryDescription().WithAll<Position>();
World.Query(in query, (ref Position pos) =>
{
// 处理逻辑
});
}
}
```
### 访问框架服务
`ArchSystemAdapter&lt;T&gt;` 继承自 `AbstractSystem`,可以使用所有 GFramework 的扩展方法:
```csharp
public sealed class ServiceAccessSystem : ArchSystemAdapter<float>
{
protected override void OnUpdate(in float deltaTime)
{
// 获取 Model
var playerModel = this.GetModel<PlayerModel>();
// 获取 Utility
var timeUtility = this.GetUtility<TimeUtility>();
// 发送命令
this.SendCommand(new SaveGameCommand());
// 发送查询
var score = this.SendQuery(new GetScoreQuery());
// 发送事件
this.SendEvent(new GameOverEvent());
}
}
```
## 查询实体
### 基本查询
```csharp
// 查询:必须有 Position 和 Velocity
var query = new QueryDescription()
.WithAll<Position, Velocity>();
World.Query(in query, (ref Position pos, ref Velocity vel) =>
{
pos.X += vel.X * deltaTime;
pos.Y += vel.Y * deltaTime;
});
```
### 过滤查询
```csharp
// 查询:必须有 Health但不能有 Damage
var query = new QueryDescription()
.WithAll<Health>()
.WithNone<Damage>();
World.Query(in query, (ref Health health) =>
{
// 只处理没有受伤的实体
});
```
### 可选组件查询
```csharp
// 查询:必须有 Position可选 Velocity
var query = new QueryDescription()
.WithAll<Position>()
.WithAny<Velocity>();
World.Query(in query, (Entity entity, ref Position pos) =>
{
// 处理逻辑
});
```
### 访问实体 ID
```csharp
var query = new QueryDescription().WithAll<Position>();
World.Query(in query, (Entity entity, ref Position pos) =>
{
// 可以访问实体 ID
Console.WriteLine($"Entity {entity.Id}: ({pos.X}, {pos.Y})");
// 可以对实体进行操作
if (pos.X > 100)
{
World.Destroy(entity);
}
});
```
## 系统优先级
系统按照优先级顺序执行,数值越小优先级越高:
```csharp
using GFramework.Core.Abstractions.bases;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
// 使用 Priority 特性设置优先级
[Priority(10)] // 高优先级,先执行
public sealed class InputSystem : ArchSystemAdapter<float>
{
// ...
}
[Priority(20)] // 中优先级
public sealed class MovementSystem : ArchSystemAdapter<float>
{
// ...
}
[Priority(30)] // 低优先级,后执行
public sealed class RenderSystem : ArchSystemAdapter<float>
{
// ...
}
```
执行顺序InputSystem → MovementSystem → RenderSystem
## 性能优化
### 1. 使用 struct 组件
```csharp
// ✅ 推荐:使用 struct
[StructLayout(LayoutKind.Sequential)]
public struct Position(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
// ❌ 不推荐:使用 class
public class Position
{
public float X { get; set; }
public float Y { get; set; }
}
```
### 2. 缓存查询
```csharp
public class OptimizedSystem : ArchSystemAdapter<float>
{
// ✅ 推荐:缓存查询
private QueryDescription _cachedQuery;
protected override void OnArchInitialize()
{
_cachedQuery = new QueryDescription()
.WithAll<Position, Velocity>();
}
protected override void OnUpdate(in float deltaTime)
{
World.Query(in _cachedQuery, (ref Position pos, ref Velocity vel) =>
{
pos.X += vel.X * deltaTime;
pos.Y += vel.Y * deltaTime;
});
}
}
```
### 3. 使用 ref 访问组件
```csharp
// ✅ 推荐:使用 ref 避免复制
World.Query(in query, (ref Position pos, ref Velocity vel) =>
{
pos.X += vel.X; // 直接修改,零 GC
});
// ❌ 不推荐:不使用 ref
World.Query(in query, (Position pos, Velocity vel) =>
{
pos.X += vel.X; // 复制值,修改不会生效
});
```
### 4. 组件大小优化
```csharp
// ✅ 推荐:小而专注的组件
public struct Position(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
public struct Velocity(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
// ❌ 不推荐:大而全的组件
public struct Transform
{
public float X, Y, Z;
public float RotationX, RotationY, RotationZ;
public float ScaleX, ScaleY, ScaleZ;
public float VelocityX, VelocityY, VelocityZ;
// ... 太多数据
}
```
## 最佳实践
### 1. 组件设计原则
- 使用 `struct` 而不是 `class`
- 只包含数据,不包含逻辑
- 使用 `[StructLayout(LayoutKind.Sequential)]` 优化内存布局
- 保持组件小而专注
### 2. 系统设计原则
- 单一职责:每个系统只负责一件事
- 缓存查询:在 `OnArchInitialize` 中创建查询
- 使用 ref访问组件时使用 ref 参数
- 批量处理:一次查询处理所有实体
### 3. 标签组件
使用空结构体作为标签来分类实体:
```csharp
// 定义标签组件
public struct PlayerTag { }
public struct EnemyTag { }
public struct DeadTag { }
// 使用标签过滤实体
var query = new QueryDescription()
.WithAll<Position, Velocity, PlayerTag>()
.WithNone<DeadTag>();
```
### 4. 与传统架构结合
```csharp
// ECS 系统可以访问 Model
public class EnemySpawnSystem : ArchSystemAdapter<float>
{
protected override void OnUpdate(in float deltaTime)
{
var gameState = this.GetModel<GameStateModel>();
// 根据关卡生成敌人
for (int i = 0; i < gameState.Level; i++)
{
World.Create(
new Position(Random.Shared.Next(0, 100), 0),
new Velocity(0, -1),
new Health(50, 50)
);
}
}
}
```
## 常见问题
### Q: 如何在运行时动态添加/移除组件?
A: Arch 支持运行时修改实体的组件:
```csharp
// 动态添加组件
if (pos.X > 100 && !World.Has<FastTag>(entity))
{
World.Add(entity, new FastTag());
}
// 动态移除组件
if (pos.X < 0 && World.Has<FastTag>(entity))
{
World.Remove<FastTag>(entity);
}
```
### Q: 如何处理实体之间的交互?
A: 使用嵌套查询或事件:
```csharp
// 方式 1嵌套查询
World.Query(in playerQuery, (Entity player, ref Position playerPos) =>
{
World.Query(in enemyQuery, (Entity enemy, ref Position enemyPos) =>
{
// 检测碰撞
});
});
// 方式 2使用事件
this.SendEvent(new CollisionEvent
{
Entity1 = player,
Entity2 = enemy
});
```
### Q: 如何调试 ECS 系统?
A: 使用日志和统计信息:
```csharp
protected override void OnUpdate(in float deltaTime)
{
// 打印实体数量
Console.WriteLine($"Total entities: {World.Size}");
// 查询特定实体
var query = new QueryDescription().WithAll<Position>();
var count = 0;
World.Query(in query, (Entity entity, ref Position pos) =>
{
count++;
Console.WriteLine($"Entity {entity.Id}: ({pos.X}, {pos.Y})");
});
}
```
## 相关资源
- [Arch.Core 官方文档](https://github.com/genaray/Arch)
- [ECS 概述](./index.md)

View File

@ -1,95 +1,39 @@
--- ---
title: ECS 系统集成 title: ECS 系统集成
description: GFramework 的 ECSEntity Component System集成方案支持多种 ECS 框架 description: GFramework 当前 ECS 模块族的包边界、采用顺序与 XML 阅读入口
--- ---
# ECS 系统集成 # ECS 系统集成
## 概述 GFramework 当前仓库内已经交付并持续维护的 ECS 模块族是 `Ecs.Arch`。它分成运行时实现包
`GFramework.Ecs.Arch` 和契约包 `GFramework.Ecs.Arch.Abstractions`,分别覆盖默认装配能力与共享边界约定。
GFramework 提供了灵活的 ECSEntity Component System集成方案允许你根据项目需求选择合适的 ECS 框架。ECS ## 当前模块族
是一种数据驱动的架构模式,特别适合处理大量相似实体的场景。
## 什么是 ECS | 包 | 适用场景 | 你会得到什么 | 继续阅读 |
| --- | --- | --- | --- |
| `GFramework.Ecs.Arch` | 需要默认运行时、`UseArch(...)` 装配入口、`World` 注册和系统适配基类 | `ArchEcsModule``ArchSystemAdapter<T>``ArchExtensions.UseArch(...)`、示例组件与系统 | [`arch.md`](./arch.md) |
| `GFramework.Ecs.Arch.Abstractions` | 只想让共享宿主循环、测试替身或扩展模块依赖最小契约,而不引入默认运行时 | `IArchEcsModule``IArchSystemAdapter<T>``ArchOptions` 契约对象 | [`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md) |
ECSEntity Component System是一种架构模式将游戏对象分解为三个核心概念 ## 最小采用路径
- **Entity实体**:游戏世界中的基本对象,本质上是一个唯一标识符 ### 1. 选择包边界
- **Component组件**:纯数据结构,存储实体的状态
- **System系统**:包含游戏逻辑,处理具有特定组件组合的实体
### ECS 的优势 - 需要默认实现时安装 `GeWuYou.GFramework.Ecs.Arch`
- 只需要契约时安装 `GeWuYou.GFramework.Ecs.Arch.Abstractions`
- **高性能**:数据局部性好,缓存友好 ### 2. 在 `Initialize()` 前显式接入运行时
- **可扩展**:通过组合组件轻松创建新实体类型
- **并行处理**:系统之间相互独立,易于并行化
- **数据驱动**:逻辑与数据分离,便于序列化和网络同步
### 何时使用 ECS `UseArch(...)` 通过 `ArchitectureModuleRegistry` 注册服务模块。按当前源码与集成测试,它应在架构实例调用
`Initialize()` 之前完成。
**适合使用 ECS 的场景**
- 大量相似实体(敌人、子弹、粒子)
- 需要高性能批量处理
- 复杂的实体组合和变化
- 需要并行处理的系统
**不适合使用 ECS 的场景**
- 全局状态管理
- 单例服务
- UI 逻辑
- 游戏流程控制
## 支持的 ECS 框架
GFramework 采用可选集成的设计,你可以根据需求选择合适的 ECS 框架:
### Arch ECS推荐
[Arch](https://github.com/genaray/Arch) 是一个高性能的 C# ECS 框架,具有以下特点:
- ✅ **极致性能**:基于 Archetype 的内存布局,零 GC 分配
- ✅ **简单易用**:清晰的 API易于上手
- ✅ **功能完整**:支持查询、过滤、并行处理等高级特性
- ✅ **活跃维护**:社区活跃,持续更新
**安装方式**
```bash
dotnet add package GeWuYou.GFramework.Ecs.Arch
```
**文档链接**[Arch ECS 集成指南](./arch.md)
### 其他 ECS 框架
GFramework 的设计允许集成其他 ECS 框架,未来可能支持:
- **DefaultEcs**:轻量级 ECS 框架
- **Entitas**:成熟的 ECS 框架Unity 生态常用
- **自定义 ECS**:你可以基于 GFramework 的模块系统实现自己的 ECS 集成
## 快速开始
### 1. 选择 ECS 框架
根据项目需求选择合适的 ECS 框架。对于大多数项目,我们推荐使用 Arch ECS。
### 2. 安装集成包
```bash
# 安装 Arch ECS 集成包
dotnet add package GeWuYou.GFramework.Ecs.Arch
```
### 3. 注册 ECS 模块
```csharp ```csharp
using GFramework.Core.Architecture; using Arch.Core;
using GFramework.Ecs.Arc; using GFramework.Core.Architectures;
using GFramework.Ecs.Arch.Abstractions;
using GFramework.Ecs.Arch.Extensions;
public class GameArchitecture : Architecture public sealed class GameArchitecture : Architecture
{ {
public GameArchitecture() : base(new ArchitectureConfiguration()) public GameArchitecture() : base(new ArchitectureConfiguration())
{ {
@ -97,41 +41,29 @@ public class GameArchitecture : Architecture
protected override void OnInitialize() protected override void OnInitialize()
{ {
// 显式注册 Arch ECS 模块 RegisterSystem<MovementSystem>();
this.UseArch(options =>
{
options.WorldCapacity = 2000;
options.EnableStatistics = true;
});
} }
} }
var architecture = new GameArchitecture()
.UseArch(options =>
{
options.WorldCapacity = 2048;
options.Priority = 50;
});
architecture.Initialize();
var world = architecture.Context.GetService<World>();
var ecsModule = architecture.Context.GetService<IArchEcsModule>();
``` ```
### 4. 定义组件 ### 3. 让 ECS 系统继承 `ArchSystemAdapter<float>`
```csharp
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Position(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
[StructLayout(LayoutKind.Sequential)]
public struct Velocity(float x, float y)
{
public float X { get; set; } = x;
public float Y { get; set; } = y;
}
```
### 5. 创建系统
```csharp ```csharp
using Arch.Core; using Arch.Core;
using GFramework.Ecs.Arch; using GFramework.Ecs.Arch;
using GFramework.Ecs.Arch.Components;
public sealed class MovementSystem : ArchSystemAdapter<float> public sealed class MovementSystem : ArchSystemAdapter<float>
{ {
@ -145,147 +77,60 @@ public sealed class MovementSystem : ArchSystemAdapter<float>
protected override void OnUpdate(in float deltaTime) protected override void OnUpdate(in float deltaTime)
{ {
World.Query(in _query, (ref Position pos, ref Velocity vel) => var frameDelta = deltaTime;
World.Query(in _query, (ref Position position, ref Velocity velocity) =>
{ {
pos.X += vel.X * deltaTime; position.X += velocity.X * frameDelta;
pos.Y += vel.Y * deltaTime; position.Y += velocity.Y * frameDelta;
}); });
} }
} }
``` ```
### 6. 注册系统 ### 4. 由宿主循环驱动更新
`IArchEcsModule` 继承自 `IServiceModule`,负责初始化和销毁;真正的帧更新通过 `Update(float deltaTime)` 显式触发。
```csharp ```csharp
public class GameArchitecture : Architecture using GFramework.Ecs.Arch.Abstractions;
{
protected override void OnInitialize()
{
this.UseArch();
// 注册 ECS 系统 public sealed class GameLoop
RegisterSystem<MovementSystem>(); {
private readonly IArchEcsModule _ecsModule;
public GameLoop(IArchEcsModule ecsModule)
{
_ecsModule = ecsModule;
}
public void Tick(float deltaTime)
{
_ecsModule.Update(deltaTime);
} }
} }
``` ```
## 设计理念 ## 阅读顺序
### 显式集成 1. 先看本页,确认自己要的是运行时包还是契约包
2. 需要默认实现时继续读 [`arch.md`](./arch.md)
3. 只想保留共享边界时继续读 [`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)
4. 统一查阅 README / docs / XML 入口时回到 [`../api-reference/index.md`](../api-reference/index.md)
GFramework 采用显式集成的设计,而不是自动注册: ## 类型族级 XML Inventory
```csharp 下表记录当前 `Ecs.Arch` family 的类型声明级 XML 基线,便于从 README、站内 landing 和源码之间建立一致的审计入口。
// ✅ 显式注册 - 清晰、可控
public class GameArchitecture : Architecture
{
protected override void OnInitialize()
{
this.UseArch(); // 明确表示使用 Arch ECS
}
}
// ❌ 自动注册 - 隐式、难以控制 | 包 | 类型族 | 代表类型 | XML 状态 | 阅读重点 |
// 只需引入包,自动注册(不推荐) | --- | --- | --- | --- | --- |
``` | `GFramework.Ecs.Arch` | 运行时装配与模块生命周期 | `ArchExtensions``ArchEcsModule` | 已覆盖 | `UseArch(...)` 的接入时机、`World` 注册、模块优先级 |
| `GFramework.Ecs.Arch` | 系统桥接层 | `ArchSystemAdapter<T>` | 已覆盖 | GFramework `ISystem` 生命周期如何桥接到 Arch `ISystem<T>` |
| `GFramework.Ecs.Arch` | 示例组件与系统 | `Position``Velocity``MovementSystem` | 已覆盖 | 查询写法、组件布局、最小可运行示例 |
| `GFramework.Ecs.Arch.Abstractions` | 契约与配置对象 | `IArchEcsModule``IArchSystemAdapter<T>``ArchOptions` | 已覆盖 | 共享宿主循环、测试替身、跨程序集配置边界 |
**优势** ## 边界说明
- 清晰的依赖关系 - 当前仓库没有交付其他可直接消费的 ECS 运行时包;旧文档里把“可能支持的其他 ECS 框架”写成现有能力会误导采用路径。
- 更好的 IDE 支持 - `GFramework.Ecs.Arch.Abstractions` 负责“边界”,`GFramework.Ecs.Arch` 负责“默认实现”。
- 易于测试和调试 - 站内页面只维护可构建的 docs 链路;仓库根 README 和模块 README 继续承担包目录入口职责。
- 符合 .NET 生态习惯
### 零依赖原则
如果你不使用 ECSGFramework.Core 包不会引入任何 ECS 相关的依赖:
```xml
<!-- GFramework.Core.csproj -->
<ItemGroup>
<!-- 无 Arch 依赖 -->
</ItemGroup>
<!-- GFramework.Ecs.Arch.csproj -->
<ItemGroup>
<PackageReference Include="Arch" Version="2.1.0" />
<PackageReference Include="Arch.System" Version="1.1.0" />
</ItemGroup>
```
### 模块化设计
ECS 集成基于 GFramework 的模块系统:
```csharp
// ECS 模块实现 IServiceModule 接口
public sealed class ArchEcsModule : IArchEcsModule
{
public string ModuleName => nameof(ArchEcsModule);
public int Priority => 50;
public bool IsEnabled { get; }
public void Register(IIocContainer container) { }
public void Initialize() { }
public ValueTask DestroyAsync() { }
public void Update(float deltaTime) { }
}
```
## 与传统架构结合
ECS 可以与 GFramework 的传统架构Model、System、Utility无缝结合
```csharp
// Model 存储全局状态
public class GameStateModel : AbstractModel
{
public int Score { get; set; }
public int Level { get; set; }
}
// ECS System 处理实体逻辑
public class EnemySpawnSystem : ArchSystemAdapter<float>
{
protected override void OnUpdate(in float deltaTime)
{
// 访问 Model
var gameState = this.GetModel<GameStateModel>();
// 根据关卡生成敌人
for (int i = 0; i < gameState.Level; i++)
{
World.Create(
new Position(Random.Shared.Next(0, 100), 0),
new Velocity(0, -1),
new Health(50, 50)
);
}
}
}
// 传统 System 处理游戏逻辑
public class ScoreSystem : AbstractSystem
{
protected override void OnInit()
{
this.RegisterEvent<EnemyDestroyedEvent>(OnEnemyDestroyed);
}
private void OnEnemyDestroyed(EnemyDestroyedEvent e)
{
var gameState = this.GetModel<GameStateModel>();
gameState.Score += 100;
}
}
```
## 下一步
- [Arch ECS 集成指南](./arch.md) - 详细的 Arch ECS 使用文档
## 相关资源
- [Architecture 架构系统](../core/architecture.md)
- [System 系统](../core/system.md)
- [事件系统](../core/events.md)

View File

@ -1,612 +1,176 @@
--- ---
title: Godot 架构集成 title: Godot 架构集成
description: Godot 架构集成提供了 GFramework 与 Godot 引擎的无缝连接,实现生命周期同步和模块化开发 description: 说明 AbstractArchitecture、ArchitectureAnchor 和 Godot 模块挂接的当前生命周期语义,避免继续沿用旧版 `.Wait()` 接法
--- ---
# Godot 架构集成 # Godot 架构集成
## 概述 ## 概述
Godot 架构集成是 GFramework.Godot 中连接框架与 Godot 引擎的核心组件。它提供了架构与 Godot 场景树的生命周期绑定、模块化扩展系统,以及与 `GFramework.Godot` 当前的架构集成目标很直接:让 `Architecture` 能安全地感知 Godot `SceneTree` 生命周期,并在需要时把
Godot 节点系统的深度集成 `Node` 的扩展模块挂到场景树上
通过 Godot 架构集成,你可以在 Godot 项目中使用 GFramework 的所有功能,同时保持与 Godot 引擎的完美兼容。 当前真正参与这条链路的核心类型只有三类:
**主要特性** - `AbstractArchitecture`:在原有 `Architecture` 之上增加 Godot 生命周期绑定
- `ArchitectureAnchor`:挂在 `SceneTree.Root` 下的锚点节点,负责把 `_ExitTree()` 事件转回架构销毁
- `IGodotModule` / `AbstractGodotModule`:当模块本身需要携带 Godot `Node` 时使用
- 架构与 Godot 生命周期自动同步 它不是另一套独立的模块系统,也不意味着所有模块都必须改成 `InstallGodotModule(...)`
- 模块化的 Godot 扩展系统
- 架构锚点节点管理
- 自动资源清理
- 热重载支持
- 与 Godot 场景树深度集成
## 核心概念 ## 什么时候该用 `AbstractArchitecture`
### 抽象架构 当你的架构需要满足下面任一条件时,可以让它继承 `AbstractArchitecture`
`AbstractArchitecture` 是 Godot 项目中架构的基类: - 需要把架构生命周期绑定到 Godot `SceneTree`
- 需要在架构里安装带 `Node` 的扩展模块
- 需要通过受保护的 `ArchitectureRoot` 访问锚点节点,继续挂接 Godot 子节点
如果你只是做普通的 Model / System / Utility 注册,`AbstractArchitecture` 的主要价值仍然是“让架构知道自己何时跟随
Godot 场景树销毁”,而不是改变注册方式。
## 最小接入路径
### 常规模块仍然用 `InstallModule(...)`
当前消费者 `ai-libs/CoreGrid` 的默认做法,是保持普通模块注册方式:
```csharp ```csharp
public abstract class AbstractArchitecture : Architecture using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Environment;
using GFramework.Godot.Architectures;
namespace MyGame.Scripts.Core;
public sealed class GameArchitecture(
IArchitectureConfiguration configuration,
IEnvironment environment)
: AbstractArchitecture(configuration, environment)
{ {
protected Node ArchitectureRoot { get; }
protected abstract void InstallModules();
protected Task InstallGodotModule<TModule>(TModule module);
}
```
### 架构锚点
`ArchitectureAnchor` 是连接架构与 Godot 场景树的桥梁:
```csharp
public partial class ArchitectureAnchor : Node
{
public void Bind(Action onExit);
public override void _ExitTree();
}
```
### Godot 模块
`IGodotModule` 定义了 Godot 特定的模块接口:
```csharp
public interface IGodotModule : IArchitectureModule
{
Node Node { get; }
void OnPhase(ArchitecturePhase phase, IArchitecture architecture);
void OnAttach(Architecture architecture);
void OnDetach();
}
```
## 基本用法
### 创建 Godot 架构
```csharp
using GFramework.Godot.Architecture;
using GFramework.Core.Abstractions.Architecture;
public class GameArchitecture : AbstractArchitecture
{
// 单例实例
public static GameArchitecture Interface { get; private set; }
public GameArchitecture()
{
Interface = this;
}
protected override void InstallModules() protected override void InstallModules()
{ {
// 注册 Model InstallModule(new UtilityModule());
RegisterModel(new PlayerModel()); InstallModule(new ModelModule());
RegisterModel(new GameModel()); InstallModule(new GameplayModule());
InstallModule(new SystemModule());
// 注册 System
RegisterSystem(new GameplaySystem());
RegisterSystem(new AudioSystem());
// 注册 Utility
RegisterUtility(new StorageUtility());
} }
} }
``` ```
### 在 Godot 场景中初始化架构 这里继承 `AbstractArchitecture` 的意义,是把架构绑定到 Godot 生命周期,而不是把普通模块注册改写成 Godot 风格 API。
### 只有携带 `Node` 的模块才需要 `InstallGodotModule(...)`
如果模块本身暴露一个 Godot `Node`,并且希望由架构锚点统一托管,可以这样写:
```csharp ```csharp
using Godot; using GFramework.Core.Abstractions.Architectures;
using GFramework.Godot.Architecture; using GFramework.Godot.Architectures;
public partial class GameRoot : Node
{
private GameArchitecture _architecture;
public override void _Ready()
{
// 创建并初始化架构
_architecture = new GameArchitecture();
_architecture.InitializeAsync().AsTask().Wait();
GD.Print("架构已初始化");
}
}
```
### 使用架构锚点
架构锚点会自动创建并绑定到场景树:
```csharp
// 架构会自动创建锚点节点
// 节点名称格式: __GFramework__GameArchitecture__[HashCode]__ArchitectureAnchor__
// 当场景树销毁时,锚点会自动触发架构清理
```
## 高级用法
### 创建 Godot 模块
```csharp
using GFramework.Godot.Architecture;
using Godot; using Godot;
public class CoroutineModule : AbstractGodotModule namespace MyGame.Scripts.Core;
public sealed class HudModule : AbstractGodotModule
{ {
private Node _coroutineNode; private readonly Control _root = new()
public override Node Node => _coroutineNode;
public CoroutineModule()
{ {
_coroutineNode = new Node { Name = "CoroutineScheduler" }; Name = "HudModule"
} };
public override Node Node => _root;
public override void Install(IArchitecture architecture) public override void Install(IArchitecture architecture)
{ {
// 注册协程调度器
var scheduler = new CoroutineScheduler(new GodotTimeSource());
architecture.RegisterSystem<ICoroutineScheduler>(scheduler);
GD.Print("协程模块已安装");
} }
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) public override void OnAttach(GFramework.Core.Architectures.Architecture architecture)
{ {
if (phase == ArchitecturePhase.Ready)
{
GD.Print("协程模块已就绪");
}
} }
public override void OnDetach() public override void OnDetach()
{ {
GD.Print("协程模块已分离"); _root.QueueFree();
_coroutineNode?.QueueFree();
} }
} }
``` ```
### 安装 Godot 模块 这类模块的关键点不是“注册更多框架能力”,而是“让模块节点跟着架构锚点进出场景树”。
真正调用 `InstallGodotModule(...)` 时,也应该把它放在能够接受异步挂接流程的初始化路径里,而不是继续沿用旧文档里的
`.Wait()` 叙述。
```csharp ## 当前生命周期
public class GameArchitecture : AbstractArchitecture
{
protected override void InstallModules()
{
// 安装核心模块
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
// 安装 Godot 模块 ### 初始化阶段
InstallGodotModule(new CoroutineModule()).Wait();
InstallGodotModule(new SceneModule()).Wait();
InstallGodotModule(new UiModule()).Wait();
}
}
```
### 访问架构根节点 `AbstractArchitecture.OnInitialize()` 目前会按这个顺序工作:
```csharp 1. 生成唯一的锚点节点名称
public class SceneModule : AbstractGodotModule 2. 调用 `AttachToGodotLifecycle()`
{ 3. 在可用的 `SceneTree` 上创建并绑定 `ArchitectureAnchor`
private Node _sceneRoot; 4. 执行你重写的 `InstallModules()`
public override Node Node => _sceneRoot; 也就是说Godot 生命周期绑定先发生,业务模块注册后发生。
public SceneModule() ### `InstallGodotModule(...)` 的执行顺序
{
_sceneRoot = new Node { Name = "SceneRoot" };
}
public override void Install(IArchitecture architecture) 当前实现里,`InstallGodotModule(...)` 会:
{
// 访问架构根节点
if (architecture is AbstractArchitecture godotArch)
{
var root = godotArch.ArchitectureRoot;
root.AddChild(_sceneRoot);
}
}
}
```
### 监听架构阶段 1. 检查模块参数是否为 `null`
2. 检查 `_anchor` 是否已初始化
3. 先执行 `module.Install(this)`
4. 把模块登记进内部 `_extensions`
5. `await anchor.WaitUntilReadyAsync()`
6. 通过 `CallDeferred(AddChild, module.Node)` 把模块节点挂到锚点下
7. 调用 `module.OnAttach(this)`
```csharp 这条顺序有两个实际意义:
public class AnalyticsModule : AbstractGodotModule
{
private Node _analyticsNode;
public override Node Node => _analyticsNode; - 模块会在挂接节点前先完成框架侧注册
- 只有等锚点真正 ready 后,才进入需要访问 Godot 节点 API 的附加阶段
public AnalyticsModule() ### 销毁阶段
{
_analyticsNode = new Node { Name = "Analytics" };
}
public override void Install(IArchitecture architecture) `ArchitectureAnchor._ExitTree()` 会触发绑定好的退出回调,随后 `AbstractArchitecture` 会开始观察异步销毁流程:
{
// 安装分析系统
}
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - 防止重复销毁
{ - 依次调用已登记 Godot 模块的 `OnDetach()`
switch (phase) - 清空内部扩展列表
{ - 再进入基类 `DestroyAsync()`
case ArchitecturePhase.Initializing:
GD.Print("架构正在初始化");
break;
case ArchitecturePhase.Ready: 如果异步销毁抛异常,当前实现会把错误写到 Godot 错误输出,而不是静默吞掉。
GD.Print("架构已就绪,开始追踪");
StartTracking();
break;
case ArchitecturePhase.Destroying: ## 当前边界
GD.Prin构正在销毁停止追踪");
StopTracking();
break;
}
}
private void StartTracking() { } ### 没有锚点时不会偷偷安装模块
private void StopTracking() { }
}
```
### 自定义架构配置 `GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs` 已覆盖一个关键边界:
```csharp - 当锚点尚未初始化时,`InstallGodotModule(...)` 会直接抛 `InvalidOperationException("Anchor not initialized")`
using GFramework.Core.Abstractions.Architecture; - 失败发生在 `module.Install(...)` 之前,因此不会留下半安装副作用
using GFramework.Core.Abstractions.Environment;
public class GameArchitecture : AbstractArchitecture 这也是为什么文档不应该再把 `InstallGodotModule(...).Wait()` 写成一种随处可用的默认初始化方式。
{
public GameArchitecture() : base(
configuration: CreateConfiguration(),
environment: CreateEnvironment()
)
{
}
private static IArchitectureConfiguration CreateConfiguration() ### `AbstractGodotModule` 只是便捷基类,不代表自动阶段广播
{
return new ArchitectureConfiguration
{
EnableLogging
LogLevel = LogLevel.Debug
};
}
private static IEnvironment CreateEnvironment() 当前接口 `IGodotModule` 真正保证的成员只有:
{
return new DefaultEnvironment
{
IsDevelopment = OS.IsDebugBuild()
};
}
protected override void InstallModules() - `Node`
{ - `Install(IArchitecture architecture)`
// 根据环境配置安装模块 - `OnAttach(Architecture architecture)`
if (Environment.IsDevelopment) - `OnDetach()`
{
InstallGodotModule(new DebugModule()).Wait();
}
// 安装核心模块 `AbstractGodotModule` 里虽然保留了 `OnPhase(...)` / `OnArchitecturePhase(...)` 虚方法,但它们不在当前接口契约内,也没有在
RegisterModel(new PlayerModel()); 这条挂接流程里形成稳定的自动广播语义。不要把它写成当前公开保证。
RegisterSystem(new GameplaySystem());
}
}
```
### 热重载支持 ### `ArchitectureRoot` 只在锚点就绪后可用
```csharp `ArchitectureRoot` 是受保护属性,底层直接返回 `_anchor`。如果锚点尚未准备好或架构已经失效,它会抛
public class GameArchitecture : AbstractArchitecture `InvalidOperationException("Architecture root not ready")`。因此它适合放在明确依赖锚点存在的挂接逻辑里,而不是拿来做
{ 任意时机的全局节点查找。
private static bool _initialized;
protected override void OnInitialize() ## 继续阅读
{
// 防止热重载时重复初始化
if (_initialized)
{
GD.Print("架构已初始化,跳过重复初始化");
return;
}
base.OnInitialize(); 1. [Godot 运行时集成](./index.md)
_initialized = true; 2. [Godot 集成教程](../tutorials/godot-integration.md)
} 3. [Godot 场景系统](./scene.md)
4. [Godot UI 系统](./ui.md)
protected override async ValueTask OnDestroyAsync()
{
await base.OnDestroyAsync();
_initialized = false;
}
}
```
### 在节点中使用架构
```csharp
using Godot;
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class Player : CharacterBody2D, IController
{
public override void _Ready()
{
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var playerModel = this.GetModel<PlayerModel>();
var gameplaySystem = this.GetSystem<GameplaySystem>();
// 发送事件
this.SendEvent(new PlayerSpawnedEvent());
// 执行命令
this.SendCommand(new InitPlayerCommand());
}
public override void _Process(double delta)
{
// 在 Process 中使用架构组件
var inputSystem = this.GetSystem<InputSystem>();
var movement = inputSystem.GetMovementInput();
Velocity = movement * 200;
MoveAndSlide();
}
}
```
### 多架构支持
```csharp
// 游戏架构
public class GameArchitecture : AbstractArchitecture
{
public static GameArchitecture Interface { get; private set; }
public GameArchitecture()
{
Interface = this;
}
protected override void InstallModules()
{
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
}
}
// UI 架构
public class UiArchitecture : AbstractArchitecture
{
public static UiArchitecture Interface { get; private set; }
public UiArchitecture()
{
Interface = this;
}
protected override void InstallModules()
{
RegisterModel(new UiModel());
RegisterSystem(new UiSystem());
}
}
// 在不同节点中使用不同架构
[ContextAware]
public partial class GameNode : Node, IController
{
// 配置使用 GameArchitecture 的上下文提供者
static GameNode()
{
SetContextProvider(new GameContextProvider());
}
}
[ContextAware]
public partial class UiNode : Control, IController
{
// 配置使用 UiArchitecture 的上下文提供者
static UiNode()
{
SetContextProvider(new UiContextProvider());
}
}
```
## 最佳实践
1. **使用单例模式**:为架构提供全局访问点
```csharp
public class GameArchitecture : AbstractArchitecture
{
public static GameArchitecture Interface { get; private set; }
public GameArchitecture()
{
Interface = this;
}
}
```
2. **在根节点初始化架构**:确保架构在所有节点之前就绪
```csharp
public partial class GameRoot : Node
{
public override void _Ready()
{
new GameArchitecture().InitializeAsync().AsTask().Wait();
}
}
```
3. **使用 Godot 模块组织功能**:将相关功能封装为模块
```csharp
InstallGodotModule(new CoroutineModule()).Wait();
InstallGodotModule(new SceneModule()).Wait();
InstallGodotModule(new UiModule()).Wait();
```
4. **利用架构阶段钩子**:在适当的时机执行逻辑
```csharp
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
if (phase == ArchitecturePhase.Ready)
{
// 架构就绪后的初始化
}
}
```
5. **正确清理资源**:在 OnDetach 中释放 Godot 节点
```csharp
public override void OnDetach()
{
_node?.QueueFree();
_node = null;
}
```
6. **避免在构造函数中访问架构**:使用 _Ready 或 OnPhase
```csharp
✗ public Player()
{
var model = this.GetModel<PlayerModel>(); // 架构可能未就绪
}
✓ public override void _Ready()
{
var model = this.GetModel<PlayerModel>(); // 安全
}
```
## 常见问题
### 问题:架构什么时候初始化?
**解答**
在根节点的 `_Ready` 方法中初始化:
```csharp
public partial class GameRoot : Node
{
public override void _Ready()
{
new GameArchitecture().InitializeAsync().AsTask().Wait();
}
}
```
### 问题:如何在节点中访问架构?
**解答**
使用 `[ContextAware]` 特性或直接使用单例:
```csharp
using GFramework.Core.SourceGenerators.Abstractions.Rule;
// 方式 1: 使用 [ContextAware] 特性(推荐)
[ContextAware]
public partial class Player : Node, IController
{
public override void _Ready()
{
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var model = this.GetModel<PlayerModel>();
var system = this.GetSystem<GameplaySystem>();
}
}
// 方式 2: 直接使用单例
public partial class Enemy : Node
{
public override void _Ready()
{
var model = GameArchitecture.Interface.GetModel<EnemyModel>();
}
}
```
**注意**
- `IController` 是标记接口,不包含任何方法
- 架构访问能力由 `[ContextAware]` 特性提供
- `[ContextAware]` 会自动生成 `Context` 属性和实现 `IContextAware` 接口
- 扩展方法(如 `this.GetModel()`)基于 `IContextAware` 接口,而非 `IController`
### 问题:架构锚点节点是什么?
**解答**
架构锚点是一个隐藏的节点,用于将架构绑定到 Godot 场景树。当场景树销毁时,锚点会自动触发架构清理。
### 问题:如何支持热重载?
**解答**
使用静态标志防止重复初始化:
```csharp
private static bool _initialized;
protected override void OnInitialize()
{
if (_initialized) return;
base.OnInitialize();
_initialized = true;
}
```
### 问题:可以有多个架构吗?
**解答**
可以,但通常一个游戏只需要一个主架构。如果需要多个架构,为每个架构提供独立的单例:
```csharp
public class GameArchitecture : AbstractArchitecture
{
public static GameArchitecture Interface { get; private set; }
}
public class UiArchitecture : AbstractArchitecture
{
public static UiArchitecture Interface { get; private set; }
}
```
### 问题Godot 模块和普通模块有什么区别?
**解答**
- **普通模块**:纯 C# 逻辑,不依赖 Godot
- **Godot 模块**:包含 Godot 节点,与场景树集成
```csharp
// 普通模块
InstallModule(new CoreModule());
// Godot 模块
InstallGodotModule(new SceneModule()).Wait();
```
## 相关文档
- [架构组件](/zh-CN/core/architecture) - 核心架构系统
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法

View File

@ -1,328 +1,181 @@
# Godot 扩展方法 (Godot Extensions) ---
title: Godot 扩展方法
description: 以当前 GFramework.Godot.Extensions 源码为准说明路径、Node、signal 和 unregister 扩展的真实成员与边界。
---
## 概述 # Godot 扩展方法
Godot 扩展方法模块为 Godot 引擎提供了丰富的便捷扩展方法集合。这些扩展方法简化了常见的 Godot `GFramework.Godot.Extensions` 当前并不是“覆盖所有 Godot 节点操作”的万能层。按源码看,它实际公开的扩展主要只有四组:
开发任务,提高了代码的可读性和开发效率。该模块遵循流畅接口设计原则,支持链式调用。
## 模块结构 - `GodotPathExtensions`
- `NodeExtensions`
- `SignalFluentExtensions`
- `UnRegisterExtension`
```mermaid 这页的重点应该是识别这些扩展各自解决什么问题,以及哪些旧文档里的“大而全能力”现在并不存在。
graph TD
A[Extensions] --> B[GodotPathExtensions]
A --> C[NodeExtensions]
A --> D[SignalFluentExtensions]
A --> E[UnRegisterExtension]
D --> F[SignalBuilder]
B --> G[路径判断扩展]
C --> H[节点生命周期]
C --> I[节点查询]
C --> J[场景树操作]
C --> K[输入控制]
C --> L[调试工具]
D --> M[信号连接系统]
E --> N[事件管理]
```
## 扩展模块详解 ## 当前公开入口
### 1. 路径扩展 (GodotPathExtensions) ### `GodotPathExtensions`
提供 Godot 虚拟路径的判断和识别功能。 这组扩展只负责判断 Godot 虚拟路径前缀:
**主要方法:** - `IsUserPath(this string path)`
- `IsResPath(this string path)`
- `IsGodotPath(this string path)`
- `IsUserPath()` - 判断是否为 `user://` 路径 它们不做文件访问,也不解析目录结构,只是用字符串前缀判断 `user://``res://`
- `IsResPath()` - 判断是否为 `res://` 路径
- `IsGodotPath()` - 判断是否为 Godot 虚拟路径
**使用示例:**
```csharp ```csharp
string savePath = "user://save.dat"; using GFramework.Godot.Extensions;
string configPath = "res://config.json";
string logPath = "C:/logs/debug.log";
if (savePath.IsUserPath()) Console.WriteLine("用户数据路径"); if ("user://save.json".IsUserPath())
if (configPath.IsResPath()) Console.WriteLine("资源路径");
if (logPath.IsGodotPath()) Console.WriteLine("Godot 虚拟路径");
else Console.WriteLine("文件系统路径");
```
### 2. 节点扩展 (NodeExtensions)
最丰富的扩展模块,提供全面的节点操作功能。
#### 节点生命周期管理
```csharp
// 安全释放节点
node.QueueFreeX(); // 延迟释放
node.FreeX(); // 立即释放
// 等待节点就绪
await node.WaitUntilReadyAsync();
// 检查节点有效性
if (node.IsValidNode()) Console.WriteLine("节点有效");
if (node.IsInvalidNode()) Console.WriteLine("节点无效");
```
#### 节点查询操作
```csharp
// 查找子节点
var sprite = node.FindChildX<Sprite2D>("Sprite");
var parent = node.GetParentX<Control>();
// 获取或创建节点
var panel = parent.GetOrCreateNode<Panel>("MainPanel");
// 遍历子节点
node.ForEachChild<Sprite2D>(sprite => {
sprite.Modulate = Colors.White;
});
```
#### 场景树操作
```csharp
// 获取根节点
var root = node.GetRootNodeX();
// 异步添加子节点
await parent.AddChildXAsync(childNode);
// 设置场景树暂停状态
node.Paused(true); // 暂停
node.Paused(false); // 恢复
```
#### 输入控制
```csharp
// 标记输入事件已处理
node.SetInputAsHandled();
// 禁用/启用输入
node.DisableInput();
node.EnableInput();
```
#### 调试工具
```csharp
// 打印节点路径
node.LogNodePath();
// 打印节点树
node.PrintTreeX();
// 安全延迟调用
node.SafeCallDeferred("UpdateUI");
```
#### 类型转换
```csharp
// 安全的类型转换
var button = node.OfType<Button>();
var sprite = childNode.OfType<Sprite2D>();
```
### 3. 信号扩展 (SignalFluentExtensions)
提供流畅的信号连接 API详见 [信号扩展](signal.md)。
**快速示例:**
```csharp
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
```
### 4. 取消注册扩展 (UnRegisterExtension)
自动管理事件监听器的生命周期。
**主要方法:**
- `UnRegisterWhenNodeExitTree()` - 节点退出场景树时自动取消注册
**使用示例:**
```csharp
var unRegister = eventManager.Subscribe<GameEvent>(OnGameEvent);
unRegister.UnRegisterWhenNodeExitTree(node);
```
## 快速参考
### 常用代码片段
#### 场景节点管理
```csharp
public class GameManager : Node
{ {
private Node _uiRoot; }
public override void _Ready() if ("res://config/gameplay.yaml".IsGodotPath())
{
}
```
### `NodeExtensions`
`NodeExtensions` 是当前扩展集合里体量最大的部分,但职责仍然比较具体,主要分成下面几类。
#### 生命周期与有效性辅助
- `QueueFreeX(this Node? node)`
- `FreeX(this Node? node)`
- `WaitUntilReadyAsync(this Node node)`
- `WaitUntilReady(this Node node, Action callback)`
- `IsValidNode(this Node? node)`
- `IsInvalidNode(this Node? node)`
这里最容易写偏的地方有两个:
- `QueueFreeX()` / `FreeX()` 会先检查 null、实例是否仍有效、是否已经进入删除队列
- `IsValidNode()` 不只要求实例还活着,还要求节点已经在 `SceneTree` 里;单纯 `new` 出来但还没挂树的节点会返回 `false`
#### 节点访问与装配辅助
- `FindChildX<T>(...)`
- `GetOrCreateNode<T>(...)`
- `AddChildXAsync(...)`
- `GetParentX<T>()`
- `GetRootNodeX()`
- `ForEachChild<T>(...)`
- `OfType<T>()`
这几组方法更偏“少量常用装配动作”,不是完整查询 DSL。
特别是 `GetOrCreateNode<T>(string path)` 的当前实现要注意:
1. 先尝试 `GetNodeOrNull<T>(path)`
2. 如果没找到,就 `new T()`
3. 把新节点直接 `AddChild(...)` 到当前节点
4. 再把 `created.Name = path`
它不会按斜杠路径逐级创建中间节点,所以不要把它当成层级化路径构建器。
#### 输入、暂停与调试辅助
- `SetInputAsHandled()`
- `Paused(bool paused = true)`
- `DisableInput()`
- `EnableInput()`
- `LogNodePath()`
- `PrintTreeX(string indent = "")`
- `SafeCallDeferred(string method)`
这些方法都很薄,基本是在现有 `Viewport` / `SceneTree` / `CallDeferred(...)` 上做便捷包装,没有额外状态机。
### `SignalFluentExtensions`
`SignalFluentExtensions` 只提供一个入口:
- `Signal(this GodotObject @object, StringName signal)`
它把目标对象和 signal 名称包装成 `SignalBuilder`。具体连接语义请看 [Godot 信号系统](./signal.md)。
### `UnRegisterExtension`
`UnRegisterExtension` 当前也只有一个公开方法:
- `UnRegisterWhenNodeExitTree(this IUnRegister unRegister, Node node)`
它做的事情很明确:把 `unRegister.UnRegister` 挂到 `node.TreeExiting` 上。这样框架侧的订阅句柄就能跟 Godot 节点生命周期对齐。
```csharp
IUnRegister subscription = eventBus.Subscribe<SettingsChangedEvent>(OnSettingsChanged);
subscription.UnRegisterWhenNodeExitTree(this);
```
它不会接管普通 Godot signal 的断开逻辑,也不会帮你推断别的释放时机。
## 最小接入路径
### 1. 节点进入树之后再做装配
如果你的节点可能在 `_Ready()` 前就被访问,先用 `WaitUntilReadyAsync()`
```csharp
using GFramework.Godot.Extensions;
using GFramework.Godot.Extensions.Signal;
using Godot;
public partial class SettingsPanel : Control
{
public override async void _Ready()
{ {
_uiRoot = GetNode<Node>("UI"); await this.WaitUntilReadyAsync();
// 创建游戏面板 var applyButton = FindChildX<Button>("ApplyButton");
var gamePanel = _uiRoot.GetOrCreateNode<Panel>("GamePanel"); applyButton?.Signal(Button.SignalName.Pressed)
.To(Callable.From(OnApplyPressed));
// 安全添加子节点
var player = new Player();
await AddChildXAsync(player);
// 查找并配置玩家
var sprite = player.FindChildX<Sprite2D>("Sprite");
if (sprite.IsValidNode()) sprite.Modulate = Colors.Red;
} }
public void Cleanup() private void OnApplyPressed()
{ {
// 安全释放所有子节点 this.SetInputAsHandled();
ForEachChild<Node>(child => child.QueueFreeX());
} }
} }
``` ```
#### UI 事件处理 ### 2. 框架订阅和节点生命周期一起收尾
当订阅句柄实现了 `IUnRegister`,可以把释放时机绑到节点退出树:
```csharp ```csharp
public class MainMenu : Control public override void _Ready()
{ {
private Button _startButton; IUnRegister subscription = _eventBus.Subscribe<SettingsChangedEvent>(OnSettingsChanged);
private Button _quitButton; subscription.UnRegisterWhenNodeExitTree(this);
public override void _Ready()
{
_startButton = FindChildX<Button>("StartButton");
_quitButton = FindChildX<Button>("QuitButton");
// 流畅的信号连接
_startButton.Signal(BaseButton.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnStartPressed)));
_quitButton.Signal(BaseButton.SignalName.Pressed)
.To(new Callable(this, nameof(OnQuitPressed)));
}
private void OnStartPressed()
{
GetTree().ChangeSceneToFile("res://scenes/game.tscn");
}
private void OnQuitPressed()
{
GetTree().Quit();
}
} }
``` ```
#### 异步场景管理 这比在多个 `_ExitTree()` / `Dispose()` 分支里手写解绑更稳定,也更符合当前扩展的职责边界。
```csharp ### 3. 只在需要时使用 signal fluent API
public class SceneManager : Node
{
public async Task<T> LoadSceneAsync<T>(string scenePath) where T : Node
{
var packedScene = GD.Load<PackedScene>(scenePath);
var instance = packedScene.Instantiate<T>();
// 等待场景加载完成
await instance.WaitUntilReadyAsync();
return instance;
}
public async Task TransitionToScene(string scenePath)
{
var newScene = await LoadSceneAsync<Node>(scenePath);
// 清理当前场景
ForEachChild<Node>(child => child.QueueFreeX());
// 加载新场景
await AddChildXAsync(newScene);
// 设置输入处理
newScene.SetInputAsHandled();
}
}
```
## 设计原则 `Signal(...)` 属于扩展集合的一部分,但它已经有独立页面。实践上可以这样分工:
### 1. 安全性 - 节点查找、ready 等待、输入处理:`NodeExtensions`
- 动态 signal 绑定:`Signal(...)`
- 框架订阅释放:`UnRegisterWhenNodeExitTree(...)`
- 路径前缀判断:`GodotPathExtensions`
- 所有节点操作都包含有效性检查 ## 当前边界
- 提供安全的类型转换方法
- 避免空引用异常
### 2. 便利性 - 当前 `NodeExtensions` 没有 `GetNodeX()``CreateSignalBuilder()` 之类旧文档里提过的 API
- 它不是 router、scene factory、UI factory 或生成器的替代层
- `GetOrCreateNode<T>()` 只会创建一个直接子节点,不会递归补整条路径
- `SafeCallDeferred(...)` 只有在 `IsValidNode()``true` 时才会调用;节点未入树时不会执行
- `UnRegisterWhenNodeExitTree(...)` 只针对实现了 `IUnRegister` 的框架订阅句柄,不会自动处理 Godot 原生 `Connect(...)`
- 协程辅助扩展在 `GFramework.Godot.Coroutine` 命名空间,不属于这组 `Extensions` 页面要覆盖的核心范围
- 流畅的 API 设计 ## 继续阅读
- 支持链式调用
- 减少样板代码
### 3. 一致性 - [Godot 运行时集成](./index.md)
- [Godot 信号系统](./signal.md)
- 统一的命名约定 - [Godot 场景系统](./scene.md)
- 一致的返回类型 - [Godot UI 系统](./ui.md)
- 预测性方法行为
### 4. 性能
- 避免不必要的节点查找
- 最小化内存分配
- 优化常见操作
## 最佳实践
### 1. 节点生命周期
```csharp
// ✅ 推荐:使用安全释放
node.QueueFreeX();
// ❌ 避免:直接释放可能导致错误
node.QueueFree();
```
### 2. 节点查询
```csharp
// ✅ 推荐:类型安全的查找
var button = node.FindChildX<Button>("Button");
// ❌ 避免:需要手动类型转换
var button = node.FindChild("Button") as Button;
```
### 3. 异步操作
```csharp
// ✅ 推荐:等待节点就绪
await child.WaitUntilReadyAsync();
// ❌ 避免:假设节点已就绪
child.DoSomething();
```
### 4. 事件管理
```csharp
// ✅ 推荐:自动清理事件
var unRegister = eventSystem.Subscribe(eventHandler);
unRegister.UnRegisterWhenNodeExitTree(node);
// ❌ 避免:手动管理事件生命周期
// 可能导致内存泄漏
```

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,40 @@
--- ---
title: Godot 日志系统 title: Godot 日志系统
description: Godot 日志系统提供了 GFramework 日志功能与 Godot 引擎控制台的完整集成 description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准,说明 Godot 日志 provider、控制台输出语义与接入边界
--- ---
# Godot 日志系统 # Godot 日志系统
## 概述 `GFramework.Godot` 当前的日志能力很收敛:它不是一套独立于 Core 的新日志框架,而是把现有 `ILogger` 调用面接到
Godot 控制台。
Godot 日志系统是 GFramework.Godot 中连接框架日志功能与 Godot 引擎控制台的核心组件。它提供了与 Godot 换句话说Godot 侧真正新增的是 provider / factory / logger 这层输出适配,而不是新的日志 API。业务代码仍然继续使用
控制台的深度集成,支持彩色输出、多级别日志记录,以及与 GFramework 日志系统的无缝对接 `LoggerFactoryResolver.Provider.CreateLogger(...)``[Log]` 生成的 `ILogger` 字段
通过 Godot 日志系统,你可以在 Godot 项目中使用统一的日志接口,日志会自动输出到 Godot 编辑器控制台,并根据日志级别使用不同的颜色和输出方式。 ## 当前公开入口
**主要特性** ### `GodotLogger`
- 与 Godot 控制台深度集成 `GodotLogger` 继承自 `AbstractLogger`,负责把日志写到 Godot 的输出 API
- 支持彩色日志输出
- 多级别日志记录Trace、Debug、Info、Warning、Error、Fatal
- 日志缓存机制
- 时间戳和格式化支持
- 异常信息记录
## 核心概念
### GodotLogger
`GodotLogger` 是 Godot 平台的日志记录器实现,继承自 `AbstractLogger`
```csharp ```csharp
public sealed class GodotLogger : AbstractLogger public sealed class GodotLogger(
{ string? name = null,
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info); LogLevel minLevel = LogLevel.Info)
protected override void Write(LogLevel level, string message, Exception? exception); : AbstractLogger(name ?? RootLoggerName, minLevel)
}
``` ```
### GodotLoggerFactory 当前实现里的几个关键语义:
`GodotLoggerFactory` 用于创建 Godot 日志记录器实例: - 时间戳使用 `DateTime.UtcNow`
- 输出前缀格式是 `[yyyy-MM-dd HH:mm:ss.fff] LEVEL [LoggerName]`
- `exception` 不会被单独结构化处理,而是直接追加到消息后面
- `Trace` / `Debug``GD.PrintRich(...)`
- `Info` / `Warning` / `Error` / `Fatal` 分别走 Godot 自身的普通、警告和错误输出通道
### `GodotLoggerFactory`
`GodotLoggerFactory` 只负责按名称和最小级别创建 `GodotLogger`
```csharp ```csharp
public class GodotLoggerFactory : ILoggerFactory public class GodotLoggerFactory : ILoggerFactory
@ -46,9 +43,11 @@ public class GodotLoggerFactory : ILoggerFactory
} }
``` ```
### GodotLoggerFactoryProvider 它本身不做缓存,也不额外增加过滤规则。
`GodotLoggerFactoryProvider` 提供日志工厂实例,并支持日志缓存: ### `GodotLoggerFactoryProvider`
`GodotLoggerFactoryProvider` 是当前最常用的接入点:
```csharp ```csharp
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
@ -58,571 +57,144 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
} }
``` ```
## 基本用法 它内部用 `CachedLoggerFactory` 包装 `GodotLoggerFactory`。缓存 key 由 `name``MinLevel` 共同组成,所以:
### 配置 Godot 日志系统 - 同名、同 `MinLevel` 的 logger 会复用实例
- 调整 `MinLevel` 后,新创建的 logger 会走新的缓存 key
- 已经持有的旧 logger 不会被原地改写
在架构初始化时配置日志提供程序: ## 最小接入路径
### 1. 在 `ArchitectureConfiguration` 中挂上 Godot provider
当前仓库里更稳的接法,不是到处直接改全局 `LoggerFactoryResolver.Provider`,而是在架构配置里显式提供
`LoggerProperties.LoggerFactoryProvider``ai-libs/CoreGrid/global/GameEntryPoint.cs` 现在就是这样接的。
```csharp ```csharp
using GFramework.Godot.Architecture; using GFramework.Core.Abstractions.Environment;
using GFramework.Godot.Logging;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Properties;
using GFramework.Core.Architectures;
using GFramework.Godot.Logging;
public class GameArchitecture : AbstractArchitecture var architecture = new GameArchitecture(
{ new ArchitectureConfiguration
public static GameArchitecture Interface { get; private set; }
public GameArchitecture()
{ {
Interface = this; LoggerProperties = new LoggerProperties
// 配置 Godot 日志系统
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{ {
MinLevel = LogLevel.Debug // 设置最小日志级别 LoggerFactoryProvider = new GodotLoggerFactoryProvider
}; {
} MinLevel = LogLevel.Debug
}
}
},
environment);
protected override void InstallModules() architecture.Initialize();
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
logger.Info("游戏架构初始化开始");
RegisterModel(new PlayerModel());
RegisterSystem(new GameplaySystem());
logger.Info("游戏架构初始化完成");
}
}
``` ```
### 创建和使用日志记录器 这样做的好处是:
- 日志 provider 和架构启动配置放在同一个入口
- 不会把“Godot 控制台输出”误写成全局静态默认前提
- 和 `ArchitectureConfiguration` 默认使用 `ConsoleLoggerFactoryProvider` 的 Core 接线方式保持一致
### 2. 业务代码继续使用标准 `ILogger`
配置好 provider 之后Godot 节点、System、Model、router、factory 都继续通过统一入口拿 logger
```csharp ```csharp
using Godot;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
using Godot;
public partial class Player : CharacterBody2D public partial class SettingsPanel : Control
{ {
private ILogger _logger; private static readonly ILogger Log =
LoggerFactoryResolver.Provider.CreateLogger(nameof(SettingsPanel));
public override void _Ready() public override void _Ready()
{ {
// 创建日志记录器 Log.Info("SettingsPanel ready.");
_logger = LoggerFactoryResolver.Provider.CreateLogger("Player");
_logger.Info("玩家初始化");
_logger.Debug("玩家位置: {0}", Position);
}
public override void _Process(double delta)
{
if (_logger.IsDebugEnabled())
{
_logger.Debug("玩家速度: {0}", Velocity);
}
}
private void TakeDamage(float damage)
{
_logger.Warn("玩家受到伤害: {0}", damage);
}
private void OnError()
{
_logger.Error("玩家状态异常");
} }
} }
``` ```
### 记录不同级别的日志 如果你已经在用 `GFramework.Core.SourceGenerators`,也可以继续让 `[Log]` 生成字段。Godot provider 只改变输出落点,
不会改变 `[Log]` 的生成契约。
### 3. Scene / UI 迁移日志会自动复用同一套 provider
`GFramework.Game.Scene.Handler.LoggingTransitionHandler`
`GFramework.Game.UI.Handler.LoggingTransitionHandler` 都是普通 `ILogger` 使用者。只要当前架构挂的是
`GodotLoggerFactoryProvider`,这些迁移日志就会直接进 Godot 控制台。
```csharp ```csharp
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameSystem"); using GFramework.Game.Scene.Handler;
using GFramework.Game.UI.Handler;
// Trace - 最详细的跟踪信息(灰色) RegisterHandler(new LoggingTransitionHandler());
logger.Trace("执行函数: UpdatePlayerPosition");
// Debug - 调试信息(青色)
logger.Debug("当前帧率: {0}", Engine.GetFramesPerSecond());
// Info - 一般信息(白色)
logger.Info("游戏开始");
// Warning - 警告信息(黄色)
logger.Warn("资源加载缓慢: {0}ms", loadTime);
// Error - 错误信息(红色)
logger.Error("无法加载配置文件");
// Fatal - 致命错误(红色,使用 PushError
logger.Fatal("游戏崩溃");
``` ```
### 记录异常信息 这也说明 Godot 日志页不需要重新定义一套“Godot 专用场景日志接口”;现有 Game 运行时日志在 Godot 宿主里本来就会复用
这套 provider。
```csharp ## Godot 控制台输出语义
var logger = LoggerFactoryResolver.Provider.CreateLogger("SaveSystem");
try 当前 `GodotLogger.Write(...)` 的级别映射如下:
{
SaveGame(); | 日志级别 | Godot 输出 API | 当前行为 |
} | --- | --- | --- |
catch (Exception ex) | `Trace` | `GD.PrintRich(...)` | 使用灰色富文本输出 |
{ | `Debug` | `GD.PrintRich(...)` | 使用青色富文本输出 |
// 记录异常信息 | `Info` | `GD.Print(...)` | 普通控制台输出 |
logger.Error("保存游戏失败", ex); | `Warning` | `GD.PushWarning(...)` | 进入 Godot 警告通道 |
} | `Error` | `GD.PrintErr(...)` | 输出到错误流 |
| `Fatal` | `GD.PushError(...)` | 进入 Godot 错误通道 |
异常追加格式也来自当前实现本身:
```text
[2026-04-22 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
System.IO.IOException: ...
``` ```
## 高级用法 如果你需要 JSON formatter、rolling file、namespace 级过滤、structured sink 组合,这已经超出
`GFramework.Godot.Logging` 当前职责,应该回到 [Core 日志系统](../core/logging.md) 设计 provider 组合。
### 在 System 中使用日志 ## 什么时候用手写 logger什么时候用 `[Log]`
```csharp - 手写 `LoggerFactoryResolver.Provider.CreateLogger(...)`
using GFramework.Core.System; - 少量入口类
using GFramework.Core.Logging; - 需要自己控制字段名、静态/实例生命周期
using GFramework.Core.Abstractions.Logging; - 想明确看到 logger 初始化位置
- 用 `[Log]`
- Godot 节点、controller、system 上有大量重复 logger 字段样板
- 你已经引用 `GFramework.Core.SourceGenerators`
- 想把 logger 字段生成交给编译期
public class CombatSystem : AbstractSystem 这里的边界要分清:
{
private ILogger _logger;
protected override void OnInit() - Godot provider来自 `GFramework.Godot`
{ - `[Log]` 生成器:来自 `GFramework.Core.SourceGenerators`
_logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
_logger.Info("战斗系统初始化完成");
}
public void ProcessCombat(Entity attacker, Entity target, float damage) 它们是可组合关系,不是上下位替代关系。
{
_logger.Debug("战斗处理: {0} 攻击 {1}, 伤害: {2}",
attacker.Name, target.Name, damage);
if (damage > 100) ## 当前边界
{
_logger.Warn("高伤害攻击: {0}", damage);
}
}
protected override void OnDestroy() - 当前推荐接法是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;直接赋值
{ `LoggerFactoryResolver.Provider` 仍然可用,但不该再写成默认采用路径
_logger.Info("战斗系统已销毁"); - `GFramework.Godot.Logging` 只解决 Godot 控制台输出不提供文件落盘、JSON formatter、异步 appender 或按 namespace
} 的复杂过滤
} - `GodotLogger` 只改变输出方式,不改变 `ILogger` 接口本身;业务代码不需要切换到 Godot 专用日志 API
``` - `[Log]``[ContextAware]` 这类字段注入能力不属于 `GFramework.Godot.Logging`
- Scene / UI 的 `LoggingTransitionHandler` 位于 `GFramework.Game`Godot 侧只是通过 provider 让它们输出到 Godot 控制台
- 当前 `GodotLogger` 使用的是 UTC 时间戳;如果项目需要本地时区展示,需要自定义 provider / logger而不是假定当前实现会自动转换
### 在 Model 中使用日志 ## 继续阅读
```csharp - [Core 日志系统](../core/logging.md)
using GFramework.Core.Model; - [日志生成器](../source-generators/logging-generator.md)
using GFramework.Core.Logging; - [Godot 运行时集成](./index.md)
using GFramework.Core.Abstractions.Logging; - [Godot 场景系统](./scene.md)
- [Godot UI 系统](./ui.md)
public class PlayerModel : AbstractModel
{
private ILogger _logger;
private int _health;
protected override void OnInit()
{
_logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel");
_logger.Info("玩家模型初始化");
_health = 100;
}
public void SetHealth(int value)
{
var oldHealth = _health;
_health = value;
_logger.Debug("玩家生命值变化: {0} -> {1}", oldHealth, _health);
if (_health <= 0)
{
_logger.Warn("玩家生命值归零");
}
}
}
```
### 条件日志记录
```csharp
var logger = LoggerFactoryResolver.Provider.CreateLogger("PerformanceMonitor");
// 检查日志级别是否启用,避免不必要的字符串格式化
if (logger.IsDebugEnabled())
{
var stats = CalculateComplexStats(); // 耗时操作
logger.Debug("性能统计: {0}", stats);
}
// 简化写法
if (logger.IsTraceEnabled())
{
logger.Trace("详细的执行流程信息");
}
```
### 分类日志记录
```csharp
// 为不同模块创建独立的日志记录器
var networkLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
var databaseLogger = LoggerFactoryResolver.Provider.CreateLogger("Database");
var aiLogger = LoggerFactoryResolver.Provider.CreateLogger("AI");
networkLogger.Info("连接到服务器");
databaseLogger.Debug("查询用户数据");
aiLogger.Trace("AI 决策树遍历");
```
### 自定义日志级别
```csharp
// 在开发环境使用 Debug 级别
#if DEBUG
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug
};
#else
// 在生产环境使用 Info 级别
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Info
};
#endif
```
### 在 Godot 模块中使用日志
```csharp
using GFramework.Godot.Architecture;
using GFramework.Core.Logging;
using GFramework.Core.Abstractions.Logging;
using Godot;
public class SceneModule : AbstractGodotModule
{
private ILogger _logger;
private Node _sceneRoot;
public override Node Node => _sceneRoot;
public SceneModule()
{
_sceneRoot = new Node { Name = "SceneRoot" };
_logger = LoggerFactoryResolver.Provider.CreateLogger("SceneModule");
}
public override void Install(IArchitecture architecture)
{
_logger.Info("场景模块安装开始");
// 安装场景系统
var sceneSystem = new SceneSystem();
architecture.RegisterSystem<ISceneSystem>(sceneSystem);
_logger.Info("场景模块安装完成");
}
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
_logger.Debug("场景模块阶段: {0}", phase);
if (phase == ArchitecturePhase.Ready)
{
_logger.Info("场景模块已就绪");
}
}
public override void OnDetach()
{
_logger.Info("场景模块已分离");
_sceneRoot?.QueueFree();
}
}
```
## 日志输出格式
### 输出格式说明
Godot 日志系统使用以下格式输出日志:
```
[时间戳] 日志级别 [日志器名称] 日志消息
```
**示例输出**
```
[2025-01-09 10:30:45.123] INFO [GameArchitecture] 游戏架构初始化开始
[2025-01-09 10:30:45.456] DEBUG [Player] 玩家位置: (100, 200)
[2025-01-09 10:30:46.789] WARNING [CombatSystem] 高伤害攻击: 150
[2025-01-09 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
```
### 日志级别与 Godot 输出方法
| 日志级别 | Godot 方法 | 颜色 | 说明 |
|-------------|------------------|----|----------|
| **Trace** | `GD.PrintRich` | 灰色 | 最详细的跟踪信息 |
| **Debug** | `GD.PrintRich` | 青色 | 调试信息 |
| **Info** | `GD.Print` | 白色 | 一般信息 |
| **Warning** | `GD.PushWarning` | 黄色 | 警告信息 |
| **Error** | `GD.PrintErr` | 红色 | 错误信息 |
| **Fatal** | `GD.PushError` | 红色 | 致命错误 |
### 异常信息格式
当记录异常时,异常信息会附加到日志消息后:
```
[2025-01-09 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
System.IO.IOException: 文件访问被拒绝
at SaveSystem.SaveGame() in SaveSystem.cs:line 42
```
## 最佳实践
1. **在架构初始化时配置日志系统**
```csharp
public GameArchitecture()
{
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug
};
}
```
2. **为每个类创建独立的日志记录器**
```csharp
private ILogger _logger;
public override void _Ready()
{
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
}
```
3. **使用合适的日志级别**
- `Trace`:详细的执行流程,仅在深度调试时使用
- `Debug`:调试信息,开发阶段使用
- `Info`:重要的业务流程和状态变化
- `Warning`:潜在问题但不影响功能
- `Error`:错误但程序可以继续运行
- `Fatal`:严重错误,程序无法继续
4. **检查日志级别避免性能损失**
```csharp
if (_logger.IsDebugEnabled())
{
var expensiveData = CalculateExpensiveData();
_logger.Debug("数据: {0}", expensiveData);
}
```
5. **提供有意义的上下文信息**
```csharp
// ✗ 不好
logger.Error("错误");
// ✓ 好
logger.Error("加载场景失败: SceneKey={0}, Path={1}", sceneKey, scenePath);
```
6. **记录异常时提供上下文**
```csharp
try
{
LoadScene(sceneKey);
}
catch (Exception ex)
{
logger.Error($"加载场景失败: {sceneKey}", ex);
}
```
7. **使用分类日志记录器**
```csharp
var networkLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
var aiLogger = LoggerFactoryResolver.Provider.CreateLogger("AI");
```
8. **在生命周期方法中记录关键事件**
```csharp
protected override void OnInit()
{
_logger.Info("系统初始化完成");
}
protected override void OnDestroy()
{
_logger.Info("系统已销毁");
}
```
## 性能考虑
1. **日志缓存**
- `GodotLoggerFactoryProvider` 使用 `CachedLoggerFactory` 缓存日志记录器实例
- 相同名称和级别的日志记录器会被复用
2. **级别检查**
- 日志方法会自动检查日志级别
- 低于最小级别的日志不会被处理
3. **字符串格式化**
- 使用参数化日志避免不必要的字符串拼接
```csharp
// ✗ 不好 - 总是执行字符串拼接
logger.Debug("位置: " + position.ToString());
// ✓ 好 - 只在 Debug 启用时格式化
logger.Debug("位置: {0}", position);
```
4. **条件日志**
- 对于耗时的数据计算,先检查日志级别
```csharp
if (logger.IsDebugEnabled())
{
var stats = CalculateComplexStats();
logger.Debug("统计: {0}", stats);
}
```
## 常见问题
### 问题:如何配置 Godot 日志系统?
**解答**
在架构构造函数中配置日志提供程序:
```csharp
public GameArchitecture()
{
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Debug
};
}
```
### 问题:日志没有输出到 Godot 控制台?
**解答**
检查以下几点:
1. 确认已配置 `GodotLoggerFactoryProvider`
2. 检查日志级别是否低于最小级别
3. 确认使用了正确的日志记录器
```csharp
// 确认配置
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Trace // 设置为最低级别测试
};
// 创建日志记录器
var logger = LoggerFactoryResolver.Provider.CreateLogger("Test");
logger.Info("测试日志"); // 应该能看到输出
```
### 问题:如何在不同环境使用不同的日志级别?
**解答**
使用条件编译或环境检测:
```csharp
public GameArchitecture()
{
var minLevel = OS.IsDebugBuild() ? LogLevel.Debug : LogLevel.Info;
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = minLevel
};
}
```
### 问题:如何禁用某个模块的日志?
**解答**
为该模块创建一个高级别的日志记录器:
```csharp
// 只记录 Error 及以上级别
var logger = new GodotLogger("VerboseModule", LogLevel.Error);
```
### 问题:日志输出影响性能怎么办?
**解答**
1. 提高最小日志级别
2. 使用条件日志
3. 避免在高频调用的方法中记录日志
```csharp
// 提高日志级别
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
{
MinLevel = LogLevel.Warning // 只记录警告及以上
};
// 使用条件日志
if (_logger.IsDebugEnabled())
{
_logger.Debug("高频数据: {0}", data);
}
// 避免在 _Process 中频繁记录
public override void _Process(double delta)
{
// ✗ 不好 - 每帧都记录
// _logger.Debug("帧更新");
// ✓ 好 - 只在特定条件下记录
if (someErrorCondition)
{
_logger.Error("检测到错误");
}
}
```
### 问题:如何记录结构化日志?
**解答**
使用参数化日志或 `IStructuredLogger` 接口:
```csharp
// 参数化日志
logger.Info("玩家登录: UserId={0}, UserName={1}, Level={2}",
userId, userName, level);
// 使用结构化日志(如果实现了 IStructuredLogger
if (logger is IStructuredLogger structuredLogger)
{
structuredLogger.Log(LogLevel.Info, "玩家登录",
("UserId", userId),
("UserName", userName),
("Level", level));
}
```
## 相关文档
- [核心日志系统](/zh-CN/core/logging) - GFramework 核心日志功能
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构系统
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
- [最佳实践](/zh-CN/best-practices/architecture-patterns) - 架构最佳实践

View File

@ -1,583 +1,321 @@
--- ---
title: Godot 场景系统 title: Godot 场景系统
description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成 description: 以当前 GFramework.Godot 源码、Game 场景契约与 CoreGrid 接线为准,说明 PackedScene 场景工厂、行为包装和最小接入路径
--- ---
# Godot 场景系统 # Godot 场景系统
## 概述 `GFramework.Godot` 在场景这一层负责的是 Godot runtime 适配,而不是再提供一个 Godot 专属 router。
Godot 场景系统是 GFramework.Godot 中连接框架场景管理与 Godot 场景树的核心组件。它提供了场景行为封装、场景工厂、场景注册表等功能,让你可以在 当前真正参与场景接线的核心类型是:
Godot 项目中使用 GFramework 的场景管理系统。
通过 Godot 场景系统,你可以使用 GFramework 的场景路由、生命周期管理等功能,同时保持与 Godot 场景系统的完美兼容。 - `IGodotSceneRegistry` / `GodotSceneRegistry`
- `GodotSceneFactory`
- `SceneBehaviorFactory`
- `SceneBehaviorBase<T>` 及其 `Node2D` / `Node3D` / `Control` / `Generic` 实现
- 项目侧实现的 `ISceneRoot`
- 项目侧继承 `SceneRouterBase` 的 router
**主要特性** 也就是说Godot 集成页的重点不是“再造一套场景导航 API”而是把 `PackedScene``Node``GFramework.Game`
`ISceneRouter` / `ISceneBehavior` 契约接起来。
- 场景行为封装SceneBehavior ## 当前公开入口
- 场景工厂和注册表
- 与 Godot PackedScene 集成
- 多种场景行为类型Node2D、Node3D、Control
- 场景生命周期管理
- 场景根节点管理
## 核心概念 ### `IGodotSceneRegistry`
### 场景行为 Godot 侧的场景资源表,底层是 `IAssetRegistry<PackedScene>`。它只负责:
`SceneBehaviorBase<T>` 封装了 Godot 节点的场景行为: - `sceneKey -> PackedScene` 映射
- 让 `GodotSceneFactory` 能按 key 实例化场景
框架当前不会自动扫描项目里的 `.tscn` 文件并填充 registry。
### `GodotSceneFactory`
`GodotSceneFactory.Create(string sceneKey)` 的当前行为很明确:
1. 从 `IGodotSceneRegistry` 取出 `PackedScene`
2. 调用 `Instantiate()`
3. 如果节点实现了 `ISceneBehaviorProvider`,优先返回 `provider.GetScene()`
4. 否则回退到 `SceneBehaviorFactory.Create(node, sceneKey)`
这和旧文档里“必须有 Godot 专属 router / 专属 scene provider 才能工作”的说法不同。当前源码允许两条路径:
- 显式 provider项目自己决定行为对象
- 自动包装:按节点类型回退到默认 behavior
### `SceneBehaviorBase<T>`
`SceneBehaviorBase<T>` 是当前 Godot 场景行为包装基类。它把 `ISceneBehavior` 的生命周期接到 `Node` 上:
- `OnLoadAsync`
- `OnEnterAsync`
- `OnPauseAsync`
- `OnResumeAsync`
- `OnExitAsync`
- `OnUnloadAsync`
如果 owner 还实现了 `IScene`,这些阶段会继续转发到业务节点;如果没有实现 `IScene`,默认 behavior 仍会处理 Godot 节点的
process 开关和 `QueueFreeX()` 释放。
### `SceneBehaviorFactory`
自动包装的选择规则来自当前实现:
- `Node2D` -> `Node2DSceneBehavior`
- `Node3D` -> `Node3DSceneBehavior`
- `Control` -> `ControlSceneBehavior`
- 其他 `Node` -> `GenericSceneBehavior`
这意味着 Godot runtime 确实能“自动给节点补一个 behavior”但它不会替你补项目侧 router、root 或 registry。
## 最小接入路径
推荐按下面顺序接入。
### 1. 继续在项目层保留自己的 router
`GFramework.Godot` 当前没有 `GodotSceneRouter` 类型。消费者项目的实际做法,是在项目层继承
`GFramework.Game.Scene.SceneRouterBase`
`ai-libs/CoreGrid` 的 router 就是这样:
```csharp ```csharp
public abstract class SceneBehaviorBase<T> : ISceneBehavior using global::CoreGrid.global;
where T : Node using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;
namespace CoreGrid.scripts.core.scene;
public partial class SceneRouter : SceneRouterBase
{ {
protected readonly T Owner; [GetUtility] private IGodotSceneRegistry _sceneRegistry = null!;
public string Key { get; }
public IScene Scene { get; }
}
```
### 场景工厂 public Node? SceneRoot => Root as Node;
`GodotSceneFactory` 负责创建场景实例: protected override void RegisterHandlers()
```csharp
public class GodotSceneFactory : ISceneFactory
{
public ISceneBehavior Create(string sceneKey);
}
```
### 场景注册表
`IGodotSceneRegistry` 管理场景资源:
```csharp
public interface IGodotSceneRegistry
{
void Register(string key, PackedScene scene);
PackedScene Get(string key);
}
```
## 基本用法
### 创建场景脚本
```csharp
using Godot;
using GFramework.Game.Abstractions.Scene;
public partial class MainMenuScene : Control, IScene
{
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{ {
GD.Print("加载主菜单资源"); __InjectContextBindings_Generated();
await Task.CompletedTask; RegisterHandler(new LoggingTransitionHandler());
} RegisterAroundHandler(
new SceneTransitionAnimationHandler(() => SceneTransitionManager.Instance!, _sceneRegistry.GetAll()));
public async ValueTask OnEnterAsync()
{
GD.Print("进入主菜单");
Show();
await Task.CompletedTask;
}
public async ValueTask OnPauseAsync()
{
GD.Print("暂停主菜单");
await Task.CompletedTask;
}
public async ValueTask OnResumeAsync()
{
GD.Print("恢复主菜单");
await Task.CompletedTask;
}
public async ValueTask OnExitAsync()
{
GD.Print("退出主菜单");
Hide();
await Task.CompletedTask;
}
public async ValueTask OnUnloadAsync()
{
GD.Print("卸载主菜单资源");
await Task.CompletedTask;
} }
} }
``` ```
### 注册场景 这里可以看到Godot 适配点在 factory / registry / root / transition handler 上,而 router 仍然是项目类。
### 2. 注册 `IGodotSceneRegistry``ISceneFactory`
最小 wiring 需要把 registry 和 factory 装进架构:
```csharp ```csharp
using GFramework.Godot.Scene;
using Godot;
public class GameSceneRegistry : GodotSceneRegistry
{
publieneRegistry()
{
// 注册场景资源
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
Register("Pause", GD.Load<PackedScene>("res://scenes/Pause.tscn"));
}
}
```
### 设置场景系统
```csharp
using GFramework.Godot.Architecture;
using GFramework.Godot.Scene;
public class GameArchitecture : AbstractArchitecture
{
protected override void InstallModules()
{
// 注册场景注册表
var sceneRegistry = new GameSceneRegistry();
RegisterUtility<IGodotSceneRegistry>(sceneRegistry);
// 注册场景工厂
var sceneFactory = new GodotSceneFactory();
RegisterUtility<ISceneFactory>(sceneFactory);
// 注册场景路由
var sceneRouter = new GodotSceneRouter();
RegisterSystem<ISceneRouter>(sceneRouter);
}
}
```
### 使用场景路由
```csharp
using Godot;
using GFramework.Godot.Extensions;
public partial class GameController : Node
{
public override void _Ready()
{
// 切换到主菜单
SwitchToMainMenu();
}
private async void SwitchToMainMenu()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.ReplaceAsync("MainMenu");
}
private async void StartGame()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.ReplaceAsync("Gameplay");
}
private async void ShowPause()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.PushAsync("Pause");
}
}
```
## 高级用法
### 使用场景行为提供者
```csharp
using Godot;
using GFramework.Game.Abstractions.Scene; using GFramework.Game.Abstractions.Scene;
using GFramework.Godot.Scene; using GFramework.Godot.Scene;
using Godot;
public partial class GameplayScene : Node2D, ISceneBehaviorProvider public sealed class GameSceneRegistry : GodotSceneRegistry
{ {
private GameplaySceneBehavior _behavior; public GameSceneRegistry()
{
Register(nameof(SceneKey.MainMenu), GD.Load<PackedScene>("res://scenes/main_menu.tscn"));
Register(nameof(SceneKey.Gameplay), GD.Load<PackedScene>("res://scenes/gameplay.tscn"));
}
}
architecture.RegisterUtility<IGodotSceneRegistry>(new GameSceneRegistry());
architecture.RegisterUtility<ISceneFactory>(new GodotSceneFactory());
architecture.RegisterSystem(new SceneRouter());
```
项目用什么 key 类型、资源目录或配置表都可以,但最终要能落到 `sceneKey -> PackedScene`
### 3. 提供 `ISceneRoot`
`SceneRouterBase` 只负责切换编排,真正把场景节点挂到 Godot 场景树的是项目自己的 `ISceneRoot`
CoreGrid 的 `SceneRoot` 当前做了两件关键事:
- 在 `_Ready()` 时调用 `_sceneRouter.BindRoot(this)`
- 在 `AddScene` / `RemoveScene` 里把 `scene.Original` 当作 `Node` 挂入或移出树
最小形态可以写成:
```csharp
public sealed class SceneRoot : Node2D, ISceneRoot
{
[GetSystem] private ISceneRouter _sceneRouter = null!;
public override void _Ready() public override void _Ready()
{ {
_behavior = new GameplaySceneBehavior(this, "Gameplay"); __InjectContextBindings_Generated();
_sceneRouter.BindRoot(this);
} }
public void AddScene(ISceneBehavior scene)
{
if (scene.Original is not Node node)
throw new InvalidOperationException("SceneBehavior must inherit Godot Node.");
if (node.GetParent() == null)
AddChild(node);
}
public void RemoveScene(ISceneBehavior scene)
{
if (scene.Original is Node node && node.GetParent() == this)
RemoveChild(node);
}
}
```
### 4. 让场景节点提供 behavior
当前有两种可行方式。
#### 方式 A实现 `ISceneBehaviorProvider`
如果你想显式控制 behavior 类型,直接实现 `GetScene()`
```csharp
public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene
{
private ISceneBehavior? _scene;
public ISceneBehavior GetScene() public ISceneBehavior GetScene()
{ {
return _behavior; return _scene ??= SceneBehaviorFactory.Create(this, nameof(SceneKey.Gameplay));
}
}
// 自定义场景行为
public class GameplaySceneBehavior : Node2DSceneBehavior
{
public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key)
{
} }
protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param) public ValueTask OnLoadAsync(ISceneEnterParam? param)
{ {
GD.Print("加载游戏场景"); return ValueTask.CompletedTask;
// 加载游戏资源
await Task.CompletedTask;
} }
protected override async ValueTask OnEnterInternalAsync() public ValueTask OnEnterAsync()
{ {
GD.Print("进入游戏场景"); return ValueTask.CompletedTask;
Owner.Show(); }
await Task.CompletedTask;
public ValueTask OnPauseAsync()
{
return ValueTask.CompletedTask;
}
public ValueTask OnResumeAsync()
{
return ValueTask.CompletedTask;
}
public ValueTask OnExitAsync()
{
return ValueTask.CompletedTask;
}
public ValueTask OnUnloadAsync()
{
return ValueTask.CompletedTask;
} }
} }
``` ```
### 不同类型的场景行为 #### 方式 B`[AutoScene]` 让生成器补样板
当前更贴近真实消费者 wiring 的方式,是让 `GFramework.Godot.SourceGenerators` 生成 `SceneKeyStr``GetScene()`
```csharp ```csharp
// Node2D 场景
public class Node2DSceneBehavior : SceneBehaviorBase<Node2D>
{
public Node2DSceneBehavior(Node2D owner, string key) : base(owner, key)
{
}
}
// Node3D 场景
public class Node3DSceneBehavior : SceneBehaviorBase<Node3D>
{
public Node3DSceneBehavior(Node3D owner, string key) : base(owner, key)
{
}
}
// Control 场景UI
public class ControlSceneBehavior : SceneBehaviorBase<Control>
{
public ControlSceneBehavior(Control owner, string key) : base(owner, key)
{
}
}
```
### 场景根节点管理
```csharp
using Godot;
using GFramework.Godot.Scene;
public partial class SceneRoot : Node, ISceneRoot
{
private Node _currentSceneNode;
public void AttachScene(Node sceneNode)
{
// 移除旧场景
if (_currentSceneNode != null)
{
RemoveChild(_currentSceneNode);
_currentSceneNode.QueueFree();
}
// 添加新场景
_currentSceneNode = sceneNode;
AddChild(_currentSceneNode);
}
public void DetachScene(Node sceneNode)
{
if (_currentSceneNode == sceneNode)
{
RemoveChild(_currentSceneNode);
_currentSceneNode = null;
}
}
}
```
### 场景参数传递
```csharp
// 定义场景参数
public class GameplayEnterParam : ISceneEnterParam
{
public int Level { get; set; }
public string Difficulty { get; set; }
}
// 在场景中接收参数
public partial class GameplayScene : Node2D, IScene
{
private int _level;
private string _difficulty;
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
if (param is GameplayEnterParam gameplayParam)
{
_level = gameplayParam.Level;
_difficulty = gameplayParam.Difficulty;
GD.Print($"加载关卡 {_level},难度: {_difficulty}");
}
await Task.CompletedTask;
}
// ... 其他生命周期方法
}
// 切换场景时传递参数
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
{
Level = 1,
Difficulty = "Normal"
});
```
### 场景预加载
```csharp
public partial class LoadingScene : Control
{
public override async void _Ready()
{
// 预加载下一个场景
await PreloadNextScene();
// 切换到预加载的场景
var sceneRouter = this.GetSystem<ISceneRouter>();
await sceneRouter.ReplaceAsync("Gameplay");
}
private async Task PreloadNextScene()
{
var sceneFactory = this.GetUtility<ISceneFactory>();
var sceneBehavior = sceneFactory.Create("Gameplay");
// 预加载场景资源
await sceneBehavior.LoadAsync(null);
GD.Print("场景预加载完成");
}
}
```
### 场景转换动画
```csharp
using Godot;
using GFramework.Game.Abstractions.Scene; using GFramework.Game.Abstractions.Scene;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
public class FadeTransitionHandler : ISceneTransitionHandler [AutoScene(nameof(SceneKey.Gameplay))]
public partial class GameplayRoot : Node2D, ISceneBehaviorProvider, IScene
{ {
private ColorRect _fadeRect; public ValueTask OnLoadAsync(ISceneEnterParam? param)
public FadeTransitionHandler(ColorRect fadeRect)
{ {
_fadeRect = fadeRect; return ValueTask.CompletedTask;
} }
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event) public ValueTask OnEnterAsync()
{ {
// 淡出动画 return ValueTask.CompletedTask;
var tween = _fadeRect.CreateTween();
tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f);
await tween.ToSignal(tween, Tween.SignalName.Finished);
} }
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event) public ValueTask OnPauseAsync()
{ {
// 淡入动画 return ValueTask.CompletedTask;
var tween = _fadeRect.CreateTween();
tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f);
await tween.ToSignal(tween, Tween.SignalName.Finished);
} }
// ... 其他方法 public ValueTask OnResumeAsync()
}
```
### 场景间通信
```csharp
// 通过事件通信
public partial class GameplayScene : Node2D, IScene
{
public async ValueTask OnEnterAsync()
{ {
// 发送场景进入事件 return ValueTask.CompletedTask;
this.SendEvent(new GameplaySceneEnteredEvent());
await Task.CompletedTask;
}
}
// 在其他地方监听
public partial class HUD : Control
{
public override void _Ready()
{
this.RegisterEvent<GameplaySceneEnteredEvent>(OnGameplayEntered);
} }
private void OnGameplayEntered(GameplaySceneEnteredEvent evt) public ValueTask OnExitAsync()
{ {
GD.Print("游戏场景已进入,显示 HUD"); return ValueTask.CompletedTask;
Show(); }
public ValueTask OnUnloadAsync()
{
return ValueTask.CompletedTask;
} }
} }
``` ```
## 最佳实践 生成器当前会补出与源码一致的 `GetScene()`
1. **场景脚本实现 IScene 接口**:获得完整的生命周期管理
```csharp
✓ public partial class MyScene : Node2D, IScene { }
✗ public partial class MyScene : Node2D { } // 无生命周期管理
```
2. **使用场景注册表管理场景资源**:集中管理所有场景
```csharp
public class GameSceneRegistry : GodotSceneRegistry
{
public GameSceneRegistry()
{
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
}
}
```
3. **在 OnLoadAsync 中加载资源**:避免场景切换卡顿
```csharp
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
// 异步加载资源
await LoadTexturesAsync();
await LoadAudioAsync();
}
```
4. **使用场景根节点管理场景树**:保持场景树结构清晰
```csharp
// 创建场景根节点
var sceneRoot = new Node { Name = "SceneRoot" };
AddChild(sceneRoot);
// 绑定到场景路由
sceneRouter.BindRoot(sceneRoot);
```
5. **正确清理场景资源**:在 OnUnloadAsync 中释放资源
```csharp
public async ValueTask OnUnloadAsync()
{
// 释放资源
_texture?.Dispose();
_audioStream?.Dispose();
await Task.CompletedTask;
}
```
6. **使用场景参数传递数据**:避免使用全局变量
```csharp
✓ await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam { Level = 1 });
✗ GlobalData.CurrentLevel = 1; // 避免全局状态
```
## 常见问题
### 问题:如何在 Godot 场景中使用 GFramework
**解答**
场景脚本实现 `IScene` 接口:
```csharp ```csharp
public partial class MyScene : Node2D, IScene public ISceneBehavior GetScene()
{ {
public async ValueTask OnLoadAsync(ISceneEnterParam? param) { } return __autoSceneBehavior_Generated ??= SceneBehaviorFactory.Create(this, SceneKeyStr);
public async ValueTask OnEnterAsync() { }
// ... 实现其他方法
} }
``` ```
### 问题:场景切换时节点如何管理? 要注意两点:
**解答** - `[AutoScene]` 只生成方法和 key不会替你自动补 `: ISceneBehaviorProvider`
使用场景根节点管理: - `IScene` 仍然是业务生命周期契约;不实现它时,默认 behavior 只会保留基础节点切换语义
### 5. 从业务代码发起导航
一旦 registry、factory、router、root 都装好,导航入口仍然是 `ISceneRouter`
```csharp ```csharp
// 场景路由会自动管理节点的添加和移除 await sceneRouter.ReplaceAsync(nameof(SceneKey.MainMenu));
await sceneRouter.ReplaceAsync("NewScene"); await sceneRouter.ReplaceAsync(nameof(SceneKey.Gameplay), new GameplayEnterParam());
// 旧场景节点会被移除,新场景节点会被添加 await sceneRouter.PushAsync(nameof(SceneKey.PauseMenu));
await sceneRouter.PopAsync();
``` ```
### 问题:如何实现场景预加载? ## 当前边界
**解答** ### 没有 `GodotSceneRouter`
使用场景工厂提前创建场景:
```csharp 仓库当前不存在 `GodotSceneRouter` 类型。旧文档里把它写成默认入口是失真的;实际入口仍然是项目侧继承
var sceneFactory = this.GetUtility<ISceneFactory>(); `SceneRouterBase` 的 router。
var sceneBehavior = sceneFactory.Create("NextScene");
await sceneBehavior.LoadAsync(null);
```
### 问题:场景生命周期方法的调用顺序是什么? ### 没有自动注册所有场景
**解答** 当前运行时只认识你注册进 `IGodotSceneRegistry``PackedScene`。它不会扫描目录、不会从脚本类型自动反推出注册表。
- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow` ### provider 是“优先路径”,不是“唯一路径”
- 暂停场景:`OnPause` -> `OnHide`
- 恢复场景:`OnShow` -> `OnResume`
- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync`
### 问题:如何在场景中访问架构组件? `GodotSceneFactory` 会优先使用 `ISceneBehaviorProvider`,但没有 provider 时仍会按节点类型自动包装。这个行为和 UI 系统不同;
UI 工厂当前没有同等的自动回退。
**解答** ### root 仍然是项目职责
使用扩展方法:
```csharp `ISceneRoot` 的实现决定:
public partial class MyScene : Node2D, IScene
{
public async ValueTask OnEnterAsync()
{
var playerModel = this.GetModel<PlayerModel>();
var gameSystem = this.GetSystem<GameSystem>();
await Task.CompletedTask;
}
}
```
### 问题:场景切换时如何显示加载界面? - 节点挂到哪里
- 移除时如何释放
- 是否保留额外的当前视图引用
**解答** Godot runtime 不会替项目生成统一的 root 节点。
使用场景转换处理器:
```csharp ## 继续阅读
public class LoadingScreenHandler : ISceneTransitionHandler
{
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
{
// 显示加载界面
ShowLoadingScreen();
await Task.CompletedTask;
}
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event) 1. [Godot 运行时集成](./index.md)
{ 2. [Godot 架构集成](./architecture.md)
// 隐藏加载界面 3. [Game 场景系统](../game/scene.md)
HideLoadingScreen(); 4. [AutoScene 生成器](../source-generators/auto-scene-generator.md)
await Task.CompletedTask;
}
}
```
## 相关文档
- [场景系统](/zh-CN/game/scene) - 核心场景管理
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法

View File

@ -1,420 +1,158 @@
# 信号连接系统 (Signal Connection System) ---
title: Godot 信号系统
description: 以当前 GFramework.Godot 源码与 CoreGrid 的动态绑定用法为准,说明 Signal(...) fluent API、SignalBuilder 行为与接入边界。
---
## 概述 # Godot 信号系统
信号连接系统是 Godot 扩展方法模块中的一个专门子模块,提供流畅、类型安全的 Godot 信号连接 API。该系统采用构建器模式Builder `GFramework.Godot` 当前提供的信号能力很收敛:它不是另一套事件系统,也不是自动生成绑定代码的入口,而是对
Pattern和流畅接口设计Fluent Interface大大简化了信号订阅代码提高了代码的可读性和可维护性 `GodotObject.Connect(...)` 的一层 fluent 包装
## 核心类 当前真正公开的入口只有两个:
### SignalBuilder - `SignalFluentExtensions.Signal(...)`
- `SignalBuilder`
信号连接构建器,负责构建和执行信号连接操作。 如果你需要的是场景节点字段注入和静态 signal 自动绑订,请看
`GFramework.Godot.SourceGenerators``[GetNode]``[BindNodeSignal]`,不要把它们和这里的运行时 fluent API 混成同一层。
**特性:** ## 当前公开入口
- 支持链式调用 ### `Signal(...)`
- 可配置连接标志
- 支持连接后立即调用
- 返回原始对象以便继续操作
### SignalFluentExtensions `Signal(...)` 是定义在 `GodotObject` 上的扩展方法:
`GodotObject` 提供信号连接扩展方法,创建 `SignalBuilder` 实例。
## 架构设计
```mermaid
graph TD
A[GodotObject] --> B[SignalFluentExtensions]
B --> C[Signal Extension Method]
C --> D[SignalBuilder]
D --> E[WithFlags]
D --> F[To]
D --> G[ToAndCall]
D --> H[End]
F --> I[Connect Signal]
G --> J[Connect + Call]
H --> K[Return GodotObject]
L[ConnectFlags] --> E
M[Callable] --> F
M --> G
```
## 使用示例
### 基本信号连接
```csharp
// 基本连接
button.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnButtonPressed)));
// 带连接标志
timer.Signal(Timer.SignalName.Timeout)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnTimerTimeout)));
```
### 连接并立即调用
```csharp
// 连接信号并立即调用一次
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
// 连接带参数的信号并立即调用
area2D.Signal(Area2D.SignalName.BodyEntered)
.ToAndCall(new Callable(this, nameof(OnBodyEntered)), new Variant[] { node });
```
### 复杂的连接链
```csharp
// 设置连接标志并连接
player.Signal(Player.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnHealthChanged)));
// 连接多个信号
var button = GetNode<Button>("StartButton");
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnGameStarted)));
```
## API 详细说明
### SignalBuilder 构造函数
```csharp
public SignalBuilder(GodotObject target, StringName signal)
```
**参数:**
- `target` - 要连接信号的 Godot 对象
- `signal` - 要连接的信号名称
### SignalBuilder 方法
#### WithFlags
设置连接标志。
```csharp
public SignalBuilder WithFlags(GodotObject.ConnectFlags flags)
```
**参数:**
- `flags` - Godot 连接标志枚举值
**常用的连接标志:**
- `ConnectFlags.Deferred` - 延迟调用
- `ConnectFlags.OneShot` - 一次性连接
- `ConnectFlags.ConnectPersisted` - 连接持久化
- `ConnectFlags.ReferenceCounted` - 引用计数
#### To
连接信号到指定的可调用对象。
```csharp
public SignalBuilder To(Callable callable, GodotObject.ConnectFlags? flags = null)
```
**参数:**
- `callable` - 要连接的可调用对象
- `flags` - 可选的连接标志,覆盖之前设置的标志
#### ToAndCall
连接信号并立即调用一次。
```csharp
public SignalBuilder ToAndCall(Callable callable, GodotObject.ConnectFlags? flags = null, params Variant[] args)
```
**参数:**
- `callable` - 要连接的可调用对象
- `flags` - 可选的连接标志
- `args` - 调用时传递的参数
#### End
显式结束构建,返回原始对象。
```csharp
public GodotObject End()
```
### SignalFluentExtensions 扩展方法
#### Signal
为 Godot 对象创建信号构建器。
```csharp ```csharp
public static SignalBuilder Signal(this GodotObject @object, StringName signal) public static SignalBuilder Signal(this GodotObject @object, StringName signal)
``` ```
**参数:** 它只做一件事:基于目标对象和 signal 名称创建一个 `SignalBuilder`。这意味着当前 fluent API 不只适用于 `Node`,也适用于
其他 Godot 对象。
- `@object` - 扩展方法的目标对象 ### `SignalBuilder`
- `signal` - 要连接的信号名称
## 实际应用场景 `SignalBuilder` 的当前行为来自运行时代码本身:
### UI 事件处理 - `WithFlags(GodotObject.ConnectFlags flags)`
- 把 flags 保存到 builder 内部,作为后续 `To(...)` / `ToAndCall(...)` 的默认连接选项
- `To(Callable callable, GodotObject.ConnectFlags? flags = null)`
- 优先使用参数传入的 flags如果没有再回退到之前 `WithFlags(...)` 保存的值
- 最终直接调用 `target.Connect(signal, callable)``target.Connect(signal, callable, (uint)flags)`
- `ToAndCall(Callable callable, GodotObject.ConnectFlags? flags = null, params Variant[] args)`
- 先执行 `To(...)`
- 再立即执行一次 `callable.Call(args)`
- `End()`
- 返回原始 `GodotObject`
- 主要用于在 fluent 语句结束后重新拿回目标对象,而不是增加新的信号语义
可以把它理解成“对原生 `Connect(...)` 做顺手的链式包装”,而不是带订阅管理、自动解绑、诊断系统的高层抽象。
## 最小接入路径
### 1. 动态绑定时直接用 `Signal(...)`
适合这类场景:
- 运行时创建的节点或弹窗
- signal 名称需要按条件选择
- 你就是想保留手写 `Callable` 的控制权
最小示例:
```csharp ```csharp
public class MainMenu : Control using GFramework.Godot.Extensions.Signal;
using Godot;
public partial class SettingsPanel : Control
{ {
public override void _Ready() public override void _Ready()
{ {
var startButton = GetNode<Button>("StartButton"); var applyButton = GetNode<Button>("%ApplyButton");
var quitButton = GetNode<Button>("QuitButton");
var settingsButton = GetNode<Button>("SettingsButton"); applyButton.Signal(Button.SignalName.Pressed)
.To(Callable.From(OnApplyPressed));
// 开始按钮 - 一次性连接并立即禁用
startButton.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnStartPressed)));
// 退出按钮
quitButton.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnQuitPressed)));
// 设置按钮 - 延迟调用避免嵌套问题
settingsButton.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnSettingsPressed)));
} }
private void OnStartPressed() private void OnApplyPressed()
{ {
GetTree().ChangeSceneToFile("res://scenes/game.tscn");
}
private void OnQuitPressed()
{
GetTree().Quit();
}
private void OnSettingsPressed()
{
// 打开设置面板
GetNode<Control>("SettingsPanel").Show();
} }
} }
``` ```
### 游戏逻辑事件 ### 2. 需要连接 flags 时,用 `WithFlags(...)`
`SignalBuilder` 不会解释 flags 的业务含义,只是把它们原样传给 Godot。
```csharp ```csharp
public class Player : CharacterBody2D button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(Callable.From(OnStartPressed));
```
如果某一次连接想覆盖默认 flags可以直接在 `To(...)` / `ToAndCall(...)` 上传第二个参数。
### 3. 只有在“连接后立即跑一次”时才用 `ToAndCall(...)`
`ToAndCall(...)` 的语义很直接:先连,再立刻调一次 handler。它适合“先补一次当前状态再继续监听变化”的场景。
```csharp
slider.Signal(Range.SignalName.ValueChanged)
.ToAndCall(Callable.From<double>(OnVolumeChanged), args: [(Variant)slider.Value]);
```
这类调用要求 handler 对“初始化时主动调用一次”是安全的;如果你的处理逻辑不是幂等的,继续用 `To(...)` 更稳妥。
### 4. 静态场景绑定优先交给 `[BindNodeSignal]`
`GFramework.Godot.SourceGenerators/README.md``ai-libs/CoreGrid` 的当前接法看,静态场景按钮、滑条、菜单项这类固定
节点,更常见的路径仍然是 `[BindNodeSignal]`
```csharp
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartPressed()
{ {
private HealthComponent _health;
private AnimationPlayer _animPlayer;
public override void _Ready()
{
_health = GetNode<HealthComponent>("HealthComponent");
_animPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
// 生命值变化 - 延迟处理避免在动画中修改状态
_health.Signal(HealthComponent.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnHealthChanged)));
// 死亡事件 - 一次性连接
_health.Signal(HealthComponent.SignalName.Died)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnDied)));
}
private void OnHealthChanged(float newHealth, float maxHealth)
{
// 更新UI或状态
UpdateHealthBar(newHealth / maxHealth);
// 播放受伤动画
if (newHealth < _health.PreviousHealth)
{
_animPlayer.Play("hurt");
}
}
private void OnDied()
{
// 播放死亡动画
_animPlayer.Play("death");
// 游戏结束
GetTree().CallDeferred(SceneTree.MethodName.Quit);
}
} }
``` ```
### 音频管理 `Signal(...)` 更常出现在这些动态或补充性绑定里:
- 对话框确认 / 取消等运行时实例
- 运行时选出的 signal 名称
- 需要临时追加监听的 dock、panel、overlay
`ai-libs/CoreGrid` 当前就有这类用法:
```csharp ```csharp
public class AudioManager : Node _quitConfirmDialog.Signal("Confirmed")
{ .To(Callable.From(OnQuitConfirmDialogConfirmed))
private AudioStreamPlayer _bgmPlayer; .End();
private AudioStreamPlayer _sfxPlayer;
public override void _Ready()
{
_bgmPlayer = GetNode<AudioStreamPlayer>("BGMPlayer");
_sfxPlayer = GetNode<AudioStreamPlayer>("SFXPlayer");
// 背景音乐播放完成
_bgmPlayer.Signal(AudioStreamPlayer.SignalName.Finished)
.To(new Callable(this, nameof(OnBGMFinished)));
// 音效播放完成 - 延迟清理
_sfxPlayer.Signal(AudioStreamPlayer.SignalName.Finished)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnSFXFinished)));
}
private void OnBGMFinished()
{
// 循环播放背景音乐
PlayBGM(_currentBGM);
}
private void OnSFXFinished()
{
// 清理音效资源或播放队列中的下一个音效
CleanupSFXResources();
}
}
``` ```
## 设计模式分析 ## 什么时候用 fluent API什么时候用生成器
### Builder Pattern - 用 `Signal(...)`
- 动态节点
- 动态 signal 名称
- 想保留手写 `Callable` 和连接 flags
- 用 `[BindNodeSignal]`
- 节点字段和 signal 都是静态已知
- 你已经在用 `[GetNode]`
- 希望把 `_Ready()` 里的重复绑定样板交给生成器
SignalBuilder 实现了构建器模式: 这两条路径是互补关系,不是前后代际关系。当前源码没有“先用 `CreateSignalBuilder(...)`,再升级到生成器”这种迁移链。
- 分步构建复杂的信号连接 ## 当前边界
- 支持链式调用
- 延迟执行到最终调用时
### Fluent Interface - 当前入口是 `Signal(...)`,不是旧文档里的 `CreateSignalBuilder(...)`
- 这里不会自动生成 `_Ready()` / `_ExitTree()`,这类能力属于 `GFramework.Godot.SourceGenerators`
- `SignalBuilder` 不提供取消订阅 token也不会替你包装 `Disconnect(...)`
- `End()` 只返回原始对象,不会提交额外配置,也不是必须调用的终止步骤
- signal 名称是否合法、callable 签名是否匹配,仍然遵循 Godot 自身运行时规则
- `ToAndCall(...)` 会在完成连接后立刻执行 handler如果 handler 有副作用,需要你自己确认时机
流畅接口设计: ## 继续阅读
- 方法链式调用 - [Godot 运行时集成](./index.md)
- 可读性强 - [Godot 扩展方法](./extensions.md)
- 表达力强 - [Godot 集成教程](../tutorials/godot-integration.md)
- [BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md)
### Extension Method Pattern
扩展方法模式:
- 为现有类型添加功能
- 不修改原始类
- 保持向后兼容
## 与原生 API 对比
### 原生 Godot API
```csharp
// 传统方式
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)));
// 带标志的方式
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)), (uint)GodotObject.ConnectFlags.OneShot);
// 连接并立即调用
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)));
new Callable(this, nameof(OnButtonPressed)).Call();
```
### 信号连接系统 API
```csharp
// 流畅方式
button.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnButtonPressed)));
// 带标志的方式
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnButtonPressed)));
// 连接并立即调用
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
```
## 性能考虑
### 内存分配
- SignalBuilder 是轻量级对象
- 创建开销很小
- 使用后可被垃圾回收
### 调用开销
- 与原生 API 性能基本相同
- 主要开销在方法链调用
- 运行时性能无差异
### 推荐做法
- 避免在热循环中创建大量 SignalBuilder
- 适合 UI 事件、游戏逻辑等场景
- 可以放心使用,性能影响可忽略
## 最佳实践
### 1. 选择合适的连接标志
```csharp
// UI 事件通常使用延迟调用
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(callable);
// 一次性事件使用一次性标志
dialog.Signal(CustomDialog.SignalName.Accepted)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(callable);
```
### 2. 合理使用 ToAndCall
```csharp
// ✅ 适合:初始化时立即触发
settingsSlider.Signal(Slider.SignalName.ValueChanged)
.ToAndCall(new Callable(this, nameof(OnSettingsChanged)), initialSliderValue);
// ❌ 避免:重复连接并调用
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed))); // 可能不必要
```
### 3. 链式调用可读性
```csharp
// ✅ 推荐:清晰的链式调用
player.Signal(Player.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(UpdateHealthUI)));
// ❌ 避免:过度嵌套
node.Signal(CustomSignal.Signal1).WithFlags(Flags1).To(callable1)
.Signal(CustomSignal.Signal2).WithFlags(Flags2).To(callable2);
```

View File

@ -1,643 +1,351 @@
--- ---
title: Godot UI 系统 title: Godot UI 系统
description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成 description: 以当前 GFramework.Godot 源码、Game UI 契约与 CoreGrid 接线为准,说明 PackedScene UI 工厂、页面行为和层级接入路径
--- ---
# Godot UI 系统 # Godot UI 系统
## 概述 `GFramework.Godot.UI` 当前负责的是把 `GFramework.Game` 的 UI 路由契约接到 `Control` / `CanvasLayer` /
`PackedScene` 上,而不是定义一个 Godot 专属 router。
Godot UI 系统是 GFramework.Godot 中连接框架 UI 管理与 Godot Control 节点的核心组件。它提供了 UI 页面行为封装、UI 工厂、UI 当前真正参与这条链路的核心类型是:
注册表等功能,支持多层级 UI 显示,让你可以在 Godot 项目中使用 GFramework 的 UI 管理系统。
通过 Godot UI 系统,你可以使用 GFramework 的 UI 路由、生命周期管理、多层级显示等功能,同时保持与 Godot UI 系统的完美兼容。 - `IGodotUiRegistry` / `GodotUiRegistry`
- `GodotUiFactory`
- `CanvasItemUiPageBehaviorBase<T>`
- `UiPageBehaviorFactory`
- `Page` / `Overlay` / `Modal` / `Toast` / `Topmost` 五类 layer behavior
- 项目侧实现的 `IUiRoot`
- 项目侧继承 `UiRouterBase` 的 router
**主要特性** ## 当前公开入口
- UI 页面行为封装 ### `IGodotUiRegistry`
- UI 工厂和注册表
- 与 Godot PackedScene 集成
- 多层级 UI 支持Page、Overlay、Modal、Toast、Topmost
- UI 生命周期管理
- UI 根节点管理
## 核心概念 Godot 侧 UI 资源表,底层是 `IAssetRegistry<PackedScene>`。它只负责:
### UI 页面行为 - `uiKey -> PackedScene` 映射
- 让 `GodotUiFactory` 可以按 key 实例化 UI 页面
`CanvasItemUiPageBehaviorBase<T>` 封装了 Godot Control 节点的 UI 行为: 框架当前不会自动扫描 `.tscn`、不会自动根据类型名补全注册表。
### `GodotUiFactory`
`GodotUiFactory.Create(string uiKey)` 的当前行为比场景工厂更严格:
1. 从 `IGodotUiRegistry` 取出 `PackedScene`
2. 调用 `Instantiate()`
3. 节点必须实现 `IUiPageBehaviorProvider`
4. 返回 `provider.GetPage()`
如果实例化得到的节点没有实现 `IUiPageBehaviorProvider`,当前实现会直接抛 `InvalidCastException`。这也是 UI 页面文档必须强调
`GetPage()` / `[AutoUiPage]` 的原因。
### `CanvasItemUiPageBehaviorBase<T>`
Godot runtime 的页面行为包装基类。它把 `IUiPageBehavior` 的这些语义接到 `CanvasItem` 上:
- `Key`
- `Layer`
- `Handle`
- `IsAlive`
- `IsVisible`
- `InteractionProfile`
- `OnEnter` / `OnExit`
- `OnPause` / `OnResume`
- `OnShow` / `OnHide`
- `TryHandleUiAction(UiInputAction action)`
如果 owner 同时实现了 `IUiPage``IUiInteractionProfileProvider``IUiActionHandler`,这些契约都会被页面行为继续利用。
### `UiPageBehaviorFactory`
当前 layer 到 behavior 的映射来自运行时代码本身:
- `UiLayer.Page` -> `PageLayerUiPageBehavior<T>`
- `UiLayer.Overlay` -> `OverlayLayerUiPageBehavior<T>`
- `UiLayer.Modal` -> `ModalLayerUiPageBehavior<T>`
- `UiLayer.Toast` -> `ToastLayerUiPageBehavior<T>`
- `UiLayer.Topmost` -> `TopmostLayerUiPageBehavior<T>`
几个容易被旧文档写偏的默认语义如下:
- `Page`
- 不可重入,阻断输入
- `Overlay`
- 可重入,非模态,不阻断输入;暂停时不会停掉节点处理
- `Modal`
- 可重入,模态,阻断输入
- `Toast`
- 可重入,非模态,不阻断输入
- `Topmost`
- 不可重入,模态,阻断输入
## 最小接入路径
### 1. 继续在项目层保留自己的 router
仓库当前不存在 `GodotUiRouter` 类型。实际做法仍然是项目侧继承 `GFramework.Game.UI.UiRouterBase`
`ai-libs/CoreGrid``UiRouter` 目前就是:
```csharp ```csharp
public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
where T : CanvasItem
namespace CoreGrid.scripts.core.ui;
[Log]
public partial class UiRouter : UiRouterBase
{ {
protected readonly T Owner; protected override void RegisterHandlers()
public string Key { get; } {
public UiLayer Layer { get; } _log.Debug("Registering default transition handlers");
public bool IsReentrant { get; } RegisterHandler(new LoggingTransitionHandler());
}
} }
``` ```
### UI 工厂 Godot runtime 自身并不接管这层 router 的定义。
`GodotUiFactory` 负责创建 UI 实例: ### 2. 注册 `IGodotUiRegistry``IUiFactory`
最小 wiring 需要显式注册 UI 资源表和工厂:
```csharp ```csharp
public class GodotUiFactory : IUiFactory
{
public IUiPageBehavior Create(string uiKey);
}
```
### UI 层级行为
不同层级的 UI 有不同的行为类:
```csharp
// Page 层(栈管理)
public class PageLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
{
public override UiLayer Layer => UiLayer.Page;
public override bool IsReentrant => false;
}
// Modal 层(模态对话框)
public class ModalLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
{
public override UiLayer Layer => UiLayer.Modal;
public override bool IsReentrant => true;
}
```
## 基本用法
### 创建 UI 脚本
```csharp
using Godot;
using GFramework.Game.Abstractions.UI; using GFramework.Game.Abstractions.UI;
using GFramework.Godot.UI;
using Godot;
public partial class MainMenuPage : Control, IUiPage public sealed class GameUiRegistry : GodotUiRegistry
{ {
public GameUiRegistry()
{
Register(nameof(UiKey.MainMenu), GD.Load<PackedScene>("res://ui/main_menu.tscn"));
Register(nameof(UiKey.PauseMenu), GD.Load<PackedScene>("res://ui/pause_menu.tscn"));
Register(nameof(UiKey.OptionsMenu), GD.Load<PackedScene>("res://ui/options_menu.tscn"));
}
}
architecture.RegisterUtility<IGodotUiRegistry>(new GameUiRegistry());
architecture.RegisterUtility<IUiFactory>(new GodotUiFactory());
architecture.RegisterSystem(new UiRouter());
```
### 3. 提供 `IUiRoot`
`UiRouterBase` 只负责页面栈、layer UI、输入仲裁和暂停语义真正把页面挂到 Godot 容器的是项目自己的 `IUiRoot`
CoreGrid 当前的 `UiRoot` 做法和源码契约一致:
- 继承 `CanvasLayer`
- 为每个 `UiLayer` 创建一个 `Control` 容器
- 在 `_Ready()` 时调用 `_uiRouter.BindRoot(this)`
- 在 `AddUiPage` / `RemoveUiPage` 中处理 `CanvasItem` 挂载与释放
最小形态可以写成:
```csharp
public sealed class UiRoot : CanvasLayer, IUiRoot
{
[GetSystem] private IUiRouter _uiRouter = null!;
public override void _Ready()
{
__InjectContextBindings_Generated();
_uiRouter.BindRoot(this);
}
public void AddUiPage(IUiPageBehavior child)
{
AddUiPage(child, UiLayer.Page);
}
public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
{
if (child.View is not CanvasItem item)
throw new InvalidOperationException("UIPage View must be a Godot Node");
AddChild(item);
item.ZIndex = (int)layer * 100 + orderInLayer;
}
public void RemoveUiPage(IUiPageBehavior child)
{
if (child.View is Node node && node.GetParent() == this)
RemoveChild(node);
}
}
```
### 4. 让页面节点提供 `GetPage()`
因为 `GodotUiFactory` 不会自动回退到默认 behavior页面节点必须显式提供 `GetPage()`
#### 方式 A手写 `IUiPageBehaviorProvider`
```csharp
public partial class PauseMenu : Control, IUiPage, IUiPageBehaviorProvider
{
private IUiPageBehavior? _page;
public IUiPageBehavior GetPage()
{
return _page ??= UiPageBehaviorFactory.Create(this, nameof(UiKey.PauseMenu), UiLayer.Modal);
}
public void OnEnter(IUiPageEnterParam? param) public void OnEnter(IUiPageEnterParam? param)
{ {
GD.Print("进入主菜单");
Show();
} }
public void OnExit() public void OnExit()
{ {
GD.Print("退出主菜单");
Hide();
} }
public void OnPause() public void OnPause()
{ {
GD.Print("暂停主菜单");
} }
public void OnResume() public void OnResume()
{ {
GD.Print("恢复主菜单");
} }
public void OnShow() public void OnShow()
{ {
Show();
} }
public void OnHide() public void OnHide()
{ {
Hide();
} }
} }
``` ```
### 实现 UI 页面行为提供者 #### 方式 B`[AutoUiPage]` 让生成器补样板
当前更贴近真实消费者 wiring 的方式,是让生成器产出 `UiKeyStr``GetPage()`
```csharp ```csharp
using Godot;
using GFramework.Game.Abstractions.UI; using GFramework.Game.Abstractions.UI;
using GFramework.Godot.UI; using GFramework.Godot.SourceGenerators.Abstractions.UI;
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
{
private PageLayerUiPageBehavior _behavior;
public override void _Ready()
{
_behavior = new PageLayerUiPageBehavior(this, "MainMenu");
}
public IUiPageBehavior GetPage()
{
return _behavior;
}
}
```
### 注册 UI
```csharp
using GFramework.Godot.UI;
using Godot; using Godot;
public class GameUiRegistry : GodotUiRegistry [AutoUiPage(nameof(UiKey.MainMenu), nameof(UiLayer.Page))]
{ public partial class MainMenu : Control, IUiPageBehaviorProvider, IUiPage
public GameUiRegistry()
{
// 注册 UI 资源
Register("MainMenu", GD.Load<PackedScene>("res://ui/MainMenu.tscn"));
Register("Settings", GD.Load<PackedScene>("res://ui/Settings.tscn"));
Register("ConfirmDialog", GD.Load<PackedScene>("res://ui/ConfirmDialog.tscn"));
Register("Toast", GD.Load<PackedScene>("res://ui/Toast.tscn"));
}
}
```
### 设置 UI 系统
```csharp
using GFramework.Godot.Architecture;
using GFramework.Godot.UI;
public class GameArchitecture : AbstractArchitecture
{
protected override void InstallModules()
{
// 注册 UI 注册表
var uiRegistry = new GameUiRegistry();
RegisterUtility<IGodotUiRegistry>(uiRegistry);
// 注册 UI 工厂
var uiFactory = new GodotUiFactory();
RegisterUtility<IUiFactory>(uiFactory);
// 注册 UI 路由
var uiRouter = new GodotUiRouter();
RegisterSystem<IUiRouter>(uiRouter);
}
}
```
### 使用 UI 路由
```csharp
using Godot;
using GFramework.Godot.Extensions;
public partial class GameController : Node
{
public override void _Ready()
{
ShowMainMenu();
}
private async void ShowMainMenu()
{
var uiRouter = this.GetSystem<IUiRouter>();
await uiRouter.PushAsync("MainMenu");
}
private async void ShowSettings()
{
var uiRouter = this.GetSystem<IUiRouter>();
await uiRouter.PushAsync("Settings");
}
private void ShowDialog()
{
var uiRouter = this.GetSystem<IUiRouter>();
uiRouter.Show("ConfirmDialog", UiLayer.Modal);
}
private void ShowToast(string message)
{
var uiRouter = this.GetSystem<IUiRouter>();
uiRouter.Show("Toast", UiLayer.Toast, new ToastParam { Message = message });
}
}
```
## 高级用法
### 不同层级的 UI 行为
```csharp
// Page 层 UI栈管理不可重入
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new PageLayerUiPageBehavior(this, "MainMenu");
}
}
// Overlay 层 UI浮层可重入
public partial class InfoPanel : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new OverlayLayerUiPageBehavior(this, "InfoPanel");
}
}
// Modal 层 UI模态对话框可重入
public partial class ConfirmDialog : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new ModalLayerUiPageBehavior(this, "ConfirmDialog");
}
}
// Toast 层 UI提示可重入
public partial class ToastMessage : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new ToastLayerUiPageBehavior(this, "Toast");
}
}
// Topmost 层 UI顶层不可重入
public partial class LoadingScreen : Control, IUiPageBehaviorProvider
{
public IUiPageBehavior GetPage()
{
return new TopmostLayerUiPageBehavior(this, "Loading");
}
}
```
### UI 参数传递
```csharp
// 定义 UI 参数
public class ConfirmDialogParam : IUiPageEnterParam
{
public string Title { get; set; }
public string Message { get; set; }
public Action OnConfirm { get; set; }
public Action OnCancel { get; set; }
}
// 在 UI 中接收参数
public partial class ConfirmDialog : Control, IUiPage
{
private Label _titleLabel;
private Label _messageLabel;
private Action _onConfirm;
private Action _onCancel;
public override void _Ready()
{
_titleLabel = GetNode<Label>("Title");
_messageLabel = GetNode<Label>("Message");
GetNode<Button>("ConfirmButton").Pressed += OnConfirmPressed;
GetNode<Button>("CancelButton").Pressed += OnCancelPressed;
}
public void OnEnter(IUiPageEnterParam? param)
{
if (param is ConfirmDialogParam dialogParam)
{
_titleLabel.Text = dialogParam.Title;
_messageLabel.Text = dialogParam.Message;
_onConfirm = dialogParam.OnConfirm;
_onCancel = dialogParam.OnCancel;
}
Show();
}
private void OnConfirmPressed()
{
_onConfirm?.Invoke();
CloseDialog();
}
private void OnCancelPressed()
{
_onCancel?.Invoke();
CloseDialog();
}
private void CloseDialog()
{
var uiRouter = this.GetSystem<IUiRouter>();
if (Handle.HasValue)
{
uiRouter.Hide(Handle.Value, UiLayer.Modal, destroy: true);
}
}
// ... 其他生命周期方法
}
// 显示对话框
var uiRouter = this.GetSystem<IUiRouter>();
uiRouter.Show("ConfirmDialog", UiLayer.Modal, new ConfirmDialogParam
{
Title = "确认",
Message = "确定要退出吗?",
OnConfirm = () => GD.Print("确认"),
OnCancel = () => GD.Print("取消")
});
```
### UI 根节点管理
```csharp
using Godot;
using GFramework.Godot.UI;
public partial class UiRoot : CanvasLayer, IUiRoot
{
private Control _pageLayer;
private Control _overlayLayer;
private Control _modalLayer;
private Control _toastLayer;
private Control _topmostLayer;
public override void _Ready()
{
// 创建各层级容器
_pageLayer = new Control { Name = "PageLayer" };
_overlayLayer = new Control { Name = "OverlayLayer" };
_modalLayer = new Control { Name = "ModalLayer" };
_toastLayer = new Control { Name = "ToastLayer" };
_topmostLayer = new Control { Name = "TopmostLayer" };
AddChild(_pageLayer);
AddChild(_overlayLayer);
AddChild(_modalLayer);
AddChild(_toastLayer);
AddChild(_topmostLayer);
}
public void AttachPage(Control page, UiLayer layer)
{
var container = GetLayerContainer(layer);
container.AddChild(page);
}
public void DetachPage(Control page, UiLayer layer)
{
var container = GetLayerContainer(layer);
container.RemoveChild(page);
}
private Control GetLayerContainer(UiLayer layer)
{
return layer switch
{
UiLayer.Page => _pageLayer,
UiLayer.Overlay => _overlayLayer,
UiLayer.Modal => _modalLayer,
UiLayer.Toast => _toastLayer,
UiLayer.Topmost => _topmostLayer,
_ => _pageLayer
};
}
}
```
### UI 动画和过渡
```csharp
public partial class AnimatedPage : Control, IUiPage
{ {
public void OnEnter(IUiPageEnterParam? param) public void OnEnter(IUiPageEnterParam? param)
{ {
// 淡入动画
Modulate = new Color(1, 1, 1, 0);
Show();
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
} }
public void OnExit() public void OnExit()
{ {
// 淡出动画 }
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 0.0f, 0.3f); public void OnPause()
tween.TweenCallback(Callable.From(Hide)); {
}
public void OnResume()
{
} }
public void OnShow() public void OnShow()
{ {
Show();
} }
public void OnHide() public void OnHide()
{ {
Hide();
} }
// ... 其他方法
} }
``` ```
### UI 句柄管理 当前生成器补出的核心样板与源码一致:
```csharp ```csharp
public partial class DialogManager : Node public IUiPageBehavior GetPage()
{ {
private UiHandle? _currentDialog; return __autoUiPageBehavior_Generated ??=
UiPageBehaviorFactory.Create(this, UiKeyStr, UiLayer.Page);
public void ShowDialog(string dialogKey)
{
// 关闭当前对话框
CloseCurrentDialog();
// 显示新对话框
var uiRouter = this.GetSystem<IUiRouter>();
_currentDialog = uiRouter.Show(dialogKey, UiLayer.Modal);
}
public void CloseCurrentDialog()
{
if (_currentDialog.HasValue)
{
var uiRouter = this.GetSystem<IUiRouter>();
uiRouter.Hide(_currentDialog.Value, UiLayer.Modal, destroy: true);
_currentDialog = null;
}
}
} }
``` ```
### 多个 Toast 显示 要注意两点:
- `[AutoUiPage]` 不会替你自动补 `: IUiPageBehaviorProvider`
- UI 层级是生成器输入的一部分;`Page` / `Modal` / `Overlay` 语义不是后面再猜出来的
### 5. 按 layer 选择正确入口
Godot runtime 只是落地 `UiRouterBase` 的语义,因此入口仍然和 `GFramework.Game` 一致:
页面栈:
```csharp ```csharp
public partial class ToastManager : Node await uiRouter.ReplaceAsync(nameof(UiKey.MainMenu));
{ await uiRouter.PushAsync(nameof(UiKey.Settings));
private readonly List<UiHandle> _activeToasts = new(); await uiRouter.PopAsync();
public void ShowToast(string message, float duration = 3.0f)
{
var uiRouter = this.GetSystem<IUiRouter>();
// Toast 层支持重入,可以同时显示多个
var handle = uiRouter.Show("Toast", UiLayer.Toast, new ToastParam
{
Message = message
});
_activeToasts.Add(handle);
// 自动隐藏
GetTree().CreateTimer(duration).Timeout += () =>
{
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
_activeToasts.Remove(handle);
};
}
public void ClearAllToasts()
{
var uiRouter = this.GetSystem<IUiRouter>();
foreach (var handle in _activeToasts)
{
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
}
_activeToasts.Clear();
}
}
``` ```
## 最佳实践 层级 UI
1. **UI 脚本实现 IUiPage 接口**:获得完整的生命周期管理
```csharp
✓ public partial class MyPage : Control, IUiPage { }
✗ public partial class MyPage : Control { } // 无生命周期管理
```
2. **使用正确的 UI 层级**:根据 UI 类型选择合适的层级
```csharp
✓ Page: 主要页面(主菜单、设置)
✓ Overlay: 浮层(信息面板)
✓ Modal: 模态对话框(确认框)
✓ Toast: 提示消息
✓ Topmost: 系统级(加载界面)
```
3. **在 OnEnter 中显示 UI**:确保 UI 正确显示
```csharp
public void OnEnter(IUiPageEnterParam? param)
{
Show(); // 显示 UI
// 初始化 UI 状态
}
```
4. **在 OnExit 中隐藏 UI**:确保 UI 正确隐藏
```csharp
public void OnExit()
{
Hide(); // 隐藏 UI
// 清理 UI 状态
}
```
5. **使用 UI 句柄管理非栈 UI**:对于 Modal、Toast 等层级
```csharp
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
// 保存句柄以便后续关闭
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
```
6. **使用 UI 参数传递数据**:避免使用全局变量
```csharp
✓ uiRouter.Show("Dialog", UiLayer.Modal, new DialogParam { ... });
✗ GlobalData.DialogMessage = "..."; // 避免全局状态
```
## 常见问题
### 问题:如何在 Godot UI 中使用 GFramework
**解答**
UI 脚本实现 `IUiPage``IUiPageBehaviorProvider` 接口:
```csharp ```csharp
public partial class MyPage : Control, IUiPage, IUiPageBehaviorProvider var handle = uiRouter.Show(nameof(UiKey.PauseMenu), UiLayer.Modal);
{ uiRouter.Hide(handle, UiLayer.Modal);
public void OnEnter(IUiPageEnterParam? param) { }
public IUiPageBehavior GetPage() { return new PageLayerUiPageBehavior(this, "MyPage"); }
}
``` ```
### 问题UI 层级有什么区别? 当前实现里,`Show(..., UiLayer.Page)` 会直接抛异常;`Page` 层必须走 `PushAsync` / `ReplaceAsync`
**解答** ## 输入与暂停语义
- **Page**:栈管理,不可重入,用于主要页面 如果页面只实现 `IUiPage`,它只有基础生命周期。
- **Overlay**:可重入,用于浮层
- **Modal**:可重入,带遮罩,用于对话框
- **Toast**:可重入,轻量提示
- **Topmost**:不可重入,最高优先级
### 问题:如何实现 UI 动画? 如果还需要更强的输入仲裁或暂停语义,可以像 CoreGrid 的 `PauseMenu` 一样继续实现:
**解答** - `IUiInteractionProfileProvider`
在生命周期方法中使用 Godot Tween - `IUiActionHandler`
```csharp 当前这条链路是成立的:
public void OnEnter(IUiPageEnterParam? param)
{
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
}
```
### 问题:如何在 UI 中访问架构组件? 1. 页面行为从 owner 读取 `UiInteractionProfile`
2. router 根据 profile 判断动作捕获、世界输入阻断和暂停策略
3. 如果页面实现了 `IUiActionHandler``TryHandleUiAction(...)` 会继续下沉到页面
**解答** 这也是为什么 `PauseMenu` 一类 modal 页面可以声明:
使用扩展方法:
```csharp - 捕获 `Cancel`
public partial class MyPage : Control, IUiPage - 阻断 World pointer / action input
{ - 在可见时持有暂停
public void OnEnter(IUiPageEnterParam? param) - 即使在暂停状态也继续处理节点逻辑
{
var playerModel = this.GetModel<PlayerModel>();
var gameSystem = this.GetSystem<GameSystem>();
}
}
```
### 问题:如何关闭 Modal 或 Toast ## 当前边界
**解答** ### 没有 `GodotUiRouter`
使用 UI 句柄:
```csharp 仓库当前没有这个类型。旧文档把它写成默认入口是不准确的;真实入口仍然是项目侧的 `UiRouterBase` 派生类。
// 显示时保存句柄
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
// 关闭时使用句柄 ### UI 工厂不会自动补 behavior
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
```
### 问题UI 生命周期方法的调用顺序是什么? `GodotSceneFactory` 不同,`GodotUiFactory` 当前不会按节点类型自动创建 behavior。节点不实现
`IUiPageBehaviorProvider` 时会直接失败。
**解答** ### `Page` 层不是 `Show(...)` 的适用对象
- 进入:`OnEnter` -> `OnShow` `UiLayer.Page` 代表页面栈语义,而不是普通 layer UI。当前实现明确要求
- 暂停:`OnPause` -> `OnHide`
- 恢复:`OnShow` -> `OnResume`
- 退出:`OnHide` -> `OnExit`
## 相关文档 - `Page``PushAsync` / `ReplaceAsync`
- `Overlay` / `Modal` / `Toast` / `Topmost``Show` / `Hide`
- [UI 系统](/zh-CN/game/ui) - 核心 UI 管理 ### root 仍然由项目控制
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成 `IUiRoot` 决定:
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
- 每个 layer 是否拆独立容器
- 层内排序怎么算
- 页面移除时如何释放节点
Godot runtime 不会替项目自动生成统一 UI 根节点。
## 继续阅读
1. [Godot 运行时集成](./index.md)
2. [Game UI 系统](../game/ui.md)
3. [AutoUiPage 生成器](../source-generators/auto-ui-page-generator.md)
4. [Godot 架构集成](./architecture.md)

View File

@ -1,21 +1,26 @@
---
title: AutoRegisterExportedCollections 生成器
description: 说明批量注册生成器当前会生成什么、可匹配哪些集合与注册器成员,以及 null-skip 与编译期诊断的边界。
---
# AutoRegisterExportedCollections 生成器 # AutoRegisterExportedCollections 生成器
> 为 Godot 导出集合生成批量注册方法,收敛启动入口里的重复 `foreach + Registry(...)` 样板。 `[AutoRegisterExportedCollections]` 用来把“遍历一组配置并逐项调用 registry 方法”的启动样板收敛成一个生成方法
## 概述 它最常见的落点确实是 Godot Inspector 导出的数组,但当前生成器真正依赖的不是 `[Export]` 本身,而是:
在游戏启动入口中,常见的一类样板是: - 宿主类型被标记了 `[AutoRegisterExportedCollections]`
- 某个实例字段或可读实例属性被标记了 `[RegisterExportedCollection(...)]`
- 该成员可枚举,且元素类型可在编译期推导
- 目标 registry 成员存在,并能找到兼容的单参数实例方法
- 在 Inspector 中导出一批配置、资源映射或预制体条目 ## 当前包关系
- 从某个 Registry 成员拿到注册器
- 遍历集合逐项调用 `Register(...)` / `Registry(...)`
`AutoRegisterExportedCollections` 会把这类样板收敛成声明式配置。 - 特性来源:`GFramework.Godot.SourceGenerators.Abstractions.UI`
- 生成器实现:`GFramework.Godot.SourceGenerators`
- 典型消费者Godot 启动入口、资源入口节点、配置引导节点
它特别适合 `GameEntryPoint`、资源根节点、配置引导节点这类“导出即注册”的场景。 ## 最小用法
相关特性当前位于 `GFramework.Godot.SourceGenerators.Abstractions.UI` 命名空间。
## 基础使用
```csharp ```csharp
using System.Collections.Generic; using System.Collections.Generic;
@ -39,13 +44,6 @@ public sealed class TextureConfig : Resource, IKeyValue<string, Texture2D>
{ {
} }
public sealed class TextureRegistry : IAssetRegistry<Texture2D>
{
public void Registry(IKeyValue<string, Texture2D> mapping)
{
}
}
[AutoRegisterExportedCollections] [AutoRegisterExportedCollections]
public partial class GameEntryPoint : Node public partial class GameEntryPoint : Node
{ {
@ -57,119 +55,169 @@ public partial class GameEntryPoint : Node
public override void _Ready() public override void _Ready()
{ {
_textureRegistry ??= new TextureRegistry(); _textureRegistry ??= ResolveTextureRegistry();
__RegisterExportedCollections_Generated(); __RegisterExportedCollections_Generated();
} }
private static IAssetRegistry<Texture2D> ResolveTextureRegistry()
{
throw new NotImplementedException();
}
} }
``` ```
为了让示例具备完整的调用路径,这里在 `_Ready()` 里先初始化了 `_textureRegistry` 当前生成器不会自动调用 `__RegisterExportedCollections_Generated()`。你需要在 registry 成员和集合成员都准备好之后手动调用。
实际项目里,这个字段通常来自架构容器、服务定位或外部注入;关键点是调用 `__RegisterExportedCollections_Generated()`
之前,注册器成员必须已经可用,否则生成代码会按设计静默跳过注册。
## 生成的代码 ## 当前会生成什么
对于上面的成员,当前生成器会产出:
```csharp ```csharp
// <auto-generated /> private void __RegisterExportedCollections_Generated()
#nullable enable
partial class GameEntryPoint
{ {
private void __RegisterExportedCollections_Generated() if (this._textureConfigs is not null && this._textureRegistry is not null)
{ {
if (this._textureConfigs is not null && this._textureRegistry is not null) foreach (var __generatedItem in this._textureConfigs)
{ {
foreach (var __generatedItem in this._textureConfigs) this._textureRegistry.Registry(__generatedItem);
{
this._textureRegistry.Registry(__generatedItem);
}
} }
} }
} }
``` ```
## 参数说明 最重要的运行时语义只有两条:
### `[AutoRegisterExportedCollections]` - 集合成员为 `null` 时,本次注册直接跳过
- registry 成员为 `null` 时,本次注册直接跳过
类级标记,声明该类型允许生成 `__RegisterExportedCollections_Generated()` 这里的“跳过”只针对运行时 `null` 情况;配置错误、方法不匹配、元素类型无法推导等问题都会在编译期直接给出诊断,而不是静默吞掉
### `[RegisterExportedCollection(registryMemberName, registerMethodName)]` ## 当前支持的成员形状
| 参数 | 类型 | 说明 | ### 集合成员
|----------------------|----------|----------------------------------|
| `registryMemberName` | `string` | 当前类型上用于执行注册的字段或属性名 |
| `registerMethodName` | `string` | 注册方法名,例如 `Register``Registry` |
推荐优先使用 `nameof(...)` 表达式,而不是手写字符串。 `[RegisterExportedCollection]` 可以标在:
## 支持的匹配规则 - 实例字段
- 可读、非索引器的实例属性
生成器会在编译期验证: 它们不必一定带 `[Export]`,但在 Godot 项目里通常会配合 `[Export]` 使用。
- 集合成员必须是实例字段,或可读的实例属性 ### registry 成员
- 集合类型必须可枚举
- 集合元素类型必须能在编译期推导
- 注册器成员必须是实例字段,或可读的实例属性
- 注册方法必须是单参数实例方法,且参数类型能接收集合元素类型
当前版本还支持从以下位置解析注册方法 `registryMemberName` 指向的目标也必须是:
- 注册器具体类型本身 - 实例字段,或
- 注册器基类 - 可读、非索引器的实例属性
- 注册器实现的接口
静态字段、静态属性、只写属性都不受支持。
## 当前匹配规则
### 可枚举集合
集合成员必须实现 `System.Collections.IEnumerable`,并且生成器还要能推导出元素类型。
因此:
- `List<int>``Godot.Collections.Array<TextureConfig>` 这类泛型集合可以
- 非泛型 `IEnumerable` / `ArrayList` 这类只能枚举 `object` 的集合不可以
### 注册方法
当前会查找名称匹配、且满足以下条件的方法:
- 实例方法
- 只有一个参数
- 对宿主类型可访问
- 参数类型能接收集合元素类型
查找范围不只限于 registry 具体类型本身,还包括:
- 基类
- 直接实现的接口
- 继承链上的接口 - 继承链上的接口
这意味着像 `IAssetRegistry<T>` 继承 `IRegistry<TKey, TValue>` 的项目结构也能正常生成,不必再把注册器字段改成具体实现类型。 所以像下面这种接口继承链是受支持的:
## 适用场景 ```csharp
[RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new();
```
推荐用于: 只要 `_registry` 的接口链上能找到兼容的 `Registry(...)` 即可。
- `GameEntryPoint` 中的资源注册 ### 明确不支持的情况
- 场景启动时的配置条目注册
- Inspector 预配置的纹理、音频、Prefab、场景映射批量接入
不推荐用于: 当前测试明确覆盖了这些边界:
- 注册前需要复杂过滤、去重、排序、条件判断的集合 - 只显式实现接口方法,未在具体类型上暴露可访问成员
- 需要记录失败项、错误聚合或回滚逻辑的批量导入 - 注册方法存在,但对宿主类型不可访问
- 每个元素注册时都依赖额外上下文或副作用控制的流程 - 集合元素类型无法推导
- registry 成员不存在
- 注册方法名存在但签名不兼容
这些情况都会直接触发编译期诊断。
## 真实采用路径
`ai-libs/CoreGrid/global/GameEntryPoint.cs` 是当前最直接的消费者参考:
- `UiPageConfigs`
- `GameSceneConfigs`
- `PrefabSceneConfigs`
- `TextureConfigs`
这几个 `Array<T>` 成员都通过 `[RegisterExportedCollection(...)]` 声明 registry 目标,并在 `_Ready()` 里调用
`__RegisterExportedCollections_Generated()`
这个例子说明两件事:
1. 这项能力适合“启动时集中接入一批静态配置”的节点
2. 生成器只负责循环调用,不负责 registry 的获取、生命周期或错误恢复
## 使用约束 ## 使用约束
- 目标类型必须是 `partial class` 当前最重要的约束有这些:
- 宿主类型必须是顶层 `partial class`
- 不支持嵌套类 - 不支持嵌套类
- 生成器不会自动调用 `__RegisterExportedCollections_Generated()` - 生成器不会自动接入 `_Ready()` 或其他生命周期方法
- 非泛型 `IEnumerable` 之类无法推导元素类型的集合不受支持 - 宿主类型若已声明 `__RegisterExportedCollections_Generated()`,会触发命名冲突诊断
- 注册方法必须对宿主类型可访问 - 只有当至少一个成员成功通过验证时,才会生成方法
## 诊断信息 ## 诊断速查
| 诊断 ID | 含义 | | 诊断 ID | 含义 |
|-----------------------|-------------------------------------------------------------| | --- | --- |
| `GF_Common_Class_001` | 目标类型不是 `partial`,生成被跳过 | | `GF_Common_Class_001` | 宿主类型不是 `partial class` |
| `GF_Common_Class_002` | 宿主类型已声明 `__RegisterExportedCollections_Generated()`,与生成代码冲突 | | `GF_Common_Class_002` | 已手写 `__RegisterExportedCollections_Generated()`,与生成代码冲突 |
| `GF_AutoExport_001` | `AutoRegisterExportedCollections` 不支持嵌套类 | | `GF_AutoExport_001` | 不支持嵌套类 |
| `GF_AutoExport_002` | 指定的注册器成员不存在 | | `GF_AutoExport_002` | 指定的 registry 成员不存在 |
| `GF_AutoExport_003` | 注册器成员上找不到兼容的注册方法 | | `GF_AutoExport_003` | 找不到兼容且可访问的注册方法 |
| `GF_AutoExport_004` | 被标记的成员不是可枚举集合 | | `GF_AutoExport_004` | 被标记成员不可枚举 |
| `GF_AutoExport_005` | 无法推导集合元素类型 | | `GF_AutoExport_005` | 无法安全推导集合元素类型 |
| `GF_AutoExport_006` | 集合成员不是实例可读成员 | | `GF_AutoExport_006` | 集合成员不是实例可读成员 |
| `GF_AutoExport_007` | 注册器成员不是实例可读成员 | | `GF_AutoExport_007` | registry 成员不是实例可读成员 |
| `GF_AutoExport_008` | `RegisterExportedCollectionAttribute` 参数无效 | | `GF_AutoExport_008` | `RegisterExportedCollectionAttribute` 构造参数无效 |
## 调用时机建议 ## 何时适合用它
推荐在以下时机之一调用生成方法 适合
- `_Ready()` 中,且在注册器字段已经准备好之后 - 启动入口里有多组“集合 -> registry”的重复注册代码
- 启动入口的显式 `Initialize()``Bootstrap()` 方法中 - 每个元素都只需要一次简单的单参数注册
- 测试中的装配阶段 - 你想把“注册到哪个 registry、调用哪个方法”直接挂在成员声明上
要在构造函数中调用,因为此时 Godot 导出字段和外部依赖通常还未准备完毕。 适合:
## 相关文档 - 注册流程需要排序、过滤、去重或事务式回滚
- 每个元素注册前后还要插入复杂副作用
- 注册规则依赖运行时动态上下文,而不是静态成员绑定
- [源码生成器总览](./index) ## 推荐阅读
- [游戏内容配置系统](/zh-CN/game/config-system)
1. [/zh-CN/source-generators/index](./index.md)
2. [/zh-CN/game/config-system](../game/config-system.md)
3. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
4. `GFramework.Godot.SourceGenerators/README.md`

View File

@ -1,283 +1,46 @@
---
title: BindNodeSignal 生成器
description: 说明 [BindNodeSignal] 当前生成什么、如何与 GetNode 协作,以及 _Ready 和 _ExitTree 的接入要求。
---
# BindNodeSignal 生成器 # BindNodeSignal 生成器
> 自动生成 Godot 节点信号绑定与解绑逻辑,消除事件订阅样板代码 `[BindNodeSignal]` 把 Godot CLR event 的 `+=` / `-=` 样板收敛成生成方法。它只生成“如何订阅与解绑”,不会替你查找节点,也不会自动生成完整生命周期方法。
## 概述 ## 当前包关系
BindNodeSignal 生成器为标记了 `[BindNodeSignal]` 特性的方法自动生成节点事件绑定和解绑代码。它将 `_Ready()` - 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
`_ExitTree()` 中重复的 `+=``-=` 样板代码收敛到生成器中统一维护。 - 生成器实现:`GFramework.Godot.SourceGenerators`
- 目标字段基线:`nodeFieldName` 指向的字段必须继承 `Godot.Node`
### 核心功能 ## 最小用法
- **自动事件绑定**:在 `_Ready()` 中自动订阅节点事件
- **自动事件解绑**:在 `_ExitTree()` 中自动取消订阅
- **多事件绑定**:一个方法可以绑定到多个节点事件
- **类型安全检查**:编译时验证方法签名与事件委托的兼容性
- **与 GetNode 集成**:无缝配合 `[GetNode]` 特性使用
## 基础使用
### 标记事件处理方法
使用 `[BindNodeSignal]` 特性标记处理节点事件的方法:
```csharp ```csharp
using GFramework.Godot.SourceGenerators.Abstractions; using GFramework.Godot.SourceGenerators.Abstractions;
using Godot; using Godot;
public partial class MainMenu : Control public partial class Hud : Control
{ {
[GetNode]
private Button _startButton = null!; private Button _startButton = null!;
private Button _settingsButton = null!;
private Button _quitButton = null!; [GetNode]
private SpinBox _startOreSpinBox = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed() private void OnStartButtonPressed()
{ {
StartGame();
} }
[BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))] [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
private void OnSettingsButtonPressed() private void OnStartOreValueChanged(double value)
{ {
ShowSettings();
}
[BindNodeSignal(nameof(_quitButton), nameof(Button.Pressed))]
private void OnQuitButtonPressed()
{
QuitGame();
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 生成的代码
编译器会为标记的类自动生成以下代码:
```csharp
// <auto-generated />
#nullable enable
namespace YourNamespace;
partial class MainMenu
{
private void __BindNodeSignals_Generated()
{
_startButton.Pressed += OnStartButtonPressed;
_settingsButton.Pressed += OnSettingsButtonPressed;
_quitButton.Pressed += OnQuitButtonPressed;
}
private void __UnbindNodeSignals_Generated()
{
_startButton.Pressed -= OnStartButtonPressed;
_settingsButton.Pressed -= OnSettingsButtonPressed;
_quitButton.Pressed -= OnQuitButtonPressed;
}
}
```
## 参数说明
`[BindNodeSignal]` 特性需要两个参数:
| 参数 | 类型 | 说明 |
|-----------------|--------|-----------------------------|
| `nodeFieldName` | string | 目标节点字段名(使用 `nameof` 推荐) |
| `signalName` | string | 目标节点上的 CLR 事件名(使用 `nameof` |
```csharp
[BindNodeSignal("_startButton", "Pressed")] // 字符串字面量
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] // 推荐nameof 表达式
```
## 高级用法
### 带参数的事件处理
处理带参数的事件(如 `SpinBox.ValueChanged`
```csharp
using Godot;
public partial class SettingsPanel : Control
{
private SpinBox _volumeSpinBox = null!;
private SpinBox _brightnessSpinBox = null!;
// 参数类型必须与事件委托匹配
[BindNodeSignal(nameof(_volumeSpinBox), nameof(SpinBox.ValueChanged))]
private void OnVolumeChanged(double value)
{
SetVolume((float)value);
}
[BindNodeSignal(nameof(_brightnessSpinBox), nameof(SpinBox.ValueChanged))]
private void OnBrightnessChanged(double value)
{
SetBrightness((float)value);
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 多事件绑定
一个方法可以同时绑定到多个节点的事件:
```csharp
public partial class MultiButtonHud : Control
{
private Button _buttonA = null!;
private Button _buttonB = null!;
private Button _buttonC = null!;
// 一个方法处理多个按钮的点击
[BindNodeSignal(nameof(_buttonA), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonB), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonC), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
{
PlayClickSound();
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 与 [GetNode] 组合使用
推荐与 `[GetNode]` 特性结合使用:
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
public partial class GameHud : Control
{
// 使用 GetNode 自动获取节点
[GetNode]
private Button _pauseButton = null!;
[GetNode]
private ProgressBar _healthBar = null!;
[GetNode("UI/ScoreLabel")]
private Label _scoreLabel = null!;
// 使用 BindNodeSignal 绑定事件
[BindNodeSignal(nameof(_pauseButton), nameof(Button.Pressed))]
private void OnPauseButtonPressed()
{
TogglePause();
}
// 多事件绑定示例
[BindNodeSignal(nameof(_healthBar), nameof(Range.ValueChanged))]
private void OnHealthChanged(double value)
{
UpdateHealthDisplay(value);
}
public override void _Ready()
{
// 先注入节点,再绑定信号
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
### 复杂事件处理场景
实现完整的 UI 事件处理:
```csharp
public partial class InventoryUI : Control
{
// 节点
[GetNode]
private ItemList _itemList = null!;
[GetNode]
private Button _useButton = null!;
[GetNode]
private Button _dropButton = null!;
[GetNode]
private LineEdit _searchBox = null!;
// 事件处理
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemSelected))]
private void OnItemSelected(long index)
{
SelectItem((int)index);
}
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemActivated))]
private void OnItemActivated(long index)
{
UseItem((int)index);
}
[BindNodeSignal(nameof(_useButton), nameof(Button.Pressed))]
private void OnUseButtonPressed()
{
UseSelectedItem();
}
[BindNodeSignal(nameof(_dropButton), nameof(Button.Pressed))]
private void OnDropButtonPressed()
{
DropSelectedItem();
}
[BindNodeSignal(nameof(_searchBox), nameof(LineEdit.TextChanged))]
private void OnSearchTextChanged(string newText)
{
FilterItems(newText);
} }
public override void _Ready() public override void _Ready()
{ {
__InjectGetNodes_Generated(); __InjectGetNodes_Generated();
__BindNodeSignals_Generated(); __BindNodeSignals_Generated();
InitializeInventory();
} }
public override void _ExitTree() public override void _ExitTree()
@ -287,394 +50,143 @@ public partial class InventoryUI : Control
} }
``` ```
## 生命周期管理 当前生成器会产出:
### 自动生成生命周期方法
如果类没有 `_Ready()``_ExitTree()`,生成器会自动生成:
```csharp ```csharp
public partial class AutoLifecycleHud : Control private void __BindNodeSignals_Generated()
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed()
{
// 处理点击
}
// 无需手动声明 _Ready 和 _ExitTree
// 生成器会自动生成:
// public override void _Ready() { __BindNodeSignals_Generated(); }
// public override void _ExitTree() { __UnbindNodeSignals_Generated(); }
}
```
### 手动生命周期调用
如果已有生命周期方法,需要手动调用生成的方法:
```csharp
public partial class CustomLifecycleHud : Control
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed()
{
HandlePress();
}
public override void _Ready()
{
// 必须手动调用绑定方法
__BindNodeSignals_Generated();
// 自定义初始化逻辑
InitializeUI();
}
public override void _ExitTree()
{
// 必须手动调用解绑方法
__UnbindNodeSignals_Generated();
// 自定义清理逻辑
CleanupResources();
}
}
```
**注意**:如果在 `_Ready()` 中不调用 `__BindNodeSignals_Generated()`,编译器会发出警告 `GF_Godot_BindNodeSignal_008`
## 诊断信息
生成器会在以下情况报告编译错误或警告:
### GF_Godot_BindNodeSignal_001 - 不支持嵌套类
**错误信息**`Class '{ClassName}' cannot use [BindNodeSignal] inside a nested type`
**解决方案**:将嵌套类提取为独立的类
```csharp
// ❌ 错误
public partial class Outer
{
public partial class Inner
{
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { } // 错误
}
}
// ✅ 正确
public partial class Inner
{
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
}
```
### GF_Godot_BindNodeSignal_002 - 不支持静态方法
**错误信息**`Method '{MethodName}' cannot be static when using [BindNodeSignal]`
**解决方案**:改为实例方法
```csharp
// ❌ 错误
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private static void OnPressed() { }
// ✅ 正确
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
```
### GF_Godot_BindNodeSignal_003 - 节点字段不存在
**错误信息**
`Method '{MethodName}' references node field '{FieldName}', but no matching field exists on class '{ClassName}'`
**解决方案**:确保引用的字段存在且名称正确
```csharp
// ❌ 错误_button 字段不存在
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
// ✅ 正确
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnPressed() { }
```
### GF_Godot_BindNodeSignal_004 - 节点字段必须是实例字段
**错误信息**`Method '{MethodName}' references node field '{FieldName}', but that field must be an instance field`
**解决方案**:将节点字段改为实例字段(非静态)
```csharp
// ❌ 错误
private static Button _button = null!;
// ✅ 正确
private Button _button = null!;
```
### GF_Godot_BindNodeSignal_005 - 字段类型必须继承自 Godot.Node
**错误信息**`Field '{FieldName}' must be a Godot.Node type to use [BindNodeSignal]`
**解决方案**:确保字段类型继承自 `Godot.Node`
```csharp
// ❌ 错误
private string _text = null!; // string 不是 Node 类型
[BindNodeSignal(nameof(_text), "Changed")] // 错误
// ✅ 正确
private Button _button = null!; // Button 继承自 Node
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### GF_Godot_BindNodeSignal_006 - 目标事件不存在
**错误信息**`Field '{FieldName}' does not contain an event named '{EventName}'`
**解决方案**:确保事件名称正确
```csharp
private Button _button = null!;
// ❌ 错误Click 不是 Button 的事件
[BindNodeSignal(nameof(_button), "Click")]
// ✅ 正确:使用正确的事件名
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### GF_Godot_BindNodeSignal_007 - 方法签名不兼容
**错误信息**`Method '{MethodName}' is not compatible with event '{EventName}' on field '{FieldName}'`
**解决方案**:确保方法签名与事件委托匹配
```csharp
private SpinBox _spinBox = null!;
// ❌ 错误SpinBox.ValueChanged 需要 double 参数
[BindNodeSignal(nameof(_spinBox), nameof(SpinBox.ValueChanged))]
private void OnValueChanged() { } // 缺少参数
// ✅ 正确
[BindNodeSignal(nameof(_spinBox), nameof(SpinBox.ValueChanged))]
private void OnValueChanged(double value) { }
```
### GF_Godot_BindNodeSignal_008 - 需要在 _Ready 中调用绑定方法
**警告信息**
`Class '{ClassName}' defines _Ready(); call __BindNodeSignals_Generated() there to bind [BindNodeSignal] handlers`
**解决方案**:在 `_Ready()` 中手动调用 `__BindNodeSignals_Generated()`
```csharp
public override void _Ready()
{
__BindNodeSignals_Generated(); // ✅ 必须手动调用
// 其他初始化...
}
```
### GF_Godot_BindNodeSignal_009 - 需要在 _ExitTree 中调用解绑方法
**警告信息**
`Class '{ClassName}' defines _ExitTree(); call __UnbindNodeSignals_Generated() there to unbind [BindNodeSignal] handlers`
**解决方案**:在 `_ExitTree()` 中手动调用 `__UnbindNodeSignals_Generated()`
```csharp
public override void _ExitTree()
{
__UnbindNodeSignals_Generated(); // ✅ 必须手动调用
// 其他清理...
}
```
### GF_Godot_BindNodeSignal_010 - 构造参数无效
**错误信息**
`Method '{MethodName}' uses [BindNodeSignal] with an invalid '{ParameterName}' constructor argument; it must be a non-empty string literal`
**解决方案**:使用有效的字符串字面量或 nameof 表达式
```csharp
// ❌ 错误:空字符串
[BindNodeSignal("", nameof(Button.Pressed))]
// ❌ 错误null 值
[BindNodeSignal(null, nameof(Button.Pressed))]
// ✅ 正确
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
## 最佳实践
### 1. 使用 nameof 表达式
使用 `nameof` 而不是字符串字面量,以获得重构支持和编译时检查:
```csharp
// ❌ 不推荐:字符串字面量
[BindNodeSignal("_button", "Pressed")]
// ✅ 推荐nameof 表达式
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
```
### 2. 保持方法命名一致
使用统一的命名约定提高代码可读性:
```csharp
// ✅ 推荐On + 节点名 + 事件名
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed() { }
[BindNodeSignal(nameof(_volumeSlider), nameof(Slider.ValueChanged))]
private void OnVolumeSliderValueChanged(double value) { }
```
### 3. 分组相关事件处理
将相关的事件处理方法放在一起,便于维护:
```csharp
public partial class GameHud : Control
{
// UI 节点
[GetNode]
private Button _pauseButton = null!;
[GetNode]
private Button _menuButton = null!;
// UI 事件处理(放在一起)
[BindNodeSignal(nameof(_pauseButton), nameof(Button.Pressed))]
private void OnPauseButtonPressed() { }
[BindNodeSignal(nameof(_menuButton), nameof(Button.Pressed))]
private void OnMenuButtonPressed() { }
}
```
### 4. 正确处理生命周期
始终确保事件解绑,避免内存泄漏:
```csharp
public partial class SafeHud : Control
{
private Button _button = null!;
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
private void OnButtonPressed() { }
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
// 确保解绑事件
__UnbindNodeSignals_Generated();
}
}
```
### 5. 对比手动事件绑定
| 方式 | 代码量 | 可维护性 | 错误风险 | 推荐场景 |
|--------------------|-----|------|----------|------------|
| 手动 `+=` / `-=` | 多 | 中 | 高(易遗漏解绑) | 简单场景 |
| `[BindNodeSignal]` | 少 | 高 | 低(编译器检查) | 复杂 UI、频繁事件 |
```csharp
// ❌ 不推荐:手动绑定
public override void _Ready()
{ {
_startButton.Pressed += OnStartButtonPressed; _startButton.Pressed += OnStartButtonPressed;
_settingsButton.Pressed += OnSettingsButtonPressed; _startOreSpinBox.ValueChanged += OnStartOreValueChanged;
_quitButton.Pressed += OnQuitButtonPressed;
} }
public override void _ExitTree() private void __UnbindNodeSignals_Generated()
{ {
// 容易遗漏解绑
_startButton.Pressed -= OnStartButtonPressed; _startButton.Pressed -= OnStartButtonPressed;
_quitButton.Pressed -= OnQuitButtonPressed; // 遗漏了 _settingsButton _startOreSpinBox.ValueChanged -= OnStartOreValueChanged;
} }
// ✅ 推荐:使用 [BindNodeSignal]
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed() { }
[BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))]
private void OnSettingsButtonPressed() { }
[BindNodeSignal(nameof(_quitButton), nameof(Button.Pressed))]
private void OnQuitButtonPressed() { }
``` ```
### 6. 与 [ContextAware] 组合使用 ## 生命周期边界
在需要架构访问的场景中,与 `[ContextAware]` 结合: ### 它只生成辅助方法,不生成 `_Ready()` / `_ExitTree()`
这是当前和 `[GetNode]` 最大的区别:
- `[GetNode]` 在缺少 `_Ready()` 时会补一个 override
- `[BindNodeSignal]` 只生成 `__BindNodeSignals_Generated()``__UnbindNodeSignals_Generated()`
所以你需要自己决定在哪个生命周期里调用它们。
### 已有生命周期但没调用时会给 warning
如果类型已经定义了 `_Ready()``_ExitTree()`,但没有调用对应生成方法,当前会给出 warning提醒你完成接线。
这意味着它更像“声明式订阅语法”,而不是“自动生命周期织入”。
## 当前契约
`[BindNodeSignal(nodeFieldName, signalName)]` 的两个参数都指向现有代码里的稳定符号:
- `nodeFieldName`:目标节点字段名
- `signalName`:该节点类型上的 CLR event 名
最推荐的写法仍然是:
```csharp ```csharp
using GFramework.Core.SourceGenerators.Abstractions.Rule; [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
using GFramework.Godot.SourceGenerators.Abstractions; ```
[ContextAware] 这样字段或事件改名时,编译器能一起帮你更新。
public partial class GameController : Node
## 当前会验证什么
生成器不是盲目拼字符串。按当前源码,它会在编译期验证:
- 方法必须是实例方法
- `nodeFieldName` 必须能解析到当前类型里的实例字段
- 该字段类型必须继承 `Godot.Node`
- `signalName` 必须能解析到该字段类型上的 CLR event
- 处理方法签名必须和 event delegate 兼容
例如:
- `Button.Pressed` 对应无参处理方法
- `SpinBox.ValueChanged` 对应 `double` 参数
如果签名不匹配,会直接报错,而不是生成一个运行时才失败的订阅。
## 多重绑定
`BindNodeSignalAttribute` 允许重复标记在同一个方法上,所以一个处理方法可以绑定多个事件:
```csharp
[BindNodeSignal(nameof(_buttonA), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonB), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_buttonC), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
{ {
[GetNode]
private Button _actionButton = null!;
private IGameModel _gameModel = null!;
[BindNodeSignal(nameof(_actionButton), nameof(Button.Pressed))]
private void OnActionButtonPressed()
{
// 可以直接使用架构功能
this.SendCommand(new PlayerActionCommand());
}
public override void _Ready()
{
__InjectContextBindings_Generated();
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
} }
``` ```
## 相关文档 当前生成器会为每个特性都生成一条 `+=` 和一条 `-=`
- [Source Generators 概述](./index) `ai-libs/CoreGrid` 里的 `GameplayHud``PauseMenu``OptionBrowser` 都在大量使用这种声明式绑定方式。
- [GetNode 生成器](./get-node-generator)
- [ContextAware 生成器](./context-aware-generator) ## 与 GetNode 的协作边界
- [Godot 信号文档](https://docs.godotengine.org/en/stable/classes/class_signal.html)
`[BindNodeSignal]` 不负责拿到字段实例,只负责在字段已经可用的前提下做事件接线。
因此同类型同时使用时,顺序应该是:
1. `__InjectGetNodes_Generated()`
2. `__BindNodeSignals_Generated()`
3. 在 `_ExitTree()` 调用 `__UnbindNodeSignals_Generated()`
这是当前项目侧真实采用路径,不是文档偏好。
## 当前强约束
以下约束直接来自生成器源码与测试:
- 目标类型必须是顶层 `partial class`
- 不支持嵌套类
- 方法不能是 `static`
- 节点字段必须存在且是实例字段
- 节点字段类型必须继承 `Godot.Node`
- 事件名必须是 CLR event不是任意字符串
- 如果你自己声明了 `__BindNodeSignals_Generated()``__UnbindNodeSignals_Generated()`,会触发命名冲突诊断
## 什么时候适合用 `[BindNodeSignal]`
适合:
- UI、菜单、HUD、面板类里按钮或输入事件很多
- 你想把订阅/解绑语义放回方法声明旁边,而不是堆在 `_Ready()` / `_ExitTree()`
- 你已经用 `[GetNode]` 或其他方式稳定拿到节点字段
不适合:
- 事件目标需要在运行时动态决定
- 你用的是 `Connect()` / `Disconnect()` 风格,而不是 CLR event
- 你需要比“字段 + 事件名”更复杂的订阅条件
## 与旧写法的边界
下面这些旧说法已经不准确:
- “`[BindNodeSignal]` 会自动生成 `_Ready()` / `_ExitTree()`
- “它能处理所有 Godot signal 连接方式”
- “有没有 `__UnbindNodeSignals_Generated()` 都无所谓”
当前更准确的理解是:
- 它只生成成对的绑定/解绑辅助方法
- 当前设计面向 CLR event不自动调用 `Connect()` / `Disconnect()`
- 如果要避免节点退出后残留订阅,应在 `_ExitTree()` 中显式解绑
## 推荐阅读
1. [/zh-CN/source-generators/get-node-generator](./get-node-generator.md)
2. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
3. [/zh-CN/godot/ui](../godot/ui.md)
4. `GFramework.Godot.SourceGenerators/README.md`

View File

@ -0,0 +1,127 @@
---
title: CQRS Handler Registry 生成器
description: 为消费端程序集生成 CQRS handler registry并在需要时附带精确 reflection fallback 元数据。
---
# CQRS Handler Registry 生成器
`GFramework.Cqrs.SourceGenerators` 会在编译期为当前业务程序集生成 `ICqrsHandlerRegistry`,让 `GFramework.Cqrs`
runtime 在注册 handlers 时优先走静态注册表,而不是先扫描整个程序集。
它服务的是 `Cqrs` 家族,不是独立运行时:
- 契约层:`GeWuYou.GFramework.Cqrs.Abstractions`
- 默认 runtime`GeWuYou.GFramework.Cqrs`
- 编译期生成器:`GeWuYou.GFramework.Cqrs.SourceGenerators`
## 生成什么
当前生成器会分析消费端程序集中的:
- `IRequestHandler<,>`
- `INotificationHandler<>`
- `IStreamRequestHandler<,>`
然后输出两类结果:
1. 一个实现 `ICqrsHandlerRegistry` 的内部注册器类型
2. 程序集级 `CqrsHandlerRegistryAttribute`
当某些 handler 不能被生成代码安全地直接引用时,还会补发:
- 程序集级 `CqrsReflectionFallbackAttribute`
这意味着运行时会先使用生成注册器完成可静态表达的映射,再只对剩余类型做补扫,而不是退回整程序集盲扫。
## 最小接入路径
安装方式保持 runtime 包与生成器包版本一致,并把生成器作为编译期依赖引入:
```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>
```
运行时侧仍然按 `Core` 的标准入口注册程序集:
```csharp
protected override void OnInitialize()
{
RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly);
}
```
如果你的 handlers 分布在多个业务程序集里,则改用:
```csharp
RegisterCqrsHandlersFromAssemblies(
[
typeof(InventoryCqrsMarker).Assembly,
typeof(BattleCqrsMarker).Assembly
]);
```
## 运行时如何消费生成结果
`Cqrs` runtime 当前的注册顺序是:
1. 先读取程序集上的 `CqrsHandlerRegistryAttribute`
2. 优先激活生成的 `ICqrsHandlerRegistry`
3. 若生成元数据损坏、registry 不可激活,记录告警并回退到反射路径
4. 若存在 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
5. 同一程序集按稳定键去重,避免重复注册
这个行为由 `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs`
`GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 共同覆盖。
## 什么时候值得安装
推荐安装:
- 业务程序集内 handler 数量较多
- 想把 handler 注册路径前移到编译期
- 希望冷启动阶段减少整程序集反射扫描
- 需要更明确地观察“哪些 handler 走静态注册,哪些只能走 fallback”
可以先不装:
- 项目体量很小handler 很少
- 当前只做原型,尚不关心注册成本
- 你还没稳定到 `Cqrs` runtime 的最终接入边界
## fallback 边界
生成器并不会承诺“所有 handler 都能被静态表达”。
当前实现遵循一个保守原则:
- 能直接引用的 handler生成直接注册语句
- 实现类型不能直接引用、但服务接口还能精确表达时,生成反射实现类型查找
- 服务接口本身也需要运行时解析时,生成精确 type lookup
- 只有在 runtime 提供 `CqrsReflectionFallbackAttribute` 合同时,才允许发射依赖 fallback 的结果
如果当前编译环境缺少这个 fallback 合同,而某些 handler 又必须依赖它,生成器会报:
- `GF_Cqrs_001`
这条诊断的含义不是“某个 handler 写错了”,而是“当前 runtime 合同不足以安全承载这轮生成结果”。
## XML / API 阅读入口
如果你要核对生成器对外暴露的契约,优先看这些类型:
- `GFramework.Cqrs.ICqrsHandlerRegistry`
- `GFramework.Cqrs.CqrsHandlerRegistryAttribute`
- `GFramework.Cqrs.CqrsReflectionFallbackAttribute`
- `GFramework.Cqrs.SourceGenerators.Cqrs.CqrsHandlerRegistryGenerator`
模块族入口见:
- [../core/cqrs.md](../core/cqrs.md)
- [./index.md](./index.md)

View File

@ -1,496 +1,198 @@
---
title: GetNode 生成器
description: 说明 [GetNode] 当前生成什么、路径如何推断,以及 _Ready 生命周期里的接入边界。
---
# GetNode 生成器 # GetNode 生成器
> 自动生成 Godot 节点获取逻辑,简化节点引用代码 `[GetNode]` 用来把 Godot 节点查找样板收敛到生成器里。它只处理“字段如何取到节点”,不负责事件订阅,也不负责其他运行时装配。
## 概述 ## 当前包关系
GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode<T>()` 方法。这在处理复杂 - 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
UI 或场景树结构时特别有用。 - 生成器实现:`GFramework.Godot.SourceGenerators`
- 目标类型基线:字段类型必须继承 `Godot.Node`
### 核心功能 ## 最小用法
- **自动节点获取**:根据路径或字段名自动获取节点
- **多种查找模式**:支持唯一名、相对路径、绝对路径查找
- **可选节点支持**:可以标记节点为可选,获取失败时返回 null
- **智能路径推导**:未显式指定路径时自动从字段名推导
- **_Ready 钩子生成**:自动生成 `_Ready()` 方法注入节点获取逻辑
## 基础使用
### 标记节点字段
使用 `[GetNode]` 特性标记需要自动获取的节点字段:
```csharp ```csharp
using GFramework.Godot.SourceGenerators.Abstractions; using GFramework.Godot.SourceGenerators.Abstractions;
using Godot; using Godot;
public partial class PlayerHud : Control public partial class TopBar : HBoxContainer
{ {
[GetNode] [GetNode]
private Label _healthLabel = null!; private HBoxContainer _leftContainer = null!;
[GetNode] [GetNode]
private ProgressBar _manaBar = null!; private HBoxContainer m_rightContainer = null!;
[GetNode("ScoreContainer/ScoreValue")]
private Label _scoreLabel = null!;
public override void _Ready()
{
__InjectGetNodes_Generated();
_healthLabel.Text = "100";
}
} }
``` ```
### 生成的代码 如果目标类型还没有 `_Ready()`,当前生成器会补出:
编译器会为标记的类自动生成以下代码:
```csharp ```csharp
// <auto-generated /> private void __InjectGetNodes_Generated()
#nullable enable
namespace YourNamespace;
partial class PlayerHud
{ {
private void __InjectGetNodes_Generated() _leftContainer = GetNode<global::Godot.HBoxContainer>("%LeftContainer");
{ m_rightContainer = GetNode<global::Godot.HBoxContainer>("%RightContainer");
_healthLabel = GetNode<global::Godot.Label>("%HealthLabel");
_manaBar = GetNode<global::Godot.ProgressBar>("%ManaBar");
_scoreLabel = GetNode<global::Godot.Label>("ScoreContainer/ScoreValue");
}
partial void OnGetNodeReadyGenerated();
public override void _Ready()
{
__InjectGetNodes_Generated();
OnGetNodeReadyGenerated();
}
}
```
## 配置选项
### 节点查找模式
通过 `Lookup` 参数控制节点查找方式:
```csharp
public partial class GameHud : Control
{
// 自动推断(默认):根据路径前缀自动选择
[GetNode]
private Label _titleLabel = null!; // 默认使用唯一名 %TitleLabel
// 唯一名查找
[GetNode(Lookup = NodeLookupMode.UniqueName)]
private Button _startButton = null!; // %StartButton
// 相对路径查找
[GetNode("UI/HealthBar", Lookup = NodeLookupMode.RelativePath)]
private ProgressBar _healthBar = null!;
// 绝对路径查找
[GetNode("/root/Main/GameUI/Score", Lookup = NodeLookupMode.AbsolutePath)]
private Label _scoreLabel = null!;
}
```
### 查找模式说明
| 模式 | 路径前缀 | 适用场景 |
|----------------|------|----------------|
| `Auto` | 自动选择 | 默认行为,推荐用于大多数场景 |
| `UniqueName` | `%` | 场景中使用唯一名的节点 |
| `RelativePath` | 无 | 需要相对路径查找的节点 |
| `AbsolutePath` | `/` | 场景树根节点的绝对路径 |
### 可选节点
对于可能不存在的节点,可以设置为非必填:
```csharp
public partial class SettingsPanel : Control
{
// 必须存在的节点(默认)
[GetNode]
private Label _titleLabel = null!;
// 可选节点,可能不存在
[GetNode(Required = false)]
private Label? _debugLabel; // 使用可空类型
// 显式路径的可选节点
[GetNode("AdvancedOptions", Required = false)]
private VBoxContainer? _advancedOptions;
public override void _Ready()
{
__InjectGetNodes_Generated();
// 安全地访问可选节点
_debugLabel?.Hide();
_advancedOptions?.Hide();
}
}
```
### 路径规则
生成器根据字段名和配置自动推导节点路径:
```csharp
public partial class Example : Control
{
// 驼峰命名 → PascalCase 路径
[GetNode]
private Label _playerNameLabel = null!; // → %PlayerNameLabel
// m_ 前缀会被移除
[GetNode]
private Button m_confirmButton = null!; // → %ConfirmButton
// _ 前缀会被移除
[GetNode]
private ProgressBar _healthBar = null!; // → %HealthBar
// 显式路径优先于推导
[GetNode("UI/CustomPath")]
private Label _myLabel = null!; // → UI/CustomPath
}
```
## 高级用法
### 与 [ContextAware] 组合使用
在 Godot 项目中结合使用 `[GetNode]``[ContextAware]`
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using Godot;
[ContextAware]
public partial class GameController : Node
{
[GetNode]
private Label _scoreLabel = null!;
[GetNode("HUD/HealthBar")]
private ProgressBar _healthBar = null!;
private IGameModel _gameModel = null!;
public override void _Ready()
{
__InjectContextBindings_Generated(); // ContextAware 生成
__InjectGetNodes_Generated(); // GetNode 生成
_gameModel.Score.Register(OnScoreChanged);
}
private void OnScoreChanged(int newScore)
{
_scoreLabel.Text = newScore.ToString();
}
}
```
### 复杂 UI 场景
处理复杂的嵌套 UI 结构:
```csharp
public partial class InventoryUI : Control
{
// 主容器
[GetNode]
private GridContainer _itemGrid = null!;
// 详细信息面板
[GetNode("DetailsPanel/ItemName")]
private Label _itemNameLabel = null!;
[GetNode("DetailsPanel/ItemDescription")]
private RichTextLabel _itemDescription = null!;
// 操作按钮
[GetNode("Actions/UseButton")]
private Button _useButton = null!;
[GetNode("Actions/DropButton")]
private Button _dropButton = null!;
// 可选的统计信息
[GetNode("DetailsPanel/Stats", Required = false)]
private VBoxContainer? _statsContainer;
public override void _Ready()
{
__InjectGetNodes_Generated();
// 使用注入的节点
_useButton.Pressed += OnUseButtonPressed;
_dropButton.Pressed += OnDropButtonPressed;
}
}
```
### 手动 _Ready 调用
如果类已经有 `_Ready()` 方法,需要手动调用注入方法:
```csharp
public partial class CustomHud : Control
{
[GetNode]
private Label _statusLabel = null!;
public override void _Ready()
{
// 必须手动调用节点注入
__InjectGetNodes_Generated();
// 自定义初始化逻辑
_statusLabel.Text = "Ready";
InitializeOtherComponents();
}
partial void OnGetNodeReadyGenerated()
{
// 这个方法会被生成器调用,可以在此添加额外初始化
}
}
```
**注意**:如果不手动调用 `__InjectGetNodes_Generated()`,编译器会发出警告 `GF_Godot_GetNode_006`
## 诊断信息
生成器会在以下情况报告编译错误或警告:
### GF_Godot_GetNode_001 - 不支持嵌套类
**错误信息**`Class '{ClassName}' cannot use [GetNode] inside a nested type`
**解决方案**:将嵌套类提取为独立的类
```csharp
// ❌ 错误
public partial class Outer
{
public partial class Inner
{
[GetNode]
private Label _label = null!; // 错误
}
} }
// ✅ 正确 partial void OnGetNodeReadyGenerated();
public partial class Inner
{
[GetNode]
private Label _label = null!;
}
```
### GF_Godot_GetNode_002 - 不支持静态字段
**错误信息**`Field '{FieldName}' cannot be static when using [GetNode]`
**解决方案**:改为实例字段
```csharp
// ❌ 错误
[GetNode]
private static Label _label = null!;
// ✅ 正确
[GetNode]
private Label _label = null!;
```
### GF_Godot_GetNode_003 - 不支持只读字段
**错误信息**`Field '{FieldName}' cannot be readonly when using [GetNode]`
**解决方案**:移除 `readonly` 关键字
```csharp
// ❌ 错误
[GetNode]
private readonly Label _label = null!;
// ✅ 正确
[GetNode]
private Label _label = null!;
```
### GF_Godot_GetNode_004 - 字段类型必须继承自 Godot.Node
**错误信息**`Field '{FieldName}' must be a Godot.Node type to use [GetNode]`
**解决方案**:确保字段类型继承自 `Godot.Node`
```csharp
// ❌ 错误
[GetNode]
private string _text = null!; // string 不是 Node 类型
// ✅ 正确
[GetNode]
private Label _label = null!; // Label 继承自 Node
```
### GF_Godot_GetNode_005 - 无法推导路径
**错误信息**`Field '{FieldName}' does not provide a path and its name cannot be converted to a node path`
**解决方案**:显式指定节点路径
```csharp
// ❌ 错误:字段名无法转换为有效路径
[GetNode]
private Label _ = null!;
// ✅ 正确
[GetNode("UI/Label")]
private Label _ = null!;
```
### GF_Godot_GetNode_006 - 需要在 _Ready 中调用注入方法
**警告信息**
`Class '{ClassName}' defines _Ready(); call __InjectGetNodes_Generated() there or remove _Ready() to use the generated hook`
**解决方案**:在 `_Ready()` 中手动调用 `__InjectGetNodes_Generated()`
```csharp
public partial class MyHud : Control
{
[GetNode]
private Label _label = null!;
public override void _Ready()
{
__InjectGetNodes_Generated(); // ✅ 必须手动调用
// 其他初始化...
}
}
```
## 最佳实践
### 1. 使用一致的命名约定
保持字段名与场景树中节点名的一致性:
```csharp
// ✅ 推荐:字段名与节点名一致
[GetNode]
private Label _healthLabel = null!; // 场景中的节点名为 HealthLabel
[GetNode]
private Button _startButton = null!; // 场景中的节点名为 StartButton
```
### 2. 优先使用唯一名查找
在 Godot 编辑器中为重要节点启用唯一名Unique Name然后使用 `[GetNode]`
```csharp
// Godot 场景中:%HealthBar唯一名已启用
// C# 代码中:
[GetNode]
private ProgressBar _healthBar = null!; // 自动使用 %HealthBar
```
### 3. 合理处理可选节点
对于可能不存在的节点,使用 `Required = false`
```csharp
public partial class DynamicUI : Control
{
[GetNode]
private Label _titleLabel = null!;
// 可选组件
[GetNode(Required = false)]
private TextureRect? _iconImage;
public override void _Ready()
{
__InjectGetNodes_Generated();
// 安全地初始化可选组件
if (_iconImage != null)
{
_iconImage.Texture = LoadDefaultIcon();
}
}
}
```
### 4. 组织复杂 UI 的路径
对于深层嵌套的 UI使用显式路径
```csharp
public partial class ComplexUI : Control
{
// 使用相对路径明确表达层级关系
[GetNode("MainContent/Header/Title")]
private Label _title = null!;
[GetNode("MainContent/Body/Stats/Health")]
private Label _healthValue = null!;
[GetNode("MainContent/Footer/ActionButtons/Save")]
private Button _saveButton = null!;
}
```
### 5. 与 GetNode 方法的对比
| 方式 | 代码量 | 可维护性 | 类型安全 | 推荐场景 |
|----------------|-----|------|--------|-----------|
| 手动 `GetNode()` | 多 | 中 | 需要显式转换 | 简单场景 |
| `[GetNode]` 特性 | 少 | 高 | 编译时检查 | 复杂 UI、控制器 |
```csharp
// ❌ 不推荐:手动获取
public override void _Ready() public override void _Ready()
{ {
_healthLabel = GetNode<Label>("%HealthLabel"); __InjectGetNodes_Generated();
_manaBar = GetNode<ProgressBar>("%ManaBar"); OnGetNodeReadyGenerated();
_scoreLabel = GetNode<Label>("ScoreContainer/ScoreValue");
} }
```
// ✅ 推荐:使用 [GetNode] 特性 这个行为来自当前生成器测试,不是文档约定。
[GetNode]
private Label _healthLabel = null!;
[GetNode] ## 当前路径推断规则
private ProgressBar _manaBar = null!;
### 没写路径时
如果 `[GetNode]` 没有显式路径,当前默认按字段名推导唯一名路径:
- `_leftContainer` -> `%LeftContainer`
- `m_rightContainer` -> `%RightContainer`
也就是说,默认不是普通相对路径,而是 Godot 的 `%Name` 唯一名语法。
### 显式路径优先
```csharp
[GetNode("ScoreContainer/ScoreValue")] [GetNode("ScoreContainer/ScoreValue")]
private Label _scoreLabel = null!; private Label _scoreLabel = null!;
```
显式路径会直接进入生成结果,不再按字段名推断。
## `Lookup``Required` 的当前语义
### `Lookup`
`GetNodeAttribute.Lookup` 支持 4 个模式:
- `Auto`
- `UniqueName`
- `RelativePath`
- `AbsolutePath`
对文档来说,最关键的结论是:
- `Auto` 在未给路径时默认走唯一名推断
- 显式路径会结合 `Lookup` 决定最终生成的字符串
### `Required`
默认 `Required = true`,生成器会调用 `GetNode<T>()`
```csharp
[GetNode]
private Label _title = null!;
```
如果设为 `false`,生成器会改用 `GetNodeOrNull<T>()`
```csharp
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
private HBoxContainer? _rightContainer;
```
当前生成结果会是:
```csharp
_rightContainer = GetNodeOrNull<global::Godot.HBoxContainer>("RightContainer");
```
所以可选节点最好同时用可空字段类型表达你的意图。
## 生命周期边界
### 没有 `_Ready()`
生成器会补:
- `__InjectGetNodes_Generated()`
- `partial void OnGetNodeReadyGenerated()`
- 一个 `public override void _Ready()`
`OnGetNodeReadyGenerated()` 只在这种“生成器自己补 `_Ready()`”的路径里出现。
### 已经有 `_Ready()`
如果类型已经实现了 `_Ready()`,生成器不会覆盖它,也不会再额外生成 `OnGetNodeReadyGenerated()`。你必须自己调用:
```csharp
public override void _Ready() public override void _Ready()
{ {
__InjectGetNodes_Generated(); __InjectGetNodes_Generated();
} }
``` ```
## 相关文档 如果 `_Ready()` 存在但没有调用生成方法,当前会给出 warning提醒你手动接入。
- [Source Generators 概述](./index) ## 当前强约束
- [BindNodeSignal 生成器](./bind-node-signal-generator)
- [ContextAware 生成器](./context-aware-generator) 这些约束都直接来自生成器源码和测试:
- [Godot 节点文档](https://docs.godotengine.org/en/stable/classes/class_node.html)
- 目标类型必须是顶层 `partial class`
- 不支持嵌套类
- 字段必须是实例字段
- 字段不能是 `readonly`
- 字段类型必须继承 `Godot.Node`
- 如果无法从字段名或显式参数推断出路径,会报错
- 如果你自己定义了 `__InjectGetNodes_Generated()`,会触发命名冲突诊断
## 与 BindNodeSignal 的配合顺序
如果同一个类型同时用了 `[GetNode]``[BindNodeSignal]`,当前推荐顺序是:
```csharp
public override void _Ready()
{
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
}
```
先注入节点,再绑定事件;否则 `BindNodeSignal` 对应的字段还没完成解析。
这也是 `ai-libs/CoreGrid` 里项目侧节点类的实际用法。
## 什么时候适合用 `[GetNode]`
适合:
- 节点字段很多,`GetNode<T>()` 样板明显重复
- 你希望把“字段名到节点路径”的约定收敛到声明式特性
- 你在 Godot `Control``Node``CanvasLayer` 等项目侧类型上频繁访问子节点
不适合:
- 目标不是 `Godot.Node`
- 节点路径完全动态,必须在运行时决定
- 你需要更复杂的节点查找策略,而不是字段级静态描述
## 与旧写法的边界
下面这些旧理解已经不准确:
- “`[GetNode]` 总会自动帮你改写 `_Ready()`
- “不管是否已有 `_Ready()`,都会生成 `OnGetNodeReadyGenerated()`
- “可选节点只是文档建议,生成结果不会变”
当前更准确的理解是:
- 只有缺少 `_Ready()` 时才会自动补 override
- `OnGetNodeReadyGenerated()` 只存在于自动补 `_Ready()` 的路径
- `Required = false` 会真实切换到 `GetNodeOrNull<T>()`
## 推荐阅读
1. [/zh-CN/source-generators/bind-node-signal-generator](./bind-node-signal-generator.md)
2. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
3. [/zh-CN/godot/ui](../godot/ui.md)
4. `GFramework.Godot.SourceGenerators/README.md`

View File

@ -1,52 +1,64 @@
---
title: Godot 项目元数据生成器
description: 说明 project.godot 当前会生成什么、何时生效,以及 AutoLoad 和 Input Action 的映射边界。
---
# Godot 项目元数据生成器 # Godot 项目元数据生成器
> 从 `project.godot` 生成 AutoLoad 与 Input Action 的强类型访问入口。 `GodotProjectMetadataGenerator` 读取 `project.godot`,把 Godot 工程级配置转成稳定的编译期入口。
## 概述 当前只覆盖两类信息:
`GFramework.Godot.SourceGenerators` 会读取 Godot 项目根目录下的 `project.godot`,并把其中最常用的项目级元数据暴露为稳定的编译期 - `[autoload]` 段生成 `GFramework.Godot.Generated.AutoLoads`
API。 - `[input]` 段生成 `GFramework.Godot.Generated.InputActions`
当前覆盖: 它不处理场景节点注入,也不处理节点事件绑定。这两部分分别由 `/zh-CN/source-generators/get-node-generator`
`/zh-CN/source-generators/bind-node-signal-generator` 负责。
- `[autoload]` 段:生成 `GFramework.Godot.Generated.AutoLoads` ## 当前包关系
- `[input]` 段:生成 `GFramework.Godot.Generated.InputActions`
这项能力的目标不是替代场景级生成器,而是把 Godot 工程配置和 C# 代码之间的字符串约定收敛到编译期。 - 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
- 生成器实现:`GFramework.Godot.SourceGenerators`
- 运行时依赖:`GFramework.Godot`
- 消费侧生成命名空间:`GFramework.Godot.Generated`
## 接入方式 ## 最小接入路径
### NuGet 引用 ### NuGet 引用
当项目通过 NuGet 引用 `GeWuYou.GFramework.Godot.SourceGenerators` 时,生成器会默认把项目根目录下的 `project.godot` 加入 常规 Godot C# 项目安装 `GeWuYou.GFramework.Godot.SourceGenerators` 后,包内 `targets` 会自动做两件事:
`AdditionalFiles`
如需覆盖默认路径,可以设置: 1. 注入 analyzer
2. 如果项目根目录存在 `project.godot`,把它加入 `AdditionalFiles`
- 可以改成项目根目录下的其他相对路径
- 文件名必须仍然是 `project.godot`,否则生成器会给出警告并忽略该文件
```xml ```xml
<PropertyGroup> <ItemGroup>
<GFrameworkGodotProjectFile>Config/project.godot</GFrameworkGodotProjectFile> <PackageReference Include="GeWuYou.GFramework.Godot.SourceGenerators"
</PropertyGroup> Version="x.y.z"
PrivateAssets="all"
ExcludeAssets="runtime" />
</ItemGroup>
``` ```
### 仓库内直接引用生成器 ### 仓库内直接引用生成器
如果你通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器项目,则需要手动加入: 如果你通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器项目,需要自己把 `project.godot` 放进
`AdditionalFiles`
```xml ```xml
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\GFramework.Godot.SourceGenerators\GFramework.Godot.SourceGenerators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<AdditionalFiles Include="project.godot" /> <AdditionalFiles Include="project.godot" />
</ItemGroup> </ItemGroup>
``` ```
## AutoLoad 访问层 ## 当前会生成什么
### 基础行为 ### AutoLoad 入口
假设 `project.godot`声明了 假设 `project.godot`
```ini ```ini
[autoload] [autoload]
@ -66,33 +78,14 @@ if (AutoLoads.TryGetAudioBus(out var audioBus))
} }
``` ```
- 对于能唯一映射到 C# 节点类型的条目,属性会是强类型的 当前输出同时包含:
- 对于无法映射或对应非 C# 脚本的条目,属性会退化为 `Godot.Node`
- 生成器通过 `Godot.Engine.GetMainLoop()` 与当前 `SceneTree.Root` 解析 `/root/<AutoLoadName>` 节点
### 显式映射 - `AutoLoads.<Name>`
- `AutoLoads.TryGet<Name>(out TNode? value)`
当 AutoLoad 名称无法仅靠类名唯一推断时,可以使用 `[AutoLoad]` 明确指定: 这些访问器最终都通过当前 `SceneTree.Root` 解析 `/root/<AutoLoadName>`
```csharp ### Input Action 常量
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[AutoLoad("GameServices")]
public partial class GameServices : Node
{
}
```
规则如下:
- 显式 `[AutoLoad]` 映射优先于隐式类名推断
- 标记了 `[AutoLoad]` 的类型必须继承 `Godot.Node`
- 若多个类型映射到同一个 AutoLoad生成器会报告诊断并退化为 `Godot.Node` 访问器,直到映射唯一
## Input Action 常量
### 基础行为
假设 `project.godot` 中有: 假设 `project.godot` 中有:
@ -114,59 +107,114 @@ if (Input.IsActionJustPressed(InputActions.MoveUp))
} }
``` ```
转换规则: 这部分只生成稳定字符串常量,不会替你封装 `Input` 调用。
- `move_up` -> `MoveUp` ## AutoLoad 类型推断的当前规则
- `ui_cancel` -> `UiCancel`
- 非法字符会被清理后再转换为 PascalCase
- 如果多个动作名落到同一个标识符,生成器会追加稳定数字后缀,例如 `MoveUp_2`
## 与现有 Godot 生成器的关系 ### 优先级顺序
这项能力和现有的场景级生成器是互补的 当前映射顺序是
- `AutoLoads` / `InputActions` 解决的是项目级元数据访问 1. 显式 `[AutoLoad("Name")]`
- `[GetNode]` 解决的是场景节点引用注入 2. 按 C# 类型名与 AutoLoad 名称做唯一匹配
- `[BindNodeSignal]` 解决的是节点事件订阅样板 3. 无法唯一确定时退化为 `Godot.Node`
推荐组合方式 例如
```csharp ```csharp
using GFramework.Godot.Generated;
using GFramework.Godot.SourceGenerators.Abstractions; using GFramework.Godot.SourceGenerators.Abstractions;
using Godot; using Godot;
public partial class MainHud : Control [AutoLoad("GameServices")]
public partial class GameServices : Node
{ {
[GetNode]
private Button _startButton = null!;
public override void _Ready()
{
__InjectGetNodes_Generated();
if (Input.IsActionPressed(InputActions.UiCancel))
{
}
var services = AutoLoads.GameServices;
}
} }
``` ```
这类显式映射优先于按类名推断。
### 什么时候会退化成 `Godot.Node`
以下情况不会中断全部生成,但会把对应入口退化成 `Godot.Node` 并报告诊断:
- 多个类型显式映射到同一个 AutoLoad
- 不同命名空间下出现同名 `Node` 类型,导致隐式推断不唯一
- 对应条目实际无法唯一绑定到一个 C# 节点类型
## `project.godot` 文件约束
### 可以改路径,不能改文件名
NuGet `targets` 支持通过 `GFrameworkGodotProjectFile` 改相对路径:
```xml
<PropertyGroup>
<GFrameworkGodotProjectFile>Config/project.godot</GFrameworkGodotProjectFile>
</PropertyGroup>
```
但当前生成器按文件名识别 `project.godot`,所以:
- `Config/project.godot` 可以
- `Config/game.project` 不可以
如果文件名不是 `project.godot``targets` 会给出 warning生成器也会忽略该文件。
### 缺文件或空节时不会生成任何代码
按当前测试,下面几种情况都不会产出源码,也不会报告额外诊断:
- 没有把 `project.godot` 传进 `AdditionalFiles`
- `project.godot` 是空文件
- `[autoload]` / `[input]` 只有空节,没有有效条目
## 标识符与重复条目的当前语义
### 标识符冲突
如果不同名字清洗后落到同一个 C# 标识符,生成器会追加稳定后缀并报告诊断,例如:
- `move_up` -> `MoveUp`
- `move-up` -> `MoveUp_2`
AutoLoad 名称也遵循同样的冲突处理策略。
### 重复条目
如果同一个 `project.godot` 里重复声明同名 AutoLoad 或 Input Action当前行为是
- 报告诊断
- 只保留第一条声明参与生成
这和“冲突后同时生成多个重名成员”不是一回事。
## 与场景级生成器的边界
这项能力解决的是“项目级元数据入口”:
- `AutoLoads`
- `InputActions`
场景级样板仍然需要其他生成器:
- 节点字段注入:`[GetNode]`
- 节点 CLR event 订阅:`[BindNodeSignal]`
`ai-libs/CoreGrid` 中,这三类能力是并行使用的:`project.godot` 负责 AutoLoad / Input Action具体 UI 或场景节点再通过
`[GetNode]``[BindNodeSignal]` 处理。
## 诊断与约束 ## 诊断与约束
当前会重点报告以下问题: 当前最值得记住的约束有这些
- `[AutoLoad]` 标记在非 `Godot.Node` 类型上 - `[AutoLoad]` 只能标在继承 `Godot.Node`类型上
- 多个类型映射到同一个 AutoLoad 名称 - 显式或隐式 AutoLoad 映射不唯一时,会退化为 `Godot.Node`
- 不同 AutoLoad 名称或 Input Action 名称在清洗后发生标识符冲突 - 标识符冲突会追加稳定后缀,而不是覆盖已有成员
- `project.godot` 内部重复声明同名 AutoLoad 或 Input Action - 重复条目只保留第一条声明
这些诊断的目的不是阻断所有生成,而是在可能的情况下保留稳定输出,同时把不确定性显式暴露出来。 ## 推荐阅读
## 相关文档 1. [/zh-CN/source-generators/get-node-generator](./get-node-generator.md)
2. [/zh-CN/source-generators/bind-node-signal-generator](./bind-node-signal-generator.md)
- [GetNode 生成器](./get-node-generator) 3. [/zh-CN/tutorials/godot-integration](../tutorials/godot-integration.md)
- [BindNodeSignal 生成器](./bind-node-signal-generator) 4. `GFramework.Godot.SourceGenerators/README.md`
- [Godot 集成教程](../tutorials/godot-integration)

View File

@ -65,7 +65,9 @@ GFramework 当前发布的生成器包是:
- 配置 schema 生成与运行时接法: - 配置 schema 生成与运行时接法:
- [../game/config-system.md](../game/config-system.md) - [../game/config-system.md](../game/config-system.md)
- CQRS registry 生成入口: - CQRS handler registry 生成器:
- [cqrs-handler-registry-generator](./cqrs-handler-registry-generator.md)
- CQRS 模块族采用入口:
- [../core/cqrs.md](../core/cqrs.md) - [../core/cqrs.md](../core/cqrs.md)
### Godot 专用生成器 ### Godot 专用生成器

File diff suppressed because it is too large Load Diff

View File

@ -28,24 +28,22 @@
### [Godot 集成教程](./godot-integration.md) ### [Godot 集成教程](./godot-integration.md)
> 深入学习 GFramework 与 Godot 引擎的深度集成,掌握高级开发技巧 > 按当前源码和真实项目接线,完成 Godot 项目级配置、场景节点生成器接入与运行时生命周期协作
**适合人群** **适合人群**
- 已完成基础教程的开发者 - 已完成基础教程的开发者
- 需要优化 Godot 项目性能的开发者 - 正在把现有 Godot C# 项目接入 GFramework 的开发者
- 希望实现复杂游戏系统的架构师 - 需要厘清 `project.godot``[GetNode]``[BindNodeSignal]` 边界的维护者
**学习内容** **学习内容**
- 节点生命周期管理 - `GeWuYou.GFramework.Godot` 与生成器包的职责划分
- 信号系统集成与桥接 - `project.godot``AutoLoads` / `InputActions` 的生成链路
- 资源管理优化策略 - `[GetNode]``[BindNodeSignal]``_Ready()` / `_ExitTree()` 的协作顺序
- 对象池化系统实现 - 常见旧写法迁移边界与后续阅读入口
- 性能优化最佳实践
- 调试与测试方法
**预计时间**3-4 小时 **预计时间**1-2 小时
--- ---