端口与数据流
端口承载两件事:执行顺序和值。本页说明值的这一侧:目标来源、集合、动作结果、变量,以及值的读取时机。
当你的图不再是一条直线时读这一页。理解这些规则时,优先使用玩法概念,而不是 C# 类型。图里的每根线不是控制线就是数据线。控制线在编辑器工作流里讲过;这一页讲数据这一侧。用玩法概念思考:单位、卡牌、状态、数字、是/否、文本、集合。

Rapid Fire 是个紧凑的数据流示例:All Enemies 产出一个单位集合,Random Elements 把它收窄到一个单位,这个单位在每次循环迭代时流进 Deal Damage.Target。目标每次迭代重新拉取,所以四次攻击可能落在不同的敌人身上。
控制端口
控制端口决定接下来运行什么。
| 端口形状 | 含义 |
|---|---|
| 单个输出 | 继续到下一个节点 |
True / False | 按是/否条件分支 |
Body / Done | 每次迭代运行 Body,循环结束后从 Done 继续。Loop 和 Foreach 还输出 Index 数据端口 |
OnChosen / Skipped | 玩家做出选择后继续,或玩家放弃后继续 |
| 编号或命名输出 | Switch、Sequence 或 Intent 模式里每个分支一条路径 |
每张图至少需要一个起点:入口、Hook 或 Intent 模式。没有控制路径的一堆数据节点永远不会运行。
数据端口
| 值类型 | 常见来源 | 常见消费者 |
|---|---|---|
| 单位 | Self Unit、Opponent Unit、入口 Source / Target、Pick Element | Deal Damage、Change HP、Unit Info、Flash Unit |
| 单位集合 | All Enemies、All Units、Filter、Random Elements | Deal Damage、Foreach、Count |
| 卡牌 | This Card、卡牌入口、Pick Element | Card Info、Play Card、Transform Card |
| 卡牌集合 | Card Piles、Deck Cards、Filter、Take | Move Cards、Upgrade Cards、Remove Cards |
| 状态 | 状态入口、Unit Info.Statuses 加 Pick Element | Reapply Status、Remove Statuses |
| 数字 | 入口 Amount、Player Energy、Unit Info、数学节点 | Action 的数值、Loop.Count、条件、Write Hook |
| 是/否 | Condition、Logic、Is Empty、Hook 的 Playable | Branch、While、Gate.Allow、Write Hook |
| 文本 | Card Info.Type、Unit Info.Team、Constant、Event Arg | Condition 比较、Switch 分支、过滤器 |
连线有类型检查:数字输出接数字输入,整数可以进小数槽,单个单位可以进"单位或单位集合"端口。把集合接进单目标端口也可以运行,但系统会取第一个元素;如果这是设计意图,建议显式使用 Pick Element,让图的含义更清楚。
Rapid Fire 的连线一次用上了表里的好几行:All Enemies 传给 Random Elements,选出的单位驱动 Deal Damage.Target,Dealt 结果再接进表现分组。

