常用资源加载方式

  • 拖到组件上,直接引用

  • Resources
    • 编辑器/实机均可以使用
    • 资源必须位于Resources目录下,加载路径是相对于Resources的路径
    • Resources目录下的文件大小有限制
  • AssetDataBase
    • 只可以在编辑器下使用
    • 路径以Asssets开始,要求加入资源后缀
  • AssetBundle

AssetBundle入门

基本定义

AssetBundle(以下简称AB)可以理解为一种压缩包,能够压缩任何能够放入U3D中的资源,并且还能够自动保存资源之间的依赖关系

一般来说AB包里的内容可以分为两类

  • 不可以在电脑上直接预览的资源,比如Prefab,所有此类资源会被统一打入一个Serialized File中
  • 可以再电脑上直接预览的资源,比如图片,音频,每一个资源都会被保存为一个Resources File

因此,一个AB包里有1个Serialized文件和N个Resource文件

image-20220326014826909

基本流程

1.指定AB包的打包属性

点击想要打包的资源

image-20220326015305566

在下方可以对目标AB包进行设置,第一个是所属包名,第二个是包后缀,后缀无实际意义

可以通过XXX/包名,目标AB包的打包路径进行设置

image-20220326020502855

2.打包脚本

public class Build 
{
    [MenuItem("Tools/打包")]
    public static void BuildAB()
    {
        if (!Directory.Exists("AssetBundles"))
            Directory.CreateDirectory("AssetBundles");
        BuildPipeline.BuildAssetBundles("AssetBundles", BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
    }
}
  • 第一个参数是打包路径,这里的目录是从工程根目录开始计算的,如果路径不存在会报错
  • 第二个参数是压缩格式
    • BuildAssetBundleOptions.None:默认构建AssetBundle的方式。使用LZMA算法压缩,此算法压缩包小,但是加载时间长,而且使用之前必须要整体解压。解压以后,这个包又会使用LZ4算法重新压缩,之后使用就不用整体解压了,也就是第一次解压很慢,之后就变快了
    • BuildAssetBundleOptions.UncompressedAssetBundle:不压缩数据,包大,但是加载很快
    • BuildAssetBundleOptions.ChunkBaseCompression:使用LZ4算法压缩,压缩率没有LZMA高,但是加载资源不必整体解压。这种方法中规中矩,比较常用
    • BuildAssetBundleOptions.DisableWriteTypeTree:不会在AssetBundle中包含类型信息
    • BuildAssetBundleOptions.DeterministicAssetBundle:使用存储在Asset Bundle中的对象的id的哈希构建Asset Bundle
    • BuildAssetBundleOptions.ForceRebuildAssetBundle:强制重建Asset Bundles
    • BuildAssetBundleOptions.IgnoreTypeTreeChanges:执行增量构建检查时忽略类型树更改
    • BuildAssetBundleOptions.AppendHashToAssetBundleName:将哈希附加到assetBundle名称
    • BuildAssetBundleOptions.StrictMode:如果在其中报告任何错误,则不允许构建成功
    • BuildAssetBundleOptions.DryRunBuild:不实际构建它们
    • BuildAssetBundleOptions.DisableLoadAssetByFileName:通过文件名禁用Asset Bundle的加载
    • BuildAssetBundleOptions.DisableLoadAssetByFileNameWithExtension:通过带扩展名的文件名禁用Asset Bundle 的加载
  • 第三个参数是目标平台,不同平台的AB包不能够通用

image-20220326020634673

3.加载资源

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        var ab = AssetBundle.LoadFromFile("AssetBundles/test/model.ab");
        var go = ab.LoadAsset<GameObject>("Attack");
        Instantiate(go);
    }
}

首先加载AB包,需要带后缀,目录是以项目根目录开始

然后加载资源,资源名称就是打包时的资源名

加载的只是资源,最终需要实例化

运行之后应该是可以成功加载的

image-20220326022018853

依赖打包

假如多个Prefab资源都依赖了某一个资源

