Plan y proceso de actualización en caliente de Unity

La actualización en caliente es un módulo muy importante en el desarrollo de juegos comerciales. La actualización en caliente mencionada aquí no significa solo corregir errores, sino actualizar las funciones del juego. En pocas palabras, después de iniciar el juego, ejecuta una barra, descarga los recursos y el código y luego ingresa al juego. El contenido escrito en este blog no es la solución óptima, solo completa la actualización en caliente. El uso específico requiere que los usuarios lo vean en función de sus propios proyectos.

La solución adoptada aquí es utilizar AssetBundle y xLua. El uso de AssetBundle es para un control autónomo completo de los recursos. La parte lógica de todo el juego se implementa utilizando xLua. Por supuesto, es imposible no tener ningún código C#, son solo algunos módulos funcionales centrales. Generalmente, las cosas que no cambiarán después de escribirse, o las cosas que requieren un alto rendimiento, se pueden colocar en C#.

Toda la función se divide en la parte del editor y la parte del tiempo de ejecución. La parte del editor es compilar Bundle, generar archivos de versión, etc. La parte del tiempo de ejecución es descargar el archivo de versión de CDN, comparar el número de versión y los recursos locales para ver si hay algo que deba actualizarse; de ​​ser así, actualícelo e ingrese al juego después de la actualización. Si no, ve directamente al juego.

Sección de edición

La parte del editor genera principalmente archivos Bundle. Primero, divido los Bundles por directorio. Los archivos en cualquier directorio (excluyendo los subdirectorios) se escribirán en un Bundle. Por ejemplo, la siguiente estructura de directorios

Res/
    - ConfigBytes/
    - UI/
    - LuaScripts/
    - Data/
        - ItemsData/
        - CharactersData/

En primer lugar, el directorio Res es el directorio principal de recursos y hay varios subdirectorios a continuación (no habrá archivos que deban agruparse en el directorio Res). Los archivos en el directorio ConfigBytes se escribirán en un paquete. Los archivos en el directorio de la interfaz de usuario se agruparán en un paquete. Los archivos en el directorio LuaScripts se agruparán en un paquete. Los archivos en el directorio ItemsData en el directorio Data se escribirán en un paquete, y el directorio CharactersData en el directorio Data se escribirá en un paquete. Si hay archivos (no directorios) en el directorio de datos, estos archivos se agruparán en un paquete. En pocas palabras, determinará qué archivos formar un paquete según la carpeta. Al verificar, solo se recuperarán los archivos de una carpeta y los subdirectorios de esta carpeta no se recuperarán de forma recursiva.

El directorio LuaScripts se colocará fuera del directorio Assets durante el desarrollo. Está al mismo nivel que este. Al compilar el paquete, el directorio LuaScripts y todos los archivos siguientes se copiarán al directorio Res de acuerdo con la estructura del directorio original, y cada xxx. La extensión del archivo lua se cambia a xxx.txt. Porque .lua no se reconoce en Unity.

El script de creación del paquete registrará cada recurso y el nombre del paquete donde se encuentra, y finalmente generará un archivo index.json, que registrará la ruta de carga de cada recurso y el nombre del paquete donde se encuentra.

Una vez generado el paquete, se generará un archivo version.json, que registra el nombre, MD5 y el tamaño de archivo de cada paquete. La actualización en caliente compara si un archivo necesita actualizarse juzgando si el MD5 del archivo remoto es el mismo que el MD5 del archivo local. Si no son iguales, es necesario actualizar el archivo remoto.

Lo anterior es lo que hace el editor. En resumen,

  1. Copie LuaScripts al directorio Assets/Res/

  1. Generar paquetes de archivos en el directorio Res/ por directorio

  1. Genere un archivo de índice de recursos (index.json) y escriba este archivo en un paquete

  1. Según el paquete generado, genere el archivo version.json

  1. Copie los archivos Bundle y version.json anteriores al directorio StreamingAssets

  1. Cargue los archivos Bundle y version.json anteriores al servidor remoto o CDN

Paso 5: La razón por la que se copia al directorio StreamingAssets es que cuando el usuario instala el juego por primera vez, la lógica de tiempo de ejecución primero determinará si hay recursos locales, de lo contrario, o el número de versión es menor que la versión. versión del archivo .json en StreamingAssets No., debes copiar los archivos Bundle y version.json en el directorio StreamingAssets al directorio Persistent. De esta manera, no necesitas instalar el juego por primera vez, y también necesitas para ejecutar un hilo para actualizar los recursos. Por supuesto, a pesar de este proceso, la verificación normal de la versión se seguirá realizando después de la copia.

Lo anterior es lo que se hace en el editor y lo siguiente es el proceso de ejecución.

Sección de actualización de recursos en tiempo de ejecución

La lógica de verificación y actualización de la versión se puede llevar a cabo en una escena independiente. Una vez completada la actualización, la escena saltará directamente a la escena de inicio del juego. Esta lógica es relativamente simple y clara, y no es propensa a errores.

Cuando se inicia el juego, primero extraerá el archivo version.json de forma remota y luego comparará el número de versión en el archivo version.json con el número de versión en el archivo version.json local. Si son diferentes, debe verificar qué paquetes deben actualizarse según la información del paquete en el archivo version.json. Después de encontrar los paquetes que deben actualizarse, descárguelos en secuencia. Debido a que version.json contiene el tamaño de archivo de cada paquete, aquí también se puede calcular el progreso de la barra de progreso de descarga.

Al comparar los números de versión, debes proceder de acuerdo con la situación real de tu juego, que se divide en el número de versión principal y el número de versión de actualización en caliente. Si los números de versión principal son diferentes, no es necesario juzgar el paquete directamente, por lo que que el usuario no puede ingresar al juego, y una ventana emergente le indica al usuario que simplemente descargue el último paquete de instalación. Si las versiones principales son las mismas, se realiza la lógica de actualización en caliente. El juicio de la versión principal aquí no debe basarse en el número de versión en el archivo version.json, es mejor juzgar en función del número de versión en el código del paquete o en un archivo de configuración del paquete, porque la versión El archivo .json está en Para Android, es fácil encontrar este archivo en el directorio de lectura y escritura del teléfono móvil y luego cambiarlo, evitando así las actualizaciones en caliente.

