Skip to content

从现象定位问题并做回归 ​

建立静态契约、真实编辑器试玩、日志归因、截图和回归测试证据链。

什么时候查这篇 ​

玩法或界面与预期不同,或准备交给其他作者安装时,使用本专题。按当前需求选择小节即可,不必按专题编号顺序读完。

  • 开始前:能重现一个具体问题并找到当前试玩输出;不会 Lua 可先看现象排错页。
  • 安装与示例范围:日志片段用于观察;诊断函数不改变分数。CLI 命令是可选工具,只在已绑定的专用教程工程运行,首次学习无需安装自动化平台。
  • 本次要看到:写清一条复现操作、预期与实际结果;修复后重复同一操作并检查相邻功能。

如果还没有完成可运行的小游戏,先回到主线:10 课。语法卡住时查Lua 速查,运行结果不符时查按现象排错。

你会学到什么 ​

  • 如何用期望日志定位玩法链路。
  • 如何按 editor-cli 跑测闭环验证修改。
  • 如何用 play session 精确读取日志,并在结束后形成完整证据。
  • 如何区分运行时错误、业务日志和 UI 截图问题。
  • 如何用 EUI testspec + 截图做回归测试。

如何阅读本专题 ​

本专题不是要求你一次搭完完整测试平台。初次排错先掌握“写下预期、重新试玩、复现、读本次日志、修复后重跑”;当项目开始接入 UI、存档、资产或平台服务时,再回来看 testspec、截图审查和可复现测试记录。

如果你正在排一个具体 bug,可以直接从“复杂链路分层定位”开始;如果你准备发布或交给别人评审,再补齐“跨模块失败复盘清单”和“可复现测试记录”。需要临时定位时先看诊断片段,需要重复验证时再读 CLI 与 testspec。

QA 的基本心智 ​

写完代码不等于功能完成。你需要证明三件事:

证明目标证据
脚本没有运行时错误editor-cli log trace --play-session ... 没有语义错误
玩法链路按预期发生期望日志按顺序出现
UI 真的显示正确testspec、断言日志、试玩中截图审查都通过

手工排错按“重新试玩 → 复现 → 读当前输出 → 修改 → 重跑”即可。下文的 ready、session 与 idle 检查适用于 CLI 自动化,不能省略,但不作为手工入门的前置。

日志文件的实际路径由 editor-cli status 的 log_file 返回,不要假定项目根目录一定有 log.txt。日志可以在试玩中按 session 查询;停止试玩的意义是关闭本轮时间窗口、释放运行态并确保下一轮从 idle 开始,而不是“只有停止后日志才可读”。

日志规范 ​

好的日志不是越多越好,而是能证明关键节点发生了:

示例类别:解释片段;运行端:server。 只定义日志函数,由已有回合和计分回调传入真实值后调用。

lua
-- @runtime server
local function LogRoundStarted(gameEndTime)
    if type(gameEndTime) ~= "number" then
        print("[SE Lua Guide][WARN] 回合结束时间无效")
        return
    end
    print("[SE Lua Guide][Round] started endTime=", gameEndTime)
end

local function LogScoreChanged(player, score)
    if player == nil or not player:IsA("Player") or type(score) ~= "number" then
        print("[SE Lua Guide][WARN] 计分日志参数无效")
        return
    end
    print("[SE Lua Guide][Score] player=", player:GetName(), "score=", score)
end

规则:

  • 使用统一前缀,例如 [SE Lua Guide]。
  • 把模块和动作写进固定标签,例如 [Round] started,方便精确检索;不要只靠自然语言句子。
  • 成功路径和失败路径都要有日志。
  • Heartbeat 中必须节流,不能每帧刷屏。
  • 临时诊断日志解决问题后要删除,保留关键业务日志。
  • 不要记录预留服务器访问码、订单凭据、完整存档或其他敏感数据。

期望日志驱动开发 ​

写功能前先写“我期待看到什么日志”:

text
[SE Lua Guide] 回合开始
[SE Lua Guide] 碰撞检测触发
[SE Lua Guide] 玩家识别成功: <name>
[SE Lua Guide] 当前得分: 1
[SE Lua Guide] 回合结束

运行后对比实际日志:

缺哪条日志常见原因
回合开始初始化没执行、main.lua 未挂载、require 路径错
碰撞检测触发对象没找到、事件没绑定、碰撞/触发配置错误
玩家识别成功otherUnit 不是角色,需要检查碰撞对象
当前得分服务端计分逻辑没走到,或被防抖/校验拦截
UI 更新RemoteEvent 未定义一致,或 client 节点没找到

