下载 App
10/211 浏览案例分享
【案例分享】上线前 12 条自查:把 6 篇实测坑压成一张能照着走的表
素材来源:《别停下》开发实录,01 / 03 / 07 / 13 / 24 / 26 六篇实测记录
类型:图文案例 + 可执行清单
一句话结论
上线前最该做的不是通读代码,是按表把「不报错但没生效」的点过一遍。这 6 篇里最贵的故障,大都不是崩溃,是一眼扫过去毫无异常的干净控制台。
1:界面全白,控制台干净 → 漏了 UI.SetRoot(见 13)
2:线上报资源找不到,本地正常 → 资源没进构建产物(见 07)
3:启动满屏报错,标题页空白 → 字体被 assert 住(见 24)
4:角色不走,或物理在走画面不动 → 四件套缺一个组件(见 03)
5:面板关掉再开全黑 → 释放顺序不对或漏置 nil(见 26)
翻车不随机。上面五条的共同点是「设置没被消费」,而不是「设置写错」。清单的价值不在长,在每条都能落到一个看得见的判据上,查完就能判过没过。下面 12 条的目标只有一个:让你在点发布之前,知道哪一处可能在静默失效。它们也不是清单式罗列:每一条背后都有一次具体事故,反复改参数、反复开关面板才复现、上线当天收到第一封反馈。把它们改写成检查动作,才是这张表的价值。
踩过的坑
12 条按上线前真正会查的顺序排:资源、界面、画面、角色、收尾。每条只说三件事:检查什么、不过会怎样、出处。顺序也不是随意的:资源没进包,后面界面和画面查得再细也白查。
这些坑看上去零散,归到根上只有三类:资源没进产物(第 1、2 条)、设置没人消费(第 5 到 11 条)、资源没人释放(第 12 条);第 3、4 条是时序没留出口。
一、资源与启动
1. 资源在构建产物里吗。本地能跑、线上报 Could not find resource,九成是它没进包。先读构建日志那两行策略声明,再看 manifest;manifest 里没有的本地资源,运行时一定访问不到。改法只有两条:把资源加进 groups,或者把路径改成构建器能静态解析的写法。见 07。
2. 资源路径是完整字面量吗。增强引用会沿引用链裁剪,靠 .. 拼出来的路径可能被静默裁掉;兜底用的结尾模糊匹配不保证覆盖所有拼接;同一行里混用引号也会干扰提取。见 07。
3. 字体在启动时被 assert 了吗。.ttf 是 DWP 媒体资源,不在 render-blocking 名单里。启动那一刻断言它,代价不是一行日志,实机表现是 1000+ 次 XHR 报错。改成不中断启动、逐帧重试。见 24。
4. 首屏遮罩上写字了吗。遮罩出现的时刻,恰好是字体还没下好的时刻。一个字都画不出来的标题页,玩家只会判定游戏坏了。遮罩只画几何图形,并且必须有超时出口;顺序上它要挂在字体尝试之后,否则会漏掉那一次下载。见 24。
二、界面
5. UI 树建完调 UI.SetRoot 了吗。界面只有三步:UI.Init、建树、UI.SetRoot。第三步最容易漏,漏了全屏空白、控制台干净,和主题配错长得一模一样。见 13。
6. 事件回调的签名对了吗。onClick(widget, event) 写反,按钮点了没反应,也不报错。这套事件有两套签名,写反了静默不触发。见 13。
三、画面
7. 半径真的由你控制吗。nvgRadialGradient 的 inner / outer 会被忽略,边界固定在引擎默认的约 0.35 × min(W,H)。改了参数画面不动,不是颜色算错。量一下亮度剖面就清楚:不同参数的两张图,剖面完全重合。见 01。
8. 同心圆是描边还是填充。填充同心圆会累加 alpha,中心反而最亮,和「中心干净、边缘有色」正好相反;描边环每个半径只描一次。环数别太少,少了会出色带。见 01。
四、角色
9. 四件套齐了吗。RigidBody、CollisionShape、KinematicCharacterController、CharacterComponent 少挂一个,角色不走、不受重力,或者物理在算但画面不动,一条报错都没有。创建顺序也要对:脚本路径下顺序反了,引擎不会事后补救。见 03。
10. 关父节点够吗。SetEnabled(false) 不递归,子节点上的模型照样渲染;要关整棵子树,用 SetDeepEnabled(false)。判断方法一句话:这个对象是单个平铺节点,还是父节点带一堆子节点。见 03。
五、收尾
11. 3D 进 UI 的 CMD_CLEAR 改透明了吗。默认清屏会把离屏纹理涂成雾色,画出来是一块雾蒙蒙的方块压在界面上。useFogColor = false 和 clearColor 要一起改,只改后者,雾色照样把 alpha 顶回 1。见 26。
12. 释放顺序记在脑子里吗。nvgDeleteVideo → SetNumViewports(0) → Dispose() → Lua 引用置 nil。顺序反了不报错;第 3 步和第 4 步最容易搞反,反了会留下悬空引用;漏最后一步的表现是「第一次打开正常,关掉再开全黑」。见 26。
三段代码,把其中最省事的几条变成可以现场跑的检查:
-- 见 24:字体不 assert,失败也不中断启动
local ok, h = pcall(nvgCreateFont, vg_, "sans", "Fonts/NotoSansSC.ttf")
fontReady_ = ok and type(h) == "number" and h >= 0
if not fontReady_ then
pcall(function()
local cache = GetCache()
if cache and cache.DownloadResource then
cache:DownloadResource("Fonts/NotoSansSC.ttf", nil)
end
end)
end
-- 每 30 帧重试一次;字体没好,后面画什么都是空白
if not fontReady_ then
fontRetry_ = fontRetry_ + 1
if fontRetry_ % 30 == 0 then
local ok2, h2 = pcall(nvgCreateFont, vg_, "sans", "Fonts/NotoSansSC.ttf")
if ok2 and type(h2) == "number" and h2 >= 0 then fontReady_ = true end
end
end
-- 见 03:四件套按顺序建,缺一件的症状各不相同
local body = node:CreateComponent("RigidBody")
local shape = node:CreateComponent("CollisionShape") -- 必须在 KCC 之前
local kcc = node:CreateComponent("KinematicCharacterController")
local cc = node:CreateComponent("CharacterComponent") -- 少了它,物理在走画面不动
assert(shape and kcc and cc, "[自检] 角色四件套不全,查日志里缺哪一个")
-- 见 26:关闭时按这个顺序走,顺序错了不报错
local function DestroyScenePreview()
if image and image ~= 0 then nvgDeleteVideo(imageContext, image) end
image, imageContext = nil, nil
if surface then surface:SetNumViewports(0) end
if texture then texture:Dispose() end
surface, viewport, texture = nil, nil, nil
end
适用边界
这张表是给「已经有内容、准备上线」的项目用的,不是新建项目第一天用的。
小体量原型不查。一个场景、没有 UI、没有存档、玩法还在试,表里能命中的只是少数,提前查只会拖慢验证玩法。等第一个可玩版本出来再进来,一次就够。已经有自动构建的项目,把构建日志里那两行策略声明存下来,每次发版对比一眼,比翻配置快。
每条都有触发条件,对号入座,不是满分考试。不画自绘渐变和光效,第 7、8 条与你无关;不做 3D 角色,第 9、10 条可以跳过;不做 3D 进 UI,第 11、12 条可以整段跳过;纯数值平衡的改动,这 12 条一条都不适用。
也不要为了跑表去改安全的默认值。全量引用包体大,但默认不会丢资源;从安全切到省要一次系统排查,反着切只要改一行(见 07)。顺序也要对:先让代码满足新约束,再改配置,否则中间会开一个「资源丢失」窗口。
表是用来提前暴露的,不是用来交差的。查完没改的,把理由写一句;比全部打勾有用。上线前只剩两小时,就从第 1 条往下走,资源与界面先过,后面几条按项目类型挑。
可迁移的经验
1. 「没反应」先查「有没有生效」,再调参数。半径改了画面不动、按钮点了没反应、角色不走,都是同一类问题(见 01 / 03 / 13)。参数调了半天没用,多半是这一层根本没跑起来。
2. 判据要落在「东西到没到位」上。资源看 manifest 里有没有,角色看组件挂没挂,UI 看根节点挂没挂。这三处都有明确的是非,不用靠感觉;把判据写成能打印的一行,就不用靠回忆。
3. 把释放责任写成表。每类资源配一个成对的 Release*,顺序抄进注释;拆的时候没有 assert 兜着,错了只会在开关几十次之后以显存增长的形式冒出来(见 26)。
4. 兜底必须有出口。遮罩要超时,探测要重探,重试要有上限;没有出口的等待,等于把玩家锁在门外(见 24)。
5. 人记不住 12 条,就把它压成一条命令。启动时跑几行自检,把失败项打出来,比上线前夜逐条回想可靠:
-- 见 03 / 13 / 24:能自动判的项,启动时跑一遍
local PREFLIGHT = {
{ id = "03", name = "角色四件套", ok = shape ~= nil and kcc ~= nil and cc ~= nil },
{ id = "13", name = "UI 已挂根节点", ok = uiRoot ~= nil },
{ id = "24", name = "字体已就绪", ok = fontReady_ == true },
}
for _, item in ipairs(PREFLIGHT) do
if not item.ok then print("[自检失败] " .. item.id .. " " .. item.name) end
end
这张表不负责让你不犯错。它负责让错在今晚暴露,而不是在玩家手机上暴露。
(完)