[Spieleentwicklung][Unity]Assetbundle Packaging Kapitel (5) Verwenden von Manifest zum zweimaligen Erstellen eines Ressourcenindexes

Inhaltsverzeichnis

Verzeichnis des Verpackungs- und Ressourcenlade-Frameworks

Text

Bevor wir mit dem Text beginnen, fügen wir hier den Verpackungscode ein. Bitte beachten Sie, dass der vorherige Code weggelassen wurde. Vergleichen Sie ihn selbst mit dem vorherigen Artikel. Dieser Artikel beginnt mit der ersten Ausführung des Verpackungscodes.

public void PostAssetBuild()
{
    //前面的代码省略,和上一篇文章一致

    Log($"开始构建......");
    BuildAssetBundleOptions opt = MakeBuildOptions();
    AssetBundleManifest buildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
    if (buildManifest == null)
        throw new Exception("[BuildPatch] 构建过程中发生错误!");

    //本篇的代码从这开始==============================================
    // 清单列表
    string[] allAssetBundles = buildManifest.GetAllAssetBundles();
    Log($"资产清单里总共有{allAssetBundles.Length}个资产");

    //create res manifest
    var resManifest = CreateResManifest(buildMap, buildManifest);
    var manifestAssetInfo = new AssetInfo(AssetDatabase.GetAssetPath(resManifest));
    var label = "Assets/Manifest";
    manifestAssetInfo.ReadableLabel = label;
    manifestAssetInfo.AssetBundleVariant = PatchDefine.AssetBundleDefaultVariant;
    manifestAssetInfo.AssetBundleLabel = HashUtility.BytesMD5(Encoding.UTF8.GetBytes(label));
    var manifestBundleName = $"{manifestAssetInfo.AssetBundleLabel}.{manifestAssetInfo.AssetBundleVariant}".ToLower();
    _labelToAssets.Add(manifestBundleName, new List<AssetInfo>() { manifestAssetInfo });

    //build ResManifest bundle
    buildInfoList.Clear();                
    buildInfoList.Add(new AssetBundleBuild()
    {
        assetBundleName = manifestAssetInfo.AssetBundleLabel,
        assetBundleVariant = manifestAssetInfo.AssetBundleVariant,
        assetNames = new[] { manifestAssetInfo.AssetPath }
    });
    var resbuildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
    //加密代码省略,后面文章讲解
}

Nach dem ersten Aufruf der BuildPipeline.BuildAssetBundles-Paketierungs-API (Einzelheiten finden Sie in der siebten Zeile des Codes) wird ein Verweis auf AssetBundleManifest zurückgegeben.

[Frage]:Die BuildPipeline.BuildAssetBundles-Paketierungs-API hat uns bereits dabei geholfen, Abhängigkeitsreferenzen zwischen AB-Paketen zu erstellen. Warum müssen wir Referenzbeziehungen zwischen AB-Paketen erstellen?

[Antwort]:Die nach der Ausführung der BuildPipeline.BuildAssetBundles-Paketierungs-API generierte Datei UnityManifest.manifest zeichnet alleAB-Paketinformationen und -Abhängigkeiten auf. aber ! Bei der Projektverpackung auf Unternehmensebene muss eine inkrementelle Verpackung berücksichtigt werden. Daher möchten wir wissen, welche Version jedes AB typisiert ist, und wir benötigen eine Markierung, z. B. die Aufzeichnung, dass das AB-Paket ab einer bestimmten SVN-Stufe typisiert ist. Daher ist die von der Verpackungsschnittstelle generierte Datei UnityManifest.manifest ein halbfertiges Produkt.


Beginnen wir mit einer formalen Einführung in die Sekundärverarbeitung der Datei UnityManifest.manifest.

