oynix

于无声处听惊雷,于无色处见繁花

Asset的一生(一)

转载:原文地址

Unity Asset的简单介绍

前言

在开发Unity项目的时候,必然会经常和Asset打交道,例如我们工程的Assets文件夹下,基本都是各式各样的Asset,接下来会从以下几个方面来聊聊它:

  1. Asset是如何产生的

  2. Asset的导入设置

  3. Asset和Assetbundle的一些关系

  4. Asset的加载与管理

  5. Asset的卸载

文章参考自:

  • https://www.bilibili.com/video/BV1Wv41167i2
  • https://docs.unity3d.com/Manual/AssetWorkflow.html

Asset是如何产生的

Asset在我们平时使用的时候主要是由下面两种方法产生的:

  1. 用第三方的工具来产生Asset,这种情况最常见的有:FBX,Texture,Sound等。它们经常是由第三方工具(例如maya,3dmax,photoshop)等生成,然后放到Unity中来用。

  2. Unity自己产生的Asset,比如说Prefab,Scene,Animator文件等。

注:其中Script从Unity引擎的角度来看也是一种Asset,但是Unity在管理它以及进一步处理时,和其他的Asset有一定的区别。

不管哪种方式产生的Asset,一般都是由下面两部分组成的:

  1. 本身的数据内容,是文件的主旨,原始数据的所在。

  2. meta文件,主要是记录了一些额外的信息。

比如我们导入一张.png图片,导进来的png文件本身是一个数据内容,同时Unity会为你产生一个同名的meta文件。以及我们生成一个Prefab同样也会生成一个同名的meta文件。

对于第三方的Asset,例如.png、.wav、.fbx等文件,它们的文件内容和它们各自所对应的文件格式有关,不做过多介绍。我们来看看Prefab文件的内容,Sphere.prefab其实就是Unity自带的球体,其在Unity中的Inspector界面如下图:

接着我们使用文本编辑器来打开Sphere.prefab来一探究竟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &2704994518885799472
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3702433357692089522}
- component: {fileID: 3757503778730075621}
- component: {fileID: 7766305049339339301}
- component: {fileID: 7900522011432960373}
m_Layer: 0
m_Name: Sphere
......
--- !u!4 &3702433357692089522
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2704994518885799472}
......
--- !u!33 &3757503778730075621
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2704994518885799472}
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
......

Unity自己生成的Asset打开来看基本都是这个格式的文本,该文本格式我们称之为YAML。其中第一行第二行记录的类似于一个版本信息。

接着我们看后面的内容,可以发现这些信息被 — 给切割成一块一块的,每一块代表的都是一个Object所对应的信息(在Unity中所有的类都是继承于Object),并且每一块的开头的格式都是由 !u! 后面跟两串用 & 符合相连的数字,例如:

1
---!u!1&2704994518885799472

其中第一串数字,我们称之为ClassID,是Unity内部为所有的Object做的一个类型枚举,例如GameObject的ClassID=1,Transform的ClassID=4,而我们自定义的MonoBehaviour组件的ClassID=114等。ClassID与下面一行记录的Object类型一一对应,例如 !u!1 的下一行一定是GameObject, !u!4 的下一行一定是Transform。

接着是第二串数字,它被称之为fileID,它是该Asset文件内不同Object所对应的一个唯一ID。举个例子,我们一个Prefab下可以挂很多相同的组件(比如好几个BoxCollider),那么这些组件也就是Object它们的ClassID都是一样的,而fileID则会各不相同。并且跟其他类型的Object的fileID也都不相同,在当前文件内起到唯一性的作用。

fileID除了通过文本编辑器来打开Asset查看外,其实也可以在Unity中查看,在Inspector面板右上角选择Debug模式即可:

其中Local Identfier In File对应的就是fileID的值。利用该方法,我们还可以查看第三方产生的Asset的fileID,例如.png,.wav这些文件的fileID,如下图:

