下载 App
10/162 浏览案例分享
【案例分享】边玩边下(DWP)实战:14 类阻塞资源、10 类占位资源,和一份进度回调对照表
投稿方向:星火编辑器论坛「案例分享」专区 / 生态贡献度
类型:图文案例
素材来源:《别停下》开发实录 —— 关卡资源加载链
一句话结论
DWP 不是一个要你去打开的开关,它是引擎的默认行为。真正需要我写代码的只有第三类资源——既不在 14 类阻塞扩展名里、也没有占位热替换机制的自定义数据文件(.csv / .bin / .dat)。
这句话我花了两次事故才想明白。头一次是关卡的 .csv 没下完就 GetResource,拿回 nil,玩法参数全走默认值,玩家照常跑,只是"感觉不太对",我查了一下午。第二次是给背景图套遮罩,把引擎自己发起的 UI 图片下载也一起挡住了,加载条卡在 80% 不动,点哪都没反应。
下面是我后来稳定下来的三层结构。
一、引擎已经把资源分成了三堆
不用我分,它按扩展名和类型自己分好了:
堆:判据 引擎行为
阻塞集:14 个扩展名 启动前预加载,脚本跑起来时已经就绪
占位集:10 个资源类型 缺了先显示占位,下完自动热替换
剩下的:其他一切 不预加载、不占位,GetResource 直接返回 nil
阻塞集的 14 个扩展名,一个不多一个不少:
.lua .json .xml .material .prefab .effect .fsm .blendspace .cube .mdl .skel .atlas .vox .shader
.vox 要单独记一笔:体素模型是一次性烘焙的,它虽然算阻塞集,但不支持占位热替换。我一开始把它当普通媒体资源排进预下载队列,白等了两轮构建。
占位集的 10 个类型:Texture2D、Texture3D、TextureCube、Texture2DArray、Image、Model、Animation、LogicalAnimation、Sound、Font。
纹理没到就显示默认贴图,模型没到就是 T-pose,音频没到就是静音。这些是 DWP 的正常表现,别去追。
第三堆才是我的活。
二、进度回调:字段和触发时机
这张表我贴在工位上了。同名参数在不同接口里含义不同,混过一次就会写错进度条。
接口:回调 字段 含义
DownloadResources:onProgress completed / total 已完成 / 总资源个数
DownloadResources:onProgress downloadedBytes / totalBytes 已下载 / 总字节
DownloadResources:onComplete success / failedCount 整批结果 / 失败个数
ObserveDownloads:onProgress completedCount / totalCount 全局计数
ObserveDownloads:onProgress completedBytes 全局字节(没有 totalBytes)
ObserveDownloads:onComplete completedBytes activeCount 归零时触发一次
DownloadResource:onComplete success / errorMessage 单个结果
GetResourceAsync:callback resource 失败为 nil
GetDownloadState(uri) 另有 5 个状态值:DOWNLOAD_PENDING、DOWNLOADING、COMPLETED、FAILED、CANCELLED。它只用来查,不用来等。
三、完整代码
一个文件,dwp.lua,挂在关卡管理器上。
-- dwp.lua 客户端 Lua
-- 《别停下》关卡资源管理:自定义数据 / 预下载 / 全局遮罩
local DWP = {}
local cache = GetCache()
local MB = 1048576
-- 关卡逻辑要用的自定义数据。不在阻塞清单里,也没有占位机制
local LEVEL_DATA = {
"Levels/03/tiles.csv",
"Levels/03/waves.csv",
}
-- ── 第一层:自定义数据,进关前必须齐 ─────────────────────
-- .csv / .bin / .dat 引擎既不预加载也不占位。
-- 下载发起前调 GetResource 会写进负缓存,之后一直返回 nil
local function ensureData(uri, cb)
if cache:Exists(uri) then -- Exists 只看本地,不触发下载
cb(true)
return
end
cache:DownloadResource(uri, function(success, err)
if not success then
LogError("关卡数据下载失败 " .. uri .. ":" .. tostring(err))
end
cb(success)
end)
end
function DWP.LoadLevelData(cb)
local pending, allOk = #LEVEL_DATA, true
if pending == 0 then cb(true) return end
for _, uri in ipairs(LEVEL_DATA) do
ensureData(uri, function(ok)
if not ok then allOk = false end
pending = pending - 1
if pending == 0 then cb(allOk) end
end)
end
end
第二层,关卡预下载。关键是 GetResRefs —— 它把 prefab 的依赖链递归展开成 uuid:// 列表,正好能直接喂给 DownloadResources。
-- ── 第二层:关卡预下载 + 进度条 ──────────────────────────
function DWP.PreloadLevel(prefabUri, onDone)
local refs = cache:GetResRefs(prefabUri, true) or {}
-- 先声明再赋值:onComplete 闭包里要用到 groupId,
-- 写成 local groupId = DownloadResources(...) 的话闭包捕获的是全局 nil
local groupId
groupId = cache:DownloadResources(
refs,
-- onComplete:success 为 false 时,failedCount 是失败个数
function(success, failedCount)
if not success then
LogWarning("关卡预下载失败 " .. tostring(failedCount) .. " 个资源")
end
onDone(success, groupId)
end,
-- onProgress:前两个是资源计数,后两个是字节
function(completed, total, downloadedBytes, totalBytes)
if total <= 0 then return end -- 首帧 total 可能是 0
SetLoadingBar(completed / total)
SetLoadingText(string.format("%d/%d %.1f/%.1f MB",
completed, total,
downloadedBytes / MB, totalBytes / MB))
end
)
return groupId
end
-- 玩家中途退关:整组取消,别一个个 CancelDownload
-- cache:CancelDownloadGroup(groupId)
第三层是兜底。UI 系统的图片、序列化场景反序列化时触发的 DWP,都不经过 DownloadResources,只有全局观察者看得见。
-- ── 第三层:全局观察,兜住引擎自己发起的下载 ──────────────
local maskObserveId = nil
function DWP.WatchBackgroundDownloads()
if maskObserveId then return end
-- 不传 onComplete:持续观察,遮罩开关自己控
maskObserveId = cache:ObserveDownloads(
function(completedCount, totalCount, completedBytes)
if totalCount <= 0 then
SetMaskVisible(false) -- 静默期不弹遮罩
return
end
SetMaskVisible(true)
SetMaskProgress(completedCount / totalCount)
end
)
end
function DWP.StopWatching()
if maskObserveId then
cache:StopObservingDownloads(maskObserveId)
maskObserveId = nil
end
SetMaskVisible(false)
end
单个资源想消除占位跳变,就等真资源到位再换:
-- 用 GetResourceAsync 等真图,避免玩家看到占位纹理闪一下
cache:GetResourceAsync("Texture2D", "Textures/unlocked_skin.png", function(tex)
if tex and skinSprite then
skinSprite:SetTexture(tex)
end
end)
四、下载优先级
顺序反了,遮罩就白挂了。我按这个排:
优先级:内容 时机 用什么
0:.csv / .bin / .dat 关卡逻辑数据 进关前,同步等 DownloadResource
1:关卡 prefab 及其阻塞依赖 进关前,遮罩 DownloadResources(GetResRefs(...))
2:本关纹理、模型、音效 进关后,后台 引擎 DWP 自动
3:下一关资源 结算界面挂着的时候 DownloadResources
优先级 2 我一行代码都不写。引擎自己会做,玩家看到的是默认贴图闪一下再变正常 —— 只要这一下不超过半秒,没人会注意到。
五、踩过的坑
坑 1:没下完就用,拿回 nil 还顺手挖了个负缓存
-- ❌ 我第一版
local tex = cache:GetResource("Texture2D", "Textures/boss.png")
if tex then bossSprite:SetTexture(tex) end
-- nil 静默跳过,什么都没发生;更麻烦的是这次调用写进了负缓存
负缓存的意思是:这个 URI 之后一直返回 nil,直到有代码发起它的下载,才会被清掉。我那次是在关卡里连调了 30 次,等于把 30 个资源挨个污染了一遍。
-- ✅ 正确
cache:GetResourceAsync("Texture2D", "Textures/boss.png", function(tex)
if tex and bossSprite then bossSprite:SetTexture(tex) end
end)
坑 2:用 onProgress 判断"下完了"
-- ❌ 错的
cache:DownloadResources(refs, nil, function(completed, total)
if completed == total then EnterLevel() end
end)
两个问题。首帧 total 是 0,0 == 0 直接成立,关卡在资源一个没下的时候就被推进去了。就算躲过首帧,onProgress 也不保证收尾那一次一定命中 —— 它只是"有进展时通知"。
-- ✅ 对的
cache:DownloadResources(
refs,
function(success, failedCount) EnterLevel(success) end, -- 完成看 onComplete
function(completed, total) UpdateBar(total > 0 and completed / total or 0) end
)
坑 3:下载失败没有分支
-- ❌ 错的:失败也往下走
cache:DownloadResource(uri, function() LoadScene() end)
玩家掉进一个没贴图、没音效、地形数据还是默认值的场景。他不会报错,他会说"这游戏怎么这样"。
-- ✅ 对的
cache:DownloadResource(uri, function(success, err)
if not success then
LogError("资源下载失败 " .. uri .. ":" .. tostring(err))
ShowRetryPanel(uri) -- 给玩家一个重试入口
return
end
LoadScene()
end)
坑 4:ObserveDownloads 的 onComplete 只触发一次
-- ❌ 我第二版:想用 onComplete 关遮罩
cache:ObserveDownloads(onProgress, function(bytes) HideMask() end)
它在 activeCount 归零时触发一次,然后观察就结束了。之后引擎又发起新下载,再也没有人管遮罩。
-- ✅ 持续观察,开关自己控
local id = cache:ObserveDownloads(onProgress)
-- 需要的时候 cache:StopObservingDownloads(id)
另外别写 while cache:GetDownloadState(uri) ~= "COMPLETED" do end。主线程会卡死,连下载回调都没机会跑。
六、服务端要多看一眼
前后端分离的项目里,服务端把 .mdl 也加进了阻塞集(物理碰撞依赖它),而 DWP 媒体资源在服务端只占位、不下载。所以服务端脚本里读纹理尺寸、读音频时长这类操作,拿到的永远是占位值。逻辑要用的数值,走 .csv 或者配置,别去问媒体资源。
七、适用边界
该开 DWP 的情况:
- 首包体量已经卡住转化,玩家进来先盯着进度条
- 资源里有大比例是"只在某一关、某个皮肤用到"的
- 你能接受进关前有一段几百毫秒的遮罩
该全量预下载的情况:
情况:原因
资源总量在几十 MB 量级:省下的流量还不够遮罩那几秒的体验损失
玩法对帧率、时序敏感:后台下载会抢 IO 和带宽,帧率会抖
服务端逻辑依赖媒体资源:服务端只占位不下载,逻辑读到的是空值
上架前一周:DWP 的问题都在运行时才暴露,压线切风险太高
还有个容易忽略的点:DWP 的开关在构建配置里(preload_groups 清空即启用),而引用范围在 groups 里。前者管"什么时候下",后者管"进不进包"。先确认资源进包了,再来调 DWP —— 顺序反了,你会花一整天调一个根本不在包里的资源。
八、一条可迁移的判断方法
打开资源目录,对每个文件问一句:这一帧没有它,游戏逻辑会不会算错?
会算错的,划进阻塞集,全部前置 —— 进包,启动前下齐,不给它留任何"以后再说"的余地。
只是难看的,交给占位热替换。
这条线一划完,剩下的都是实现细节。浏览器把 CSS 和 JS 塞进 <head>、把 <img> 留给懒加载,走的是同一条线。
(完)
星火编辑器-新手教程-关于多游戏模式的配置和触发的获取截图
星火编辑器-新手教程-关于多游戏模式的配置和触发的获取
HELLO 大家好,我是阿木木牧。最近第一版的内部小范围(几人)第一个图的测试已经结束了。 整体游戏以装备数值的崩坏而告终~ (虽然大家都很欢快。。。) 现在准备开始开发下个阶段的内容啦,就是将游戏模式拆分出来。分成单人模式,多人组队模式(合作PVE),多人匹配模式(对抗模式)。那么,怎么配置呢?说来也很简单哈~ 1,在游戏编辑器中,数据编辑器,找到地图设置。 2,编辑地图设置。比如我希望最大数量
14 赞
3 回复
不支持当前设备截图
不支持当前设备
荣耀400pro
2 赞
3 回复
00:18
修复一下这个挂起过。有概率用不了的bug吧截图
修复一下这个挂起过。有概率用不了的bug吧
太绝望了[表情_豹哭][表情_豹哭][表情_豹哭][表情_豹哭]
1 赞
1 回复
05:19
1截图
1
1.完成后,获得信用点的增益依旧有效,可以卡无限信用点。理论上 2.部分天赋和在一起可以实现永续,那些天赋,我下一个发,因为只能传一个视频。 3.卡,人多点就卡,知道卡死,但是手机不会卡就游戏卡
2 赞
无须申请,星火编辑器现已公测开放下载截图
无须申请,星火编辑器现已公测开放下载
—————————————————— 各位关注星火编辑器的小伙伴: 感谢大家一直以来对编辑器支持! 破晓已去,曙光降临! 星火编辑器现已开启公测! · 关于下载和资格申请 现已无需申请,请去下面的官方首页直接下载! 缘起测试是星火编辑器自诞生以来第一次对外测试! 从2022年八月开始到十一月,历时三个月。 缘起之后缘聚测试,历时又七个月! 缘聚之后,破晓降临,历时又十二个月! 曾经微弱的星火现已渐
官方
331 赞
375 回复
00:11
人物有小概率会卡着不往前走截图
人物有小概率会卡着不往前走
3 赞
1 回复