string[] allAssetBundles = buildManifest.GetAllAssetBundles(); Holen Sie sich allAssetBundles und verwenden Sie die Methode CreateResManifest, um eine Unity-Asset-Datei zu erstellen und die wenigen Daten in UnityManifest.manifest in die Asset-Datei zu serialisieren. Das Serialisierungsskript des Assets ist ResManifes, wie unten gezeigt

Sekundäre Verarbeitung der UnityManifest.manifest-DateiDer Code lautet wie folgt:

//assetList在前面的打包代码里有
//buildManifest第一次打包API返回的文件
private ResManifest CreateResManifest(List<AssetInfo> assetList , AssetBundleManifest buildManifest)
{
    string[] bundles = buildManifest.GetAllAssetBundles();
    var bundleToId = new Dictionary<string, int>();
    for (int i = 0; i < bundles.Length; i++)
    {
        bundleToId[bundles[i]] = i;
    }

    var bundleList = new List<BundleInfo>();
    for (int i = 0; i < bundles.Length; i++)
    {                
        var bundle = bundles[i];
        var deps = buildManifest.GetAllDependencies(bundle);
        var hash = buildManifest.GetAssetBundleHash(bundle).ToString();

        var encryptMethod = ResolveEncryptRule(bundle);
        bundleList.Add(new BundleInfo()
        {
            Name = bundle,
            Deps = Array.ConvertAll(deps, _ => bundleToId[_]),
            Hash = hash,
            EncryptMethod = encryptMethod
        });
    }

    var assetRefs = new List<AssetRef>();
    var dirs = new List<string>();
    foreach (var assetInfo in assetList)
    {
        if (!assetInfo.IsCollectAsset) continue;
        var dir = Path.GetDirectoryName(assetInfo.AssetPath).Replace("\\", "/");
        CollectionSettingData.ApplyReplaceRules(ref dir);
        var foundIdx = dirs.FindIndex(_ => _.Equals(dir));
        if (foundIdx == -1)
        {
            dirs.Add(dir);
            foundIdx = dirs.Count - 1;
        }

        var nameStr = $"{assetInfo.AssetBundleLabel}.{assetInfo.AssetBundleVariant}".ToLower();
        assetRefs.Add(new AssetRef()
        {
            Name = Path.GetFileNameWithoutExtension(assetInfo.AssetPath),
            BundleId = bundleToId[$"{assetInfo.AssetBundleLabel}.{assetInfo.AssetBundleVariant}".ToLower()],
            DirIdx = foundIdx
        });
    }

    var resManifest = GetResManifest();
    resManifest.Dirs = dirs.ToArray();
    resManifest.Bundles = bundleList.ToArray();
    resManifest.AssetRefs = assetRefs.ToArray();
    EditorUtility.SetDirty(resManifest);
    AssetDatabase.SaveAssets();
    AssetDatabase.Refresh();

    return resManifest;
}

Hier ist der Code zum Serialisieren der Daten:

 /// <summary>
    /// design based on Google.Android.AppBundle AssetPackDeliveryMode
    /// </summary>
    [Serializable]
    public enum EAssetDeliveryMode
    {
        // ===> AssetPackDeliveryMode.InstallTime
        Main = 1,
        // ====> AssetPackDeliveryMode.FastFollow
        FastFollow = 2,
        // ====> AssetPackDeliveryMode.OnDemand
        OnDemand = 3
    }

    /// <summary>
    /// AssetBundle打包位置
    /// </summary>
    [Serializable]
    public enum EBundlePos
    {
        /// <summary>
        /// 普通
        /// </summary>
        normal,
        
        /// <summary>
        /// 在安装包内
        /// </summary>
        buildin,

        /// <summary>
        /// 游戏内下载
        /// </summary>
        ingame,
    }

    [Serializable]
    public enum EEncryptMethod
    {
        None = 0,
        Quick, //padding header
        Simple, 
        X, //xor
        QuickX //partial xor
    }

    [Serializable]
    [ReadOnly]
    public struct AssetRef
    {
        [ReadOnly, EnableGUI]
        public string Name;

        [ReadOnly, EnableGUI]
        public int BundleId;

        [ReadOnly, EnableGUI]
        public int DirIdx;
    }

    [Serializable]
    public enum ELoadMode
    {
        None,
        LoadFromStreaming,
        LoadFromCache,
        LoadFromRemote,
    }
     

    [Serializable]
    public struct BundleInfo
    {
        [ReadOnly, EnableGUI]
        public string Name;

        [ReadOnly, EnableGUI]
        [ListDrawerSettings(Expanded=false)]
        public int[] Deps;

        [ReadOnly]
        public string Hash;

        [ReadOnly]
        public EEncryptMethod EncryptMethod;
        
        // public ELoadMode LoadMode;
    }
    
    public class ResManifest : ScriptableObject
    {
        [ReadOnly, EnableGUI]
        public string[] Dirs = new string[0];
        [ListDrawerSettings(IsReadOnly = true)]
        public AssetRef[] AssetRefs = new AssetRef[0];
        [ListDrawerSettings(IsReadOnly = true)]
        public BundleInfo[] Bundles = new BundleInfo[0];
    }
}

