主题
行为问题排查 Skill
先按从配置到运行证据对齐安装版本、实际依赖和对局身份。报告分别标注已确认事实、待验证假设和临时绕法;不要将默认值或一次参数调整成功归纳为框架硬限制。
当前宿主支持时,先对目标单位发起 gameplay.snapshot,传 trace_seconds: 30 显式开启短期追踪;复现后发起新快照读取阶段变化和命令结果。原 request_id 的轮询不会重新采样。默认不追踪,每单位最多 32 条事件;需要取消调用来源时,再按下文订阅事件。对照 execution_target / aim_snapshot、范围来源、最近 AI 节点结果、实际技能和 Buff 已应用修改,避免先写项目探针或静态覆盖数编。
WasiCore 大多数子系统的关键状态变化都会同步派发事件。利用这个性质,在游戏脚本里订阅事件,回调里打 System.Environment.StackTrace,就能反向看出"是哪条代码路径改了这个状态" —— 即使引发它的逻辑藏在框架内部。
适用场景:
- AI 不动 / 攻击不出手 / 中途停下重来
- 技能没生效 / 中途被打断 / 重复入队后又消失
- Buff 提前结束 / 莫名被覆盖
- 单位莫名死亡或被清理
- 属性突变(HP / 攻击力等)找不到出处
- 任何"内部状态变了,但脚本上看不出是谁动的"
> 不适合用本 Skill 的场景:编译错误、API 用法不对、坐标算错、UI 布局问题等"静态可推导"的问题,先用 wasicore-dev / 3d-unit-game 之类的语义类 Skill。本 Skill 专攻"动态行为不符合预期"。
核心思路:事件 + StackTrace
响应先核对目标与生成结果
反伤、自疗、死亡复活“没生效”时,先按 response-system 区分原事件的 Attacker/Defender 与附加效果的 Caster/Target,并核对源 JSON、生成代码和实际挂载对象。枚举数字被省略、附加效果选中另一方,都可能造成无失败日志但没有预期结果;对活单位复活是无变化的成功调用。先确认实际执行目标,再用事件与调用栈追查动态问题。
先获取权威状态和隔离复现
涉及跨端状态变化时,先列出服务端最终状态、客户端可见且可操作的结果及失败后的恢复目标,并以同一调试局、玩家槽位和同步 ID 关联证据。权威状态正常只验证了服务端环节;报告应明确已验证的端与路径。跨场景后出现蓝底、血蓝缺失、技能空或镜头丢失时,先读 角色跨场景迁移,核对地图加载、英雄副本与主控,再按需要跟踪事件来源。
在编辑器双端调试局中,先按 debug-tools 的请求/轮询流程使用 gameplay.snapshot。用新局的 debug.ping.session_id、目标地图/玩家和报告的服务端 session/time 对齐证据,查看真实 XYZ、最终移动/转身速度、生命值、AI 目标及指令阶段。持续 Preswing 且朝向不变时,先核对转向条件与执行器;坐标相近不代表目标对象一致,结合 order identity、turn ownership 和目标身份判断。当前状态不能解释历史取消来源时,再订阅下面的事件。
测试追击时让目标位于扫描范围内、攻击范围外;扫描范围小于攻击射程的摆放只能检验原地攻击。暂时隔离防御塔或其他伤害源,避免怪物提前死亡干扰判断。源配置、生成代码、实际运行属性分别核对;样例数据缺项应修源数据,不据此认定 MCP 没有生成代码。
脚本中需要频繁判断场景模式时读取托管 Scene.Cache.SpaceModel;一次判断内复用 Position,避免重复跨 WASM 获取坐标。Surface2D 的平面距离和 Volumetric3D 的 XYZ 距离按具体 API 契约使用,不能从三维画面推断所有搜索/投射物碰撞都是三维。
AI 选敌顺序先检查配置
排查“为什么选了这个敌人”时,先核对排序与目标保持,不必立刻加全局日志:
GameDataAINodeValidateScan.TargetSorts未设置或为null:回退到AIThinkTree.Cache.ScanSorts的标签优先级。TargetSorts = []:显式不排序,不回退到标签规则。- 自定义比较函数完全覆盖默认排序。前一项返回
0才继续下一项;需要尊重标签时由公式主动读取thinkTree.Cache.ScanSorts,框架不会在公式后偷偷追加标签比较。 - 同优先级保留本次搜索结果顺序;不能把该顺序当作最近优先或跨帧稳定顺序。
RetainTarget保留合法旧目标时不执行排序;新出现的英雄或近处敌人不会自动抢走焦点。- 排序是优先级,不是候选过滤;“扇区内优先”仍可能选择扇区外目标。
GameSparkCore 的 默认战斗目标排序 是可编辑的 Validator:显式按树上标签优先级,再按二维中心距离选择。它已经挂在默认战斗树和战斗思考模板的 Scan 节点上。不要把默认公式的组合策略误认为所有自定义公式的强制行为。详见 AI 系统。
跟踪实际状态变化
- 找到状态变化对应的事件(见下表)。脚本里用
Trigger<TEvent>订阅。 - 在事件回调里
Game.Logger.LogInformation("... stack:\n{Stack}", System.Environment.StackTrace); - 因为事件是同步派发的(在 setter / publisher 里直接
Invoke),stack 上方是事件机制本身(set_Stage、publisher.Invoke、EventAggregator.Publish这一串),stack 下方就是真正的调用源 —— 比如某个Order.Dispose()←OrderQueue.Clear()←AIThinkTree.CombatClearUp()←LeftCombatThink(),一眼看穿是哪个框架路径触发了这次变更。
不需要也不能修改框架源码 —— SDK 用户拿到的是 DLL,但只要它内部加载了 PDB / 符号信息(Debug 配置下都有),Environment.StackTrace 仍能解析出框架内部的方法名和文件位置。
常见事件速查
| 现象 | 看哪个事件 | 关注什么字段 |
|---|---|---|
| AI 指令突然消失 / Stage 跃迁异常 | EventEntityOrderStage | Order, Stage, StagePrevious, Target |
| 单位被攻击 / 扣血 | EventEntityDamageTaken | 攻击者、伤害值、伤害类型 |
| 单位死亡 | EventEntityDeath | DeathType |
| 单位被清理 | EventEntityRemove | 看 stack 找清理调用源 |
| 单位位置变化 | EventEntityPositionChangeStart / End | IsMoverMove 区分 |
| 技能挂上 / 卸下 | EventAbilityAttached / EventAbilityRemoved | Ability link |
| 技能属性变 | EventAbilityPropertyChange | 属性名 |
| Buff 挂上 / 卸下 | EventBuffAttached / EventBuffRemoved | Buff link |
| Buff 属性变 | EventBuffPropertyChange | 属性名 |
| 单位属性变 | EventUnitPropertyChange / EventUnitPropertyComplexFinalChange | property link、old / new |
| 单位创建 / 复活 | EventUnitCreate / EventUnitRevive | 看 stack 找创建调用源 |
详细字段去查 docs/api/server/gamecore_event.cs(每个 Event 类都有完整签名)。
Pattern A:基本订阅 + StackTrace
csharp
// 服务端代码:在 OnGameTriggerInitialization 或单位 spawn 后注册
new Trigger<EventEntityOrderStage>((s, d) =>
{
if (d.Stage.InnerValue is EOrderStage.Left
&& d.StagePrevious.InnerValue is EOrderStage prev
&& prev != EOrderStage.Backswing // 正常打完后摇结束的不算
&& prev != EOrderStage.WaitToQueue) // 未入队就丢的不算
{
Game.Logger.LogInformation(
"Order {Prev}->Left, ability={A}, target={T}, stack:\n{Stack}",
prev,
d.Order.Command.AbilityLink?.ToString() ?? "(none)",
d.Target,
System.Environment.StackTrace);
}
return Task.CompletedTask;
}).Register(targetUnit); // 注册到具体单位,事件源由它发出注册目标决定事件作用域:
.Register(unit)— 监听该单位的事件.Register(gameDataUnit.Data)— 监听同类型所有单位的事件.Register(player)— 监听某玩家的事件.RegisterGlobal()/Game.Subscribe<T>(...)— 全局事件(EventGameStart、DualEndEvent等)
Pattern B:对象身份验证(Position 相等 ≠ 同一个对象)
打日志时直接读 target.Position 容易误判 —— 即使 ApproachTarget 已经 fallback 到 Owner(自己),它的 Position 也可能"看起来正常"。验证身份用 ReferenceEquals:
csharp
var approachIdent = ReferenceEquals(order.ApproachTarget, hero) ? "Hero"
: ReferenceEquals(order.ApproachTarget, ai) ? "Self"
: order.ApproachTarget?.GetType().Name ?? "null";对 Order 这种可能被复用 / 哈希被重写的类型,用 System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj) 拿引用身份,避免 Object.GetHashCode 被业务覆盖造成假阳性 / 假阴性:
csharp
// 用这个判断"是否还是同一个 Order",比 .Equals() 或 .GetHashCode() 稳
var stableId = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(order);Pattern C:现有事件不够用怎么办
绝大多数 framework 状态变化都有对应事件。如果找了一圈确实没有(比如想看 Command.IssueOrder 的入口 Target 是不是已经不对,而又没有相应的事件可订阅),先做下面这些:
- 退回到调用方加日志:你的脚本是在哪里下达指令 / 触发技能的?在那里
Environment.StackTrace+ 关键字段 dump 一遍,先确认离开你代码时数据是不是已经异常。 - 用相邻事件夹逼:比如
EventAbilityAttached紧邻技能挂载,结合EventEntityOrderStage紧邻指令入队,你能把"问题发生时间窗口"压到几毫秒级。 - 如果确认问题在框架内部、又找不到合适事件:复现路径 + log 截图发反馈给框架团队。SDK 文档里事件列表是会不断扩展的;常被反馈的"信号点"会被加成正式事件。
> 框架内部代码(GameCore.dll 等)对 SDK 用户是只读的,不要尝试反编译或 patch DLL —— 一是更新 SDK 时会被覆盖,二是不同版本的二进制差异会让你的工作完全白费。
实战案例:AI 追击中途卡顿
症状:默认 TacticalAI 追玩家时,每隔 ~2 秒卡一下再继续,玩家明显在 ScanRange 内也是如此。
排查:
- 加
EventEntityOrderStage订阅,发现Approach->Left来自 stack:set_Stage → Order.Dispose → Order.Destroy → OrderQueue.Clear(filter) → AIThinkTree.ClearUpAIOrders → AIThinkTree.CombatClearUp → AIThinkTree.LeftCombatThink ← !! → Task.set_Status (CombatDelay 到期) - 看到
LeftCombatThink来源是CombatDelay(InCombatMinimalDuration默认 2 秒)到期 → 框架内部把当前 Approach 指令一起清掉了。 - 把这个 stack 反馈给框架团队,框架那侧确认是回归 bug 并修复(行为树扫到敌人时刷新 CombatDelay)。
整套定位从"加事件 + 打 stack"到根因,约 30 分钟。没有事件 + StackTrace 这条路径,几乎只能逐行读 API 文档猜,或者在 SDK 用户和框架团队之间反复对话。
注意事项
- 事件必须同步派发才能拿到有效 stack。WasiCore 绝大多数 Entity / Unit 事件都是同步派发(在 setter / property change handler 里直接
Invoke(new Event(...)))。DualEndEvent是跨端异步,stack 不能传递。 - 用 Debug 配置跑。
Server-Debug/Client-Debug配置下框架 DLL 带完整符号,stack 帧最全。Release 配置可能 inline 部分方法,stack 看着会缺一两层。 - 不要在生产代码里留 StackTrace 日志。
System.Environment.StackTrace每次都会捕获完整调用栈,开销不小。问题查清就回滚相关 trigger,或加#if DEBUG围起来。 - stack trace 截断要警惕。如果
grep用了关键字过滤日志,stack 中间不含关键字的行会被滤掉,看上去像截断 —— 这是误读,直接cat/Read完整看。 - 事件触发频率高的要 dedupe。
EventEntityOrderStage在每个 Stage 跃迁都会触发,一个完整的攻击 cycle 就有 6~8 次。StagePrevious/Stage配合早期 return,只对真正关心的跃迁打 stack,否则日志会爆。