NodeScript+ 导入了个 UI Extend

Signed-off-by: TRADER_FOER <lhf190@outlook.com>
This commit is contained in:
2026-05-23 21:05:16 +08:00
parent 7ea1f1d8c3
commit 51878f15ae
531 changed files with 198095 additions and 144473 deletions

View File

@@ -0,0 +1,455 @@
# NodeScript 重构大纲
## 一、重构目标
将节点系统从**推送式push-based求值**改为**Manager 集中控制的拉取式pull-based生命周期循环**。不改 UI 层,聚焦于 NodeManager 对节点的调度逻辑和节点自身的行为表现。
---
## 二、核心架构变更
### 2.1 节点生命周期状态
`NodeBase` 新增状态枚举:
```csharp
enum NodeStatus { Ready, Hang, Complete }
```
- **Ready** — 节点等待被触发执行
- **Hang** — 节点因前置节点未完成而挂起,保留在运行时表中等待下一周期
- **Complete** — 节点已完成本轮运算
变量节点的特殊规则:不存在 Hang 状态,启动后立即返回值/引用并 Complete。
### 2.2 触发表 + 运行时表Manager 侧)
Manager 维护两张表:
| 表 | 作用 |
|---|---|
| **触发表** `triggerTable` | 收集本周期要加入运行的节点,周期开始前并入运行时表 |
| **运行时表** `runtimeTable` | 当前周期正在遍历的节点集合 |
周期流程:
1. `triggerTable` → 并入 `runtimeTable`,清空 `triggerTable`
2. 遍历 `runtimeTable`,调用每个节点的 `Loop()` 方法
3. `Loop()` 返回:下一轮要触发的节点(加入 `triggerTable`+ 是否从 `runtimeTable` 移除自己
### 2.3 拉取式取值
节点不再通过 Output → Input 推送数据。改为:
- 每个周期节点从前置节点的 Output 中**主动拉取**值或引用
- 如果前置节点不处于 `Complete`,本节点 `Status = Hang`,将前置节点加入 `triggerTable`,自己保留在 `runtimeTable`
- 取值条件满足后,执行运算,返回 `Complete`
### 2.4 最短距离 L单向逻辑保证
每个节点计算到 Start 节点的最短距离 `L`,用于约束连线方向:
- L=0 的节点的输出可以连接到 L=7 的节点的输入
- L=7 的节点的输出**不能**连接到 L=2 的节点的输入
- 确保逻辑单向流动
---
## 三、文件级重构计划
### 3.1 NodeCore.cs — 核心类型重定义
| 变更项 | 说明 |
|---|---|
| 移除 `Input<T>.Notify()` / `Output<T>.SetValue()` 推送链 | 不再需要推送机制 |
| `Input<T>` 改为存储对上游 `Output<T>` 的引用,通过 `Pull()` 取值 | 拉取模式 |
| `NodeBase` 新增 `NodeStatus Status` 属性 | 生命周期状态 |
| `NodeBase` 新增抽象方法 `Loop()` | 替代 `Evaluate()`,返回 `(List<NodeBase> triggers, bool removeFromRuntime)` |
| `NodeBase` 新增 `int L` 属性 | 到 Start 节点的最短距离 |
| `NodeBase` 新增 `List<NodeBase> GetPrecedingNodes()` | 获取所有前置依赖节点 |
| 保留 `IInput` / `IOutput` 接口 | UI 层依赖不变 |
| 保留 `Signal` 结构体 | 仅用于触发操作 |
| 新增 `InputAny` 概念(见 3.4 | 动态类型支持 |
### 3.2 NodeManager.cs — 生命周期调度
| 变更项 | 说明 |
|---|---|
| 新增 `HashSet<NodeBase> triggerTable` | 触发表 |
| 新增 `HashSet<NodeBase> runtimeTable` | 运行时表 |
| 新增 `void RunCycle()` | 单周期执行逻辑 |
| 重写 `RunGraph()` | 初始化触发表为 Start/Entry 节点,循环调用 `RunCycle()` 直到运行时表为空 |
| 新增 `void ComputeLValues()` | 在连线变更后重新计算所有节点的最短距离 L |
| 新增 `bool ValidateConnection(NodeBase from, NodeBase to)` | 连线前验证 L 约束 |
| 新增子控制器管理 | 用于循环节点和子函数节点的内部运行监控 |
| 新增 `void RegisterSubFunction(NodeBase definition)` | 扫描注册子函数定义 |
| `TryConnect()` 中增加 L 约束检查 | 阻止反向连线 |
| `SaveToFile()` / `LoadFromFile()` 适配新状态 | 保存/加载兼容 |
| UI 部分(拖线、复制粘贴等)保持不变 | — |
### 3.3 NodeObject.cs / ConnectorSlot.cs / NodeUIBuilder.cs — UI 层
**原则上不修改**,仅可能的微调:
- `NodeObject.Init()` 中调用 `nodeBase.InitConnectors()` 后触发 L 值计算
- 循环/子函数节点的 Rect 容器支持(见 3.6
### 3.4 动态类型 — InputAny / OutputAny 机制(多类型统一节点的基石)
#### 3.4.1 问题
当前同一功能、不同类型的节点大量重复:
| 功能 | 现有节点 | 覆盖类型 |
|---|---|---|
| 常量 | `NodeConstFloat`, `NodeConstVector2`, `NodeConstVector3`, `NodeConstColor` | float, Vector2, Vector3, Color |
| 拆分 | `NodeSplitV2`, `NodeSplitV3` | Vector2, Vector3 |
| 合并 | `NodeCombineV2`, `NodeCombineV3` | Vector2, Vector3 |
| 数学 | `NodeMath`(仅 float | float |
这些节点的**逻辑完全一致**,仅类型不同。引入 `InputAny` / `OutputAny` 后,一个节点覆盖所有类型。
#### 3.4.2 类型列表
```csharp
// 系统支持的全部可连线类型
static readonly HashSet<Type> SupportedTypes = new()
{
typeof(float), typeof(int), typeof(bool), typeof(string),
typeof(Vector2), typeof(Vector3), typeof(Color),
typeof(GameElement), typeof(List<GameElement>),
typeof(Signal),
};
```
#### 3.4.3 InputAny / OutputAny 设计
```csharp
/// <summary>未锁定的输入端口,连线后类型自动锁定</summary>
public class InputAny : IInput
{
object _sourceOutput; // 连到的 Output可能是 Output<T> 或 OutputAny
public string Name { get; set; }
public Type DataType { get; private set; } // null 表示未锁定,连线后锁定
public bool IsConnected => _sourceOutput != null;
public bool HasReceived { get; set; }
public Color ConnectorColor => DataType != null ? NodeColors.Get(DataType) : Color.grey;
/// <summary>直接取已连接的上游值(泛型方式)</summary>
public T GetValue<T>() { ... }
/// <summary>取值为 object</summary>
public object GetValue() { ... }
/// <summary>连线时由 Manager 调用,锁定端口类型</summary>
internal void LockType(Type t) { DataType = t; }
}
/// <summary>未锁定的输出端口,类型由同节点的 InputAny 传播决定</summary>
public class OutputAny : IOutput
{
object _value;
public string Name { get; set; }
public Type DataType { get; private set; } // null 表示未锁定
public bool IsConnected => _targets.Count > 0;
public Color ConnectorColor => DataType != null ? NodeColors.Get(DataType) : Color.grey;
public T GetValue<T>() { ... }
public void SetValue<T>(T v) { _value = v; }
/// <summary>由节点的某个 InputAny 锁定后传播过来</summary>
internal void LockType(Type t) { DataType = t; }
}
```
#### 3.4.4 类型传播规则
一个节点上存在多个 `InputAny` / `OutputAny` 时,类型按以下优先级传播:
```
规则 1外部优先: 任一 InputAny 被连线 → 锁定该端口类型 → 传播到同节点所有未锁定的 InputAny / OutputAny
规则 2冲突检测: 两个已连线的 InputAny 类型不一致 → Manager 阻止连线,报 "Type mismatch"
规则 3OutputAny: 总是跟随同节点的首个已锁定 InputAny 的类型
规则 4无输入节点: 常量类节点无 InputAnyOutputAny 类型由节点内置字段/UI 选择决定
```
#### 3.4.5 ConnectorSlot UI 适配
- 未锁定的 `InputAny` / `OutputAny` 连接点显示**灰色**
- 连线锁定后,动态更新连接点的颜色以匹配锁定类型
- `ConnectorSlot` 需要监听 `DataType` 变化并刷新颜色
- Manager 在 `TryConnect` 成功后调用 `Slot.RefreshAppearance()`
---
### 3.5 多类型统一节点设计
以下节点取代现有的同功能多类型节点。
#### 3.5.1 NodeConst — 通用常量(取代 NodeConstFloat/V2/V3/Color
```
NodeConst:
[UI] Dropdown: Type (float / int / bool / Vector2 / Vector3 / Color)
[UI] 根据所选类型动态显示对应输入控件
OutputAny value ← 类型由 UI 选择锁定
```
逻辑无输入Loop 中直接 Complete。输出值被下游拉取。
#### 3.5.2 NodeMath — 通用数学运算(扩展覆盖类型)
```
NodeMath:
[UI] Dropdown: Op (Add / Sub / Mul / Div)
InputAny a ← 连线后锁定类型
InputAny b ← 跟随 a 的类型(或相反,谁先连跟谁)
OutputAny result ← 类型来源同 InputAny 的锁定类型
```
支持的运算映射LUT 注册):
| 类型 | Add | Sub | Mul | Div |
|---|---|---|---|---|
| float, int | `+` | `-` | `*` | `/` |
| Vector2, Vector3 | `+` | `-` | `* float` | `/ float` |
| string | 拼接 | — | — | — |
| Color | `+` (叠加) | `-` | `* float` | — |
> 实现:用 `Dictionary<(Type, Op), Func<object, object, object>>` 查表分派。
#### 3.5.3 NodeSplit — 通用拆分(取代 NodeSplitV2/V3
```
NodeSplit:
InputAny input ← Vector2 → 输出 X(float), Y(float)
← Vector3 → 输出 X(float), Y(float), Z(float)
OutputAny x, y, z ← z 仅在 Vector3 时激活
```
- 默认所有 OutputAny 端口可见但灰掉,`input` 锁定类型后按需激活
- 对于 Color输出 R, G, B, A (float)
#### 3.5.4 NodeCombine — 通用合并(取代 NodeCombineV2/V3
```
NodeCombine:
[UI] Dropdown: Type (Vector2 / Vector3 / Color)
InputAny x, y, z, w ← 数量按类型动态显示
OutputAny output ← 类型跟随 UI 选择
```
#### 3.5.5 NodeLerp — 线性插值(新节点)
```
NodeLerp:
InputAny a, b ← 连线锁定类型
InputAny t ← 预期 float不受 a/b 锁定影响,标记为 fixed-type
OutputAny result ← 跟随 a/b 类型
```
支持 float, int, Vector2, Vector3, Color。
#### 3.5.6 NodeCompare — 比较运算(新节点)
```
NodeCompare:
[UI] Dropdown: Op (==, !=, >, <, >=, <=)
InputAny a, b ← 连线锁定类型(支持 float, int
OutputAny result ← 固定 bool
```
> 注意:> \ < 仅在数值类型有效,== / != 可扩展至 string。
#### 3.5.7 NodeSelect — 二选一(新节点)
```
NodeSelect:
InputAny condition ← 如果未连线,用 [UI] Toggle(bool);如果连线则类型锁定 bool
InputAny trueValue, falseValue ← 类型互相跟随
OutputAny result ← 跟随 trueValue/falseValue 类型
```
#### 3.5.8 NodeSet — 万能赋值(新节点,引用语义)
```
NodeSet:
InputAny targetRef ← 连接到变量节点的 get 输出(锁定为目标类型)
InputAny value ← 跟随 targetRef 类型
// 无 OutputAny纯副作用节点
```
> 关键targetRef 不仅是取值,还要修改其引用的 Variable 内部值。需要 InputAny 能够"反向写入"。
#### 3.5.9 节点对比总结
| 统一节点 | 取代旧节点 | 覆盖类型数 |
|---|---|---|
| `NodeConst` | `NodeConstFloat`, `NodeConstVector2`, `NodeConstVector3`, `NodeConstColor` | 6+ |
| `NodeMath` | `NodeMath`(扩展) | 5 (float, int, Vector2, Vector3, Color) |
| `NodeSplit` | `NodeSplitV2`, `NodeSplitV3` | 3 (Vector2, Vector3, Color) |
| `NodeCombine` | `NodeCombineV2`, `NodeCombineV3` | 3 (Vector2, Vector3, Color) |
| `NodeLerp` | (新) | 5 |
| `NodeCompare` | (新) | 3 |
| `NodeSelect` | (新) | 任意 |
| `NodeSet` | (新) | 任意 |
---
### 3.6 InputAny 类型锁定流程Manager 侧)
```
TryConnect(OutputAny/Output<T> src, InputAny dst):
1. 获取 src 的实际 DataType → T
2. 如果 dst.DataType == null → dst.LockType(T) → 传播到同节点其他端口
3. 如果 dst.DataType == T → OK
4. 如果 dst.DataType != T → 拒绝,类型不匹配
5. 调用 dst.ownerNode.OnTypePropagated() 通知节点刷新 UI
```
传播方法(在 `NodeBase` 上):
```csharp
/// <summary>当某个 InputAny 或 OutputAny 锁定了类型后,通知节点刷新其他端口</summary>
public virtual void OnTypePropagated(ConnectorSlot lockedSlot, Type lockedType)
{
// 默认遍历所有未锁定端口LockType(lockedType)
foreach (var slot in GetAnySlots())
if (slot.DataType == null)
slot.LockType(lockedType);
}
```
#### 3.6.1 fixed-type 端口标记
某些 InputAny 不接受类型传播(如 `NodeLerp.t` 必须是 float。新增标记
```csharp
public class InputAny : IInput
{
public bool IsFixedType { get; init; } // true = 不被传播覆盖,始终保持初始类型
}
```
`NodeLerp.t``new InputAny { IsFixedType = true, Name = "t" }`(已指定 float 意图时 LockType(float)
---
### 3.7 特殊节点:变量节点
```csharp
class NodeVariable<T> : NodeBase
{
Input<Signal> signal; // 通常不连,仅在循环内使用
Input<T> set;
Output<T> get;
}
```
- `Loop()` 立即返回当前值,不存在 Hang 状态
- 有 Signal 输入时(循环体内),等待 Signal 触发才更新
- 变量节点不参与 InputAny 类型传播——类型由泛型参数 T 固定
### 3.8 循环节点 & 子函数节点(新 UIRect 容器)
**UI 变更NodeUIBuilder 扩展):**
- 新增一个可手动拖拽缩放大小的 Rect 区域
- 将其他节点拖入此 Rect 即表示"在节点内部"
- 循环节点的 Index 和 Signal 输出点移到第一列(输入列),可拉线到 Rect 内部
**节点结构:**
```
NodeForLoop:
外部输入: exec(Signal), count(InputAny) ← count 支持 int/float
外部输出: completed(Signal)
内部 Rect 中:
子节点...(由主 Manager 调度,子控制器监控)
Index 输出(int) — 在第一列,拉入 Rect 内
LoopBody 输出(Signal) — 在第一列,拉入 Rect 内
```
**子控制器职责:**
- 检查循环体内所有节点是否 Complete
- 重置内部节点,开启下一步循环
- 向主 Manager 报告循环是否全部完成
**子函数定义节点:**
```
NodeSubFunctionDef:
输入: name(string)
Rect 外→Rect 内的变量节点 → 代表函数输入参数(变量类型即参数类型)
Rect 内的 set 节点 → Rect 外 → 代表函数输出
```
**子函数执行节点(配合 InputAny**
```
NodeSubFunctionCall:
— 根据已注册的子函数定义,动态生成 InputAny/OutputAny 端口
— 端口类型由子函数定义中变量节点的泛型参数决定
— Manager 扫描所有文件注册子函数定义
```
### 3.9 现有节点迁移计划
| 旧节点 | 处理方式 | 说明 |
|---|---|---|
| `NodeStart` | 迁移:`Evaluate()``Loop()` | 启动后 Complete |
| `NodeEntry` | 同上 | — |
| `NodeMath` | **扩展为多类型版本**3.5.2 | 旧版删除 |
| `NodeConstFloat/V2/V3/Color` | **合并为 `NodeConst`**3.5.1 | 四个节点合一,旧版全部删除 |
| `NodeSplitV2/V3` | **合并为 `NodeSplit`**3.5.3 | 两个节点合一 |
| `NodeCombineV2/V3` | **合并为 `NodeCombine`**3.5.4 | 两个节点合一 |
| `NodeForLoop` | 大改Rect 容器 + 子控制器 | count 改用 InputAny |
| `NodeForEach<T>` | 类似 ForLoop 改造 | — |
| `NodeBranch` | 迁移到 Loop()condition 改用 InputAny | 支持 float/int 条件 |
| `NodeGameElement` / `NodeSetTransform` / `NodeClone` 等 | 生命周期适配Signal 等待逻辑不变 | — |
| `NodeVariable<T>` | 特殊处理:立即 Complete不经过 Hang | 保持泛型不变 |
| `NodeDebugLog` / `NodeLog` | 迁移InputAny 支持任意显示类型 | — |
| `NodeList<T>` / `NodeListAdd<T>` / `NodeListGet<T>` | 保持泛型Loop() 适配 | — |
| `NodePositionStepper` | 迁移,参数改用 InputAny | — |
| **新增** `NodeLerp` | 全新 | — |
| **新增** `NodeCompare` | 全新 | — |
| **新增** `NodeSelect` | 全新 | — |
| **新增** `NodeSet` | 全新(需要反向写入能力) | — |
---
## 四、实施步骤建议
1. **Phase 1: 核心类型层** — 修改 `NodeCore.cs`
- 新增 `NodeStatus` 枚举
- `NodeBase` 新增 `Loop()``L``Status`
- `Input<T>` / `Output<T>` 改为拉取模式
- 实现 `InputAny``OutputAny``OnTypePropagated``IsFixedType`
2. **Phase 2: Manager 调度层** — 修改 `NodeManager.cs`
- 实现触发表/运行时表
- 实现 `RunCycle()` 循环调度
- 实现 L 值计算 + 连线验证
- 实现 `TryConnect` 中的 InputAny 类型锁定传播流程
3. **Phase 3: 统一节点实现** — 删除旧重复节点,实现新版
- 先实现 `NodeConst``NodeMath`(验证 InputAny 机制可用)
- 再实现 `NodeSplit``NodeCombine``NodeLerp`
- `NodeCompare``NodeSelect``NodeSet`(反向写入)
- 迁移保留的节点(`NodeStart``NodeBranch``NodeVariable<T>` 等)
4. **Phase 4: 循环/子函数** — Rect 容器 + 子控制器
- `NodeUIBuilder` 增加 Rect 容器支持
- 循环节点子控制器实现
- 子函数定义/执行节点 + Manager 注册扫描
5. **Phase 5: 测试 & 清理**
- 验证保存/加载兼容(含新旧类型映射)
- 删除旧版备份文件 `NodeBase.cs.bak`
- 删除旧节点类(`NodeConstFloat` 等)
- 验证所有节点类型覆盖无遗漏
---
## 五、风险点 & 注意事项
- **UI 不变原则**`NodeObject``ConnectorSlot``NodeUIBuilder` 的接口保持稳定,节点层改动不应破坏 UI 渲染ConnectorSlot 仅新增 `RefreshAppearance()`
- **InputAny 类型冲突**:同一节点两个已连线的 InputAny 类型不一致时Manager 拒绝并报错
- **反向写入**`NodeSet` 的 targetRef 需要能修改上游 Variable 内部值,这是 InputAny 设计的关键难点
- **向后兼容**:旧 JSON 中的 `NodeConstFloat` 等类型名加载时需映射到新的 `NodeConst`
- **循环嵌套**:子控制器设计需考虑循环内嵌套循环的递归情况
- **Performance**:大图时每帧遍历运行时表的开销,类型检查用 `Type` 引用比较(非字符串)
- **Signal 类型**:保留但不参与 InputAny 的类型传播