找回密码
 立即注册
查看: 570|回复: 0

[简易教程] Unity AssetBundle 教程

[复制链接]
发表于 2020-12-5 07:29 | 显示全部楼层 |阅读模式
《游戏AI程序设计实战》作者
Github:onelei - Overview
CSDN:https://blog.csdn.net/onelei1994
QQ群:754814245
========================
Unity AssetBundle 教程

AssetBundle是Unity用来处理资源热更新的,下面简单介绍AssetBundle的所有操作。本教程使用的Unity版本:Unity 2018.2.12f1 (64-bit)
AssetBundle打包

设置AssetBundle名字

手动设置





打包之前按照上图所示的方法,设置一下AssetBundle的名字。
自动设置

将需要进行AssetBundle打包的图片按照“UI_”的前缀命名,然后根据图片的父目录来设置AssetBundle的名字。如下所示




然后新建一个ImageImporter.cs文件放入Editor目录下
using UnityEngine;
using UnityEditor;

/// <summary>
/// 根据名字前缀自动化设置图片的格式以及TAG等设置
/// </summary>
public class ImageImporter : AssetPostprocessor
{
    /// <summary>
    /// 图片导入之前调用,可设置图片的格式、spritePackingTag、assetBundleName等信息
    /// </summary>
    void OnPreprocessTexture()
    {
        TextureImporter importer = (TextureImporter)assetImporter;
        string path = importer.assetPath;
        string[] pathArray = importer.assetPath.Split('/');
        if (pathArray.Length <= 2)
        {
            Debug.LogError("获取路径名失败");
            return;
        }
        string imageName = pathArray[pathArray.Length - 1];
        string packTag = pathArray[pathArray.Length - 2];

        if (imageName.StartsWith("UI_"))
        {
            importer.textureType = TextureImporterType.Sprite;
            importer.mipmapEnabled = false;
            //设置spritePackingTag
            importer.spritePackingTag = packTag;
            //设置assetBundleName
            importer.assetBundleName = packTag;
        }
    }
}接着执行Reimport操作即可自动设置好图片的AssetBundle的名字。




AssetBundle打包

新建一个QAssetBundleEditor.cs脚本放入Editor文件夹下面,代码如下
using System.IO;
using UnityEditor;

/// <summary>
/// AssetBundle打包脚本Editor
/// </summary>
public class QAssetBundleEditor
{
    static string OUT_PATH_WIN64 = "AssetBundles/Win64/AssetBundles";
    static string OUT_PATH_IOS = "AssetBundles/IOS/AssetBundles";
    static string OUT_PATH_Android = "AssetBundles/Android/AssetBundles";

    /// <summary>
    /// BuildWin64
    /// </summary>
    [MenuItem("AssetBundle/BuildWin64")]
    public static void BuildAssetBundle_Win64()
    {
        BuildAssetBundles(OUT_PATH_WIN64, BuildTarget.StandaloneWindows64);
    }

    /// <summary>
    /// BuildWin64
    /// </summary>
    [MenuItem("AssetBundle/BuildIOS")]
    public static void BuildAssetBundle_IOS()
    {
        BuildAssetBundles(OUT_PATH_IOS, BuildTarget.iOS);
    }

    /// <summary>
    /// BuildWin64
    /// </summary>
    [MenuItem("AssetBundle/BuildAndroid")]
    public static void BuildAssetBundle_Android()
    {
        BuildAssetBundles(OUT_PATH_Android, BuildTarget.Android);
    }

    public static void BuildAssetBundles(string outPath, BuildTarget buildTarget)
    {
        if (Directory.Exists(outPath))
        {
            Directory.Delete(outPath, true);
        }
        Directory.CreateDirectory(outPath);
        BuildPipeline.BuildAssetBundles(outPath, BuildAssetBundleOptions.UncompressedAssetBundle, buildTarget);

        AssetDatabase.Refresh();
    }
}然后通过Unity的菜单栏进行AssetBundle打包操作,选择“AssetBundle/BuildWin64”打出一个Windows平台使用的AssetBundle。




在Asset同级目录生成了AssetBundles目录




文件如下所示,有一个以文件夹名字“AssetBundles”命名的AssetBundle,可以理解为主AssetBundle,下面的uibackground就是将几张图片打出来的AssetBundle包。




AssetBundles.manifest文件的“AssetBundleInfos”保存了当前打出的所有AssetBundle的文件名称。




uibackground.manifest文件的“Assets”保存了当前打出的AssetBundle里面包含的所有图片名称。




AssetBundle加载

我们将打出来的所有AssetBundle放入Unity的StreamingAssets目录




接下来就可以通过代码加载AssetBundle了。
新建一个QAssetBundleManager.cs文件,代码如下
using UnityEngine;
using System.Collections.Generic;

public class QAssetBundleManager
{
    static AssetBundle assetbundle = null;

    static Dictionary<string, AssetBundle> DicAssetBundle = new Dictionary<string, AssetBundle>();