Una vez descargado el paquete, debe escribir el archivo version.json remoto localmente y sobrescribir el archivo version.json local.

Finalmente, otro paso de la revisión de recursos locales es calcular si el MD5 de cada paquete local es consistente con el MD5 en version.json. Si es inconsistente, se requiere una ventana emergente para decirle al jugador que el recurso debe ser reparado manualmente, o el reproductor necesita descargarlo y sobrescribirlo directamente. La reparación manual significa volver a descargar recursos desde el control remoto para sobrescribirlos.

Una vez superado el último paso de verificación de recursos, salte a la escena de inicio de la lógica del juego.

Implementé la lógica de verificación y actualización de versiones en C#. Por supuesto, también se puede implementar en Lua, pero es necesario volver a crear todo el entorno Lua después de la actualización para garantizar que se utilicen los recursos más recientes.

运行时资源加载部分

资源的加载,可以使用一个资源管理器脚本来实现。资源管理器在初始化时首先要加载 Bundle 的 AssetBundleManifest 信息,这个资源里记录了各个 Bundle 与其他 Bundle 的依赖关系。然后加载 index 文件,也就是我们一开始生成的资源索引文件,这样才能知道哪一个资源,在哪一个 Bundle 里。当要加载一个资源时,传入资源加载路径,首先会根据 index 文件中的信息,找到这个 Bundle,然后从 Manifest 信息中,读取这个 Bundle 的依赖 Bundle,如果有,则先加载依赖,最后,再加载当前 Bundle。Bundle加载完后,从 Bundle 中加载资源。

具体代码(仅供参考)

AssetBundleBuilder.cs 编辑器下编 Bundle 的代码

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Build;
using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Xml.Linq;
using System.Security.Cryptography;
using UnityEditor.Build.Reporting;

// 注意:BundleCombineConfig.json 中的配置,目录最后!不要!加上 '/'publicclassAssetBundleBuilder{
privatestaticstring RES_TO_BUILD_PATH = "Assets/Res/";
privatestaticstring MANIFEST_FILES_PATH = string.Format("{0}/../BundleManifest/", Application.dataPath);
privatestatic StringBuilder IndexFileContent = null;
privatestatic StringBuilder VersionFileContent = null;
privatestatic MD5 md5 = null;
privatestatic BuildAssetBundleOptions BuildOption = BuildAssetBundleOptions.ChunkBasedCompression |
                                                        BuildAssetBundleOptions.ForceRebuildAssetBundle;

privatestatic BundleCombineConfig combineConfig = null;
privatestatic Dictionary<string, int> combinePathDict = null;

privatestaticstring version = "0.0.0";
privatestaticbool copyToStreaming = false;

private static voidInitBuilder()    {
        IndexFileContent = new StringBuilder();
        VersionFileContent = new StringBuilder();
        md5 = new MD5CryptoServiceProvider();
        combineConfig = null;
        combinePathDict = new Dictionary<string, int>();
    }

private static voidWriteIndexFile(string key, string value)    {
        IndexFileContent.AppendFormat("{0}:{1}", key, value);
        IndexFileContent.AppendLine();
    }

private static voidWriteVersionFile(string key, string value1, long value2)    {
        VersionFileContent.AppendFormat("{0}:{1}:{2}", key, value1, value2);
        VersionFileContent.AppendLine();
    }

private static longGetFileSize(string fileName)    {
try        {
            FileInfo fileInfo = new FileInfo(fileName);
return fileInfo.Length;
        }
catch (Exception ex)
        {
thrownew Exception("GetFileSize() fail, error:" + ex.Message);
        }
    }

private static stringGetMD5(byte[] retVal)    {
        StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
        {
            sb.Append(retVal[i].ToString("x2"));
        }
return sb.ToString();
    }

private static stringGetMD5HashFromFile(string fileName)    {
try        {
            FileStream file = new FileStream(fileName, FileMode.Open);
byte[] retVal = md5.ComputeHash(file);
            file.Close();

return GetMD5(retVal);
        }
catch (Exception ex)
        {
thrownew Exception("GetMD5HashFromFile() fail, error:" + ex.Message);
        }
    }

static stringGetBundleName(string path)    {
byte[] md5Byte = md5.ComputeHash(Encoding.Default.GetBytes(path));
string str = GetMD5(md5Byte) + ".assetbundle";
return str;
    }
privateclassBuildBundleData    {
private AssetBundleBuild build = new AssetBundleBuild();
privateList<string> assets = new List<string>();
privateList<string> addresses = new List<string>();

publicBuildBundleData(string bundleName)        {
            build.assetBundleName = bundleName;
        }

public voidAddAsset(string filePath)        {
string addressableName = GetAddressableName(filePath);
            assets.Add(filePath);
            addresses.Add(addressableName);
            WriteIndexFile(addressableName, build.assetBundleName);
        }

public AssetBundleBuild Gen()        {
            build.assetNames = assets.ToArray();
            build.addressableNames = addresses.ToArray();
return build;
        }
    }

private static stringGetAddressableName(string file_path)    {
string addressable_name = file_path;
        addressable_name = addressable_name.Replace(RES_TO_BUILD_PATH, "");
int dot_pos = addressable_name.LastIndexOf('.');
if (dot_pos != -1)
        {
int count = addressable_name.Length - dot_pos;
            addressable_name = addressable_name.Remove(dot_pos, count);
        }
return addressable_name;
    }

private static string[] GetTopDirs(string rPath)    {
return Directory.GetDirectories(rPath, "*", SearchOption.TopDirectoryOnly);
    }

