总览
你提到的“支持玩家编辑技能连招”非常适合拿来讲 Chooser,但要先拆清楚:玩家不是直接编辑 Chooser 表,玩家编辑的是自己的连招档案;Chooser 用这份档案加上战斗上下文,选择下一段动作;GAS 最后做权威激活。
目标体验
玩家在菜单里看到技能面板:
轻攻击链:
第 1 段:迅斩
第 2 段:旋斩
第 3 段:破甲斩
空中轻攻击:坠击
闪避后轻攻击:回身斩
玩家可以替换某个槽位,但只能放入已解锁、当前武器支持、槽位类型匹配的技能。进入战斗后,按轻攻击时系统按档案解析下一段;如果目标太远、蓝不够、角色被硬直,不能强行释放。
数据模型
技能定义用 DataAsset:
UCLASS(BlueprintType)
class UComboMoveData : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag MoveTag;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTagContainer SlotTags;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTagContainer RequiredWeaponTags;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TSubclassOf<UGameplayAbility> AbilityClass;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TObjectPtr<UAnimMontage> PreviewMontage;
};
玩家档案只保存轻量标识:
USTRUCT(BlueprintType)
struct FPlayerComboProfile
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TMap<FGameplayTag, FGameplayTag> SlotToMove;
};
比如 ComboSlot.Sword.Light.02 -> Move.Sword.SpinSlash。不要保存直接对象指针,跨版本和存档迁移会麻烦。
UI 怎么列出可选技能
Common UI 面板打开某个槽位时,构造 Context:
EditingSlotTag = ComboSlot.Sword.Light.02
WeaponTag = Weapon.Sword
PlayerTags = 已解锁技能、职业、天赋
InputTag = Input.Attack.Light
调用 Chooser 多结果表 CHT_Combo_EditCandidates,返回所有合法 UComboMoveData。这张表只负责“这个槽位能放什么”。UI 显示 MoveData 的名字、图标、预览动画。玩家选中后,SaveGame 只记录 MoveTag。
战斗时怎么解析
战斗里用另一张表 CHT_Combo_NextMove。Context 包含:
USTRUCT(BlueprintType)
struct FComboRuntimeContext
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite) FGameplayTag InputTag;
UPROPERTY(EditAnywhere, BlueprintReadWrite) FGameplayTag PreviousMoveTag;
UPROPERTY(EditAnywhere, BlueprintReadWrite) FGameplayTag CurrentSlotTag;
UPROPERTY(EditAnywhere, BlueprintReadWrite) FGameplayTag PlayerPreferredMoveTag;
UPROPERTY(EditAnywhere, BlueprintReadWrite) FGameplayTag WeaponTag;
UPROPERTY(EditAnywhere, BlueprintReadWrite) FGameplayTagContainer PlayerTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite) FGameplayTagContainer TargetTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite) float TargetDistance = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite) bool bIsInAir = false;
};
PlayerPreferredMoveTag 来自档案;如果档案里的技能已经被版本移除或未解锁,表可以自然落到 Fallback 或默认技能。
两张表的职责
| 表 | 调用时机 | Result |
|---|---|---|
CHT_Combo_EditCandidates | UI 编辑槽位 | 多个 UComboMoveData |
CHT_Combo_NextMove | 战斗按键 | 一个 UComboMoveData 或 Ability Class |
编辑表偏“可放入什么”,战斗表偏“此刻能释放什么”。它们可以共享部分列设计,但不要硬合成一张巨表。
GAS 验证
Chooser 选中 MoveData 后,不要直接播放动画:
if (!MoveData || !MoveData->AbilityClass)
{
return EComboResolveResult::NoMove;
}
FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromClass(MoveData->AbilityClass);
if (!Spec || !ASC->TryActivateAbility(Spec->Handle))
{
return EComboResolveResult::RejectedByGAS;
}
服务器权威项目里,客户端可以预测选择,但服务器仍要用同样的上下文或可验证输入重新判断。玩家档案必须由服务器认可,不能只信客户端传来的 MoveTag。
输出参数
FComboChooserOutput 可以包含:
| 字段 | 用途 |
|---|---|
MontageSection | Ability 播放哪段 Montage |
NextComboWindow | 下一段输入窗口 |
CostMultiplier | 资源消耗倍率 |
CameraCueTag | 触发 Gameplay Camera 或震屏 |
VfxCueTag | 触发 GameplayCue |
bCanBeCanceled | 是否允许闪避取消 |
这些适合 Output Struct,而不是塞进 Result 名字里。
版本升级和存档修复
玩家档案存的是 Tag,就要准备修复表:
Move.Sword.OldSpin -> Move.Sword.SpinSlash
Move.Sword.RemovedHeavy -> Move.Sword.DefaultHeavy
加载 SaveGame 后先迁移,再用编辑候选 Chooser 校验。如果玩家存档里的技能不再合法,替换成默认技能并给 UI 一个提示。
使用案例:玩家改轻攻击第二段
玩家打开连招编辑页,点选 ComboSlot.Sword.Light.02。UI 调用编辑候选表,传入当前武器、已解锁技能和槽位 Tag,得到 DA_Move_SwordSpin、DA_Move_SwordUppercut、DA_Move_SwordCrossSlash 三个候选。玩家选择旋斩后,SaveGame 只保存 Move.Sword.SpinSlash。
战斗中第一次轻攻击成功后,组件把 PreviousMoveTag 设为 Move.Sword.Light01。第二次轻攻击时,运行时 Context 带着 PlayerPreferredMoveTag=Move.Sword.SpinSlash 进入战斗解析表。表先检查输入、武器、上一段、地面状态和距离,再检查玩家偏好。命中后返回 DA_Move_SwordSpin,OutputStruct 写出 MontageSection=Spin_Entry、CostMultiplier=1.15 和 CameraCueTag=Camera.Combo.Spin。最后 GAS 激活 Ability;如果冷却或资源不足,GAS 拒绝,UI 播失败提示,连招状态回退或保持窗口。
架构分析
这套方案有三层安全边界:Common UI 限制玩家只能选择合法候选;Chooser 在战斗中按上下文解析,处理版本和状态变化;GAS 在执行前做最终权威判断。任何一层失败,系统都应该能回退,而不是崩掉或让玩家释放非法技能。
常见坑
- UI 只按本地数组列技能,不走同一套候选规则,导致菜单能选、战斗不能出。
- SaveGame 直接存资产路径,重命名或 DLC 移除后全坏。
- 玩家配置影响了 Ability Class,但服务器没有重新验证。
- Chooser 表里把冷却/资源当最终权威,绕过 GAS。
- 连招状态只在客户端更新,服务器和动画表现不同步。
源码依据
EvaluateChooserMulti 和 ChooseMulti 支持收集多个候选,适合编辑 UI 列表。FChooserEvaluationContext 支持结构体输入和输出;OutputStruct 列可以把每行参数写回。UChooserTable::EvaluateChooser 在没有行成功时会走 FallbackResult,适合处理玩家档案过期或上下文异常。
源码路径索引
Engine/Plugins/Chooser/Source/Chooser/Public/Chooser.hEngine/Plugins/Chooser/Source/Chooser/Public/ChooserFunctionLibrary.hEngine/Plugins/Chooser/Source/Chooser/Internal/GameplayTagColumn.hEngine/Plugins/Chooser/Source/Chooser/Internal/GameplayTagQueryColumn.hEngine/Plugins/Chooser/Source/Chooser/Internal/OutputStructColumn.hEngine/Plugins/Chooser/Source/Chooser/Private/Chooser.cpp