04-一次Ability的完整执行链

5872 字
29 分钟
04-一次Ability的完整执行链

第 4 章:一次 Ability 的完整执行链#

问题导入#

玩家按下火球术快捷键,客户端立刻播了抬手动画,火球看着已经释放出来了——但服务器那边,可能发现施法者没有这个技能,可能 Mana 不够,可能冷却尚未结束,也可能客户端锁定的目标早已死亡。就算请求全部合法,后面还有投射物飞行、命中结算、持续灼烧、中途被打断和生命周期清理。

如果把这些行为都写进一个 CastFireball(),代码表面上很直接,实际却混合了不同责任:

  • 输入层负责表达玩家意图;
  • Ability 负责组织执行流程;
  • 规则层负责检查、扣费和结算;
  • 网络层负责请求、确认、拒绝与校正;
  • 表现层负责动画、特效、音效和界面反馈。

单机原型可以依赖调用顺序维持这些责任,但多人游戏却必须让每一步都可识别、可拒绝、可重放、可清理。

分阶段不是为了增加仪式,而是为了给权威判定、失败处理和异步生命周期建立明确边界。

4.1 为什么执行链必须分阶段#

一次网络化 Ability 是一段跨帧、跨对象、跨机器的流程,至少要面对三种不确定性。

第一种是状态会变化。客户端按键时看到 Mana 为 40,请求到达服务器前,角色可能已被另一个 Effect 扣掉 20 Mana。客户端本地检查通过,不代表服务器检查也会通过。

第二种是结果会延迟。火球从生成到命中需要时间,期间施法者可能被眩晕,目标可能死亡,投射物可能撞到场景。Ability 不能在发出请求后一直占着一组临时状态却没有结束规则。

第三种是预测会出错。客户端为了手感可以先播放施法动作、显示临时冷却遮罩,甚至生成纯表现用的预测火球;服务器拒绝时,这些变化必须能够被精确撤销,而不是重置整个角色。

因此,六个阶段并不是六个彼此隔绝的函数,而是六个职责边界:

阶段主要输入主要输出最终权威典型失败清理责任
1. 输入请求输入、AbilityId、Target Data、预测序号AbilityRequest、本地预测上下文客户端表达意图,服务器决定是否受理输入被本地阻断、请求过期撤销未发送或未登记的本地预测
2. 激活检查Ability Spec、Tag、资源、冷却、目标摘要允许或拒绝、稳定错误码服务器未授予、被 Tag 阻断、资源不足、目标无效结束未 Commit 的执行上下文
3. Commit已通过验证的请求、成本和冷却定义扣除 30 Mana、添加 2 秒冷却、Commit 记录服务器提交前状态再次变化、重复请求不创建后续权威对象;按策略处理已提交结果
4. Target Data目标引用、方向、起点、客户端 Tick经服务器验证的目标上下文、权威投射物参数服务器目标失效、距离异常、数据过期释放目标选择与投射物上下文
5. Effect 结算权威命中、来源与目标属性、Effect Spec120 直接伤害、5 秒周期 Burning、复制结果服务器未命中、目标免疫、重复结算结束命中等待,保留应持续的 Active Effect
6. 等待/取消/结束Task 回调、取消原因、服务器结果完成、取消或拒绝后的终态服务器决定规则结果,各端清理自身运行时眩晕、超时、断线、主动取消按统一顺序释放全部临时资源

这里的“阶段”是逻辑顺序,不代表 Target Data 到第 4 阶段才第一次出现。它通常已经随第 1 阶段的请求发送,并在第 2 阶段参与服务器验证;第 4 阶段强调的是:经过验证的目标意图如何成为权威执行上下文。

4.1.1 六个阶段形成的链#

玩家输入
|
v
[1] 输入请求(表达意图,建立预测记录)
|
v
[2] 激活检查(服务器做最终决定)---- 拒绝 -> 拒绝路径(撤销预测)
|
v
[3] Commit(扣除 30 Mana,添加 2 秒冷却)
|
v
[4] Target Data(验证意图,固化权威发射上下文)
|
v
[5] Effect 结算(命中后应用 120 伤害与 5 秒 Burning)
|
v
[6] 等待 / 取消 / 结束(统一清理,进入 Ended)