private static voidCopyLuaDir()    {
// Copy Luastring luaOutPath = Application.dataPath + "/../LuaScripts";
string luaInPath = Application.dataPath + "/Res/LuaScripts";

        DeleteLuaDir();

        MoeUtils.DirectoryCopy(luaOutPath, luaInPath, true, ".txt");
        AssetDatabase.Refresh();
    }

private static voidDeleteLuaDir()    {
string luaInPath = Application.dataPath + "/Res/LuaScripts";

if (Directory.Exists(luaInPath))
        {
            Directory.Delete(luaInPath, true);
        }
    }

public static voidBuildBundleWithVersion(string v, bool copy)    {
        version = v;
        copyToStreaming = copy;
        BuildAssetBundle();
    }

    [MenuItem("Tools/Build Bundles")]private static voidBuildAssetBundle()    {
if (version == "0.0.0")
        {
            Debug.LogErrorFormat("请确认版本号");
return;
        }
        CopyLuaDir();

        InitBuilder();
        LoadBundleCombineConfig();
        Dictionary<string, BuildBundleData> bundleDatas = new Dictionary<string, BuildBundleData>();
        IndexFileContent.Clear();
        VersionFileContent.Clear();

        List<DirBundleInfo> dirList = new List<DirBundleInfo>();

// ============================        Queue<DirBundleInfo> dirQueue = new Queue<DirBundleInfo>();
        dirQueue.Enqueue(new DirBundleInfo(RES_TO_BUILD_PATH));
while (dirQueue.Count > 0)
        {
            DirBundleInfo rootDirInfo = dirQueue.Dequeue();
if (rootDirInfo.dir != RES_TO_BUILD_PATH)
            {
if (combinePathDict.ContainsKey(rootDirInfo.dir))
                {
                    rootDirInfo.combine2Dir = rootDirInfo.dir;
                }
                dirList.Add(rootDirInfo);
            }

foreach (string subDir inGetTopDirs(rootDirInfo.dir))            {
                DirBundleInfo subDirInfo = new DirBundleInfo(subDir);
                subDirInfo.combine2Dir = rootDirInfo.combine2Dir;
                dirQueue.Enqueue(subDirInfo);

                Debug.LogFormat("Dir: {0}, Combine2Dir: {1}", subDirInfo.dir, subDirInfo.combine2Dir);
            }
        }

foreach (DirBundleInfo dirInfo in dirList)
        {
string[] files = GetFiles(dirInfo.dir, SearchOption.TopDirectoryOnly);
if (files.Length > 0)
            {
                Debug.LogFormat("Dir: {0}, FileCount: {1}", dirInfo.dir, files.Length);
string bundleDirName = dirInfo.BundleDirName;
                BuildBundleData bbData = null;
if (bundleDatas.ContainsKey(bundleDirName))
                {
                    bbData = bundleDatas[bundleDirName];
                }
else                {
                    bbData = new BuildBundleData(GetBundleName(bundleDirName));
                    bundleDatas.Add(bundleDirName, bbData);
                }

foreach (string file in files)
                {
                    bbData.AddAsset(file);
                }
            }
        }

        List<AssetBundleBuild> bundleBuildList = new List<AssetBundleBuild>();
foreach (BuildBundleData data in bundleDatas.Values)
        {
            bundleBuildList.Add(data.Gen());
        }

string index_file_path = string.Format("{0}{1}.txt", RES_TO_BUILD_PATH, "index");
        File.WriteAllText(index_file_path, IndexFileContent.ToString());
        AssetDatabase.ImportAsset(index_file_path);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        AssetBundleBuild indexBuild = new AssetBundleBuild();
        indexBuild.assetBundleName = "index";
        indexBuild.assetNames = newstring[] { index_file_path };
        indexBuild.addressableNames = newstring[] { "index" };
        bundleBuildList.Add(indexBuild);
string bundleExportPath = string.Format("{0}/{1}/", Application.dataPath + "/../streaming", "Bundles");
if (Directory.Exists(bundleExportPath))
        {
            Directory.Delete(bundleExportPath, true);
        }
        Directory.CreateDirectory(bundleExportPath);

if (Directory.Exists(MANIFEST_FILES_PATH))
        {
            Directory.Delete(MANIFEST_FILES_PATH, true);
        }
        Directory.CreateDirectory(MANIFEST_FILES_PATH);

        BuildPipeline.BuildAssetBundles(bundleExportPath, bundleBuildList.ToArray(), BuildOption, EditorUserBuildSettings.activeBuildTarget);
        AssetDatabase.Refresh();
        DeleteLuaDir();
        AssetDatabase.Refresh();

// VersionProfile
        List<VersionBundleInfo> versionBundleList = new List<VersionBundleInfo>();
        MoeVersionInfo versionInfo = new MoeVersionInfo();
        versionInfo.version = version;
        versionInfo.asset_date = DateTime.Now.ToString("yyyyMMddHHmm");
string[] ab_files = Directory.GetFiles(bundleExportPath);
foreach (string ab_file in ab_files)
        {
if (Path.GetExtension(ab_file) == ".manifest")
            {
string new_path = ab_file.Replace(bundleExportPath, MANIFEST_FILES_PATH);
                File.Move(ab_file, new_path);
            }
else            {

                Debug.LogFormat("BundleName: {0}", ab_file);
var data = File.ReadAllBytes(ab_file);
using (var abStream = new AssetBundleStream(ab_file, FileMode.Create))
                {
                    abStream.Write(data, 0, data.Length);
                }

string md5 = GetMD5HashFromFile(ab_file);
long size = GetFileSize(ab_file);
string bundleName = string.Format("Bundles/{0}", Path.GetFileName(ab_file));
                VersionBundleInfo bInfo = new VersionBundleInfo();
                bInfo.bundle_name = bundleName;
                bInfo.md5 = md5;
                bInfo.size = size;
                versionBundleList.Add(bInfo);
            }
        }

        versionInfo.bundles = versionBundleList.ToArray();
string versionInfoText = Newtonsoft.Json.JsonConvert.SerializeObject(versionInfo);

        File.WriteAllText(string.Format("{0}/{1}", bundleExportPath, "version.json"), versionInfoText);

if (copyToStreaming)
        {
            CopyBundleToStreaming(bundleExportPath);
        }
        MoveToVersionDir(bundleExportPath, version);
        AssetDatabase.Refresh();
    }

