Skip to content

行为问题排查 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 系统。

跟踪实际状态变化 ​

  1. 找到状态变化对应的事件(见下表)。脚本里用 Trigger<TEvent> 订阅。
  2. 在事件回调里 Game.Logger.LogInformation("... stack:\n{Stack}", System.Environment.StackTrace);
  3. 因为事件是同步派发的(在 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 跃迁异常EventEntityOrderStageOrder, Stage, StagePrevious, Target
单位被攻击 / 扣血EventEntityDamageTaken攻击者、伤害值、伤害类型
单位死亡EventEntityDeathDeathType
单位被清理EventEntityRemove看 stack 找清理调用源
单位位置变化EventEntityPositionChangeStart / EndIsMoverMove 区分
技能挂上 / 卸下EventAbilityAttached / EventAbilityRemovedAbility link
技能属性变EventAbilityPropertyChange属性名
Buff 挂上 / 卸下EventBuffAttached / EventBuffRemovedBuff link
Buff 属性变EventBuffPropertyChange属性名
单位属性变EventUnitPropertyChange / EventUnitPropertyComplexFinalChangeproperty 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 是不是已经不对,而又没有相应的事件可订阅),先做下面这些:

  1. 退回到调用方加日志:你的脚本是在哪里下达指令 / 触发技能的?在那里 Environment.StackTrace + 关键字段 dump 一遍,先确认离开你代码时数据是不是已经异常。
  2. 用相邻事件夹逼:比如 EventAbilityAttached 紧邻技能挂载,结合 EventEntityOrderStage 紧邻指令入队,你能把"问题发生时间窗口"压到几毫秒级。
  3. 如果确认问题在框架内部、又找不到合适事件:复现路径 + log 截图发反馈给框架团队。SDK 文档里事件列表是会不断扩展的;常被反馈的"信号点"会被加成正式事件。

> 框架内部代码(GameCore.dll 等)对 SDK 用户是只读的,不要尝试反编译或 patch DLL —— 一是更新 SDK 时会被覆盖,二是不同版本的二进制差异会让你的工作完全白费。

实战案例:AI 追击中途卡顿 ​

症状:默认 TacticalAI 追玩家时,每隔 ~2 秒卡一下再继续,玩家明显在 ScanRange 内也是如此。

排查:

  1. 加 EventEntityOrderStage 订阅,发现 Approach->Left 来自 stack:
    set_Stage → Order.Dispose → Order.Destroy
    → OrderQueue.Clear(filter)
    → AIThinkTree.ClearUpAIOrders
    → AIThinkTree.CombatClearUp
    → AIThinkTree.LeftCombatThink       ← !!
    → Task.set_Status (CombatDelay 到期)
  2. 看到 LeftCombatThink 来源是 CombatDelay(InCombatMinimalDuration 默认 2 秒)到期 → 框架内部把当前 Approach 指令一起清掉了。
  3. 把这个 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,否则日志会爆。