医院网站优化策划,佛山seo网站优化,乐山建网站,科技公司建设网站文章目录 整体介绍Native Managed Objects什么是序列化序列化用来做什么Editor和运行时序列化的区别脚本序列化针对序列化的使用建议 Unity资产管理导入Asset Process为何要做引擎资源文件导入Main-Assets和 Sub-Assets资产的导入管线Hook#xff0c;AssetPostprocessor… 文章目录 整体介绍Native Managed Objects什么是序列化序列化用来做什么Editor和运行时序列化的区别脚本序列化针对序列化的使用建议 Unity资产管理导入Asset Process为何要做引擎资源文件导入Main-Assets和 Sub-Assets资产的导入管线HookAssetPostprocessorThe Asset DataBaseMetadata资源元数据Asset和实例对象UnityEngine.Objects的区别Unity中的资源寻址File GUID LocalIDYAML解析跨文件索引依赖引用关系可以编写的工具不限于 Instance IDLoading large hierarchies加载复杂层级对象典型案例和优化 Resources资源文件夹运行时加载不建议建议序列化 AssetBundle基础运行时加载什么是Assetbundle优势数据结构HeaderData Segment 内存占用分析AssetBundle的加载AB包中的Assets加载Asset加载底层实现AssetBundle内存管理Unloadfalse的问题 AssetBundle使用中需要注意的问题常见的Assetbundle生命周期管理方案Assetbundle分包策略Assetbundle更新方案 Unity资源内存管理资源生命周期Resoruce lifecycle加载时机卸载时机内存泄露 需要理解的核心基础知识UnityEngine.ObjectObject的 和 ! 的重载 PersistentManagerUnity持久化寻址管理器Profiler中对应的持久化数据源类型Serialized File!!!Object加载流程底层实现 PPtrTPPtr在序列化文件中的数据结构PPtrT类定义PPtr Curve引用Object的曲线 TypeTreeClassID在调用Object.InstantiateUnity做了哪些事情 参考资料 整体介绍
学习和使用Unity引擎我们需要对Unity的一些核心概念和基础必须有一定的了解比如一个Png图片导入到Unity做了哪些事情哪些文件支持导入如果我们想要导入一个Unity不存在的资源格式我们要怎么做比如解析gltf格式并生成prefab这种需求如何做引用如何运行时加载等核心基础内容文章主要介绍一下内容
Unity中的序列化Unity资产导入流程和详细实现Unity如何做文件内索引和夸文件索引介绍Unity提供的两种资源动态加载方案Resources和AssetBundle介绍AssetBundle的数据格式和使用方法配合第三方的工具分析AssetBundle的格式PPtrPersistentManagerTypeTree等核心概念
有关AssetBundle部分的学习感兴趣的可以参考AssetStudio这个插件的源码里面可以大体的看到AssetBundle底层的数据结构是怎么定义的
持续更新和补充完善
Native Managed Objects
原生对象和托管对象 Native Object所有继承自UnityEngine.Object的游戏对象在C引擎底层都会有一个NativeObject对应记录了完整的Object信息 Managed ObjectsC#侧的Wrapper Object
原生引擎对象Native Object和Managed Object有各自的生命周期管理和垃圾回收机制。或者说C#的Object和Native底层的Object生命周期并不是一一对应的。
Unity是一个C引擎实际UnityEngine.Object储存数据都在C侧所以如果通过C#访问方法或者属性都要经过一次“引擎”调用所以尽量减少属性减少引擎API的调用减少C#和C的交互
Unity底层重载了和!操作符C#层UnityEngine.Objectwrapper objects指向在C侧的Native Object对象但是两者的生命周期并不是完全一样的两者垃圾回收机制也是不一样比如一个Object已经被Destroy但是C#侧没有触发GC的情况下如果使用 UnityEngine.Object NullUnity会返回true因为在Native底层Object已经被卸载回收但是C# Wrapper Object并不是Null
MONO和IL2CPP在处理继承自UnityEngine.Object类的时候有特殊的处理调用对象实例的成员函数底层触发调用到底层的引擎代码需要将C#侧的Object映射到Native层的Object并且检测合法性检测当前Native Object是否存活unityObjectnullUnity底层做了一些外的工作效率要比单纯的比较C#引用类型是否为NULL要低。
如果检测Object对象在Native底层的生命周期使用 ! 或者 ?. ??如果只是检测某个C#的Object对象是否被赋值使用object.ReferenceEquals判断是否被赋值。 明确期望生命周期 ! 在Unity做了重载处理谨慎使用效率不高 ?? ?.是纯C#侧的判断和Unity底层的生命周期无关
在实际开发中需要对Object的操作注意以下几点
明确自己的意图比如是要做引用是否被赋值还是要判断底层NativeObject是否为NULL减少Object的使用减少对属性的访问减少C和C#之间的交互对Object是一个很重的操作尽量避免在Update中使用UnityEngne.Object的操作和访问CC#调用类型判断合法性检测等等Object更像是一个被持有的表现层必须要用到的时候才去用在Update中尽量减少Object null或者Object ! null的判断Unity中的序列化
什么是序列化
数据如何被高效的组织并持久化到文件中数据如何按照反序列化规则重新被读取和创建最常见的Json To ObjectObject To JsonMessage to Binary Binary To Message
序列化用来做什么
Saving/Loading data to/from memory or disk 文件格式binary二进制文件YAMLjson等等undoanimationeditors Non-serialisation cases InstantiationUnloading unused assetsRemapping references in prefabsGenerating type metadata(“typetree”) 脚本中的属性变量保存 storing data stored in scriptsEditor Inspector Windows属性面板通过序列化文件serializedobject去保存和展示serializedobject SerializedObject SerializedProperty直接的序列化对象使用TypeTree遍历成员不许用C#侧的Wrapper Class Prefabs预设prefab本身就是一个序列化的数据包含了多个gameobjects或者components组件prefab其实只存在在editor层面在构建对的时候所有对prefab做的变动都会被应用到正常的序列化过程中实例化的时候本身并不关系是不是prefabUnity Editor实例化一个游戏对象GameObject根据两个数据the Prefab source and the Prefab instance’s modifications.Instantiation实例化实例化一个prefabgameobject所有继承自UnityEngine.Object都可以被序列化当开发者调用Instantiate(object)的时候先对object执行序列化操作创建一个新的object将实例化对象object的数据反序列化到新的object如果引用“外部”资源比如纹理贴图保持引用如果引用“内部”比如引用子节点会直接重定向到新创建对象的子节点Saving保存如果使用文本编辑器打开一个.unity文件“force text serialization”打开我们将会使用采用yaml序列化接口重新序列化对象Loadingunity底层的加载系统也是建立在序列化基础之上eidtor模式下的yaml文件读取运行时加载场景和asset加载Assetbundles的加载也是使用序列化系统Hot reloading of Editor code当修改脚本Unity会将所有的windows做一次序列化操作删除老的windows重新加载新的C#代码重新创建windows然后将序列化的数据重新反序列化到windowsResources.GarbageCollectSharedAssets()unity底层的垃圾回收器不同于C#的垃圾回收器unity底层的垃圾回收器是用来确定那些游戏对象未被引用比如加载场景过后no-additive或者手动触发了Resources.UnloadUnuedAssets,将遍历出来的游戏对象Native Object给从内存中卸载清除 The native garbage collector runs the serializer in a mode where we use it to have objects report all references to external UnityEngine.Objects. This is what makes textures that were used by scene1, get unloaded when you load scene2.
Unity中的序列化系统使用C开发用来做引擎底层native object类型texturesanimationclipcamera等等的数据序列化。序列化发生在UnityEngine.Object层级整个Object和引用都会被完整的序列化
Editor和运行时序列化的区别 脚本序列化
属性必须满足被序列化的条件
Be public, or have [SerializeField] attributeNot be staticNot be constNot be readonlyThe fieldtype needs to be of a type that we can serialize.
可以序列化的数据类型
Primitive data types (int, float, double, bool, string, etc.)Enum types (32 bites or smaller)Fixed-size buffersUnity built-in types, for example, Vector2, Vector3, Rect, Matrix4x4, Color, AnimationCurveCustom structs with the Serializable attributeReferences to objects that derive from UnityEngine.ObjectCustom classes with the Serializable attribute. (See Serialization of custom classes).An array of a field type mentioned aboveA List of a field type mentioned above
不支持的数据类型
No support for polymorphism不支持继承多维数组不规则数组字典只能修改数据结构使用支持的数据类型进行组合 Unity提供了可以自定义序列化的方式Use serialization callbacks, by implementing ISerializationCallbackReceiver, to perform custom serialization.我们可以通过ISerializationCallbackReceiver进行扩展
针对序列化的使用建议
尽量减少序列化的数据量减少重复序列化的数据比如MonoBehavior中有多个相同的属性可以放到ScriptableObject中如果有100份MonoBehaviour则有100份重复的数据多个MonoBehaviour引用同一份ScriptableObject避免多层嵌套和循环嵌套
Unity资产管理 导入Asset Process 将Origin原始文件导入到Unity工程中生成引擎Native Object Asset的过程Game-Ready OptimizedSerialized Native Asset 为何要做引擎资源文件导入
如何引擎都有自己的资产导入和管理系统 The conversion process is required because most file formats are optimized to save storage space, whereas in a game or a real-time application, the asset data needs to be in a format that is ready for hardware, such as the CPU, graphics, or audio hardware, to use immediately. For example, when Unity imports a .png image file as a texture, it does not use the original .png-formatted data at runtime. Instead, when you import the texture, Unity creates a new representation of the image in a different format which is stored in the Project’s Library folder. The Texture class in the Unity engine uses this imported version, and Unity uploads it to the GPU for real-time display.
针对硬件更友好的数据格式更高效的存储和序列化更好控制和扩展导入流程
Unity资源导入简单分为三个处理步骤
Unity为资源分配唯一GUIDUnity为每个资源自动生成一个.meta文件Unity执行导入资源流程使用内置的Importer或者开发者自定义的Importer在Library文件中生成引擎使用的Native Asset 将原始文件比如FBXmp3Png导入到Unity并生成引擎资产Native Asset ObjectUnity引擎通过定义的Native Importer将原始文件转化为引擎资产Unity引擎提供了一些内置导入器Png - TextureImporter-Native TextureMp3- AudioImporter - Native AudioFBX- Model Importer- Native Mesh,Animation,Material通过Importer定义一个从外部导入到Unity工程的原始资源是被如何解析生成Native Object Asset的过程一个FBX可能会生成多个Native AssetMeshAnimationMaterial等等MainAssetSubAssets Unity内部针对不同的原始文件类型定了特定导入器 Unity提供了可扩展的Scripted Importer定义一个外部的原始文件导入到Unity工程中的行为开发者可以自定义导入器比如针对扩展名为.cube或者.gltf的原始文件开发者可以自定义导入到Unity中的逻辑包括生成prefab解析创建文件夹比如解析GLTF并生成Prefab
Main-Assets和 Sub-Assets
Unity中的Asset可以包含多个SubAssets比如一个FBX在导入到Unity中可能存在多个Sub-AssetMaterialAnimationAvatarModel
资产的导入管线HookAssetPostprocessor
针对Unity导入原始资产生成引擎Asset的过程开发者可以Hook到对应的流程
OnPreprocessModelOnPostprocessModeOnAssignMaterialModel…
一般通过AssetPostprocessor做资产的检查和ImportSettings的设置比如常见的设置
UI图片导入关闭mipmapfbx导入关闭isReadable贴图纹理尺寸限定在1024Apply项目制定的资产导入规则TextureImporterModelImporter等等
The Asset DataBase
Origin Asset 转化为 Unity引擎运行时加载的Native Object Asset格式AssetDatabase用来管理被转换过后的Native Object Asset
Source Assets跟踪当前原始文件状态更新时间文件hashGUIDS Library\SourceAssetDB Artiface Assets DataBase记录每个Source Asset导入结果 Library\ArtifactDB 通过AssetDataBase API来访问Unity资源
Metadata资源元数据 Contains the asset’s import settings, and contains a GUID which allows Unity to connect the original asset file with the artifact in the asset database. fileFormatVersion: 2
guid: dbf51b524b581ec44a55c14e92280e22
PrefabImporter:externalObjects: {}userData: assetBundleName: assetBundleVariant: fileFormatVersion: 2
guid: 85d9545c7c154a743a2a0233004898fa
NativeFormatImporter:externalObjects: {}mainObjectFileID: 0userData: assetBundleName: assetBundleVariant: fileFormatVersion: 2
guid: db164ed4ea6391b40a3c1649b3653d1e
ModelImporter:serializedVersion: 21300internalIDToNameTable: []externalObjects: {}materials:materialImportMode: 0materialName: 0materialSearch: 1materialLocation: 1animations:legacyGenerateAnimations: 4meshes:userData: assetBundleName: assetBundleVariant:
.meta文件中包含三个信息
fileFormatVersion资源版本号基本上不会发生变化File GUID内部用来做资源索引保证资产在移动重命名的时候保持引用关系如果meta文件被删除资源位置并没有发生变化Unity会重新生成meta文件并保证GUID不会发生变化保证引用关系是正常的XXImporterSource Asset资产的导入设置每一种Importer都有自己的导入设置和参数 根据导入资源类型不同AssetImportSetting有不同的导入信息FBX FBXImporter额外的导入参数PNG TextureImporter额外的导入参数Assetbundle 名称等等
由于Meta文件处理不当可能会造成引用丢失问题
在UnityEditor之外做文件移动和重命名所有的移动或者重命名操作都应该在UnityEditor里面操作这样Unity会正确的处理好引用关系引用关系中的GUID对应的文件在整个项目中无法找到就是引用丢失GUID冲突如果发现GUID冲突也有可能会造成引用丢失
Asset和实例对象UnityEngine.Objects的区别
在分析Unity的资源管理模块的时候需要区分Asset和Object的区别
Asset是存储在本地文件中的可见资产ProjectName/Assets文件夹中的可见的资产TexturesModelaudiomaterialshader等等.asset文件一个UnityEngine.Object或者多个UnityEngine.Object组成的一组序列化数据共同描述了Asset的实例比如MeshspriteAudioClipAnimationClip所有的Objects都是继承自UnityEngine.ObjectUnity引擎对象。比如我们Resources.Load一个GameObject并不能直接的使用需要执行一次实例化操作才能得到可以使用的GameObject判断一个Object是否是Asset可以使用AssetDataBase.Contains返回true表示Object是一个Asset在AssetFolder中找到对应的Source文件返回false表示Object是scene中的对象或者是运行时动态创建的ObjectAsset和Object是一对多的关系一个Asset可能包含了多个序列化的Object
几乎所有的Object类型都是内置的Unity中有两个特殊的类型
ScriptableObject给开发者提供了一种可以自定义数据结构的方式Unity可以直接序列化和反序列化ScriptableObject对象并且可以在Editor Inspector Window进行序列化展示可以用来存放游戏运行时或者编辑器共享的数据MonoBehavior是链接到MonoScript的封装器MonoScript是底层的数据类型Unity用来引用到一个实际的脚本对象MonoScript只是做引用并不包含具体的代码MonoBehavior脚本也是一个Asset资源引用MonoBehavior脚本也是用fileIDguid来实现每个Prefab添加了对应的MonoBehavior脚本该Prefab只会有对MonoBehavior脚本的引用关系并不包含源代码 MonoBehaviour:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_GameObject: {fileID: 8002146119907625182}m_Enabled: 1m_EditorHideFlags: 0// 引用一个继承自MonoBehavior的脚本m_Script: {fileID: 11500000, guid: 96e7330c395831247b6d283dfd987152, type: 3}m_Name: m_EditorClassIdentifier: bConsolePrinting: 1bSGJDebug: 0Unity中的资源寻址
个人理解Unity资源管理中最核心的部分就是“寻址”需要了解Unity中如何定义引用关系内部引用快文件外部引用底层是如何实现加载一个Object如何根据一个Instance ID能够从“外部存储空间”中正确的加载Object为何Unity开发中会出现引用丢失的问题
寻址关系可以简单的理解为InstanceID ------FileGUID LocalIDUnity底层维护这样一个关系映射表Object是能被正确可寻址的才能正确的加载ReadObject成功
Unity基于AssetBundle封装的Addressable翻译成“可寻址”还是很贴切的。
给定某个需要加载的InstanceId进行寻址映射关系寻址成功映射关系合法则根据寻址得到的Source源执行加载寻址失败则加载失败。
File GUID LocalID
Unity处理资产组件的引用关系通过FileID Local ID方式实现夸文件寻址找到文件定位到文件中的具体位置通过FileID找到对应的File Asset通过LocalID定位到具体的Object File GUID 一级Asset文件guid存储在.meta文件中通过GUID找到Asset文件位置Source源位置如果meta文件被删除资源位置并没有发生变化Unity会重新生成meta文件并保证GUID不会发生变化保证引用关系是正常的内部保存了一个GUID和文件路径的映射关系使用File GUID做文件映射不用关心Asset File的具体位置位置发生变更也不用根据Path更新引用关系 LocalID 二级文件中的标识ID一个Asset可能包含多个Sub Asset定位到具体的Object每个Asset可以有多个Object组成
AssetDatabase.TryGetGUIDAndLocalFileIdentifier
instanceIDInstanceIDof the object to retrieve information for.
objThe object to retrieve GUID and File Id for.
assetRefThe asset reference to retrieve GUID and File Id for.
guidThe GUID of an asset.
localIdThe local file identifier of this asset.比较典型的例子一个Source Asset资源导入过程的结果是一个或多个UnityEngine.Object。这些在 Unity 编辑器中作为父资源中的多个子资源可见例如嵌套在作为精灵图集导入的纹理资源下方的多个精灵。这些对象中的每一个都将共享一个File GUID因为它们的源数据存储在同一资产文件中。它们将在导入的纹理资源中通过Local ID 进行区分和标识。
YAML解析
--- !u!1 505266372
GameObject:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}serializedVersion: 6m_Component:- component: {fileID: 505266376}- component: {fileID: 505266375}- component: {fileID: 505266374}- component: {fileID: 505266373}m_Layer: 0m_Name: Planem_TagString: Untaggedm_Icon: {fileID: 0}m_NavMeshLayer: 0m_StaticEditorFlags: 4294967295m_IsActive: 1--- !u!4 505266376
Transform:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_GameObject: {fileID: 505266372}m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}m_LocalPosition: {x: 12.396989, y: 5.15, z: 14.302996}m_LocalScale: {x: 2.3686056, y: 2.5200627, z: 2.6944714}m_ConstrainProportionsScale: 0m_Children: []m_Father: {fileID: 921185474}m_RootOrder: 6m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}m_Childern 子节点m_Father 根节点MonoBehaviour:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_GameObject: {fileID: 8002146119907625182}m_Enabled: 1m_EditorHideFlags: 0// 引用一个继承自MonoBehavior的脚本m_Script: {fileID: 11500000, guid: 96e7330c395831247b6d283dfd987152, type: 3}m_Name: m_EditorClassIdentifier: bConsolePrinting: 1bSGJDebug: 0--- 父节点GameObject--- !u!1 921185473
GameObject:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}serializedVersion: 6m_Component:- component: {fileID: 921185474}m_Layer: 0m_Name: Framem_TagString: Untaggedm_Icon: {fileID: 0}m_NavMeshLayer: 0m_StaticEditorFlags: 0m_IsActive: 1
--- !u!4 921185474
Transform:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_GameObject: {fileID: 921185473}m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}m_LocalPosition: {x: 0, y: 0, z: 0}m_LocalScale: {x: 1, y: 1, z: 1}m_ConstrainProportionsScale: 0m_Children:- {fileID: 1887623492}- {fileID: 1925342256}- {fileID: 505266376}- {fileID: 868233995952783049}- {fileID: 267251194}m_Father: {fileID: 1405857324}m_RootOrder: 2m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}所有的层级关系都会记录到YAML文件中“— !u!{CLASS ID} {FILE ID}” which can be analyzed in two parts:头部信息包含了两个部分
!u!{CLASS ID}: This tells Unity which class the object belongs to. The “!u!” part will be replaced with the previously defined macro, leaving us with “tag:unity3d.com,2011:1” – the number 1 referring to the GameObject ID in this case. Each Class ID is defined in Unity’s source code, but a full list of them can be found here.Unity中对应的CLASS ID列表{FILE ID}: This part defines the ID for the object itself, which is used to reference objects between each other. It’s called File ID because it represents the ID of the object in a specific file表示该Object在当前文件中的位置CLASS ID FILE ID OBJECT TYPE
有些特殊的情况开发者可以自己直接修改YAML文件比如修改一个带有动画的节点名称可能造成动画曲线对修改名称过后的节点无法驱动因为AnimationCurve底层使用PathName来做的映射这种情况下我们可以手动修改Animation File的path来实现Rename节点的操作保证动画能够正常播放 动画系统通过string-based 路径来识别对应的节点而不是通过GameObject IDs 跨文件索引 Reference to another file object
GUIDFile ID找寻到Asset FileFile GUIDFile IDLocal ID定位到AssetFile内部的ObjectLocalFileIDType Type 2: Assets that can be loaded directly from the Assets folder by the Editor, like Materials and .asset filesType 3: Assets that have been processed and written in the Library folder, and loaded from there by the Editor, like Prefabs, textures, and 3D models Unity的脚本也是当作Asset来处理的MonoBehaviour引用一个真正的Script资源
依赖引用关系可以编写的工具不限于
查找一个Asset被引用情况根据Asset 的GUID在其他的Asset YAML中查找GUID确定Asset被引用的情况替换GUID将一个Asset替换为另外一个Asset替换有不同后缀名的Asset比如将Mp3文件替换为Wav文件删除Mp3文件重命名meta文件查找引用丢失的Asset将引用的丢失Asset GUID替换为新的GUID删除或者重新添加相同的资产
Instance ID
随着运行时维护的资源增多通过File GUID和Local ID维护引用关系效率变得低为了提升索引效率通过运行时的instanceID快速确定引用关系
Unity引擎会在在底层运行时Unity会维护一个Instance ID cacheID to Object*指针File GUID Local ID确定唯一的一个InstanceID在运行时更加的高效做引用Unity在序列化的时候通过PPtr表示引用了一个Object对象通过PPtr的Transfer构建寻址关系instanceId-FileID LocalFileID
在Unity RunTime启动时会首先初始化InstanceId Cache将打包Scene所引用的资源和Resources目录下的所有资源根据File GUID Local ID生成对应的Instance ID添加到Cache中这个也是不建议在Resouces中存放较多资源的原因资源越多生成Instance ID数量越多速度会越慢Resources中的文件会被序列化成一个data文件在APP启动的时候需要根据Resources中的资源名字建立索引查找树通过AssetName能够获取到对应的File GUID和Local ID从而进行加载资源Indexing lookup tree来用来确定指定的Object在在序列化文件中的读取位置读取大小等信息
运行时动态Load进来的资源比如通过Assetbundle加载的Asset也会加载到Unity运行时维护的Instance ID Cache中(BaseObject维护的IDToPointerMap映射表)只有在AssetBundle被UnLoad的时候涉及到加载到内存中的Asset的Instance ID才会被从cache移除节省内存。下次重新加载AssetBundle加载相应的AssetUnity会重新创建新的Intance ID。
Loading large hierarchies加载复杂层级对象
游戏对象层级Hierarchy会被完整的的序列化所有的GameObject和Component都会被序列化到数据中所以如果Hierarchy层级越复杂序列化数据会越大导致Unity在加载并建立游戏对象的Hierarchy有一定的性能开销。 在创建GameObject游戏对象Hierarchy的时候Unity需要做的几项工作
读取Source源外部存储空间AssetBundleResources或者其他的GameObject创建新的Transfroms组件并建立父子节点关系实例化GameObjects和Components在Unity主线程Awake所有实例化出来的GameObjects和Components 234三个步骤不管GameObject是从已经加载过的GameObject或者从内存中实例化耗时基本相同但是第一步从Source读取的时间和Hierarchy的复杂度成正比。在实例化一个GameObject对象的耗时可能会在IO处理上需要花大量的时间将序列化数据读取出来。
典型案例和优化
一个UI Prefab包含了30个相同的元素那么序列化数据将会包含30遍相同元素的序列化数据。生成大量的二进制序列化数据。在加载的时候也是一样会读取和反序列化30遍相同的元素。这样会比较耗费时间。
简化Hierarchy降低序列化数据大小not create large blob of binary data简化Hierarchy比如Unity提供的Optimize Game Objects选项隐藏节点选择对外暴露的节点Rig Settings简化Hierarchy复杂度尽量避免重复数据将重复的数据抽离到公用的数据结构中比如ScriptableObject实例化一个GameObject的时候采用可以指定Transform Parent的重载API将多个操作放到同一个API调用中 Instantiate(Object original, Transform parent)在实例化的时候指定父节点Transform指定旋转和位置避免先实例化然后单独指定父节点Transfrom旋转和位置
Unity提供了多种运行时资源管理方案一种是Resources文件夹模式一种是AssetBundle接下来一一介绍这两种方案
Resources资源文件夹运行时加载
将资源文件放到Resources文件夹中运行时可以通过Resources API动态的加载和卸载其中的Asest资源。
不建议
使用Resources文件夹会增大APP启动和加载时间增大APP包体大小难以在运行时进行细粒度的内存管理整体打包成一个序列化文件不能热更新资源不能做资源定制化管理需要重新打包APP不支持DLCdownloadable content
建议
快速开发迭代制作游戏原型以下情况可以考虑使用Resources文件夹资源文件不是内存敏感的占用内存比较少在游戏运行期间一直需要的资源文件没有热更新需求的资源文件全平台通用APP或者游戏启动时最小数据的配置文件启动配置相关的
序列化
Unity引擎在打包游戏的时候会将整个Resources文件夹序列到一个文件中serialized file对应的在PersistentManager中维护的一个SerializedFile文件该文件包含了索引信息indexing information和metadata原数据索引信息包含了从资源名称到File GUID和LocalID的映射同时还有一个AssetInfor列表存储每个游戏对象Object在二进制文件中的字节位置和读取大小定位到Object加载的字节地址位置和内容。通常平台查找关系使用平衡二叉搜索树来构建表示构建的时间复杂度是nlogn所以如果Resources文件越多indexing information构建的耗时也会比较的长。indexing information索引信息的构建在APP或者游戏启动的时候unity spalsh screen出现必须要做的工作且无法跳过例如游戏的首贞画面并没有使用到Resources中的文件但是从APP启动到进入到首场景也会经历Resources文件夹初始化indexing information构建的过程会造成APP或者游戏启动速度变慢。 官方的实际测试数据一个拥有10000个Assets的Resources目录在低端移动设备上的初始化需要5-10秒甚至更长。但其实这些Assets并不会在一开始就全部用到。 AssetBundle基础运行时加载
继续介绍下Unity提供的另外一种资源加载方案Assetbundle
什么是Assetbundle
Assetbundle本质上是一个包含了各种Game Ready的资源存档文件models贴图prefabaudio clip场景等等本质上就是一个二进制数据存档文件Archive File在PersistentManager中按照SerializedFile形式保存参考下文PersistentManager中的讲解 优势
DLC downloadable content支持资源分发相比较Resources文件夹AssetBundle可以做到更细节粒度的内存管理和资产管理 内存粒度可控资产粒度可控 减少安装包体大小 运行时下载一边玩一边下载 多平台多设备细粒度资源分级管理 提供多套Assetbundle变体适配多平台设备中高低档资源包更细粒度的资源内存管理灵活性高 热更新 资源热更新逻辑脚本资源热更新Lua
数据结构
Header Data Segment
Header
Assetbundle头部信息包含标识符identifier压缩类型compression typemanifest内容清单manifest是一个以Object资源名称为Key的查找字典每个字典元素确定了当前Object在data segment中的字节索引位置。用来加载Object。在大多数的平台中查找字典底层是一个平衡二叉搜索树Windows和OSX-derived 平台IOS使用的是红黑树查找字典的运行时构建和当前Assetbundle的资源量成正比资源越多查找字典构建越耗时。
Data Segment Data Segment是Assetbundle数据部分包含了真正的Object对象集合数据Unity提供了三种AssetBundle压缩方式
LZMA压缩格式(stream-based-block)所有的资源做整体压缩压缩率高加载速度慢需要将整个文件bundle进行解压因为所有的资源整体压缩LZ4压缩格式(chunk-based-block)多个Asset压缩到一个Chunk中在加载Asset的时候不需要将整个bundle都做解压chunk-based压缩算法加载一个Object只需要解压和处理对应的chunk数据即可压缩率相对LZMA不高但是加载速度较快内存友好.WebGL只支持LZ4的压缩格式LZ4LocalFromFile一般就是APP侧最合理的加载AssetBundle对的方式了未压缩数据部分就是一个原始二进制字节流加载速度最快bundle文件最大,
内存占用分析
当加载一个AsesetBundle的时候Unity会开辟一定的内存空间用来存储AssetBundle数据加载到内存中通过Memory Profiler查看SerializedFile占用的内存Unity底层是通过PersistentManager来管理和维护Serialized File对象
CAB-开头的可以理解为从Assetbundle加载的Serialized File AssetBundle有以下几点内存占用
Loading Cache默认是1MB AssetBundle.memoryBudgetKB可以设置大小默认数值是1.0MB适合大多数的情况缓存池大小运行时可能会多次访问一个AssetBundle多个segment使用缓存可以提高Asset读取速度CPU的多级缓存机制 可以通过详细的测试确定项目中合适的大小TypeTrees参考下文中TypeTree的详细介绍 定义每个Class Type的序列化的规则Per type not per object数据安全和版本 data safety and versioningA TypeTree describes the field layout of one of your data types.定义成员的内存布局。解决一个对象的序列化和反序列化规则不一致情况比如不同Unity版本打包出来的Assetbundle在某些type增加或者删除了某些字段对于新增的字段可以给默认值比如int给0保证不会反序列化的时候报错更好的兼容Typetree不会在每个Assetbundle中共享所以每一个assetbundle都有一个Typetree信息TypeTree可以通过BuildAssetBundleOptions.DisableWriteTypeTree在打包AssetBundle的时候不生成TypeTree信息这样ab包会更小加载速度更快但是无法做到版本兼容性加载不同版本的Assetbundle会加载失败针对特定运行平台必须强制携带类型信息typetree是无法剔除的比如WebGL平台Assetbundle中必须携带TypeTree信息 Table of contentsAssets查找表Serialized File Asset根据名称的访问查找表前文中提到的Indexing Mappingtypedef vector_mapLocalIdentifierInFileType, ObjectInfo ObjectMap;根据名称找到对应的ObjectInfo有了映射关系我们可以根据ObjectInfo实际的加载Object实现按名称按需加载根据名称定位到对应的Asset加载信息定位到data segment中具体的加载位置包含的Asset越多该部分数据占用的内存越大 Preload table预加载表涵盖了所有的Assetbundle需要涉及到的Object包含引用的完整Object比如如果引用的Object是个GameObject也包含GameObject所有的组件和所有的子节点以及子节点上的组件和组件引用的外部ObjectPreloadtable还是很大的一个数据结构加载Asset的是也是加载该Asset从PreloadTable中引用的startIndexsize关联的对象从Preload中逐个加载Asset加载完毕然后返回PreloadTable[startIndex]就是目标加载的Asset我们可以通过工具查看当前AB包所有的Preload Table内容 PreLoad Table定义了Asset所有依赖的其他资源加载Asset的时候需要将所有依赖的资源也需要加载比如一个prefab中的组件可能引用到了其他的Assetmeshtextureshader等等。 Preload table可能会非常的大比如prefabA10个Element和prefabB5个Element引用了一个复杂结构的prefabC(100个Element)总共是10 * 100 10 * 100个Element那么preload table会存在两份数据都包含了完整的prefabC的信息不管prefabC是否在Ab包中造成很大的冗余会发现preload table的size会很大尽量简化减少asset的引用简化层级复杂度比如我们有一个ui_merge_together的AB包里面包含了所有项目中用到的UI预设都放到一起总共有6W多个Element
AssetBundle的加载
Assetbundle.LoadFromMemory提供Async异步方法Unity不建议使用 内存不友好占用三分内存native一份C#一份GPU或者Asset本身占用一份内存 Assetbundle.LoadFromFile提供Async异步方法强烈建议 速度最快高效内存友好从磁盘或者SD卡中高效加载Assetbundle未压缩和lz4压缩可以直接从外部存储读取lzma需要读取过后在内存中做解压针对lzma不够友好只会读取Assetbundle的header头信息只有Object在被请求加载到游戏中才会加载对应的Data segment数据如果UnityWebRequest有缓存通过DownloadHandler的方式速度和从LoadFromFile一样快 UnityWebRequest’s DownloadHandlerAssetBundle 适用于WebGL或者Ab从远端直接加载的情况内存友好不需要额外的内存开销DownloadHandlerAssetbundle数据下载和处理都在底层Native Code层和工作线程Work Thread中进行。下载并处理完毕handler并不会在native层持有额外的内存拷贝。降低来了内存开销。lzma压缩的assetbundle会在下载的过程中进行解压并且使用lz4压缩格式重新压缩并缓存。可以通过Caching.CompressionEnabled控制行为当下载完毕Assetbundle可以直接通过DownloadHandler提供的API访问到如果给UnityWebRequest提供了版本信息如果已经有缓存AssetBundle可以被立刻的访问类似通过AssetBundle.LoadFromFile加载一样通过Using关键字保证资源UnityWebRequest被安全的卸载disposed。 WWW.LoadFromCacheOrDownload(on Unity5.6 or older)不建议使用 已经废弃不建议使用建议使用UnityWebRequest内存不够友好保存双份内存和UnityWebRequest不同每次调用WWW都会创建一个新的Worker Thread造成性能浪费在内存限制较大的平台上每次只能保证一个Assetbundle被下载并发不友好
AB包中的Assets加载
UnityEngine.Object可以通过Unity提供的API从Assetbundle中加载Unity提供了同步和异步加载方法
LoadAssetLoadAssetAsyncLoaAllAssetsLoadAllAssetsAsync 适用于将整个AssetBundle中的资源读取出来或者读取一个Assetbundle中大部分的Asset读取速度较快一次性读取要比多次调用LoadAsset更快Unity官方的建议如果从一个AB中加载较大的Asset但是Asset大小占比小于整个AssetBundle的66%可以将AssetBundle分离到多个AssetBundle并且使用LoadAllAssets LoadAssetWithSubAssetsLoadAssetsWithSubAssetsAsync
Asset加载底层实现
UnityEngine.Object的加载逻辑从存储读取Object数据在Work Thread工作线程中执行所有不涉及处理Unity线程敏感部分比如脚本图像相关的操作都会在Work Thread工作线程中执行比如VBO创建纹理解压缩。
异步加载方法Asynchronous LoadResources.LoadAsyncAssetbundle.LoadAssetAsyncAssetbundle.LoadAllAssetAsyncscenesSceneManager.LoadSceneAsync将会在单独的Work thread执行Object对象的读取反序列化非线程敏感的操作然后在主线程MainThread执行Object交互object integration会调用Object.AwakeFromLoad方法具体执行的逻辑和Object对象的类型有关针对textures在调用AwakeFromLoad的时候会执行UploadTexturemeshesUploadMeshData向GPU上传数据如果是audio数据交互代表在主线程准备音频数据并准备播放。
加载 UnityEngine.Object Work Thread Partloadingdeserializationand so on Main thread PartObject integrationAwakeOnLoad 都完成比如GameobjectObject对象会调用Awake表示当前Object已经加载完毕可以使用
在主线程的Object交互操作为了不阻塞主线程造成卡顿Unity提供了Application.backgroundLoadingPriority设置异步方法可以在单帧中执行的最长时间如果为了加快读取时间可以使用High等级加快Object加载速度。具体的应该设置为多少需要根据项目和实际的加载场景设置。
ThreadPriority.Low - 2msThreadPriority.BelowNormal - 4msThreadPriority.Normal - 10msThreadPriority.High - 50ms
AssetBundle内存管理
这部分属于老生常谈的知识点了我们在做一个介绍AssetBundle.Unload将加载的Header Information删除提供的unloadAllLoadedObjects该参数规定了AssetBundle被Unload之后对已经加载从AssetBundle中加载的Object如何做处理而且我们是没有办法将AssetBundle的一部分资源给卸载掉的只能完整的卸载整个AssetBundle
True 从该Assetbundle中加载的Asset都被强制卸载即使这些Assets还被引用中比如场景中的游戏对象或者当前场景中还在使用的纹理模型动画等等造成Missing问题引用丢失Asset丢失问题 False 从该Assetbundle中加载的M Asset不会被卸载。但是当前M Asset和Assetbundle的关联断开从Remapper删除映射信息File GUIDLocalID 到InstanceID的映射当Assetbundle被重新加载新的Header Information加载映射信息会被重新创建M Asset和重新加载的Assetbundle无任何关联删除PersistentManager.Remapper中信息如果M Asset由于某人原因被强制卸载比如切到后台APP丢失焦点会导致强制卸载Unity相关的资源再次回到APP的时候Unity需要Reload恢复渲染环境比如M Asset因为对应的Remapper映射已经被清空无法被寻址则无法被Reload导致的现象是Unloadfalse表现正常但是切到后台再切回来变成粉红色如果重新LoadAsset资源M会创建一个新的M场景中会存在两个相同的资源M这种情况会造成“内存泄露”被断开链接的资源M被回收只有两种方法必须触发UnloadUnusedAssets 消除代码或者场景对资源M的引用然后手动调用Resources.UnloadUnusedAssets通过非Addtive方式加载一个场景Unity会删除当前中所有的游戏对象destroy all objects in curren scene and invoke UnloadunsedAssets并且调用Resources.UnloadUnusedAssets系统接收到内存不足警告主动触发Resources.UnloadUnusedAssets
Unloadfalse的问题
False所有被加载的Object不会被销毁和卸载的AssetBundle断开联系寻址关系被从映射表中删除InstanceID-File GUIDLocalID可能会造成的三个问题 系统Reload异常如果Object被强制清空则Reload的时候无法寻址到Object则无法完成ReadObject操作表现粉色或者mesh丢失等Missing“内存泄露”永远不会被卸载掉的内存如果被引用比如Lua中的脚本引用则永远无法被卸载。这种Object的卸载方式只能在Unity引擎执行UnLoadUnUsedObjects时候被从内存中删除卸载。参见UnLoadUnUsedObjects的触发时机内存重复已经和AssetBundle断开连接的Object比如Texture再次加载相同Object的时候会重新从AssetBundle加载而不是复用断开连接的Object因为寻址关系已经被清空加载名称相同的Object会重新获取新的InstanceId并根据新的InstanceId建立新的寻址关系IntanceID发生的变化有造成“内存泄露”的风险
但是并不意味着我们项目中一定都要用UnLoadTrue
AssetBundle使用中需要注意的问题
管理好Assetbundle在内存中的数量按照一定的规则即使卸载Assetbundle虽然Assetbundle一般占用的空间和AB包大小有关如果Assetbundle较多那么Assetbundle占用的内存将会比较可观如果使用Unloadfalse需要注意可能存在内存泄漏的问题已经断开链接的Asset只能通过触发Resoruces.UnloadUsedAssets卸载如果使用Unloadtrue需要注意所有从该AssetBundle中加载的Asset都会被强制的卸载和清空当前场景或者业务逻辑有依赖这些Asset的都会Missing具体表现要看Asset的类型比如粉色mesh消失继续脚本访问会直接NullReferenceException异常适合项目的资源规划打包规则关联性适合项目的Assetbundle卸载规则
常见的Assetbundle生命周期管理方案
一般都采用引用计数的方案针对每个Asset做引用计数管理只有在AssetBundle涉及到的Assets引用计数都为0的情况才去卸载Assetbundle这样可以保证Unity在卸载AssetBundle和Reload Assetbundle的时候不会造成Asset重复内存泄露的问题。为了能够精细化的控制内存可以根据使用时机打包Assetbundle然后使用AssetBundle.Unloadtrue强制卸载有些还会增加对AssetBundle更加细致的管理比如引用计数为0之后而且10s之后才会去真正的Unloadtrue所有的方案的目的 解决Asset重复加载内存开销增大解决内存泄露的问题运行时的部分Asset永远无法被卸载提高命中率提升加载Assetbundle速度比如引入更加复杂的方案保证内存的同时保证最长最近使用的AssetBundle能够更合理的存在游戏内存中
Assetbundle分包策略
不同类型的项目打包分包规则可能差别会很大
逻辑实体对象类型逻辑紧密相关的Object多数情况一起出现的Objects需要同时更新的Object硬件系统适配AssetBundle变体
Assetbundle更新方案
在游戏加载执行全部的资源检查和执行更新更新结束才能进入到游戏边玩边下让玩家进入到游戏前选择需要加载的包比如高清画质还是低画质的资源包
Unity资源内存管理
在使用Unity引擎开发的过程中我们必须对Unity的资源内存有一定的认知这样才能尽可能的减少开发过程中带来的内存问题
资源生命周期Resoruce lifecycle
为了更好管理运行时内存需要了解Asset的生命周期管理资源卸载和加载的时机。
加载时机
寻址Asset Source源找得到Object的源地址File GUID Local IDSource location就可以加载Object找不到Source加载就会失败 AssetBundle.UnLoad将Header information销毁加载AssetBundle中的Asset就会寻址失败
可以通过API手动加载Object比如AssetBundle.LoadAsset等等
一个Object游戏对象被自动加载到内存
当前Object对应的指针PPtr被解引用有引用当前Object的Object被加载当前Object未被加载当前Object对应的源地址可以索引到File ID LocalID能找到Object所在源位置source locationAB包Resources
当一个Object被加载的时候Unity会检测当前加载的Object所引用包含的所有File IDLocalID生成对应的InstanceID同时执行PPtr解引用操作。
InstanceID对应的Object没有被加载通过File IDLocalID能正确的找到Object所在的Source文件The Instance ID has a valid File GUID and Local ID registered in the cache 如果通过FileIDLocal ID找不到对应的Object该Object将会加载失败表现的现象就是引用丢失比如shader丢失表现为粉色mesh表现为不可见texture表现为粉红色
卸载时机
未被引用的Objects被自动从内存中卸载当Unity的Unused Asset CleanUp发生的时候 Unity切换场景的时候会触发SceneManager.LoadScene非Additive形式会自动触发Unused Asset CleanUp脚本调用Resoures.UnloadUnusedAssets会触发Unused Asset CleanUp所有未被引用的Objects会被从内存中卸载所有HideFlags.DontUnloadUnusedAsset和HideFlags.HideAndDontSave的对象都不会被卸载在部分系统中如果收到了内存警告也会触发UnloadUnusedAssets 文件源在Resources中的Persistent对象可以通过Resources.UnloadAsset进行主动卸载。Instance ID和File IDLocal ID并不会被销毁如果有PPtr被解引用操作会重新加载文件源在AssetBundle中的Objects执行了Destroy操作Unity并不会立刻清空内存在调用AssetBundle.UnLoad(true)的时候才会从内存中一并和Assetbundle一起卸载同时FileIDLocalID和Instance ID映射变为不合法所有从Assetbundle加载的Asset都会被强制卸载如果仍然引用Asset会出现Missing的情况比如mesh不可见texture粉红色shader丢失空引用异常NullReferenceExceptionAssetBundle.UnLoad(false)如果被调用已经在内存中的Objects对象不会被从内存中卸载但是File IDLocalID和Instance ID的映射变为不合法如果这些Objects被从内存中卸载Unity将无法正确的执行Reload操作因为IDs索引已经被不合法清空。已经和AssetBundle断开关联的Asset只在在系统触发Resources.UnloadUnusedAssets回收AssetBundle.Unload(false)对应的serilizefile也会从内存中清空TODO需要做个实验AssetBundle是不能被部分卸载的
File IDLocal ID和Instance ID的映射不合法意味着Object无法寻址对应的加载源SourceSerialized File无法加载到内存中寻址不到Source源。)
如果调用了Unloadfalse 映射关系会被清空但是从AssetBundle中加载的Object不会被销毁后续运行中Unity如果触发了必须从Assetbundle中Reload资源的情况会导致加载失败。一种可能会出现的问题的场景如下
Unity失去了对graphic context上下文丢失移动平台上APP被强制暂停或者被切换到后台一般移动端的系统会将清空所有GPU内存当APP被重新唤醒Unity需要重新恢复渲染状态将TexturesShadersMeshes资源重新加载到GPU如果需要Reload的Object但是Object已经无法寻址所在的Assetbundle无法找到就会造成Reload 失败造成Missing情况场景粉红色
内存泄露
已经不会被使用的资源仍然在内存中一直被引用导致GC的时候无法被卸载比如Lua脚本中如果引用到了C#中的Object如果没有被正确的处理很容易造成内存泄露
需要理解的核心基础知识
UnityEngine.Object
UnityEngine.Object所有Unity引擎相关的资源对象组件都继承自该Object类维护全局的IDToPointerMap全局表存储当前所有存活的Object对象维护ClassID对应的RTTI信息用来根据ClassID工厂化申请内存并创建Object对象Object* Object::Produce (int classID, int instanceID, MemLabelId memLabel, ObjectCreationMode mode)创建C底层Native Object对象
Object的 和 ! 的重载
public override bool Equals(object o) { return CompareBaseObjects (this, o as Object); } CSRAW private static bool CompareBaseObjects (Object lhs, Object rhs)
{
#if UNITY_WINRTreturn UnityEngineInternal.ScriptingUtils.CompareBaseObjects(lhs, rhs);
#elsereturn CompareBaseObjectsInternal (lhs, rhs);
#endif
}// Compares if two objects refer to the same
CSRAW public static bool operator (Object x, Object y) { return CompareBaseObjects (x, y); }// Compares if two objects refer to a different object
CSRAW public static bool operator ! (Object x, Object y) { return !CompareBaseObjects (x, y); } CONSTRUCTOR_SAFE
CUSTOM private static bool CompareBaseObjectsInternal ([Writable]Object lhs, [Writable]Object rhs)
{return Scripting::CompareBaseObjects (lhs.GetScriptingObject(), rhs.GetScriptingObject());
}/// Compares two Object classes.
/// Returns true if both have the same instance id
/// or both are NULL (Null can either mean that the object is gone or that the instanceID is 0)
bool CompareBaseObjects (ScriptingObjectPtr lhs, ScriptingObjectPtr rhs)
{int lhsInstanceID 0;int rhsInstanceID 0;bool isLhsNull true, isRhsNull true;if (lhs){lhsInstanceID GetInstanceIDFromScriptingWrapper (lhs);ScriptingObjectOfTypeObject lhsRef(lhs);isLhsNull !lhsRef.IsValidObjectReference();}if (rhs){rhsInstanceID GetInstanceIDFromScriptingWrapper (rhs);ScriptingObjectOfTypeObject rhsRef(rhs);isRhsNull !rhsRef.IsValidObjectReference();}if (isLhsNull || isRhsNull)return isLhsNull isRhsNull;elsereturn lhsInstanceID rhsInstanceID;
}PersistentManagerUnity持久化寻址管理器
所有外部加载的Source FileSerialized File维护所有Object的读取都必须通过PersistentManager加载维护寻址关系表RemapperInstanceID - FileGUID LocalID
管理Unity底层持久化资源StreamContainer管理所有Unity引擎中加载的SerializedFileSerialized File可能有不同的来源比如 From Assetbundle 从AssetBundle加载From Memory 直接从Memory加载From Resources 从Reources加载 通过Remapper维护资源寻址映射关系InstanceId - serializedObjectFileIndexFile GUID localIdentifierInFile LocalID可以理解为根据InstanceId可以定位到该实例对象在外部存储空间或者内存中的位置可寻址提供接口从持久化的Stream中读取加载Object一个Object可以被正确加载Read的前提可寻址不可寻址表示Object无法找到Source源Serialized File Stream无法加载
Profiler中对应的持久化数据源类型Serialized File
一个Object可寻址表示从SerializedFile中能找到对应的Source源我们可以使用Profiler中查看当前加载的所有Serialized File信息
archive:/CAB-guid 来自Assetbundle的Source资产源Serialized File Stream .sharedAssets场景Assetbundle额外的资产源 globalgamemanagers global Project Settings data for the playerAPP用到的全局配置全局Manager序列化文件resources.assets Resoruces文件夹中的所有资源如果Resources文件中有依赖Resources之外的资源也会包含到resources.assets中Library/unity default resources Unity内部默认资源打包产物中对应的unity default resourcesResources/unity_built_extra Unity内置额外资源
!!!Object加载流程底层实现
当一个PPtr解引用执行Object加载参见下文中的PPtr介绍 operator T* () const;T* operator - () const;T operator * () const;检测当前InstanceId对应的Object是否已经被加载从Object类维护的全局ms_IDToPointer映射表中检查底层实现
// Finds the pointer to the object referenced by instanceID (NULL if none found in memory)
static Object* IDToPointer (int inInstanceID);templateclass T inline
PPtrT::operator T* () const
{if (GetInstanceID () 0)return NULL;Object* temp Object::IDToPointer (GetInstanceID ());if (temp NULL)temp ReadObjectFromPersistentManager (GetInstanceID ());return static_castT* (temp);
}如果在全局内存中不存在输入的InstanceID则调用PersistentManager尝试从“外部存储Serialized File”中加载ObjectPersistentManager进行寻址InstanceId确定是否可寻址到Source Serialized FileInstanceID 到 FileID Local File ID是否合法PersistentManager如果是合法寻址找到对应的Source Serialized File Stream调用ReadObject方法执行读取 PersistentManager如果寻址不合法则Object加载失败 PersistentManager根据拿到的Serialized File执行调用SeriaFile.ReadObject方法isPersistent true可寻址到的Object而不是动态创建的Object比如new Meshnew Texturenew GameObject等等
Object* PersistentManager::ReadObject (int heapID)
{Object* o LoadFromActivationQueue(heapID);if (o ! NULL){m_Mutex.Unlock();return o;}// 根据InstanceId进行寻址拿到合法的持久化Stream// 找到instanceId 和 FileID LocalId的映射// File ID 确定了Stream// Find and load the right streamSerializedObjectIdentifier identifier;if (!m_Remapper-InstanceIDToSerializedObjectIdentifier(heapID, identifier)){m_Mutex.Unlock();return NULL;}// 根据已经被转换的streamIndex拿到PersistentManager保存的stream进行读取SerializedFile* stream GetSerializedFileInternal (identifier.serializedFileIndex);if (stream NULL){#if DEBUG_MAINTHREAD_LOADINGLogString(Format(--- Loading from main thread failed loading stream %f, (GetTimeSinceStartup () - time) * 1000.0F));#endifm_Mutex.Unlock();return NULL;}// Find file id in stream and read the object// 根据fileidlocal id in a file读取对应的Objectm_ActiveNameSpace.push (identifier.serializedFileIndex);TypeTree* oldType;bool didTypeTreeChange;o NULL;stream-ReadObject (identifier.localIdentifierInFile, heapID, kCreateObjectDefault, true, oldType, didTypeTreeChange, o);m_ActiveNameSpace.pop ();// Awake the objectif (o){AwakeFromLoadQueue::PersistentManagerAwakeSingleObject (*o, oldType, kDidLoadFromDisk, didTypeTreeChange, gSafeBinaryReadCallback);}return o;
}SerializedFileReadObject 获取ObjectInfo获取失败表示当前localFileID在ObjectInfoMap无法找到return NULL根据ObjectInfo.ClassID执行Object.Produce操作根据ClassID申请内存并创建Object架子结构并将该Object注册到全局Object查找表中https://docs.unity3d.com/Manual/ClassIDReference.html参考下文TypeTree根据ObjectInfo定义的字节流起始偏移量和大小使用底层填充Object会执行Unity序列化系统的Transfer方法objectPtr-VirtualRedirectTransfer (readStream)递归调用Transfer最终完成填充Object并返回
// Reads the object referenced by id from disk
// Returns a pointer to the object. (NULL if no object was found on disk)
// object is either PRODUCED or the object already in memory referenced by id is used
// isMarkedDestroyed is a returned by value (non-NULL)
// registerInstanceID should the instanceID be register with the ID To Object lookup (false for threaded loading)
// And reports whether the object read was marked as destroyed or not
void ReadObject (LocalIdentifierInFileType fileID, int instanceId, ObjectCreationMode mode, bool isPersistent, TypeTree** oldTypeTree, bool* didChangeTypeTree, Object** readObject);void SerializedFile::ReadObject (LocalIdentifierInFileType fileID, int instanceId, ObjectCreationMode mode, bool isPersistent, TypeTree** oldTypeTree, bool* didChangeTypeTree, Object** outObjectPtr)
{// 检测当前localFileID是否在该SerializedFile中存在// typedef vector_mapLocalIdentifierInFileType, ObjectInfo ObjectMap;// 读取的ObjectInfo列表表示当前File所包含的所有Objects信息ObjectMap::iterator iter m_Object.find (fileID);// 获取ObjectInfo执行创建const ObjectInfo info iter-second;// Create empty objectObject* objectPtr *outObjectPtr;if (objectPtr NULL){*outObjectPtr objectPtr Object::Produce (info.classID, instanceId, kMemBaseObject, mode);}// Type Tree?// 选择合适的Transfer底层序列化工具执行反序列化操作else{#if SUPPORT_SERIALIZED_TYPETREESStreamedBinaryReadtrue readStream;CachedReader cache readStream.Init (options);cache.InitRead (*m_ReadFile, byteStart, info.byteSize);Assert(m_ResourceImageGroup.resourceImages[0] NULL);// Read the objectobjectPtr-VirtualRedirectTransfer (readStream);int position cache.End ();if (position - byteStart ! info.byteSize)OutOfBoundsReadingError (info.classID, info.byteSize, position - byteStart);*didChangeTypeTree false;#elseAssertString(reading endian swapped is not supported);#endif}
}PersistentManager执行Awake Object在Profiler中会看到这些Awake调用比如Texture.AwakeFromLoadMesh.AwakeFromLoad每种Object类型都会有自己的AwakeFromLoad并完成相应的路基比如渲染相关的ObjectMeshTexture会直接将数据UpLoad到GPU如果是audio数据交互代表在主线程准备音频数据并准备播放 // Awake the objectif (o){AwakeFromLoadQueue::PersistentManagerAwakeSingleObject (*o, oldType, kDidLoadFromDisk, didTypeTreeChange, gSafeBinaryReadCallback);}o.AwakeFromLoad (awakeMode);o.ClearPersistentDirty ();PPtr
InstanceID和Object映射关系InstanceId-ObjectPtrPPtr是一个指向Object对象的指针。需要的时候延迟加载Object对象解引用PPtr的Transfer可以理解为寻址注册映射关系构建Unity通过InstanceID来记录引用关系PPtr指向一个Object对象在执行实例化Instantiate的时候也是需要将PPtr指向的Object进行拷贝操作
PPtr在序列化文件中的数据结构
PPtr assetint m_FileID 索引文件File GUID定位到文件SInt64 m_PathID文件中的Object ID定位文件中的具体Object
AssetBundle中表示当前Bundle中所有的Asset指针定义了FileIDLocalFileID在AssetBundle进行Transfer的时候遇到PPtr会执行PPtr的Transfer
map m_ContainerArray Arrayint size 31[0]pair datastring first assets/res/character/npc/shop_batai_001.prefabAssetInfo secondint preloadIndex 569int preloadSize 26PPtrObject assetint m_FileID 0SInt64 m_PathID 1810623846293925203[1]pair datastring first assets/res/character/npc/shop_batai_002.prefabAssetInfo secondint preloadIndex 434int preloadSize 31PPtrObject assetint m_FileID 0SInt64 m_PathID -287100744004624113[2]pair datastring first assets/res/character/npc/shop_caidan_001.prefabAssetInfo secondint preloadIndex 150int preloadSize 27PPtrObject assetint m_FileID 0SInt64 m_PathID -2163428893405175807[3]pair datastring first assets/res/character/npc/shop_deng_001.prefabAssetInfo secondint preloadIndex 512int preloadSize 22PPtrObject assetint m_FileID 0SInt64 m_PathID 495874309500947719 PPtr类定义
templateclass T
class PPtr
{SInt32 m_InstanceID;#if !UNITY_RELEASEmutable T* m_DEBUGPtr;#endifprotected:inline void AssignObject (const Object* o);private:static string s_TypeString;public:static const char* GetTypeString ();static bool IsAnimationChannel () { return false; }static bool MightContainPPtr () { return true; }static bool AllowTransferOptimization () { return false; }templateclass TransferFunctionvoid Transfer (TransferFunction transfer);// Assignmentexplicit PPtr (int instanceID){m_InstanceID instanceID;#if !UNITY_RELEASEm_DEBUGPtr NULL;#endif}PPtr (const T* o) { AssignObject (o); }PPtr (const PPtrT o){m_InstanceID o.m_InstanceID;#if !UNITY_RELEASEm_DEBUGPtr NULL;#endif}PPtr (){#if !UNITY_RELEASEm_DEBUGPtr NULL;#endifm_InstanceID 0;}PPtr operator (const T* o) { AssignObject (o); return *this; }PPtr operator (const PPtrT o){#if !UNITY_RELEASEm_DEBUGPtr NULL;#endifm_InstanceID o.m_InstanceID; return *this;}void SetInstanceID (int instanceID) { m_InstanceID instanceID; }int GetInstanceID ()const { return m_InstanceID; }// Comparisonbool operator (const PPtr p)const { return m_InstanceID p.m_InstanceID; }bool operator (const PPtr p)const { return m_InstanceID p.m_InstanceID; }bool operator ! (const PPtr p)const { return m_InstanceID ! p.m_InstanceID; }operator T* () const;T* operator - () const;T operator * () const;
};PPtr解引用操作
T* PPtr::operator - () constT PPtr::operator * () constPPtr::operator T* () const
Object被从外部Load到内存并加载的时机当前PPtr被“解引用”的时候会触发从PersistentManager中读取Object通常情况下只会持有InstanceID只有真正的被“解引用”访问的时候才会从持久化管理器中ReadObject加载对应的Object
templateclass T inline
T PPtrT::operator * () const
{// 如果当前Object在运行时IDMap中不存在// 从PersistentManager读取ObjectObject* temp Object::IDToPointer (GetInstanceID ());if (temp NULL)temp ReadObjectFromPersistentManager (GetInstanceID ());#if !UNITY_RELEASEm_DEBUGPtr (T*) (temp);#endif#if DEBUGMODE || !GAMERELEASET* casted dynamic_pptr_castT* (temp);if (casted ! NULL)return *casted;else{if (temp ! NULL){ErrorStringObject (PPtr cast failed when dereferencing! Casting from temp-GetClassName () to T::GetClassStringStatic () !, temp);}else{ErrorString (Dereferencing NULL PPtr!);}ANALYSIS_ASSUME(casted);return *casted;}#elsereturn *static_castT* (temp);#endif
}//
// 从PersistentManager中根据InstanceID读取加载UnityEngine.Object解引用PPtr
//
Object* ReadObjectFromPersistentManager (int id)
{if (id 0)return NULL;else{// In the Player it is not possible to call MakeObjectPersistent,// thus instance ids that are positive are the only ones that can be loaded from disk#if !UNITY_EDITORif (id 0){#if DEBUGMODE//AssertIf(GetPersistentManager ().ReadObject (id));#endifreturn NULL;}#endifObject* o GetPersistentManager ().ReadObject (id);return o;}
}PPtr解引用的时机
PPtr解引用时机加载完AssetBundle初始化AssetBundle会对PPtr做一次解引用需要将解析AssetBundle所有的AssetInfoHeaderInfoContainerPreloadTableMainAssset头部信息可能是TreeType相关的信息PPtr执行从AssetBundle中加载AssetObject需要对当前AssetInfo定义的PreloadPreLoad TablestartIndextableSize的Object执行解引用操作将所有的Preload Object加载到内存
PPtr Curve引用Object的曲线
m_PPtrCurves引用外部的Object对象PPtr Curve类型EditorCurveBinding.PPtrCurve指向Object的动画曲线比如给一个Image在动画文件中做一个序列帧动画每一帧都会引用一个外部的Sprite对象
m_EulerCurves: []m_PositionCurves: []m_ScaleCurves: []m_FloatCurves: []m_PPtrCurves:- curve:- time: 0value: {fileID: 0}- time: 0.033333335value: {fileID: 21300000, guid: ff2bba53a71aed5468e208397796e1c9, type: 3}- time: 0.05value: {fileID: 21300000, guid: 8c9de47e31a111d4996d705840dba765, type: 3}- time: 0.06666667value: {fileID: 21300000, guid: 3127dd5fdad8c1148850546ce86c831b, type: 3}- time: 0.083333336value: {fileID: 21300000, guid: 1dc8c048bb3b7be408250bc0263d4169, type: 3}- time: 0.1value: {fileID: 21300000, guid: acb5c2a66fd2211459bf7ec94ab220d2, type: 3}- time: 0.11666667value: {fileID: 21300000, guid: 062d0d1b5a5462046a980205b54b8626, type: 3}- time: 0.31666666value: {fileID: 0}attribute: m_Spritepath: classID: 114// Monobehaviour// Image.csscript: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!74 7400000
AnimationClip:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_Name: New AnimationserializedVersion: 6m_Legacy: 0m_Compressed: 0m_UseHighQualityCurve: 1m_RotationCurves: []m_CompressedRotationCurves: []m_EulerCurves: []m_PositionCurves: []m_ScaleCurves: []m_FloatCurves: []m_PPtrCurves:- curve:- time: 0value: {fileID: 0}- time: 0.033333335value: {fileID: 21300000, guid: ff2bba53a71aed5468e208397796e1c9, type: 3}- time: 0.05value: {fileID: 21300000, guid: 8c9de47e31a111d4996d705840dba765, type: 3}- time: 0.06666667value: {fileID: 21300000, guid: 3127dd5fdad8c1148850546ce86c831b, type: 3}- time: 0.083333336value: {fileID: 21300000, guid: 1dc8c048bb3b7be408250bc0263d4169, type: 3}- time: 0.1value: {fileID: 21300000, guid: acb5c2a66fd2211459bf7ec94ab220d2, type: 3}- time: 0.11666667value: {fileID: 21300000, guid: 062d0d1b5a5462046a980205b54b8626, type: 3}- time: 0.31666666value: {fileID: 0}attribute: m_Spritepath: classID: 114// Monobehaviour 114// Image.cs fe87c0e1cc204ed48ad3b37840f39efcscript: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}m_SampleRate: 60m_WrapMode: 0m_Bounds:m_Center: {x: 0, y: 0, z: 0}m_Extent: {x: 0, y: 0, z: 0}m_ClipBindingConstant:genericBindings:- serializedVersion: 2path: 0attribute: 2015549526script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}typeID: 114customType: 0isPPtrCurve: 1pptrCurveMapping:- {fileID: 0}- {fileID: 21300000, guid: ff2bba53a71aed5468e208397796e1c9, type: 3}- {fileID: 21300000, guid: 8c9de47e31a111d4996d705840dba765, type: 3}- {fileID: 21300000, guid: 3127dd5fdad8c1148850546ce86c831b, type: 3}- {fileID: 21300000, guid: 1dc8c048bb3b7be408250bc0263d4169, type: 3}- {fileID: 21300000, guid: acb5c2a66fd2211459bf7ec94ab220d2, type: 3}- {fileID: 21300000, guid: 062d0d1b5a5462046a980205b54b8626, type: 3}- {fileID: 0}m_AnimationClipSettings:serializedVersion: 2m_AdditiveReferencePoseClip: {fileID: 0}m_AdditiveReferencePoseTime: 0m_StartTime: 0m_StopTime: 0.33333334m_OrientationOffsetY: 0m_Level: 0m_CycleOffset: 0m_HasAdditiveReferencePose: 0m_LoopTime: 1m_LoopBlend: 0m_LoopBlendOrientation: 0m_LoopBlendPositionY: 0m_LoopBlendPositionXZ: 0m_KeepOriginalOrientation: 0m_KeepOriginalPositionY: 1m_KeepOriginalPositionXZ: 0m_HeightFromFeet: 0m_Mirror: 0m_EditorCurves: []m_EulerEditorCurves: []m_HasGenericRootTransform: 0m_HasMotionFloatCurves: 0m_Events: []TypeTree
定义一个Class成员的内存布局A TypeTree describes the field layout of one of your data typesPer type not per object数据安全和版本 data safety and versioning不能跨AssetBundle共享每个AssetBundle都有TypeTree信息
Transfer的过程中TypeTree用来定义Object成员变量的读取规则给一段字节流根据TypeTree我们可以解析出来每个字段的类型和具体内容或者简单的理解为“解码规则”如果去读取一个不同版本引擎打包的AssetBundle内容不同的引擎版本或者我们自己定义的脚本同一个Class可能有字段差异所以如果直接按照当前Class进行解析会导致解析报错。
static char const* kIncompatibleScriptsMsg “The asset bundle ‘%s’ could not be loaded because it references scripts that are not compatible with the currently loaded ones. Rebuild the AssetBundle to fix this error.”;static char const* kIncompatibleRuntimeClassMsg “The asset bundle ‘%s’ could not be loaded because it contains run-time classes of incompatible version. Rebuild the AssetBundle to fix this error.”;static char const* kIncompatibleRuntimeMsg “The asset bundle ‘%s’ could not be loaded because it is not compatible with this newer version of the Unity runtime. Rebuild the AssetBundle to fix this error.”;
AssetBundle中有TypeTree信息可以进行比对Class哪些字段的类型变更或者字段增加删除新增的给默认值删除字段也给默认值做到保证读取数据安全和做到版本兼容
TypeTree不能共享每个AssetBundle必须包含自己的TypeTree数据这样就会造成信息冗余内存有额外的开销而且TypeTree是根据Class type定义的而不是根据每个Object来定义
不少大厂都针对TypeTree做了引擎级别的定制优化如果没有版本兼容问题TypeTree可以通过BuildAssetBundleOptions.DisableWriteTypeTree在打包AssetBundle的时候不生成TypeTree信息这样ab包会更小加载速度更快直接读取并反序列化Transfer不用创建TypeTree检测是否合法并做修正但是无法做到版本兼容性加载不同版本的Assetbundle会加载失败针对特定运行平台必须强制携带类型信息typetree是无法剔除的比如WebGL平台Assetbundle中必须携带TypeTree信息
ClassID
ClassID定义了和Unity中引擎Class和ID映射关系通过ClassID能够确定是引擎内部哪一个具体的类 0Object4Transform23MeshRender Unity引擎在Object中维护ClassID对应的RTTI信息根据ClassID获取对应的Class创建器根据ClassID工厂化申请内存并创建ClassID对应的Object对象每个Unity底层继承自Object的对象都需要执行注册 REGISTER_DERIVED_CLASSREGISTER_DERIVED_ABSTRACT_CLASS*outObjectPtr objectPtr Object::Produce (info.classID, instanceId, kMemBaseObject, mode); 如果对引擎代码进行裁剪Strip Engine Code可能在运行时会出现某个ClassID无法被创建Produce的错误,Could not produce class with ID xx,This could be caused by a class being stripped from the build even though it is needed,Try disabling ‘Strip Engine Code’ in PlayerSettings可以通过关闭Strip Engine Code解决或者将Produce创建失败的ClassID在link.xml进行显示的声明 assembly fullnameUnityEnginetype fullnameUnityEngine.AI.NavMeshObstacle preserveall/type fullnameUnityEngine.AI.NavMeshData preserveall/type fullnameUnityEngine.AI.NavMeshAgent preserveall/type fullnameUnityEngine.AI.NavMeshPath preserveall/type fullnameUnityEngine.Collider preserveall/type fullnameUnityEngine.MeshCollider preserveall/type fullnameUnityEngine.SkinnedMeshRenderer preserveall/type fullnameUnityEngine.Avatar preserveall/type fullnameUnityEngine.LODGroup preserveall/type fullnameUnityEngine.LightProbeGroup preserveall/type fullnameUnityEngine.Animations.PositionConstraint preserveall//assembly在调用Object.InstantiateUnity做了哪些事情
可以概括的在调用Object.Instantiate的时候Unity分三个步骤ProduceCopyAwake如果在Profiler中开启Deep Profile模式可以看到在实例化的时候Unity底层具体做了哪些事情可以简单的概括为以下几个详细步骤
创建实例化的Object层级关系骨架如果是一个GameObject对象递归创建所有的子节点和子节点组件如果是单个Object直接执行创建操作不做子节点和子节点组件的递归操作Creates a gameobjects/components hierarchy same as original object - Instantiate.Produce.TempRemapTable填充映射表typedef vector_mapSInt32, SInt32, std::less, STL_ALLOCATOR(kMemTempAlloc, IntPair) TempRemapTable;用来后续做内容拷贝remappedPtrs-insert(make_pair(singleObject.GetInstanceID(), clone.GetInstanceID()));根据TempRemaptable映射表读取Origin的数据然后将数据反序列化到Clone对象上通过PPtr的Transfer进行序列化和反序列化PPtrCopies all fields of all objects from the original objects - Instantiate.Copy激活Clone对象上的所有的Component并调用Awake函数Calls Awake on all scripts of new objects Instantiate.Awake.AwakeAndActivateClonedObjects
Object InstantiateObject (Object inObject, const Vector3f worldPos, const Quaternionf worldRot)
{TempRemapTable ptrs;Object obj InstantiateObject (inObject, worldPos, worldRot, ptrs);AwakeAndActivateClonedObjects(ptrs);return obj;
}static Object* CloneObjectImpl (Object* object, TempRemapTable ptrs)
{// 采集Clone目标Object的所有的子节点和组件信息执行Object.Produce创建操作CollectAndProduceClonedIsland (*object, ptrs);TempRemapTable::iterator it;#if UNITY_FLASH//specialcase for flash, as that needs to be able to assume linear memorylayout.dynamic_arrayUInt8 buffer(kMemTempAlloc);MemoryCacheWriter cacheWriter (buffer);
#elseBlockMemoryCacheWriter cacheWriter (kMemTempAlloc);
#endif// 执行Clone数据操作// Origin Key源PPtrObject序列化到writeStream中// Clone Value将数据反序列到Cloned PPtrObject上RemapFunctorTempRemapTable functor (ptrs);RemapPPtrTransfer remapTransfer (kSerializeForPrefabSystem, true);remapTransfer.SetGenerateIDFunctor (functor);for (itptrs.begin ();it ! ptrs.end ();it){Object original *PPtrObject (it-first);// Copy DataObject clone *PPtrObject (it-second);StreamedBinaryWritefalse writeStream;CachedWriter writeCache writeStream.Init (kSerializeForPrefabSystem, BuildTargetSelection::NoTarget());writeCache.InitWrite (cacheWriter);original.VirtualRedirectTransfer (writeStream);writeCache.CompleteWriting();StreamedBinaryReadfalse readStream;CachedReader readCache readStream.Init (kSerializeForPrefabSystem);readCache.InitRead (cacheReader, 0, writeCache.GetPosition());clone.VirtualRedirectTransfer (readStream);readCache.End();if (!IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_0_a1)){GameObject* clonedGameObject dynamic_pptr_castGameObject* (clone);if (clonedGameObject)clonedGameObject-SetActiveBitInternal(true);}// Remap referencesclone.VirtualRedirectTransfer (remapTransfer);}// TempRemapTable::iterator found ptrs.find (object-GetInstanceID ());AssertIf (found ptrs.end ());object PPtrObject (found-second);return object;
}void CollectAndProduceClonedIsland (Object o, TempRemapTable* remappedPtrs)
{AssertIf(!remappedPtrs-empty());remappedPtrs-reserve(64);GameObject* go GetGameObjectPtr(o);if (go){///TODO: It would be useful to lock object creation around a long instantiate call.// Butwe have to be careful that we dont load anything during the object creation in order to avoid // a deadlock: case 389317CollectAndProduceGameObjectHierarchy(*go, go-QueryComponent(Transform), remappedPtrs);}elseCollectAndProduceSingleObject(o, remappedPtrs);remappedPtrs-sort();
}Transform* CollectAndProduceGameObjectHierarchy (GameObject go, Transform* transform, TempRemapTable* remappedPtrs)
{GameObject* cloneGO static_castGameObject* (Object::Produce (ClassID(GameObject)));remappedPtrs-insert(make_pair(go.GetInstanceID(), cloneGO-GetInstanceID()));GameObject::Container goContainer go.GetComponentContainerInternal();GameObject::Container clonedContainer cloneGO-GetComponentContainerInternal();clonedContainer.resize(goContainer.size());for (int i0;igoContainer.size();i){Unity::Component component *goContainer[i].second;Unity::Component clone static_castUnity::Component (ProduceClone(component));clonedContainer[i].first goContainer[i].first;clonedContainer[i].second clone;clone.SetGameObjectInternal(cloneGO);remappedPtrs-insert(make_pair(component.GetInstanceID(), clone.GetInstanceID()));}if (transform){Transform cloneTransform cloneGO-GetComponent(Transform);Transform::TransformComList srcTransformArray transform-GetChildrenInternal();Transform::TransformComList dstTransformArray cloneTransform.GetChildrenInternal();dstTransformArray.resize_uninitialized(srcTransformArray.size(), false);for (int i0;isrcTransformArray.size();i){Transform curT *srcTransformArray[i];GameObject curGO curT.GetGameObject();Transform* curCloneTransform CollectAndProduceGameObjectHierarchy(curGO, curT, remappedPtrs);curCloneTransform-GetParentPtrInternal() cloneTransform;dstTransformArray[i] curCloneTransform;}return cloneTransform;}else{return NULL;}
}参考资料
https://learn.unity.com/tutorial/assets-resources-and-assetbundles必须要看的文章本文大部分来自对该文章的解读https://blog.unity.com/engine-platform/serialization-in-unityhttps://www.youtube.com/watch?vN-HJvfVuKRw Unity中的序列化https://github.com/Perfare/AssetStudio 查看AssetBundle工具