mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-06-10 04:14:29 +08:00
Compare commits
20 Commits
fe4fd1aa5e
...
a1dbed3c8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1dbed3c8d | ||
|
|
da0ae700a3 | ||
|
|
af21f16c09 | ||
|
|
704fdaa2c8 | ||
|
|
cc49b8638f | ||
|
|
45a87c6988 | ||
|
|
310aeafa57 | ||
|
|
b2a5555c75 | ||
|
|
76e7f68544 | ||
|
|
3ba1e3f202 | ||
|
|
03ecbe5989 | ||
|
|
5d436694f8 | ||
|
|
a77f79b3e2 | ||
|
|
aec1931c74 | ||
|
|
214f52b6c2 | ||
|
|
3425b299f0 | ||
|
|
a980a042ae | ||
|
|
a9f86348ff | ||
|
|
685897f2de | ||
|
|
8831cb42a8 |
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
`GFramework.Core.Abstractions` 承载 `Core` 运行时对应的接口、枚举和值对象,用来定义跨模块协作边界。
|
||||
|
||||
它只描述契约,不提供默认的架构、事件、状态、资源或 IoC 实现;这些实现都在 `GFramework.Core` 中。
|
||||
|
||||
## 什么时候单独依赖它
|
||||
|
||||
- 你在做插件、适配层或扩展包,只想依赖契约,不想把完整运行时拉进来
|
||||
@ -20,23 +22,34 @@
|
||||
|
||||
## 契约地图
|
||||
|
||||
| 目录 | 作用 |
|
||||
| 目录族 | 作用 |
|
||||
| --- | --- |
|
||||
| `Architectures/` | `IArchitecture`、模块、阶段监听与服务管理契约 |
|
||||
| `Command/` / `Query/` | 旧版命令与查询执行器接口 |
|
||||
| `Controller/` | `IController` |
|
||||
| `Events/` | 事件契约、解绑接口与传播上下文 |
|
||||
| `Model/` / `Systems/` / `Utility/` | 核心组件接口 |
|
||||
| `State/` / `StateManagement/` | 状态机、Store、reducer、selector 契约 |
|
||||
| `Property/` | `IBindableProperty` 与只读属性接口 |
|
||||
| `Resource/` | 资源管理与释放策略契约 |
|
||||
| `Localization/` | 本地化表、格式化与异常类型 |
|
||||
| `Logging/` | logger、log entry、factory 相关契约 |
|
||||
| `Ioc/` | `IIocContainer` |
|
||||
| `Lifecycle/` | 初始化 / 销毁生命周期契约 |
|
||||
| `Coroutine/` | 时间源、yield 指令与协程状态枚举 |
|
||||
| `Pause/` | 暂停栈、token 与状态事件 |
|
||||
| `Storage/` / `Serializer/` / `Versioning/` | 通用存储、序列化与版本化契约 |
|
||||
| `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、上下文、模块、服务模块、阶段监听、注册表基类与生命周期契约 |
|
||||
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` | 组件角色接口、优先级 / key 值对象、上下文感知约束与扩展边界 |
|
||||
| `Command/` `Query/` `Cqrs/` | 旧版命令 / 查询执行器接口,以及 `ICqrsRuntime` 这类新请求模型接线契约 |
|
||||
| `Events/` `Property/` `State/` `StateManagement/` | 事件总线、解绑对象、可绑定属性、状态机、Store / reducer / middleware 契约 |
|
||||
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | 协程状态、时间源、暂停栈、键控异步锁和统计对象 |
|
||||
| `Resource/` `Pool/` `Logging/` `Localization/` | 资源句柄、对象池、日志、日志工厂、本地化表与格式化契约 |
|
||||
| `Configuration/` `Environment/` | 配置管理器、环境对象与运行时环境访问契约 |
|
||||
| `Data/` `Serializer/` `Storage/` `Versioning/` | 数据装载、序列化、存储与版本化契约 |
|
||||
| `Enums/` `Properties/` | 架构阶段枚举,以及架构 / logger 相关属性键 |
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
截至 `2026-04-22`,已按顶层目录对 `GFramework.Core.Abstractions` 的公开 / 内部类型声明做过一轮轻量盘点;当前契约目录族的类型声明都已带
|
||||
XML 注释。这里记录的是类型族级基线,成员级契约细节仍需要在后续波次继续审计。
|
||||
|
||||
| 类型族 | 基线状态 | 代表类型 |
|
||||
| --- | --- | --- |
|
||||
| `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 包
|
||||
|
||||
## 重点 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)
|
||||
- Core 抽象页:[`../docs/zh-CN/abstractions/core-abstractions.md`](../docs/zh-CN/abstractions/core-abstractions.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)
|
||||
|
||||
@ -62,6 +62,35 @@ public class ArchitectureLifecycleBehaviorTests
|
||||
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>
|
||||
/// 验证用户初始化失败时,等待 Ready 的任务会失败并进入 FailedInitialization 阶段。
|
||||
/// </summary>
|
||||
@ -183,7 +212,7 @@ public class ArchitectureLifecycleBehaviorTests
|
||||
public PhaseTrackingArchitecture(Action? onInitializeAction = null)
|
||||
{
|
||||
_onInitializeAction = onInitializeAction;
|
||||
PhaseChanged += phase => PhaseHistory.Add(phase);
|
||||
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -214,7 +243,7 @@ public class ArchitectureLifecycleBehaviorTests
|
||||
public DestroyOrderArchitecture(List<string> destroyOrder)
|
||||
{
|
||||
_destroyOrder = destroyOrder;
|
||||
PhaseChanged += phase => PhaseHistory.Add(phase);
|
||||
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -247,7 +276,7 @@ public class ArchitectureLifecycleBehaviorTests
|
||||
public FailingInitializationArchitecture(List<string> destroyOrder)
|
||||
{
|
||||
_destroyOrder = destroyOrder;
|
||||
PhaseChanged += phase => PhaseHistory.Add(phase);
|
||||
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -43,6 +43,6 @@ public abstract class TestArchitectureBase : Architecture
|
||||
_postRegistrationHook?.Invoke(this);
|
||||
|
||||
// 订阅阶段变更事件以记录历史
|
||||
PhaseChanged += phase => PhaseHistory.Add(phase);
|
||||
PhaseChanged += (_, eventArgs) => PhaseHistory.Add(eventArgs.Phase);
|
||||
}
|
||||
}
|
||||
@ -331,6 +331,63 @@ public class CoroutineSchedulerTests
|
||||
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>
|
||||
|
||||
@ -77,6 +77,73 @@ public class AsyncLogAppenderTests
|
||||
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]
|
||||
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
|
||||
{
|
||||
public void Append(LogEntry entry)
|
||||
|
||||
@ -49,6 +49,7 @@ public abstract class Architecture : IArchitecture
|
||||
// 初始化管理器
|
||||
_bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger);
|
||||
_lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger);
|
||||
_lifecycle.PhaseChanged += HandleLifecyclePhaseChanged;
|
||||
_componentRegistry = new ArchitectureComponentRegistry(
|
||||
this,
|
||||
resolvedConfiguration,
|
||||
@ -98,13 +99,17 @@ public abstract class Architecture : IArchitecture
|
||||
public virtual Action<IServiceCollection>? Configurator => null;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段变更事件(用于测试和扩展)
|
||||
/// 在架构生命周期阶段发生变化时触发。
|
||||
/// </summary>
|
||||
public event Action<ArchitecturePhase>? PhaseChanged
|
||||
{
|
||||
add => _lifecycle.PhaseChanged += value;
|
||||
remove => _lifecycle.PhaseChanged -= value;
|
||||
}
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 订阅者应通过 <see cref="ArchitecturePhaseChangedEventArgs.Phase" /> 读取当前阶段,而不是依赖内部生命周期对象。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 事件委托中的 <c>sender</c> 始终为当前 <see cref="Architecture" /> 实例,便于测试与外部扩展保持稳定的发布者契约。
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public event EventHandler<ArchitecturePhaseChangedEventArgs>? PhaseChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
@ -142,6 +147,21 @@ public abstract class Architecture : IArchitecture
|
||||
|
||||
#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
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -71,6 +71,7 @@ internal sealed class ArchitectureLifecycle(
|
||||
public void EnterPhase(ArchitecturePhase next)
|
||||
{
|
||||
_phaseCoordinator.EnterPhase(next);
|
||||
PhaseChanged?.Invoke(this, new ArchitecturePhaseChangedEventArgs(next));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -127,11 +128,7 @@ internal sealed class ArchitectureLifecycle(
|
||||
/// <summary>
|
||||
/// 阶段变更事件(用于测试和扩展)
|
||||
/// </summary>
|
||||
public event Action<ArchitecturePhase>? PhaseChanged
|
||||
{
|
||||
add => _phaseCoordinator.PhaseChanged += value;
|
||||
remove => _phaseCoordinator.PhaseChanged -= value;
|
||||
}
|
||||
public event EventHandler<ArchitecturePhaseChangedEventArgs>? PhaseChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@ -22,12 +22,6 @@ internal sealed class ArchitecturePhaseCoordinator(
|
||||
/// </summary>
|
||||
public ArchitecturePhase CurrentPhase { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 在架构阶段变更时触发。
|
||||
/// 该事件用于测试和扩展场景,保持现有公共行为不变。
|
||||
/// </summary>
|
||||
public event Action<ArchitecturePhase>? PhaseChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个生命周期钩子。
|
||||
/// 就绪后是否允许追加注册由架构配置控制,以保证阶段回调的一致性。
|
||||
@ -45,8 +39,8 @@ internal sealed class ArchitecturePhaseCoordinator(
|
||||
|
||||
/// <summary>
|
||||
/// 进入指定阶段并广播给所有阶段消费者。
|
||||
/// 顺序保持为“更新阶段值 → 生命周期钩子 → 容器中的阶段监听器 → 外部事件”,
|
||||
/// 以兼容既有调用约定。
|
||||
/// 顺序保持为“更新阶段值 → 生命周期钩子 → 容器中的阶段监听器”,
|
||||
/// 以保证框架扩展与运行时组件看到一致的阶段视图。
|
||||
/// </summary>
|
||||
/// <param name="next">目标阶段。</param>
|
||||
public void EnterPhase(ArchitecturePhase next)
|
||||
@ -61,7 +55,6 @@ internal sealed class ArchitecturePhaseCoordinator(
|
||||
|
||||
NotifyLifecycleHooks(next);
|
||||
NotifyPhaseListeners(next);
|
||||
PhaseChanged?.Invoke(next);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
30
GFramework.Core/Coroutine/CoroutineExceptionEventArgs.cs
Normal file
30
GFramework.Core/Coroutine/CoroutineExceptionEventArgs.cs
Normal 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; }
|
||||
}
|
||||
42
GFramework.Core/Coroutine/CoroutineFinishedEventArgs.cs
Normal file
42
GFramework.Core/Coroutine/CoroutineFinishedEventArgs.cs
Normal 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; }
|
||||
}
|
||||
@ -91,7 +91,7 @@ public sealed class CoroutineScheduler(
|
||||
/// 为了避免阻塞调度器主循环,该事件会被派发到线程池回调中执行。
|
||||
/// 如果调用方需要与宿主线程保持一致,请同时订阅 <see cref="OnCoroutineFinished" />。
|
||||
/// </remarks>
|
||||
public event Action<CoroutineHandle, Exception>? OnCoroutineException;
|
||||
public event EventHandler<CoroutineExceptionEventArgs>? OnCoroutineException;
|
||||
|
||||
/// <summary>
|
||||
/// 当协程以完成、取消或失败任一结果结束时触发。
|
||||
@ -99,7 +99,7 @@ public sealed class CoroutineScheduler(
|
||||
/// <remarks>
|
||||
/// 该事件在调度器所在的驱动线程中同步触发,适合与宿主生命周期管理逻辑集成。
|
||||
/// </remarks>
|
||||
public event Action<CoroutineHandle, CoroutineCompletionStatus, Exception?>? OnCoroutineFinished;
|
||||
public event EventHandler<CoroutineFinishedEventArgs>? OnCoroutineFinished;
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定协程句柄是否仍然处于活跃状态。
|
||||
@ -622,7 +622,7 @@ public sealed class CoroutineScheduler(
|
||||
UpdateCompletionMetadata(handle, completionStatus);
|
||||
ReleaseCompletedCoroutine(slotIndex, slot, handle);
|
||||
CompleteCoroutineLifecycle(handle, completionStatus);
|
||||
OnCoroutineFinished?.Invoke(handle, completionStatus, exception);
|
||||
OnCoroutineFinished?.Invoke(this, new CoroutineFinishedEventArgs(handle, completionStatus, exception));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -642,7 +642,7 @@ public sealed class CoroutineScheduler(
|
||||
{
|
||||
try
|
||||
{
|
||||
handler(handle, ex);
|
||||
handler(this, new CoroutineExceptionEventArgs(handle, ex));
|
||||
}
|
||||
catch (Exception callbackEx)
|
||||
{
|
||||
|
||||
@ -23,6 +23,7 @@ public sealed class AsyncLogAppender : ILogAppender
|
||||
private readonly Action<Exception>? _processingErrorHandler;
|
||||
private readonly Task _processingTask;
|
||||
private bool _disposed;
|
||||
private int _isProcessingEntry;
|
||||
private volatile bool _flushRequested;
|
||||
|
||||
/// <summary>
|
||||
@ -117,14 +118,14 @@ public sealed class AsyncLogAppender : ILogAppender
|
||||
/// </summary>
|
||||
void ILogAppender.Flush()
|
||||
{
|
||||
var success = Flush();
|
||||
OnFlushCompleted?.Invoke(success);
|
||||
Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush 操作完成事件,参数指示是否成功(true)或超时(false)
|
||||
/// Flush 操作完成事件。
|
||||
/// 事件数据通过 <see cref="AsyncLogFlushCompletedEventArgs" /> 提供。
|
||||
/// </summary>
|
||||
public event Action<bool>? OnFlushCompleted;
|
||||
public event EventHandler<AsyncLogFlushCompletedEventArgs>? OnFlushCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新缓冲区,等待所有日志写入完成
|
||||
@ -140,12 +141,13 @@ public sealed class AsyncLogAppender : ILogAppender
|
||||
|
||||
// 请求刷新
|
||||
_flushRequested = true;
|
||||
TrySignalFlushCompletion();
|
||||
|
||||
try
|
||||
{
|
||||
// 等待处理任务发出完成信号
|
||||
var success = _flushSemaphore.Wait(actualTimeout);
|
||||
OnFlushCompleted?.Invoke(success);
|
||||
OnFlushCompleted?.Invoke(this, new AsyncLogFlushCompletedEventArgs(success));
|
||||
return success;
|
||||
}
|
||||
finally
|
||||
@ -166,6 +168,7 @@ public sealed class AsyncLogAppender : ILogAppender
|
||||
{
|
||||
try
|
||||
{
|
||||
Volatile.Write(ref _isProcessingEntry, 1);
|
||||
_innerAppender.Append(entry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -173,18 +176,12 @@ public sealed class AsyncLogAppender : ILogAppender
|
||||
// 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。
|
||||
ReportProcessingError(ex);
|
||||
}
|
||||
|
||||
// 检查是否有刷新请求且通道已空
|
||||
if (_flushRequested && _channel.Reader.Count == 0)
|
||||
finally
|
||||
{
|
||||
_innerAppender.Flush();
|
||||
|
||||
// 发出完成信号
|
||||
if (_flushSemaphore.CurrentCount == 0)
|
||||
{
|
||||
_flushSemaphore.Release();
|
||||
}
|
||||
Volatile.Write(ref _isProcessingEntry, 0);
|
||||
}
|
||||
|
||||
TrySignalFlushCompletion();
|
||||
}
|
||||
}
|
||||
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>
|
||||
/// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。
|
||||
/// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。
|
||||
|
||||
@ -10,9 +10,10 @@
|
||||
|
||||
- `Architecture` 与 `ArchitectureContext`
|
||||
- `Model` / `System` / `Utility` 运行时
|
||||
- 旧版 `Command` / `Query` 执行器
|
||||
- 事件、属性、状态机、状态管理
|
||||
- 资源、日志、协程、并发、环境与本地化
|
||||
- 旧版 `Command` / `Query` 执行器,以及与新版 `CQRS` runtime 的接线入口
|
||||
- 事件、属性、状态机、状态管理、规则与上下文扩展
|
||||
- 资源、对象池、日志、协程、并发、环境、配置与本地化
|
||||
- 服务模块管理、时间提供器与默认的 IoC 容器适配
|
||||
|
||||
它不负责:
|
||||
|
||||
@ -37,6 +38,7 @@
|
||||
| 目录 | 作用 |
|
||||
| --- | --- |
|
||||
| `Architectures/` | 架构入口、上下文、生命周期、模块安装与组件注册 |
|
||||
| `Services/` | 服务模块注册、生命周期协调与模块管理 |
|
||||
| `Command/` | 旧版命令执行器与同步 / 异步命令基类 |
|
||||
| `Query/` | 旧版查询执行器与同步 / 异步查询基类 |
|
||||
| `Events/` | 事件总线、事件作用域、统计与过滤 |
|
||||
@ -44,15 +46,37 @@
|
||||
| `State/` | 状态机与状态切换事件 |
|
||||
| `StateManagement/` | Store、selector、middleware 与状态诊断 |
|
||||
| `Coroutine/` | 协程调度、快照、统计与优先级 |
|
||||
| `Time/` | 默认时间提供器与协程时间源 |
|
||||
| `Resource/` | 资源缓存、句柄和释放策略 |
|
||||
| `Pool/` | 对象池系统与常用池化辅助实现 |
|
||||
| `Logging/` | logger、factory、配置与组合日志器 |
|
||||
| `Ioc/` | 基于 `Microsoft.Extensions.DependencyInjection` 的容器适配 |
|
||||
| `Concurrency/` | 键控异步锁与统计 |
|
||||
| `Configuration/` | 配置管理器与配置监听解绑对象 |
|
||||
| `Environment/` | 运行环境对象与上下文环境扩展 |
|
||||
| `Pause/` | 暂停栈和暂停范围 |
|
||||
| `Localization/` | 本地化表与格式化入口 |
|
||||
| `Rule/` | `ContextAwareBase` 等上下文感知基类 |
|
||||
| `Functional/` | `Option`、`Result` 等轻量函数式工具 |
|
||||
| `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
|
||||
@ -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/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)
|
||||
- 入门指南:[`../docs/zh-CN/getting-started/index.md`](../docs/zh-CN/getting-started/index.md)
|
||||
|
||||
@ -1144,6 +1144,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
string HandlerInterfaceDisplayName,
|
||||
string HandlerInterfaceLogName);
|
||||
|
||||
/// <summary>
|
||||
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该枚举只服务于输出排序与代码分支选择,用来保证生成注册器在“直接注册”
|
||||
/// “反射实现类型查找”和“精确运行时类型解析”之间保持稳定顺序。
|
||||
/// </remarks>
|
||||
private enum OrderedRegistrationKind
|
||||
{
|
||||
Direct,
|
||||
@ -1151,6 +1158,14 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
PreciseReflected
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述生成注册器中某个运行时类型引用的构造方式。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 某些 handler 服务类型可以直接以 <c>typeof(...)</c> 输出,某些则需要在运行时补充
|
||||
/// 反射查找、数组/指针封装或泛型实参重建。该记录把这些差异收敛为统一的递归结构,
|
||||
/// 供源码输出阶段生成稳定的类型解析语句。
|
||||
/// </remarks>
|
||||
private sealed record RuntimeTypeReferenceSpec(
|
||||
string? TypeDisplayName,
|
||||
string? ReflectionTypeMetadataName,
|
||||
@ -1161,18 +1176,27 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
RuntimeTypeReferenceSpec? GenericTypeDefinitionReference,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> GenericTypeArguments)
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个可直接通过 <c>typeof(...)</c> 表达的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(typeDisplayName, null, null, null, 0, null, null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个需要从当前消费端程序集反射解析的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, null, null, 0, null, null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个需要从被引用程序集反射解析的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromExternalReflectionLookup(
|
||||
string reflectionAssemblyName,
|
||||
string reflectionTypeMetadataName)
|
||||
@ -1182,18 +1206,27 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个数组类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(null, null, null, elementTypeReference, arrayRank, null, null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个指针类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromPointer(RuntimeTypeReferenceSpec pointedAtTypeReference)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(null, null, null, null, 0, pointedAtTypeReference, null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个封闭泛型类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromConstructedGeneric(
|
||||
RuntimeTypeReferenceSpec genericTypeDefinitionReference,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> genericTypeArguments)
|
||||
|
||||
@ -651,32 +651,70 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述某个程序集在生成注册器之后仍需运行时补扫的 handler 元数据。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该对象把“是否存在精确 fallback 类型列表”与“是否只能回退到整程序集扫描”收敛为同一份内部状态,
|
||||
/// 供注册流水线后续阶段统一判断。
|
||||
/// </remarks>
|
||||
private sealed class ReflectionFallbackMetadata(IReadOnlyList<Type> types)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取需要通过运行时反射补充注册的 handler 类型集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<Type> Types { get; } = types ?? throw new ArgumentNullException(nameof(types));
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否持有精确的 fallback 类型清单。
|
||||
/// </summary>
|
||||
public bool HasExplicitTypes => Types.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述单个程序集在注册阶段提取到的 generated registry 与 reflection fallback 元数据。
|
||||
/// </summary>
|
||||
private sealed class AssemblyRegistrationMetadata(
|
||||
IReadOnlyList<Type> registryTypes,
|
||||
ReflectionFallbackMetadata? reflectionFallbackMetadata)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取程序集上声明的 generated registry 类型集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<Type> RegistryTypes { get; } =
|
||||
registryTypes ?? throw new ArgumentNullException(nameof(registryTypes));
|
||||
|
||||
/// <summary>
|
||||
/// 获取该程序集是否还要求运行时补充 reflection fallback。
|
||||
/// </summary>
|
||||
public ReflectionFallbackMetadata? ReflectionFallbackMetadata { get; } = reflectionFallbackMetadata;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 缓存 generated registry 激活所需的类型判定结果与工厂委托。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该缓存把“是否实现契约”“是否为抽象类型”“是否已构建激活委托”封装为不可变快照,
|
||||
/// 避免对同一 registry 类型重复执行反射分析。
|
||||
/// </remarks>
|
||||
private sealed class RegistryActivationMetadata(
|
||||
bool implementsRegistryContract,
|
||||
bool isAbstract,
|
||||
Func<ICqrsHandlerRegistry>? factory)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取目标类型是否实现了 <see cref="ICqrsHandlerRegistry" />。
|
||||
/// </summary>
|
||||
public bool ImplementsRegistryContract { get; } = implementsRegistryContract;
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标类型是否为抽象类型。
|
||||
/// </summary>
|
||||
public bool IsAbstract { get; } = isAbstract;
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用于实例化 registry 的工厂委托。
|
||||
/// </summary>
|
||||
public Func<ICqrsHandlerRegistry>? Factory { get; } = factory;
|
||||
}
|
||||
}
|
||||
|
||||
103
GFramework.Ecs.Arch.Abstractions/README.md
Normal file
103
GFramework.Ecs.Arch.Abstractions/README.md
Normal 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)
|
||||
@ -1,16 +1,30 @@
|
||||
# GFramework.Ecs.Arch
|
||||
|
||||
GFramework 的 Arch ECS 集成包,提供开箱即用的 ECS(Entity 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. 安装包
|
||||
|
||||
@ -18,53 +32,43 @@ GFramework 的 Arch ECS 集成包,提供开箱即用的 ECS(Entity Component
|
||||
dotnet add package GeWuYou.GFramework.Ecs.Arch
|
||||
```
|
||||
|
||||
### 2. 注册 ECS 模块
|
||||
### 2. 在 `Initialize()` 之前显式接入 Arch runtime
|
||||
|
||||
按当前实现,`UseArch(...)` 会把 `ArchEcsModule` 提前登记到 `ArchitectureModuleRegistry`,因此调用时机应早于
|
||||
`Initialize()`。
|
||||
|
||||
```csharp
|
||||
// 在架构初始化时添加 Arch ECS 支持
|
||||
var architecture = new GameArchitecture(config)
|
||||
.UseArch(); // 添加 ECS 支持
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Ecs.Arch.Extensions;
|
||||
|
||||
architecture.Initialize();
|
||||
```
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
public GameArchitecture() : base(new ArchitectureConfiguration())
|
||||
{
|
||||
}
|
||||
|
||||
### 3. 带配置的注册
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
RegisterSystem<MovementSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
```csharp
|
||||
var architecture = new GameArchitecture(config)
|
||||
var architecture = new GameArchitecture()
|
||||
.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2000;
|
||||
options.EnableStatistics = true;
|
||||
options.WorldCapacity = 2048;
|
||||
options.Priority = 50;
|
||||
});
|
||||
|
||||
architecture.Initialize();
|
||||
```
|
||||
|
||||
```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. 创建系统
|
||||
### 3. 编写并注册系统
|
||||
|
||||
```csharp
|
||||
using Arch.Core;
|
||||
using GFramework.Ecs.Arch;
|
||||
using GFramework.Ecs.Arch.Components;
|
||||
|
||||
public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
@ -78,115 +82,57 @@ public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
|
||||
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;
|
||||
pos.Y += vel.Y * deltaTime;
|
||||
position.X += velocity.X * frameDelta;
|
||||
position.Y += velocity.Y * frameDelta;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 注册系统
|
||||
### 4. 初始化后获取 `World` 与 ECS 模块
|
||||
|
||||
```csharp
|
||||
public class MyArchitecture : Architecture
|
||||
{
|
||||
protected override void OnRegisterSystem(IIocContainer container)
|
||||
{
|
||||
container.Register<MovementSystem>();
|
||||
}
|
||||
}
|
||||
using Arch.Core;
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
var world = architecture.Context.GetService<World>();
|
||||
var ecsModule = architecture.Context.GetService<IArchEcsModule>();
|
||||
```
|
||||
|
||||
### 7. 创建实体
|
||||
### 5. 由宿主循环驱动更新
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
## 运行时职责地图
|
||||
|
||||
### 代码配置
|
||||
| 文件 | 作用 |
|
||||
| --- | --- |
|
||||
| `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
|
||||
var architecture = new GameArchitecture(config)
|
||||
.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2000;
|
||||
options.EnableStatistics = true;
|
||||
options.Priority = 50;
|
||||
});
|
||||
```
|
||||
## XML 阅读基线
|
||||
|
||||
### 配置说明
|
||||
下表记录当前模块 README 与源码可对照的类型声明级 XML 基线。
|
||||
|
||||
- `WorldCapacity` - World 初始容量(默认:1000)
|
||||
- `EnableStatistics` - 是否启用统计信息(默认:false)
|
||||
- `Priority` - 模块优先级(默认:50)
|
||||
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 装配入口 | `ArchExtensions` | 已覆盖 | `UseArch(...)` 的时机与返回值 |
|
||||
| 运行时模块 | `ArchEcsModule` | 已覆盖 | `World` 注册、系统排序、销毁顺序 |
|
||||
| 系统桥接层 | `ArchSystemAdapter<T>` | 已覆盖 | `OnArchInitialize`、`OnUpdate`、`OnArchDispose` |
|
||||
| 示例类型 | `Position`、`Velocity`、`MovementSystem` | 已覆盖 | 组件布局、查询写法、最小示例 |
|
||||
|
||||
## 架构说明
|
||||
## 对应文档入口
|
||||
|
||||
### 显式注册模式
|
||||
|
||||
本包采用 .NET 生态标准的显式注册模式,基于架构实例:
|
||||
|
||||
**优点:**
|
||||
|
||||
- ✅ 符合 .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
|
||||
- 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)
|
||||
- 抽象契约页:[`../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)
|
||||
|
||||
@ -781,15 +781,13 @@ public partial class Timing : Node
|
||||
/// <summary>
|
||||
/// 在协程结束时解除节点归属回调并清理索引。
|
||||
/// </summary>
|
||||
/// <param name="handle">已结束的协程句柄。</param>
|
||||
/// <param name="status">协程最终状态。</param>
|
||||
/// <param name="exception">若失败则为异常对象。</param>
|
||||
/// <param name="sender">触发事件的协程调度器。</param>
|
||||
/// <param name="eventArgs">协程结束事件数据。</param>
|
||||
private void HandleCoroutineFinished(
|
||||
CoroutineHandle handle,
|
||||
CoroutineCompletionStatus status,
|
||||
Exception? exception)
|
||||
object? sender,
|
||||
CoroutineFinishedEventArgs eventArgs)
|
||||
{
|
||||
CleanupOwnedCoroutineRegistration(handle);
|
||||
CleanupOwnedCoroutineRegistration(eventArgs.Handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
14
README.md
14
README.md
@ -27,11 +27,22 @@
|
||||
| `GFramework.Game.Abstractions` | `Game` 对应的契约层 | [README](GFramework.Game.Abstractions/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.Abstractions` | Arch ECS 集成对应的契约层,适合共享宿主循环与 ECS 模块边界 | [README](GFramework.Ecs.Arch.Abstractions/README.md) |
|
||||
| `GFramework.Core.SourceGenerators` | Core 侧通用源码生成器与分析器 | [README](GFramework.Core.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.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 与文档站点保持同一套栏目命名:
|
||||
@ -119,10 +130,13 @@ GFramework.sln
|
||||
├─ GFramework.Game.Abstractions/
|
||||
├─ GFramework.Godot/
|
||||
├─ GFramework.Ecs.Arch/
|
||||
├─ GFramework.Ecs.Arch.Abstractions/
|
||||
├─ GFramework.Core.SourceGenerators/
|
||||
├─ GFramework.Core.SourceGenerators.Abstractions/
|
||||
├─ GFramework.Game.SourceGenerators/
|
||||
├─ GFramework.Cqrs.SourceGenerators/
|
||||
├─ GFramework.Godot.SourceGenerators/
|
||||
├─ GFramework.Godot.SourceGenerators.Abstractions/
|
||||
├─ GFramework.SourceGenerators.Common/
|
||||
└─ docs/
|
||||
```
|
||||
|
||||
@ -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`.
|
||||
- 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`
|
||||
- `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`
|
||||
- Purpose: continue the coroutine semantics, host integration, observability, regression coverage, and migration-doc
|
||||
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.
|
||||
- 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`
|
||||
- `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
|
||||
|
||||
@ -64,10 +65,12 @@ help the current worktree land on the right recovery documents without scanning
|
||||
- Priority 1: `data-repository-persistence`
|
||||
- Branch: `docs/sdk-update-documentation`
|
||||
- Worktree hint: `GFramework-update-documentation`
|
||||
- Priority 1: `documentation-governance-and-refresh`
|
||||
|
||||
- Priority 1: `documentation-full-coverage-governance`
|
||||
## Archived Topics
|
||||
|
||||
- `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.
|
||||
- `documentation-governance-and-refresh`
|
||||
- Archive root: `ai-plan/public/archive/documentation-governance-and-refresh/`
|
||||
- Note: PR #268 已合并;文档治理与 Godot 栏目刷新阶段已完成,后续仅作为历史恢复材料保留。
|
||||
|
||||
@ -7,10 +7,14 @@
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-012`
|
||||
- 当前阶段:`Phase 12`
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-015`
|
||||
- 当前阶段:`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 类型,`MA0015` 与 `MA0077`
|
||||
只是当前最明显的低数量示例,不构成限定
|
||||
@ -24,8 +28,13 @@
|
||||
- 已完成当前 PR #265 review follow-up:修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚
|
||||
- 已继续完成当前 PR #265 review follow-up:修复 `Event<T>` 与 `Event<T, TK>` 监听器计数的 off-by-one,并补充回归测试
|
||||
- 已增强 `gframework-pr-review` 脚本与 skill 文档,降低超长 JSON 直出导致的 review 信号漏看风险
|
||||
- 当前 `PauseStackManager`、`Store`、`CoroutineScheduler` 与 `GFramework.Core` 的 `MA0048`
|
||||
文件/类型命名冲突已从 active 入口移除;主题内剩余 warning 主要集中在 `MA0046` delegate 形状、
|
||||
- 已完成 `GFramework.Core` 当前 `MA0046` 批次:将阶段、协程与异步日志事件统一迁移到 `EventHandler<TEventArgs>` 形状,
|
||||
并同步更新 `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` 两个低数量尾项
|
||||
|
||||
## 当前活跃事实
|
||||
@ -51,16 +60,24 @@
|
||||
委托导致的 `GetListenerCount()` off-by-one,并以定向事件测试验证注册、注销和计数语义
|
||||
- `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 映射
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 公共契约兼容风险:剩余 `MA0046` / `MA0016` 若直接改公开委托或集合类型,可能波及用户代码
|
||||
- 公共契约兼容风险:剩余 `MA0016` 若直接改公开集合类型,可能波及用户代码
|
||||
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
|
||||
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
|
||||
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
|
||||
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
|
||||
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
|
||||
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
|
||||
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
|
||||
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
|
||||
- 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership,主代理负责合并验证
|
||||
|
||||
@ -121,11 +138,32 @@
|
||||
- 结果:通过;`--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`
|
||||
- 结果:`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 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录
|
||||
2. 下一轮优先以 `MA0046` 为主批次启动,先从 `Architecture*` 与 `CoroutineScheduler` 的低风险 delegate 形状修正中选一个切入点
|
||||
3. 若 `MA0046` 的文件 ownership 可以清晰切分,允许使用不同模型的 subagent 并行处理互不冲突的目录或类型簇
|
||||
2. 下一轮优先在 `MA0016` 与 `MA0002` 之间选择低风险批次继续推进,默认先看 `LoggingConfiguration` /
|
||||
`FilterConfiguration` 与 `CollectionExtensions`
|
||||
3. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
|
||||
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
|
||||
|
||||
@ -1,5 +1,104 @@
|
||||
# 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
|
||||
|
||||
### 阶段:PR review workflow 输出收窄增强(RP-012)
|
||||
|
||||
@ -7,19 +7,26 @@
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-010`
|
||||
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-017`
|
||||
- 当前阶段:`Phase 3`
|
||||
- 当前焦点:
|
||||
- 已建立统一公开 skill:`.agents/skills/gframework-doc-refresh/`
|
||||
- 文档重构入口已从“按 guide/tutorial/api 类型拆 skill”收口为“按源码模块驱动文档刷新”
|
||||
- PR #268 的当前未解决 review 线程已进入收口:Scene/UI 标题层级修正、共享脚本 review 修复、`gframework-pr-review` 多 AI reviewer 支持补齐
|
||||
- 下一轮需要用统一 skill 推进 Godot 相关生成器页面核对
|
||||
- `docs/zh-CN/godot/index.md` 已改成源码优先的模块 landing page,不再把 `GetNodeX`、`CreateSignalBuilder`、`InstallGodotModule(...)` 写成默认入口
|
||||
- `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、站点入口与采用链路不再依赖旧文档自证
|
||||
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口
|
||||
- 当前主题仍是 active topic,因为 `source-generators` 栏目下的 Godot 相关页面仍可能包含与实现漂移的旧内容,且统一 skill 还需要在该场景上继续落地使用
|
||||
- 高优先级模块入口、`core` 关键专题页与 `tutorials/godot-integration.md` 已回到“以源码 / 测试 / README 为准”的状态
|
||||
- `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、动态优先级边界与诊断”的结构,
|
||||
不再把 `GetAllByPriority<T>()` / `system.Init()` 当作所有场景的默认示例
|
||||
- 本轮重写后再次执行 `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` 的 `description` 已加引号,修复 `Recommended command:` 中冒号导致的
|
||||
invalid YAML skill 加载警告
|
||||
@ -74,7 +116,12 @@
|
||||
|
||||
- 旧专题页示例失真风险:`docs/zh-CN/game/*` 与 `source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例
|
||||
- 缓解措施:`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 相关页面,不把旧文档当事实来源
|
||||
- Godot 栏目归档过早风险:虽然 `logging.md` 已完成收口,但如果在推送前就把当前阶段过早归档,后续 review 跟进会缺少
|
||||
清晰的 active 恢复入口
|
||||
- 缓解措施:先保留当前 topic 为 active;待确认本轮页面集与 PR #268 的 review 跟进节奏后,再决定是否迁入 `archive/`
|
||||
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
|
||||
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
|
||||
- 模块映射不全风险:统一 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 Godot.SourceGenerators`
|
||||
- `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` 与
|
||||
`bind-node-signal-generator.md`,优先用 `gframework-doc-refresh` 的模块扫描结果驱动判断
|
||||
2. 下一次推送后先重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛
|
||||
3. 再继续确认 `project.godot`、`AutoLoad` / `InputActions`、`GetNode` / `BindNodeSignal` 示例仍与当前包关系和生成器入口一致
|
||||
1. 评估当前 Godot 栏目页面集是否已足够稳定,决定是否把本阶段 active 恢复点收口并迁入 `archive/`
|
||||
2. 如需继续保持 active,优先精简 tracking / trace,只保留归档决策、当前风险与下一次 PR follow-up 入口
|
||||
3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛
|
||||
@ -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 是否关闭或减少
|
||||
@ -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 映射
|
||||
@ -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/` 读取归档材料
|
||||
@ -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 阻塞,避免影响后续代码类验证
|
||||
@ -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 内扩张为环境修复任务
|
||||
@ -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`
|
||||
@ -252,6 +252,7 @@ export default defineConfig({
|
||||
{ text: 'ContextAware 生成器', link: '/zh-CN/source-generators/context-aware-generator' },
|
||||
{ text: 'Priority 生成器', link: '/zh-CN/source-generators/priority-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: 'GetNode 生成器 (Godot)', link: '/zh-CN/source-generators/get-node-generator' },
|
||||
{ text: 'BindNodeSignal 生成器 (Godot)', link: '/zh-CN/source-generators/bind-node-signal-generator' }
|
||||
@ -264,7 +265,8 @@ export default defineConfig({
|
||||
text: '抽象接口',
|
||||
items: [
|
||||
{ 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' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@ -1,192 +1,104 @@
|
||||
---
|
||||
title: Core Abstractions
|
||||
description: GFramework.Core.Abstractions 的契约边界、包关系与 XML 阅读重点。
|
||||
---
|
||||
|
||||
# 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
|
||||
public interface IArchitecture
|
||||
{
|
||||
void Initialize();
|
||||
void Destroy();
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
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
|
||||
|
||||
数据模型接口:
|
||||
|
||||
```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
|
||||
public sealed class DiagnosticsFeature
|
||||
{
|
||||
private readonly IArchitecture _architecture;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MyService(IArchitecture architecture)
|
||||
public DiagnosticsFeature(IArchitecture architecture, ILogger logger)
|
||||
{
|
||||
_architecture = architecture;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义事件
|
||||
### 2. 什么时候切到运行时包
|
||||
|
||||
```csharp
|
||||
public class PlayerDiedEvent : IEvent
|
||||
{
|
||||
public int PlayerId { get; set; }
|
||||
public Vector2 Position { get; set; }
|
||||
}
|
||||
```
|
||||
下面这些需求都属于 `GFramework.Core` 的职责,而不是本包:
|
||||
|
||||
---
|
||||
- 继承 `Architecture` 并完成默认初始化流程
|
||||
- 使用 `ContextAwareBase`、`AbstractModel`、`AbstractSystem` 等默认基类
|
||||
- 使用默认的 `CommandExecutor`、`QueryExecutor`、`BindableProperty<T>`、`StateMachine`
|
||||
- 直接启用默认的 `Microsoft.Extensions.DependencyInjection` 容器适配或资源 / 协程 / 日志实现
|
||||
|
||||
**相关文档**:
|
||||
## XML 阅读重点
|
||||
|
||||
- [Core 概述](../core/index.md)
|
||||
- [Architecture](../core/architecture)
|
||||
- [Events](../core/events)
|
||||
- [Command](../core/command)
|
||||
- [Query](../core/query)
|
||||
如果你在做契约审计、采用设计或扩展适配,优先核对这些类型族的 XML 文档:
|
||||
|
||||
- 架构与模块入口:`IArchitecture`、`IArchitectureContext`、`IServiceModule`
|
||||
- 运行时基础设施:`IIocContainer`、`ILogger`、`IResourceManager`、`IConfigurationManager`
|
||||
- 状态与并发能力:`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)
|
||||
|
||||
103
docs/zh-CN/abstractions/ecs-arch-abstractions.md
Normal file
103
docs/zh-CN/abstractions/ecs-arch-abstractions.md
Normal 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 与专题页维护。
|
||||
@ -1,3 +1,8 @@
|
||||
---
|
||||
title: 抽象接口
|
||||
description: GFramework 各抽象层模块的阅读入口与使用边界。
|
||||
---
|
||||
|
||||
# 抽象接口
|
||||
|
||||
`GFramework.*.Abstractions` 用来承载跨模块协作所需的契约,而不是运行时实现。
|
||||
@ -12,9 +17,19 @@
|
||||
|
||||
- Core 抽象层:[`core-abstractions.md`](./core-abstractions.md)
|
||||
- Game 抽象层:[`game-abstractions.md`](./game-abstractions.md)
|
||||
- Ecs.Arch 抽象层:[`ecs-arch-abstractions.md`](./ecs-arch-abstractions.md)
|
||||
|
||||
## 使用建议
|
||||
|
||||
- 如果你只是想直接使用框架功能,优先从对应运行时模块的 `README.md` 和栏目页开始。
|
||||
- 只有在明确需要“契约层而非实现层”时,才单独依赖 `*.Abstractions` 包。
|
||||
- 抽象层页面会解释接口分组与职责;实际安装与接入路径,仍应以运行时模块 README 与 `getting-started` 为主。
|
||||
|
||||
## 当前边界
|
||||
|
||||
- `GFramework.Core.SourceGenerators.Abstractions`
|
||||
- `GFramework.Godot.SourceGenerators.Abstractions`
|
||||
- `GFramework.SourceGenerators.Common`
|
||||
|
||||
这些目录当前不作为独立抽象接口栏目维护,而是作为源码生成器家族的内部支撑模块,分别跟随所属模块 README 和
|
||||
`source-generators` 栏目维护。
|
||||
|
||||
@ -1,571 +1,81 @@
|
||||
# API 参考文档
|
||||
|
||||
本文档提供 GFramework 各模块的完整 API 参考。
|
||||
|
||||
## 核心命名空间
|
||||
|
||||
### 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}");
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
title: API 参考
|
||||
description: GFramework 的 API 阅读入口,按模块映射 README、专题页、XML 文档和教程链路。
|
||||
---
|
||||
|
||||
更多详情请查看各模块的详细文档。
|
||||
# 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 文档出现冲突时,以源码和测试所反映的当前实现为准。
|
||||
|
||||
@ -123,10 +123,24 @@ protected override void OnInitialize()
|
||||
- `PhaseChanged`
|
||||
- `RegisterLifecycleHook(...)`
|
||||
|
||||
其中 `PhaseChanged` 现在遵循标准 `EventHandler<ArchitecturePhaseChangedEventArgs>` 约定,
|
||||
阶段值通过 `args.Phase` 读取。
|
||||
|
||||
如果你正在从旧版本迁移,需要把单参数写法 `phase => ...` 改成 `(_, args) => ...`,
|
||||
并通过 `ArchitecturePhaseChangedEventArgs.Phase` 读取阶段值。
|
||||
|
||||
如果你需要在 `Ready`、`Destroying` 等阶段执行横切逻辑,比起把这类逻辑塞进某个具体 `System`,更适合单独实现
|
||||
`IArchitectureLifecycleHook`。
|
||||
|
||||
```csharp
|
||||
architecture.PhaseChanged += (_, args) =>
|
||||
{
|
||||
if (args.Phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
Console.WriteLine("Architecture ready from event.");
|
||||
}
|
||||
};
|
||||
|
||||
public sealed class MetricsHook : IArchitectureLifecycleHook
|
||||
{
|
||||
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
|
||||
@ -1,15 +1,29 @@
|
||||
---
|
||||
title: CQRS
|
||||
description: 当前推荐的新请求模型,统一覆盖 command、query、notification、stream request 和 pipeline behaviors。
|
||||
description: Cqrs 模块族的运行时、契约层、生成器入口,以及 XML / API 阅读链路。
|
||||
---
|
||||
|
||||
# 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
|
||||
dotnet add package GeWuYou.GFramework.Cqrs
|
||||
@ -22,15 +36,6 @@ dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
|
||||
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.Cqrs.Command` / `Query` / `Notification`
|
||||
|
||||
示例:
|
||||
|
||||
```csharp
|
||||
using GFramework.Cqrs.Abstractions.Cqrs.Command;
|
||||
using GFramework.Cqrs.Command;
|
||||
using GFramework.Cqrs.Cqrs.Command;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs.Command;
|
||||
|
||||
public sealed record CreatePlayerInput(string Name) : ICommandInput;
|
||||
|
||||
@ -66,9 +69,7 @@ public sealed class CreatePlayerCommandHandler
|
||||
}
|
||||
```
|
||||
|
||||
## 发送请求
|
||||
|
||||
如果你在 `IContextAware` 对象内部:
|
||||
如果你在 `IContextAware` 对象内部发送请求:
|
||||
|
||||
```csharp
|
||||
using GFramework.Cqrs.Extensions;
|
||||
@ -77,7 +78,7 @@ var playerId = await this.SendAsync(
|
||||
new CreatePlayerCommand(new CreatePlayerInput("Alice")));
|
||||
```
|
||||
|
||||
如果你在组合根或测试里:
|
||||
如果你在组合根或测试里发送请求:
|
||||
|
||||
```csharp
|
||||
var playerId = await architecture.Context.SendRequestAsync(
|
||||
@ -92,7 +93,7 @@ var playerId = await architecture.Context.SendRequestAsync(
|
||||
- `PublishAsync(...)`
|
||||
- `CreateStream(...)`
|
||||
|
||||
## 查询、通知和流
|
||||
## 统一请求模型
|
||||
|
||||
这套 runtime 不只处理 command,也统一处理:
|
||||
|
||||
@ -103,9 +104,9 @@ var playerId = await architecture.Context.SendRequestAsync(
|
||||
- Stream Request
|
||||
- 返回 `IAsyncEnumerable<T>`
|
||||
|
||||
也就是说,新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。
|
||||
新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。
|
||||
|
||||
## 注册处理器
|
||||
## 处理器注册与生成器协作
|
||||
|
||||
在标准 `Architecture` 启动路径中,CQRS runtime 会自动接入基础设施。你通常只需要在 `OnInitialize()` 里追加行为或额外程序集:
|
||||
|
||||
@ -123,11 +124,15 @@ protected override void OnInitialize()
|
||||
}
|
||||
```
|
||||
|
||||
默认逻辑会:
|
||||
默认注册流程当前遵循这些语义:
|
||||
|
||||
1. 优先使用消费端程序集上的生成注册器
|
||||
2. 生成注册器不可用时回退到反射扫描
|
||||
3. 对同一程序集去重,避免重复注册
|
||||
1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute`
|
||||
2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry`
|
||||
3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径
|
||||
4. 如果程序集带有 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
|
||||
5. 同一程序集按稳定键去重,避免重复注册
|
||||
|
||||
`Cqrs.SourceGenerators` 的专题入口见 [../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)。
|
||||
|
||||
## Pipeline Behavior
|
||||
|
||||
@ -145,7 +150,7 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
|
||||
- 审计
|
||||
- 重试或统一异常封装
|
||||
|
||||
旧的 `Mediator` 兼容别名入口已经移除;当前公开入口只有 `RegisterCqrsPipelineBehavior<TBehavior>()`。
|
||||
当前公开入口只有 `RegisterCqrsPipelineBehavior<TBehavior>()`。
|
||||
|
||||
## 和旧 Command / Query 的关系
|
||||
|
||||
@ -157,15 +162,28 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
|
||||
- 新路径
|
||||
- `GFramework.Cqrs`
|
||||
|
||||
`IArchitectureContext` 仍然会兼容旧入口,但新代码应优先使用 CQRS runtime。
|
||||
`IArchitectureContext` 仍然兼容旧入口,但新代码应优先使用 CQRS runtime。
|
||||
|
||||
一个简单判断规则:
|
||||
|
||||
- 在维护历史代码:允许继续使用旧 Command / Query
|
||||
- 在写新功能或新模块:优先使用 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)
|
||||
- 上下文入口:[context](./context.md)
|
||||
- 生成器专题:[../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)
|
||||
- 模块 README:`GFramework.Cqrs/README.md`
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Core
|
||||
description: GFramework.Core 与 GFramework.Core.Abstractions 的运行时入口、采用顺序和 XML 阅读导航。
|
||||
---
|
||||
|
||||
# Core
|
||||
|
||||
`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core` 与 `GFramework.Core.Abstractions`,以及与之直接相邻的旧版
|
||||
@ -29,28 +34,70 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
|
||||
|
||||
`Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织:
|
||||
|
||||
- 架构与上下文
|
||||
- 架构与生命周期
|
||||
- [architecture](./architecture.md)
|
||||
- [context](./context.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)
|
||||
- [query](./query.md)
|
||||
- [cqrs](./cqrs.md)
|
||||
- 核心横切能力
|
||||
- 状态、事件与规则
|
||||
- [events](./events.md)
|
||||
- [property](./property.md)
|
||||
- [rule](./rule.md)
|
||||
- [logging](./logging.md)
|
||||
- [resource](./resource.md)
|
||||
- [coroutine](./coroutine.md)
|
||||
- [ioc](./ioc.md)
|
||||
- 状态与扩展能力
|
||||
- [state-machine](./state-machine.md)
|
||||
- [state-management](./state-management.md)
|
||||
- 运行时支撑能力
|
||||
- [resource](./resource.md)
|
||||
- [pool](./pool.md)
|
||||
- [coroutine](./coroutine.md)
|
||||
- [pause](./pause.md)
|
||||
- [localization](./localization.md)
|
||||
- [configuration](./configuration.md)
|
||||
- [ioc](./ioc.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.Abstractions/README.md`
|
||||
- `docs/zh-CN/api-reference/index.md`
|
||||
- 仓库根 `README.md`
|
||||
|
||||
@ -138,10 +138,13 @@ architecture.RegisterLifecycleHook(new MetricsHook());
|
||||
|
||||
如果你只需要观察阶段变化,也可以直接订阅:
|
||||
|
||||
如果你从旧版本的 `PhaseChanged` 迁移过来,需要把旧写法 `phase => ...` 改成 `(_, args) => ...`,
|
||||
并通过 `ArchitecturePhaseChangedEventArgs.Phase` 读取阶段值。
|
||||
|
||||
```csharp
|
||||
architecture.PhaseChanged += phase =>
|
||||
architecture.PhaseChanged += (_, args) =>
|
||||
{
|
||||
Console.WriteLine($"Phase changed: {phase}");
|
||||
Console.WriteLine($"Phase changed: {args.Phase}");
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
@ -1,46 +1,43 @@
|
||||
---
|
||||
title: Arch ECS 集成
|
||||
description: GFramework 的 Arch ECS 集成包使用指南,提供高性能的实体组件系统支持。
|
||||
description: GFramework.Ecs.Arch 的默认运行时装配路径、系统桥接方式与 XML 阅读入口。
|
||||
---
|
||||
|
||||
# Arch ECS 集成
|
||||
|
||||
## 概述
|
||||
`GFramework.Ecs.Arch` 是当前仓库里负责 Arch ECS 默认接入路径的运行时包。它把 Arch `World`、GFramework 的
|
||||
`IServiceModule` 生命周期,以及 `AbstractSystem` / `ISystem` 体系桥接到同一条初始化与更新链路中。
|
||||
|
||||
`GFramework.Ecs.Arch` 是 GFramework 的 Arch ECS 集成包,提供开箱即用的 ECS(Entity Component
|
||||
System)支持。基于 [Arch.Core](https://github.com/genaray/Arch) 实现,具有极致的性能和简洁的 API。
|
||||
## 什么时候依赖它
|
||||
|
||||
**主要特性**:
|
||||
当你需要下面任一能力时,应直接依赖 `GeWuYou.GFramework.Ecs.Arch`:
|
||||
|
||||
- 🎯 **显式集成** - 符合 .NET 生态习惯的显式注册方式
|
||||
- 🔌 **零依赖** - 不使用时,Core 包无 Arch 依赖
|
||||
- 🎯 **类型安全** - 完整的类型系统和编译时检查
|
||||
- ⚡ **高性能** - 基于 Arch ECS 的高性能实现
|
||||
- 🔧 **易扩展** - 简单的系统适配器模式
|
||||
- 📊 **优先级支持** - 系统按优先级顺序执行
|
||||
- 在架构实例上调用 `UseArch(...)`
|
||||
- 让 `World` 在服务模块注册阶段自动创建并注入容器
|
||||
- 让 ECS 系统继承 `ArchSystemAdapter<T>`
|
||||
- 使用仓库自带的 `Position`、`Velocity`、`MovementSystem` 最小示例
|
||||
|
||||
**性能特点**:
|
||||
如果你只想保留共享边界,而不依赖默认实现,请改看
|
||||
[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)。
|
||||
|
||||
- 10,000 个实体更新 < 100ms
|
||||
- 1,000 个实体创建 < 50ms
|
||||
- 基于 Archetype 的高效内存布局
|
||||
- 零 GC 分配的组件访问
|
||||
## 最小接入路径
|
||||
|
||||
## 安装
|
||||
### 1. 安装包
|
||||
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework.Ecs.Arch
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
### 2. 在 `Initialize()` 之前调用 `UseArch(...)`
|
||||
|
||||
### 1. 注册 ECS 模块
|
||||
当前实现通过 `ArchitectureModuleRegistry.Register(...)` 提前登记 `ArchEcsModule`。这意味着调用时机应位于
|
||||
`Initialize()` 之前,而不是放进 `OnInitialize()` 里。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Architecture;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Ecs.Arch.Extensions;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
public GameArchitecture() : base(new ArchitectureConfiguration())
|
||||
{
|
||||
@ -48,698 +45,100 @@ public class GameArchitecture : Architecture
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 显式注册 Arch ECS 模块
|
||||
this.UseArch();
|
||||
RegisterSystem<MovementSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
var architecture = new GameArchitecture()
|
||||
.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2048;
|
||||
options.Priority = 50;
|
||||
});
|
||||
|
||||
architecture.Initialize();
|
||||
```
|
||||
|
||||
### 2. 带配置的注册
|
||||
### 3. 用 `ArchSystemAdapter<float>` 编写系统
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
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<T>`:
|
||||
`ArchSystemAdapter<T>` 在 `OnInit()` 中从当前上下文解析 `World`,再把 Arch 的 `Initialize / BeforeUpdate /
|
||||
AfterUpdate / Dispose` 钩子桥接到可重写方法。
|
||||
|
||||
```csharp
|
||||
using Arch.Core;
|
||||
using GFramework.Ecs.Arch;
|
||||
using MyGame.Components;
|
||||
using GFramework.Ecs.Arch.Components;
|
||||
|
||||
namespace MyGame.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// 移动系统 - 更新实体位置
|
||||
/// </summary>
|
||||
public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
private QueryDescription _query;
|
||||
|
||||
protected override void OnArchInitialize()
|
||||
{
|
||||
// 创建查询:查找所有同时拥有 Position 和 Velocity 组件的实体
|
||||
_query = new QueryDescription()
|
||||
.WithAll<Position, Velocity>();
|
||||
}
|
||||
|
||||
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;
|
||||
pos.Y += vel.Y * deltaTime;
|
||||
position.X += velocity.X * frameDelta;
|
||||
position.Y += velocity.Y * frameDelta;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 注册系统
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
this.UseArch();
|
||||
|
||||
// 注册 ECS 系统
|
||||
RegisterSystem<MovementSystem>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 创建实体
|
||||
### 4. 初始化后解析 `World` 与模块服务
|
||||
|
||||
```csharp
|
||||
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;
|
||||
|
||||
public class GameLoop
|
||||
{
|
||||
private IArchEcsModule _ecsModule;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// 获取 ECS 模块
|
||||
_ecsModule = architecture.Context.GetService<IArchEcsModule>();
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
// 更新所有 ECS 系统
|
||||
_ecsModule.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
var world = architecture.Context.GetService<World>();
|
||||
var ecsModule = architecture.Context.GetService<IArchEcsModule>();
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
### ArchOptions
|
||||
### 5. 由宿主循环显式调用 `Update`
|
||||
|
||||
```csharp
|
||||
public sealed class ArchOptions
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
ecsModule.Update(deltaTime);
|
||||
```
|
||||
|
||||
### 配置示例
|
||||
这一步在 `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
|
||||
// 创建空实体
|
||||
var entity = world.Create();
|
||||
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 装配入口 | `ArchExtensions` | 已覆盖 | `UseArch(...)` 的时机、链式调用返回值 |
|
||||
| 服务模块 | `ArchEcsModule` | 已覆盖 | `World` 注册、系统收集、模块销毁顺序 |
|
||||
| 系统桥接层 | `ArchSystemAdapter<T>` | 已覆盖 | `OnArchInitialize` / `OnUpdate` / `OnArchDispose` |
|
||||
| 示例类型 | `Position`、`Velocity`、`MovementSystem` | 已覆盖 | 组件布局、查询写法、最小集成样例 |
|
||||
|
||||
// 创建带组件的实体
|
||||
var entity = world.Create(
|
||||
new Position(0, 0),
|
||||
new Velocity(1, 1)
|
||||
);
|
||||
## 相关入口
|
||||
|
||||
// 销毁实体
|
||||
world.Destroy(entity);
|
||||
```
|
||||
|
||||
### 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<T>
|
||||
|
||||
`ArchSystemAdapter<T>` 桥接 Arch.System.ISystem<T> 到 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<T>` 继承自 `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)
|
||||
- ECS 模块总览:[`index.md`](./index.md)
|
||||
- 抽象契约页:[`../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)
|
||||
|
||||
@ -1,95 +1,39 @@
|
||||
---
|
||||
title: ECS 系统集成
|
||||
description: GFramework 的 ECS(Entity Component System)集成方案,支持多种 ECS 框架。
|
||||
description: GFramework 当前 ECS 模块族的包边界、采用顺序与 XML 阅读入口。
|
||||
---
|
||||
|
||||
# ECS 系统集成
|
||||
|
||||
## 概述
|
||||
GFramework 当前仓库内已经交付并持续维护的 ECS 模块族是 `Ecs.Arch`。它分成运行时实现包
|
||||
`GFramework.Ecs.Arch` 和契约包 `GFramework.Ecs.Arch.Abstractions`,分别覆盖默认装配能力与共享边界约定。
|
||||
|
||||
GFramework 提供了灵活的 ECS(Entity 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) |
|
||||
|
||||
ECS(Entity Component System)是一种架构模式,将游戏对象分解为三个核心概念:
|
||||
## 最小采用路径
|
||||
|
||||
- **Entity(实体)**:游戏世界中的基本对象,本质上是一个唯一标识符
|
||||
- **Component(组件)**:纯数据结构,存储实体的状态
|
||||
- **System(系统)**:包含游戏逻辑,处理具有特定组件组合的实体
|
||||
### 1. 选择包边界
|
||||
|
||||
### ECS 的优势
|
||||
- 需要默认实现时安装 `GeWuYou.GFramework.Ecs.Arch`
|
||||
- 只需要契约时安装 `GeWuYou.GFramework.Ecs.Arch.Abstractions`
|
||||
|
||||
- **高性能**:数据局部性好,缓存友好
|
||||
- **可扩展**:通过组合组件轻松创建新实体类型
|
||||
- **并行处理**:系统之间相互独立,易于并行化
|
||||
- **数据驱动**:逻辑与数据分离,便于序列化和网络同步
|
||||
### 2. 在 `Initialize()` 前显式接入运行时
|
||||
|
||||
### 何时使用 ECS?
|
||||
|
||||
**适合使用 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 模块
|
||||
`UseArch(...)` 通过 `ArchitectureModuleRegistry` 注册服务模块。按当前源码与集成测试,它应在架构实例调用
|
||||
`Initialize()` 之前完成。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Architecture;
|
||||
using GFramework.Ecs.Arc;
|
||||
using Arch.Core;
|
||||
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())
|
||||
{
|
||||
@ -97,41 +41,29 @@ public class GameArchitecture : Architecture
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 显式注册 Arch ECS 模块
|
||||
this.UseArch(options =>
|
||||
{
|
||||
options.WorldCapacity = 2000;
|
||||
options.EnableStatistics = true;
|
||||
});
|
||||
RegisterSystem<MovementSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
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. 定义组件
|
||||
|
||||
```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. 创建系统
|
||||
### 3. 让 ECS 系统继承 `ArchSystemAdapter<float>`
|
||||
|
||||
```csharp
|
||||
using Arch.Core;
|
||||
using GFramework.Ecs.Arch;
|
||||
using GFramework.Ecs.Arch.Components;
|
||||
|
||||
public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
{
|
||||
@ -145,147 +77,60 @@ public sealed class MovementSystem : ArchSystemAdapter<float>
|
||||
|
||||
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;
|
||||
pos.Y += vel.Y * deltaTime;
|
||||
position.X += velocity.X * frameDelta;
|
||||
position.Y += velocity.Y * frameDelta;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 注册系统
|
||||
### 4. 由宿主循环驱动更新
|
||||
|
||||
`IArchEcsModule` 继承自 `IServiceModule`,负责初始化和销毁;真正的帧更新通过 `Update(float deltaTime)` 显式触发。
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
this.UseArch();
|
||||
using GFramework.Ecs.Arch.Abstractions;
|
||||
|
||||
// 注册 ECS 系统
|
||||
RegisterSystem<MovementSystem>();
|
||||
public sealed class GameLoop
|
||||
{
|
||||
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
|
||||
// ✅ 显式注册 - 清晰、可控
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
this.UseArch(); // 明确表示使用 Arch ECS
|
||||
}
|
||||
}
|
||||
下表记录当前 `Ecs.Arch` family 的类型声明级 XML 基线,便于从 README、站内 landing 和源码之间建立一致的审计入口。
|
||||
|
||||
// ❌ 自动注册 - 隐式、难以控制
|
||||
// 只需引入包,自动注册(不推荐)
|
||||
```
|
||||
| 包 | 类型族 | 代表类型 | 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` | 已覆盖 | 共享宿主循环、测试替身、跨程序集配置边界 |
|
||||
|
||||
**优势**:
|
||||
## 边界说明
|
||||
|
||||
- 清晰的依赖关系
|
||||
- 更好的 IDE 支持
|
||||
- 易于测试和调试
|
||||
- 符合 .NET 生态习惯
|
||||
|
||||
### 零依赖原则
|
||||
|
||||
如果你不使用 ECS,GFramework.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)
|
||||
- 当前仓库没有交付其他可直接消费的 ECS 运行时包;旧文档里把“可能支持的其他 ECS 框架”写成现有能力会误导采用路径。
|
||||
- `GFramework.Ecs.Arch.Abstractions` 负责“边界”,`GFramework.Ecs.Arch` 负责“默认实现”。
|
||||
- 站内页面只维护可构建的 docs 链路;仓库根 README 和模块 README 继续承担包目录入口职责。
|
||||
|
||||
@ -1,612 +1,176 @@
|
||||
---
|
||||
title: Godot 架构集成
|
||||
description: Godot 架构集成提供了 GFramework 与 Godot 引擎的无缝连接,实现生命周期同步和模块化开发。
|
||||
description: 说明 AbstractArchitecture、ArchitectureAnchor 和 Godot 模块挂接的当前生命周期语义,避免继续沿用旧版 `.Wait()` 接法。
|
||||
---
|
||||
|
||||
# Godot 架构集成
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 架构集成是 GFramework.Godot 中连接框架与 Godot 引擎的核心组件。它提供了架构与 Godot 场景树的生命周期绑定、模块化扩展系统,以及与
|
||||
Godot 节点系统的深度集成。
|
||||
`GFramework.Godot` 当前的架构集成目标很直接:让 `Architecture` 能安全地感知 Godot `SceneTree` 生命周期,并在需要时把
|
||||
带 `Node` 的扩展模块挂到场景树上。
|
||||
|
||||
通过 Godot 架构集成,你可以在 Godot 项目中使用 GFramework 的所有功能,同时保持与 Godot 引擎的完美兼容。
|
||||
当前真正参与这条链路的核心类型只有三类:
|
||||
|
||||
**主要特性**:
|
||||
- `AbstractArchitecture`:在原有 `Architecture` 之上增加 Godot 生命周期绑定
|
||||
- `ArchitectureAnchor`:挂在 `SceneTree.Root` 下的锚点节点,负责把 `_ExitTree()` 事件转回架构销毁
|
||||
- `IGodotModule` / `AbstractGodotModule`:当模块本身需要携带 Godot `Node` 时使用
|
||||
|
||||
- 架构与 Godot 生命周期自动同步
|
||||
- 模块化的 Godot 扩展系统
|
||||
- 架构锚点节点管理
|
||||
- 自动资源清理
|
||||
- 热重载支持
|
||||
- 与 Godot 场景树深度集成
|
||||
它不是另一套独立的模块系统,也不意味着所有模块都必须改成 `InstallGodotModule(...)`。
|
||||
|
||||
## 核心概念
|
||||
## 什么时候该用 `AbstractArchitecture`
|
||||
|
||||
### 抽象架构
|
||||
当你的架构需要满足下面任一条件时,可以让它继承 `AbstractArchitecture`:
|
||||
|
||||
`AbstractArchitecture` 是 Godot 项目中架构的基类:
|
||||
- 需要把架构生命周期绑定到 Godot `SceneTree`
|
||||
- 需要在架构里安装带 `Node` 的扩展模块
|
||||
- 需要通过受保护的 `ArchitectureRoot` 访问锚点节点,继续挂接 Godot 子节点
|
||||
|
||||
如果你只是做普通的 Model / System / Utility 注册,`AbstractArchitecture` 的主要价值仍然是“让架构知道自己何时跟随
|
||||
Godot 场景树销毁”,而不是改变注册方式。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 常规模块仍然用 `InstallModule(...)`
|
||||
|
||||
当前消费者 `ai-libs/CoreGrid` 的默认做法,是保持普通模块注册方式:
|
||||
|
||||
```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()
|
||||
{
|
||||
// 注册 Model
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterModel(new GameModel());
|
||||
|
||||
// 注册 System
|
||||
RegisterSystem(new GameplaySystem());
|
||||
RegisterSystem(new AudioSystem());
|
||||
|
||||
// 注册 Utility
|
||||
RegisterUtility(new StorageUtility());
|
||||
InstallModule(new UtilityModule());
|
||||
InstallModule(new ModelModule());
|
||||
InstallModule(new GameplayModule());
|
||||
InstallModule(new SystemModule());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在 Godot 场景中初始化架构
|
||||
这里继承 `AbstractArchitecture` 的意义,是把架构绑定到 Godot 生命周期,而不是把普通模块注册改写成 Godot 风格 API。
|
||||
|
||||
### 只有携带 `Node` 的模块才需要 `InstallGodotModule(...)`
|
||||
|
||||
如果模块本身暴露一个 Godot `Node`,并且希望由架构锚点统一托管,可以这样写:
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.Architecture;
|
||||
|
||||
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 GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Godot.Architectures;
|
||||
using Godot;
|
||||
|
||||
public class CoroutineModule : AbstractGodotModule
|
||||
namespace MyGame.Scripts.Core;
|
||||
|
||||
public sealed class HudModule : AbstractGodotModule
|
||||
{
|
||||
private Node _coroutineNode;
|
||||
|
||||
public override Node Node => _coroutineNode;
|
||||
|
||||
public CoroutineModule()
|
||||
private readonly Control _root = new()
|
||||
{
|
||||
_coroutineNode = new Node { Name = "CoroutineScheduler" };
|
||||
}
|
||||
Name = "HudModule"
|
||||
};
|
||||
|
||||
public override Node Node => _root;
|
||||
|
||||
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()
|
||||
{
|
||||
GD.Print("协程模块已分离");
|
||||
_coroutineNode?.QueueFree();
|
||||
_root.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
|
||||
public class SceneModule : AbstractGodotModule
|
||||
{
|
||||
private Node _sceneRoot;
|
||||
1. 生成唯一的锚点节点名称
|
||||
2. 调用 `AttachToGodotLifecycle()`
|
||||
3. 在可用的 `SceneTree` 上创建并绑定 `ArchitectureAnchor`
|
||||
4. 执行你重写的 `InstallModules()`
|
||||
|
||||
public override Node Node => _sceneRoot;
|
||||
也就是说,Godot 生命周期绑定先发生,业务模块注册后发生。
|
||||
|
||||
public SceneModule()
|
||||
{
|
||||
_sceneRoot = new Node { Name = "SceneRoot" };
|
||||
}
|
||||
### `InstallGodotModule(...)` 的执行顺序
|
||||
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 访问架构根节点
|
||||
if (architecture is AbstractArchitecture godotArch)
|
||||
{
|
||||
var root = godotArch.ArchitectureRoot;
|
||||
root.AddChild(_sceneRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
当前实现里,`InstallGodotModule(...)` 会:
|
||||
|
||||
### 监听架构阶段
|
||||
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)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case ArchitecturePhase.Initializing:
|
||||
GD.Print("架构正在初始化");
|
||||
break;
|
||||
- 防止重复销毁
|
||||
- 依次调用已登记 Godot 模块的 `OnDetach()`
|
||||
- 清空内部扩展列表
|
||||
- 再进入基类 `DestroyAsync()`
|
||||
|
||||
case ArchitecturePhase.Ready:
|
||||
GD.Print("架构已就绪,开始追踪");
|
||||
StartTracking();
|
||||
break;
|
||||
如果异步销毁抛异常,当前实现会把错误写到 Godot 错误输出,而不是静默吞掉。
|
||||
|
||||
case ArchitecturePhase.Destroying:
|
||||
GD.Prin构正在销毁,停止追踪");
|
||||
StopTracking();
|
||||
break;
|
||||
}
|
||||
}
|
||||
## 当前边界
|
||||
|
||||
private void StartTracking() { }
|
||||
private void StopTracking() { }
|
||||
}
|
||||
```
|
||||
### 没有锚点时不会偷偷安装模块
|
||||
|
||||
### 自定义架构配置
|
||||
`GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs` 已覆盖一个关键边界:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Architecture;
|
||||
using GFramework.Core.Abstractions.Environment;
|
||||
- 当锚点尚未初始化时,`InstallGodotModule(...)` 会直接抛 `InvalidOperationException("Anchor not initialized")`
|
||||
- 失败发生在 `module.Install(...)` 之前,因此不会留下半安装副作用
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public GameArchitecture() : base(
|
||||
configuration: CreateConfiguration(),
|
||||
environment: CreateEnvironment()
|
||||
)
|
||||
{
|
||||
}
|
||||
这也是为什么文档不应该再把 `InstallGodotModule(...).Wait()` 写成一种随处可用的默认初始化方式。
|
||||
|
||||
private static IArchitectureConfiguration CreateConfiguration()
|
||||
{
|
||||
return new ArchitectureConfiguration
|
||||
{
|
||||
EnableLogging
|
||||
LogLevel = LogLevel.Debug
|
||||
};
|
||||
}
|
||||
### `AbstractGodotModule` 只是便捷基类,不代表自动阶段广播
|
||||
|
||||
private static IEnvironment CreateEnvironment()
|
||||
{
|
||||
return new DefaultEnvironment
|
||||
{
|
||||
IsDevelopment = OS.IsDebugBuild()
|
||||
};
|
||||
}
|
||||
当前接口 `IGodotModule` 真正保证的成员只有:
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 根据环境配置安装模块
|
||||
if (Environment.IsDevelopment)
|
||||
{
|
||||
InstallGodotModule(new DebugModule()).Wait();
|
||||
}
|
||||
- `Node`
|
||||
- `Install(IArchitecture architecture)`
|
||||
- `OnAttach(Architecture architecture)`
|
||||
- `OnDetach()`
|
||||
|
||||
// 安装核心模块
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
`AbstractGodotModule` 里虽然保留了 `OnPhase(...)` / `OnArchitecturePhase(...)` 虚方法,但它们不在当前接口契约内,也没有在
|
||||
这条挂接流程里形成稳定的自动广播语义。不要把它写成当前公开保证。
|
||||
|
||||
### 热重载支持
|
||||
### `ArchitectureRoot` 只在锚点就绪后可用
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
private static bool _initialized;
|
||||
`ArchitectureRoot` 是受保护属性,底层直接返回 `_anchor`。如果锚点尚未准备好或架构已经失效,它会抛
|
||||
`InvalidOperationException("Architecture root not ready")`。因此它适合放在明确依赖锚点存在的挂接逻辑里,而不是拿来做
|
||||
任意时机的全局节点查找。
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 防止热重载时重复初始化
|
||||
if (_initialized)
|
||||
{
|
||||
GD.Print("架构已初始化,跳过重复初始化");
|
||||
return;
|
||||
}
|
||||
## 继续阅读
|
||||
|
||||
base.OnInitialize();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
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 扩展方法
|
||||
1. [Godot 运行时集成](./index.md)
|
||||
2. [Godot 集成教程](../tutorials/godot-integration.md)
|
||||
3. [Godot 场景系统](./scene.md)
|
||||
4. [Godot UI 系统](./ui.md)
|
||||
|
||||
@ -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[事件管理]
|
||||
```
|
||||
## 当前公开入口
|
||||
|
||||
## 扩展模块详解
|
||||
### `GodotPathExtensions`
|
||||
|
||||
### 1. 路径扩展 (GodotPathExtensions)
|
||||
这组扩展只负责判断 Godot 虚拟路径前缀:
|
||||
|
||||
提供 Godot 虚拟路径的判断和识别功能。
|
||||
- `IsUserPath(this string path)`
|
||||
- `IsResPath(this string path)`
|
||||
- `IsGodotPath(this string path)`
|
||||
|
||||
**主要方法:**
|
||||
|
||||
- `IsUserPath()` - 判断是否为 `user://` 路径
|
||||
- `IsResPath()` - 判断是否为 `res://` 路径
|
||||
- `IsGodotPath()` - 判断是否为 Godot 虚拟路径
|
||||
|
||||
**使用示例:**
|
||||
它们不做文件访问,也不解析目录结构,只是用字符串前缀判断 `user://` 和 `res://`。
|
||||
|
||||
```csharp
|
||||
string savePath = "user://save.dat";
|
||||
string configPath = "res://config.json";
|
||||
string logPath = "C:/logs/debug.log";
|
||||
using GFramework.Godot.Extensions;
|
||||
|
||||
if (savePath.IsUserPath()) Console.WriteLine("用户数据路径");
|
||||
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
|
||||
if ("user://save.json".IsUserPath())
|
||||
{
|
||||
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 gamePanel = _uiRoot.GetOrCreateNode<Panel>("GamePanel");
|
||||
|
||||
// 安全添加子节点
|
||||
var player = new Player();
|
||||
await AddChildXAsync(player);
|
||||
|
||||
// 查找并配置玩家
|
||||
var sprite = player.FindChildX<Sprite2D>("Sprite");
|
||||
if (sprite.IsValidNode()) sprite.Modulate = Colors.Red;
|
||||
var applyButton = FindChildX<Button>("ApplyButton");
|
||||
applyButton?.Signal(Button.SignalName.Pressed)
|
||||
.To(Callable.From(OnApplyPressed));
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
private void OnApplyPressed()
|
||||
{
|
||||
// 安全释放所有子节点
|
||||
ForEachChild<Node>(child => child.QueueFreeX());
|
||||
this.SetInputAsHandled();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### UI 事件处理
|
||||
### 2. 框架订阅和节点生命周期一起收尾
|
||||
|
||||
当订阅句柄实现了 `IUnRegister`,可以把释放时机绑到节点退出树:
|
||||
|
||||
```csharp
|
||||
public class MainMenu : Control
|
||||
public override void _Ready()
|
||||
{
|
||||
private Button _startButton;
|
||||
private Button _quitButton;
|
||||
|
||||
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();
|
||||
}
|
||||
IUnRegister subscription = _eventBus.Subscribe<SettingsChangedEvent>(OnSettingsChanged);
|
||||
subscription.UnRegisterWhenNodeExitTree(this);
|
||||
}
|
||||
```
|
||||
|
||||
#### 异步场景管理
|
||||
这比在多个 `_ExitTree()` / `Dispose()` 分支里手写解绑更稳定,也更符合当前扩展的职责边界。
|
||||
|
||||
```csharp
|
||||
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>();
|
||||
### 3. 只在需要时使用 signal fluent API
|
||||
|
||||
// 等待场景加载完成
|
||||
await instance.WaitUntilReadyAsync();
|
||||
`Signal(...)` 属于扩展集合的一部分,但它已经有独立页面。实践上可以这样分工:
|
||||
|
||||
return instance;
|
||||
}
|
||||
- 节点查找、ready 等待、输入处理:`NodeExtensions`
|
||||
- 动态 signal 绑定:`Signal(...)`
|
||||
- 框架订阅释放:`UnRegisterWhenNodeExitTree(...)`
|
||||
- 路径前缀判断:`GodotPathExtensions`
|
||||
|
||||
public async Task TransitionToScene(string scenePath)
|
||||
{
|
||||
var newScene = await LoadSceneAsync<Node>(scenePath);
|
||||
## 当前边界
|
||||
|
||||
// 清理当前场景
|
||||
ForEachChild<Node>(child => child.QueueFreeX());
|
||||
- 当前 `NodeExtensions` 没有 `GetNodeX()`、`CreateSignalBuilder()` 之类旧文档里提过的 API
|
||||
- 它不是 router、scene factory、UI factory 或生成器的替代层
|
||||
- `GetOrCreateNode<T>()` 只会创建一个直接子节点,不会递归补整条路径
|
||||
- `SafeCallDeferred(...)` 只有在 `IsValidNode()` 为 `true` 时才会调用;节点未入树时不会执行
|
||||
- `UnRegisterWhenNodeExitTree(...)` 只针对实现了 `IUnRegister` 的框架订阅句柄,不会自动处理 Godot 原生 `Connect(...)`
|
||||
- 协程辅助扩展在 `GFramework.Godot.Coroutine` 命名空间,不属于这组 `Extensions` 页面要覆盖的核心范围
|
||||
|
||||
// 加载新场景
|
||||
await AddChildXAsync(newScene);
|
||||
## 继续阅读
|
||||
|
||||
// 设置输入处理
|
||||
newScene.SetInputAsHandled();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 1. 安全性
|
||||
|
||||
- 所有节点操作都包含有效性检查
|
||||
- 提供安全的类型转换方法
|
||||
- 避免空引用异常
|
||||
|
||||
### 2. 便利性
|
||||
|
||||
- 流畅的 API 设计
|
||||
- 支持链式调用
|
||||
- 减少样板代码
|
||||
|
||||
### 3. 一致性
|
||||
|
||||
- 统一的命名约定
|
||||
- 一致的返回类型
|
||||
- 预测性方法行为
|
||||
|
||||
### 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);
|
||||
|
||||
// ❌ 避免:手动管理事件生命周期
|
||||
// 可能导致内存泄漏
|
||||
```
|
||||
- [Godot 运行时集成](./index.md)
|
||||
- [Godot 信号系统](./signal.md)
|
||||
- [Godot 场景系统](./scene.md)
|
||||
- [Godot UI 系统](./ui.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,43 +1,40 @@
|
||||
---
|
||||
title: Godot 日志系统
|
||||
description: Godot 日志系统提供了 GFramework 日志功能与 Godot 引擎控制台的完整集成。
|
||||
description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准,说明 Godot 日志 provider、控制台输出语义与接入边界。
|
||||
---
|
||||
|
||||
# Godot 日志系统
|
||||
|
||||
## 概述
|
||||
`GFramework.Godot` 当前的日志能力很收敛:它不是一套独立于 Core 的新日志框架,而是把现有 `ILogger` 调用面接到
|
||||
Godot 控制台。
|
||||
|
||||
Godot 日志系统是 GFramework.Godot 中连接框架日志功能与 Godot 引擎控制台的核心组件。它提供了与 Godot
|
||||
控制台的深度集成,支持彩色输出、多级别日志记录,以及与 GFramework 日志系统的无缝对接。
|
||||
换句话说,Godot 侧真正新增的是 provider / factory / logger 这层输出适配,而不是新的日志 API。业务代码仍然继续使用
|
||||
`LoggerFactoryResolver.Provider.CreateLogger(...)` 或 `[Log]` 生成的 `ILogger` 字段。
|
||||
|
||||
通过 Godot 日志系统,你可以在 Godot 项目中使用统一的日志接口,日志会自动输出到 Godot 编辑器控制台,并根据日志级别使用不同的颜色和输出方式。
|
||||
## 当前公开入口
|
||||
|
||||
**主要特性**:
|
||||
### `GodotLogger`
|
||||
|
||||
- 与 Godot 控制台深度集成
|
||||
- 支持彩色日志输出
|
||||
- 多级别日志记录(Trace、Debug、Info、Warning、Error、Fatal)
|
||||
- 日志缓存机制
|
||||
- 时间戳和格式化支持
|
||||
- 异常信息记录
|
||||
|
||||
## 核心概念
|
||||
|
||||
### GodotLogger
|
||||
|
||||
`GodotLogger` 是 Godot 平台的日志记录器实现,继承自 `AbstractLogger`:
|
||||
`GodotLogger` 继承自 `AbstractLogger`,负责把日志写到 Godot 的输出 API:
|
||||
|
||||
```csharp
|
||||
public sealed class GodotLogger : AbstractLogger
|
||||
{
|
||||
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info);
|
||||
protected override void Write(LogLevel level, string message, Exception? exception);
|
||||
}
|
||||
public sealed class GodotLogger(
|
||||
string? name = null,
|
||||
LogLevel minLevel = LogLevel.Info)
|
||||
: 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
|
||||
public class GodotLoggerFactory : ILoggerFactory
|
||||
@ -46,9 +43,11 @@ public class GodotLoggerFactory : ILoggerFactory
|
||||
}
|
||||
```
|
||||
|
||||
### GodotLoggerFactoryProvider
|
||||
它本身不做缓存,也不额外增加过滤规则。
|
||||
|
||||
`GodotLoggerFactoryProvider` 提供日志工厂实例,并支持日志缓存:
|
||||
### `GodotLoggerFactoryProvider`
|
||||
|
||||
`GodotLoggerFactoryProvider` 是当前最常用的接入点:
|
||||
|
||||
```csharp
|
||||
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
|
||||
using GFramework.Godot.Architecture;
|
||||
using GFramework.Godot.Logging;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Abstractions.Environment;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Properties;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Godot.Logging;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
var architecture = new GameArchitecture(
|
||||
new ArchitectureConfiguration
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 配置 Godot 日志系统
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
LoggerProperties = new LoggerProperties
|
||||
{
|
||||
MinLevel = LogLevel.Debug // 设置最小日志级别
|
||||
};
|
||||
}
|
||||
LoggerFactoryProvider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Debug
|
||||
}
|
||||
}
|
||||
},
|
||||
environment);
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
|
||||
logger.Info("游戏架构初始化开始");
|
||||
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
|
||||
logger.Info("游戏架构初始化完成");
|
||||
}
|
||||
}
|
||||
architecture.Initialize();
|
||||
```
|
||||
|
||||
### 创建和使用日志记录器
|
||||
这样做的好处是:
|
||||
|
||||
- 日志 provider 和架构启动配置放在同一个入口
|
||||
- 不会把“Godot 控制台输出”误写成全局静态默认前提
|
||||
- 和 `ArchitectureConfiguration` 默认使用 `ConsoleLoggerFactoryProvider` 的 Core 接线方式保持一致
|
||||
|
||||
### 2. 业务代码继续使用标准 `ILogger`
|
||||
|
||||
配置好 provider 之后,Godot 节点、System、Model、router、factory 都继续通过统一入口拿 logger:
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Core.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()
|
||||
{
|
||||
// 创建日志记录器
|
||||
_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("玩家状态异常");
|
||||
Log.Info("SettingsPanel ready.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 记录不同级别的日志
|
||||
如果你已经在用 `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
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameSystem");
|
||||
using GFramework.Game.Scene.Handler;
|
||||
using GFramework.Game.UI.Handler;
|
||||
|
||||
// Trace - 最详细的跟踪信息(灰色)
|
||||
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("游戏崩溃");
|
||||
RegisterHandler(new LoggingTransitionHandler());
|
||||
```
|
||||
|
||||
### 记录异常信息
|
||||
这也说明 Godot 日志页不需要重新定义一套“Godot 专用场景日志接口”;现有 Game 运行时日志在 Godot 宿主里本来就会复用
|
||||
这套 provider。
|
||||
|
||||
```csharp
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("SaveSystem");
|
||||
## Godot 控制台输出语义
|
||||
|
||||
try
|
||||
{
|
||||
SaveGame();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录异常信息
|
||||
logger.Error("保存游戏失败", ex);
|
||||
}
|
||||
当前 `GodotLogger.Write(...)` 的级别映射如下:
|
||||
|
||||
| 日志级别 | Godot 输出 API | 当前行为 |
|
||||
| --- | --- | --- |
|
||||
| `Trace` | `GD.PrintRich(...)` | 使用灰色富文本输出 |
|
||||
| `Debug` | `GD.PrintRich(...)` | 使用青色富文本输出 |
|
||||
| `Info` | `GD.Print(...)` | 普通控制台输出 |
|
||||
| `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
|
||||
using GFramework.Core.System;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
- 手写 `LoggerFactoryResolver.Provider.CreateLogger(...)`
|
||||
- 少量入口类
|
||||
- 需要自己控制字段名、静态/实例生命周期
|
||||
- 想明确看到 logger 初始化位置
|
||||
- 用 `[Log]`
|
||||
- Godot 节点、controller、system 上有大量重复 logger 字段样板
|
||||
- 你已经引用 `GFramework.Core.SourceGenerators`
|
||||
- 想把 logger 字段生成交给编译期
|
||||
|
||||
public class CombatSystem : AbstractSystem
|
||||
{
|
||||
private ILogger _logger;
|
||||
这里的边界要分清:
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
|
||||
_logger.Info("战斗系统初始化完成");
|
||||
}
|
||||
- Godot provider:来自 `GFramework.Godot`
|
||||
- `[Log]` 生成器:来自 `GFramework.Core.SourceGenerators`
|
||||
|
||||
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()
|
||||
{
|
||||
_logger.Info("战斗系统已销毁");
|
||||
}
|
||||
}
|
||||
```
|
||||
- 当前推荐接法是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;直接赋值
|
||||
`LoggerFactoryResolver.Provider` 仍然可用,但不该再写成默认采用路径
|
||||
- `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
|
||||
using GFramework.Core.Model;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
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) - 架构最佳实践
|
||||
- [Core 日志系统](../core/logging.md)
|
||||
- [日志生成器](../source-generators/logging-generator.md)
|
||||
- [Godot 运行时集成](./index.md)
|
||||
- [Godot 场景系统](./scene.md)
|
||||
- [Godot UI 系统](./ui.md)
|
||||
|
||||
@ -1,583 +1,321 @@
|
||||
---
|
||||
title: Godot 场景系统
|
||||
description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成。
|
||||
description: 以当前 GFramework.Godot 源码、Game 场景契约与 CoreGrid 接线为准,说明 PackedScene 场景工厂、行为包装和最小接入路径。
|
||||
---
|
||||
|
||||
# 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
|
||||
public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
||||
where T : Node
|
||||
using global::CoreGrid.global;
|
||||
using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;
|
||||
|
||||
namespace CoreGrid.scripts.core.scene;
|
||||
|
||||
public partial class SceneRouter : SceneRouterBase
|
||||
{
|
||||
protected readonly T Owner;
|
||||
public string Key { get; }
|
||||
public IScene Scene { get; }
|
||||
}
|
||||
```
|
||||
[GetUtility] private IGodotSceneRegistry _sceneRegistry = null!;
|
||||
|
||||
### 场景工厂
|
||||
public Node? SceneRoot => Root as Node;
|
||||
|
||||
`GodotSceneFactory` 负责创建场景实例:
|
||||
|
||||
```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)
|
||||
protected override void RegisterHandlers()
|
||||
{
|
||||
GD.Print("加载主菜单资源");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
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;
|
||||
__InjectContextBindings_Generated();
|
||||
RegisterHandler(new LoggingTransitionHandler());
|
||||
RegisterAroundHandler(
|
||||
new SceneTransitionAnimationHandler(() => SceneTransitionManager.Instance!, _sceneRegistry.GetAll()));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册场景
|
||||
这里可以看到,Godot 适配点在 factory / registry / root / transition handler 上,而 router 仍然是项目类。
|
||||
|
||||
### 2. 注册 `IGodotSceneRegistry` 与 `ISceneFactory`
|
||||
|
||||
最小 wiring 需要把 registry 和 factory 装进架构:
|
||||
|
||||
```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.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()
|
||||
{
|
||||
_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()
|
||||
{
|
||||
return _behavior;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义场景行为
|
||||
public class GameplaySceneBehavior : Node2DSceneBehavior
|
||||
{
|
||||
public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key)
|
||||
{
|
||||
return _scene ??= SceneBehaviorFactory.Create(this, nameof(SceneKey.Gameplay));
|
||||
}
|
||||
|
||||
protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param)
|
||||
public ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
GD.Print("加载游戏场景");
|
||||
// 加载游戏资源
|
||||
await Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async ValueTask OnEnterInternalAsync()
|
||||
public ValueTask OnEnterAsync()
|
||||
{
|
||||
GD.Print("进入游戏场景");
|
||||
Owner.Show();
|
||||
await Task.CompletedTask;
|
||||
return ValueTask.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
|
||||
// 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.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 FadeTransitionHandler(ColorRect fadeRect)
|
||||
public ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
_fadeRect = fadeRect;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
|
||||
public ValueTask OnEnterAsync()
|
||||
{
|
||||
// 淡出动画
|
||||
var tween = _fadeRect.CreateTween();
|
||||
tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f);
|
||||
await tween.ToSignal(tween, Tween.SignalName.Finished);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
public ValueTask OnPauseAsync()
|
||||
{
|
||||
// 淡入动画
|
||||
var tween = _fadeRect.CreateTween();
|
||||
tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f);
|
||||
await tween.ToSignal(tween, Tween.SignalName.Finished);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### 场景间通信
|
||||
|
||||
```csharp
|
||||
// 通过事件通信
|
||||
public partial class GameplayScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnEnterAsync()
|
||||
public ValueTask OnResumeAsync()
|
||||
{
|
||||
// 发送场景进入事件
|
||||
this.SendEvent(new GameplaySceneEnteredEvent());
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// 在其他地方监听
|
||||
public partial class HUD : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
this.RegisterEvent<GameplaySceneEnteredEvent>(OnGameplayEntered);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnGameplayEntered(GameplaySceneEnteredEvent evt)
|
||||
public ValueTask OnExitAsync()
|
||||
{
|
||||
GD.Print("游戏场景已进入,显示 HUD");
|
||||
Show();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask OnUnloadAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
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` 接口:
|
||||
生成器当前会补出与源码一致的 `GetScene()`:
|
||||
|
||||
```csharp
|
||||
public partial class MyScene : Node2D, IScene
|
||||
public ISceneBehavior GetScene()
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param) { }
|
||||
public async ValueTask OnEnterAsync() { }
|
||||
// ... 实现其他方法
|
||||
return __autoSceneBehavior_Generated ??= SceneBehaviorFactory.Create(this, SceneKeyStr);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:场景切换时节点如何管理?
|
||||
要注意两点:
|
||||
|
||||
**解答**:
|
||||
使用场景根节点管理:
|
||||
- `[AutoScene]` 只生成方法和 key,不会替你自动补 `: ISceneBehaviorProvider`
|
||||
- `IScene` 仍然是业务生命周期契约;不实现它时,默认 behavior 只会保留基础节点切换语义
|
||||
|
||||
### 5. 从业务代码发起导航
|
||||
|
||||
一旦 registry、factory、router、root 都装好,导航入口仍然是 `ISceneRouter`:
|
||||
|
||||
```csharp
|
||||
// 场景路由会自动管理节点的添加和移除
|
||||
await sceneRouter.ReplaceAsync("NewScene");
|
||||
// 旧场景节点会被移除,新场景节点会被添加
|
||||
await sceneRouter.ReplaceAsync(nameof(SceneKey.MainMenu));
|
||||
await sceneRouter.ReplaceAsync(nameof(SceneKey.Gameplay), new GameplayEnterParam());
|
||||
await sceneRouter.PushAsync(nameof(SceneKey.PauseMenu));
|
||||
await sceneRouter.PopAsync();
|
||||
```
|
||||
|
||||
### 问题:如何实现场景预加载?
|
||||
## 当前边界
|
||||
|
||||
**解答**:
|
||||
使用场景工厂提前创建场景:
|
||||
### 没有 `GodotSceneRouter`
|
||||
|
||||
```csharp
|
||||
var sceneFactory = this.GetUtility<ISceneFactory>();
|
||||
var sceneBehavior = sceneFactory.Create("NextScene");
|
||||
await sceneBehavior.LoadAsync(null);
|
||||
```
|
||||
仓库当前不存在 `GodotSceneRouter` 类型。旧文档里把它写成默认入口是失真的;实际入口仍然是项目侧继承
|
||||
`SceneRouterBase` 的 router。
|
||||
|
||||
### 问题:场景生命周期方法的调用顺序是什么?
|
||||
### 没有自动注册所有场景
|
||||
|
||||
**解答**:
|
||||
当前运行时只认识你注册进 `IGodotSceneRegistry` 的 `PackedScene`。它不会扫描目录、不会从脚本类型自动反推出注册表。
|
||||
|
||||
- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow`
|
||||
- 暂停场景:`OnPause` -> `OnHide`
|
||||
- 恢复场景:`OnShow` -> `OnResume`
|
||||
- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync`
|
||||
### provider 是“优先路径”,不是“唯一路径”
|
||||
|
||||
### 问题:如何在场景中访问架构组件?
|
||||
`GodotSceneFactory` 会优先使用 `ISceneBehaviorProvider`,但没有 provider 时仍会按节点类型自动包装。这个行为和 UI 系统不同;
|
||||
UI 工厂当前没有同等的自动回退。
|
||||
|
||||
**解答**:
|
||||
使用扩展方法:
|
||||
### root 仍然是项目职责
|
||||
|
||||
```csharp
|
||||
public partial class MyScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameSystem = this.GetSystem<GameSystem>();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
`ISceneRoot` 的实现决定:
|
||||
|
||||
### 问题:场景切换时如何显示加载界面?
|
||||
- 节点挂到哪里
|
||||
- 移除时如何释放
|
||||
- 是否保留额外的当前视图引用
|
||||
|
||||
**解答**:
|
||||
使用场景转换处理器:
|
||||
Godot runtime 不会替项目生成统一的 root 节点。
|
||||
|
||||
```csharp
|
||||
public class LoadingScreenHandler : ISceneTransitionHandler
|
||||
{
|
||||
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 显示加载界面
|
||||
ShowLoadingScreen();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
## 继续阅读
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 隐藏加载界面
|
||||
HideLoadingScreen();
|
||||
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 扩展方法
|
||||
1. [Godot 运行时集成](./index.md)
|
||||
2. [Godot 架构集成](./architecture.md)
|
||||
3. [Game 场景系统](../game/scene.md)
|
||||
4. [AutoScene 生成器](../source-generators/auto-scene-generator.md)
|
||||
|
||||
@ -1,420 +1,158 @@
|
||||
# 信号连接系统 (Signal Connection System)
|
||||
---
|
||||
title: Godot 信号系统
|
||||
description: 以当前 GFramework.Godot 源码与 CoreGrid 的动态绑定用法为准,说明 Signal(...) fluent API、SignalBuilder 行为与接入边界。
|
||||
---
|
||||
|
||||
## 概述
|
||||
# Godot 信号系统
|
||||
|
||||
信号连接系统是 Godot 扩展方法模块中的一个专门子模块,提供流畅、类型安全的 Godot 信号连接 API。该系统采用构建器模式(Builder
|
||||
Pattern)和流畅接口设计(Fluent Interface),大大简化了信号订阅代码,提高了代码的可读性和可维护性。
|
||||
`GFramework.Godot` 当前提供的信号能力很收敛:它不是另一套事件系统,也不是自动生成绑定代码的入口,而是对
|
||||
`GodotObject.Connect(...)` 的一层 fluent 包装。
|
||||
|
||||
## 核心类
|
||||
当前真正公开的入口只有两个:
|
||||
|
||||
### SignalBuilder
|
||||
- `SignalFluentExtensions.Signal(...)`
|
||||
- `SignalBuilder`
|
||||
|
||||
信号连接构建器,负责构建和执行信号连接操作。
|
||||
如果你需要的是场景节点字段注入和静态 signal 自动绑订,请看
|
||||
`GFramework.Godot.SourceGenerators` 的 `[GetNode]` 与 `[BindNodeSignal]`,不要把它们和这里的运行时 fluent API 混成同一层。
|
||||
|
||||
**特性:**
|
||||
## 当前公开入口
|
||||
|
||||
- 支持链式调用
|
||||
- 可配置连接标志
|
||||
- 支持连接后立即调用
|
||||
- 返回原始对象以便继续操作
|
||||
### `Signal(...)`
|
||||
|
||||
### SignalFluentExtensions
|
||||
|
||||
为 `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 对象创建信号构建器。
|
||||
`Signal(...)` 是定义在 `GodotObject` 上的扩展方法:
|
||||
|
||||
```csharp
|
||||
public static SignalBuilder Signal(this GodotObject @object, StringName signal)
|
||||
```
|
||||
|
||||
**参数:**
|
||||
它只做一件事:基于目标对象和 signal 名称创建一个 `SignalBuilder`。这意味着当前 fluent API 不只适用于 `Node`,也适用于
|
||||
其他 Godot 对象。
|
||||
|
||||
- `@object` - 扩展方法的目标对象
|
||||
- `signal` - 要连接的信号名称
|
||||
### `SignalBuilder`
|
||||
|
||||
## 实际应用场景
|
||||
`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
|
||||
public class MainMenu : Control
|
||||
using GFramework.Godot.Extensions.Signal;
|
||||
using Godot;
|
||||
|
||||
public partial class SettingsPanel : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var startButton = GetNode<Button>("StartButton");
|
||||
var quitButton = GetNode<Button>("QuitButton");
|
||||
var settingsButton = GetNode<Button>("SettingsButton");
|
||||
var applyButton = GetNode<Button>("%ApplyButton");
|
||||
|
||||
// 开始按钮 - 一次性连接并立即禁用
|
||||
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)));
|
||||
applyButton.Signal(Button.SignalName.Pressed)
|
||||
.To(Callable.From(OnApplyPressed));
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
public class AudioManager : Node
|
||||
{
|
||||
private AudioStreamPlayer _bgmPlayer;
|
||||
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();
|
||||
}
|
||||
}
|
||||
_quitConfirmDialog.Signal("Confirmed")
|
||||
.To(Callable.From(OnQuitConfirmDialogConfirmed))
|
||||
.End();
|
||||
```
|
||||
|
||||
## 设计模式分析
|
||||
## 什么时候用 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 有副作用,需要你自己确认时机
|
||||
|
||||
流畅接口设计:
|
||||
## 继续阅读
|
||||
|
||||
- 方法链式调用
|
||||
- 可读性强
|
||||
- 表达力强
|
||||
|
||||
### 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);
|
||||
```
|
||||
- [Godot 运行时集成](./index.md)
|
||||
- [Godot 扩展方法](./extensions.md)
|
||||
- [Godot 集成教程](../tutorials/godot-integration.md)
|
||||
- [BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md)
|
||||
|
||||
@ -1,643 +1,351 @@
|
||||
---
|
||||
title: Godot UI 系统
|
||||
description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成。
|
||||
description: 以当前 GFramework.Godot 源码、Game UI 契约与 CoreGrid 接线为准,说明 PackedScene 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 页面行为封装
|
||||
- UI 工厂和注册表
|
||||
- 与 Godot PackedScene 集成
|
||||
- 多层级 UI 支持(Page、Overlay、Modal、Toast、Topmost)
|
||||
- UI 生命周期管理
|
||||
- UI 根节点管理
|
||||
### `IGodotUiRegistry`
|
||||
|
||||
## 核心概念
|
||||
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
|
||||
public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior
|
||||
where T : CanvasItem
|
||||
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
|
||||
|
||||
namespace CoreGrid.scripts.core.ui;
|
||||
|
||||
[Log]
|
||||
public partial class UiRouter : UiRouterBase
|
||||
{
|
||||
protected readonly T Owner;
|
||||
public string Key { get; }
|
||||
public UiLayer Layer { get; }
|
||||
public bool IsReentrant { get; }
|
||||
protected override void RegisterHandlers()
|
||||
{
|
||||
_log.Debug("Registering default transition handlers");
|
||||
RegisterHandler(new LoggingTransitionHandler());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 工厂
|
||||
Godot runtime 自身并不接管这层 router 的定义。
|
||||
|
||||
`GodotUiFactory` 负责创建 UI 实例:
|
||||
### 2. 注册 `IGodotUiRegistry` 与 `IUiFactory`
|
||||
|
||||
最小 wiring 需要显式注册 UI 资源表和工厂:
|
||||
|
||||
```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.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)
|
||||
{
|
||||
GD.Print("进入主菜单");
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
GD.Print("退出主菜单");
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void OnPause()
|
||||
{
|
||||
GD.Print("暂停主菜单");
|
||||
}
|
||||
|
||||
public void OnResume()
|
||||
{
|
||||
GD.Print("恢复主菜单");
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实现 UI 页面行为提供者
|
||||
#### 方式 B:用 `[AutoUiPage]` 让生成器补样板
|
||||
|
||||
当前更贴近真实消费者 wiring 的方式,是让生成器产出 `UiKeyStr` 和 `GetPage()`:
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
using GFramework.Godot.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 GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
public class GameUiRegistry : GodotUiRegistry
|
||||
{
|
||||
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
|
||||
[AutoUiPage(nameof(UiKey.MainMenu), nameof(UiLayer.Page))]
|
||||
public partial class MainMenu : Control, IUiPageBehaviorProvider, IUiPage
|
||||
{
|
||||
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()
|
||||
{
|
||||
// 淡出动画
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 0.0f, 0.3f);
|
||||
tween.TweenCallback(Callable.From(Hide));
|
||||
}
|
||||
|
||||
public void OnPause()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnResume()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### UI 句柄管理
|
||||
当前生成器补出的核心样板与源码一致:
|
||||
|
||||
```csharp
|
||||
public partial class DialogManager : Node
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
private UiHandle? _currentDialog;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return __autoUiPageBehavior_Generated ??=
|
||||
UiPageBehaviorFactory.Create(this, UiKeyStr, UiLayer.Page);
|
||||
}
|
||||
```
|
||||
|
||||
### 多个 Toast 显示
|
||||
要注意两点:
|
||||
|
||||
- `[AutoUiPage]` 不会替你自动补 `: IUiPageBehaviorProvider`
|
||||
- UI 层级是生成器输入的一部分;`Page` / `Modal` / `Overlay` 语义不是后面再猜出来的
|
||||
|
||||
### 5. 按 layer 选择正确入口
|
||||
|
||||
Godot runtime 只是落地 `UiRouterBase` 的语义,因此入口仍然和 `GFramework.Game` 一致:
|
||||
|
||||
页面栈:
|
||||
|
||||
```csharp
|
||||
public partial class ToastManager : Node
|
||||
{
|
||||
private readonly List<UiHandle> _activeToasts = new();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
await uiRouter.ReplaceAsync(nameof(UiKey.MainMenu));
|
||||
await uiRouter.PushAsync(nameof(UiKey.Settings));
|
||||
await uiRouter.PopAsync();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
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` 接口:
|
||||
层级 UI:
|
||||
|
||||
```csharp
|
||||
public partial class MyPage : Control, IUiPage, IUiPageBehaviorProvider
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param) { }
|
||||
public IUiPageBehavior GetPage() { return new PageLayerUiPageBehavior(this, "MyPage"); }
|
||||
}
|
||||
var handle = uiRouter.Show(nameof(UiKey.PauseMenu), UiLayer.Modal);
|
||||
uiRouter.Hide(handle, UiLayer.Modal);
|
||||
```
|
||||
|
||||
### 问题:UI 层级有什么区别?
|
||||
当前实现里,`Show(..., UiLayer.Page)` 会直接抛异常;`Page` 层必须走 `PushAsync` / `ReplaceAsync`。
|
||||
|
||||
**解答**:
|
||||
## 输入与暂停语义
|
||||
|
||||
- **Page**:栈管理,不可重入,用于主要页面
|
||||
- **Overlay**:可重入,用于浮层
|
||||
- **Modal**:可重入,带遮罩,用于对话框
|
||||
- **Toast**:可重入,轻量提示
|
||||
- **Topmost**:不可重入,最高优先级
|
||||
如果页面只实现 `IUiPage`,它只有基础生命周期。
|
||||
|
||||
### 问题:如何实现 UI 动画?
|
||||
如果还需要更强的输入仲裁或暂停语义,可以像 CoreGrid 的 `PauseMenu` 一样继续实现:
|
||||
|
||||
**解答**:
|
||||
在生命周期方法中使用 Godot Tween:
|
||||
- `IUiInteractionProfileProvider`
|
||||
- `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
|
||||
public partial class MyPage : Control, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameSystem = this.GetSystem<GameSystem>();
|
||||
}
|
||||
}
|
||||
```
|
||||
- 捕获 `Cancel`
|
||||
- 阻断 World pointer / action input
|
||||
- 在可见时持有暂停
|
||||
- 即使在暂停状态也继续处理节点逻辑
|
||||
|
||||
### 问题:如何关闭 Modal 或 Toast?
|
||||
## 当前边界
|
||||
|
||||
**解答**:
|
||||
使用 UI 句柄:
|
||||
### 没有 `GodotUiRouter`
|
||||
|
||||
```csharp
|
||||
// 显示时保存句柄
|
||||
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
|
||||
仓库当前没有这个类型。旧文档把它写成默认入口是不准确的;真实入口仍然是项目侧的 `UiRouterBase` 派生类。
|
||||
|
||||
// 关闭时使用句柄
|
||||
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
|
||||
```
|
||||
### UI 工厂不会自动补 behavior
|
||||
|
||||
### 问题:UI 生命周期方法的调用顺序是什么?
|
||||
和 `GodotSceneFactory` 不同,`GodotUiFactory` 当前不会按节点类型自动创建 behavior。节点不实现
|
||||
`IUiPageBehaviorProvider` 时会直接失败。
|
||||
|
||||
**解答**:
|
||||
### `Page` 层不是 `Show(...)` 的适用对象
|
||||
|
||||
- 进入:`OnEnter` -> `OnShow`
|
||||
- 暂停:`OnPause` -> `OnHide`
|
||||
- 恢复:`OnShow` -> `OnResume`
|
||||
- 退出:`OnHide` -> `OnExit`
|
||||
`UiLayer.Page` 代表页面栈语义,而不是普通 layer UI。当前实现明确要求:
|
||||
|
||||
## 相关文档
|
||||
- `Page` 用 `PushAsync` / `ReplaceAsync`
|
||||
- `Overlay` / `Modal` / `Toast` / `Topmost` 用 `Show` / `Hide`
|
||||
|
||||
- [UI 系统](/zh-CN/game/ui) - 核心 UI 管理
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
### root 仍然由项目控制
|
||||
|
||||
`IUiRoot` 决定:
|
||||
|
||||
- 每个 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)
|
||||
|
||||
@ -1,21 +1,26 @@
|
||||
---
|
||||
title: AutoRegisterExportedCollections 生成器
|
||||
description: 说明批量注册生成器当前会生成什么、可匹配哪些集合与注册器成员,以及 null-skip 与编译期诊断的边界。
|
||||
---
|
||||
|
||||
# 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
|
||||
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]
|
||||
public partial class GameEntryPoint : Node
|
||||
{
|
||||
@ -57,119 +55,169 @@ public partial class GameEntryPoint : Node
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_textureRegistry ??= new TextureRegistry();
|
||||
_textureRegistry ??= ResolveTextureRegistry();
|
||||
__RegisterExportedCollections_Generated();
|
||||
}
|
||||
|
||||
private static IAssetRegistry<Texture2D> ResolveTextureRegistry()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
为了让示例具备完整的调用路径,这里在 `_Ready()` 里先初始化了 `_textureRegistry`。
|
||||
实际项目里,这个字段通常来自架构容器、服务定位或外部注入;关键点是调用 `__RegisterExportedCollections_Generated()`
|
||||
之前,注册器成员必须已经可用,否则生成代码会按设计静默跳过注册。
|
||||
当前生成器不会自动调用 `__RegisterExportedCollections_Generated()`。你需要在 registry 成员和集合成员都准备好之后手动调用。
|
||||
|
||||
## 生成的代码
|
||||
## 当前会生成什么
|
||||
|
||||
对于上面的成员,当前生成器会产出:
|
||||
|
||||
```csharp
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
partial class GameEntryPoint
|
||||
private void __RegisterExportedCollections_Generated()
|
||||
{
|
||||
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()`
|
||||
- 非泛型 `IEnumerable` 之类无法推导元素类型的集合不受支持
|
||||
- 注册方法必须对宿主类型可访问
|
||||
- 生成器不会自动接入 `_Ready()` 或其他生命周期方法
|
||||
- 宿主类型若已声明 `__RegisterExportedCollections_Generated()`,会触发命名冲突诊断
|
||||
- 只有当至少一个成员成功通过验证时,才会生成方法
|
||||
|
||||
## 诊断信息
|
||||
## 诊断速查
|
||||
|
||||
| 诊断 ID | 含义 |
|
||||
|-----------------------|-------------------------------------------------------------|
|
||||
| `GF_Common_Class_001` | 目标类型不是 `partial`,生成被跳过 |
|
||||
| `GF_Common_Class_002` | 宿主类型已声明 `__RegisterExportedCollections_Generated()`,与生成代码冲突 |
|
||||
| `GF_AutoExport_001` | `AutoRegisterExportedCollections` 不支持嵌套类 |
|
||||
| `GF_AutoExport_002` | 指定的注册器成员不存在 |
|
||||
| `GF_AutoExport_003` | 注册器成员上找不到兼容的注册方法 |
|
||||
| `GF_AutoExport_004` | 被标记的成员不是可枚举集合 |
|
||||
| `GF_AutoExport_005` | 无法推导集合元素类型 |
|
||||
| `GF_AutoExport_006` | 集合成员不是实例可读成员 |
|
||||
| `GF_AutoExport_007` | 注册器成员不是实例可读成员 |
|
||||
| `GF_AutoExport_008` | `RegisterExportedCollectionAttribute` 参数无效 |
|
||||
| 诊断 ID | 含义 |
|
||||
| --- | --- |
|
||||
| `GF_Common_Class_001` | 宿主类型不是 `partial class` |
|
||||
| `GF_Common_Class_002` | 已手写 `__RegisterExportedCollections_Generated()`,与生成代码冲突 |
|
||||
| `GF_AutoExport_001` | 不支持嵌套类 |
|
||||
| `GF_AutoExport_002` | 指定的 registry 成员不存在 |
|
||||
| `GF_AutoExport_003` | 找不到兼容且可访问的注册方法 |
|
||||
| `GF_AutoExport_004` | 被标记成员不可枚举 |
|
||||
| `GF_AutoExport_005` | 无法安全推导集合元素类型 |
|
||||
| `GF_AutoExport_006` | 集合成员不是实例可读成员 |
|
||||
| `GF_AutoExport_007` | registry 成员不是实例可读成员 |
|
||||
| `GF_AutoExport_008` | `RegisterExportedCollectionAttribute` 构造参数无效 |
|
||||
|
||||
## 调用时机建议
|
||||
## 何时适合用它
|
||||
|
||||
推荐在以下时机之一调用生成方法:
|
||||
适合:
|
||||
|
||||
- `_Ready()` 中,且在注册器字段已经准备好之后
|
||||
- 启动入口的显式 `Initialize()` 或 `Bootstrap()` 方法中
|
||||
- 测试中的装配阶段
|
||||
- 启动入口里有多组“集合 -> registry”的重复注册代码
|
||||
- 每个元素都只需要一次简单的单参数注册
|
||||
- 你想把“注册到哪个 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`
|
||||
|
||||
@ -1,283 +1,46 @@
|
||||
---
|
||||
title: BindNodeSignal 生成器
|
||||
description: 说明 [BindNodeSignal] 当前生成什么、如何与 GetNode 协作,以及 _Ready 和 _ExitTree 的接入要求。
|
||||
---
|
||||
|
||||
# BindNodeSignal 生成器
|
||||
|
||||
> 自动生成 Godot 节点信号绑定与解绑逻辑,消除事件订阅样板代码
|
||||
`[BindNodeSignal]` 把 Godot CLR event 的 `+=` / `-=` 样板收敛成生成方法。它只生成“如何订阅与解绑”,不会替你查找节点,也不会自动生成完整生命周期方法。
|
||||
|
||||
## 概述
|
||||
## 当前包关系
|
||||
|
||||
BindNodeSignal 生成器为标记了 `[BindNodeSignal]` 特性的方法自动生成节点事件绑定和解绑代码。它将 `_Ready()` 和
|
||||
`_ExitTree()` 中重复的 `+=` 和 `-=` 样板代码收敛到生成器中统一维护。
|
||||
- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
|
||||
- 生成器实现:`GFramework.Godot.SourceGenerators`
|
||||
- 目标字段基线:`nodeFieldName` 指向的字段必须继承 `Godot.Node`
|
||||
|
||||
### 核心功能
|
||||
|
||||
- **自动事件绑定**:在 `_Ready()` 中自动订阅节点事件
|
||||
- **自动事件解绑**:在 `_ExitTree()` 中自动取消订阅
|
||||
- **多事件绑定**:一个方法可以绑定到多个节点事件
|
||||
- **类型安全检查**:编译时验证方法签名与事件委托的兼容性
|
||||
- **与 GetNode 集成**:无缝配合 `[GetNode]` 特性使用
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 标记事件处理方法
|
||||
|
||||
使用 `[BindNodeSignal]` 特性标记处理节点事件的方法:
|
||||
## 最小用法
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
public partial class MainMenu : Control
|
||||
public partial class Hud : Control
|
||||
{
|
||||
[GetNode]
|
||||
private Button _startButton = null!;
|
||||
private Button _settingsButton = null!;
|
||||
private Button _quitButton = null!;
|
||||
|
||||
[GetNode]
|
||||
private SpinBox _startOreSpinBox = null!;
|
||||
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
StartGame();
|
||||
}
|
||||
|
||||
[BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))]
|
||||
private void OnSettingsButtonPressed()
|
||||
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
|
||||
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()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
__BindNodeSignals_Generated();
|
||||
InitializeInventory();
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
@ -287,394 +50,143 @@ public partial class InventoryUI : Control
|
||||
}
|
||||
```
|
||||
|
||||
## 生命周期管理
|
||||
|
||||
### 自动生成生命周期方法
|
||||
|
||||
如果类没有 `_Ready()` 或 `_ExitTree()`,生成器会自动生成:
|
||||
当前生成器会产出:
|
||||
|
||||
```csharp
|
||||
public partial class AutoLifecycleHud : Control
|
||||
{
|
||||
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()
|
||||
private void __BindNodeSignals_Generated()
|
||||
{
|
||||
_startButton.Pressed += OnStartButtonPressed;
|
||||
_settingsButton.Pressed += OnSettingsButtonPressed;
|
||||
_quitButton.Pressed += OnQuitButtonPressed;
|
||||
_startOreSpinBox.ValueChanged += OnStartOreValueChanged;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
private void __UnbindNodeSignals_Generated()
|
||||
{
|
||||
// 容易遗漏解绑
|
||||
_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
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
```
|
||||
|
||||
[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)
|
||||
- [GetNode 生成器](./get-node-generator)
|
||||
- [ContextAware 生成器](./context-aware-generator)
|
||||
- [Godot 信号文档](https://docs.godotengine.org/en/stable/classes/class_signal.html)
|
||||
`ai-libs/CoreGrid` 里的 `GameplayHud`、`PauseMenu` 和 `OptionBrowser` 都在大量使用这种声明式绑定方式。
|
||||
|
||||
## 与 GetNode 的协作边界
|
||||
|
||||
`[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`
|
||||
|
||||
127
docs/zh-CN/source-generators/cqrs-handler-registry-generator.md
Normal file
127
docs/zh-CN/source-generators/cqrs-handler-registry-generator.md
Normal 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)
|
||||
@ -1,496 +1,198 @@
|
||||
---
|
||||
title: GetNode 生成器
|
||||
description: 说明 [GetNode] 当前生成什么、路径如何推断,以及 _Ready 生命周期里的接入边界。
|
||||
---
|
||||
|
||||
# GetNode 生成器
|
||||
|
||||
> 自动生成 Godot 节点获取逻辑,简化节点引用代码
|
||||
`[GetNode]` 用来把 Godot 节点查找样板收敛到生成器里。它只处理“字段如何取到节点”,不负责事件订阅,也不负责其他运行时装配。
|
||||
|
||||
## 概述
|
||||
## 当前包关系
|
||||
|
||||
GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode<T>()` 方法。这在处理复杂
|
||||
UI 或场景树结构时特别有用。
|
||||
- 特性来源:`GFramework.Godot.SourceGenerators.Abstractions`
|
||||
- 生成器实现:`GFramework.Godot.SourceGenerators`
|
||||
- 目标类型基线:字段类型必须继承 `Godot.Node`
|
||||
|
||||
### 核心功能
|
||||
|
||||
- **自动节点获取**:根据路径或字段名自动获取节点
|
||||
- **多种查找模式**:支持唯一名、相对路径、绝对路径查找
|
||||
- **可选节点支持**:可以标记节点为可选,获取失败时返回 null
|
||||
- **智能路径推导**:未显式指定路径时自动从字段名推导
|
||||
- **_Ready 钩子生成**:自动生成 `_Ready()` 方法注入节点获取逻辑
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 标记节点字段
|
||||
|
||||
使用 `[GetNode]` 特性标记需要自动获取的节点字段:
|
||||
## 最小用法
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
public partial class PlayerHud : Control
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode]
|
||||
private Label _healthLabel = null!;
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
[GetNode]
|
||||
private ProgressBar _manaBar = null!;
|
||||
|
||||
[GetNode("ScoreContainer/ScoreValue")]
|
||||
private Label _scoreLabel = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
_healthLabel.Text = "100";
|
||||
}
|
||||
private HBoxContainer m_rightContainer = null!;
|
||||
}
|
||||
```
|
||||
|
||||
### 生成的代码
|
||||
|
||||
编译器会为标记的类自动生成以下代码:
|
||||
如果目标类型还没有 `_Ready()`,当前生成器会补出:
|
||||
|
||||
```csharp
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace YourNamespace;
|
||||
|
||||
partial class PlayerHud
|
||||
private void __InjectGetNodes_Generated()
|
||||
{
|
||||
private void __InjectGetNodes_Generated()
|
||||
{
|
||||
_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!; // 错误
|
||||
}
|
||||
_leftContainer = GetNode<global::Godot.HBoxContainer>("%LeftContainer");
|
||||
m_rightContainer = GetNode<global::Godot.HBoxContainer>("%RightContainer");
|
||||
}
|
||||
|
||||
// ✅ 正确
|
||||
public partial class Inner
|
||||
{
|
||||
[GetNode]
|
||||
private Label _label = null!;
|
||||
}
|
||||
```
|
||||
partial void OnGetNodeReadyGenerated();
|
||||
|
||||
### 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()
|
||||
{
|
||||
_healthLabel = GetNode<Label>("%HealthLabel");
|
||||
_manaBar = GetNode<ProgressBar>("%ManaBar");
|
||||
_scoreLabel = GetNode<Label>("ScoreContainer/ScoreValue");
|
||||
__InjectGetNodes_Generated();
|
||||
OnGetNodeReadyGenerated();
|
||||
}
|
||||
```
|
||||
|
||||
// ✅ 推荐:使用 [GetNode] 特性
|
||||
[GetNode]
|
||||
private Label _healthLabel = null!;
|
||||
这个行为来自当前生成器测试,不是文档约定。
|
||||
|
||||
[GetNode]
|
||||
private ProgressBar _manaBar = null!;
|
||||
## 当前路径推断规则
|
||||
|
||||
### 没写路径时
|
||||
|
||||
如果 `[GetNode]` 没有显式路径,当前默认按字段名推导唯一名路径:
|
||||
|
||||
- `_leftContainer` -> `%LeftContainer`
|
||||
- `m_rightContainer` -> `%RightContainer`
|
||||
|
||||
也就是说,默认不是普通相对路径,而是 Godot 的 `%Name` 唯一名语法。
|
||||
|
||||
### 显式路径优先
|
||||
|
||||
```csharp
|
||||
[GetNode("ScoreContainer/ScoreValue")]
|
||||
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()
|
||||
{
|
||||
__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`
|
||||
|
||||
@ -1,52 +1,64 @@
|
||||
---
|
||||
title: Godot 项目元数据生成器
|
||||
description: 说明 project.godot 当前会生成什么、何时生效,以及 AutoLoad 和 Input Action 的映射边界。
|
||||
---
|
||||
|
||||
# Godot 项目元数据生成器
|
||||
|
||||
> 从 `project.godot` 生成 AutoLoad 与 Input Action 的强类型访问入口。
|
||||
`GodotProjectMetadataGenerator` 读取 `project.godot`,把 Godot 工程级配置转成稳定的编译期入口。
|
||||
|
||||
## 概述
|
||||
当前只覆盖两类信息:
|
||||
|
||||
`GFramework.Godot.SourceGenerators` 会读取 Godot 项目根目录下的 `project.godot`,并把其中最常用的项目级元数据暴露为稳定的编译期
|
||||
API。
|
||||
- `[autoload]` 段生成 `GFramework.Godot.Generated.AutoLoads`
|
||||
- `[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 引用 `GeWuYou.GFramework.Godot.SourceGenerators` 时,生成器会默认把项目根目录下的 `project.godot` 加入
|
||||
`AdditionalFiles`。
|
||||
常规 Godot C# 项目安装 `GeWuYou.GFramework.Godot.SourceGenerators` 后,包内 `targets` 会自动做两件事:
|
||||
|
||||
如需覆盖默认路径,可以设置:
|
||||
|
||||
- 可以改成项目根目录下的其他相对路径
|
||||
- 文件名必须仍然是 `project.godot`,否则生成器会给出警告并忽略该文件
|
||||
1. 注入 analyzer
|
||||
2. 如果项目根目录存在 `project.godot`,把它加入 `AdditionalFiles`
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<GFrameworkGodotProjectFile>Config/project.godot</GFrameworkGodotProjectFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GeWuYou.GFramework.Godot.SourceGenerators"
|
||||
Version="x.y.z"
|
||||
PrivateAssets="all"
|
||||
ExcludeAssets="runtime" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### 仓库内直接引用生成器
|
||||
|
||||
如果你通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器项目,则需要手动加入:
|
||||
如果你通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器项目,需要自己把 `project.godot` 放进
|
||||
`AdditionalFiles`:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Godot.SourceGenerators\GFramework.Godot.SourceGenerators.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
<AdditionalFiles Include="project.godot" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
## AutoLoad 访问层
|
||||
## 当前会生成什么
|
||||
|
||||
### 基础行为
|
||||
### AutoLoad 入口
|
||||
|
||||
假设 `project.godot` 中声明了:
|
||||
假设 `project.godot` 中有:
|
||||
|
||||
```ini
|
||||
[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
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
[AutoLoad("GameServices")]
|
||||
public partial class GameServices : Node
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
规则如下:
|
||||
|
||||
- 显式 `[AutoLoad]` 映射优先于隐式类名推断
|
||||
- 标记了 `[AutoLoad]` 的类型必须继承 `Godot.Node`
|
||||
- 若多个类型映射到同一个 AutoLoad,生成器会报告诊断,并退化为 `Godot.Node` 访问器,直到映射唯一
|
||||
|
||||
## Input Action 常量
|
||||
|
||||
### 基础行为
|
||||
### Input Action 常量
|
||||
|
||||
假设 `project.godot` 中有:
|
||||
|
||||
@ -114,59 +107,114 @@ if (Input.IsActionJustPressed(InputActions.MoveUp))
|
||||
}
|
||||
```
|
||||
|
||||
转换规则:
|
||||
这部分只生成稳定字符串常量,不会替你封装 `Input` 调用。
|
||||
|
||||
- `move_up` -> `MoveUp`
|
||||
- `ui_cancel` -> `UiCancel`
|
||||
- 非法字符会被清理后再转换为 PascalCase
|
||||
- 如果多个动作名落到同一个标识符,生成器会追加稳定数字后缀,例如 `MoveUp_2`
|
||||
## AutoLoad 类型推断的当前规则
|
||||
|
||||
## 与现有 Godot 生成器的关系
|
||||
### 优先级顺序
|
||||
|
||||
这项能力和现有的场景级生成器是互补的:
|
||||
当前映射顺序是:
|
||||
|
||||
- `AutoLoads` / `InputActions` 解决的是项目级元数据访问
|
||||
- `[GetNode]` 解决的是场景节点引用注入
|
||||
- `[BindNodeSignal]` 解决的是节点事件订阅样板
|
||||
1. 显式 `[AutoLoad("Name")]`
|
||||
2. 按 C# 类型名与 AutoLoad 名称做唯一匹配
|
||||
3. 无法唯一确定时退化为 `Godot.Node`
|
||||
|
||||
推荐组合方式:
|
||||
例如:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Generated;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
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 名称
|
||||
- 不同 AutoLoad 名称或 Input Action 名称在清洗后发生标识符冲突
|
||||
- `project.godot` 内部重复声明同名 AutoLoad 或 Input Action
|
||||
- `[AutoLoad]` 只能标在继承 `Godot.Node` 的类型上
|
||||
- 显式或隐式 AutoLoad 映射不唯一时,会退化为 `Godot.Node`
|
||||
- 标识符冲突会追加稳定后缀,而不是覆盖已有成员
|
||||
- 重复条目只保留第一条声明
|
||||
|
||||
这些诊断的目的不是阻断所有生成,而是在可能的情况下保留稳定输出,同时把不确定性显式暴露出来。
|
||||
## 推荐阅读
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [GetNode 生成器](./get-node-generator)
|
||||
- [BindNodeSignal 生成器](./bind-node-signal-generator)
|
||||
- [Godot 集成教程](../tutorials/godot-integration)
|
||||
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)
|
||||
3. [/zh-CN/tutorials/godot-integration](../tutorials/godot-integration.md)
|
||||
4. `GFramework.Godot.SourceGenerators/README.md`
|
||||
|
||||
@ -65,7 +65,9 @@ GFramework 当前发布的生成器包是:
|
||||
|
||||
- 配置 schema 生成与运行时接法:
|
||||
- [../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)
|
||||
|
||||
### Godot 专用生成器
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -28,24 +28,22 @@
|
||||
|
||||
### [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 小时
|
||||
|
||||
---
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user