private static voidMoveToVersionDir(string rootBundlePath, string version)    {
string destPath = rootBundlePath + "/" + version;
        Directory.CreateDirectory(destPath);
        destPath += "/Bundles";
        Directory.CreateDirectory(destPath);

string[] files = GetFiles(rootBundlePath, SearchOption.TopDirectoryOnly);
foreach (string file in files)
        {
string fileName = System.IO.Path.GetFileName(file);
string destFilePath = destPath + "/" + fileName;
            File.Move(file, destFilePath);
        }
    }

private static voidCopyBundleToStreaming(string bundleExportPath)    {
string destPath = Application.streamingAssetsPath + "/Bundles";
if (Directory.Exists(destPath))
        {
            Directory.Delete(destPath, true);
        }

        MoeUtils.DirectoryCopy(bundleExportPath, destPath, true);
    }

private static string[] GetFiles(string path, SearchOption so)    {
string[] files = Directory.GetFiles(path, "*", so);
        List<string> fileList = new List<string>();
foreach (string file in files)
        {
string ext = Path.GetExtension(file);
if (ext == ".meta" || ext == ".DS_Store")
            {
continue;
            }
            fileList.Add(file);
        }

return fileList.ToArray();
    }

classDirBundleInfo    {
publicstring dir;
publicstring combine2Dir;

publicbool IsCombine
        {
get            {
return !string.IsNullOrEmpty(combine2Dir);
            }
        }

publicstring BundleDirName
        {
get            {
if (IsCombine)
                {
return combine2Dir;
                }
else                {
return dir;
                }
            }
        }

publicDirBundleInfo(string dir, string combine2Dir = null)        {
this.dir = dir;
this.combine2Dir = combine2Dir;
        }

    }

classBundleCombineConfig    {
publicstring[] combieDirs;
    }

private static voidLoadBundleCombineConfig()    {
string path = Application.dataPath + RES_TO_BUILD_PATH.Replace("Assets", "") + "BundleCombineConfig.json";
if (File.Exists(path))
        {
string text = File.ReadAllText(path);
if (!string.IsNullOrEmpty(text))
            {
                combineConfig = Newtonsoft.Json.JsonConvert.DeserializeObject<BundleCombineConfig>(text);
if (combineConfig != null)
                {
                    Debug.LogFormat("Bundle合并配置成功!");
foreach (string cPath in combineConfig.combieDirs)
                    {
if (!combinePathDict.ContainsKey(cPath))
                        {
                            combinePathDict.Add(cPath, 0);
                        }
                    }
                }
            }
        }
    }
}

MoeVersionManager.cs 资源版本检查及 Bundle 更新逻辑

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using BestHTTP;
using System;

publicclassMoeVersionManager : MoeSingleton<MoeVersionManager>
{
conststring REMOTE_URL = "这里改成自己的CDN域名或IP";
staticstring VERSION_FILE_DIR;
staticstring VERSION_FILE_PATH;
staticstring IN_VERSION_FILE_PATH;

private MoeVersionInfo currVersionInfo = null;
private MoeVersionInfo remoteVersionInfo = null;
private UpdateInfo updateInfo = null;

privatestatic OnVersionStateParam versionStateParam = new OnVersionStateParam();
privatestatic OnUpdateProgressParam updateProgressParam = new OnUpdateProgressParam();
privatestatic OnVersionMsgBoxParam msgBoxParam = new OnVersionMsgBoxParam();

privateenum EnProcessType
    {
        Normal,
        Fix,
    }

private Action<EnProcessType> actionTryUnCompress = null;
private Action<EnProcessType> actionUpdateVersionFile = null;
private Action<EnProcessType> actionUpdateBundles = null;
private Action<EnProcessType> actionCheckAssets = null;
private Action<EnProcessType> actionForceUpdateVersionFile = null;



protected override voidInitOnCreate()    {
        VERSION_FILE_DIR = Application.persistentDataPath + "/Bundles/";
        VERSION_FILE_PATH = Application.persistentDataPath + "/Bundles/version.json";
        IN_VERSION_FILE_PATH = Application.streamingAssetsPath + "/Bundles/version.json";
        Debug.LogFormat("{0}", VERSION_FILE_PATH);
        InitProcessChain();
        StartNormalProcess();
    }


private voidInitProcessChain()    {
this.actionTryUnCompress = (EnProcessType param) =>
        {
            Debug.LogFormat("Action>>> 解压: {0}", param);
this.currVersionInfo = LoadVersionInfo(VERSION_FILE_PATH);
// if (!CheckBundleCorrect())if (currVersionInfo == null)
            {
                UpdateUIState("正在解压资源");
                UnCompressBundle();
this.currVersionInfo = LoadVersionInfo(VERSION_FILE_PATH);
            }
else            {
// 判断是不是更新包,也就是StreamingAssets里的版本是否比Persistent版本高,如果高的话,再次解压Bundle                MoeVersionInfo inVersionInfo = LoadVersionInfo(IN_VERSION_FILE_PATH);
if (inVersionInfo != null)
                {
int[] inVersionDigit = inVersionInfo.GetVersionDigitArray();
int[] currVersionDigit = this.currVersionInfo.GetVersionDigitArray();
// if (inVersionInfo.GetVersionLong() > this.currVersionInfo.GetVersionLong())if (inVersionDigit[0] > currVersionDigit[0] ||
                       inVersionDigit[1] > currVersionDigit[1] ||
                       inVersionDigit[2] > currVersionDigit[2])
                    {
// 包里的版本比Persistent的版本高,可能玩家进行了大版本更新,重新解压                        Debug.LogFormat("包里的Bundle版本 > Persistent Bundle 版本,重新解压");
                        UpdateUIState("正在解压资源");
                        UnCompressBundle();
this.currVersionInfo = LoadVersionInfo(VERSION_FILE_PATH);
                    }
else                    {
                        Debug.LogFormat("包里Bundle版本 <= Persistent Bundle版本,无需解压~");
                    }
                }
else                {
                    Debug.LogErrorFormat("逻辑错误,从StreamingAssets 中加载VersionInfo文件失败");
                }
            }
        };

this.actionUpdateVersionFile = (EnProcessType param) =>
        {
            Debug.LogFormat("Action>>> 获取远程版本文件: {0}", param);
            StartCoroutine(TryUpdateVersion((bool ok, bool majorUpdate) =>
            {
if (ok)
                {
if (majorUpdate)
                    {
// 调用商店                        OnMsgBox("新的大版本已更新,请下载最新安装包!", "确定", () =>
                        {
                            JumpToDownloadMarket();
                        });
                    }
else                    {
// 成功了,接下来更新Bundlethis.actionUpdateBundles?.Invoke(param);
                    }
                }
else                {
// 版本文件更新失败,弹窗询问                    OnMsgBox("版本信息获取失败,请检查网络连接!", "重试", () =>
                    {
this.actionUpdateVersionFile?.Invoke(param);
                    });
                }
            }));
        };