从中可以发现,所有相同类型的Asset其fileID都是相同的,例如第三方生成的图片、声音或是Unity自己生成的Material、Animation等,它们都是一个Asset对应一个Object。而Prefab或Scene文件内,由多个Object构成,因此各个Object的fileID都不相同,由Unity随机分配。由于只在单个Asset内保证是唯一的,因此fileID也常被称为localID。

从前面的图中可以发现还有个InstanceID,该ID并没有记录在我们的Asset文件中,我们可以通过 Object.GetInstanceID 的API来获取,每个Object的InstanceID都是唯一的。不过需要注意的是一个Asset的InstanceID在不同的机器上或者重启Unity以及切换场景(包括运行时)都会发生变化,因此不能作为一个持久化的数据。

接下来的内容是一系列类似键值对的数据,记录着当前Object的属性,不同的Object有着不同的属性,大部分的键值对我们都可以和Unity中的Inspector界面一一对应起来,如下图:

因此如果你想批量修改一些Asset的某些属性(比如把所有prefab的scale都设置为1),可以不用在Unity里操作,而是自己写脚本,遍历所有相关的Asset文件,按照YAML的格式读出来,然后去做修改。

简单介绍几个常用的属性:

  1. GameObject的m_Component:
1
2
3
4
m_Component:
- component: {fileID: 3702433357692089522}
- component: {fileID: 3757503778730075621}
......

我们知道一个GameObject上可以挂载各式各样的组件(component),该属性对应的就是该GameObject上挂载的所有组件。并且通过每个组件的fileID来做索引,例如3702433357692089522对应的就是Transform。

因此如果Asset内某个键值对的值是fileID: xxx那么它指向的就是该Asset内fileID=xxx的某个Object。

  1. fileID与guid的组合:

除了上面单独fileID的情况,我们在Asset里面还能经常看见fileID和guid的组合。例如我在一个material里面设置了_mainTex:

那么查看material文件时会发现下面一行:

1
2
- _MainTex:
m_Texture: {fileID: 2800000, guid: 6ad45645da157cf4989f9c4390aad785, type: 3}

fileID:2800000前面已经介绍了,所有的Texture2D的fileID都是2800000。此外这里又来了个新的id:guid,它是Asset文件的唯一ID(介绍meta文件的时候再详细介绍)。如果我们打开这张图片对应的meta文件就可以看到这个id,如下:


因此如果Asset里面有fileID和guid的组合,那么它就是指向另一个Asset的Object。如果我们把Material上的图片换一换,其实改变的就是这个guid,如下:

若我们打包的时候不想要某个资源,只需要打包前遍历所有Asset文件,找到引用它的地方,设置为 {fileID: 0} 即可。

  1. 绑定事件:有时候我们可能会在Animation文件里添加Event来调用一些函数,如下图:

此时这些Event数据也会被记录在Animation文件中:

1
2
3
4
5
6
7
8
m_Events:
- time: 0
functionName: AnimationEvent1
data:
objectReferenceParameter: {fileID: 0}
floatParameter: 1.1
intParameter: 0
messageOptions: 0

因此如果有时候我们想找某些函数是否被Animation所调用,可以通过IDE全局搜Animation文件,而不是在Unity里一个个的找。此外,例如UI Button上添加的OnClick事件也是同理,会记录m_OnClick的数据。

此外,从另一个角度来看,Unity的Asset又可分为下面两种:

  1. 运行时(Runtime)Asset,它比较好理解,例如生成的纹理,声音,动画,最终打成包的时候是要跟着你的游戏一起发布出去的。玩家去玩的时候会看见这些东西,我们管这些Asset叫运行时的Asset。

  2. 编辑期(Editor)Asset,指的是Editor里面,当我们做一些Editor设置的时候,它会生成一些Asset,这些Asset最终不会打到你的包里,但它会参与你整个的编辑以及生成包的过程,最常见的就是ProjectSettings文件夹里的Asset。还有一些是在生成运行期的Asset的时候,它会有一部分的信息内容,是用于Build或者是编辑的时候才会用到的数据内容。这些数据最终不会打到你运行时的包体里面,但他对于你的编辑和如何生成最终的包体是有指导意义的,这种也说是编辑器的Asset。