这条链在两侧各自存在一份:客户端先按本地快照执行一遍预测版本,服务器再按权威状态执行正式版本。两份副本通过请求、确认、拒绝与校正保持同步

4.1.2 四个词不是同一个状态#

项目中最常见的生命周期错误,是把“允许激活”“已经激活”“已经付费”和“已经结束”当成同一件事。先看这四个词分别指什么:

名称准确含义
CanActivate当前快照下允许尝试激活的检查结果
ActivatedAbility 已进入运行时,可以创建 Task、临时 Tag 和等待条件
CommittedCommitAbility 或等价事务成功,成本与冷却按策略进入权威状态
Ended本次 Ability 生命周期已关闭,原因可以是完成、取消或拒绝

CanActivate 严格说更像一次查询结果,而不是持久状态。ActivatedCommittedEnded 也不必实现成一个公开枚举;重要的是运行时能回答这些边界是否已经跨过,从而决定失败时该清理什么、该不该退款、还能不能接受异步回调。

4.1.3 在 UE GAS 中,这条链由谁执行#

前面只描述通用职责,没有引入具体引擎术语。在 UE GAS 中,这条链由 UAbilitySystemComponentUGameplayAbilityAbilityTask 协作完成,实际调用顺序大致如下:

Client(预测副本)
TryActivateAbility(SpecHandle, InputPressed)
-> CanActivateAbility:本地快照检查
-> ActivateAbility:创建 Ability Instance,启动 Task
-> ServerTryActivateAbility(RPC,携带 PredictionKey)
Server(权威副本)
ServerCanActivateAbility:完整权威检查
ServerCommitAbility -> CommitAbility
-> ApplyCost:扣除 30 Mana
-> ApplyCooldown:添加 2 秒 Cooldown
-> 验证 Target Data,生成权威投射物
-> 命中后应用 GameplayEffect
-> EndAbility / CancelAbility

对照六个阶段:

六阶段UE GAS 中的主要入口
1. 输入请求TryActivateAbilityInputPressedFPredictionKey
2. 激活检查CanActivateAbility(客户端)与 ServerCanActivateAbility(服务器)
3. CommitCommitAbility,内部执行 ApplyCostApplyCooldown
4. Target DataFGameplayAbilityTargetDataHandleWaitTargetDataServerSetReplicatedTargetData
5. Effect 结算FGameplayEffectSpecApplyGameplayEffectSpecToTarget、GameplayCue
6. 等待/取消/结束AbilityTaskEndAbilityCancelAbility

4.2 阶段一:输入请求#

输入阶段只负责把“玩家想做什么”转换成能力请求,不负责宣布结果。

火球术请求至少需要携带:

RequestId
AbilityId = Ability.Fireball
PredictionKey
TargetData
ClientTick
InputSource

其中 RequestId 用于幂等处理,PredictionKey 用于关联本地预测与服务器结果,ClientTick 用于判断数据是否过期。InputSource 可以记录这是快捷键、手柄、AI 指令还是 Gameplay Event 触发,但它不能绕过后续规则。

客户端此时可以做一次低成本预检查:

  • 本地是否已知拥有火球术;
  • 本地是否存在明显的 State.DeadState.StunnedState.Silenced
  • 本地显示的 Mana 是否至少为 30;
  • 本地显示的 2 秒冷却是否结束;
  • 是否已经取得基本目标输入。

预检查失败时,不必发送一个必然失败的请求。预检查通过时,客户端可以立刻播放抬手动画、进入瞄准态或显示临时冷却反馈,然后发送 AbilityRequest

权威边界也很明确:客户端权威地表达自己的输入,服务器权威地决定这次输入是否能改变战斗状态。客户端不能在请求中夹带“目标受到 120 伤害”或“Burning 已生效”这样的结论。

