[게임개발][유니티]에셋번들 패키징 챕터(5) 매니페스트를 사용하여 리소스 인덱스를 두 번 빌드

목차

패키징 및 리소스 로딩 프레임워크 디렉터리

텍스트

본문을 시작하기 전에 여기에 패키징 코드를 넣겠습니다. 이전 코드는 생략되었으니 주의하시기 바랍니다. 이전 글과 직접 비교해 보세요. 이 글은 패키징 코드를 처음 실행하는 것부터 시작됩니다.

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);
    //加密代码省略,后面文章讲解
}

BuildPipeline.BuildAssetBundles 패키징 API를 처음 호출한 후(자세한 내용은 코드의 일곱 번째 줄 참조) AssetBundleManifest에 대한 참조가 반환됩니다.

[질문]:BuildPipeline.BuildAssetBundles 패키징 API는 이미 AB 패키지 간 종속성 참조를 생성하는 데 도움이 되었습니다. AB 패키지 간 참조 관계를 생성해야 하는 이유는 무엇입니까?

[답변]:BuildPipeline.BuildAssetBundles 패키징 API를 실행한 후 생성된 UnityManifest.manifest 파일은 모든AB 패키지 정보 및 종속성을 기록합니다. ! 기업 수준의 프로젝트 패키징은 증분 패키징을 고려해야 하기 때문에 각 AB의 어떤 버전이 입력되었는지 알고 싶고, SVN의 특정 단계부터 AB 패키지가 입력되었음을 기록하는 등의 표시가 필요합니다. 따라서 패키징 인터페이스에 의해 생성된 UnityManifest.manifest 파일은 반제품입니다.


UnityManifest.manifest 파일의 2차 처리에 대한 공식적인 소개부터 시작하겠습니다.

string[] allAssetBundles = buildManifest.GetAllAssetBundles(); allAssetBundles를 가져오고 CreateResManifest 메서드를 사용하여 Unity 에셋 파일을 생성하고 UnityManifest.manifest의 일부 데이터를 에셋 파일로 직렬화합니다. 자산의 직렬화 스크립트는 아래와 같이 ResManifes입니다.

UnityManifest.manifest 파일의 2차 처리코드는 다음과 같습니다.

//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;
}

데이터를 직렬화하는 코드는 다음과 같습니다.

 /// <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];
    }
}

그림에서 볼 수 있듯이 CreateResManifest 메서드는 자체 리소스 집합과 AB 패키지 인덱스 관계를 생성합니다.

ResManifes 직렬화(아래 코드) 파일은 3가지 유형의 데이터를 저장합니다.

  1. 모든 리소스 폴더 목록

  1. AB 패키지 리소스가 위치한 목록 번호, 리소스가 위치한 폴더의 목록 번호

  1. AB 패키지 이름, 종속 패키지 이름, 버전 번호 MD5 및 암호화 유형.


[질문]: 이 자산 파일을 직렬화해야 하는 이유는 무엇입니까?

질문에 답하기 전에 먼저 한 가지 질문을 드리고 싶습니다: 리소스 로딩은 확실히 개발자를 위한 것입니다. 개발자는 원하는 리소스가 있는 ab 패키지를 어떻게 찾나요?

[답변]: 프로젝트가 시작되면 이 자산 파일을 사용하여 모든 리소스에 대한 참조 정보를 생성해야 합니다. 프로젝트가 시작된 후에는 이 자산을 로드해야 합니다. .로딩 코드는 다음과 같습니다.

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;
}

위의 코드를 보면 이 에셋 파일도 번들에 포함되어 있고 별도의 ab 패키지라는 것을 알 수 있습니다. 이 기사의 제목인 "Manifest를 사용하여 리소스 인덱스를 두 번 빌드"를 다시 살펴보세요. 그러면 이 자산이 위치한 번들이 이 기사의 핵심입니다! ! !

개발자가 프로젝트에서 리소스를 로드하는 방법에 대해 이야기해 보겠습니다. 먼저 개발자는 Loader를 호출하여 리소스를 로드합니다. AB 패키지 로딩 모드를 사용하는 경우(로컬 리소스 로딩은 논의되지 않음) 리소스 경로가 전달되고, 성공 콜백 로드 중

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

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

