Skip to content

指令、施法与技能动画的完整生命周期 ​

移动、攻击、施法都通过指令系统执行。理解一个技能何时接近、转身、锁定目标、扣费、发动效果和结束表现,需要把服务端的 Order 与客户端的动画、Actor 生命周期放在一起看。本文先讲通用指令,再展开 GameDataAbilityExecute 的正常施法路径。

字段与配置示例见指令系统、技能系统和模型动画系统;实际调试可使用技能试验场。

配置是否接入、继承值是否生效,以及 AI、Buff 和表现的运行证据,见技能、AI 与表现排查指南。

1. Command、Order、Ability 分别负责什么 ​

对象含义生命周期
Command一次操作请求:做什么、向谁做、由哪个玩家请求、怎样排队描述输入意图,本身不代表操作已经完成
Order服务端执行这次请求的运行时对象从创建、入队到执行、取消或完成,每次执行有自己的阶段和结果
OrderQueue单位的指令队列决定哪条指令成为队首,以及新指令怎样覆盖、排队或请求抢占
Ability单位身上的技能实例可跨越多次施法,持有冷却、充能、等级等技能状态
动画控制器与 StagedSpawns客户端对施法的表现随施法阶段创建、推进、停止和清理
mermaid
flowchart TD
    Input["玩家输入 / AI / 服务端脚本"] --> Command["Command:操作意图"]
    Command --> Check["权限、目标、技能等入队检查"]
    Check -->|通过| Order["创建并提交运行时 Order"]
    Check -->|拒绝| Error["返回 CmdResult 错误"]
    Order --> Queue["OrderQueue 调度"]
    Queue --> Run["服务端执行移动或技能逻辑"]
    Run --> Stage["同步技能施法阶段"]
    Stage --> Presentation["客户端动画与阶段 Actor"]

服务端 IssueOrder 成功表示请求被接受,不表示施法已命中或已完成;瞬态指令可能在返回前就已执行结束。客户端调用成功只表示本地检查通过并已发送请求,最终由服务端判断。testOnly 用于检查当前条件,不入队执行,也不能保证稍后真正执行时仍然成功。

2. 普通指令如何入队和执行 ​

队列决定执行次序 ​

设置行为
默认,不带 Queued请求覆盖队列中允许被覆盖的指令;受当前阶段的打断规则和优先级约束
CommandFlag.Queued加入队列等待成为队首
CommandFlag.Preempt请求优先执行;是否能打断当前队首仍取决于打断规则
瞬态指令,不带 Queued直接走瞬态执行路径,不占用队列位置;具体效果仍可主动改变队列
瞬态指令,带 Queued先排队,成为队首后走瞬态执行路径

IsUser、IsAI 都隐含 IsRequest,需要对应玩家并接受控制权限检查。服务端系统脚本通常使用默认的 CommandFlag.None;需要模拟玩家请求时显式提供 Player 和请求标志。

队列的 Pause() / Resume() 成对使用,暂停的是队首调度,不会冻结已经开始的异步施法等待、移动或整个游戏时间。优先执行也不等于通用协程挂起和恢复,不能把任意指令当成可从中途继续的任务。

普通移动与瞬态操作 ​

不带 Queued 的瞬态指令可以在另一条移动、攻击或技能指令执行期间直接生效,无需等待当前指令完成,也不通过抢占来取得队首。 例如,单位正在向目的地移动时,可以立即发动瞬态护盾;单位正在引导技能 A 时,也可以立即发动瞬态技能 B 的效果。调度本身不会取消或暂停原来的移动、引导,原指令继续执行。

对于技能,在 GameDataAbilityActive.AbilityActiveFlags 中设置 Transient = true,并下达不带 CommandFlag.Queued 的指令,即使用上述方式。瞬态技能仍需通过自身的权限、目标、技能可用性、冷却和资源等检查;显式带 Queued 时则先排队。其效果若主动停止移动、清理指令队列或改变其他技能的执行条件,仍可能影响原指令,不能把“调度不打断”理解为效果绝不干扰。

普通 OrderMove 使用通用阶段管线,主要耗时发生在 Approach,到达后其余零时长阶段会很快推进。它不需要配置技能动画或技能 Effect。方向移动 OrderVectorMove 则是瞬态指令:执行时改变持续移动状态,指令结束不表示单位立刻停止移动。OrderStop 也是瞬态操作,其效果会请求清理可打断的队列指令。

