Skip to content

项目资源路径指南 ​

本文说明项目资源文件应该放在哪里,以及代码中应该写什么路径。这里的“引擎资源”指由 UI 控件、Canvas、Spine、GameGraph、Actor、粒子、音频、模型等引擎 API 加载的资源;它不同于脚本用 .NET File / Directory 直接读取的普通文件。

核心规则 ​

不要把所有文件都放进 user_files。user_files 只用于脚本直接读写普通文件;引擎渲染、动画、模型、粒子、音频等资源应按引擎资源目录规则放置。

场景文件放置位置代码中的路径说明
UI 图片、Canvas Image、图标图片、精灵图ui/...从 ui/ 下一级开始写,例如 image/abc.pngui/image/abc.png 对应 new Image("image/abc.png")
视频控件、视频纹理res/video/...从 res/ 下一级开始写,例如 video/intro.mp4Video.Source 和 VideoPlayer 使用相同的项目相对路径,详见视频播放
UI Spine 动画ui/Spine/...从 Spine/ 开始,不写 .skel 后缀,例如 Spine/hero/heroui/Spine/hero/hero.skel、.atlas、.png 对应 spine.Resource = "Spine/hero/hero"
GameGraph 2D 动画(Spriter / Spine)Spriter/序列帧优先放 ui/image/...;Spine 优先放 ui/spine/...;旧项目可继续放 res/...从资源根开始并包含动画文件扩展名,例如 image/Animated/coin.scml 或 spine/hero/hero.skelAnimationSet2D.Load(...) 先按 UI 资源依赖优先级找,找不到再兼容当前项目 res/...
GameUI Label / Input / TextButton 自定义字体项目私有字体放 res/font/<family>/...;公共 UI 字体为 ui/font/<family>/...项目字体写 font/<family>;公共 UI 字体写 ui/font/<family>Label.Font 是字体 family,不是 .ttf 文件路径;还必须加入 ref/fontref.txt,详见 自定义字体指南
官方或当前环境已上传云的模型、粒子、音频、动画等使用资源库或 docs/resources/ 中的资源路径直接写资源路径,例如 characters/monster/sm_slm_a/model.prefab发布时由资源统计和云端资源下载处理;自定义云端资源在外网和线上互相独立
未上传云但必须随项目发布的自定义模型、粒子、音频、材质等res/...从 res/ 下一级开始写,例如 effect/custom/fire/particle.effect 或 sound/custom/click.wavres/effect/custom/fire/particle.effect 对应 ParticleSystem.Load("effect/custom/fire/particle.effect");res/sound/custom/click.wav 对应 Sound soundPath = "sound/custom/click.wav"; SoundResource.Load(soundPath)
脚本自己读取 JSON、文本、配置、模板等普通文件AppBundle/user_files/ 或 ui/AppBundle/user_files/user_files/...使用 .NET File / Directory API,详见 项目文件读写指南

文档示例统一使用 /。部分旧接口和旧示例中可能出现 \,但新代码优先使用 /,便于跨平台和 AI 生成代码保持一致。

如果是在资源管理器里选择“上传云端资源”还是“放入项目资源”,或项目要从外网迁移到线上,先看 部署环境与运行时环境指南。云端资源适合多项目共享和跨游戏下载缓存复用;项目资源更自洽,不依赖目标环境提前存在自定义云资源。