우리는 프로젝트가 시작될 때 이 리소스 인덱스 파일이 로드된다는 것을 알고 있으므로 프레임워크는 모든 리소스 경로와 참조하는 AB 패키지 이름을 알고 있으므로 리소스를 로드할 때 자연스럽게 해당 AB 패키지와 리소스를 찾습니다. index 파일에는 AB 패키지도 기록됩니다. 상호 의존성은 대상 AB 패키지를 로드할 때 모든 종속 패키지를 재귀적으로 로드하기만 하면 됩니다.

이 보조 빌드 리소스 인덱스 파일을 프로젝트에서 사용하는 방법은 위에서 명확하게 설명했으며, 이제 프로젝트 시작 시 모든 AB 패키지를 핫 업로드하는 방법부터 시작하겠습니다.


CreatePatchManifestFile 메소드는 AB 패키지 다운로드 목록을 생성하는 방법으로, 새 목록을 생성하기 전에 이전 목록을 로드하고, AB 패키지에서 생성된 MD5를 비교하여 변경 사항이 있는지 확인한다는 점에 유의하시기 바랍니다. 변경되면 이전 목록의 버전 번호가 계속 사용됩니다. 예: UI_Login 기본값이 버전 1에서 생성되었고 이번에는 버전 2에 패키징되었다고 가정합니다. 이 패키지에서 변경되었더라도 UI_Login이 있는 AB 패키지 버전은 여전히 ​​1을 쓰고, 기타 변경 사항과 새로운 추가 사항은 리소스 버전 번호에 대해 2를 씁니다.


        /// <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");
            }
        }

생성된 AB 패키지 목록은 다음과 같습니다.

첫 번째 줄은 SVN 버전 번호입니다.

두 번째 줄은 AB 패키지 수입니다.

세 번째 줄부터 시작하는 리소스 패키지 정보는 각각 유효한 데이터를 구분하기 위해 = 기호로 구분됩니다.

MD5.unity3d = 리소스 경로 = 리소스 경로의 HashId = 패키지 본문 KB 크기 = SVN 버전 번호 = 핫 업데이트 모드 시작

마지막으로 이 InitManifest.txt를 바이트 단위로 작성하고 서버로 전송하여 데이터 패킷을 비교합니다.

이 기사 시리즈의 로딩 부분에서는 AB 패키지의 로딩에 대해 공식적으로 설명할 예정이며, 이 기사에서는 간략한 소개만 제공합니다.

첫 번째 단계:

클라이언트가 시작되면 다운로드 목록 상태 머신으로 들어가며, Http는 먼저 InitManifest.txt 또는 InitManifest.bytes 파일을 다운로드하고 AB 패키지 목록을 구문 분석합니다.

다음은 AB 패키지 목록을 구문 분석하는 코드입니다.


    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);
        }
    }

2단계:

다운로드 중단 및 재개도 매우 중요한 기능입니다. AB 패키지 목록에는 각 AB 패키지의 크기가 기록됩니다. 프로젝트가 시작되면 Temp 폴더에 있는 AB 패키지가 먼저 탐색됩니다. 크기가 일치하지 않는 경우 Http 다운로드 기능, Http는 중단점 재개를 지원하며 다운로드할 데이터 세그먼트는 Http Header에 정의되어 있습니다. 이것이 안전하지 않다고 생각되면 AB 패키지를 직접 삭제하고 다시 다운로드할 수 있습니다.

AB 패키지 목록이 구문 분석된 후 다운로드 목록 상태 머신으로 전환하고 목록의 각 파일 다운로드를 시작합니다. 핫 업데이트용 파일을 다운로드할 때 먼저 임시 폴더를 생성할 수 있습니다. 모든 AB 패키지가 성공적으로 다운로드되기 전에 여기에 있습니다. 모든 다운로드가 성공한 후 모두 PertantData 폴더로 잘라냅니다. PertantData 폴더는 Unity의 내장 샌드박스 디렉터리이며 Unity에는 읽기 및 쓰기 권한이 있습니다.

모든 다운로드가 완료되면 PertantData 폴더 잘라내기를 완료합니다.

세 번째 단계:

모든 자원이 마련되고 공식적인 비즈니스 프레임워크가 시작됩니다.

질문: 핫 업데이트가 완료된 후 공식 비즈니스 프레임워크를 시작하는 이유는 무엇입니까?

현재 대부분의 상용 프로젝트는 Tolua 및 Xlua 프레임워크를 기반으로 하며 많은 프레임워크 레이어 코드가 Lua로 작성됩니다. Lua 코드는 AB 패키지의 일부이므로 핫 업데이트가 완료된 후에만 시작할 수 있습니다.

추천

출처blog.csdn.net/liuyongjie1992/article/details/129184612