Skip to content

第 18 章:自定义资产系统 ​

上传和异步加载自定义 preset,并设计有界、可观测的官方资源降级路径。

开始前与本章路线 ​

  • 准备:准备自己有权使用并已保存为 Unit preset 的资源;取得真实 URI;官方备用 preset 先单独测试。
  • 安装范围:data/custom_assets.lua 配套其中一个 server/main.lua;两种加载入口二选一。
  • 本次要看到:主资源成功时只生成一份;主资源失败时最多尝试一次备用资源;两者都失败后停止。

停止试玩后再换入口;这些章节是能力实验,第 22 章才提供统一的六文件项目。每次只改变一个条件,并保留本次输出用于对照。

你会学到什么 ​

  • 自定义资产和官方资产的区别。
  • 如何在编辑器中上传自定义资源。
  • 如何在 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 接口

在编辑器中上传自定义资源 ​

  1. 在编辑器的资源管理面板中切换到目标资源类型(图片、音频、模型、动作或特效);具体分页名称以当前 Editor 版本为准。
  2. 点击上传按钮,选择本地文件(模型 .fbx、图片 .png/.jpg/.jpeg、音频 .mp3/.wav)。
  3. 若要用 LoadUnitAssetAsync 加载完整模型组合,应先在编辑器中把层级、材质和物理配置保存为 Unit preset;单个 mesh URI 不能直接当作 Unit preset 使用。
  4. 上传或保存完成后,资源会获得 custom://{id} 格式的 URI;通过资源菜单里的“复制引用 ID”取得它,不要手写或猜测 ID。

编辑器可能会把资源信息导出到 data/CustomAsset.lua。你可以直接 require 这类导出表,也可以在自己的 data/custom_assets.lua 中整理一层更适合项目阅读的配置。无论哪种方式,data 里只放 URI 和参数,不直接加载资源。

data/custom_assets.lua ​

运行端:data

示例 1|完整配置 data/custom_assets.lua;必须替换占位 URI。 本次演示端:server。

lua
-- @runtime server
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

示例 2|配套入口 server/main.lua;依赖 data/custom_assets.lua。 本次演示端:server。

lua
-- @runtime server
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。

错误处理 ​

自定义资产加载可能失败(网络问题、资源不存在、格式错误)。必须处理失败情况:

示例 3|带降级的配套入口 server/main.lua;整体替换上个入口。 本次演示端:server。

lua
-- @runtime server
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、网络或格式问题:

示例 4|独立实验;整体替换 server/main.lua。 本次演示端:server。

lua
-- @runtime server
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 走已经公开且有失败回调的路径。

自定义资产的最佳实践 ​

  1. 预加载:在游戏开始时用 PreloadAsync 批量预下载自定义资产,避免运行时卡顿。
  2. 降级方案:自定义资产可能加载失败,准备一个官方资产作为备用。
  3. 集中管理:把所有 custom:// URI 放到 data/CustomAsset.lua 或自己的 data/custom_assets.lua 中,方便统一维护。
  4. 资源大小:自定义资产越大,下载时间越长。尽量优化资源大小。

常见错误 ​

错误:用同步接口加载 custom:// ​

本章的 custom Unit preset 必须用 LoadUnitAssetAsync,不能用同步 LoadUnitAsset 代替下载与失败回调。

错误:把 mesh 当成 preset 加载 ​

编辑器导出的模型组合或 preset 用 LoadUnitAssetAsync 加载整棵 Unit 资产。单个 custom mesh 还需要明确的预加载完成语义才能交给同步 CreateUnit;当前教程没有这条已验证链路,因此不提供直接创建示例,新手先保存成 preset。

错误:不处理加载失败 ​

自定义资产可能因网络问题加载失败。如果不处理失败,后续代码访问 nil 对象会报错。

错误:忘了设 Parent = World ​

和官方资产一样,自定义资产加载后也必须设 Parent = World 才能可见。

练习任务 ​

  1. 在编辑器中上传一个自定义模型,获取其 custom:// URI。
  2. 把 URI 从代码里移到 data/custom_assets.lua,同时配置一个 FallbackPreset。
  3. 用 LoadUnitAssetAsync 加载它,成功后打印根单位名称和根 Unit 位置。
  4. 故意填错一次 custom:// URI,确认会走降级逻辑,并记录 WARN 日志。
  5. 按第 22 章的运行记录模板写一份记录,包含成功加载和失败降级两段关键日志。

练习参考与复核 ​

先完成练习,再展开对照;没有运行条件的项目填写“未验证”和缺少的条件。

参考思路与应观察的结果

custom://12345 是占位符,不能视为可用资产。先记录真实成功,再在隔离实验中填错 URI 检验降级。attempted 防止重复尝试,不代表下载有时间上限;长期加载还要设置超时、过期回调丢弃与已加载对象清理。

  • [ ] 我已完成本章选定实验,记录了上面的预期现象。
  • [ ] 我能故意改变一个前置条件,解释失败发生在哪一步,并在恢复后重跑。

本章验收标准 ​

  • [ ] 我知道 custom:// 和 official:// 的区别。
  • [ ] 我能用 LoadUnitAssetAsync 异步加载自定义资产。
  • [ ] 我知道自定义 Unit preset 必须用 LoadUnitAssetAsync;其它资源按各自公开 API 处理。
  • [ ] 我知道加载失败需要降级处理。
  • [ ] 我知道 data 只保存 URI 和参数,不直接加载资源。
  • [ ] 我能留下自定义资产成功加载和失败降级的运行证据。

本章产物 ​

  • 一个 data/custom_assets.lua 配置表,包含 custom:// URI 和 FallbackPreset。
  • 一份自定义资产成功加载记录,包含根 Unit 名称、位置和 Parent。
  • 一份失败降级记录,包含错误 URI、WARN 日志和降级到官方 preset 的结论。

本章 API 对照 ​

下一章预告 ​

资产能加载以后,项目逻辑通常会开始变长。下一章先不急着做包,而是学习 Manager、生命周期与依赖边界:先把当前地图拆清楚,再决定是否需要包化复用。