Asset导入设置

当我们在Unity导入一些第三方的Asset的时候,在Inspector界面会发现有很多的设置选项,如下图是图片的相关设置:

这些设置我们称之为Asset的导入设置,接下来来了解了解它。

1.meta文件

前面我们说了每个Asset都会生成一个meta文件,它到底是什么?为什么每次Unity都会生成它,并且当你更改Asset的导入设置的时候它也会跟着发生变化。还有如果我们将资源上传到VCS(Version Control System)的时候,没有上传meta,那么下次还可能出错,例如打出来的AssetBundle不一样了,或者打出来的设置不一样了等。

同样的,我们用文本编辑器打开.meta文件来一探究竟,下面是图片对应的.meta文件内容的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fileFormatVersion: 2
guid: 6ad45645da157cf4989f9c4390aad785
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 11
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
......

meta文件同样是以YAML的格式生成的,在第一行记录的是 fileFormatVersion 的值,它其实是告诉我们当前的meta文件是哪种格式类型,现在是第2版格式类型。Unity在很久很久都没有改变该值了,而且预计的将来也不会变,因此基本可以不用管它。

第二行记录的是guid,它就非常重要(在前面也简单提到了一下),当我们去生成或导入一个Asset的时候,Unity要唯一的去标识这个Asset,这个唯一值就是guid。这也解释了为什么VCS上没有传meta,再使用的时候可能会出错,就是因为这个guid变了。

举个例子,假如你做了个新功能,写了个新的组件,然后上传VCS的时候没有上传对应组件的meta文件。这个时候当你的同事拉取了你的代码后,就会发现找不到对应的组件了,出现了Script Missing,如下图:

可是明明该组件的cs文件都在,为什么会Missing呢?这就是因为Unity是通过我们前面所说的记录在Asset内的fileid+guid来关联对象的。当你上传Asset的时候,里面记录的是你本机对应cs文件的guid,也就是meta文件里的。而由于没有上传meta文件,别人拉取下该cs文件的时候,Unity会给该组件分配一个新的guid,这就和Asset里面记录的不一样,因此也就导致Asset无法定位到正确的cs文件了,即Script Missing。还有例如其他一些关联的资源找不到的问题(例如绑定的图片丢了等等),都可能是这个原因所造成,解决方法同样很简单,只需要把被关联资源的meta文件里的guid设置成Asset里面记录的guid即可。

此外,guid还关联到了Library里面的东西,在稍后做介绍。

从第三行开始,meta文件记录了Importer信息,也就是该Asset的导入设置,这些内容和在Inspector里面看见很多内容基本都是一一对应的。例如我们声音文件的设置:

不管是我们修改Unity里面的设置,还是修改.meta文件,另一边都会同步跟着变换。在很多时候,我们对于一些资源都会有一些特定的设置需求,例如图片在不同平台往往也会需要不同的format,UI图片关闭mipmap,声音文件开启ForceToMono等等。如果每次导入新资源都要手动来设置的话,实在是太愚蠢了,因此Unity为我们提供了 AssetPostprocessor类,利用它里面的AssetImporter对象我们可以在每次Asset被导入的时候更改其导入设置。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEditor;
public class ImageImporter : AssetPostprocessor
{
public void OnPreprocessTexture()
{
TextureImporter textureImporter = (TextureImporter)assetImporter;
if (textureImporter == null) return;
UnityEngine.Debug.Log("change image import setting");
TextureImporterPlatformSettings settings = new TextureImporterPlatformSettings();
settings.name = BuildTarget.Android.ToString();
settings.overridden = true;
settings.maxTextureSize = 1024;
settings.format = TextureImporterFormat.ASTC_6x6;
textureImporter.SetPlatformTextureSettings(settings);
textureImporter.SaveAndReimport();
}
}