复杂链路分层定位 ​

复杂功能出问题时,不要只问“是不是代码错了”。先把链路拆成可观察的层:

层典型证据常见问题
场景对象启动日志打印对象名、GetFullPath()、Position命名不一致、对象未加载、父节点不对
server 事件OnCollisionEnter / OnServerEvent 进入日志碰撞配置错、客户端没发请求、事件名不一致
server 判定校验通过/拒绝日志、分数变化日志防抖过严、玩家识别失败、权限校验漏了
RemoteEvent 下发FireClient / FireAllClients 前后的日志目标玩家错、单个 table payload schema 不一致
client 接收OnClientEvent 进入日志client 脚本未加载、require 路径错
UI 表现节点存在日志、截图、testspec仍依赖旧节点映射、节点 Name 不稳定、布局遮挡
存储/排行pcall 成败、默认值、最终榜单Async 裸调用、GetAsync + SetAsync 覆盖、OrderedDataStore 值非整数

比如“技能命中后 UI 不更新”,应该先证明 server 已判定命中,再证明 server 已下发事件,再证明 client 收到事件,最后才看 UI 节点。这样能避免在 UI 里改半天,结果根因是服务端根本没有通过冷却校验。

失败记录模板 ​

每次修复杂问题,建议留下一个短记录,方便回归:

md
### 失败记录:碰撞计分不增长

- 复现步骤:启动试玩,玩家推动「球体」撞「方块」。
- 期望结果:服务端打印“碰撞检测触发”,分数从 0 变 1,HUD 更新。
- 实际结果:只看到“碰撞检测触发”,没有“LastOwnerUID”。
- 分层定位:server 碰撞事件已进入;玩家识别层失败。
- 根因:球体碰到方块前没有触碰角色,`LastOwnerUID` 未写入。
- 修复:在球体触碰对象先确认 `otherUnit:IsA("EggyUnit")`,再用 `Players:GetPlayerFromCharacter(otherUnit)` 写属性。
- 回归:重跑同一流程,日志出现“加分后: 1”,截图中 HUD 显示 1。

失败记录不是给别人看的仪式感,它能迫使你区分“已验证事实”和“猜测”。当同类问题第二次出现时,你也能直接复用分层定位路径。

跨模块失败复盘清单 ​

正式公开前,建议每个 Part 至少补一条真实失败复盘。复盘不要求问题很大,但必须有复现步骤、日志或截图、定位过程和重跑结论。

范围建议复盘问题关联专题证据落点
Part 1对象找不到、碰撞不触发、Timer 重复触发02、05、06evidence/editor-runs/
Part 2RemoteEvent 单载荷 schema 不一致、UI 节点 Name 不一致、DataStore 失败 fallback08、09、11evidence/editor-runs/ + evidence/screenshots/
Part 3动画播放了但技能没判定、相机 Raycast 方向写错14、15evidence/editor-runs/
Part 4custom:// 加载失败、preset 忘记 Parent17、18evidence/editor-runs/
Part 5平台 Async 裸调用、Capstone 中途加入 HUD 不更新20、22evidence/editor-runs/ + evidence/screenshots/

这些复盘会让教程从“展示正确写法”升级为“教读者如何把错误修到正确”。如果只能先补一条,优先补完成小游戏后选择扩展挑战 Capstone 的中途加入或 UI 节点缺失,因为它同时覆盖 server、client、RemoteEvent、EUI 和 QA。

editor-cli 跑测闭环 ​

editor-cli 用于选择编辑器实例、做只读预检、控制试玩、注入临时代码、读取 session 日志和截图。当前默认安装位置是用户目录下的 .eggitor/cli/editor-cli.exe。以下 PowerShell 先保存路径和目标实例;如果同时开了多个编辑器,优先用 PID 或 HTTP port,避免同名地图选错:

powershell
$editorCli = Join-Path $env:USERPROFILE ".eggitor\cli\editor-cli.exe"
& $editorCli editor-instances list --json

# 替换成上一步返回的准确 map_name、pid 或 http_port。
$editorTarget = "your-map-name-or-port"
& $editorCli --editor-instance $editorTarget status --json

1. 先做只读预检 ​

powershell
& $editorCli --editor-instance $editorTarget preflight `
    --capabilities connection,map,play,screenshot `
    --expect-state idle --json