在 UE GAS 中,这一阶段对应 TryActivateAbility 入口。输入系统或 AI 调用它时,ASC 先在本地执行一轮 CanActivateAbility,通过后立即以预测方式 ActivateAbility,再通过 ServerTryActivateAbility 把携带 FPredictionKey 的请求发往服务器。FGameplayAbilitySpecHandle 只指向“这个 Actor 拥有的这份能力”,它本身不携带本次目标——目标通过 Target Data 单独传递。

失败路径包括本地预检查失败、请求未发出、请求超时和重复请求。没有进入本地预测时无需额外清理;已经创建预测记录时,超时不能直接当成成功,而应进入等待重试或拒绝清理流程。重复请求的去重规则在第 8 章展开。

4.3 阶段二:激活检查#

服务器收到请求后,需要使用自己的最新状态重新检查。推荐顺序是先做便宜且决定性的检查,再做目标解析、距离和场景查询等较昂贵检查:

  1. 请求发送者是否拥有并控制该 Actor;
  2. Actor 是否被授予 Ability.Fireball
  3. Required Tags 是否满足,Blocked Tags 是否存在;
  4. 是否有至少 30 Mana;
  5. Cooldown.Fireball 是否已经结束;
  6. 当前是否有互斥 Ability 或输入锁定;
  7. Target Data 的结构是否与火球术匹配;
  8. 目标是否存在、可被选择且仍然有效;
  9. 起点、方向、目标距离和视线是否合理;
  10. ClientTickPredictionKey 和请求窗口是否仍然有效。

本地 CanActivate 和服务器 CanActivate 可以得到不同结果。这不是框架失效,而是预测模型允许的正常分歧。服务器结果到达之前,客户端的 Activated 只表示本地预测执行已开始,不能升级为权威事实。

在 UE GAS 中,这一阶段由 CanActivateAbilityServerCanActivateAbility 承担。前者的检查集中在 Tag 的 Required/Blocked 查询,以及成本、冷却对应 GameplayEffect 的可用性检查;服务器版本在 ServerTryActivateAbility 到达后重新执行同一套检查,并额外验证连接与 Actor 所有权。UE 允许项目覆写这两个函数,但覆写不应破坏“客户端可拒绝、服务器权威决定”的边界。

如果服务器在这一阶段拒绝,火球术尚未 Commit,也没有权威投射物、伤害或 Burning。服务器只需记录拒绝结果并回复同一 RequestId;客户端则根据 PredictionKey 撤销对应预测。重复请求到达时,服务器应重发第一次的相同结论,避免重复扣费或前后返回不同结果。

4.4 阶段三:Commit#

Commit 是从“允许尝试”跨到“正式承担成本与限制”的事务边界。对于本章火球术,它至少包含:

Mana -= 30
添加 Cooldown.Fireball,持续 2 秒
记录 RequestId / PredictionKey 已提交

Commit 时机:三种常见选择#

时机网络与规则代价
激活开始时服务器拒绝或预测分歧时更常需要回滚;高延迟下容易看到 Mana、冷却闪回
服务器验证后、生成权威执行对象前需要客户端用预测掩盖往返延迟;服务器必须把验证与 Commit 做成连续事务
命中或结算时飞行期间可重复请求或取消套利;冷却开始偏晚;命中与资源状态竞争更复杂

本章采用第二种:服务器完成所有权、Tag、资源、冷却、目标、距离与时效验证后,在生成权威投射物之前提交 30 Mana 成本和 2 秒冷却。

选择这个时机有三个直接后果:

  • 目标无效会在 Commit 前拒绝,服务器不需要先扣 30 Mana 再退款;
  • Commit 成功后,即使火球最终打空,Mana 和冷却通常也保留,因为玩家已经完成了一次合法施法;
  • 权威投射物只会在 Commit 成功后生成,避免出现“火球已经存在,但扣费失败”的半提交状态。

服务器仍需防止检查与 Commit 之间的状态变化。例如另一条请求可能刚好消耗了 Mana。工程上应让“再次确认提交条件、应用 Cost、应用 Cooldown、登记提交结果”处于同一个不可重复的处理单元中。