除了例子中用到的回调,还有诸如Asset导入成功后的回调:OnPostprocessTexture等,以及当所有Asset都导入完成后的回调:OnPostprocessAllAssets

此外,我们同样可以写一个脚本来遍历所有的meta文件,修改里面的导入设置。

2.Library文件夹

当我们往Unity导入第三方资源的时候,是否会思考过,到底导入什么样格式的文件才是最好的?例如声音文件,是MP3好呢还是WAV或者是其他。其实都可以,因为Unity不管你放入工程的Asset是什么,它最终会根据导入设置按你需要的格式把它导出。当我们在不停的修改Unity的导入设置时,原文件却从来没有被碰过,它依然保持这你放进去的样子。

因为实际上生成的Asset不管是Unity自己产生的还是第三方产生的,最终都会被导入到Unity引擎里面的Library文件夹下。真正在Unity引擎里面或者是你运行时使用的,实际上是你Library文件夹里面的东西。因此当我们修改导入设置时,影响的Asset是导入到Library文件夹里的那个文件,而并不是你的原文件。

按这个角度来说,放WAV是最好的,因为它的原始采样率是最好的,声音是无损的,那么经过压缩之后放到Library里只经过一次压缩。如果放的是MP3,那么系统首先会去解压这个MP3,然后再重新把它压缩成你最终需要的格式,那么这样的一个过程就会导致一个二次压缩的损害,使得声音或多或少的会有音质上的损伤。所以这里建议大家还是放原文件进去,甚至把Photoshop的.psd放进去,Unity会将你的.psd文件转换和导入成纹理,非常的方便。

那么我们的Asset到底在Library里的什么地方呢?这里要分两个版本来介绍了。Unity利用了Asset Database来维护由我们原文件转换后在Library里的文件。在Unity2019.3之前,它并不是真的Database,而是一套查询系统,称之为AssetPipeline Version 1。在Version 1中,Library下会有一个名为metadata的文件夹:

该文件夹里面存放的就是所有我们运行时真正用到的Asset,子文件夹通过Asset的guid前两位进行分类。例如我们导入一张图片,通过.meta文件发现它的guid如下:

那么我们就可以在5e的文件夹下找到与guid相同的文件:

这是一个二进制文件,可以通过Unity在安装目录下\Editor\Data\Tools提供的binary2text.exe工具将其转换成可读的文件,转换后的文件内容截图如下:

可以发现这里面存储了我们原文件图片的数据信息,除此之外还包含了meta信息,导入设置等等。

如果有时候我们发现我们的原文件更新了,但是在Unity里看见的效果或者打出来的AB包的内容都还是旧的,那么大概率就是因为Library里对应的文件没有重新生成,我们可以检查下里面的文件时间,对需要更新的文件Reimport一下,强制Library下对应的文件重新生成。

注:非必要的情况下,可别不小心点到Reimport All了,否则就要恭喜你,可以划水小半天了。

利用这个机制,在做AssetBundle的时候,可以算Library里面东西的系统时间来判断它需不需要被重新打一下,这样就可以实现只针对当前修改过的资源打AssetBundle。

但是在2019.3版本之后,Unity使用了一套新的Asset Database系统,叫Version 2。可以在Project Setting-Editor里面进行切换:

注:在Unity2020之后的版本就彻底弃用了Version 1,也就没有这个切换功能了。

Unity在Version 2里使用了名为LMDB(Lightning Memory-Mapped Database)的内存数据库,真正的把大家导入的东西放入到database里,因此使用Version 2的速度会比Version 1更加的快。

使用Version 2会发现,原本的metadata文件夹没了,多了一个叫Artifacts的文件夹,虽然里面的目录结构看着和原本的metadata一样,但是却无法根据guid找到对应文件了。不过当我们在Unity新导入一个Asset的时候,例如一张图片,还是可以发现在Artifacts下会同时新增两个文件,其中一个我们可以通过binary2text.exe进行转换,里面的内容如下图,包含了导入设置,以及最终的数据。

