当前位置: 首页 > news >正文

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

【文件格式】

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

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

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

例如,要保存一系列角色和怪物的当前血量。为了读取时能区分是角色和怪物,可以先保存角色个数。读取时先读取角色个数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; //Hash128public byte[] m_OldTypeHash; //Hash128public 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_IsArraypublic 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;//custompublic 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热更新完整工作流与知识点解析 | 登峰造极者,殊途亦同归。

相关文章:

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

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

前端对接fastGPT流式数据+打字机效果

首先在对接api时 参数要设置stream: true, const data {chatId: abc,stream: true,//这里true返回流式数据detail: false,variables: {uid: sfdsdf,name: zhaoyunyao,},messages: [{ content: text, role: user }]}; 不要用axios发请求 不然处理不了流式数据 我这里使用fetch …...

避免使用第三方工具完成电脑环境检测

0. 简介 在之前配置各种深度学习环境的时候经常需要先检测一下电脑的软硬件环境&#xff0c;其实整个过程比较重复和固定&#xff0c;所以我们是否有可能一键检测Python版本、PIP版本、Conda版本、CUDA版本、电脑系统、CPU核数、CPU频率、内存、硬盘等内容这是很多Deepper苦恼…...

vue 中 mixin 的应用场景,原理和合并规则

应用场景 多个组件的相同逻辑可以提出去来一个公共的 mixin 原理 Mixin 的工作原理是将 Mixin 中的选项合并到组件的选项中 合并规则 优先处理 mixinsprops 、method、inject、computed 同名的使用组件内的&#xff0c;不使用mixin 的data 进行合并生命周期和watch 先执行…...

点击按钮(文字)调起elementUI大图预览

时隔一年&#xff0c;我又回来了 ~ 最近在做后台&#xff0c;遇到一个需求&#xff0c;就是点击“查看详情”按钮&#xff0c;调起elementUI的大图预览功能&#xff0c;预览多张图片&#xff0c;如下图&#xff1a; 首先想到的是使用element-ui的el-image组件&#xff0c;但它是…...

全面学习SpringCloud框架指南

要深入学习Spring Cloud框架,你需要系统地掌握其核心组件和概念,并了解如何在实际项目中应用这些知识。以下是一些关键的学习点和相应的学习内容: 一共分为10个模块包括: 1、微服务架构基础: 理解微服务架构的概念和优势。 学习单体架构向微服务架构演进的过程。 掌握…...

5G智慧水利数字孪生可视化平台,推进水利行业数字化转型

5G智慧水利数字孪生可视化平台&#xff0c;推进水利行业数字化转型。随着5G技术的快速发展&#xff0c;越来越多的行业开始探索数字化转型的道路。水利行业作为国民经济的重要支柱&#xff0c;也面临着数字化转型的迫切需求。5G智慧水利数字孪生可视化平台作为水利行业数字化转…...

新手入门:大语言模型训练指南

在这个信息爆炸的时代&#xff0c;人工智能技术正以前所未有的速度渗透到我们生活的方方面面。从智能手机上的语音助手到自动驾驶汽车&#xff0c;AI的应用无处不在。而在这些令人惊叹的技术背后&#xff0c;大语言模型&#xff08;LLM&#xff09;扮演着至关重要的角色。它们不…...

Win11 WSL2 install Ubuntu20.04 and Seismic Unix

Win11系统&#xff0c;先启用或关闭Windows功能&#xff0c;勾选“适用于Linux的Windows子系统”和“虚拟机平台”两项 设置wsl默认版本为wsl2&#xff0c;并更新 wsl --list --verbose # 查看安装版本及内容 wsl --set-default-version 2 # 设置wsl默认版本为wsl2 # 已安装…...

rust使用print控制台打印输出五颜六色的彩色红色字体

想要在控制台打印输出彩色的字体&#xff0c;可以使用一些已经封装好的依赖库&#xff0c;比如ansi_term这个依赖库&#xff0c;官方依赖库地址&#xff1a;https://crates.io/crates/ansi_term 安装依赖&#xff1a; cargo add ansi_term 或者在Cargo.toml文件中加入&#…...

贪心算法|435.无重叠区间

力扣题目链接 class Solution { public:// 按照区间右边界排序static bool cmp (const vector<int>& a, const vector<int>& b) {return a[1] < b[1];}int eraseOverlapIntervals(vector<vector<int>>& intervals) {if (intervals.siz…...

C++的并发世界(七)——互斥锁

0.死锁的由来 假设有两个线程T1和T2&#xff0c;它们需要对两个互斥量mtx1和mtx2进行访问。而且需要按照以下顺序获取互斥量的所有权&#xff1a; -T1先获取mte1的所有权,再获取mt2的所有权。 -T2先获取 mtx2的所有权。再铁取 mtx1的所有权。 如果两个线程同时执行&#xff0c…...

NI-LabView的DAQ缺少或丢失的解决办法(亲测有效)

DAQmx在Labview中不显示或缺失 问题&#xff1a;在NI Packasge Manager安装完DAQ后在labview中不显示控件解决办法 问题&#xff1a;在NI Packasge Manager安装完DAQ后在labview中不显示控件 在打开测量I/O时&#xff0c;见不到 DAQmx&#xff0c;或者在Express中见不到DAQ助手…...

cesium 调整3dtiles的位置 世界坐标下 相对坐标下 平移矩阵

cesium调整3dtiles的位置用到的是平移矩阵&#xff0c;原理是在世界坐标系中用偏移点减去原始点得到一个平移向量&#xff0c;再根据这个向量得到平移矩阵。 原始点&#xff1a;一般是模型的中心点位置&#xff0c;可通过模型的包围盒得到偏移点&#xff1a;可分为两种情况&…...

flutter跑通腾讯云直播Demo

运行示例 前提条件 要求java jdk 11版本 并且配置到了环境变量 重要 要求flutter 版本 2.8.0 并且配置到了环境变量 重要 要求dart-sdk版本2.15 并且配置到了环境变量 重要 您已 注册腾讯云 账号&#xff0c;并完成 实名认证。 申请 SDKAPPID 和 SECRETKEY 登录实时音视频控…...

飞机降落蓝桥杯[2023蓝桥省赛B组]

2023蓝桥省赛B组 B题 飞机降落 题解 标准深搜板子题&#xff0c;难度不大 #include<bits/stdc.h> using namespace std; #define MAX 10 struct node{int t,d,l;//t:飞机到达时间 d:飞机最大盘旋时间 l:飞机降落所需时间bool v;//标记此架飞机是否被搜索过 用于剪枝 };…...

如何动态渲染HTML内容?用v-html!

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…...

EFcore 6 连接oracle19 WinForm vs2022

用EFcore访问Oracle&#xff0c;终于不需要Oracle的什么安装包了&#xff0c;直接在VS2022中就可以轻松搞定。在csdn上看到一哥们的帖子&#xff0c;测试了一下&#xff0c;发现很方便。使用的场景是&#xff1a;VS2022中EFcore6。经过测试&#xff0c;同 Navicat Premium 16比…...

(delphi11最新学习资料) Object Pascal 学习笔记---第9章第2节(finally代码块)

9.2 finally 代码块 ​ 还有第四个用于异常处理的关键字&#xff0c;我已经提到过&#xff0c;但到目前为止还没有使用过&#xff0c;那就是 finally。finally块用于执行一些应始终执行的操作&#xff08;通常是清理操作&#xff09;。事实上&#xff0c;无论是否发生异常&…...

220 基于matlab的考虑直齿轮热弹耦合的动力学分析

基于matlab的考虑直齿轮热弹耦合的动力学分析&#xff0c;输入主动轮、从动轮各类参数&#xff0c;考虑润滑油温度、润滑油粘度系数等参数&#xff0c;输出接触压力、接触点速度、摩擦系数、对流传热系数等结果。程序已调通&#xff0c;可直接运行。 220直齿轮热弹耦合 接触压力…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

在软件开发中正确使用MySQL日期时间类型的深度解析

在日常软件开发场景中&#xff0c;时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志&#xff0c;到供应链系统的物流节点时间戳&#xff0c;时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库&#xff0c;其日期时间类型的…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

django filter 统计数量 按属性去重

在Django中&#xff0c;如果你想要根据某个属性对查询集进行去重并统计数量&#xff0c;你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求&#xff1a; 方法1&#xff1a;使用annotate()和Count 假设你有一个模型Item&#xff0c;并且你想…...

基于当前项目通过npm包形式暴露公共组件

1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹&#xff0c;并新增内容 3.创建package文件夹...

从零实现STL哈希容器:unordered_map/unordered_set封装详解

本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

Mysql8 忘记密码重置,以及问题解决

1.使用免密登录 找到配置MySQL文件&#xff0c;我的文件路径是/etc/mysql/my.cnf&#xff0c;有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

Bean 作用域有哪些?如何答出技术深度?

导语&#xff1a; Spring 面试绕不开 Bean 的作用域问题&#xff0c;这是面试官考察候选人对 Spring 框架理解深度的常见方式。本文将围绕“Spring 中的 Bean 作用域”展开&#xff0c;结合典型面试题及实战场景&#xff0c;帮你厘清重点&#xff0c;打破模板式回答&#xff0c…...

Web后端基础(基础知识)

BS架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器架构模式。客户端只需要浏览器&#xff0c;应用程序的逻辑和数据都存储在服务端。 优点&#xff1a;维护方便缺点&#xff1a;体验一般 CS架构&#xff1a;Client/Server&#xff0c;客户端/服务器架构模式。需要单独…...