Skip to content

第 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 接口

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

  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

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

自定义资产的最佳实践

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

本章产物

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

本章 API 对照

下一章预告

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