AI 助手选择资源时,如果完整编辑器 MCP 可用,应优先使用 resource_catalog_search / resource_catalog_get_detail / resource_get_preview_info 查询实时资源目录和结构化预览信息;MCP 不可用时再回退到 SDK 随包的 docs/resources/*.json 静态索引。查询当前项目自己的本地资源时使用 source=project, rootKind=currentMap,需要连同依赖库一起搜索时再放宽到 rootKind=any。这些工具返回的是代码可用的资源路径和安全 metadata,不应把编辑器维护仓库、下载缓存或本机绝对路径写进项目代码。

UI 图片和 Canvas 图片 ​

引擎读取 UI 图片时,项目的 ui/ 目录是文件放置根,但代码路径不包含 ui/ 前缀。

text
MyProject/
└── ui/
    └── image/
        └── abc.png
csharp
#if CLIENT
var image = new Image("image/abc.png");
canvas.DrawImage(image, 100, 100, 256, 128);
#endif

同一规则也适用于 UI 控件上的图片路径、图标路径、Canvas 精灵图,以及 GameDataAnimatedImage.Image 这类由引擎绘制的图片资源。

如果需要普通控件显示一张大图中的指定区域,可让 AI 助手生成 ui/atlas/atlas.json 清单与 custom_icons.json 区域配置,再让控件的 Image 引用子图片逻辑名称;不要求存在对应的散图。完整 JSON、路径前缀、坐标规则及自动打包注意点见自定义 UI 图集。

不要写成 new Image("ui/image/abc.png"),也不要把图片放到 user_files 后再用 Image("user_files/...")。Image 是引擎资源路径,不是 .NET 文件路径。

发布图片的加载与检查 ​

PC 和手机发布包都可能把 PNG/JPG 转为 KTX 等运行时纹理容器,逻辑路径及文件后缀仍保持不变。KTX 不限于手机:PC 可存放 BC 系列压缩纹理,也可存放未压缩像素;不能根据 .png 后缀判断文件内容,也不能根据 KTX 容器判断目标平台。

GameGraph.ResourceSystem.Image 是 CPU 图片资源,new GameCore.ResourceType.Image(path) 是供 Canvas 使用的逻辑路径值。构造路径值不代表已成功加载,但 CPU Image.Load(path) 返回空也不代表 UI 图片不存在或无法绘制:CPU 解码器与 UI 纹理加载器支持的格式不同。不要用 CPU Image.Load 的结果作为控件或 Canvas 绘制的前提,也不要因此永久标记图片失败或切换占位图。

普通绘制直接使用图片逻辑路径。确实需要提前加载独立散图纹理以实现失败回落时,使用 GameGraph.ResourceSystem.Texture2D.Load(path);现有客户端实现通过 TextureX 加载发布容器并取得 Texture2D,再兼容普通 Texture2D 加载:

csharp
#if CLIENT
string path = "image/hero.png"; // 项目文件 ui/image/hero.png
var texture = GameGraph.ResourceSystem.Texture2D.Load(path);
if (texture != null)
{
    var image = new GameCore.ResourceType.Image(path);
    canvas.DrawImage(image, 100, 100, 256, 256);
}
else
{
    // 在这里选择项目自己的缺图回落逻辑。
}
#endif

检查应在初始化或按需加载时执行并复用结果,不要每帧重复加载。该方式用于有独立纹理文件的散图,不适用于只有图集区域的虚拟子图路径;子图按图集清单和区域配置解析,不要求存在同名散图。Texture2D 加载成功只证明纹理可加载,最终仍需在目标平台发布包中验证 Canvas/控件的实际显示。旧客户端若不含 TextureX 加载适配,需要更新客户端,不能改回 CPU Image.Load 检查。

排查“编辑器正常、发布后缺图”时,先对照客户端实际下载包和原生日志;Unsupported texture format in KTX file 配合项目的 CPU Image.Load 前置检查,应优先核对读取器与容器格式,不要直接认定图片漏包或下载失败。

移动端发布后的图片格式 ​

项目 ui/ 下可识别的普通图片在移动端发布时会转为 GPU 压缩纹理(UI 图片通常为 ASTC 4×4);逻辑路径仍可能以 .png 结尾,但文件内容不再是可直接编辑像素的 PNG。当前项目发布流程没有受支持的单图「保留原始 PNG」开关;不能依赖贴图旁的 XML 配置跳过这一转换。此规则针对发布图片,不限制在运行时自行创建的未压缩 GameGraph.ResourceSystem.Image。

反例:先用 GameGraph.ResourceSystem.Image.Load(...) 读取项目 UI 图片,再 GetSubimage(...)、Resize(...)、Texture2D.CreateFromImage(...)。这条 CPU 像素处理链在编辑器或 PC 下可能正常,在手机上 GetSubimage 得到的仍是压缩图片,Resize 会失败;忽略返回值继续上传可能产生洋红色或错误画面。普通 UI 控件显示子区域可使用图集区域 JSON,Canvas 可使用 DrawImage 的源区域重载;确实需要独立图片文件时,先在项目制作阶段生成,再发布。若只是改变显示尺寸,直接用 UI 控件布局或 Canvas.DrawImage 的目标尺寸,不要对发布图片调用 CPU Resize。发布到手机后再验收,不能只以 PC 调试结果为准。

需要判断 PC、Android、iOS 时,在客户端使用 GameUI.Device.DeviceInfo.Platform,分别比较 EngineInterface.Enum.Platform.Windows、Platform.Android、Platform.iOS。核心约束是:在 Android/iOS 手机环境,不要对项目发布的 UI 图片调用 GameGraph.ResourceSystem.Image.Resize。平台判断只用于避开不兼容的 CPU 图片操作,不要求普通 UI 都分平台实现:

csharp
#if CLIENT
using EngineInterface.Enum;
using GameUI.Device;

bool isPhone = DeviceInfo.Platform is Platform.Android or Platform.iOS;
// isPhone 时不要对 Image.Load(...) 读取的项目 UI 图片调用 Resize。
#endif

Platform.EditorMobileSimulation 是编辑器移动端模拟,不等于实机发布包;GameRuntimeEnvironment.Test/Formal 区分项目测试/正式环境,不能用于判断设备系统。即使区分了平台,也不能在手机分支继续对压缩图片做 CPU 裁剪或缩放。

开发中替换同路径图片 ​

GameUI 控件图片和 Canvas.DrawImage 的普通散图走当前 GameUI 视图的图片缓存,不是 GameGraph ResourceCache 中名为 Image(CPU 图片)或 Texture2D(引擎纹理)的缓存;不能用 ResourceCache.ReleaseResource("Image", ...) 或 ReleaseResource("Texture2D", ...) 代替控件图片失效。停止 PIE 时,编辑器会自动失效当前 GameUI 中已加载、来源于项目根目录 ui/ 的普通图片缓存,并兼容清理地图目录下的 ui/ 资源;下一次启动 PIE 时,控件和 Canvas 应重新加载这些项目图片。只需要跨 PIE 验证文件修改时,正常停止并重新启动 PIE 即可。

如果要在同一轮 PIE 中由外部工具或项目代码覆盖同路径图片,并在不重启 Game View 的情况下继续验证,则应在文件写入完成后、下一次使用图片或截图验收之前主动失效该逻辑路径:

csharp
#if CLIENT
var accepted = GameUI.ImageCache.Invalidate("image/abc.png");
#endif

参数必须与控件或 Canvas.DrawImage 使用的逻辑路径一致:从项目 ui/ 的下一级开始,不写 ui/ 前缀,也不要传本机绝对路径。不要每帧调用,也不要为了普通跨 PIE 运行流程把它常驻在正式游戏逻辑中。

返回 true 只表示当前 GameUI 视图接受了失效请求,不表示文件一定存在、内容一定变化、调用前一定已经缓存,或现有控件已经即时重绘;失效后应让控件或 Canvas 再次请求 / 使用该图片,再以截图确认结果。路径为空或当前没有可用的 GameUI 视图时返回 false。

该 API 主要用于开发期间替换原本存在的同路径普通 GameUI / Canvas 文件散图,不是通用资源缓存重置接口。如果路径在本轮 PIE 中首次查找时不存在、之后才新增文件,失败的路径解析结果也可能已被缓存,此时应重新启动 Game View。图集条目、打包图片集合、Spine、字体、GameGraph 资源和其他 CPU / 原生缓存可能仍需重建资源或重新启动 Game View。名称以 RT: 开头的运行时纹理应使用 RuntimeTexture.Unregister(...),不要调用 ImageCache.Invalidate(...)。

docs/resources/ui-images.json 中的图片来自 GameSystemUI。它们可以直接引用,也可以在编辑器下载缓存中查看原图后再挑选。缓存路径模式为:

text
<EditorUpdateRoot>/res/_m/maps/gamesystemui/<version>/gamesystemui/ui/<path>

例如 path = "image/icon/weapon/ironsword.png" 的原图可能位于 D:/www 33/update/editor-alpha.spark.xd.com/res/_m/maps/gamesystemui/19/gamesystemui/ui/image/icon/weapon/ironsword.png。如果有多个版本目录,优先查看版本号最大的目录。缓存路径只用于本地查看,代码里仍然写 image/icon/weapon/ironsword.png。

官方图标素材 ​

docs/resources/ui-images.json 中的图片来自 GameSystemUI,项目默认依赖后可以直接用。docs/resources/official-icons.json 是另一类官方图标素材:它们不是 GameSystemUI 内置资源,也不在客户端实机的依赖库下载流程中。使用前必须先复制到项目 ui/icon/...,然后代码或数据里仍然写 icon/...。

text
MyProject/
└── ui/
    └── icon/
        └── buff/
            └── _0000_水晶能量.png
csharp
// 文件在 ui/icon/buff/_0000_水晶能量.png
var iconPath = "icon/buff/_0000_水晶能量.png";

这些图标在编辑器下载缓存中是非加密图片。AI 可以先用 official-icons.json 的 sourceRelativePath 拼到 <EditorUpdateRoot>/res/icon/ 下查看原图,再决定使用哪个图标。如果复制源不存在,请先让用户打开编辑器的美术资源库,点击左侧资源树的「图标」节点,等待右侧分类缩略图正常显示,再复制 official-icons.json 中列出的图标。

UI Spine 动画 ​

Spine 资源也放在项目 ui/ 目录下,通常放在 ui/Spine/。代码路径从 Spine/ 开始,并且 Spine.Resource 不写文件后缀。

版本兼容性:当前运行时支持 Spine 4.2/4.1 资源,并额外支持 Spine 3.8 导出的 .skel 二进制资源;旧版本 .json 不在兼容范围内。

预乘 Alpha(PMA)由 .atlas 页中的 pma: true 标记决定,不根据 .skel 是 4.x 还是 3.8 自动切换。部分 Spine 3.8 旧资源的贴图已经使用预乘 Alpha,但 atlas 没有写出该标记,使用 UI Spine 控件时可能出现黑边;可优先重新按一致的 PMA 设置导出,或确认贴图确实已经预乘后,在对应 atlas 页补上 pma: true。不要给普通直通 Alpha 贴图误加该标记,否则也会产生错误的颜色和透明混合。

text
MyProject/
└── ui/
    └── Spine/
        └── hero/
            ├── hero.skel
            ├── hero.atlas
            └── hero.png
csharp
#if CLIENT
var spine = new Spine
{
    Resource = "Spine/hero/hero",
    Animation = "idle",
    Skin = "default",
    IsLooping = true,
};
spine.AddToVisualTree();
#endif

界面 Spine 的显示大小由控件宽高决定,会把骨架包围盒等比装进控件。这和场景里的 AnimatedSprite2D 不同:后者按世界单位绘制,加载时大约 100 个 Spine 单位对应 1 个世界单位,再用节点缩放调整。细节见 界面 Spine 与场景 Spine 的尺寸。

如果 Spine 图集引用了多张 png,需要把 .atlas 中引用到的图片也放在同一套 ui/Spine/... 资源目录下,保持相对路径一致。

GameGraph 2D 动画资源 ​

AnimatedSprite2D 使用 AnimationSet2D.Load(...) 加载 Spriter 或 Spine 动画集。新项目优先按 UI 资源惯例放置这类 2D 表现资源:Spriter/序列帧和配套图片放在 ui/image/...,Spine 的 .skel / .json、.atlas 和贴图放在 ui/spine/...。代码路径从 ui/ 下一级开始写,不包含 ui/ 前缀,但必须保留 .scml、.skel 或 .json 扩展名。

text
MyProject/
└── ui/
    ├── image/
    │   └── Animated/
    │       ├── coin.scml
    │       └── coin/
    │           └── coin.png
    └── spine/
        └── spineboy/
            ├── spineboy.skel
            ├── spineboy.atlas
            └── spineboy.png
csharp
#if CLIENT
var coinSet = AnimationSet2D.Load("image/Animated/coin.scml");
var spineSet = AnimationSet2D.Load("spine/spineboy/spineboy.skel");
#endif

这条路径和 UI Spine.Resource 不同:UI Spine 不写后缀,GameGraph AnimationSet2D.Load 要写动画数据文件后缀。AnimationSet2D.Load("spine/...")、AnimationSet2D.Load("image/...") 会按 UI 系统的依赖库优先级找资源;显式 @gameui/spine/... 这类写法只从指定库查找。若 UI 资源路径下找不到,运行时会 fallback 到当前项目 res/,旧项目中 AnimationSet2D.Load("Animated/spineboy/spineboy.skel") 这类写法仍可继续使用。更完整的 3D 场景用法见 GameGraph 2D 动画组件。

如果 GameGraph Spine 或 Spriter / SCML 动画比原图明显发白,颜色贴图通常需要开启 sRGB 解码。为每张 PNG 添加同目录同名 XML,例如 spineboy.png 对应 spineboy.xml:

xml
<texture>
    <srgb enable="true" />
</texture>

Spine 多页 atlas 的每张页图片都要分别配置。Spriter / SCML 多帧图片应保持纹理参数一致,以便继续合并内部图集;配置缺失或互相冲突时运行时会分别加载并记录警告。法线、遮罩等线性数据纹理不要开启 sRGB。修改后重新启动 Game View 或重新进入游戏局,避免缓存影响验证。透明边缘出现黑边或白边时则应检查 PMA,它与 sRGB 是不同问题。

GameUI 自定义字体 ​

Label.Font、Input.Font 和 TextButton.Font 传的是字体 family,不是具体字体文件路径。项目私有字体通常放在 res/font/<family>/,代码里从 res/ 下一级开始写:

text
MyProject/
├── ref/
│   └── fontref.txt
└── res/
    └── font/
        └── my_title/
            └── my_title.ttf

ref/fontref.txt 写:

text
font/my_title
csharp
#if CLIENT
var title = new Label
{
    Text = "标题",
    Font = "font/my_title"u8,
    FontSize = 32,
};
#endif

公共 UI 字体使用 ui/font/<family>,例如 ui/font/Regular。运行时会在游戏初始化时读取 ref/fontref.txt 并预加载字体;新增字体后需要重新进入地图。未来可能会有类似资源收集工具的字体 ref 自动收集工具,当前先手写维护 fontref.txt。

更完整的加载时机、font_subset.txt 区别和排查清单见 自定义字体指南。

模型、粒子、音频和其他引擎资源 ​

模型、粒子、音频、材质、动画等非 UI 图片资源通常来自资源库或官方资源包。AI 或开发者应优先使用 docs/resources/ 中列出的资源路径,或资源库中已经上传到云端的资源。常见示例:

csharp
// using GameCore.ResourceType;

// 官方模型路径
_ = new GameDataModel(heroModel)
{
    Asset = "characters1/baiyijianke_e4wa/model.prefab"u8,
};

// 官方粒子路径
var effect = ParticleSystem.Load("effect/eff_tongyong/huoqiu2/particle.effect");

// 官方音频路径
Sound soundPath = "sound/a2_SFX/Action/MoveAction/Action_Body_FallDown.ogg";
var sound = SoundResource.Load(soundPath);

音频资源路径在 Actor 表现层和 GameGraph 底层是一致的。GameDataSound.Asset / ActorSound 与 SoundResource.Load(...) 都写资源根路径,例如 sound/custom/psionic_discovery.wav;文件本体放在项目 res/sound/custom/psionic_discovery.wav。SoundResource.Load 的参数类型是 GameCore.ResourceType.Sound;Sound 支持从 string 和 UTF8String 隐式转换,所以字符串字面量、string 变量和 UTF8String 变量都可以直接传入。SoundResource 支持 WAV 和 OGG,文件扩展名应与实际资源一致。发布资源统计主要识别 IFile 类型字段和直接转发到 IFile 字段的字面量 helper 参数;如果自定义资源路径只出现在 SoundResource.Load(...) 这类代码调用中,应同时让同一路径出现在 GameDataSound.Asset 或其他 Sound / IFile 类型字段中,并优先直接写字面量路径,避免资源清单漏收。不要让 AI 助手只在运行时临时 new GameDataSound 注册一批音效;静态音频应优先进入可收集的静态数据或显式资源清单。

如果资源库已经能即时上传自定义模型或美术资源,且这些资源需要被多个项目共享,优先走资源库和云端资源流程,不要额外复制到项目目录。需要注意,自定义云端资源在外网和线上环境互相独立;如果项目要迁移到线上,目标环境也必须有对应自定义资源。只有当资源有明确随包需求、暂时不走云端下载、属于项目私有的离线资源,或需要提升跨环境自洽性时,才放到项目 res/ 目录。

text
MyProject/
└── res/
    ├── characters/custom_hero/model.prefab
    ├── effect/custom/fire/particle.effect
    └── sound/custom/click.wav

代码中从 res/ 下一级开始写:

csharp
// using GameCore.ResourceType;

// res/characters/custom_hero/model.prefab
Asset = "characters/custom_hero/model.prefab"u8;

// res/effect/custom/fire/particle.effect
var effect = ParticleSystem.Load("effect/custom/fire/particle.effect");

// res/sound/custom/click.wav
_ = new GameDataSound(soundLink)
{
    Asset = "sound/custom/click.wav",
};

Sound clickPath = "sound/custom/click.wav";
var sound = SoundResource.Load(clickPath);

项目 res 内资源随客户端包封装,不需要资源统计或 objref.txt 条目。若缺失,先检查本地依赖路径及项目资源包。云资源需要客户端预下载,缺失时继续看 发布资源缺失排查指南。本地模型包导入、材质适配与资源库/地编操作见 项目模型资源包导入。

脚本直接读取文件 ​

如果代码要直接读取自定义 JSON、文本、关卡辅助数据或调试输出文件,不要使用 ui/ 或 res/ 规则,也不要把这些路径传给引擎资源 API。应使用 user_files/...,并通过 .NET File / Directory 读取。

csharp
var json = File.ReadAllText("user_files/client-config.json");

客户端文件放在 ui/AppBundle/user_files/,服务端文件放在 AppBundle/user_files/。完整权限、依赖库路径和写入限制见 项目文件读写指南。

客户端启动资源预加载 ​

资源下载和资源加载是两个阶段:下载把项目包及引用的云资源包准备到本地,加载则把文件解析成运行时资源。完整资源统计仍需保留,保证后续关卡的资源可以获得。

游戏模式 GameDataGameMode.PreloadResourcesOnStartup 默认为 true,客户端启动时会预加载资源统计清单中的资源。对于关卡资源较多、希望先显示首页的项目,可在数据编辑器的游戏模式中关闭“启动时预加载资源”,或在游戏模式的静态定义中设置:

csharp
PreloadResourcesOnStartup = false,

配置在客户端启动读取游戏模式时生效,游戏开始后再修改不影响本次启动。关闭后仍会下载完整资源,场景及其他资源在场景加载或实际使用时按需加载;这不代表首关或首次播放音效可以立即完成。应同时验证首页启动、首次进关和切关的等待与资源显示,必要时由项目提前准备下一关所需资源。该设置通过依赖库更新生效。

常见错误 ​

错误写法问题正确写法
new Image("ui/image/abc.png")UI 图片路径不应包含 ui/ 前缀new Image("image/abc.png")
把 Canvas 图片放到 user_filesImage 是引擎资源路径,不是脚本文件路径放到 ui/image/...
spine.Resource = "ui/Spine/hero/hero.skel"UI Spine 路径不含 ui/,也不写后缀spine.Resource = "Spine/hero/hero"
AnimationSet2D.Load("ui/spine/hero/hero.skel")GameGraph AnimationSet2D 也按 UI 资源根查找,但代码路径不写 ui/AnimationSet2D.Load("spine/hero/hero.skel")
label.Font = "font/my_title/my_title.ttf"GameUI 文本控件字体写 family,不写字体文件label.Font = "font/my_title"u8,并在 ref/fontref.txt 加 font/my_title
label.Font = "res/font/my_title"项目 res/ 是资源根,代码路径不含 res/label.Font = "font/my_title"u8
自定义随包模型放到 ui/非 UI 引擎资源不走 UI 图片搜索根放到 res/characters/...,代码写 characters/...
只在启动/运行时代码里动态 new GameDataSound 注册音效运行时配置不等同于发布资源声明,容易漏进 objref.txt使用静态 GameDataSound.Asset / GameDataActorSound 数据、显式资源清单,或确认直接字面量 helper 已被 Client-Resource 收集
用 File.ReadAllText("res/...") 读取业务配置脚本直接文件读取应走 WASI 预开放目录放到 user_files,代码写 user_files/...