瞬态路径跳过接近、等待冷却、前摇、出手和后摇的常规等待:

mermaid
flowchart TD
    Ready["立即执行 / 排队后成为队首"] --> Channel["设置 Channel 阶段"]
    Channel --> Validate["执行检查与扣费"]
    Validate -->|成功| Effect["执行效果"]
    Effect --> Done["Completed → Left"]
    Validate -->|失败| Cancel["取消 → Left"]

这里 Channel 阶段事件发生在最终执行检查、扣费之前。因此,对瞬态指令不能仅凭看到 Channel 事件就断定已经扣费或发动效果。

所有施法时长设为零,与 Transient = true 不等价。 前者仍经过普通管线的检查、首次前摇快照和阶段切换;后者使用上面的专用路径,不经过 Preswing。

3. 普通排队指令的完整阶段 ​

OrderStage 表示“当前走到哪里”,OrderState 表示“这次执行以什么结果结束”。运行中通常一直是 Normal;阶段可以前进,也可以因重新接近而回跳。

mermaid
flowchart TD
    Create["WaitToQueue:创建后等待入队"] --> Queue["Queued:队列等待"]
    Queue --> Approach["Approach:进入接近射程"]
    Approach --> Cooldown["WaitForCooldown:等待冷却条件"]
    Cooldown --> Preswing["Preswing:首次瞄准快照、转身与前摇"]
    Preswing --> Cast["Cast:停止接近和转身,等待出手时长"]
    Cast --> Retry{"启用 Reapproachable 且目标超出接近射程?"}
    Retry -->|是,仍可执行与接近| Approach
    Retry -->|否| Commit["最终执行检查、扣费"]
    Commit --> Channel["Channel:发动 Effect,等待引导结束"]
    Channel --> Back["Backswing:后摇"]
    Back --> Complete["Completed"]
    Complete --> Left["Left:销毁指令并清理"]
    Commit -->|检查或扣费失败| Cancel["取消"]
    Cancel --> Left

上图是可执行技能的正常排队路径。各等待阶段还可能因目标失效、路径失败、技能被禁用或允许的打断而提前退出;退出统一进入 Left。普通指令复用这条管线,但可以没有冷却、动画、效果或阶段耗时。

阶段可执行技能中的职责
WaitToQueue已创建,尚未正式加入队列
Queued等待成为队首
Approach向 ApproachTarget 接近;已经在射程内则直接通过
WaitForCooldown检查或等待冷却条件;能否在冷却中接受指令由 AllowEnqueueInCooldown 等配置决定
Preswing首次锁定需要快照的目标;必要时发起转身,等待前摇及允许的朝向误差
Cast停止本指令的接近与转身,等待出手时间;此时尚未执行配置的主 Effect
Channel最终检查和扣费成功后进入;执行主 Effect,等待引导时间及支持引导的效果结束
Backswing后摇收尾,可按配置被后续指令打断;连续施法可在此请求下一次施法
Left离开指令,停止相关接近、转身和引导,解除事件监听并移出队列

Left 是清理阶段,成功、取消和中止都可能到达。指令销毁后不要继续通过 Owner、Queue、IsHead 查询运行中的队列关系。

结果、取消与打断 ​

OrderState含义
Normal尚未结束,包括排队、接近、施法及重新接近
Canceled未正常完成而被取消
Aborted调用 Cancel() 时正在 Channel,引导被中止
Completed正常完成,或调用 Cancel() 时已在 Backswing

具体到 Cancel():进入 Channel 前取消得到 Canceled;在 Channel 取消得到 Aborted;在 Backswing 取消视为 Completed。队列覆盖、直接 Destroy() 等路径不完全等同于调用 Cancel(),应读取最终 State,不要仅凭退出时所在阶段自行推断结果。

技能各阶段能否被新指令打断由 SpellInterruptConfig 控制。销毁会停止仍关联的引导和表现,但已经产生的伤害、已创建的独立效果或已花掉的资源不会自动回滚。

4. 接近、转身与目标快照 ​

一条技能指令同时涉及三个不同概念:

