Unity构建详解(7)——AssetBundle格式解析

news2024/11/29 20:30:11

【文件格式】

文件可以分为文本文件、图片文件、音频文件、视频文件等等,我们常见的这些文件都有行业内的标准格式,其意味着按照一定的规则和规范去保存读取文件,可以获取我们想要的数据。

有些软件会有自己的文件格式,会按照其自己设计的规则和规范去保存读取文件。

我们自己在做开发时,也需要保存和读取一些文件,但我们需要保存的数据一般会比较简单,很少去做设计格式。

例如,要保存一系列角色和怪物的当前血量。为了读取时能区分是角色和怪物,可以先保存角色个数。读取时先读取角色个数n,随后读取n个血量数据,这些都是角色的,随后读取的是怪物的。

个数n就是文件头,血量是文件数据。

一般来说,任何文件都可以分为文件头和文件数据两个部分,文件头用于描述文件数据。如果要保存的数据很复杂,可以进一步将文件头分为文件头(描述整个文件的,例如版本号)和数据头(描述数据部分的)、将文件数据分为主要数据段和次要数据段等。

如果复杂些,例如还需要保存当前的魔力值,那么需要区分当前的值是魔力值还是血量。如果是自己做开发,自己读写,可以默认先是血量,随后的值是魔力值。如果要保存角色的更多属性,那么要保存的不能是单一的数值,而是不同数值组成的数据结构了。

如果是软件,那么采用默认的方式就不合适,随着版本的迭代,要保存的数据会发生变化,必须在文件头中加入版本号和其他信息以描述要保存的数据。要保存的数据越复杂,文件头也就越复杂。

尽管更加复杂,但只要明白保存读取的规则和规范(即文件格式),那么和我们自定义的保存读取的主要逻辑在本质上没有区别。

行业标准格式更为复杂,其会被多个软件使用,可以看看场景的文件格式了解下文件是怎么样构成的:

PNG文件解读(2):PNG格式文件结构与数据结构解读—解码PNG数据-腾讯云开发者社区-腾讯云

MP3文件结构解析(超详细)-CSDN博客

AVI文件格式详解-CSDN博客

【AssetBundle格式】

详细的请看下Unity 的AssetBundle解析视频和AssetStudio读取Bundle的源码,这里只做简要介绍

  • AssetBundleHeader
    • AssetBundleFileHeader
    • StorageBlock[] m_BlocksInfo
    • Node[] m_DirectoryInfo
  • SerializedFile
    • SerializedFileHeader 文件头
    • MetaData
      • Types 每个对象的类型是什么
        • TypeTree
      • Objects 有多少对象,分别是什么,多大,在哪开始读取
      • ScriptTypes 如果有mono脚本的话,脚本的类型是什么
      • Externals 如果包里的对象引用了其他AB里的资产,分别在哪里可以找到
      • RefTypes
    • Object(第一个Object类型为AssetBundle)
    • 一系列的其他Object (包内Object的数据)
  • ResourceFile

下面是相关的数据结构

// AssetBundle文件头结构
public class AssetBundleFileHeader {
            public string signature;
            public uint version;
            public string unityVersion;
            public string unityRevision;
            public long size;
            public uint compressedBlocksInfoSize;
            public uint uncompressedBlocksInfoSize;
            public ArchiveFlags flags;
};

        public class StorageBlock
        {
            public uint compressedSize;
            public uint uncompressedSize;
            public StorageBlockFlags flags;
        }

        public class Node
        {
            public long offset;
            public long size;
            public uint flags;
            public string path;
        }

  public class SerializedFileHeader
    {
        public uint m_MetadataSize;
        public long m_FileSize;
        public SerializedFileFormatVersion m_Version;
        public long m_DataOffset;
        public byte m_Endianess;
        public byte[] m_Reserved;
    }


    public class SerializedType
    {
        public int classID;//unity有个classId的映射表
        public bool m_IsStrippedType;
        public short m_ScriptTypeIndex = -1;
        public TypeTree m_Type;
        public byte[] m_ScriptID; //Hash128
        public byte[] m_OldTypeHash; //Hash128
        public int[] m_TypeDependencies;
        public string m_KlassName;
        public string m_NameSpace;
        public string m_AsmName;
    }

    public class TypeTree
    {
        public List<TypeTreeNode> m_Nodes;
        public byte[] m_StringBuffer;
    }

    public class TypeTreeNode
    {
        public string m_Type;
        public string m_Name;
        public int m_ByteSize;
        public int m_Index;
        public int m_TypeFlags; //m_IsArray
        public int m_Version;
        public int m_MetaFlag;
        public int m_Level;
        public uint m_TypeStrOffset;
        public uint m_NameStrOffset;
        public ulong m_RefTypeHash;
}

    public class ObjectInfo
    {
        public long byteStart;//相对于SerializedFileHeader的偏移
        public uint byteSize;
        public int typeID;
        public int classID;
        public ushort isDestroyed;
        public byte stripped;

        public long m_PathID;
        public SerializedType serializedType;
    }

    public class LocalSerializedObjectIdentifier
    {
        public int localSerializedFileIndex;
        public long localIdentifierInFile;
    }

    public class FileIdentifier
    {
        public Guid guid;
        public int type; //enum { kNonAssetType = 0, kDeprecatedCachedAssetType = 1, kSerializedAssetType = 2, kMetaAssetType = 3 };
        public string pathName;

        //custom
        public string fileName;
    }