此外在Library下多了两个DB文件:SourceAssetDB和ArtifactDB。

Unity通过这两个DB文件来追踪我们的Asset,其中SourceAssetDB包含了我们原文件的最后修改时间,文件内容的hash,guid以及meta信息等,Unity用它来判断原文件是否发生了变化,是否需要重新导入。而ArtifactDB中是原文件由Unity导入处理后得到的最终文件的信息,包含了导入依赖的信息,Artifact文件的meta信息以及Artifact文件信息。

个人猜测:在Version 2中,由Artifacts文件夹存储Unity最终运行时用的Asset,然后通过ArtifactDB来管理原文件和Artifacts文件的映射关系。然后利用LMDB的Memory Map文件映射的方式来读取Artifacts文件,该方式相比Version 1更加快速安全。

自己尝试了下用LMDB的源码去看SourceAssetDB以及ArtifactDB的存储内容,想看看原文件与Artifacts文件的映射关系,结果失败了。有Unity源码的大佬们要是知道,可以指点指点。顺便看了下Unity的AssetImportWorker.log文件,感觉也是对不上号=。=

1
2
3
4
5
6
7
========================================================================
Received Import Request.
Time since last request: 12.844051 seconds.
path: Assets/xun.png
artifactKey: Guid(6ad45645da157cf4989f9c4390aad785) Importer(815301076,1909f56bfc062723c751e8b465ee728b)
Start importing Assets/xun.png using Guid(6ad45645da157cf4989f9c4390aad785) Importer(815301076,1909f56bfc062723c751e8b465ee728b) -> (artifact id: '54ac80b7fa6200d6b374a546d79b8ab3') in 0.161613 seconds
Import took 0.167522 seconds .

不过在通过UnityHub咨询的时候,结果遇到了高川老师,也算是追星成功了,哈哈。让我们期待老师更多的技术分享!

然后不同的平台(Windows,Android…)由于导入的过程不一样,所以可能会产生差异。因此当我们在Unity引擎里切换一个平台,会重新Loading(Importing)一次,这个过程是因为在不同的平台上,Asset最终导入进来的结果是不一样的。

除此以外,Asset Database还与Cache Server以及Accelerator也有着密切的关系。

3.StreamingAssets文件夹

在我们打包的时候,往往会把事先生成好的AB包放到StreamingAssets目录下,该目录下的所有文件都会原封不动的打进包里,那是为什么呢?而且在Android系统上它们是可以被直接读出来。

在Android系统上,apk最终打的是一个压缩包,那么StreamingAssets下的文件是怎么被直接读出来的,而其他文件夹里的文件不行。如果看Android的Gradle的Build,不管是AAPT(Android Asset Packaging Tool)或者是其他工具往apk里压东西,它身上是有选项的,利用这些选项我们可以指定哪些东西是不压缩,直接放进去的。

例如AAPT2的 link 指令里,我们可以用 -0 来指定哪些文件不压缩:

也就是说,Unity在Build apk的时候就会对StreamingAssets下的文件标记为不压缩,也就是说它是真正的原样放进去了,那么读取的时候就可以直接读取出来了,不需要解压缩的操作。

4.害羞的波浪线

当我们把工程项目里的某个文件夹的名称后面加上一个小小的波浪线,那么Unity就会帮我们在项目中隐藏掉这个文件夹。这个技巧在做工程打包工程配置的时候非常的有用,很多的开发者在使用的时候呢没有注意过它,但是用起来却非常的好用。例如在开发的时候有些人很喜欢用Resource文件夹,然后Build的时候不希望要它,那么在Build的时候就可以在Resource后面加个波浪线。凡是这种以波浪线结尾的文件夹,在Unity里面是会被直接无视掉的。不会出现也不会导入,示意图如下:

如图所示,在后面加了~的文件夹内的Asset都会被忽略,并且若之前有别的Asset引用了它们,就会导致Missing的情况出现。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2025/10/3b17f0e9cc5f/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道