只有 JSON 中 ready 为 true 才进入下一步。preflight 不会启动/停止试玩、保存地图或同步代码。

如果本轮还要同步 Lua 工程,先额外检查 code-sync 并查看双向 diff;发现 only-local、only-map 或 differs 时先确认所有权,不得机械 push / pull 覆盖他人改动:

powershell
& $editorCli --editor-instance $editorTarget code diff `
    --workspace "path-to-your-lua-workspace" --json

2. 开始试玩 ​

powershell
& $editorCli --editor-instance $editorTarget play start `
    --wait-ready --wait-timeout 30 --json

保存返回的 play_session。--wait-ready 会等待游戏运行时实际执行 Lua 探针;没有这个参数时,成功只代表启动请求已提交。不要用固定 sleep 猜测地图是否加载完成。

3. 复现问题 ​

按本次测试目标操作:

测试目标操作
碰撞计分推球撞方块
UI 更新触发服务端分数变化
按钮请求点击按钮
存档完成一局后离开或触发保存

4. 试玩中按 session 查日志和截图 ​

powershell
$playSession = "play-start-returned-session"
& $editorCli --editor-instance $editorTarget log grep "\[SE Lua Guide\]" `
    --play-session $playSession --max-matches 200 --json
& $editorCli --editor-instance $editorTarget log trace `
    --play-session $playSession --json
& $editorCli --editor-instance $editorTarget screenshot game `
    --output "evidence\screenshots\qa-current.png" --no-clobber --json

截图必须在游戏窗口仍处于试玩状态时采集。log grep 负责查期望 marker,log trace 负责提取语义错误;不要只搜一两个英文错误片段就断言“没有错误”。

5. 停止试玩并确认 idle ​

powershell
& $editorCli --editor-instance $editorTarget play stop --json
& $editorCli --editor-instance $editorTarget status --json

play stop 成功只确认停止请求。继续轮询 status,直到同时满足:

text
edit_mode = true
in_game_runtime = false
editor_state = idle

6. 复核本轮完整日志 ​

停止后继续用同一个 play_session 做最终复核,避免上一轮或编辑器启动日志污染结论:

powershell
& $editorCli --editor-instance $editorTarget log trace `
    --play-session $playSession --json
& $editorCli --editor-instance $editorTarget log grep `
    "\[SE Lua Guide\]\[(WARN|ERROR)\]" `
    --play-session $playSession --max-matches 200 --json

需要人工浏览时可再检索:

text
attempt to
stack traceback
not found
encoding is not valid
[SE Lua Guide][WARN]

但发布结论应保存结构化命令结果、session ID、期望 marker 和截图,不要只写“看过日志没问题”。

7. 修复并重跑 ​

每轮只改和诊断一致的最小范围。修复后重新执行完整闭环,不要只凭代码看起来合理就结束。

text
preflight ready → play start --wait-ready → 复现 → session 日志/截图 → play stop → idle → 最终复核 → 修复 → 重跑

试玩中执行临时代码 ​

试玩运行时可以通过 editor-cli 注入 Lua 查询状态:

powershell
& $editorCli --editor-instance $editorTarget exec `
    "print('[SE Lua Guide][QA] probe=hello')" `
    --runtime server --json

& $editorCli --editor-instance $editorTarget log grep `
    "\[SE Lua Guide\]\[QA\] probe=hello" `
    --play-session $playSession --platform server --json

注意:

  • exec --runtime server/client 是异步提交;命令成功不等于片段后来执行成功。
  • 用唯一 marker 写入日志,再按本轮 play_session 回读;查到 BEGIN 而没有 END 也不能算通过。
  • exec 能执行任意 Lua,属于变更操作。注入前先审查副作用,数据存储、交易、传送和分析上报只在隔离环境执行。
  • 复杂代码优先写入临时 UTF-8 文件,再使用 exec --file ...,避免多层引号转义错误;临时文件放项目 tmp/。

pcall:包住可能失败的操作 ​

网络、资源加载、数据存储、排行榜等都可能失败。用 pcall 捕获错误,避免脚本中断:

示例类别:沙盒接入片段;运行端:server。 只定义读取函数,使用本页隔离存储名并由调用方传入测试 key;没有专用存储环境时只阅读。

lua
-- @runtime server
local DataStoreService = game:GetService("DataStoreService")
local Task = game:GetService("Task")
if DataStoreService == nil or Task == nil then return end

local store = DataStoreService:GetDataStore("Tutorial_QA_Read_v1")