目标用途
Command.Target原始输入,可以是实体、场景点、角度或空
ApproachTarget接近与射程判断依据
ExecutionTarget实际发动技能效果的目标;可执行技能转身也朝向它

首次 Preswing 是锁定辅助瞄准的时刻 ​

点、向量技能可以通过在 Command.Target 中传入实体对象进行辅助瞄准。例如,技能的 TargetType 配置为 Ground 或 Vector 时,仍可将要瞄准的单位 targetUnit 赋给 Command.Target:

csharp
#if SERVER
// abilityLink 指向已配置为 Ground 或 Vector 的可执行技能。
var command = new Command
{
    AbilityLink = abilityLink,
    Target = targetUnit, // 传入实体,由框架在首次前摇开始时取得瞄准快照
};
var result = command.IssueOrder(caster);
#endif

这里技能仍是点或向量目标类型,传入实体只是提供辅助瞄准输入,不会将技能改成单位目标技能。如果改为 Target = targetUnit.Position,传入的则是构造指令当时取得的场景坐标点,无法表达“首次起手前跟随该实体,再在首次前摇开始时取快照”的语义。

在第一次进入 Preswing 的流程中,框架先取得实体的瞄准快照,再发起转身及发布前摇阶段事件。第一次起手前,接近阶段仍可跟随这个实体;起手后按下表处理。

技能目标类型与输入第一次起手之后
Unit + 实体保留实体目标,读取它的实时位置
UnitOrGround + 实体保留实体目标,读取它的实时位置
UnitOrGround + 场景点按输入的固定点执行
Ground + 实体辅助瞄准锁定实体当时的场景坐标点
Vector + 实体辅助瞄准锁定实体当时的瞄准点,同时锁定由该方向和射程计算的执行落点
Interact目标是技能宿主实体,保留宿主身份及实时位置;施法者可能是另一单位

实体辅助瞄准的快照规则也覆盖其他非 Unit、非 UnitOrGround、非 Interact 类型,但实际执行目标仍按各类型定义解释;例如无目标技能通常作用于施法者自身。直接输入角度不属于“实体辅助瞄准快照”的情况。

向量的瞄准点与执行落点可能不同。 向量技能的 ExecutionTarget 通常是从施法者沿瞄准方向按 Range 投影的点,并非直接使用被瞄准实体的位置。首次前摇缓存后,同一指令重试或施法者随后发生位移,都不会重新从该实体计算执行落点。AlwaysAcquireTarget 还决定向量等技能是否需要向瞄准点接近;关闭时 ApproachTarget 可为施法者自身。

快照用于本条指令的瞄准,不会清除原始 Command.Target,也不意味着目标实体删除、跨场景后所有有效性检查都会被跳过。

前摇时间结束不一定立刻出手 ​

进入前摇流程时,若朝向不满足 Arc,框架请求转身。前摇计时结束后,还会检查 Arc + ArcSlop;没有达到允许误差时继续等待转身。因而“预警出现了但效果尚未发动”可能是仍在等待朝向,不一定是在等待射程。

Cast 开始时会停止这条指令的接近和转身。对于实时实体目标,之后执行时仍可读取目标的新位置,但这不意味着单位在整个出手和引导期间持续旋转追踪它。

接近射程也不等于技能的最终伤害区域:判断会考虑 Range、双方交互半径和距离容差;Surface2D 使用 XY 距离,Volumetric3D 使用 XYZ 距离。扇形 AoE 的角度、半径和筛选对象由效果链决定。

5. Reapproachable:出手后、引导前重新接近 ​

设置 AbilityExecuteFlags.Reapproachable = true 后,完成 Cast、尚未扣费和进入 Channel 时,框架再次检查接近射程。仍在范围内就继续;超出范围且仍能执行、能够接近时,同一条 Order 回到 Approach。