    public static T LoadResource<T>(string assetBundleName, string assetBundleGroupName) where T : Object
    {
        if (string.IsNullOrEmpty(assetBundleGroupName))
        {
            return default(T);
        }

        if (!DicAssetBundle.TryGetValue(assetBundleGroupName, out assetbundle))
        {
            assetbundle = AssetBundle.LoadFromFile(GetStreamingAssetsPath() + assetBundleGroupName);//+ ".assetbundle"
            DicAssetBundle.Add(assetBundleGroupName, assetbundle);
        }
        object obj = assetbundle.LoadAsset(assetBundleName, typeof(T));
        var one = obj as T;
        return one;
    }

    public static void UnLoadResource(string assetBundleGroupName)
    {
        if (DicAssetBundle.TryGetValue(assetBundleGroupName, out assetbundle))
        {
            assetbundle.Unload(false);
            if (assetbundle != null)
            {
                assetbundle = null;
            }
            DicAssetBundle.Remove(assetBundleGroupName);
            Resources.UnloadUnusedAssets();
        }
    }

    public static string GetStreamingAssetsPath()
    {
        string StreamingAssetsPath =
#if UNITY_EDITOR
        Application.streamingAssetsPath + "/";
#elif UNITY_ANDROID
        "jar:file://" + Application.dataPath + "!/assets/";
#elif UNITY_IPHONE
        Application.dataPath + "/Raw/";
#else
        string.Empty;
#endif
        return StreamingAssetsPath;
    }
}核心代码就是下面这两行代码
assetbundle = AssetBundle.LoadFromFile(GetStreamingAssetsPath() + assetBundleGroupName);
object obj = assetbundle.LoadAsset(assetBundleName, typeof(T));



通过AssetBundle.LoadFromFile加载出AssetBundle,然后根据图片的名字从AssetBundle里面取出即可,接口为
assetbundle.LoadAsset下面通过一个例子来查看如何使用




我们创建两个按钮,分别用来加载两张图片,按钮的回调函数就是加载出某个AssetBundle(uibackground)下面的某个图片(UI_1002)
QAssetBundleManager.LoadResource<Sprite>("UI_1002", "uibackground");具体代码如下:
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class QAssetBundleSample : MonoBehaviour
{
    public Button Button_Load;
    public Button Button_Load2;
    public Image Image_BackGround;

    private void Awake()
    {
        Button_Load.onClick.AddListener(OnClickLoad);
        Button_Load2.onClick.AddListener(OnClickLoad2);
    }

    void OnClickLoad()
    {
        Image_BackGround.overrideSprite = QAssetBundleManager.LoadResource<Sprite>("UI_1002", "uibackground");
    }

    void OnClickLoad2()
    {
        Image_BackGround.overrideSprite = QAssetBundleManager.LoadResource<Sprite>("UI_1003", "uibackground");
    }
}运行Unity,结果如下




AssetBundle卸载

assetbundle.Unload(false);
assetbundle.Unload(true);Unload(false)   只卸载内存镜像;
Unload(true)    卸载内存镜像以及Asset的内存实例;
一般AssetBundle的卸载是放在切场景的时候,或者低内存的时候,手动调用该接口,执行完Unload之后assetbundle就会被Unity置为null。QAssetBundleManager.cs增加如下函数即可。
public static void UnLoadResource(string assetBundleGroupName)
    {
        if (DicAssetBundle.TryGetValue(assetBundleGroupName, out assetbundle))
        {
            assetbundle.Unload(false);
            DicAssetBundle.Remove(assetBundleGroupName);
            Resources.UnloadUnusedAssets();
        }
    }AssetBundle下载

方式:在登录游戏的时候检查更新,下载对应的AssetBundle到本地。然后运行的时候直接从本地加载AssetBundle,游戏运行中不从网络上下载AssetBundle。
搭建本地服务器

为了测试下载,这里通过nodejs搭建一个本地服务器,将打出来的AssetBundle放入本地服务器中,通过C#去下载即可方便测试。
安装Node.js。 在安装完成后再安装http-server,命令行输入:
npm install http-server -g等待下载完成,我们到AssetBundle生成的目录下,开启命令行,输入
http-server即可在当前目录开启服务。




我们在浏览器中输入http://127.0.0.1:8080/即可访问本地的服务器




下面我们开始通过C#代码来下载对应的AssetBundle文件。
UnityWebRequest





我们在界面中增加一个“WebLoad”按钮,当点击按钮的时候从服务器中下载对应的AssetBundle文件,保存到本地之后按照之前的流程加载即可。这里我们使用Unity自带的UnityWebRequest函数来进行下载操作。
先下载主AssetBundle也就是“AssetBundles”文件,该文件中保存了本地所有的AssetBundle的名字以及AssetBundle的依赖关系。然后下载所有的AssetBundle文件同时保存到本地。
AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(localPath);
            if (mainAssetBundle == null)
                yield return null;
            //获取AssetBundleManifest文件
            AssetBundleManifest manifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

            //获取AssetBundleManifest中的所有AssetBundle的名称信息
            string[] assets = manifest.GetAllAssetBundles();
            for (int i = 0; i < assets.Length; i++)
            {
                Debug.Log(AssetBundlePath + assets);
                //开启协程下载所有的
                StartCoroutine(DownloadAssetBundleAndSave(AssetBundlePath, assets, () =>
                {
                    //下载完成,按照之前的方法,从本地加载AssetBundle并设置。
                    Image_BackGround.overrideSprite = QAssetBundleManager.LoadResource<Sprite>("UI_1003", "uibackground");
                }));
            }完整代码如下