local function ReadForQa(key)
    if type(key) ~= "string" or key == "" then return end

    Task:Spawn(function()
        local ok, result = pcall(function()
            return store:GetAsync(key)
        end)

        if ok then
            print("[SE Lua Guide][QA] datastore_read=success")
        else
            print("[SE Lua Guide][WARN] datastore_read=failed error=", result)
        end
    end)
end

不要用 pcall 吞掉所有错误后什么都不打印。失败日志是 QA 的证据;同时不要把读取到的完整玩家数据打进日志。DataStore 具有外部副作用和真实数据风险,这段只在隔离测试存储中做行为验证,普通地图仅做契约校对与编译。

LogService ​

LogService 可用于测试代码监听运行日志:

示例类别:独立实验;运行端:client;文件:client/main.lua。

lua
-- @runtime client
local LogService = game:GetService("LogService")
if LogService == nil then return end

LogService.MessageOut:Once(function(message, messageType)
    print("[SE Lua Guide][LOG]", messageType, message)
end)

LogService:Info("qa_probe", {
    module = "tutorial",
})

普通玩法代码不一定需要监听 LogService。自动化测试、错误收集、调试工具更常用它。这里必须用 Once:如果用永久 Connect,又在回调里继续 print,新日志可能再次触发同一个回调,形成递归日志风暴。长期监听器应避免在回调里再次产生日志,并在销毁时断开连接。

EUI testspec + 截图审查 ​

UI 测试不能只看断言。断言全部 PASS,也可能出现文字被裁切、按钮偏位、颜色看不清等问题。

推荐顺序:

  1. 读取 test/testspec_{module}.md,确认节点、交互和设计意图。
  2. 启动试玩并注入测试。
  3. 按本轮 play_session 轮询日志,直到出现 [TEST:END] 或错误。
  4. 试玩仍运行时截取 game 窗口;否则只能得到编辑态画面或截图失败。
  5. 先检查断言与 log trace,确认测试真正跑到 END 且没有语义错误。
  6. 停止试玩并确认 idle,再用同一 session 做最终日志复核。
  7. 查看刚才保存的截图,逐项检查视觉;日志 PASS 不能替代视觉验收。

截图审查至少检查:

检查项判定标准
文字可读性不被遮挡,对比度足够
布局合理性居中/贴边/对齐符合 testspec
无异常重叠元素没有意外互相盖住
无裁切异常文字和图片没有被截断
颜色与样式状态色、透明度、禁用态正确
无渲染异常没有黑块、白块、拉伸变形

如果截图异常,即使断言 PASS,也要修 UI 或 testspec。

保存可复现的测试记录 ​

交给其他作者时,写下地图/工程版本、实际操作、预期结果、实际日志和仍未通过的情况。UI 附一张实际截图并说明要观察的节点。记录可以跟工程放在一起,不要求作者建立教程维护者的发布证据目录。

一个 testspec 至少要描述三类信息:

md
# testspec_hud.md

## 节点

| Name | 类型 | 用途 |
|---|---|---|
| label_time | EUITextLabel | 显示倒计时 |
| label_point | EUITextLabel | 显示当前分数 |

## 交互/数据

- 收到 `HudSnapshot { phase, endTime, serverTime, score }` 后,`label_time` 按 server 时间基准刷新。
- 同一份 `HudSnapshot` 中的 `score` 更新后,`label_point` 显示最新分数。
- 节点缺失时打印 WARN,不抛运行时错误。

## 截图验收

- 两个文本不重叠。
- 数字变化后仍完整显示。
- 低分辨率下不会被边缘裁切。

最小排错示例:碰撞计分不增长 ​

1. 写期望日志 ​

text
[SE Lua Guide][Score] collision other= 球体
[SE Lua Guide][Score] last_owner= <uid>
[SE Lua Guide][Score] observed_score= <当前分数>

2. 插入临时诊断日志 ​

示例类别:解释片段;运行端:server。 这里只定义函数,由调用方传入本小节要求的对象后执行,不是完整入口。

