技能系统设计
Category
server / sync 子树。节点定位:技能系统是架构问题——描述/实例化/执行/同步/调试/扩展统一设计,时间轴与状态机分工、效果管线独立、Buff 一等公民。
Definition
技能系统不是“每个技能写一个脚本”这么简单。它本质上是在回答一组更硬的问题:技能如何描述、如何实例化、如何执行、如何同步、如何被调试、如何被内容团队持续扩展。如果这些问题没有被统一设计,项目前期也许只是代码难看一点,中后期就会演变成新技能越来越慢、线上问题越来越难查、平衡调整越来越容易误伤旧逻辑。
Problem
「每个技能写一个脚本」前期只是代码难看,中后期会变成新技能越来越慢、线上问题越来越难查、平衡调整误伤旧逻辑。技能系统与同步边界必须一起定——技能时序改了,同步模型要跟着动。
为什么技能系统迟早会变成架构问题
策划看技能,看到的是玩法、手感、连招和职业差异;客户端看技能,看到的是输入反馈、动画、特效和镜头;服务端看技能,看到的是裁决、命中、冷却、资源和反作弊;AI 和关卡看技能,看到的是可复用的行为能力;工具链看技能,看到的是配置、校验、日志和回放。
一个“冲锋并击飞”的技能,往往同时牵涉:
- 位移与碰撞。
- 目标过滤。
- 伤害与控制。
- Buff 与被动触发。
- 表现广播与回放记录。
技能系统之所以难,不是单个技能复杂,而是它天然跨了太多边界。
技能系统真正要设计什么
技能系统真正要设计的,不是“技能表长什么样”,而是下面几层东西如何分工:
- 技能定义:静态描述技能拥有什么行为。
- 施法请求:玩家或 AI 这一次想放什么技能,对谁放,在什么位置放。
- 技能实例:这次施法真正进入运行时后的对象,带有阶段、计时器、目标快照和上下文。
- 目标选择:谁会成为候选目标,如何过滤、排序、截断和锁定。
- 效果结算:伤害、治疗、Buff、位移、召唤、打断等如何统一执行。
- 表现事件:动画、特效、音效、震屏、飘字和镜头如何订阅权威结果。
- 同步裁决:哪些结果可预测,哪些必须等待服务端确认。
- 调试与回放:为什么没命中、为什么被打断、为什么多触发了一次,系统能不能说清楚。
如果这些层没有拆开,项目通常会退化成“技能 ID + if else + 到处发事件”的形态。
一套更稳妥的运行时骨架
如果技能系统要长期可维护,通常至少需要下面这些运行时对象:
SkillDefinition:技能静态定义,描述标签、施法方式、费用、冷却、阶段时间轴、目标规则和效果列表。CastRequest:施法请求,包含施法者、技能 ID、输入方向、目标点、目标对象和发起时间。CastContext:一次施法上下文,包含当前 Tick、施法者快照、随机种子、地图环境和链路来源。SkillInstance:运行中的技能实例,包含当前阶段、剩余时间、锁定目标和已触发效果。TargetResolver:目标选择器,统一负责从候选集中找出最终命中集。EffectProcessor:效果处理器,统一执行伤害、治疗、Buff、位移、召唤和驱散等效果。BuffInstance:状态效果实例,负责持续时间、叠层、周期 Tick 和消失条件。CombatEvent:标准战斗事件,把施法开始、命中、躲避、打断、Buff 添加、护盾吸收和死亡记录成统一事件流。
这套骨架的核心思想很简单:技能负责组织施法流程,不直接独占所有业务逻辑;效果负责具体改状态;Buff 负责持续性;表现层订阅事件,而不是反向改裁决。
一次施法最好拆成什么流程
一个成熟的技能系统,通常会把一次施法拆成下面这条链路:
- 输入或 AI 发起
CastRequest。 - 进入前置校验,检查冷却、费用、死亡状态、控制状态和公共 CD。
- 创建
SkillInstance,记录施法上下文。 - 进入施法阶段,按时间轴或状态机推进前摇、生效点、持续段和收招。
- 到达特定节点时执行目标选择,拿到命中集合或作用区域。
- 将效果列表送入统一效果管线,逐个执行伤害、治疗、Buff、位移等。
- 产出权威战斗事件,驱动客户端表现、日志、回放和后续触发链。
- 技能结束、取消或被打断,清理运行时状态,进入冷却或恢复态。
把流程拆成这样,最大的好处不是架构漂亮,而是任何一个环节出了问题都能被定位到明确责任层。
时间轴和状态机应该怎么分工
大多数常规动作技能、RPG 技能和怪物技能,最常用也最稳定的套路是时间轴驱动。技能定义里不直接写一段完整脚本,而是定义:
startup:前摇,决定是否可打断、是否锁朝向、是否开始播动作。active:生效段,可以是一个点,也可以是多个片段。recovery:后摇,决定角色何时恢复移动或衔接别的技能。
在这个模型下,很多看似不同的技能都能被统一成“在某些时间点触发某些效果”。
当技能存在明显条件跳转、打断恢复、引导维持和阶段切换时,状态机会比纯时间轴更稳。典型例子包括:
- 引导技能:
casting -> channeling -> complete / interrupt - 蓄力技能:
hold -> charged_lv1 -> charged_lv2 -> release - 连段技能:
stage1 -> combo_window -> stage2
比较稳妥的实践通常是:普通技能走时间轴,复杂持续技能或条件分支技能走状态机,两者共享同一套效果和事件管线。
效果管线为什么必须独立出来
技能系统最容易写崩的地方,是把每个技能都写成一段“自己算一切”的逻辑。更稳妥的做法是把技能和效果拆开:
- 技能定义“什么时候触发哪些效果”。
- 效果处理器定义“某类效果如何统一结算”。
常见效果类型包括:
- 伤害。
- 治疗。
- 加 Buff / 移除 Buff。
- 位移、拉拽、击退、击飞。
- 资源变更。
- 召唤。
- 护盾。
- 打断、沉默、眩晕、免控。
这样设计有几个直接收益:
- 新增一个技能时,大多数时候只是组合已有能力,而不是再写一段新脚本。
- 新增一种效果类型时,只需要扩展一个效果处理器,而不是改所有技能。
- 伤害加成、抗性、护盾吸收、暴击和标签修正可以统一挂到同一结算管线里。
Buff 必须被当成一等公民
很多项目前期把 Buff 当成“技能附带的一点状态”,后面几乎一定会出问题。Buff 实际上承担了太多持续性逻辑:
- 持续伤害和持续治疗。
- 控制与免控。
- 属性修正和叠层。
- 光环、领域、标记和触发器。
比较稳妥的设计通常是:
- 技能负责施加或移除 Buff。
- Buff 自己负责持续时间、叠层、周期 Tick、触发器和消失条件。
- 属性系统和 Buff 系统打通,不在技能里直接临时改基础属性。
如果 Buff 不被建模成独立对象,越往后就越难处理覆盖、刷新、驱散、免疫和日志追踪。
技能系统和同步边界必须一起定
技能系统不能脱离同步体系单独设计。至少要先明确:
- 哪些阶段允许客户端预播,例如起手动作和局部特效。
- 哪些结果必须等待权威确认,例如命中、伤害、死亡和关键控制。
- 技能实例的哪些状态需要进入回放和重连恢复。
- 目标选择和效果结算是否依赖严格逻辑帧。
如果这些边界没先定,技能系统最后就会在客户端先演、服务端再修、回放又说不清之间反复打补丁。
长期扩展能力主要取决于什么
技能系统能不能长期活下去,关键不在于“最开始能不能写出几个大招”,而在于下面这些能力是否存在:
- 新技能能否主要靠组合已有能力完成。
- 新效果类型能否局部扩展,而不是影响全局。
- 技能、Buff、被动和召唤物能否复用统一事件流。
- 线上问题能否定位到具体施法实例、具体目标选择和具体效果结算。
没有这些能力,技能数量一上来,系统很快就会进入“每次加内容都像在拆炸弹”的状态。
常见误区
- 把技能系统理解成“每个技能写一个脚本”,结果越做越难维护。
- 让目标选择、伤害计算、Buff 逻辑和表现广播全部写在技能脚本里,最终没有任何边界。
- 技能系统单独设计,同步、回放和重连之后再补,最后所有裁决问题都说不清。
技能系统设计真正要做的,不是把技能写出来,而是把技能如何长期增长、如何被裁决、如何被解释这三件事一起组织起来。
实战案例:一次「技能越加越慢,一改平衡就误伤」的脚本化堆叠排查
案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「一套更稳妥的运行时骨架」「效果管线为什么必须独立出来」「技能系统和同步边界必须一起定」用到深水区:每个技能一段脚本,越堆越拆不开——这是技能系统没有统一设计的排查,也是「技能负责流程、效果负责结算」的完整展开。
背景与现象:某项目 320 个技能脚本各自为政,目标选择、伤害计算、Buff 逻辑、表现广播全写在脚本里。新技能交付从早期 4 天涨到 12 天;一次平衡调整把 17 个旧技能打出误伤,线上「为什么没命中」的工单平均 4 小时还说不清。
第一步:现象与口径——先看慢和误伤出在哪一层
交付口径:新技能要复制粘贴上一个技能的整段脚本,12 天交付里 8 天在对齐旧脚本的各种隐含约定。
误伤口径:伤害加成、抗性、护盾吸收散落在各技能脚本里,改一处基础公式,17 个引用旧写法的技能被连带改动。
第二步:分层归因——按本页技能分层清单排查
技能系统越做越难归因 占比
脚本自算一切:目标、伤害、Buff、广播混写 54%
Buff 未建模成独立对象,靠约定管覆盖刷新 26%
同步边界后补,回放说不清裁决 20%逐项比对:本页说技能系统难「不是单个技能复杂,而是它天然跨了太多边界」,脚本把边界全部内联;Buff 没有独立对象,覆盖、刷新、驱散、免疫靠各脚本约定;同步边界后补,客户端先演、服务端再修、回放说不清。三类都在,脚本自算一切那条占比最大。
第三步:根因——把技能系统理解成了「每个技能写一个脚本」
问题分层没有拆开,前期只是代码难看,中后期会变成新技能越来越慢、线上问题越来越难查、平衡调整误伤旧逻辑——项目退化成「技能 ID + if else + 到处发事件」的形态。本页的分层——技能定义、施法请求、技能实例、目标选择、效果结算、表现事件、同步裁决、调试回放——被压进一段脚本,任何一层改动都要人肉排查所有脚本。
第四步:处置与回填——按骨架把层拆开
- 运行时八件套落位:
SkillDefinition、CastRequest、CastContext、SkillInstance、TargetResolver、EffectProcessor、BuffInstance、CombatEvent各管一段,脚本只组织施法流程,不直接独占所有业务逻辑。 - 时间轴 / 状态机分工:普通技能走时间轴(
startup/active/recovery),条件分支与引导蓄力走状态机,两者共享同一套效果与事件管线。 - 效果管线独立 + Buff 一等公民:效果处理器统一结算,Buff 自管持续、叠层、周期 Tick 与消失条件;同步边界同步定,命中、伤害、死亡必须等权威确认。
复核账:新技能交付 12 天 → 3 天;平衡调整误伤 17 个 → 0 个;「为什么没命中」工单定位 4 小时 → 15 分钟。
回填清单:
| 回填项 | 动作 | 回填处 |
|---|---|---|
| 运行时八件套 | 从 SkillDefinition 到 CombatEvent 分八层,脚本只组织流程 | 一套更稳妥的运行时骨架 |
| 时间轴 / 状态机分工 | 普通技能走时间轴,条件分支走状态机,共享效果管线 | 时间轴和状态机应该怎么分工 |
| 效果管线独立 | 技能定义何时触发,效果处理器统一结算 | 效果管线为什么必须独立出来 |
| Buff 一等公民 | Buff 自管持续、叠层、周期 Tick 与消失条件 | Buff 必须被当成一等公民 |
案例的三个教训
- 技能系统是架构问题,不是脚本数量问题。分层不拆开,技能越多,每次加内容都像在拆炸弹。
- 技能负责组织施法流程,不直接独占所有业务逻辑。效果负责改状态,Buff 负责持续性,表现层订阅事件而不是反向改裁决。
- 技能系统和同步边界必须一起定。边界后补的结果,就是客户端先演、服务端再修、回放又说不清之间反复打补丁。
Used By
技能系统设计评审;效果管线与 Buff 体系搭建;技能调试能力建设。
Related
关系链:架构问题 → 设计内容 → 运行时骨架 → 施法流程 → 时间轴/状态机分工 → 效果管线 → Buff → 同步边界。上游:sync/02(同步边界)。下游:system 树技能系统(部件边界与扩展口径)、numerical/02(战斗数值)。
Reference
本页为工程经验归纳;页内实战案例为教学示例(数字取整简化)。