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 Spec | 120 直接伤害、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 | 当前快照下允许尝试激活的检查结果 |
Activated | Ability 已进入运行时,可以创建 Task、临时 Tag 和等待条件 |
Committed | CommitAbility 或等价事务成功,成本与冷却按策略进入权威状态 |
Ended | 本次 Ability 生命周期已关闭,原因可以是完成、取消或拒绝 |
CanActivate严格说更像一次查询结果,而不是持久状态。Activated、Committed和Ended也不必实现成一个公开枚举;重要的是运行时能回答这些边界是否已经跨过,从而决定失败时该清理什么、该不该退款、还能不能接受异步回调。
4.1.3 在 UE GAS 中,这条链由谁执行
前面只描述通用职责,没有引入具体引擎术语。在 UE GAS 中,这条链由 UAbilitySystemComponent、UGameplayAbility 和 AbilityTask 协作完成,实际调用顺序大致如下:
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. 输入请求 | TryActivateAbility、InputPressed、FPredictionKey |
| 2. 激活检查 | CanActivateAbility(客户端)与 ServerCanActivateAbility(服务器) |
| 3. Commit | CommitAbility,内部执行 ApplyCost 与 ApplyCooldown |
| 4. Target Data | FGameplayAbilityTargetDataHandle、WaitTargetData、ServerSetReplicatedTargetData |
| 5. Effect 结算 | FGameplayEffectSpec、ApplyGameplayEffectSpecToTarget、GameplayCue |
| 6. 等待/取消/结束 | AbilityTask、EndAbility、CancelAbility |
4.2 阶段一:输入请求
输入阶段只负责把“玩家想做什么”转换成能力请求,不负责宣布结果。
火球术请求至少需要携带:
RequestIdAbilityId = Ability.FireballPredictionKeyTargetDataClientTickInputSource其中 RequestId 用于幂等处理,PredictionKey 用于关联本地预测与服务器结果,ClientTick 用于判断数据是否过期。InputSource 可以记录这是快捷键、手柄、AI 指令还是 Gameplay Event 触发,但它不能绕过后续规则。
客户端此时可以做一次低成本预检查:
- 本地是否已知拥有火球术;
- 本地是否存在明显的
State.Dead、State.Stunned或State.Silenced; - 本地显示的 Mana 是否至少为 30;
- 本地显示的 2 秒冷却是否结束;
- 是否已经取得基本目标输入。
预检查失败时,不必发送一个必然失败的请求。预检查通过时,客户端可以立刻播放抬手动画、进入瞄准态或显示临时冷却反馈,然后发送 AbilityRequest。
权威边界也很明确:客户端权威地表达自己的输入,服务器权威地决定这次输入是否能改变战斗状态。客户端不能在请求中夹带“目标受到 120 伤害”或“Burning 已生效”这样的结论。
在 UE GAS 中,这一阶段对应 TryActivateAbility 入口。输入系统或 AI 调用它时,ASC 先在本地执行一轮 CanActivateAbility,通过后立即以预测方式 ActivateAbility,再通过 ServerTryActivateAbility 把携带 FPredictionKey 的请求发往服务器。FGameplayAbilitySpecHandle 只指向“这个 Actor 拥有的这份能力”,它本身不携带本次目标——目标通过 Target Data 单独传递。
失败路径包括本地预检查失败、请求未发出、请求超时和重复请求。没有进入本地预测时无需额外清理;已经创建预测记录时,超时不能直接当成成功,而应进入等待重试或拒绝清理流程。重复请求的去重规则在第 8 章展开。
4.3 阶段二:激活检查
服务器收到请求后,需要使用自己的最新状态重新检查。推荐顺序是先做便宜且决定性的检查,再做目标解析、距离和场景查询等较昂贵检查:
- 请求发送者是否拥有并控制该 Actor;
- Actor 是否被授予
Ability.Fireball; - Required Tags 是否满足,Blocked Tags 是否存在;
- 是否有至少 30 Mana;
Cooldown.Fireball是否已经结束;- 当前是否有互斥 Ability 或输入锁定;
- Target Data 的结构是否与火球术匹配;
- 目标是否存在、可被选择且仍然有效;
- 起点、方向、目标距离和视线是否合理;
ClientTick、PredictionKey和请求窗口是否仍然有效。
本地
CanActivate和服务器CanActivate可以得到不同结果。这不是框架失效,而是预测模型允许的正常分歧。服务器结果到达之前,客户端的Activated只表示本地预测执行已开始,不能升级为权威事实。
在 UE GAS 中,这一阶段由 CanActivateAbility 与 ServerCanActivateAbility 承担。前者的检查集中在 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:它内部调用 ApplyCost 与 ApplyCooldown,二者通过应用 Cost/Cooldown 的 GameplayEffect 完成,而不是直接给 AttributeSet 赋值;提交失败时 CommitAbility 返回 false,Ability 应进入结束路径。UE 的检查与提交本身不是原子事务,原子性、网络复制和自定义 Cost 行为取决于项目配置
4.5 阶段四:Target Data
Target Data 是对目标上下文的结构化描述。对火球术而言,它可以包含:
目标 Actor Id(可选)瞄准起点瞄准方向客户端认为的命中点ClientTickPredictionKey最重要的原则是:Target Data 是客户端意图,不是最终命中结论。
服务器使用 Target Data 前需要重新验证的检查,大多与激活阶段重叠:所有权、Tag、资源与冷却、目标合法性、空间关系。实现上不要让第 4.3 节的检查与这里的验证变成两套复制代码——让激活检查调用统一的目标验证器。这里只补充 Target Data 特有的两项:
- 形状匹配:提交的数据形状是否符合火球术,而不是把另一个 Ability 的范围或目标类型塞进来;
- 时效校验:
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 | 施法者 | Instant | Mana -30 | 生成权威投射物前(Commit) |
| Cooldown | 施法者 | Duration | Cooldown.Fireball,2 秒 | 生成权威投射物前(Commit) |
| Direct Damage | 被命中者 | Instant | Health -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 取消的清理顺序
取消时最危险的做法,是一边释放状态,一边仍允许异步回调进入。清理按四个步骤组进行:
- 切断异步来源:取消目标等待、动画等待、网络等待、投射物等待和定时器,封闭所有回调入口,使后续清理期间不会再收到“目标已确认”或“动画已结束”等迟到事件;
- 释放规则状态:移除本次 Ability 持有的临时 Tag,解除按持有者或引用计数管理的输入锁定,清除目标上下文与预测投射物引用。不要误删由其他 Effect 或 Ability 授予的同名状态,最好使用句柄或来源引用释放;
- 结算预测记录:根据结果确认、拒绝或校正
PredictionKey——确认时提交并删除可逆日志,拒绝时执行逆操作,校正时先应用服务器快照再丢弃过期记录; - 收束表现并标记结束:停止或淡出预测特效,切换失败/取消动画,隐藏临时 UI;最后关闭 Ability Instance、释放实例级引用并发送结束通知。结束操作必须幂等,重复取消、超时与服务器拒绝同时到达时,只执行一次清理。
在 UE GAS 中,等待由 AbilityTask(如 WaitMontage、WaitDelay、WaitGameplayEvent)组织,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),撤销预测显示 |
| 客户端表现 | 保留抬手,绑定权威火球 | 淡出预测效果,播放目标无效反馈 |
| 结束原因 | Completed | Rejected |