public class  AssetBundle : NamedObject
 {
     public PPtr<Object>[] m_PreloadTable;
     public KeyValuePair<string, AssetInfo>[] m_Container;
}

 public class AssetInfo
 {
     public int preloadIndex;
     public int preloadSize;
     public PPtr<Object> asset;
}

我们要关心的核心问题是:如何从Bundle中加载需要的Asset

  • 业务上层会传入一个资源路径
  • 游戏中的资源加载模块会根据资源路径得到其所在的Bundle路径
  • 资源加载模块会调用Unity接口去加载Bundle
  • 先将整个文件头加载到内存中,其以SerializedFile 格式存储在内存中(注意保存文件时也即磁盘上的数据结构和内存中的数据结构不一定一致)
  • 根据传入的路径从Bundle内的Container中找到该Asset对应的AssetInfo
  • 从AssetInfo中拿到PreloadIndex,其是PreloadTable中的索引,PreloadSize是长度,结合两者可以知道该Asset包含了哪些Object,并获取到ObjectInfo(也叫ObjectHeader)
  • 从ObjectInfo中拿到FileID和PathID,PathID是Object在AssetBundle内的标识,如果FileID为0,表明Object在该Bundle内,如果FileID不为0,则说明需要的Object在其他AssetBundle中
  • 从Exteranls中根据FileID找到对应的FileIdentifier,拿到AssetBundle的名字,再根据PathID找到Object 。
    • 在内存中会有其他转换,例如每个SerializedFile都会有个SerializedFileIndex。FileID和SerializedFileIndex不过是同一个Bundle在磁盘和内存上的不同标识而已
  • 从ObjectInfo中拿到byteStart和byteSize即可知道Object数据在整个Bundle文件中的位置,并实现读取
  • 依次将Asset内的所有Object数据读取到内存中
  • 根据读取的Object数据反序列化得到想要加载的资源

【AssetBundle的加载和卸载】

先看一张图

加载时AssetBundleHeader、SerializedFileHeader、MetaData的数据会进入到内存中,也即途中的AssetBundle内存镜像:

  • AssetBundle.LoadFromFile(path):同步加载,path为本地路径
  • AssetBundle.LoadFromFileAsync(path):异步加载,path为本地路径
  • AssetBundle.LoadFromMemory(byte[] binary):从字节数组加载,binary为目标ab二进制流
  • AssetBundle.LoadFromMemoryAsync(byte[] binary):从字节数组异步加载,binary为目标ab二进制流
  • UnityWebRequest.GetAssetBundle(string uri):url为ab文件路径,可为本地,也可为云端,

加载Asset时会从Bundle中加载一系列的Object生成对应的Asset,每个Asset会根据FileID和PathID生成唯一的标识ID:

  • assetBundle.LoadAsset<T>(name):T为目标资产类型,name为资产名称,会返回一个T实例
  • assetBundle.LoadAsset(name,type):name为资产名,type为资产类型
  • assetBundle.LoadAllAssets<T>():T为目标资产类型,会返回一个assetBundle中所有T类型资产数组
  • assetBundle.LoadAllAssets():加载assetBundle中所有资产,返回一个assetBundle中所有资产数组

实例化时,会生成一份新的Asset,其有一个对应的InstanceID,并引用原来的Asset。

【AssetBundle解析工具】

unity官方的WebExtract和Binary2Text

WebExtract路径:

cd进入路径后 输入AssetBundle路径即可 

Binary2Text路径:

AssetStudio

【AssetBundle优化】

IO优化:先了解下读取文件的详细流程