在 UE GAS 中,这一阶段对应 CommitAbility:它内部调用 ApplyCostApplyCooldown,二者通过应用 Cost/Cooldown 的 GameplayEffect 完成,而不是直接给 AttributeSet 赋值;提交失败时 CommitAbility 返回 false,Ability 应进入结束路径。UE 的检查与提交本身不是原子事务,原子性、网络复制和自定义 Cost 行为取决于项目配置

4.5 阶段四:Target Data#

Target Data 是对目标上下文的结构化描述。对火球术而言,它可以包含:

目标 Actor Id(可选)
瞄准起点
瞄准方向
客户端认为的命中点
ClientTick
PredictionKey

最重要的原则是:Target Data 是客户端意图,不是最终命中结论。

服务器使用 Target Data 前需要重新验证的检查,大多与激活阶段重叠:所有权、Tag、资源与冷却、目标合法性、空间关系。实现上不要让第 4.3 节的检查与这里的验证变成两套复制代码——让激活检查调用统一的目标验证器。这里只补充 Target Data 特有的两项:

  1. 形状匹配:提交的数据形状是否符合火球术,而不是把另一个 Ability 的范围或目标类型塞进来;
  2. 时效校验ClientTick 是否过期,PredictionKey 是否在允许窗口。

逻辑上则要区分两种语义:

激活检查:这份目标意图是否足以允许本次 Ability 继续?
权威执行:服务器最终采用什么起点、方向和目标上下文生成火球?

服务器可以接受客户端方向,但用服务器角色位置修正发射起点;也可以接受目标 Actor,却重新计算从权威挂点到目标的方向。修正后的上下文应成为后续投射物与命中检测的唯一依据。

如果目标在 Commit 前已无效,服务器返回 InvalidTarget,不扣除权威 Mana,也不添加权威冷却。如果 Commit 后目标才死亡,通常不再撤销施法成本;投射物可以继续飞行、自然消失或按项目规则寻找碰撞结果。

在 UE GAS 中,Target Data 是一份 FGameplayAbilityTargetDataHandle,由 Target Actor 收集或由 WaitTargetData Task 等待玩家选择,再通过 ServerSetReplicatedTargetData 送抵服务器。可迁移的通用原则不是照搬这些类型,而是保留“客户端提交目标意图、服务器验证并生成权威上下文”的边界。

4.6 阶段五:Effect 结算#

Commit 决定“这次施法是否付费”,Effect 结算决定“权威命中造成什么结果”。两者不能合并:合法施放的火球可能打空,而一次命中也不能脱离已确认的 Ability 执行凭空出现。

本章火球术的规则变化发生在两个时刻,可以列成一张表:

Effect目标类型参数应用时机
Mana Cost施法者InstantMana -30生成权威投射物前(Commit)
Cooldown施法者DurationCooldown.Fireball,2 秒生成权威投射物前(Commit)
Direct Damage被命中者InstantHealth -120权威碰撞确认后
Burning被命中者Duration + Periodic持续 5 秒,每 1 秒触发一次权威碰撞确认后

前两行回答“这次施法是否成立”,后两行回答“这次命中改变了什么”。若把 Cost 与 Cooldown 混进结算流程,容易误以为扣费和伤害发生在同一时刻

投射物命中后,服务器再完成以下流程:

权威碰撞或命中事件
-> 确认投射物尚未结算
-> 解析来源与目标 ASC
-> 检查目标当前是否可受伤或免疫
-> 应用 120 点直接伤害
-> 应用 5 秒 Burning
-> 复制 Attribute / Tag / Active Effect 变化
-> 触发命中 GameplayCue

在 UE GAS 中,这一阶段对应 FGameplayEffectSpec 的创建与应用:伤害通过 ApplyGameplayEffectSpecToTarget 进入目标 ASC,周期性结算由 Effect 的 Period 配置驱动,GameplayCue 只负责把“已经确认的命中”映射为表现。客户端可以预测火球轨迹、命中火花或受击停顿,但不应把其他玩家的最终 Health 与持续 Effect 预测为不可撤销事实。

