总览
Chooser 表里的列不是装饰。运行时会从全部未禁用行开始,把候选索引放进 FChooserIndexArray,然后每个过滤列拿输入索引、输出新索引。最后剩下的行按成本或随机权重处理,命中后再执行输出列。
列的三种职责
FChooserColumnBase 里有三类关键函数:
| 函数 | 含义 |
|---|---|
Filter | 根据上下文筛选候选行 |
HasCosts | 这一列会给行加成本或排序依据 |
SetOutputs | 命中某行后,把该行输出写回 Context |
所以一张表里可以同时有条件列、评分列、输出列。
常用过滤列
| 列 | 适合 |
|---|---|
| Bool | 是否在空中、是否锁定目标、是否可连段 |
| Enum / Enum(Or) | 姿态、武器类型、AI 警戒等级 |
| Name | 轻量名称匹配,如 SlotName、SectionName |
| Object | 当前武器对象、目标对象是否等于某对象 |
| Object Class | 目标是否是某类敌人、武器是否继承某类 |
| Float Range | 距离、速度、角度、资源值范围 |
| Gameplay Tag | 输入标签、武器标签、技能标签 |
| Gameplay Tag Query | “包含 A 且不包含 B”这类组合条件 |
技能连招里最常用的是 GameplayTag、GameplayTagQuery、FloatRange 和 Bool。
Float Range vs Float Difference
Float Range 是硬过滤:距离不在范围内就淘汰。比如 0-300 才能近战。
Float Difference 是评分:多个候选都能用时,谁的目标值更接近输入值,谁成本更低。比如按角色速度选择 Start/Stop/Turn 动画,Speed=430 时更接近 Jog 而不是 Walk。
Randomize 列
Randomize 列会在剩余候选里按权重随机。它不是先随机再检查条件,而是在过滤之后工作,所以很适合:
- 同一段轻攻击有 3 个等价表现变体。
- AI 近战命中后随机选吼叫、挥砍、后跳。
- 命中特效或音效做轻微变化。
如果绑定了 RandomizationContext,还可以降低重复选中同一项的概率。动作游戏里这能避免连续播放同一个受击动画。
Output 列
Output 列命中后写值,不一定改变 Result。常见输出:
| 输出 | 用途 |
|---|---|
| Output Name | Montage Section、SocketName |
| Output Float | 消耗倍率、播放速度、镜头强度 |
| Output Bool | 是否需要 Root Motion、是否允许取消 |
| Output GameplayTag | Camera Cue、SFX Cue、Combo Step Tag |
| Output Object | 附带相机 Rig、特效资产、子 Chooser |
| Output Struct | 一次写一组复杂参数 |
推荐把技能表现参数整理到 FComboChooserOutput,用 Output Struct 写入。这样行越多,表越整齐。
列顺序建议
从便宜、稳定、强约束到复杂、表现型:
InputTag -> WeaponTag -> PreviousSkillTag -> 状态 Bool -> TagQuery -> FloatRange -> Scoring/Randomize -> Output
虽然运行时都会逐列执行,但这个顺序更符合读表习惯。把 Output 放后面也符合“先选中,再写出”的心智模型。
使用案例:轻攻击连段列设计
| 列 | 绑定 | 目的 |
|---|---|---|
| GameplayTag | InputTag | 只响应 Light |
| GameplayTag | WeaponTag | 区分 Sword/Spear/Dagger |
| GameplayTag | PreviousSkillTag | 决定第几段 |
| GameplayTagQuery | PlayerTags | 排除 Stunned、Silenced、Rooted |
| FloatRange | TargetDistance | 近战、冲刺、远距派生 |
| Bool | bIsInAir | 地面/空中分支 |
| Randomize | RandomContext | 同条件变体 |
| OutputStruct | ComboOutput | 写 Section、Cost、Cue |
架构分析
列设计是把复杂判断拆成可读步骤。每一列都应该回答一个明确问题:输入对吗?武器对吗?上一段对吗?距离合适吗?状态允许吗?如果一列需要解释半天,说明 Context 或表拆分可能有问题。
常见坑
- 第一列就用复杂 TagQuery,后面读表的人不知道基础输入是什么。
MatchAny用太多,导致表看起来命中但规则实际很松。- 同一张表混用技能、动画、音效三类选择,输出列爆炸。
- Randomize 权重默认都一样,却以为某行会更容易中。
- Output 写了参数,但执行技能时没读取这些输出。
源码依据
FChooserColumnBase 定义 Filter、HasCosts、HasOutputs、SetOutputs。UChooserTable::EvaluateChooser 先构造所有候选行,再按列过滤,若候选有成本则排序,命中行后先执行所有列的 SetOutputs,再调用 Result 的 ChooseMulti。Fallback 命中时也会触发输出列的 fallback/default 输出。
源码路径索引
Engine/Plugins/Chooser/Source/Chooser/Public/IChooserColumn.hEngine/Plugins/Chooser/Source/Chooser/Private/Chooser.cppEngine/Plugins/Chooser/Source/Chooser/Internal/BoolColumn.hEngine/Plugins/Chooser/Source/Chooser/Internal/FloatRangeColumn.hEngine/Plugins/Chooser/Source/Chooser/Internal/FloatDistanceColumn.hEngine/Plugins/Chooser/Source/Chooser/Internal/GameplayTagColumn.hEngine/Plugins/Chooser/Source/Chooser/Internal/GameplayTagQueryColumn.hEngine/Plugins/Chooser/Source/Chooser/Internal/RandomizeColumn.hEngine/Plugins/Chooser/Source/Chooser/Internal/OutputStructColumn.h