mermaid
flowchart TD
    Cast["Cast 完成"] --> Range{"仍在接近射程?"}
    Range -->|是| Commit["最终检查、扣费、Channel、Effect"]
    Range -->|否| Valid{"执行条件有效且可以接近?"}
    Valid -->|否| Cancel["取消并清理"]
    Valid -->|是| Clean["停止本轮动画和阶段 Actor,重置本轮计时"]
    Clean --> Approach["Approach"]
    Approach --> Cooldown["WaitForCooldown"]
    Cooldown --> Preswing["重新完整 Preswing,保留第一次瞄准快照"]
    Preswing --> Cast
  • 回跳仍是同一条指令,State 保持 Normal;可以多次回跳,每轮重新执行完整前摇和出手。
  • 重试本身不扣技能资源、不开始技能冷却、不执行主 Effect;通过检查后的正常 Channel 流程才提交消耗和发动效果。
  • 实体目标技能继续追踪实体;点、向量技能的实体辅助瞄准快照始终保留。回跳不会重新瞄准已经跑开的玩家。
  • 返回 Approach 后使用接近阶段的打断规则;路径失败、目标失效、资源或其他执行条件不满足时取消,不会无条件永久追逐。
  • 一旦进入 Channel 就不再使用这个回跳机制。

关闭 Reapproachable 不表示“目标出范围就必定取消”。当前普通执行检查允许可移动施法者继续执行,单位目标可能在前摇期间离开射程后仍被施法;不能移动的施法者则可能因范围检查失败而取消,其他目标、消耗、效果校验也仍会生效。

DoRecast 与重新接近不同:连续施法会请求新指令,新指令首次前摇时重新取得快照。不要把 Reapproachable 的同指令回跳当作新的一次扣费施法。

自定义 Preswing / Cast 回调可能在同一指令中触发多次。回调里手工扣费、造成伤害、创建 Actor 或写入自定义标记的行为,不会随回跳自动撤销;需要自行处理重复起手。

6. 技能动画如何跟随服务端阶段 ​

服务端推进施法阶段,客户端收到技能阶段后驱动表现。伤害和资源提交由服务端施法流程决定,动画片段结束或客户端 Sequence 事件不会自动替代服务端的主 Effect 发动时机。网络同步也不保证两个端在完全同一帧看到阶段切换。

施法阶段到动画控制器的映射 ​

收到的阶段客户端动作
Preswing停止旧控制器,清理旧阶段 Actor;从 Animation 列表中随机选一个配置创建本轮控制器,调用 Preswing(),再处理前摇的阶段 Actor
Cast确保控制器存在,调用 Cast(),处理出手阶段 Actor
Channel确保控制器存在,调用 Channel(),处理引导阶段 Actor
Backswing调用 Backswing(),处理后摇阶段 Actor
Approach 回跳停止并释放本轮控制器,清理受阶段管理的 Actor
Left停止并释放控制器,清理剩余受阶段管理的 Actor

这条表现路径依赖配置了动画或 StagedSpawns,且具有可用的施法总时长。逻辑阶段即使在零时长下也能快速推进,但不应依赖零总时长施法展示可见的蓄力或预警。排队和等待冷却也不会自动播放前摇动画。

三种内置动画有各自的生命周期 ​

动画配置如何播放阶段回调的意义
普通 Animation首次阶段通知时播放单个动画,按施法总时长适配没有独立的三段跳转;通用阶段通知不会自动把单片段拆成前摇、出手、后摇
AnimationBracket维护自己的 Birth → Stand → Death → Stopped 生命周期出手时可从 Birth 进入下一段,后摇时请求收尾;按施法时长调整对应段的播放
AnimationSequence首次阶段通知时启动,按配置的片段、事件与时间轴推进,并适配施法总时长通用阶段回调不会跳转序列时间轴;关键帧应在序列中编排
mermaid
flowchart TD
    Preswing["客户端 Preswing"] --> Create["创建本轮动画控制器"]
    Create --> Start["收到 Preswing():启动播放"]
    Start --> Simple["普通 Animation:播放单片段"]
    Start --> Bracket["Bracket:Birth → Stand → Death"]
    Start --> Sequence["Sequence:按自身时间轴播放"]
    Simple --> Stop["Approach 回跳 / Left:Stop 并释放"]
    Bracket --> Stop
    Sequence --> Stop
    Stop -->|同指令再次 Preswing| Create

Birth、Stand、Death 是动画元组自己的阶段名称,不能直接等同于 Preswing、Channel、Backswing。取消和重新接近走停止路径,不保证先播完正常收尾段。Sequence 停止时会停止其时间轴;序列内部生成对象的行为还需遵循对应序列配置,不能用 StagedSpawns 的清理规则代替。