image-20220326164003048

此时如果不对引用资源进行打包,只对两个Prefab进行打包,那么每一个AB包里都会保存一份资源引用

image-20220326164706934

我们直接打包

image-20220326165729102

两个包的总大小是66+66=132kb

很明显我们的材质和贴图资源只需要打包一份就好了,我们可以选择把材质和资源也打入一个AB包中

此时Cube包和Sphere包会自动维持都材质包的引用

image-20220326164900014

对不同资源进行分包

image-20220326165334654

再次打包

image-20220326165828695

很明显,资源只会被打包一次,包体缩小了一半

Mainfest文件

打开上面打包的cube.mainfest

...
...
Assets:
- Assets/GameData/Prefab/Cube.prefab
Dependencies:
- D:/__Project/Unity/AssetModule/AssetBundles/share
  • Assets:这个AB包内有哪些资源
  • Dependencies:这个AB包依赖于哪一个AB包

当AB包存在依赖时,如果我们直接加载当前AB包,而没有加载依赖包,那么会导致资源丢失

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        var ab = AssetBundle.LoadFromFile("AssetBundles/cube");
        var go = ab.LoadAsset<GameObject>("Cube");
        Instantiate(go);
    }
}

image-20220326170556155

因为cube包依赖于share包,但我们没有加载share包

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        var share = AssetBundle.LoadFromFile("AssetBundles/share");
        var cube = AssetBundle.LoadFromFile("AssetBundles/cube");
        var go = cube.LoadAsset<GameObject>("Cube");
        Instantiate(go);
    }
}

只要加载过share包,那么我们就可以正常读取资源,加载包的顺序并不重要,只需要保证我们读取资源时,所有需要的包都已经加载

image-20220326171134732

AB包的加载方式

1.AssetBundle.LoadFromMemory(Async)

从内存中加载,有同步和异步两种方式,需要提供AB包的字节码(可以直接来自本地,也可以下载自服务器)

public class ResourceLoadTest : MonoBehaviour
{
    IEnumerator Start()
    {
        var share = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes("AssetBundles/share"));
        yield return share;
        var cube = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes("AssetBundles/cube"));
        yield return cube;
        Instantiate(cube.assetBundle.LoadAsset<GameObject>("Cube"));
    }
}

示例中的字节码直接是从本地读取的,只是提供参考

2.AssetBundle.LoadFromFile(Async)

直接从本地加载,只需要提供路径

3.UnityWebRequest

public class ResourceLoadTest : MonoBehaviour
{
    IEnumerator Start()
    {
        var request = UnityWebRequestAssetBundle.GetAssetBundle("http://localhost:6009/AssetBundles/share");
        yield return request.SendWebRequest();
        var share = (request.downloadHandler as DownloadHandlerAssetBundle).assetBundle;

        request = UnityWebRequestAssetBundle.GetAssetBundle("http://localhost:6009/AssetBundles/cube");
        yield return request.SendWebRequest();
        var cube = (request.downloadHandler as DownloadHandlerAssetBundle).assetBundle;

        Instantiate(cube.LoadAsset<GameObject>("Cube"));
    }
}

如果想把下载的内容保存下来也很简单

File.WriteAllBytes("path",request.downloadHandler.data);

依赖包的加载

打包时Unity会自动在目标目录生成一个总的AssetBundle信息包,我们可以加载他获取到所有信息

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        var mainFestAB = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
        var mainFest = mainFestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

        foreach (var bundle in mainFest.GetAllAssetBundles())
        {
            Debug.Log(bundle);
        }
    }
}

image-20220326182655556

获取某一个AB包所依赖的包也很简单

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        var mainFestAB = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
        var mainFest = mainFestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

        var deps = mainFest.GetAllDependencies("cube");
        foreach (var dep in deps)
        {
            Debug.Log(dep);
        }
    }
}

只有一个share

image-20220326182820087

