主题
指令、施法与技能动画的完整生命周期
移动、攻击、施法都通过指令系统执行。理解一个技能何时接近、转身、锁定目标、扣费、发动效果和结束表现,需要把服务端的 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| CreateBirth、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,分清正常等待、目标语义、效果配置和表现清理的问题。