this.actionForceUpdateVersionFile = (EnProcessType param) =>
        {
            Debug.LogFormat("Action>>> 强制获取远程版本文件: {0}", param);
            TryDeleteBundleDir();
            TryCreateBundleDir();
            StartCoroutine(TryUpdateVersion((bool ok, bool majorUpdate) =>
            {
if (ok)
                {
if (majorUpdate)
                    {
// 调用商店                        OnMsgBox("新的大版本已更新,请下载最新安装包!", "确定", () =>
                        {
                            JumpToDownloadMarket();
                        });
                    }
else                    {
// 成功了,接下来更新Bundlethis.actionUpdateBundles?.Invoke(param);
                    }
                }
else                {
// 版本文件更新失败,弹窗询问                    OnMsgBox("版本信息获取失败,请检查网络连接!", "重试", () =>
                    {
this.actionForceUpdateVersionFile?.Invoke(param);
                    });
                }
            }, true));
        };

this.actionUpdateBundles = (EnProcessType param) =>
        {
            Debug.LogFormat("Action>>> 更新Bundle: {0}", param);
            StartCoroutine(TryUpdateBundle((bool ok) =>
            {
if (ok)
                {
// 成功了,接下来检查资源,this.actionCheckAssets?.Invoke(param);
                }
else                {
                    OnMsgBox("资源下载失败,请检查网络连接!", "重试", () =>
                    {
this.actionUpdateBundles(param);
                    });
                }
            }));
        };

this.actionCheckAssets = (EnProcessType param) =>
        {
            Debug.LogFormat("Action>>> 校对资源: {0}", param);
if (!CheckBundleCorrect())
            {
// 更新完了,本地Bundle还是不对                Debug.LogFormat("更新完Bundle后,发现文件不对");
if (param == EnProcessType.Normal)
                {
                    OnMsgBox("资源有错误,请修复客户端!", "修复", () =>
                    {
this.actionForceUpdateVersionFile?.Invoke(EnProcessType.Fix);
                    });
                }
else                {
                    OnMsgBox("客户端修复失败,请重新下载安装包!", "确定", () =>
                    {
                        JumpToDownloadMarket();
                    });
                }
            }
else            {
                UpdateUIState("进入游戏");
                MoeEventManager.Inst.SendEvent(EventID.Event_OnUpdateEnd);
            }
        };
    }



// 跳转到下载商店private voidJumpToDownloadMarket()    {
        Application.OpenURL("https://taptap.com");
    }

private voidStartNormalProcess()    {
        TryCreateBundleDir();
this.actionTryUnCompress?.Invoke(EnProcessType.Normal);
this.actionUpdateVersionFile?.Invoke(EnProcessType.Normal);
    }

private voidStartFixProcess()    {
this.actionForceUpdateVersionFile(EnProcessType.Fix);
    }


private MoeVersionInfo LoadVersionInfo(string path)    {
        Debug.LogFormat("加载 Version 文件: {0}", path);
try        {
if (System.IO.File.Exists(path))
            {
string text = System.IO.File.ReadAllText(path);
if (!string.IsNullOrEmpty(text))
                {
                    MoeVersionInfo vInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<MoeVersionInfo>(text);
if (vInfo != null)
                    {
                        Debug.LogFormat("Version 信息加载成功: {0}", vInfo.version);
return vInfo;
                    }
                }
else                {
                    Debug.LogFormat("Version 文件内容为空");
                }
            }
else            {
                Debug.LogFormat("Version 文件不存在");
            }
        }
catch (System.Exception e)
        {
            Debug.LogErrorFormat("读取Version文件出错: {0}", e.ToString());
        }

returnnull;
    }

///<summary>/// 从 StreamingAssets 里将Bundle拷贝到 Persistent 目录里 ///</summary>private voidUnCompressBundle()    {
        TryDeleteBundleDir();
        TryCreateBundleDir();
        Debug.LogFormat("尝试从 Steaming 拷贝Bundle 到 Persistent");