对动画元组,完整状态还包括未开始的 None 和停止后的 Stopped。技能集成在启动播放、出手、引导及后摇时按当前元组阶段适配时长:Birth 对应前摇时间,Stand 对应出手与引导之和,Death 对应后摇时间。出手或引导通知只在当前仍为 Birth 时请求推进;后摇通知请求正常收尾。片段缺失、片段自行结束和停止方式仍受元组配置控制,不能假定每个元组都会完整播放三段。

重新接近后会创建新一轮控制器,Sequence 从新一轮开始,不从上次中断帧续播。动画列表是随机候选,重试时可能选中另一个候选。动画停止后是否、怎样切回移动或待机表现,取决于单位的其他动画和混合配置。

扩展:自定义技能动画数据与控制器 ​

上面的三种是内置实现。用户可以继承 GameDataAnimation,在客户端通过 override 重写 CreateSwingController(IActorAnimationPlayable animationPlayable, TimeSpan? totalTime),返回自行实现的 IAnimationSwingController,构造自己的技能动画数据和表现状态机。将自定义动画数据的 Link 加入技能的 GameDataAbilityActive.Animation 列表,即可接入同一套施法阶段通知。

GameDataAnimation 位于 GameCore.ResourceType.Data,IAnimationSwingController 位于 GameCore.ModelAnimation。创建控制器的方法及接口成员仅客户端可用,实现时放在 #if CLIENT 中。animationPlayable 是本次播放的宿主;totalTime 是可选的施法总时长,如何适配到自定义动画由实现决定。

接口成员自定义控制器应处理的含义
IsValid控制器是否仍可使用;无效时框架可在后续阶段尝试重新创建
Preswing()创建本轮控制器后收到前摇通知,可在此启动自定义前摇表现
Cast()收到出手阶段通知,推进自定义出手表现;此时尚未进入引导
Channel()收到引导阶段通知,推进自定义引导表现
Backswing()收到 Backswing 通知,推进自定义收尾表现
Stop(TimeSpan? fadeOutTime)停止并收尾本轮控制器,清理自行持有的播放、监听和临时对象;应允许重复调用
Pause(bool on)实现控制器自身的暂停与恢复;技能阶段推进不会自动在每个阶段调用它

CreateSwingController 只创建控制器,不应立即播放;Preswing() 负责启动前摇,随后按服务端阶段依次收到 Cast()、Channel()、Backswing()。三类内置数据都遵循这一约定,未开始播放的控制器仍是有效实例;提前 Stop() 后不会再启动。Preswing() 和 Channel() 在接口上提供默认空实现,自定义控制器应按自身需要覆盖。框架不会强制等待这些表现播放完毕才进入下一施法阶段。

如果客户端首次收到的已经是后续阶段,或旧控制器失效后在后续阶段重新创建,只通知当前阶段,不补发先前的前摇和出手。内置控制器也会在首次收到的阶段启动播放:Bracket 在 Cast 或 Channel 从 Birth 推进到 Stand,在 Backswing 请求进入 Death;普通动画和 Sequence 从各自配置的起点播放,不根据错过的阶段自动跳转时间轴。自定义控制器应能处理首次通知为后续阶段的情况。

重新接近的 Approach 与退出的 Left 都会调用停止路径,下一轮前摇重新创建控制器。因此运行中的状态应属于每次创建的控制器实例,自定义数据对象不应混存不同施法者、不同施法轮次的播放状态。自行创建且未交给 StagedSpawns 管理的对象,也应自行清理。

自定义的是客户端表现状态机,仍由服务端指令决定扣费和主 Effect 的发动;控制器播放完毕不会自动推进服务端施法阶段。

施法时间与动画时长 ​

SpellCastTime 配置 Preswing、Cast、Channel、Backswing 四段时间。NormalizedDuration 归一化的是前三段之和,后摇按相同比例缩放,但不计入这个和。

例如原始四段为 0.3 / 0.2 / 0.5 / 0.4 秒,归一化前三段到 2 秒后,变为 0.6 / 0.4 / 1.0 / 0.8 秒,动画适配的总时间为 2.8 秒。前三段总时长接近零时有均分归一化时长的特殊处理,不应使用上述除法推导。

施法总时长不是从下令到完成的墙钟时间:它不含排队、接近、冷却等待、额外转身等待、重新接近重试,以及支持引导的效果可能带来的额外等待。