Wie Sie auf dem Bild sehen können, erstellt die Methode „CreateResManifest“ unseren eigenen Satz von Ressourcen- und AB-Paketindexbeziehungen.

Die ResManifes-Serialisierungsdatei (Code unten) speichert drei Arten von Daten:

  1. Liste aller Ressourcenordner

  1. AB-Paket Listennummer, in der sich die Ressource befindet, Listennummer des Ordners, in dem sich die Ressource befindet

  1. Der Name des AB-Pakets, der Name des abhängigen Pakets, die Versionsnummer MD5 und der Verschlüsselungstyp.


[Frage]: Warum müssen Sie diese Asset-Datei serialisieren?

Bevor ich die Frage beantworte, möchte ich eine Frage stellen: Das Laden von Ressourcen ist definitiv etwas für Entwickler. Wie finden Entwickler das AB-Paket, in dem sich die gewünschte Ressource befindet?

[Antwort]: Wenn das Projekt startet, müssen wir diese Asset-Datei verwenden, um Referenzinformationen für alle Ressourcen zu erstellen. Nach dem Projektstart müssen wir dieses Asset laden . Der Ladecode lautet wie folgt.

protected virtual ResManifest LoadResManifest()
{
    string label = "Assets/Manifest";
    var manifestBundleName = $"{HashUtility.BytesMD5(Encoding.UTF8.GetBytes(label))}.unity3d";
    string loadPath = GetAssetBundleLoadPath(manifestBundleName);
    var offset = AssetSystem.DecryptServices.GetDecryptOffset(manifestBundleName);
    var usingFileSystem = GetLocation(loadPath) == AssetLocation.App 
        ? FileSystemManagerBase.Instance.MainVFS 
        : FileSystemManagerBase.Instance.GetSandboxFileSystem(PatchDefine.MainPackKey);
    if (usingFileSystem != null)
    {
        offset += usingFileSystem.GetBundleContentOffset(manifestBundleName);
    }
    
    AssetBundle bundle = AssetBundle.LoadFromFile(loadPath, 0, offset);
    if (bundle == null)
        throw new Exception("Cannot load ResManifest bundle");

    var manifest = bundle.LoadAsset<ResManifest>("Assets/Manifest.asset");
    if (manifest == null)
        throw new Exception("Cannot load Assets/Manifest.asset asset");

    for (var i = 0; i < manifest.Dirs.Length; i++)
    {
        var dir = manifest.Dirs[i];
        _dirToIds[dir] = i;
    }

    for (var i = 0; i < manifest.Bundles.Length; i++)
    {
        var info = manifest.Bundles[i];
        _bundleMap[info.Name] = i;
    }

    foreach (var assetRef in manifest.AssetRefs)
    {
        var path = StringFormat.Format("{0}/{1}", manifest.Dirs[assetRef.DirIdx], assetRef.Name);
        // MotionLog.Log(ELogLevel.Log, $"path is {path}");
        if (!_assetToBundleMap.TryGetValue(assetRef.DirIdx, out var assetNameToBundleId))
        {
            assetNameToBundleId = new Dictionary<string, int>();
            _assetToBundleMap.Add(assetRef.DirIdx, assetNameToBundleId);
        }
        assetNameToBundleId.Add(assetRef.Name, assetRef.BundleId);
    }

    bundle.Unload(false);
    return manifest;
}