try        {
if (System.IO.File.Exists(IN_VERSION_FILE_PATH))
            {
string text = System.IO.File.ReadAllText(IN_VERSION_FILE_PATH);
                Debug.LogFormat("Text: {0}", text);
                MoeVersionInfo inVersionInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<MoeVersionInfo>(text);
if (inVersionInfo != null)
                {
// 拷贝 Bundleforeach (VersionBundleInfo bundleInfo in inVersionInfo.bundles)
                    {
string srcFilePath = string.Format("{0}/{1}", Application.streamingAssetsPath, bundleInfo.bundle_name);
string destFilePath = string.Format("{0}/{1}", Application.persistentDataPath, bundleInfo.bundle_name);
                        Debug.LogFormat("拷贝Bundle, {0} -> {1}", srcFilePath, destFilePath);
                        System.IO.File.Copy(srcFilePath, destFilePath, true);
                    }

// 拷贝 Version文件                    System.IO.File.Copy(IN_VERSION_FILE_PATH, VERSION_FILE_PATH, true);
                }
            }
else            {
                Debug.LogErrorFormat("解压失败,StreamingAssets 中没有 Version 文件");
            }
        }
catch (System.Exception e)
        {
            Debug.LogErrorFormat("Bundle拷贝出错! {0}", e.ToString());
        }
    }

public voidTryCreateBundleDir()    {
if (!System.IO.Directory.Exists(VERSION_FILE_DIR))
        {
            Debug.LogFormat("创建 Persistent Bundle 目录");
            System.IO.Directory.CreateDirectory(VERSION_FILE_DIR);
        }
else        {
            Debug.LogFormat("Persistent Bundle 目录已存在,不需要创建");
        }
    }

public voidTryDeleteBundleDir()    {
if (System.IO.Directory.Exists(VERSION_FILE_DIR))
        {
            System.IO.Directory.Delete(VERSION_FILE_DIR, true);
        }
    }

private stringGetLocalBundleMD5(string bundle_name)    {
string bundleFilePath = string.Format("{0}/{1}", Application.persistentDataPath, bundle_name);
if (System.IO.File.Exists(bundleFilePath))
        {
string md5 = MoeUtils.GetMD5HashFromFile(bundleFilePath);
return md5;
        }

returnnull;
    }

///<summary>/// 检查当前的Bundle是否正确 ///</summary>///<returns></returns>public boolCheckBundleCorrect()    {
if (currVersionInfo != null)
        {
foreach (VersionBundleInfo bundleInfo in currVersionInfo.bundles)
            {
string bundleFilePath = string.Format("{0}/{1}", Application.persistentDataPath, bundleInfo.bundle_name);
bool matched = false;

if (GetLocalBundleMD5(bundleInfo.bundle_name) == bundleInfo.md5)
                {
                    matched = true;
                }
else                {
                    Debug.LogErrorFormat("MD5 不匹配: {0}, FileMD5: {1}, bInfoMD5: {2}", bundleInfo.bundle_name, GetLocalBundleMD5(bundleInfo.bundle_name), bundleInfo.md5);
                }

if (!matched)
                {
returnfalse;
                }
            }

            Debug.LogFormat("本地Bundle文件检完全正确");
returntrue;
        }
else        {
returnfalse;
        }
    }

///<summary>//</summary>///<param name="callback"><是否成功,是否是强更></param>///<param name="force"></param>///<returns></returns>private IEnumerator TryUpdateVersion(System.Action<bool, bool> callback, bool force = false)    {
        UpdateUIState("正在检查更新");
this.remoteVersionInfo = null;
this.updateInfo = null;
string remoteVersionUrl = REMOTE_URL + "/fishing/version.json";
        Debug.LogFormat("开始下载远程 Version 文件: {0}", remoteVersionUrl);
        HTTPRequest request = new HTTPRequest(new System.Uri(remoteVersionUrl), false, true, null).Send();

while (request.State < HTTPRequestStates.Finished)
        {
yield return newWaitForSeconds(0.1f);
        }

if (request.State == HTTPRequestStates.Finished &&
         request.Response.IsSuccess)
        {
string remoteVersionText = request.Response.DataAsText;
if (!string.IsNullOrEmpty(remoteVersionText))
            {
                MoeVersionInfo remoteVersionInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<MoeVersionInfo>(remoteVersionText);
if (remoteVersionInfo != null)
                {
                    Debug.LogFormat("远程 Version 文件解析成功, Version: {0}", remoteVersionInfo.version);
// 判断是否要更新
int appMajorVersion = AppConfig.Inst.GetMajorVersion();
// 判断是否要强更int remoteMajor = remoteVersionInfo.GetMajorVersion();
if (remoteMajor > appMajorVersion)
                    {
// 这是一个需要强更的版本,需要提示用户去商店下载                        Debug.LogFormat("发现强更版本,需要重新下包,进行大版本更新!");
                        callback?.Invoke(true, true);
                        callback = null;

                        UpdateUIState("新的大版本已更新,请下载最新安装包!");
                    }
else                    {
// 强制修复if (force)
                        {
this.remoteVersionInfo = remoteVersionInfo;
                            List<VersionBundleInfo> updateBundleList = new List<VersionBundleInfo>();
                            updateBundleList.AddRange(remoteVersionInfo.bundles);
// 有需要更新的包this.updateInfo = new UpdateInfo();
this.updateInfo.remoteVersionInfo = remoteVersionInfo;
this.updateInfo.updateBundleList = updateBundleList;
                            Debug.LogFormat("强制更新,有需要更新的Bundle");
                            callback?.Invoke(true, false);
                            callback = null;
                        }
else                        {
// 正常更新int[] remoteVersionDigit = remoteVersionInfo.GetVersionDigitArray();
int[] currVersionDigit = this.currVersionInfo == null ? newint[] { 0, 0, 0 } : this.currVersionInfo.GetVersionDigitArray();
// if (this.currVersionInfo == null || remoteVersionInfo.GetVersionLong() > this.currVersionInfo.GetVersionLong())if (remoteVersionDigit[0] > currVersionDigit[0] ||
                               remoteVersionDigit[1] > currVersionDigit[1] ||
                               remoteVersionDigit[2] > currVersionDigit[2])
                            {
                                Debug.LogFormat("这次需要热更新");
this.remoteVersionInfo = remoteVersionInfo;
                                List<VersionBundleInfo> updateBundleList = new List<VersionBundleInfo>();

foreach (VersionBundleInfo rBInfo in remoteVersionInfo.bundles)
                                {
if (GetLocalBundleMD5(rBInfo.bundle_name) != rBInfo.md5)
                                    {
                                        updateBundleList.Add(rBInfo);
                                    }
                                }

// 有需要更新的包this.updateInfo = new UpdateInfo();
this.updateInfo.remoteVersionInfo = remoteVersionInfo;
this.updateInfo.updateBundleList = updateBundleList;
                                Debug.LogFormat("有需要更新的Bundle");
                                callback?.Invoke(true, false);
                                callback = null;
                            }
else                            {
                                Debug.LogFormat("远程版本号 {0} <= 本地版本号 {1},无需更新!", remoteVersionInfo.version, this.currVersionInfo.version);
                                callback?.Invoke(true, false);
                                callback = null;
                            }
                        }
                    }
                }
else                {
                    Debug.LogErrorFormat("远程 Version 文件反序列化失败: {0}", remoteVersionText);
                }
            }