字段值与连接值
很多节点的可编辑字段也能变成输入端口。端口默认是藏起来的:点字段行末的 ○ 插槽按钮把它暴露出来,点 ◉ 折回内联值(操作细节见编辑器工作流)。运行时的规则很简单:端口接了线就以连接值为准,字段是没有连线时的 fallback。
| 情况 | 更好的选择 |
|---|---|
| 这张卡总是造成 8 点伤害 | 在 Deal Damage 上设 Amount = 8 |
| 伤害等于当前护甲 | 把 Unit Info.Armor 连进 Deal Damage.Amount |
| 这个状态总是施加 2 层 | 在 Change Status 上设 Amount = 2 |
| 层数随本回合打出的牌数变化 | 把 Count 连进 Change Status.Amount |
为什么连了线字段仍然有意义
节点输入按"先拉取输入,拉不到就回退到字段"解析。之后删掉连线,或把端口折回内联值时,节点会回退到字段值。因此,即使节点已连线,也应把字段设成合理的默认值:它记录了设计基线,也让图在重新连线的过程中保持可用。
值在什么时候被读取
数据线不会主动推送值。值在消费节点运行并请求输入时才被计算:
这会带来三个实际影响:
- Get 节点在被拉取的那一刻读取实时战斗状态。
Deal Damage之后再读Unit Info.HP,看到的是扣完之后的 HP,不是图启动时的值。 - 在同一个节点的一次运行内,对同一个 Get 节点的多次拉取结果一致。
Random也遵守这条:一个动作拉两次Random只看到一次掷骰,但控制路径上的下一个节点再拉就是新的一骰。两个动作连着同一个Random,就是两次掷骰。 - 想锁定一个算好的值给多个节点复用,用
Set Variable存下来,或者把第一个动作的结果输出接下去,避免重复读取来源。
Operator 节点(Math Binary、Condition、Filter……)是对输入的纯计算,每次被拉取都重新算。
目标来源
会作用到单位或卡牌的 Action 节点都带一个来源字段,它回答的是:没有数据线覆盖时,这个节点该对哪些目标生效?
| 来源字段 | 选项 | 含义 |
|---|---|---|
TargetSource(单位) | Self、Opponent、AllEnemies、AllUnits | 按与图拥有者的关系查找单位 |
CardsSource(卡牌) | ThisCard、HandPile、DrawPile、DiscardPile、ExhaustPile、AllPiles | 读取当前卡牌或某个已知牌堆 |
Pile(牌堆范围) | Hand、Draw、Discard、Exhaust、Master、All | 卡牌动作在哪个牌堆放置、移动或检查卡牌 |
这些枚举字段不需要连线就能覆盖常见情形:TargetSource = AllEnemies 本身就是完整的 AoE 目标。来源需要过滤、排序或随机时才用选择器节点。All Enemies -> Filter -> Random Elements 表示"一个符合条件的随机敌人",单个字段表达不了这个。
集合
集合是一组单位、卡牌或状态,集合节点在动作运行前构建目标集。
| 需求 | 节点 |
|---|---|
| 所有敌人 | All Enemies |
| 所有单位 | All Units |
| 某个牌堆的卡 | Card Piles |
| 本局牌组的卡 | Deck Cards |
| 只保留匹配项 | Filter |
| 统计数量 | Count |
| 判空或判包含 | Is Empty、Contains |
| 取一个 | Pick Element |
| 随机取几个 | Random Elements |
| 合并或做差 | Merge Collections、Exclude |
| 重排或截断 | Sort、Shuffle、Take |
接受单位集合的动作会作用到集合里的每个单位。当每个目标需要各自的后续逻辑或各自的表现节奏时,让集合经过 Foreach,不要直接整体传入。
动作结果
有些动作会输出实际结果,这和请求的数值可能不同:护甲、Hook 和上限都会参与结算。
| 节点 | 输出 | 为什么重要 |
|---|---|---|
| Deal Damage | Dealt | 经过护甲、Hook 和阻止之后的伤害 |
| Change HP | Changed | 实际变化的 HP,已按上限和归零截断 |
| Change Armor | Changed | 实际变化的护甲 |
| Change Energy | Changed | 实际变化的能量 |
| Change Status | Changed | 真实的层数增减 |
| Draw Cards | Drawn | 实际进入手牌的卡 |
| Add Cards | Added | 被创建出来的卡牌实例 |
| Spawn Unit | Spawned | 新单位(如果创建成功) |
面向玩家的反馈通常应该使用这些输出。浮动文字连接 Dealt,显示的才是穿过护甲后的 4 点,而不是卡牌原本请求的 7 点。
变量
Set Variable 存一个命名值,Variable 把它读回来。节点上的 BattleScope 开关决定寿命:关闭时值只活在这一次图执行里,打开后整场战斗有效,任何图都能读。
| 作用域 | 用于 | 避免用于 |
|---|---|---|
| 图执行(默认) | 一次执行内复用的中间值 | 一根线就能传的值 |
| 战斗作用域 | 跨回合、跨入口持续的计数和标记 | 临时计算 |
变量按玩法含义命名:BonusDamage、CardsCreated、MarkedTarget。不要把战斗作用域变量命名为 Temp 这类含义模糊的名字,否则后续很难判断它的用途和生命周期。
事件参数
Raise Internal Event 发送命名参数;接收方在 On Internal Event 之后用 Event Arg 节点逐个读取。
| 发送方绑定 | 接收方读取方式 |
|---|---|
| 攻击单位 | 同名参数的 Event Arg |
| 伤害数值 | Event Arg,再接数学或分支节点 |
| 新创建的卡 | Event Arg,再接卡牌节点 |
参数名保持稳定、具体。Attacker 和 DamageAmount 比 A、Payload 更容易在重构后继续维护。
自定义节点端口
自定义节点不引用任何编辑器代码就能获得端口。简版对照:
| 你写的 | 用户看到的 |
|---|---|
public 的 int / float / bool / string 字段 | 一个带 ○ 插槽开关的字段,可随时暴露成同名输入端口 |
名为 <Name>Source 的 UnitSource 或 CardSource 字段 | 一个目标字段,端口名叫 <Name>:TargetSource 对应 Target 端口 |
INodePorts.DeclarePorts() | 显式命名的输入、输出、控制分支和下拉端口。名字撞车时,显式声明的端口压过自动提升的 |
[FlowNode(category, displayName)] | Add Node 里的菜单组和节点标题 |
端口按玩法选择命名(Target、Amount、Duration),不要暴露实现。完整的创作契约见自定义节点和自定义端口。
数据流排障
| 症状 | 可能原因 | 检查 |
|---|---|---|
| 节点跑了但什么都没变 | 目标为空或选错 | 来源字段、选择器输出、入口输出 |
| 分支永远走同一边 | 条件输入没接 | Branch.Condition 和 Condition 节点的输入 |
| 浮动文字显示错误数字 | 显示的是预期值而非结果 | 改连动作的 Dealt 或 Changed 输出 |
两个动作连同一个 Random 却拿到不同数字 | 每个动作拉取时都重新掷骰 | 用 Set Variable 存下骰点,或复用第一个动作的结果输出 |
| 集合动作命中过多目标 | 选择器范围太宽 | 加 Filter、Take 或 Pick Element |
| Hook 从不改变值 | 缺 Write Hook 或 Kind 错误 | 数值 Hook 用 Kind = Int,可打出性用 Kind = Bool |