主题
第 18 章:自定义资产系统
上传和异步加载自定义 preset,并设计有界、可观测的官方资源降级路径。
你会学到什么
- 自定义资产和官方资产的区别。
- 如何在编辑器中上传自定义资源。
- 如何在 Lua 中加载自定义资产。
- 自定义资产的异步加载和错误处理。
- 如何把
custom://URI 集中放进data配置,并准备降级资源。
为什么需要自定义资产
官方资产(official://)是引擎内置的,数量有限。当你想用自己的模型、图片、音频时,就需要自定义资产(custom://)。
自定义资产是游戏扩展的重要系统——它让你不局限于官方资源,可以创造独特的视觉和听觉体验。
官方资产 vs 自定义资产
| 对比项 | 官方资产 official:// | 自定义资产 custom:// |
|---|---|---|
| 来源 | 引擎内置 | 用户上传 |
| 加载方式 | 对应 API 允许时可同步使用 | 先按资源类型走公开的预加载、异步 loader 或接收字段 |
| URI 格式 | official://preset/{id}、official://mesh/{id} 等 | custom://{id} |
| 可用接口 | 按资产类型选择公开 loader/接收字段 | Unit preset 使用 LoadUnitAssetAsync;本章不虚构不存在的异步 CreateUnit 接口 |
在编辑器中上传自定义资源
- 在编辑器的资源管理面板中切换到目标资源类型(图片、音频、模型、动作或特效);具体分页名称以当前 Editor 版本为准。
- 点击上传按钮,选择本地文件(模型
.fbx、图片.png/.jpg/.jpeg、音频.mp3/.wav)。 - 若要用
LoadUnitAssetAsync加载完整模型组合,应先在编辑器中把层级、材质和物理配置保存为 Unit preset;单个 mesh URI 不能直接当作 Unit preset 使用。 - 上传或保存完成后,资源会获得
custom://{id}格式的 URI;通过资源菜单里的“复制引用 ID”取得它,不要手写或猜测 ID。
编辑器可能会把资源信息导出到 data/CustomAsset.lua。你可以直接 require 这类导出表,也可以在自己的 data/custom_assets.lua 中整理一层更适合项目阅读的配置。无论哪种方式,data 里只放 URI 和参数,不直接加载资源。
data/custom_assets.lua
运行端:data
lua
return {
MainPreset = "custom://12345",
FallbackPreset = "official://preset/105205",
}在 Lua 中加载自定义 preset
如果自定义资源是一个模型组合或 Unit preset,必须用异步接口(LoadUnitAssetAsync),因为它需要下载。新手仍然优先走 preset 路线:让编辑器保存模型层级和资源设置,Lua 只负责加载、挂载和移动根 Unit。图片、音频、动画等其它 custom:// 资源不能由这条 Unit loader 规则一概推导;应先看目标属性或播放 API 接受什么 URI,必要时使用 PreloadAsync,并在目标运行包实测。
运行端:server
文件:server/main.lua
lua
local AssetService = game:GetService("AssetService")
local World = game:GetService("World")
local CustomAssets = require("data.custom_assets")
if not AssetService or not World then return end
-- 异步加载自定义资产(必须用 Async 版本)。接口只接收 uri 和回调。
AssetService:LoadUnitAssetAsync(CustomAssets.MainPreset, function(ok, units)
if ok and units and #units > 0 then
local root = units[1]
if not root:IsA("ModelUnit") then
print("[SE Lua Guide][WARN] 自定义 preset 根不是本例要求的 ModelUnit。")
root:Destroy()
return
end
-- 返回的根 Unit 默认游离,必须手动挂到 World 下
root.Parent = World
root.Position = Vector3(0, 5, 0)
root.Rotation = Quaternion.FromEulerAngles(0, 0, 0)
print("[SE Lua Guide] 自定义资产加载成功:", root.Name)
else
print("[SE Lua Guide][WARN] 自定义资产加载失败或超时")
end
end)加载成功后,units[1] 是根 Unit,默认游离,必须设置 Parent 才会进入活跃场景。本例明确要求根是 ModelUnit,因此先 IsA 再用 Position / Rotation 移动整体;其他根类型必须按对应页面处理。
为什么必须异步?
custom:// 资产首次使用时可能需要下载。对本章的 Unit preset,公开、可观察的完成路径就是 LoadUnitAssetAsync 回调,因此不能改用同步 LoadUnitAsset 猜下载时机。其它资源类型仍按各自公开 API 处理,不把“Unit preset 必须异步”扩大成所有资源都使用同一个 loader。
错误处理
自定义资产加载可能失败(网络问题、资源不存在、格式错误)。必须处理失败情况:
lua
local AssetService = game:GetService("AssetService")
local World = game:GetService("World")
local CustomAssets = require("data.custom_assets")
if not AssetService or not World then return end
local attempted = {}
local function SpawnPreset(uri, fallbackUri)
if attempted[uri] then
print("[SE Lua Guide][WARN] 已尝试过该 URI,停止重复加载:", uri)
return
end
attempted[uri] = true
AssetService:LoadUnitAssetAsync(uri, function(ok, units)
if not ok or not units or #units == 0 then
print("[SE Lua Guide][WARN] preset 加载失败:", uri)
if fallbackUri and not attempted[fallbackUri] then
SpawnPreset(fallbackUri, nil)
end
return
end
local root = units[1]
if not root:IsA("ModelUnit") then
print("[SE Lua Guide][WARN] preset 根不是 ModelUnit:", uri)
root:Destroy()
if fallbackUri and not attempted[fallbackUri] then
SpawnPreset(fallbackUri, nil)
end
return
end
root.Parent = World
root.Position = Vector3(0, 5, 0)
print("[SE Lua Guide] preset 加载成功:", uri, root.Name)
end)
end
SpawnPreset(CustomAssets.MainPreset, CustomAssets.FallbackPreset)也可以监听全局加载失败事件,辅助排查资源 URI、网络或格式问题:
lua
local AssetService = game:GetService("AssetService")
if not AssetService then return end
AssetService.AssetFetchFailed:Connect(function(uri, errorMsg)
print("[SE Lua Guide][WARN] 资产加载失败:", uri, errorMsg)
end)单个自定义 mesh 与 preset 的区别
如果你拿到的是一个完整模型组合或编辑器 preset,用 LoadUnitAssetAsync("custom://...")。
如果你拿到的是单个网格资源,它最终要进入对应 Unit 的公开网格字段;但当前公开 Game/World API 只有同步 CreateUnit,没有“异步 CreateUnit”方法。本章不对 custom mesh 的下载完成时机做猜测式封装。新手应先在编辑器中把自定义模型保存成 Unit preset,再用 LoadUnitAssetAsync 走已经公开且有失败回调的路径。
自定义资产的最佳实践
- 预加载:在游戏开始时用
PreloadAsync批量预下载自定义资产,避免运行时卡顿。 - 降级方案:自定义资产可能加载失败,准备一个官方资产作为备用。
- 集中管理:把所有
custom://URI 放到data/CustomAsset.lua或自己的data/custom_assets.lua中,方便统一维护。 - 资源大小:自定义资产越大,下载时间越长。尽量优化资源大小。
常见错误
错误:用同步接口加载 custom://
本章的 custom Unit preset 必须用 LoadUnitAssetAsync,不能用同步 LoadUnitAsset 代替下载与失败回调。
错误:把 mesh 当成 preset 加载
编辑器导出的模型组合或 preset 用 LoadUnitAssetAsync 加载整棵 Unit 资产。单个 custom mesh 还需要明确的预加载完成语义才能交给同步 CreateUnit;当前教程没有这条已验证链路,因此不提供直接创建示例,新手先保存成 preset。
错误:不处理加载失败
自定义资产可能因网络问题加载失败。如果不处理失败,后续代码访问 nil 对象会报错。
错误:忘了设 Parent = World
和官方资产一样,自定义资产加载后也必须设 Parent = World 才能可见。
练习任务
- 在编辑器中上传一个自定义模型,获取其
custom://URI。 - 把 URI 从代码里移到
data/custom_assets.lua,同时配置一个FallbackPreset。 - 用
LoadUnitAssetAsync加载它,成功后打印根单位名称和根 Unit 位置。 - 故意填错一次
custom://URI,确认会走降级逻辑,并记录 WARN 日志。 - 按第 22 章的运行记录模板写一份记录,包含成功加载和失败降级两段关键日志。
本章验收标准
- [ ] 我知道
custom://和official://的区别。 - [ ] 我能用
LoadUnitAssetAsync异步加载自定义资产。 - [ ] 我知道自定义 Unit preset 必须用
LoadUnitAssetAsync;其它资源按各自公开 API 处理。 - [ ] 我知道加载失败需要降级处理。
- [ ] 我知道
data只保存 URI 和参数,不直接加载资源。 - [ ] 我能留下自定义资产成功加载和失败降级的运行证据。
本章产物
- 一个
data/custom_assets.lua配置表,包含custom://URI 和FallbackPreset。 - 一份自定义资产成功加载记录,包含根 Unit 名称、位置和 Parent。
- 一份失败降级记录,包含错误 URI、WARN 日志和降级到官方 preset 的结论。
本章 API 对照
下一章预告
资产能加载以后,项目逻辑通常会开始变长。下一章先不急着做包,而是学习 Manager、生命周期与依赖边界:先把当前地图拆清楚,再决定是否需要包化复用。