void OnClickWebLoad()
    {
        StartCoroutine(DownloadAssetBundles());
    }

    /// <summary>
    /// 下载根目录AssetBundle文件
    /// </summary>
    /// <returns></returns>
    IEnumerator DownloadAssetBundles()
    {
        using (UnityWebRequest www = UnityWebRequest.Get(AssetBundlePath + MainAssetBundleName))
        {
            yield return www.SendWebRequest();

            if (www.isNetworkError)
            {
                yield return null;
            }

            byte[] datas = www.downloadHandler.data;
            SaveAssetBundle(MainAssetBundleName, datas);
            string localPath = QAssetBundleManager.GetApplicationdataPath() + MainAssetBundleName;
            AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(localPath);
            if (mainAssetBundle == null)
                yield return null;
            //获取AssetBundleManifest文件
            AssetBundleManifest manifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

            //获取AssetBundleManifest中的所有AssetBundle的名称信息
            string[] assets = manifest.GetAllAssetBundles();
            for (int i = 0; i < assets.Length; i++)
            {
                Debug.Log(AssetBundlePath + assets);
                //开启协程下载所有的
                StartCoroutine(DownloadAssetBundleAndSave(AssetBundlePath, assets, () =>
                {
                    //下载完成,按照之前的方法,从本地加载AssetBundle并设置。
                    Image_BackGround.overrideSprite = QAssetBundleManager.LoadResource<Sprite>("UI_1003", "uibackground");
                }));
            }
        }
    }

    IEnumerator DownloadAssetBundleAndSave(string url, string name, Action saveLocalComplate = null)
    {
        WWW www = new WWW(url + name);
        yield return www;
        if (www.isDone)
        {
            SaveAssetBundle(name, www.bytes, saveLocalComplate);
        }
    }

    void SaveAssetBundle(string fileName, byte[] bytes, Action saveLocalComplate = null)
    {
        string path = QAssetBundleManager.GetApplicationdataPath() + fileName;
        FileInfo fileInfo = new FileInfo(path);
        FileStream fs = fileInfo.Create();

        fs.Write(bytes, 0, bytes.Length);
        fs.Flush();  
        fs.Close();  
        fs.Dispose();

        if (saveLocalComplate != null)
        {
            saveLocalComplate();
        }
    }运行游戏,结果如下所示。




AssetBundle文件比对

既然能够通过服务器下载AssetBundle了,那么我们需要根据AssetBundle的文件不同来选择是否需要重新下载,这里我们通过文件的MD5来进行文件比对,如果MD5相同就不需要重新下载。这里简单写下思路。首先我们将打好的AssetBundle文件的MD5全部读取出来,然后将AssetBundle名字和对应的MD5写入“FileList.txt”文件中,然后将该文件上传至服务器,根据服务器的文件来和本地的“FileList.txt”进行比对,有MD5不一致的就需要下载,下载完了再覆盖掉本地的“FileList.txt”。当然也可以增加一个“Version.txt”文件,里面用来记录版本号,通过版本号来判断是否进行文件更新。
如何创建这个"FileList.txt"呢?增加两个编辑器函数即可
/// <summary>
    /// Create FileList
    /// </summary>
    static void CreateFileList(string outPath)
    {
        string filePath = outPath + FILE_LIST_NAME;
        if (File.Exists(filePath))
        {
            File.Delete(filePath);
        }

        StreamWriter streamWriter = new StreamWriter(filePath);

        string[] files = Directory.GetFiles(outPath);
        for (int i = 0; i < files.Length; i++)
        {
            string tmpfilePath = files;
            if (tmpfilePath.Equals(filePath) || tmpfilePath.EndsWith(".manifest"))
                continue;
            Debug.Log(tmpfilePath);
            tmpfilePath.Replace("\\", "/");
            streamWriter.WriteLine(tmpfilePath.Substring(outPath.Length) + "|" + GetFileMD5(tmpfilePath));
        }
        streamWriter.Close();
        streamWriter.Dispose();

        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 获取文件的MD5
    /// </summary>
    static System.Security.Cryptography.MD5 MD5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
    static System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
    static string GetFileMD5(string filePath)
    {
        FileStream fileStream = new FileStream(filePath, FileMode.Open);
        byte[] bytes = MD5.ComputeHash(fileStream);
        fileStream.Close();

        for (int i = 0; i < bytes.Length; i++)
        {
            stringBuilder.Append(bytes.ToString("x2"));
        }
        return stringBuilder.ToString();
    }名字和MD5之间通过“|”分隔即可。




==================
欢迎支持我的新书《游戏AI程序设计实战》

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-23 10:36 , Processed in 0.117165 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表