这里说的都是很通用的IO优化方法,不光是读取AssetBundle,读取其他文件也一样:

  • 缓存文件句柄
    • 打开关闭文件是一个耗时的操作,上层要卸载Bundle时,可以先缓存文件句柄,而不是立即关闭释放
  • Object数据重排
    • IO中很大一部分时间消耗是寻找磁道和磁头移动的时间(即寻道时间),为了减少这个时间,需要数据顺序排列,这样可以顺序读取,类似CPU读取数组比List快。在加载Asset时会读取多个Object的数据,如果保证传入的流的Pos是顺序增加或减少,而不是来回横跳,那么可以大幅度减少IO时间。
  • 无锁多线程
    • 一般来说一个成熟软件的读取文件操作会在单独的IO线程中执行,但会涉及到很多加锁操作,可以考虑改成无锁的,这个难度比较大
  • 组合IO请求
    • 一次大的IO请求比多次小的IO请求好,如果多个小的IO请求的位置大致是连续的,即使中间有部分数据可能不是需要的,也可以组合成一次大的IO请求

大小和内存优化

  • 剔除重复数据
    • 保证每个Asset只在一个Bundle内
  • 字符串优化(内存优化,很大一部分是去如何优化字符串)
    • string转Id
      • 例如所有的AssetInfo中的assetName转为int,加载资源时也传入Id
      • 所有标识改为用Id,int64改为int32
  • 合并重复数据,例如TypeTree合并成一份
  • 精简冗余数据
    • 会建立各种映射关系,可以梳理下简化映射,例如PersistentManager里的remapper
    • 有些数据在加载到内存后可能不会用了,从原有的数据结构中拆开,然后释放,例如各种版本信息
    • 动态Buffer,读取文件时一般会有个Buffer缓存数据,读Object时的Buffer是FileCacherRead,可以根据需要动态调整,或者共用Buffer

【参考】

AssetBundle研究报告 | BLOG

Unity如何把一个对象从内存序列化到磁盘 | 矩阵·空间

AssetBundle热更新完整工作流与知识点解析 | 登峰造极者,殊途亦同归。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1586500.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于SpringBoot+Vue的果蔬种植销售一体化服务平台(源码+文档+部署+讲解)

一.系统概述 伴随着我国社会的发展&#xff0c;人民生活质量日益提高。于是对果蔬种植销售一体化服务管理进行规范而严格是十分有必要的&#xff0c;所以许许多多的信息管理系统应运而生。此时单靠人力应对这些事务就显得有些力不从心了。所以本论文将设计一套果蔬种植销售一体…

数字档案馆升级改造的意义

数字档案馆升级改造的意义在于提升档案管理的效率和质量&#xff0c;更好地满足各方面的需求&#xff0c;并为数字时代的档案管理提供更好的支持和保障。具体意义包括&#xff1a; 1. 提高档案存储、检索和利用效率&#xff1a;玖拓智能数字化档案馆可以实现电子存储和快速检索…

zabbix“专家坐诊”第236期问答

问题一 Q&#xff1a;我的trap里已经可以收到信息了&#xff0c;后续要怎么创建监控项呀&#xff1f; A&#xff1a;参考&#xff1a; 问题二 Q&#xff1a;snmp和snmp trap咋搞&#xff1f; A&#xff1a;你指的是如何开启这些协议还是如何做监控项&#xff1f; Q&#xff1…

JUC并发编程2(高并发,AQS)

JUC AQS核心 当有线程想获取锁时&#xff0c;其中一个线程使用CAS的将state变为1&#xff0c;将加锁线程设为自己。当其他线程来竞争锁时会&#xff0c;判断state是不是0&#xff0c;不是自己就把自己放入阻塞队列种&#xff08;这个阻塞队列是用双向链表实现&#xff09;&am…

《QT实用小工具·二十》存款/贷款计算器

1、概述 源码放在文章末尾 该项目实现了用于存款和贷款的计算器的功能&#xff0c;如下图所示&#xff1a; 项目部分代码如下&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget>namespace Ui { class Widget; }class Widget : public QWidget {Q_OBJ…

Linux Shell:`alias`命令

Linux Shell&#xff1a;alias命令 alias命令是Linux和Unix系统中Shell的内置命令&#xff0c;用于创建命令的简短名称&#xff0c;即别名。这些别名通常用来缩短长命令或为常用命令序列创建便捷的缩写&#xff0c;从而提高工作效率。别名在当前Shell会话中有效&#xff0c;除…

vue 渲染表格两个表头,横向显示时间,纵向显示参数

vue 渲染表格两个表头&#xff0c;横向显示时间&#xff0c;纵向显示参数 具体使用 Element UI 中的 组件实现的 Vue 组件。它用于显示一个包含时间和参数的表格&#xff0c;其中时间横向显示&#xff0c;参数纵向显示。 <template><div><el-table :data"…

GNU Radio Radar Toolbox编译及安装

文章目录 前言一、GNU Radio Radar Toolbox 介绍二、gr-radar 安装三、具体使用四、OFDM 雷达仿真 前言 GNU Radio Radar Toolbox&#xff08;gr-radar&#xff09;是一个开放源码的工具箱&#xff0c;用于 GNU Radio 生态系统&#xff0c;主要目的是为雷达信号处理提供必要的…