7. StagedSpawns:预警和其他阶段表现 ​

StagedSpawns 是通用的阶段 Actor 生成机制,可用于扇形预警、蓄力、光环和其他施法表现。它与主 Effect 分工不同:Actor 负责客户端可见表现,效果链负责实际搜索目标、伤害、Buff 等玩法逻辑。看到预警并不代表主效果已经发动。

每项配置包含 Spawns、SpawnStage 和 ClearStage:

时刻对受阶段管理的非瞬态 Actor 的处理
到达 SpawnStage创建 Actor,登记到对应清理阶段
到达 ClearStage销毁登记在该阶段的 Actor
回跳 Approach清理本轮全部阶段 Actor
再次 Preswing清理旧记录,重新创建本轮应有表现
Left清理剩余阶段 Actor;该阶段用于退出清理,不创建新的阶段 Actor

同一阶段的处理顺序是先创建、再清理。若 SpawnStage 与 ClearStage 相同,非瞬态 Actor 刚创建就会被清除,不能形成持续可见预警。普通 IsTransient Actor 自行结束,不纳入这套清理登记;在阶段回调中手工创建的对象也需要自行管理。

示例:固定方向的扇形 AoE ​

假设施法以实体辅助瞄准,目标类型为 Ground 或 Vector:前摇 0.6 秒、出手 0.2 秒、引导 0.1 秒、后摇 0.3 秒,关闭归一化,不发生额外转身等待或重试。

相对首次起手的时间服务端行为客户端表现配置
0.0s首次 Preswing,取得瞄准快照扇形预警 SpawnStage = Preswing
0.6s进入 Cast,停止本指令转身预警继续显示
0.8s检查、扣费、进入 Channel,执行 AoE 主效果预警 ClearStage = Channel;可另建引导阶段冲击表现
0.9s进入 Backswing播放收尾或清理配置在此结束的表现
1.2s正常完成,进入 Left清理剩余阶段表现

玩家跑出预警后,这条指令仍面向第一次起手锁定的目标点;效果按配置的固定区域搜索当时实际位于其中的单位。扇形 Actor 的朝向、挂接点和效果链搜索区域需要作者配置一致,StagedSpawns 本身只管理创建与清理,不会自动推导 AoE 几何范围。

若改为 Unit 目标,或者给 UnitOrGround 传入实体,执行目标会实时跟随该实体。启用 Reapproachable 后,单位在提交前跑出接近射程可触发重新接近,此时本轮预警清理,下一次起手重新显示。

8. 阶段事件与排查方法 ​

EventEntityOrderStage.Stage 表示即将切换到的新阶段。事件在阶段字段更新前同步发布,因此应在回调当场读取 StagePrevious、当前目标等值;需要异步处理时先保存所需信息,不要稍后再从事件对象推断当时的旧阶段。回调也可能取消指令,后续逻辑应检查指令是否仍有效。

阶段事件可能重复出现在同一条指令中,单位宿主移除等清理路径也不应只依赖收到一次 Left 事件完成所有自定义清理。为自建表现和监听提供独立的生命周期收尾。

现象优先核对
预警出现,玩家移动后技能迟迟不发动服务端停在 Preswing 还是已经进入 Cast;前摇朝向等待、目标类型、首次快照和转身能力
Boss 跟着玩家重新走位并再次预警是否启用 Reapproachable;是否为实时实体目标;是否同一条 Order 的正常回跳
前摇一结束就误以为应该造成伤害主 Effect 在正常路径的 Channel 发动,之前还有 Cast 和最终检查
动画结束了,伤害尚未出现服务端所处阶段、额外等待、动画类型及 Sequence 自身时间轴;不能以动画结束代替效果时机
回跳后旧预警仍然存在Actor 是否为普通瞬态对象、手工创建对象或序列内部对象;是否确实由 StagedSpawns 管理
预警只闪一下SpawnStage 与 ClearStage 是否相同;阶段是否零时长或立刻被取消
同一轮重试多次扣费或造成伤害是否在可重复的 Preswing / Cast 回调中自行执行了玩法逻辑

排查时同时记录指令身份、Stage、State、原始输入目标、接近目标和执行目标。先确认服务端在等待什么,再核对客户端动画与 Actor,分清正常等待、目标语义、效果配置和表现清理的问题。