Wenn Sie sich den obigen Code ansehen, können Sie sehen, dass diese Asset-Datei ebenfalls im Bundle enthalten ist und ein separates AB-Paket darstellt. Werfen Sie noch einmal einen Blick auf den Titel dieses Artikels: „Verwenden Sie Manifest, um einen Ressourcenindex zweimal zu erstellen.“ Dann ist das Bundle, in dem sich dieses Asset befindet, der Kern dieses Artikels! ! !

Lassen Sie uns darüber sprechen, wie Entwickler Ressourcen im Projekt laden. Zuerst ruft der Entwickler einen Loader auf, um Ressourcen zu laden. Wenn der AB-Paketlademodus verwendet wird (lokales Laden von Ressourcen wird nicht diskutiert), wird ein Ressourcenpfad übergeben Erfolgreicher Rückruf wird geladen

Loader.Load("Assets/Works/Resource/Sprite/UIBG/bg_lihui",callbackFunction)

//成功后回调
void callbackFunction(资源文件)
{
    //使用资源文件
}

Wir wissen, dass diese Ressourcenindexdatei geladen wird, wenn das Projekt startet, daher kennt das Framework natürlich alle Ressourcenpfade und die AB-Paketnamen, auf die es verweist, sodass es beim Laden von Ressourcen natürlich das entsprechende AB-Paket und die Ressource findet Die Indexdatei zeichnet auch das AB-Paket auf. Interdependenzen: Beim Laden des Ziel-AB-Pakets werden einfach alle abhängigen Pakete rekursiv geladen.

Wie Sie diese sekundär erstellte Ressourcenindexdatei im Projekt verwenden, wurde oben ausführlich erläutert. Beginnen wir nun mit dem Hot-Upload aller AB-Pakete, wenn das Projekt startet.