Nevion视频会议光端机AAV-3G-XMUX系列

序号型号描述&#xff08;厂商&#xff1a;Nevion&#xff09;3G/HD/SD-SDI 视音频光端机&#xff0c;0-20km1AAV-3G-XMUX-SFP3G/HD/SD-SDI 音频嵌入/解嵌器模块&#xff0c;带SFP光模块插座。支持4路AES加嵌和解嵌&#xff0c;8路模拟音频加嵌。内置音频矩阵及处理器模块&…

Java-Web过滤器

文章目录 1.基本介绍1.为什么需要过滤器&#xff1f;2.基本介绍3.过滤器的基本原理 2.快速入门1.文件目录2.环境配置创建maven项目&#xff0c;导入依赖 3.代码实现1.login.jsp2.LoginCheck.java3.ManagerFilter.java编写过滤规则4.配置web.xml告诉tomcat5.admin.jsp 3.Filter的…

VMD + CEEMDAN 二次分解,Transformer-BiGRU预测模型

创新点&#xff1a;二次分解 多头注意力特征融合 往期精彩内容&#xff1a; 时序预测&#xff1a;LSTM、ARIMA、Holt-Winters、SARIMA模型的分析与比较-CSDN博客 风速预测&#xff08;一&#xff09;数据集介绍和预处理-CSDN博客 风速预测&#xff08;二&#xff09;基于Py…

CSGO游戏搬砖,落袋为安才是王道

1.市场燃了&#xff0c;都在赚钱&#xff0c;谁在赔钱&#xff1f; 首先要分清“纸面富贵”和“落袋为安”。市场燃了&#xff0c;你库存里的渐变大狙从5000直接涨到了1W&#xff0c;你赚到5000了吗&#xff1f;严格讲&#xff0c;你需要把库存里的渐变大狙卖出去&#xff0c;…

ruoyi-nbcio-plus基于vue3的flowable的支持自定义业务流程处理页面detail.vue的升级修改

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a…

Training - PyTorch Lightning 分布式训练的 global_step 参数 (accumulate_grad_batches)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/137640653 在 PyTorch Lightning 中&#xff0c;pl.Trainer 的 accumulate_grad_batches 参数允许在执行反向传播和优化器步骤之前&…

priority_queue的使用以及模拟实现

前言 上一期我们对stack和queue进行了使用的介绍&#xff0c;以及对底层的模拟实现&#xff01;以及容器适配器做了介绍&#xff0c;本期我们在来介绍一个容器适配器priority_queue&#xff01; 本期内容介绍 priority_queue的使用 仿函数介绍 priority_queue的模拟实现 什么…

2024年3月文章一览

2024年3月编程人总共更新了12篇文章&#xff1a; 1.2024年2月文章一览 2.Programming Abstractions in C阅读笔记&#xff1a;p308-p311 3.Programming Abstractions in C阅读笔记&#xff1a;p312-p326 4.Programming Abstractions in C阅读笔记&#xff1a;p327-p330 5.…

更改el-cascade默认的value和label的键值

后端返回的树结构中&#xff0c;label的key不是el-cascade默认的label&#xff0c;我需要改成对应的字段&#xff0c;但是一直没有成功&#xff0c;我也在文档中找到了说明&#xff0c;但是我没注意这是在props中改&#xff0c;导致一直不成功 这是我一开始错误的写法&#xf…

超越ChatGPT,国内快速访问的强大 AI 工具 Claude

claude 3 opus面世后&#xff0c;网上盛传吊打了GPT-4。网上这几天也已经有了许多应用&#xff0c;但竟然还有很多小伙伴不知道国内怎么用gpt&#xff0c;也不知道怎么去用这个据说已经吊打了gpt-4的claude3。 今天我们想要进行的一项尝试就是—— 用claude3和gpt4&#xff0c…

互联网大厂ssp面经(操作系统:part1)

1. 什么是进程和线程&#xff1f;它们之间有什么区别&#xff1f; a. 进程是操作系统中运行的一个程序实例。它拥有独立的地址空间和资源&#xff0c;可以独立执行。 b. 线程是进程内的一个执行单元&#xff0c;一个进程可以包含多个线程。 c. 线程共享进程的资源&#xff0c;…

liunx系统发布.net core项目

liunx系统发布.net core项目 准备.net6程序运行环境部署nginx&#xff0c;通过一个地址既能访问web api&#xff0c;又能访问web项目有一个客户把web api放到docker中&#xff0c;想通过nginx转发&#xff0c;nginx也支持配置多个程序api接口的其它 liunx系统&#xff1a;cento…