失败路径主要有三类:

  • 投射物未命中:结束命中等待,不应用 Direct Damage 和 Burning;
  • 目标在命中时无效或免疫:记录未结算原因,播放对应反馈;
  • 重复命中回调:通过投射物结算标记或 Effect 唯一键保证只结算一次。

结算结束后,权威投射物可以销毁,命中等待 Task 可以完成,但 2 秒冷却和 5 秒 Burning 各自继续按照 Effect 生命周期运行。Ability 结束不等于它产生的所有 Effect 都立即结束。

4.7 阶段六:异步等待、取消与结束#

4.7.1 三种结束原因#

等待需要被组织进 Ability 生命周期。这里先区分三个结束原因:

Completed:流程正常完成
Cancelled:流程开始后被主动或被动中断
Rejected:预测执行已经开始,但服务器拒绝权威激活

它们最后都进入 Ended,但清理策略并不完全相同:

  • Completed 保留已经 Commit 的成本、冷却和已应用 Effect;
  • Cancelled 是否退还成本取决于 Commit 时机和技能策略,本章火球术在 Commit 后被打断通常不退款;
  • Rejected 发生在服务器 Commit 前,不存在权威成本与冷却,客户端只撤销预测表现。

4.7.2 取消的清理顺序#

取消时最危险的做法,是一边释放状态,一边仍允许异步回调进入。清理按四个步骤组进行:

  1. 切断异步来源:取消目标等待、动画等待、网络等待、投射物等待和定时器,封闭所有回调入口,使后续清理期间不会再收到“目标已确认”或“动画已结束”等迟到事件;
  2. 释放规则状态:移除本次 Ability 持有的临时 Tag,解除按持有者或引用计数管理的输入锁定,清除目标上下文与预测投射物引用。不要误删由其他 Effect 或 Ability 授予的同名状态,最好使用句柄或来源引用释放;
  3. 结算预测记录:根据结果确认、拒绝或校正 PredictionKey——确认时提交并删除可逆日志,拒绝时执行逆操作,校正时先应用服务器快照再丢弃过期记录;
  4. 收束表现并标记结束:停止或淡出预测特效,切换失败/取消动画,隐藏临时 UI;最后关闭 Ability Instance、释放实例级引用并发送结束通知。结束操作必须幂等,重复取消、超时与服务器拒绝同时到达时,只执行一次清理。

在 UE GAS 中,等待由 AbilityTask(如 WaitMontageWaitDelayWaitGameplayEvent)组织,Task 的生命周期不能长于所属 Ability,Ability 结束或被取消时联动结束全部 Task。EndAbility 是幂等的,重复调用不会二次清理。UE 会处理好 Task 本身,但临时 Tag、输入锁定和自定义预测记录的归属与释放仍需项目自己明确。

4.8 服务器确认#

下面的文本时序把“施法被服务器接受”和“火球最终命中”分开。确认请求时,服务器承诺的是这次 Ability 已合法 Commit 并生成权威执行对象;命中仍要等待后续权威碰撞。

玩家
-> Client ASC:按下火球术
Client ASC
-> Client CanActivate:本地检查通过
-> Client Ability:进入 Activated(预测)
-> Presentation:播放抬手、显示预测火球/临时冷却
-> PredictionJournal:记录 PredictionKey = P42
-> Server ASC:发送 AbilityRequest
Ability.Fireball
TargetData
ClientTick
PredictionKey = P42
Server ASC
-> 验证连接与 Actor 所有权
-> 验证 Ability 已授予
-> 验证 Required / Blocked Tags
-> 验证 Mana >= 30
-> 验证 Cooldown.Fireball 已结束
-> 验证目标、距离与视线
-> 验证 ClientTick、RequestId、PredictionKey
-> Server Ability:进入 Activated
-> Commit:
Mana -= 30
添加 Cooldown.Fireball(2 秒)
状态进入 Committed
-> 固化权威发射起点与方向
-> Projectile:生成权威火球
-> Client ASC:返回 Confirm(P42, 权威状态增量)
Client ASC
-> PredictionJournal:Confirm(P42),删除可逆日志
-> 使用权威 Mana / Cooldown 校正本地显示
-> Presentation:保留已播放的抬手,绑定权威火球表现
稍后,Server Projectile
-> 命中目标
-> Target ASC:应用 120 点直接伤害
-> Target ASC:应用 Burning(5 秒,每 1 秒触发一次)
-> Clients:复制属性、Tag、Active Effect 与 GameplayCue
Server / Client Ability
-> 停止等待 Task
-> 清理临时上下文
-> EndAbility
-> 状态进入 Ended