else            {
                Debug.LogErrorFormat("远程 Version 文件内容为空");
            }
        }
else        {
            Debug.LogErrorFormat("远程 Version 文件下载失败: {0}, {1}", request.State, request.Response.StatusCode);
        }

        BestHTTP.PlatformSupport.Memory.BufferPool.Release(request.Response.Data);
        callback?.Invoke(false, false);
    }

private IEnumerator TryUpdateBundle(System.Action<bool> callback)    {
if (this.remoteVersionInfo != null && this.updateInfo != null)
        {
long totalSize = 0;
foreach (VersionBundleInfo bInfo inthis.updateInfo.updateBundleList)
            {
                totalSize += bInfo.size;
            }

            UpdateUIDownload(totalSize, 0);
long downloadedSize = 0;
bool hasError = false;

foreach (VersionBundleInfo bInfo inthis.updateInfo.updateBundleList)
            {
                Debug.LogFormat("Bundle信息 {0} | {1}", GetLocalBundleMD5(bInfo.bundle_name), bInfo.md5);
if (GetLocalBundleMD5(bInfo.bundle_name) != bInfo.md5)
                {
string remoteBundleUrl = string.Format("{0}/fishing/{1}/{2}", REMOTE_URL, this.updateInfo.remoteVersionInfo.version, bInfo.bundle_name);
                    Debug.LogFormat("开始更新Bundle: {0}", remoteBundleUrl);
                    HTTPRequest request = new HTTPRequest(new System.Uri(remoteBundleUrl), false, true, null).Send();
while (request.State < HTTPRequestStates.Finished)
                    {

yield return newWaitForSeconds(0.1f);
                    }

if (request.State == HTTPRequestStates.Finished && request.Response.IsSuccess)
                    {
                        downloadedSize += bInfo.size;
string bundleWritePath = Application.persistentDataPath + "/" + bInfo.bundle_name;
// 写入Bundle文件                        System.IO.File.WriteAllBytes(bundleWritePath, request.Response.Data);
                        Debug.LogFormat("{0} 更新完成", bInfo.bundle_name);
                        UpdateUIDownload(totalSize, downloadedSize);
                    }
else                    {
                        Debug.LogErrorFormat("{0} 下载出错: {1}, {2}", bInfo.bundle_name, request.State, request.Response.IsSuccess);
                        callback?.Invoke(false);
                        callback = null;
                        hasError = true;
break;
                    }
yieldreturnnull;
                    BestHTTP.PlatformSupport.Memory.BufferPool.Release(request.Response.Data);
                }
else                {
                    Debug.LogFormat("!!!!!!!!!!! 本地已存在需要更新的 {0},跳过下载", bInfo.bundle_name);
                    downloadedSize += bInfo.size;
                    UpdateUIDownload(totalSize, downloadedSize);
                }
            }

if (!hasError)
            {
                Debug.LogFormat("写入远程 Version 文件");
// 最后写入Version文件string versionText = Newtonsoft.Json.JsonConvert.SerializeObject(this.updateInfo.remoteVersionInfo);
                System.IO.File.WriteAllText(VERSION_FILE_PATH, versionText);
yieldreturnnull;

// 重新加载一遍本地文件this.currVersionInfo = LoadVersionInfo(VERSION_FILE_PATH);
                UpdateUIState("更新完成");
            }
        }
else        {
            Debug.LogFormat("无需要更新,前置数据不足: remoteVersionInfo is Null: {0}, updateInfo is Null: {1}", this.remoteVersionInfo == null, this.updateInfo == null);
        }
        callback?.Invoke(true);
    }

privateclassUpdateInfo    {
public MoeVersionInfo remoteVersionInfo;
public List<VersionBundleInfo> updateBundleList;
    }

private voidUpdateUIState(string msg)    {
        versionStateParam.state = msg;
        MoeEventManager.Inst.SendEvent(EventID.Event_OnVersionState, versionStateParam);
    }

private voidUpdateUIDownload(long total, long downloaded)    {
        updateProgressParam.totalUpdateSize = total;
        updateProgressParam.nowUpdatedSize = downloaded;
        MoeEventManager.Inst.SendEvent(EventID.Event_OnUpdateProgress, updateProgressParam);
    }

private voidOnMsgBox(string msg, string btnText, System.Action callback)    {
        msgBoxParam.msg = msg;
        msgBoxParam.btnText = btnText;
        msgBoxParam.callback = callback;
        MoeEventManager.Inst.SendEvent(EventID.Event_OnVersionMsgBox, msgBoxParam);
    }
}

MoeReleaseAssetBundleManager.cs 运行时资源管理器

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

