Ability 施法过程
基于全数据驱动理念与深度架构推演,本模型旨在彻底解决动作游戏与 RPG 中复杂的技能生命周期管理问题。
- 瞬发零开销:
Instant模型等同于极简的”扣资源-触发”行为,不被复杂的生命周期拖累。 - 生命周期原生托管:前摇(Startup)、引导(Channel)甚至后摇(Recovery),都是技能真实占用角色时间的生命周期阶段,必须由
AbilityInstance原生接管,以保证动作取消、UI 表现、并发锁的精确性。 - 打断与取消双轨语义:TagRules 提供
interruptsAbilities(硬打断,有惩罚)和cancelsAbilities(软取消,无惩罚)两种打断动词。技能在前摇时被眩晕是”打断”,在后摇时被翻滚截断是”动作取消”,底层逻辑完全自洽。 - 最少时间原语:只保留不可互相还原的四种时间模型(Instant/Startup/Charge/Channel)。连招、形态切换等复合需求通过”标签 + 组合”实现。
Ability 表结构
Section titled “Ability 表结构”table ability[id] (json) { id: int; name: text; description: text;
abilityTags: list<str> ->gameplaytag;
// 准入检查 activationConditions: list<Condition>; costs: list<ResourceCost>; cooldown: FloatValue;
// 瞄准与输入要求 targeting: TargetingRequirement;
// 施法方式 castMode: CastMode;
// 技能的核心动作(出伤、发子弹、挂 Buff 等) effect: Effect;
// Processing 阶段被 interruptsAbilities 打断时的惩罚动作 onInterrupt: list<Effect>;
// 技能主体逻辑执行完毕后的收招阶段 recovery: RecoveryConfig;}
struct ResourceCost { resource: str ->resource_definition; value: FloatValue;}Cost / Cooldown 的 FloatValue 约束:costs[].value 与 cooldown 在激活期(CanActivate / StartCooldown)用 WithoutPayload 路径求值,此时没有事件上下文。因此禁止使用 Payload-relative 表达式(PayloadMagnitude / PayloadVar)——否则会静默求值为 0,导致零消耗 / 零冷却。
瞄准与输入 (Targeting)
Section titled “瞄准与输入 (Targeting)”TargetingRequirement 定义了技能激活前需要收集什么输入,以及在持续施法期间对输入的容忍策略。
interface TargetingRequirement { // 无需外部输入(如:战吼、自身Buff) struct None {}
// 需要选中一个实体 struct SingleTarget { allowedRelations: list<Relation>; tagQuery: TagQuery; maxRange: FloatValue; onTargetLost: TargetLostPolicy; }
// 需要指定一个地点(如:火雨) struct PointTarget { maxRange: FloatValue; }
// 需要指定一个方向(如:非指向性冲刺、扫射射线) struct DirectionalTarget {}}
enum TargetLostPolicy { Cancel; // 目标死亡或超出范围,触发 cancel Continue; // 继续施法(对空放)}动态 Targeting 数据流
Section titled “动态 Targeting 数据流”引擎在 Activate 时,将瞄准数据写入 instanceState 中。
引擎在 Processing 阶段(蓄力、引导期间)每帧根据玩家鼠标/摇杆实时更新这些变量。当结算 effect 时,读取到的永远是玩家动作那一刻的最精确输入。
| Targeting 类型 | context.target 初始值 | instanceState 写入 |
|---|---|---|
None | 施法者自身 | 无 |
SingleTarget | 选中的目标 Actor | targetingActor = 选中目标 |
PointTarget | 施法者自身 | targetingPoint = 选中地点 |
DirectionalTarget | 施法者自身 | targetingDir = 选中方向 |
SingleTarget 的 Processing 阶段追踪:每帧验证目标存活 ∧ 目标在 maxRange 内 ∧ 目标满足 tagQuery。验证失败时按 onTargetLost 处理(Cancel 触发 cancel,Continue 不处理)。PointTarget 和 DirectionalTarget 的数据为标量,不存在”丢失”概念,无需追踪。
施法方式 (CastMode)
Section titled “施法方式 (CastMode)”interface CastMode { // 瞬发模型 struct Instant {}
// 前摇模型,读条 struct Startup { startupTime: FloatValue;
startupTags: list<str> ->gameplaytag; startupCues: list<str> ->cue_key_state; commit: StartupCommitTiming; }
// 蓄力模型(按住蓄力,松手触发) struct Charge { minChargeTime: FloatValue; maxChargeTime: FloatValue; releaseOnMax: bool;
chargingTags: list<str> ->gameplaytag; chargingCues: list<str> ->cue_key_state; commit: ChargeCommitTiming; }
// 引导模型(持续触发) struct Channel { duration: FloatValue; tickInterval: FloatValue; // 心跳间隔,执行effect逻辑 maxTicks: int; // -1 = 无限,由 duration 截断 tickOnStart: bool; // 是否在激活瞬间立即触发首个 tick finisherEffect: list<Effect>; // 收尾技
channelingTags: list<str> ->gameplaytag; channelingCues: list<str> ->cue_key_state; commit: ChannelCommitTiming; }}
// Commit 决定了【扣除 costs + 启动 cooldown】发生的精确时刻enum StartupCommitTiming { OnActivate; // 激活即扣(被打断不退费) OnComplete; // 前摇完成瞬间扣(被打断白嫖)}enum ChargeCommitTiming { OnActivate; // 激活即扣 OnRelease; // 松手且达到 minChargeTime 时扣(蓄力不足取消不扣费)}enum ChannelCommitTiming { OnActivate; // 激活即扣 OnFirstTick; // 发生首次 tick 时扣(tickOnStart=true 的立即执行算作首次 tick)}引擎标准输出变量
Section titled “引擎标准输出变量”部分引擎产物的语义对所有同类 CastMode 技能完全一致,不再让每个技能重复声明,而是收敛为全局约定(在 combat_settings 中统一配置)。配置端通过 FloatValue.ContextVar(varKey) 读取。
| 变量 | 来源 | 语义 | 配置位置 |
|---|---|---|---|
chargeProgressVar | Charge 阶段每帧写入 | 当前蓄力在 [minCharge, maxCharge] 区间的归一化进度(0~1,单调递增) | combat_settings.chargeProgressVar |
红线:只有「引擎产出 + 语义跨同类技能统一」的变量才进
combat_settings。技能局部计数器(命中次数、连击数等)仍由技能自身在 instanceState 中按 var_key 维护。
生命周期与状态机 (Lifecycle)
Section titled “生命周期与状态机 (Lifecycle)”Ability 运行时由「激活动作」切入三大核心阶段(Phase):Processing、Executing、Recovering。
对齐
StatusInstance.Apply:AbilityInstance的构造只做无副作用的字段初始化,真正的首帧动作(Commit / 首 tick)由Activate()显式执行,并直达首个真实阶段。Activate执行期间免疫打断——首帧挂载的标签可能触发OnTagAdded级联,免疫窗口防止级联回调重入尚未完成的激活流程。
stateDiagram-v2
direction TB
[*] --> CanActivate
CanActivate --> Activating: 准入通过
CanActivate --> [*]: 准入拒绝
note right of Activating
免疫打断窗口
Commit==OnActivate 在此扣费
Channel tickOnStart 首 tick
end note
Activating --> Executing: Instant 直达
Activating --> Processing: Startup / Charge / Channel
Processing --> Executing: 阶段完成
Processing --> Ended: interrupt → onInterrupt
Processing --> Ended: cancel → 直接清理
Executing --> Recovering: effect 结算完毕
note right of Executing: 延迟 Commit 通常在此
Executing --> Ended: duration <= 0 跳过
Recovering --> Ended: 倒计时结束
Recovering --> Ended: interrupt / cancel 均无惩罚
Ended --> [*]
CanActivate
Section titled “CanActivate”推荐检查顺序(从快到慢,尽早拒绝):
- TagRules 的
blocksAbilities是否拦截当前abilityTags cooldown就绪costs资源充足(检查但不扣除)activationConditions条件满足maxConcurrentAbilitiesPerActor未超限targeting验证(目标/地点/方向已由引擎收集且合法)
全部通过才进入 Activating。Targeting 验证放在最后,引擎可在步骤 1-5 通过后再触发目标选择 UI。
打断 (Interrupt) 与 取消 (Cancel)
Section titled “打断 (Interrupt) 与 取消 (Cancel)”| 方面 | interrupt | cancel |
|---|---|---|
| 触发源 | TagRules 的 interruptsAbilities | TagRules 的 cancelsAbilities、玩家主动操作、TargetLost |
| 执行 ability.effect | 否 | 否 |
| 执行 onInterrupt | 是(仅 Processing 阶段) | 否 |
| 已 commit 资源 | 保留 | 保留 |
| 未 commit 资源 | 不扣 | 不扣 |
| 广播事件 | Ability_Interrupted | Ability_Cancelled |
| Recovery 阶段行为 | 等同 cancel(无惩罚) | 直接清理结束 |
关键规则:Recovery 阶段(ability.effect 已成功执行)无论被 interruptsAbilities 还是 cancelsAbilities 命中,均视为动作取消——不执行 onInterrupt,广播 Ability_Completed。
Recovery 阶段(后摇)
Section titled “Recovery 阶段(后摇)”若 duration <= 0 则直接跳过此阶段。 必须由系统原生托管后摇,以保证 UI 占用状态准确,防范”动作未完但逻辑可重入”的穿透 Bug。
struct RecoveryConfig { duration: FloatValue; recoveryTags: list<str> ->gameplaytag; // 如 ["State.Recovery"] recoveryCues: list<str> ->cue_key_state;}通过 recoveryTags 与 tag_rules 的组合,策划可精确控制每个技能后摇的”硬度”:
| 后摇类型 | recoveryTags | 表现 |
|---|---|---|
| 轻型后摇 | ["State.Recovery"] | 不能攻击/施法,但可以翻滚取消 |
| 重型后摇 | ["State.Recovery.Heavy"] | 不能攻击/施法/翻滚,必须等后摇结束 |
| 无后摇 | duration=0,跳过 Recovery | 即时释放下一个动作 |
状态与 TagRules 的交互示例
Section titled “状态与 TagRules 的交互示例”TagRules 的完整定义见 ability-design.md。此处展示其 interruptsAbilities(硬打断)、cancelsAbilities(软取消)和blocksAbilities(施法约束)在施法生命周期中的具体运用示例:
tag_rules { name: "CoreCombatRules"; rules: [ // 硬控打断 { whenPresent: "State.Debuff.Control.Stun"; interruptsAbilities: ["Ability.Type"]; blocksAbilities: ["Ability.Type"]; description: "眩晕:硬打断并封锁所有技能"; },
{ whenPresent: "State.Debuff.Silence"; interruptsAbilities: ["Ability.Type.Spell"]; blocksAbilities: ["Ability.Type.Spell"]; description: "沉默:硬打断并封锁法术类技能"; },
// 软取消 { whenPresent: "State.Dodging"; cancelsAbilities: ["Ability.Type"]; description: "翻滚:软取消任何技能(含后摇)"; },
{ whenPresent: "State.Moving"; cancelsAbilities: ["Ability.Startup.MoveCancel"]; description: "移动:软取消标记为可移动取消的前摇技能"; },
// 施法约束 { whenPresent: "State.Recovery"; blocksAbilities: ["Ability.Type.Spell", "Ability.Type.Melee"]; description: "后摇期间禁止攻击和施法"; },
{ whenPresent: "State.Recovery.Heavy"; blocksAbilities: ["Ability.Type.Movement"]; description: "重型后摇期间禁止移动类技能"; } ];}技能配置示例:移动取消前摇
Section titled “技能配置示例:移动取消前摇”// 可被移动取消的治疗术ability { id: 1001; name: "治疗术"; abilityTags: ["Ability.Type.Spell", "Ability.Startup.MoveCancel"]; // ▲ 标记为可被移动取消
castMode: Startup { startupTime: Const { value: 2.5; }; startupTags: ["State.Startup.Spell"]; // ▲ 不含 State.Immobile → 移动不被 block // 但 TagRules: State.Moving cancelsAbilities Ability.Startup.MoveCancel // → 玩家一动,此技能被 cancel(无惩罚,不扣费) startupCues: ["Startup.Heal"]; commit: OnComplete; };
effect: ...;}
// 站桩带前摇的火球术(移动直接被禁止)ability { id: 1002; name: "火球术"; abilityTags: ["Ability.Type.Spell"]; // ▲ 没有 Ability.Startup.MoveCancel → 移动不会取消此技能
castMode: Startup { startupTime: Const { value: 2.0; }; startupTags: ["State.Startup.Spell", "State.Immobile"]; // ▲ State.Immobile → TagRules blocks Ability.Type.Movement → 按不动 startupCues: ["Startup.Fireball"]; commit: OnComplete; };
effect: ...; onInterrupt: [ GrantTag { grantedTags: ["State.AbilityLockout"]; duration: Const { value: 0.5; }; }, FireCue { cue: "Startup.Interrupted"; } ]; recovery: { duration: Const { value: 0.3; }; recoveryTags: ["State.Recovery"];};}生命周期广播事件
Section titled “生命周期广播事件”存在两套事件、两种 payload 语义
| 事件族 | instigator | target |
|---|---|---|
Ability 生命周期事件(Ability_Activated/Committed/Executed/...) | 施法者 | 施法者 |
Pipeline 伤害事件(Damage_Deal_Pre/Post、Damage_Take_Pre/Post) | 攻击者 | 被击者 |
| 事件名 | 触发时机 | 典型用途 |
|---|---|---|
Ability_Activated | 进入 Activating 阶段 | 触发”准备施法时获得霸体”被动 |
Ability_Committed | 实际扣除资源并启动 CD 的瞬间 | 触发”消耗法力时回血”被动 |
Ability_Executed | ability.effect 执行完毕 | 触发”释放法术后强化下一次普攻”被动 |
Ability_Interrupted | Processing 阶段被 interruptsAbilities 命中 | 触发”被打断时获得激怒”被动 |
Ability_Cancelled | 玩家主动停止 / cancelsAbilities 命中 / TargetLost | UI 提示”蓄力失败” |
Ability_Completed | Executing 阶段完成后最终退出(无论 Recovery 是否被取消) |
复合模式:连招设计指引
Section titled “复合模式:连招设计指引”连招不作为底层原语,通过”多段独立 Ability + Tag 窗口”实现。
[普攻一段] (id:1001) [普攻二段] (id:1002) Instant Instant CD: 0.0s CD: 0.0s activationConditions: HasTag "Combo.Attack.S2"
recovery: recovery: duration: 0.6s duration: 0.8s recoveryTags: ["State.Recovery"] recoveryTags: ["State.Recovery"]
effect: effect: + Damage(50) + PurgeStatusByTag ["Combo.Attack"] + GrantTag + Damage(80) "Combo.Attack.S2" + GrantTag duration: 1.0s "Combo.Attack.S3" duration: 1.0s逻辑解剖:一段普攻挥出后,产生 0.6s 后摇(不可走位),但同时抛出 1.0s 的 Combo.Attack.S2 窗口标签。在这 1.0s 内按下攻击,二段普攻释放,TagRules 自动 cancel 掉一段普攻的 Recovery,实现丝滑派生。
设计决策记录
Section titled “设计决策记录”为什么 interruptsAbilities 和 cancelsAbilities 需要分开
Section titled “为什么 interruptsAbilities 和 cancelsAbilities 需要分开”旧版只有一个 cancelsAbilities,无法区分”眩晕打断前摇(应有惩罚)“和”翻滚取消后摇(不应有惩罚)“。拆分后:
- 策划可以精确控制”移动取消前摇”这类柔性中断——不触发
onInterrupt,不扣费 - Recovery 阶段两种动词效果相同(均为无惩罚清理),保持了语义一致性
- 硬控(眩晕/沉默)用
interruptsAbilities,玩家主动行为(翻滚/移动)用cancelsAbilities,职责清晰
为什么连招不作为 CastMode 原语
Section titled “为什么连招不作为 CastMode 原语”连招的本质是”多个行为按条件链接”。每段的消耗、打断容忍度、后摇通常不同。用独立 Ability + GrantTag 窗口串联,每段保持完整的 Ability 语义,TagRules 自然覆盖。
为什么切换型不作为 CastMode 原语
Section titled “为什么切换型不作为 CastMode 原语”切换型是”Instant Ability + 持续 Status”的直接组合。作为原语不增加表达力。
Recovery 阶段为什么不区分 interrupt 和 cancel
Section titled “Recovery 阶段为什么不区分 interrupt 和 cancel”ability.effect 已成功执行,“被打断惩罚”的语义不适用。无论何种外力终止后摇,玩家的核心诉求都是”尽快恢复行动自由”。统一为无惩罚清理,消除了策划的认知负担。
运行时结构参考
Section titled “运行时结构参考”class AbilityComponent { Actor owner; List<Ability> grantedAbilities; SafeList<AbilityInstance> activeInstances;
ActivateResult tryActivate(int abilityId); void tick(float dt);}
enum ActivateResult { Success; BlockedByTags; OnCooldown; InsufficientCost; ConditionFailed; MaxConcurrent; TargetInvalid;}
class AbilityInstance implements IPendingKill { Ability config; Context context; Actor owner;
AbilityPhase phase; float phaseElapsed; boolean isCommitted; boolean pendingKill;
void tick(float dt);
// 由输入/引擎系统调用 void releaseCharge(); // Charge: 玩家松手 void cancel(); // 玩家主动取消 / TagRules cancelsAbilities 驱动 void interrupt(); // TagRules interruptsAbilities 驱动}
enum AbilityPhase { Activating; Processing; Executing; Recovering; Ended;}