这条时序中,四个生命周期词各自出现于不同位置:

CanActivate 通过
!= 已 Activated
!= 已 Committed
!= 已 Ended

客户端可能先进入预测 Activated,服务器随后才进入权威 Activated。服务器 Commit 后能力处于 Committed,但直到等待完成或取消后才 Ended。火球命中也不是 Committed 的同义词。

4.9 服务器拒绝#

假设客户端按键时锁定了一个敌人,但请求到达服务器前,该目标已经死亡、离开可选范围或其 Actor Id 已失效。客户端仍可能因为旧快照通过本地预检查并播放预测表现,服务器必须以自己的状态拒绝。

玩家
-> Client ASC:按下火球术
Client ASC
-> Client CanActivate:本地检查通过
-> Client Ability:进入 Activated(预测)
-> Presentation:播放抬手、显示预测准星与预测火球
-> Input:锁定施法相关输入
-> PredictionJournal:Begin(P43)
-> Server ASC:发送 AbilityRequest(P43, Target = Actor 42)
Server ASC
-> 验证所有权、Ability、Tag、Mana、Cooldown:通过
-> 验证 Target Data:
Actor 42 已死亡 / 不可选 / 超出有效范围
-> 返回 Reject(P43, InvalidTarget)
-> 不 Commit
-> 不扣除权威 Mana
-> 不添加权威 Cooldown
-> 不生成权威投射物
-> Server Ability:结束拒绝路径
Client ASC
-> 收到 Reject(P43, InvalidTarget)
-> 停止目标、动画、网络等待 Task
-> 移除 State.Casting / State.Targeting 等临时 Tag
-> 解除本次 Ability 持有的输入锁定
-> 清除 Actor 42 的目标上下文与预测火球
-> PredictionJournal:Reject(P43),撤销临时 Mana / Cooldown 显示
-> Presentation:淡出预测效果,播放目标无效反馈
-> Client Ability:EndAbility,状态进入 Ended

这条拒绝路径中,客户端曾经 Activated,但从未得到服务器 Committed。因此它只撤销预测表现和临时规则状态,不等待服务器“退款”

撤销预测表现也不等于把画面逐帧倒放。已播放的短抬手动画可以快速过渡到收势,预测火球可以淡出,准星可以恢复;真正必须恢复的是会影响后续规则的本地状态,包括预测 Mana、临时冷却、Tag、输入锁定、目标上下文和预测记录。

把两条时序放在一起对比,六个阶段的行为差异一目了然:

节点服务器确认服务器拒绝(InvalidTarget)
激活检查全部通过除目标验证外通过
Commit扣除 30 Mana,添加 2 秒冷却不 Commit,不扣费,不加冷却
权威投射物生成不生成
客户端预测记录Confirm(P42),删除可逆日志Reject(P43),撤销预测显示
客户端表现保留抬手,绑定权威火球淡出预测效果,播放目标无效反馈
结束原因CompletedRejected
04-一次Ability的完整执行链
https://blog.rouming-fei.top/posts/04-ability-s-complete-execution-chain/
作者
废江流
发布于
2026-08-08
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
废江流
提灯寻影,灯到影灭。
分类
标签
站点统计
文章
15
分类
4
标签
18
总字数
37,248
运行时长
0
最后活动
0 天前

文章目录