Die CreatePatchManifestFile-Methode dient zum Erstellen einer Downloadliste für AB-Pakete. Bitte beachten Sie, dass die alte Liste geladen wird, bevor eine neue Liste erstellt wird, und das vom AB-Paket generierte MD5 verglichen wird, um festzustellen, ob Änderungen vorliegen. Wenn nein Bei einer Änderung wird weiterhin die Versionsnummer der alten Liste verwendet. Beispiel: Angenommen, der UI_Login-Standard wird in Version 1 generiert und dieses Mal in Version 2 gepackt. Da UI_Login verglichen wird, wird festgestellt, dass MD5 dies nicht getan hat In diesem Paket geändert, schreibt die AB-Paketversion, in der sich UI_Login befindet, immer noch 1, andere Änderungen und neue Ergänzungen schreiben 2 für die Ressourcenversionsnummer.


        /// <summary>
        /// 1. 创建补丁清单文件到输出目录
        /// params: isInit 创建的是否是包内的补丁清单
        ///         useAAB 创建的是否是aab包使用的补丁清单
        /// </summary>
        private void CreatePatchManifestFile(string[] allAssetBundles, bool isInit = false, bool useAAB = false)
        {
            // 加载旧文件
            PatchManifest patchManifest = LoadPatchManifestFile(isInit);

            // 删除旧文件
            string filePath = OutputPath + $"/{PatchDefine.PatchManifestFileName}";
            if (isInit)
                filePath = OutputPath + $"/{PatchDefine.InitManifestFileName}";
            if (File.Exists(filePath))
                File.Delete(filePath);

            // 创建新文件
            Log($"创建补丁清单文件:{filePath}");
            var sb = new StringBuilder();
            using (FileStream fs = File.OpenWrite(filePath))
            {
                using (var bw = new BinaryWriter(fs))
                {
                    // 写入强更版本信息
                    //bw.Write(GameVersion.Version);
                    //sb.AppendLine(GameVersion.Version.ToString());
                    int ver = BuildVersion;
                    // 写入版本信息
                    // if (isReview)
                    // {
                    //     ver = ver * 10;
                    // }
                    bw.Write(ver);
                    sb.AppendLine(ver.ToString());
                    
                    // 写入所有AssetBundle文件的信息
                    var fileCount = allAssetBundles.Length;
                    bw.Write(fileCount);
                    for (var i = 0; i < fileCount; i++)
                    {
                        var assetName = allAssetBundles[i];
                        string path = $"{OutputPath}/{assetName}";
                        string md5 = HashUtility.FileMD5(path);
                        long sizeKB = EditorTools.GetFileSize(path) / 1024;
                        int version = BuildVersion;
                        EBundlePos tag = EBundlePos.buildin;
                        string readableLabel = "undefined";
                        if (_labelToAssets.TryGetValue(assetName, out var list))
                        {
                            readableLabel = list[0].ReadableLabel;
                        if (useAAB)
                            tag = list[0].bundlePos;
                        }

                        // 注意:如果文件没有变化使用旧版本号
                        PatchElement element;
                        if (patchManifest.Elements.TryGetValue(assetName, out element))
                        {
                            if (element.MD5 == md5)
                                version = element.Version;
                        }
                        var curEle = new PatchElement(assetName, md5, version, sizeKB, tag.ToString(), isInit);
                        curEle.Serialize(bw);
                        
                        
                        if (isInit)
                            sb.AppendLine($"{assetName}={readableLabel}={md5}={sizeKB}={version}={tag.ToString()}");
                        else
                            sb.AppendLine($"{assetName}={readableLabel}={md5}={sizeKB}={version}");
                    }
                }

                string txtName = "PatchManifest.txt";
                if (isInit)
                    txtName = "InitManifest.txt";
                File.WriteAllText(OutputPath + "/" + txtName, sb.ToString());
                Debug.Log($"{OutputPath}/{txtName} OK");
            }
        }

Die generierte AB-Paketliste sieht folgendermaßen aus.

Die erste Zeile ist die SVN-Versionsnummer

Die zweite Zeile ist die Anzahl der AB-Pakete

Ab der dritten Zeile stehen die Ressourcenpaketinformationen, getrennt durch das =-Zeichen, um jeweils die gültigen Daten zu trennen

MD5.unity3d = Ressourcenpfad = HashId des Ressourcenpfads = Paketkörper KB-Größe = SVN-Versionsnummer = Hot-Update-Modus starten

Schreiben Sie abschließend diese InitManifest.txt in Bytes und senden Sie sie an den Server, um die Datenpakete zu vergleichen.

Im Ladeteil dieser Artikelserie werde ich das Laden von AB-Paketen offiziell erläutern. Dieser Artikel gibt nur eine kurze Einführung.

erster Schritt:

Wenn der Client startet, gelangt er in die Download-Listen-Statusmaschine. Http lädt zuerst die Datei InitManifest.txt oder InitManifest.bytes herunter und analysiert die AB-Paketliste.

