一、构建产物总览
以 Windows Standalone 为例,MyGame_Data/目录大致如下(Android 位于assets/bin/Data/):
MyGame_Data/ ├── globalgamemanagers # 全局管理器(PlayerSettings、TagManager、QualitySettings……) ├── globalgamemanagers.assets # 全局管理器引用的资源(Always Included Shaders、Splash、MonoScript 等) ├── level0 # Build Settings 中第 0 个场景的场景对象 ├── level1 ├── ... ├── sharedassets0.assets # level0 首次引用的资源对象 ├── sharedassets0.assets.resS # 对应的大块二进制数据(纹理像素、网格顶点等) ├── sharedassets0.resource # 音视频数据(AudioClip、VideoClip) ├── sharedassets1.assets ├── ... ├── resources.assets # Resources 文件夹中的资源(及其依赖) ├── resources.assets.resS ├── resources.resource ├── Resources/ │ ├── unity default resources # 内置资源(默认字体、内置 Mesh 等) │ └── unity_builtin_extra # 内置 Shader、Default-Material 等 └── Managed/ # 托管程序集(Mono 后端)或 il2cpp_data这些文件(除.resS/.resource外)都是同一种格式:SerializedFile。区别只在于"装了哪些对象"。
二、三类文件各装什么
1. levelN —— 场景本身
对应 Build Settings 中第 N 个场景,只包含属于场景的对象:
- 场景中所有
GameObject、Transform以及各类Component(MonoBehaviour、MeshRenderer、Collider……) - 场景级设置:
RenderSettings、LightmapSettings、NavMeshSettings、OcclusionCullingSettings - 场景实例化的 Prefab 会被展开成普通 GameObject 写入(Player 中不存在"Prefab 实例"概念)
不包含:Texture、Mesh、Material、AudioClip、AnimationClip 等资源对象——它们只以引用形式(PPtr)出现在 level 文件中。
2. sharedassetsN.assets —— 场景依赖的资源
规则可以概括为:按场景构建顺序,资源放进第一个引用它的场景所对应的 sharedassets 文件中,后续场景复用,不重复存储。
Scene0 引用: TexA, MatA, MeshA → sharedassets0: TexA, MatA, MeshA Scene1 引用: TexA, TexB → sharedassets1: TexB (TexA 引用 sharedassets0) Scene2 引用: MatA, MeshC → sharedassets2: MeshC (MatA 引用 sharedassets0)因此:
- 加载
level2时,运行时需要同时访问sharedassets2和sharedassets0(通过外部引用表)。 - level0 的依赖越多,sharedassets0 越大,首场景加载越慢——这是"用一个极简启动场景做 level0"的直接原因。
3. resources.assets —— Resources 文件夹
- 所有位于任意
Resources/目录下的资源无条件打入(不管有没有被引用)。 - 这些资源的依赖(如 Prefab 引用的 Material、Texture)也会被收集进来;若某依赖已在其他序列化文件中,则可能以跨文件引用的方式指向它(具体分配策略随 Unity 版本略有差异,建议用下文的 BuildReport 实际核对)。
- Resources 的"路径 → 对象"索引表保存在
globalgamemanagers的ResourceManager中,启动时会构建查找结构,Resources 中对象越多,启动开销越大。这也是官方不推荐大量使用 Resources 的主要原因之一。
4. .resS 与 .resource —— 流式数据
Unity 5.x 之后,大块数据从 SerializedFile 中剥离:
| 文件 | 内容 | 说明 |
|---|---|---|
.assets.resS | 纹理像素、网格顶点/索引等 | 对象内只存StreamingInfo { offset, size, path } |
.resource | AudioClip、VideoClip 数据 | 支持流式读取音频 |
好处是:SerializedFile 本体小、反序列化快;大数据可异步/直接读取到 GPU 上传缓冲,Texture Streaming 也基于此机制。
三、SerializedFile 二进制结构
┌───────────────────────────────┐ │ Header │ metadataSize, fileSize, version, │ │ dataOffset, endianness ├───────────────────────────────┤ │ Metadata │ │ ├─ Unity 版本字符串 │ 如 "2022.3.20f1" │ ├─ 目标平台 (BuildTarget) │ │ ├─ enableTypeTree 标志 │ │ ├─ Types[] │ classID、是否被裁剪、脚本类型索引、 │ │ │ 类型哈希、(可选) TypeTree 节点 │ ├─ Objects[] │ PathID(int64)、byteStart、byteSize、typeIndex │ ├─ ScriptTypes[] │ 引用的脚本对象 │ ├─ Externals[] │ 外部文件表:GUID、type、pathName │ └─ RefTypes[] / UserInfo │ (SerializeReference 等,新版本) ├───────────────────────────────┤ │ Object Data │ 每个对象按 Transfer 顺序二进制写入, │ │ 对象间按 8/16 字节对齐 └───────────────────────────────┘关键点
① 跨文件引用:PPtr
任何对UnityEngine.Object的引用在磁盘上都是:
PPtr { int m_FileID; int64 m_PathID; }m_FileID == 0:同一文件内的对象m_FileID == n:Externals[n-1]指向的文件(如sharedassets0.assets、unity default resources)m_PathID:目标文件中对象的唯一标识(Player 构建时通常会重新编号,与编辑器 YAML 中的 fileID 不同)
所以 level1 里的 MeshRenderer 指向 sharedassets0 里的 Material,就是一个FileID=1, PathID=xx的 PPtr。
② TypeTree
- TypeTree 描述字段布局(名字、类型、大小、对齐),用于版本兼容的反序列化。
- Player 构建默认不写 TypeTree(引擎与数据版本完全一致,直接按编译期布局读取,省空间、更快)。
- AssetBundle 默认写入 TypeTree(因为可能被不同版本的 Player 加载),可用
BuildAssetBundleOptions.DisableWriteTypeTree关闭。
③ 对象数据写入
引擎使用StreamedBinaryWrite调用每个类型的Transfer():
- 基本类型按小端直接写入,
bool/byte后按 4 字节对齐 - 数组/字符串:先写
int32长度再写元素,结束后 4 字节对齐 MonoBehaviour(classID 114):先写引擎字段(m_GameObject、m_Enabled、m_Script等),再按 C# 序列化规则写用户字段
四、构建流程中的序列化步骤
逻辑上,BuildPipeline.BuildPlayer的流程大致是:
1. 编译脚本(Player 版本,去掉 UNITY_EDITOR 代码) 2. 按 Build Settings 顺序处理每个场景: a. 打开场景,执行 IProcessSceneWithReport / OnProcessScene 回调 b. 剥离 EditorOnly 标签对象、HideFlags.DontSaveInBuild 对象 c. 计算依赖闭包 d. 场景对象 → levelN 未分配过的依赖资源 → sharedassetsN(大数据 → .resS / .resource) 3. 收集 Resources 文件夹 → resources.assets 4. 全局管理器 → globalgamemanagers;其引用资源 → globalgamemanagers.assets 5. Shader 变体编译、纹理平台格式转换(结果缓存于 Library) 6. 可选:LZ4 压缩打包为 data.unity3d(Standalone), 或合并为 .data(WebGL)、放入 APK/AAB(Android)注意:资源在写入时已是平台格式(如 ASTC、ETC2、BC7),Material 只保留目标平台需要的 keyword 和 Shader 变体引用。
五、如何查看"谁进了哪个文件"
方法 1:BuildReport API(最权威)
usingUnityEditor;usingUnityEditor.Build.Reporting;usingUnityEngine;usingSystem.Linq;publicstaticclassBuildContentDumper{[MenuItem("Tools/Build And Dump Content")]publicstaticvoidBuildAndDump(){varoptions=newBuildPlayerOptions{scenes=EditorBuildSettings.scenes.Where(s=>s.enabled).Select(s=>s.path).ToArray(),locationPathName="Build/MyGame.exe",target=BuildTarget.StandaloneWindows64,options=BuildOptions.None};BuildReportreport=BuildPipeline.BuildPlayer(options);foreach(PackedAssetspackedinreport.packedAssets){Debug.Log($"====={packed.shortPath}({packed.overhead}bytes overhead) =====");foreach(varginpacked.contents.GroupBy(c=>c.sourceAssetPath).OrderByDescending(g=>g.Sum(c=>(long)c.packedSize))){longsize=g.Sum(c=>(long)c.packedSize);Debug.Log($"{size/1024f:F1}KB{g.Key}");}}}}packedAssets会精确列出level0、sharedassets0.assets、resources.assets等每个文件中包含的源资源及大小。
方法 2:Editor.log
构建完成后在 Editor.log 中搜索Build Report,可看到按类别统计的大小和最大资源列表。
方法 3:解析构建产物
- AssetStudio / AssetRipper:可视化浏览 SerializedFile 中的对象。
- UnityPy(Python):
importUnityPy env=UnityPy.load("MyGame_Data/sharedassets0.assets")forobjinenv.objects:print(obj.path_id,obj.type.name,obj.byte_size)注意:由于 Player 构建不含 TypeTree,第三方工具需要匹配对应 Unity 版本的类型结构才能完整解析。
六、实践建议
| 问题 | 原因 | 对策 |
|---|---|---|
| 首场景加载慢 | level0 依赖多,sharedassets0 庞大 | level0 用极简启动场景,再异步加载主场景 |
| 启动时间长 | Resources 索引在启动时构建 | 减少 Resources 使用,改用 Addressables / AssetBundle |
| 包体臃肿 | Resources 中的资源无条件打包 | 清理 Resources 中无用资源 |
| 资源"重复" | 同一资源在 Player 和 AssetBundle 中各存一份 | Player 与 Bundle 之间不会去重,需规划好资源归属 |
| 意外包含编辑器资源 | 场景中残留调试对象 | 使用 EditorOnly 标签或IProcessSceneWithReport剥离 |
| 内置资源冗余 | 引用 Default-Material、内置 Shader | 用自定义资源替代,避免依赖 unity_builtin_extra |
七、小结
- levelN:第 N 个场景的场景对象(GameObject/Component/场景设置)。
- sharedassetsN.assets:按场景顺序"首次引用即归属"的资源,跨场景通过 PPtr 共享,不重复存储。
- resources.assets:Resources 文件夹中的全部资源,无条件打包,并影响启动时间。
- .resS / .resource:从对象中剥离的大块数据,支持流式读取。
- 所有文件共享 SerializedFile 格式,通过Externals 表 + PPtr(FileID, PathID)构成跨文件引用网络。
理解这套机制后,就能有针对性地优化首场景加载、启动时间和包体大小,也更容易判断何时应迁移到 Addressables/AssetBundle 体系。