docs(godot): 收口 signal 与 extensions 文档

- 更新 godot signal 页面,明确 Signal(...)、SignalBuilder 与 [BindNodeSignal] 的分工

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

- 补充 documentation-governance-and-refresh 跟踪与 trace,记录 RP-016 和验证结果
This commit is contained in:
GeWuYou 2026-04-22 13:20:18 +08:00
parent 03ecbe5989
commit 3ba1e3f202
4 changed files with 298 additions and 689 deletions

View File

@ -7,7 +7,7 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-015`
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-016`
- 当前阶段:`Phase 3`
- 当前焦点:
- 已建立统一公开 skill`.agents/skills/gframework-doc-refresh/`
@ -15,15 +15,16 @@
- `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``docs/zh-CN/godot/extensions.md`
- `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`
## 当前状态摘要
- 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证
- 高优先级模块入口、`core` 关键专题页与 `tutorials/godot-integration.md` 已回到“以源码 / 测试 / README 为准”的状态
- `docs/zh-CN/godot/index.md``architecture.md``scene.md``ui.md` 已完成当前实现收口
- 当前主题仍是 active topic因为 `docs/zh-CN/godot/signal.md``docs/zh-CN/godot/extensions.md` 仍可能保留旧
`SignalBuilder` / 大而全扩展层叙述Godot 文档链路尚未完全收口
- 当前主题仍是 active topic因为 `docs/zh-CN/godot/logging.md` 及其与运行时扩展页的交叉引用仍需复核Godot 文档链路尚未完全收口
## 当前活跃事实
@ -85,6 +86,13 @@
输入与暂停边界”的结构,明确当前没有 `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 页面改动没有破坏站点构建
- `.agents/skills/gframework-doc-refresh/SKILL.md` 已改成标准 YAML frontmatter skill并明确支持模块输入、证据顺序、输出优先级与验证步骤
- `.agents/skills/gframework-doc-refresh/SKILL.md``description` 已加引号,修复 `Recommended command:` 中冒号导致的
invalid YAML skill 加载警告
@ -105,10 +113,10 @@
`godot-project-generator.md``get-node-generator.md``bind-node-signal-generator.md``auto-register-exported-collections-generator.md`
已完成收口;
继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对剩余 Godot 相关页面,不把旧文档当事实来源
- Godot signal / extensions 专题页失真风险:`docs/zh-CN/godot/signal.md``docs/zh-CN/godot/extensions.md` 仍可能保留
`SignalBuilder` 或“大而全扩展层”叙述,重新把已经收口的入口页带偏
- 缓解措施:`scene.md` 与 `ui.md` 已完成收口;下一轮优先按当前 `Signal(...)` fluent API、`NodeExtensions` 实际成员与
`ai-libs/CoreGrid` 的使用方式重写 `signal.md``extensions.md`
- Godot logging 专题页失真风险:`docs/zh-CN/godot/logging.md` 仍可能沿用旧扩展页引用和过时运行时说明,把已经收口的
signal / extensions / index 页重新带偏
- 缓解措施:`signal.md` 与 `extensions.md` 已完成收口;下一轮优先按当前日志 API、Godot 运行时边界与真实交叉链接复核
`logging.md`
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
- 模块映射不全风险:统一 skill 若遗漏模块别名、测试项目或 docs 栏目映射,会让后续扫描阶段直接失焦
@ -160,11 +168,13 @@
- `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`
- `rg -n "GodotSceneRouter|GodotUiRouter|CreateSignalBuilder|GetNodeX|InstallGodotModule\(" docs/zh-CN/godot -S`
- `cd docs && bun run build`
## 下一步
1. 优先重写 `docs/zh-CN/godot/signal.md``docs/zh-CN/godot/extensions.md`,清掉旧 `SignalBuilder` / 大而全扩展层叙述
2. 视 `signal.md` / `extensions.md` 收口结果,决定是否同步复核 `docs/zh-CN/godot/logging.md`
1. 优先复核 `docs/zh-CN/godot/logging.md`,确认它不会把已收口的 signal / extensions / runtime 边界重新写偏
2. 视 `logging.md` 复核结果,决定是否可以把 Godot 栏目的 active 恢复点收口并准备归档本阶段历史
3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否按预期收敛

View File

@ -2,7 +2,7 @@
## 2026-04-22
### 当前恢复点RP-015
### 当前恢复点RP-016
- 本轮从 PR #268 的最新 review 数据恢复未发现失败检查CTRF 报告显示 2139 个测试全部通过
- 本轮复核确认当前 PR 的 latest-head open thread 同时来自 `coderabbitai[bot]``greptile-apps[bot]`
@ -36,8 +36,12 @@
`IUiPageBehaviorProvider``[AutoUiPage]` 的真实关系、输入与暂停边界”,不再继续虚构 `GodotUiRouter`
- 本轮额外确认 Godot Scene / UI 的关键差异:`GodotSceneFactory` 在 provider 缺失时会回退到 `SceneBehaviorFactory`
`GodotUiFactory` 仍会在缺失 `IUiPageBehaviorProvider` 时直接抛异常;这已写入两页文档,避免继续把两者描述成同一种接入模型
- 本轮检索确认 Godot 栏目新的高优先级页面转为 `docs/zh-CN/godot/signal.md``docs/zh-CN/godot/extensions.md`
两页仍保留 `SignalBuilder` / 大而全扩展层叙述,应作为 scene / ui 之后的下一轮收口对象
- 本轮已重写 `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]` 生成器链路
### 当前决策
@ -55,8 +59,10 @@
`GFramework.Godot` 运行时
- `ui.md` 已明确记录 `Page` 必须走 `PushAsync` / `ReplaceAsync``Show(..., UiLayer.Page)` 在当前实现中会抛异常;
后续不要再把所有 UI 入口重新写回统一 `Show(...)`
- `signal.md``extensions.md` 的下一轮收口应以 `Signal(...)` fluent API 与 `NodeExtensions` 的当前成员表为准,
不再继续复刻旧版 `SignalBuilder` 教程和泛化扩展层叙述
- `signal.md` 已明确为 `Signal(...)` / `SignalBuilder` 的轻量 fluent 包装说明页,不再继续混入生成器职责
- `extensions.md` 已明确限制在 `GodotPathExtensions``NodeExtensions``SignalFluentExtensions``UnRegisterExtension`
这四组当前存在的扩展
- Godot 栏目的下一轮优先级转为 `logging.md`;后续如果它仍复用旧扩展页话术,会重新污染已收口的入口页
### 验证
@ -79,11 +85,13 @@
- `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`
- `rg -n "GodotSceneRouter|GodotUiRouter|CreateSignalBuilder|GetNodeX|InstallGodotModule\(" docs/zh-CN/godot -S`
- `cd docs && bun run build`
### 下一步
1. 优先重写 `docs/zh-CN/godot/signal.md``docs/zh-CN/godot/extensions.md`,清掉旧 `SignalBuilder` / 大而全扩展层叙述
2. 视 `signal.md` / `extensions.md` 收口结果,决定是否同步复核 `docs/zh-CN/godot/logging.md`
1. 优先复核 `docs/zh-CN/godot/logging.md`,确认它的 API 说明与交叉链接不会把 signal / extensions / runtime 边界重新写偏
2. 视 `logging.md` 复核结果,决定是否可以把当前 Godot 栏目恢复点收口并迁入 archive
3. 下一次推送后重新执行 `$gframework-pr-review`,确认 PR #268 的 CodeRabbit / Greptile open thread 是否关闭或减少

View File

@ -1,328 +1,181 @@
# Godot 扩展方法 (Godot Extensions)
---
title: Godot 扩展方法
description: 以当前 GFramework.Godot.Extensions 源码为准说明路径、Node、signal 和 unregister 扩展的真实成员与边界。
---
## 概述
# Godot 扩展方法
Godot 扩展方法模块为 Godot 引擎提供了丰富的便捷扩展方法集合。这些扩展方法简化了常见的 Godot
开发任务,提高了代码的可读性和开发效率。该模块遵循流畅接口设计原则,支持链式调用。
`GFramework.Godot.Extensions` 当前并不是“覆盖所有 Godot 节点操作”的万能层。按源码看,它实际公开的扩展主要只有四组:
## 模块结构
- `GodotPathExtensions`
- `NodeExtensions`
- `SignalFluentExtensions`
- `UnRegisterExtension`
```mermaid
graph TD
A[Extensions] --> B[GodotPathExtensions]
A --> C[NodeExtensions]
A --> D[SignalFluentExtensions]
A --> E[UnRegisterExtension]
D --> F[SignalBuilder]
B --> G[路径判断扩展]
C --> H[节点生命周期]
C --> I[节点查询]
C --> J[场景树操作]
C --> K[输入控制]
C --> L[调试工具]
D --> M[信号连接系统]
E --> N[事件管理]
```
这页的重点应该是识别这些扩展各自解决什么问题,以及哪些旧文档里的“大而全能力”现在并不存在。
## 扩展模块详解
## 当前公开入口
### 1. 路径扩展 (GodotPathExtensions)
### `GodotPathExtensions`
提供 Godot 虚拟路径的判断和识别功能。
这组扩展只负责判断 Godot 虚拟路径前缀:
**主要方法:**
- `IsUserPath(this string path)`
- `IsResPath(this string path)`
- `IsGodotPath(this string path)`
- `IsUserPath()` - 判断是否为 `user://` 路径
- `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");
// 创建游戏面板
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;
await this.WaitUntilReadyAsync();
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>();
// 等待场景加载完成
await instance.WaitUntilReadyAsync();
return instance;
}
public async Task TransitionToScene(string scenePath)
{
var newScene = await LoadSceneAsync<Node>(scenePath);
// 清理当前场景
ForEachChild<Node>(child => child.QueueFreeX());
// 加载新场景
await AddChildXAsync(newScene);
// 设置输入处理
newScene.SetInputAsHandled();
}
}
```
### 3. 只在需要时使用 signal fluent API
## 设计原则
`Signal(...)` 属于扩展集合的一部分,但它已经有独立页面。实践上可以这样分工:
### 1. 安全性
- 节点查找、ready 等待、输入处理:`NodeExtensions`
- 动态 signal 绑定:`Signal(...)`
- 框架订阅释放:`UnRegisterWhenNodeExitTree(...)`
- 路径前缀判断:`GodotPathExtensions`
- 所有节点操作都包含有效性检查
- 提供安全的类型转换方法
- 避免空引用异常
## 当前边界
### 2. 便利性
- 当前 `NodeExtensions` 没有 `GetNodeX()``CreateSignalBuilder()` 之类旧文档里提过的 API
- 它不是 router、scene factory、UI factory 或生成器的替代层
- `GetOrCreateNode<T>()` 只会创建一个直接子节点,不会递归补整条路径
- `SafeCallDeferred(...)` 只有在 `IsValidNode()``true` 时才会调用;节点未入树时不会执行
- `UnRegisterWhenNodeExitTree(...)` 只针对实现了 `IUnRegister` 的框架订阅句柄,不会自动处理 Godot 原生 `Connect(...)`
- 协程辅助扩展在 `GFramework.Godot.Coroutine` 命名空间,不属于这组 `Extensions` 页面要覆盖的核心范围
- 流畅的 API 设计
- 支持链式调用
- 减少样板代码
## 继续阅读
### 3. 一致性
- 统一的命名约定
- 一致的返回类型
- 预测性方法行为
### 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)

View File

@ -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");
// 开始按钮 - 一次性连接并立即禁用
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)));
var applyButton = GetNode<Button>("%ApplyButton");
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)