lua
-- @runtime server
local function AttachScoreDiagnostics(cube, ball, scores)
    if cube == nil or not cube:IsA("WorldUnit") then return nil end
    if ball == nil or not ball:IsA("WorldUnit") then return nil end
    if type(scores) ~= "table" then return nil end

    return cube.OnCollisionEnter:Connect(function(otherUnit)
        local otherName = otherUnit and otherUnit.Name or "<unknown>"
        print("[SE Lua Guide][Score] collision other=", otherName)

        if otherUnit ~= ball then
            print("[SE Lua Guide][WARN] score_collision=ignored reason=not_ball")
            return
        end

        local uid = ball:GetAttribute("LastOwnerUID")
        print("[SE Lua Guide][Score] last_owner=", uid)
        if type(uid) ~= "string" or uid == "" then
            print("[SE Lua Guide][WARN] score_collision=ignored reason=missing_owner")
            return
        end

        -- 诊断只观察,不修改权威分数,也不发送得分消息。
        print("[SE Lua Guide][Score] observed_score=", scores[uid] or 0)
    end)
end

这个函数只负责临时诊断,不会增加分数。碰撞回调之间的先后顺序不能用来断言加分前后,因此这里只打印观察时的当前值;真实计分是否发生,继续看原有计分模块自己的日志。调用方必须保存返回的 Connection,排查结束或系统销毁时调用 Disconnect()。它假定 LastOwnerUID 已由另一条经过 EggyUnit 类型检查的角色触碰链路写入;诊断日志不能代替那条权威判定。

3. 运行闭环判断 ​

现象下一步
没有 collision other=检查 cube 是否找到、事件连接是否建立、物理与碰撞事件是否启用
打印 reason=not_ball当前碰到别的对象,检查目标物与事件绑定对象
打印 reason=missing_owner检查已有玩法的最后触碰者记录,不能由诊断代码补造归属
原计分模块确认新分数,UI 却不变按服务端快照 → 客户端接收 → 文本赋值逐段检查

常见错误 ​

错误:不带 session 读取一大段混合日志 ​

试玩中可以安全读取日志;错误在于不限定 play_session,把上一轮残留错误或编辑器日志算进本轮。运行中用 session 查询 marker 与错误,停止后再用同一 session 做最终复核。

错误:只看最后一行报错 ​

真正的错误源通常在 stack trace 中间。要看调用栈中第一个指向自己脚本的文件和行号。

错误:没有复现步骤 ​

“有 bug”不是测试用例。至少写清:启动后做什么、期待什么、实际看到什么。

错误:UI 只看 PASS,不看截图 ​

UI 的目标是玩家看得见、点得到、读得懂。截图审查是 UI QA 的主力证据。

错误:修复后不重跑 ​

修复必须用同一复现步骤重跑确认。只改代码不重跑,不能证明问题已解决。

练习任务 ​

  1. 给用事件和计时器组织动作碰撞示例写一份期望日志清单。
  2. 用 editor-cli 完整跑一轮:只读 preflight、play start --wait-ready、复现、按 session 查日志/截图、停止并确认 idle、最终复核。
  3. 给让 EUI 显示状态并响应按钮 HUD 写 test/testspec_hud.md,并列出至少 3 个截图检查项。
  4. 找一个可能失败的 DataStore 或资产加载调用,用 pcall 包裹并打印失败日志。
  5. 给完成小游戏后选择扩展挑战 Capstone 写一份失败记录,至少覆盖“中途加入玩家 HUD 不更新”或“UI 节点缺失”其中一个问题。
  6. 从“跨模块失败复盘清单”中任选一个问题,按模板写入 evidence/editor-runs/。

本专题验收标准 ​

  • [ ] 我能写出功能的期望日志。
  • [ ] 我会用 play_session 隔离本轮日志,并在停止后做最终复核。
  • [ ] 我能按 editor-cli 跑测闭环复现和验证问题。
  • [ ] 我能用 pcall 包住可能失败的操作,并保留失败日志。
  • [ ] 我知道 EUI 测试必须结合 testspec、日志和截图。
  • [ ] 我能把复杂问题拆成场景对象、server 判定、RemoteEvent、client 接收、UI 表现和存储几层定位。
  • [ ] 我能把一次真实失败整理成可回归的证据记录。

本专题产物 ​

  • 一份期望日志清单,用来驱动一次真实 playtest。
  • 一份失败记录,包含复现步骤、实际日志、定位层级、修复动作和重跑结论。
  • 一份截图或 testspec 审查记录,说明 UI 是否可读、无遮挡、节点命名稳定。

本专题 API 对照 ​

把结果带回小游戏 ​

先确认本页“本次要看到”的现象,再把选中的功能接到已有模块。保留原有入口和清理逻辑,只迁入需要的部分;不要把多个试验入口拼在一起。

返回主线对应步骤,或去专题导航选择下一项能力。新的代码尚未完成目标地图实测时,记录为待验证,不把编译通过当作行为通过。