mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
docs(godot): 收口 signal 与 extensions 文档
- 更新 godot signal 页面,明确 Signal(...)、SignalBuilder 与 [BindNodeSignal] 的分工 - 更新 godot extensions 页面,收敛到当前存在的扩展成员与生命周期边界 - 补充 documentation-governance-and-refresh 跟踪与 trace,记录 RP-016 和验证结果
This commit is contained in:
parent
03ecbe5989
commit
3ba1e3f202
@ -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 是否按预期收敛
|
||||
|
||||
@ -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 是否关闭或减少
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user