然后加载

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        var mainFestAB = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
        var mainFest = mainFestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

        var deps = mainFest.GetAllDependencies("cube");
        foreach (var dep in deps)
            AssetBundle.LoadFromFile($"AssetBundles/{dep}");

        var cube = AssetBundle.LoadFromFile("AssetBundles/cube");
        Instantiate(cube.LoadAsset<GameObject>("Cube"));
    }
}

AB包的卸载

当我们加载AB包,并且加载某个资源使用后,AB包与资源是保持一种引用关系的

1.AssetBundle.UnLoad(false)

释放所有没有被使用的资源,如果某个资源正在被使用,那么不会卸载这个资源,并且这个资源与AB包的引用关系会被打断,相当于内存还没释放,但指针置空了,那就再也找不到未释放的内存地址了,无法在释放那一份内存,一般来说不使用这一个API

只能通过Resources.UnloadUnusedAssets释放,但这相当于一次很大的GC,很耗费时间

image-20220326183545443

2.AssetBundle.UnLoad(true)

释放所有资源,不论这个资源是否正在使用

XML序列化

序列化为XML

[System.Serializable]
public class Test
{
   public string name;
}

[System.Serializable]
public class TestXML
{
   public int id;

   public Test test;

   public List<int> list=new List<int>();
}

在类前加入[System.Serializable]标签后,类内的public属性都会被序列化

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        using (var stream = new FileStream("AssetBundles/test.xml", FileMode.OpenOrCreate, FileAccess.ReadWrite)) 
        {
            using (var writer = new StreamWriter(stream,Encoding.UTF8))
            {
                var xmlSerializer = new XmlSerializer(typeof(TestXML));
                
                var instance = new TestXML();
                instance.id = 1;
                instance.list.Add(2);
                instance.list.Add(3);
                var test = new Test();
                test.name = "999";
                instance.test = test;
                
                xmlSerializer.Serialize(writer,instance);
            }
        }
    }
}

序列化结果如下

image-20220327214856011

反序列化也非常简单

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        using (var stream = new FileStream("AssetBundles/test.xml", FileMode.OpenOrCreate, FileAccess.ReadWrite)) 
        {
            var xmlSerializer = new XmlSerializer(typeof(TestXML));
            var instance = xmlSerializer.Deserialize(stream) as TestXML;

            Debug.Log(instance.id);
            Debug.Log(instance.test.name);
        }
    }
}

image-20220327215129918

序列化为Byte

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        var instance = new TestXML();
        instance.id = 1;
        instance.list.Add(2);
        instance.list.Add(3);
        var test = new Test();
        test.name = "999";
        instance.test = test;

        using (var stream = new FileStream("AssetBundles/test.bytes", FileMode.OpenOrCreate, FileAccess.ReadWrite))
        {
            var bf = new BinaryFormatter();
            bf.Serialize(stream, instance);
        }
    }
}

然后反序列化

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        using (var stream = new FileStream("AssetBundles/test.bytes", FileMode.Open, FileAccess.Read, FileShare.Read)) 
        {
            var bf = new BinaryFormatter();
            var test = bf.Deserialize(stream) as TestXML;
            Debug.Log(test.id);
            Debug.Log(test.test.name);
        }
    }
}

image-20220402205104698

对比

如果我们添加10w个数据,然后分别序列化为XML和BYTES

public class ResourceLoadTest : MonoBehaviour
{
    void Start()
    {
        var instance = new TestXML();
        instance.list = new List<int>();
        
        // 给list添加10w个数据
        for (int i = 0; i < 100000; i++)
            instance.list.Add(i);
        
        using (var stream = new FileStream("AssetBundles/test.bytes", FileMode.OpenOrCreate, FileAccess.ReadWrite))
        {
            var bf = new BinaryFormatter();
            bf.Serialize(stream, instance);
        }
    }
}

image-20220402205503862

很明显bytes的体积小了非常非常多是吧,所以从节省资源角度来说肯定是解析为bytes的

但是bytes完全不可读,这是个问题