技能系统是战斗服务器最容易积累债务的地方。客户端为了手感希望立刻播放动作,策划希望技能有前摇、后摇、霸体、沉默、眩晕、位移、连段和取消窗口,服务器则要保证判定公平。若架构只把技能当作一个“收到请求后立即扣蓝并造成伤害”的函数,后续每加一个打断规则都会变成 if else。技能施放与打断架构要把一次技能拆成可观察的阶段:请求、预校验、锁定、前摇、引导、结算、后摇、结束,并让打断事件在明确窗口内生效。
典型场景
玩家 A 释放一个 1.2 秒吟唱火球,玩家 B 在 0.8 秒时使用沉默。客户端 A 可能已经播放到火球飞出前一帧,客户端 B 也认为沉默命中。如果服务器没有阶段模型,就很容易出现 A 看到技能放出但服务器判定失败,或者服务器先扣蓝后又撤销伤害。更稳的设计是服务器在 castId 下维护技能阶段、关键时间点和可打断窗口,沉默事件进入同一战斗时间线,按服务器 tick 顺序裁决。
架构示意
stateDiagram-v2
[*] --> Requested
Requested --> Prechecked
Prechecked --> Windup
Windup --> Channeling
Windup --> Interrupted
Channeling --> Impact
Channeling --> Interrupted
Impact --> Recovery
Recovery --> Finished
Interrupted --> Finished
技能阶段是服务端权威状态,不是动画标签
前摇、引导、命中、后摇在客户端是动画,在服务端是状态机。状态机要记录 castId、技能版本、施法者、目标快照、开始 tick、阶段结束 tick、消耗是否已提交、命中是否已结算。客户端可以预测播放,但最终以服务器阶段事件校正。没有这个状态机,后续想实现断线恢复、观战回放、反作弊审计都会缺少证据。
消耗扣减要与技能阶段匹配
有些技能在按下时扣蓝,有些在释放成功时扣资源,有些引导技能按秒扣。架构上不能把扣减写死在请求入口,而要由技能配置声明消耗点。消耗点进入技能流水线后必须幂等,避免网络重发导致重复扣蓝。打断时是否返还资源,也应由配置和阶段决定:前摇被打断可能返还,引导中断通常不返还已消耗部分。
打断事件进入同一时间线裁决
眩晕、沉默、击飞、死亡、切图、目标无效都可能打断技能。它们不应该直接在任意线程里修改技能对象,而应作为战斗事件进入房间服或战斗服的有序队列。裁决时比较事件 tick、技能当前阶段和可打断标签。这样可以解释为什么两个玩家几乎同时操作时服务器选择了某个结果,也能让回放系统复现。
客户端表现需要补偿事件,而不是靠猜
为了手感,客户端会预测技能开始。服务器确认后返回 castAccepted、stageChanged、impactResolved、castInterrupted 等事件。若客户端预测成功,事件只是校正;若预测失败,客户端播放取消或资源回滚表现。不要只返回一个错误码让客户端自己猜如何收尾。尤其是竞技游戏,打断反馈是否清晰直接影响玩家对公平性的感知。
技能配置需要版本化,避免热更新撕裂
一次技能开始时应绑定技能配置版本。即便运营在战斗中热更新了技能前摇或伤害,已开始的 castId 也应继续使用旧版本,除非明确支持战斗内强制刷新。否则同一技能的前摇结束时间、消耗点和伤害公式可能在不同服务间不一致。技能版本要写进战斗日志,方便复盘异常伤害。
关键设计取舍
| 维度 | 架构处理 | 重点风险 |
|---|---|---|
| 前摇打断 | 技能未命中,可按规则返还资源 | 沉默、硬控 |
| 引导打断 | 已生效部分保留,后续停止 | 持续治疗、蓄力 |
| 命中后打断 | 伤害已提交,只影响后摇 | 位移取消 |
| 死亡打断 | 清理未完成 cast,记录原因 | 战斗结束、角色死亡 |
落地检查清单
- 每次施法生成 castId 并记录技能配置版本
- 技能阶段状态机持久到战斗日志或回放事件中
- 打断事件通过战斗有序队列裁决
- 消耗点和返还规则由配置声明并幂等执行
- 客户端接收明确的阶段变化和打断原因
一线排障与复盘建议
这个架构上线后,团队要提前准备几类排障入口。第一是按玩家、业务单号或场景 id 查询完整链路,能看到请求进入、状态变化、关键版本、外部依赖结果和最终响应。第二是按时间窗口查看异常分布,区分是全局配置错误、单分片容量问题,还是少量玩家边界条件触发。第三是保留人工修复入口,但修复入口必须写审计流水,记录修复前状态、修复后状态、操作人、审批单和影响范围。没有审计的手工修复,短期能救火,长期会破坏系统可信度。
容量评估也要贴近玩法节奏,而不是只看平均在线。运营开活动、赛季结算、跨服匹配、周常刷新和主播带队都会让请求集中到很短窗口。压测脚本应模拟重复点击、弱网重试、服务超时、实例重启和消息乱序,不要只跑顺滑路径。对于玩家资产、资格、奖励、处罚这类敏感链路,压测结果里要额外检查幂等流水和最终状态,不只是吞吐量。
上线前可以采用影子模式:生产请求仍走旧逻辑,新架构旁路计算结果并记录差异。差异样本要由服务端、策划和客服一起看,因为有些差异来自旧逻辑 bug,有些来自新规则理解错误。等差异收敛后,再按小区服、低风险玩法或内部账号灰度。灰度期间观察错误码、超时、回滚次数、人工工单和玩家反馈,确认系统在真实噪声下仍然可解释。
技能事件日志格式
每次施法建议记录 CastStarted、StageEntered、CostApplied、InterruptReceived、ImpactResolved、CastFinished 等事件。事件里包含 castId、casterId、skillId、skillVersion、serverTick、stage、targetSnapshot、randomSeed 和 reason。对于竞技玩法,randomSeed 很重要,因为暴击、命中、弹道散布等结果需要回放复现。
事件日志不一定全量长期保存。可以按玩法分级:排位赛和锦标赛保存完整战斗日志,普通 PVE 保存摘要,压测环境保存更详细调试字段。关键是线上出现“我明明打断了为什么还中招”时,服务端能用日志说明:沉默事件到达服务器时,技能已经进入 Impact 阶段,所以只打断后摇;或者沉默命中时目标处于霸体标签,打断无效。
配置表达方式
技能配置应尽量声明式,而不是把逻辑写进代码。一个技能可以声明 stageTimeline:windup 600ms、channel 1200ms、impact at 1800ms、recovery 400ms;interruptRules 声明哪些阶段可被 silence、stun、knockup、death 打断;costRules 声明 mana at accepted、ammo at impact;refundRules 声明 windupInterrupted refund 80%。服务端运行时把这些配置编译成状态机。
当然,完全声明式也有边界。复杂 Boss 技能、连招派生、地形交互可能需要脚本或代码扩展。架构上可以允许技能效果节点自定义,但阶段推进、消耗提交、打断裁决仍由统一流水线负责。这样特殊技能不会破坏通用审计和回放能力。
故障案例:客户端取消导致服务器资源错乱
某动作游戏允许玩家翻滚取消技能后摇。早期客户端在取消后本地停止动画,并发送 cancel 请求;服务器收到 cancel 时直接把技能状态改为结束。后来发现玩家可以在高延迟环境下取消已命中的技能,服务器偶尔返还了部分资源,造成高频技能白嫖。
修复后,取消被建模为一种 interruptEvent,必须进入战斗时间线裁决。只有 recovery 阶段允许 voluntary_cancel,impact 之后不返还资源;如果 cancel 请求到达时服务器 tick 已超过命中点,伤害和消耗都保留。客户端仍能立即播放取消动作,但服务器会返回校正事件。手感和公平的矛盾通过明确阶段解决,而不是相信客户端。
性能与并发注意点
战斗服通常是单房间有序处理,技能状态机不要跨线程随意修改。若伤害计算、寻路或碰撞需要异步加速,异步结果返回时也要带 castId 和 stageVersion,回到战斗线程验证仍然有效后再提交。否则一个已被打断的技能可能因为异步伤害晚到而重新造成伤害。
大量持续技能会增加事件数量,可以合并 tick。比如持续灼烧每 200ms 结算一次,不必每帧写日志;但阶段边界、打断和最终伤害仍要记录。性能优化不能牺牲可解释的关键事件。
上线验收指标
技能流水线上线后,要比较服务器裁决和客户端表现的偏差。可以采样 castId,记录客户端预测开始时间、服务器 accepted tick、impact tick、interrupt tick 和最终结果。偏差不可能为零,但应稳定在设计范围内。若玩家经常看到技能命中表现但服务器判定失败,说明阶段事件或补偿表现需要调整。
压测不只是机器人互相放技能,还要制造高延迟、丢包、乱序、重复 cast 请求、目标死亡、施法者切图、控制效果同时命中等情况。回滚条件包括资源扣减无法解释、同一 castId 多次 impact、打断后仍造成伤害、客户端收到未知阶段。竞技玩法建议先在非排位模式灰度,确认回放和日志能解释争议后再扩大。
团队协作边界
技能阶段需要策划、动画、客户端和服务器使用同一套词汇。前摇在动画里可能叫 start,在策划表里叫 cast,在服务器里叫 windup,这会让问题沟通非常低效。建议建立统一阶段字典,并在配置、日志、客户端事件里使用同一名称。
每个新技能上线前,策划要标注可打断阶段、资源消耗点、是否允许移动取消、命中快照时机和版本兼容策略。服务器根据这些字段生成状态机,客户端根据阶段事件做表现。这样特殊技能也不会变成隐藏代码分支。
总结
技能打断不是一堆特殊规则,而是一条时间线上的状态转换。把阶段、消耗和打断窗口建模清楚,战斗系统才能在手感和公平之间找到稳定平衡。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。