Unten finden Sie den Code zum Parsen der AB-Paketliste.


    public class PatchElement
    {
        /// <summary>
        /// 文件名称
        /// </summary>
        public string Name { private set; get; }

        /// <summary>
        /// 文件MD5
        /// </summary>
        public string MD5 { private set; get; }

        /// <summary>
        /// 文件版本
        /// </summary>
        public int Version { private set; get; }

        /// <summary>
        /// 文件大小
        /// </summary>
        public long SizeKB { private set; get; }

        /// <summary>
        /// 构建类型
        /// buildin 在安装包中
        /// ingame  游戏中下载
        /// </summary>
        public string Tag { private set; get; }

        /// <summary>
        /// 是否是安装包内的Patch
        /// </summary>
        public bool IsInit { private set; get; }

        /// <summary>
        /// 下载文件的保存路径
        /// </summary>
        public string SavePath;

        /// <summary>
        /// 每次更新都会先下载到Sandbox_Temp目录,防止下到一半重启导致逻辑不一致报错
        /// temp目录下的文件在重新进入更新流程时先校验md5看是否要跳过下载
        /// </summary>
        public bool SkipDownload { get; set; }


        public PatchElement(string name, string md5, int version, long sizeKB, string tag, bool isInit = false)
        {
            Name = name;
            MD5 = md5;
            Version = version;
            SizeKB = sizeKB;
            Tag = tag;
            IsInit = isInit;
            SkipDownload = false;
        }

        public void Serialize(BinaryWriter bw)
        {
            bw.Write(Name);
            bw.Write(MD5);
            bw.Write(SizeKB);
            bw.Write(Version);
            if (IsInit)
                bw.Write(Tag);
        }

        public static PatchElement Deserialize(BinaryReader br, bool isInit = false)
        {
            var name = br.ReadString();
            var md5 = br.ReadString();
            var sizeKb = br.ReadInt64();
            var version = br.ReadInt32();
            var tag = EBundlePos.buildin.ToString();
            if (isInit)
                tag = br.ReadString();
            return new PatchElement(name, md5, version, sizeKb, tag, isInit);
        }
    }

Schritt zwei:

Bitte beachten Sie, dass das Unterbrechen und Fortsetzen des Herunterladens ebenfalls eine sehr wichtige Funktion ist. Die AB-Paketliste zeichnet die Größe jedes AB-Pakets auf. Wenn das Projekt gestartet wird, werden zuerst die AB-Pakete im Temp-Ordner durchlaufen. Wenn die Größe nicht übereinstimmt Das in der Liste aufgeführte wird aktiviert. HTTP-Download-Funktion, HTTP unterstützt die Wiederaufnahme des Haltepunkts und das herunterzuladende Datensegment ist im HTTP-Header definiert. Wenn Sie der Meinung sind, dass dies nicht sicher ist, können Sie das AB-Paket direkt löschen und erneut herunterladen.

Nachdem die AB-Paketliste analysiert wurde, wechseln Sie zur Download-Listen-Statusmaschine und beginnen mit dem Herunterladen jeder Datei in der Liste. Bitte beachten Sie, dass wir beim Herunterladen von Dateien für Hot-Updates zunächst einen temporären Ordner erstellen können. Alle AB-Pakete werden vor dem erfolgreichen Herunterladen heruntergeladen Sind hier, nachdem alle Downloads erfolgreich waren, schneiden Sie sie alle in den PersistentData-Ordner. Der PersistentData-Ordner ist das integrierte Sandbox-Verzeichnis von Unity, und Unity verfügt über Lese- und Schreibberechtigungen.

Nachdem alle Downloads abgeschlossen sind, schließen Sie das Ausschneiden des PersistentData-Ordners ab.

dritter Schritt:

Alle Ressourcen sind vorhanden und der formelle Geschäftsrahmen ist eingeführt.

Frage: Warum sollte das formelle Geschäftsframework nach Abschluss des Hot-Updates gestartet werden?

Derzeit basieren die meisten kommerziellen Projekte auf den Frameworks Tolua und Xlua, und viele Framework-Layer-Codes sind in Lua geschrieben. Lua-Code ist Teil des AB-Pakets und kann daher erst nach Abschluss des Hot-Updates gestartet werden.

Supongo que te gusta

Origin blog.csdn.net/liuyongjie1992/article/details/129184612
Recomendado
Clasificación