publicclassMoeReleaseAssetBundleManager : IMoeResAgent{
conststring INDEX_FILE = "index";
privateDictionary<int, Object> _resources = new Dictionary<int, Object>();
privateDictionary<int, AssetBundle> _bundles = new Dictionary<int, AssetBundle>();
privateDictionary<int, string> _bundles_index = new Dictionary<int, string>();

private AssetBundleManifest _manifest = null;

public voidInit()    {
        InitAndLoadManifestFile();
        InitAndLoadIndexFile();
    }

private voidInitAndLoadIndexFile()    {
        _bundles_index.Clear();
        AssetBundle indexBundle = LoadBundleSync(INDEX_FILE);
        TextAsset ta = indexBundle.LoadAsset<TextAsset>(INDEX_FILE);
if (ta == null)
        {
            Debug.LogErrorFormat("Index 文件加载失败!");
return;
        }

string[] lines = ta.text.Split('\n');
char[] trim = newchar[] { '\r', '\n' };

if (lines != null && lines.Length > 0)
        {
for (int i = 0; i < lines.Length; ++i)
            {
string line = lines[i].Trim(trim);
if (string.IsNullOrEmpty(line))
                {
continue;
                }

string[] pair = line.Split(':');
if (pair.Length != 2)
                {
                    Debug.LogErrorFormat("Index 行数据有问题: {0}", line);
continue;
                }

int hash = pair[0].GetHashCode();
if (_bundles_index.ContainsKey(hash))
                {
                    Debug.LogErrorFormat("Index 文件中存在相同的路径: {0}", pair[0]);
                }
else                {
                    _bundles_index.Add(hash, pair[1]);
                }
            }
        }

if (_bundles_index.Count != 0)
        {
            Debug.LogFormat("Bundle Index 初始化完成");
        }
else        {
            Debug.LogErrorFormat("Index 文件数据为空");
        }

        indexBundle.Unload(true);
        indexBundle = null;
    }

private voidInitAndLoadManifestFile()    {
        AssetBundle manifestBundle = LoadBundleSync("Bundles");
        _manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        manifestBundle.Unload(false);
        manifestBundle = null;
    }

public T LoadAsset<T>(string path) where T : UnityEngine.Object    {
        UnityEngine.Object obj = Load(path);
if (obj != null)
        {
return obj as T;
        }

returnnull;
    }

public byte[] LoadLuaCode(string path)    {
string assetPath = string.Format("LuaScripts/{0}", path);
        TextAsset ta = LoadAsset<TextAsset>(assetPath);
if (ta != null)
        {
return ta.bytes;
        }
returnnull;
    }



private UnityEngine.Object Load(string assetPath)    {
if (string.IsNullOrEmpty(assetPath))
        {
returnnull;
        }

int pathHash = assetPath.GetHashCode();
        Object obj = null;
if (_resources.TryGetValue(pathHash, out obj))
        {
if (obj == null)
            {
                _resources.Remove(pathHash);
            }
else            {
return obj;
            }
        }

        AssetLoadInfo loadInfo = GetAssetLoadInfo(assetPath);
// 加载依赖Bundle
for (int i = 0; i < loadInfo.dependencies.Length; ++i)
        {
if (LoadBundleSync(loadInfo.dependencies[i]) == null)
            {
                Debug.LogErrorFormat("加载依赖Bundle出错,资源 {0}, 主Bundle:{1}, 依赖:{2}", assetPath, loadInfo.mainBundle, loadInfo.dependencies[i]);
returnnull;
            }
        }

        AssetBundle mainBundle = LoadBundleSync(loadInfo.mainBundle);
if (mainBundle == null)
        {
            Debug.LogErrorFormat("加载主Bundle出错,资源:{0},主Bundle:{1}", assetPath, loadInfo.mainBundle);
returnnull;
        }

        obj = mainBundle.LoadAsset(assetPath);

if (obj == null)
        {
            Debug.LogErrorFormat("从Bundle加载资源失败,资源:{0},主Bundle:{1}", assetPath, loadInfo.mainBundle);
returnnull;
        }

        _resources.Add(pathHash, obj);
return obj;
    }

private AssetBundle LoadBundleSync(string bundleName)    {
int bundleHash = bundleName.GetHashCode();
        AssetBundle bundle = null;

if (!_bundles.TryGetValue(bundleHash, out bundle))
        {
#if UNITY_EDITORstring rootPath = Application.dataPath + "/../streaming";
#elsestring rootPath = Application.persistentDataPath;
#endifstring bundleLoadPath = System.IO.Path.Combine(rootPath, string.Format("Bundles/{0}", bundleName));
            Debug.LogFormat(">>>> 加载Bundle: {0}", bundleLoadPath);

using (var fileStream = new AssetBundleStream(bundleLoadPath, FileMode.Open, FileAccess.Read, FileShare.None, 1024 * 4, false))
            {
                bundle = AssetBundle.LoadFromStream(fileStream);
            }

// bundle = AssetBundle.LoadFromFile(bundleLoadPath);
if (bundle != null)
            {
                _bundles.Add(bundleHash, bundle);
            }
else            {
                Debug.LogErrorFormat("Bundle 加载失败 {0}, LoadPath: {1}", bundleName, bundleLoadPath);
            }
        }
else        {
// Debug.LogFormat("Bundle {0} 已加载,直接返回", bundleName);        }

return bundle;
    }

private stringGetAssetOfBundleFileName(string assetPath)    {
int assetHash = assetPath.GetHashCode();
string bundleName;
if (_bundles_index.TryGetValue(assetHash, out bundleName))
        {
return bundleName;
        }

returnstring.Empty;
    }

private AssetLoadInfo GetAssetLoadInfo(string assetPath)    {
        AssetLoadInfo loadInfo = new AssetLoadInfo();
        loadInfo.assetPath = assetPath;
        loadInfo.mainBundle = GetAssetOfBundleFileName(assetPath);
        loadInfo.dependencies = _manifest.GetAllDependencies(loadInfo.mainBundle);
return loadInfo;
    }


privateclassAssetLoadInfo    {
publicstring assetPath;
publicstring mainBundle;
publicstring[] dependencies;
    }
}

Supongo que te gusta

Origin blog.csdn.net/s178435865/article/details/128604693
Recomendado
Clasificación