Unity资源热更新框架

news2024/11/17 9:45:29

什么是热更新?

        游戏上线后,玩家下载第一个版本(1G左右或者更大),在之后运营的过程中,如果需要更换UI显示,或者修改游戏的逻辑,这个时候,如果不使用热更新,就需要重新打包,然后让玩家重新下载,很显然体验非常不好。 热更新可以在不重新下载客户端的情况下,更新游戏的内容。 如王者荣耀,经常有下载补丁的时候。

如何判断哪些文件需要更新?

        为了知道我们需要更新的内容,我们就要知道哪些文件发生了改变,或者新增文件?所以我们需要在本地保存一份 需要热更新文件信息(名称,大小、Md5值)的 配置文件。在添加或改变资源时打新的热更包时我们和这个配置文件进行比较,相同资源名称的Md5值不一致,或者在配置文件中找不到该资源配置,就说明这个资源是发生改变或新增的,需要被加进热更包中。

如何实现热更新?

        上面我们知道了哪些文件需要被热更新,那么我们需要把这些文件放到服务器上,并记录这次补丁包的信息(版本信息、第几次热更,以及这些资源的详细信息配置)。用户打开App后会去拉取这个配置文件,并找到最后一次热更的资源信息与本地的资源进行MD5校验,不通过的就加入到热更列表,下载后保存到本地上,下次进入游戏的时候MD5就校验成功不会在出现重新下载服务器资源的情况,至此我们大致的思路就完成了。

资源热更流程图

打包时记录版本信息及所有资源信息

  • 包名
  • 版本号
  • 资源MD5文件信息(name,文件md5,size)
  • 保存到本地(Xml文件或二进制文件)

一键生成热更资源

如何生成AB包,以及实现: 一键生成热更资源

热更包及配置文件

数据结构:

using System;
using System.Collections.Generic;
using System.Xml.Serialization;

namespace Hot
{
    [Serializable]
    public class GameVersion
    {
        [XmlElement]
        public ServerVersionInfo[] ServerInfo;
    }
    /// <summary>
    /// 当前游戏版本对应的所有补丁
    /// </summary>
    [Serializable]
    public class ServerVersionInfo
    {
        [XmlAttribute]
        public string Version;
        [XmlElement]
        public List<Patches> Patches = new List<Patches>();
    }
    /// <summary>
    /// 一个总补丁包信息
    /// </summary>
    [Serializable]
    public class Patches
    {
        [XmlAttribute]
        public int Version;        // 第几次热更
        [XmlAttribute] 
        public string Desc;
        [XmlElement]
        public List<Patch> patches = new List<Patch>();
    }

    /// <summary>
    /// 单个补丁包信息
    /// </summary>
    [Serializable]
    public class Patch
    {
        [XmlAttribute]
        public string Name;
        [XmlAttribute]
        public string Url;
        [XmlAttribute]
        public long Size;
        [XmlAttribute]
        public string MD5;
    }
}

服务器部署

Apache服务器搭建:

我这边使用Apache: Apache Download
下载后将期解压到需要放置的目录下

找到 Apache24/conf/httpd.conf  将 Define SRVROOT改成Apache的解压目录,端口号默认时80,如果被占用可以自行修改

Define SRVROOT "F:\WebServer/Apache24"

 运行 httpd.exe文件,测试可以在浏览器下访问 localhost 可以方位代表成功

服务器文件部署:

在  ...\Apache24\htdocs 文件夹下新建存放需要热更的AssetBundle的文件

 文件夹0.1: 版本文件夹,

文件夹 1: 第一次需要热更的资源

添加在服务器里添加GameVersion.xml文件:对应上面的 ServerInfo数据结构,每次有新的热更包时就往xml里添加 Patche.xml里的内容,需要回退的话只需要删除对应Patches的补丁配置

<?xml version="1.0"?>
<GameVersion>
    <ServerInfo Version="1.0.1">
        <Patches Version="1" Desc="测试热更">
            <patches Name="AssetBundle" Url="http://127.0.0.1/AssetBundle/1.0.1/1/AssetBundle" Size="1130" MD5="7baa969436d20f0e1b8a41e78d3cb23d" />
            <patches Name="audio" Url="http://127.0.0.1/AssetBundle/1.0.1/1/audio" Size="20555034" MD5="f4fc534e2615ca3a0c199e29a4f22df6" />
            <patches Name="image" Url="http://127.0.0.1/AssetBundle/1.0.1/1/image" Size="563082" MD5="1da1ee923a73386d0871f0287954ffa4" />
            <patches Name="material" Url="http://127.0.0.1/AssetBundle/1.0.1/1/material" Size="8010" MD5="08fc079c4a19f701c3e914e2d591f7a8" />
            <patches Name="prefab" Url="http://127.0.0.1/AssetBundle/1.0.1/1/prefab" Size="5034" MD5="90bd7fb6bee13fdabc642bb445dbaa98" />
        </Patches>
    </ServerInfo>
</GameVersion>

到这里热更新的准备都已完成,接下来就是实现热更流程

文件下载基类

using System;
using System.Collections;
using System.IO;

namespace Hot
{
    public abstract class DownloadItemBase
    {
        protected string url;
        public string Url => url;
        
        protected string fileName;
        public string FileName => fileName;
        
        protected string fileNameWithoutExt;
        public string FileNameWithoutExt => fileNameWithoutExt;

        protected string ext;
        public string Ext;

        protected string fullName;
        public string FullName => fullName;
        
        protected string fullNameWithoutExt;
        public string FullNameWithoutExt => fullNameWithoutExt;

        protected long size;
        public long Size => size;

        protected bool isLoading = false;
        public bool IsLoading = false;

        public DownloadItemBase(string savePath,string url,long size)
        {
            isLoading = false;
            this.url = url;
            fileNameWithoutExt = Path.GetFileNameWithoutExtension(url);
            ext = Path.GetExtension(url);
            fileName = Path.GetFileName(url);
            fullName = $"{savePath}/{fileName}";
            fullNameWithoutExt = $"{savePath}/{fileNameWithoutExt}";
            this.size = size;
        }
        
        public abstract void Destroy();
        
        public abstract IEnumerator StartDownload(Action<bool> callBack);
        
        public abstract float GetCurProgress();
    }
}

AB包文件下载类

using System;
using System.Collections;
using Core.Utlis;
using UnityEngine;
using UnityEngine.Networking;

namespace Hot
{
    public class ABDownloadItem:DownloadItemBase
    {
        private UnityWebRequest webRequest;
        public ABDownloadItem(string savePath, string url, long size) : base(savePath, url, size)
        {
        }
        
        public override void Destroy()
        {
            webRequest.Dispose();
        }
        
        public override IEnumerator StartDownload(Action<bool> callBack = null)
        {
            webRequest = UnityWebRequest.Get(Url);
            webRequest.timeout = 30;
            isLoading = true;
            yield return webRequest.SendWebRequest();
            if (webRequest.result == UnityWebRequest.Result.Success)
            {
                FileUtils.SaveFile(FullName, webRequest.downloadHandler.data);
                if(null != callBack) callBack(true);
            }
            else
            {
                Debug.LogError($"download {Url} fail err: {webRequest.error}");
                if(null != callBack) callBack(false);
            }
            isLoading = false;
        }
        
        public override float GetCurProgress()
        {
            return webRequest != null ? webRequest.downloadProgress : 0;
        }

    }
}

核心热更新管理类

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Build;
using Core.Base;
using Core.Utlis;
using UnityEngine;
using UnityEngine.Networking;

namespace Hot
{
    public class HotManager:Singleton<HotManager>
    {
        private string ServerGameVersionPath = $"{Application.persistentDataPath}/GameVersion.xml";
        private string version;
        private string packageName;
        private GameVersion gameVersion;
        private string hotDesc;
        public string HotDesc => hotDesc;
        
        // 服务器上需要热更的补丁包
        private List<Patch> serverHotPatches = new List<Patch>();
        private Dictionary<string,Patch> serverHotPatchDic = new Dictionary<string, Patch>();
        // 需要下载的补丁包
        private List<Patch> downLoadPatchs = new List<Patch>();
        // 下载完成的补丁包
        private List<Patch> alreadyPatchLists = new List<Patch>();
        private ABDownloadItem curDownloadItem;
        private int reloadCount = 0;
        private float hotAllSize = 0;
        public float HotAllSize => hotAllSize;

        private bool isLoading = false;
        public bool IsLoading => isLoading;
        // 加载完成回调
        private Action hotCompeleteHandler;
        // 加载失败回调
        private Action<List<Patch>> hotFailHandler;

        private MonoBehaviour corMono;
        public void Init(MonoBehaviour mono)
        {
            corMono = mono;
        }

        /// <summary>
        /// 检查版本是否需要更新
        /// </summary>
        /// <param name="callBack"></param>
        public void CheckVersionNeedHot(Action<bool> callBack)
        {
            VersionInfo versionInfo = XmlSerializerOpt.Deserialize<VersionInfo>(PathUtlis.LOCAL_VERSION_PATH);
            version = versionInfo.Version;
            packageName = versionInfo.PackageName;

            corMono.StartCoroutine(LoadServerGameVersion(() =>
            {
                // 判断是否需要热更
                GetServerPatches();
                CheckDownloadPatches();
                hotAllSize = serverHotPatches.Sum(x => x.Size);
                callBack(downLoadPatchs.Count > 0);
            }));
        }

        private IEnumerator LoadServerGameVersion(Action callBack)
        {
            UnityWebRequest webRequest = UnityWebRequest.Get("http://127.0.0.1/GameVersion.xml");
            webRequest.timeout = 30;
            yield return webRequest.SendWebRequest();

            if (webRequest.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"加载服务器游戏配置失败: {webRequest.error}");
            }
            else
            {
                Debug.Log(ServerGameVersionPath);
                if(File.Exists(ServerGameVersionPath)) File.Delete(ServerGameVersionPath);
                FileUtils.SaveFile(ServerGameVersionPath,webRequest.downloadHandler.data);
                gameVersion = XmlSerializerOpt.Deserialize<GameVersion>(ServerGameVersionPath);
            }

            callBack();
        }

        private void GetServerPatches()
        {
            if (gameVersion != null && gameVersion.ServerInfo != null)
            {
                for (int i = 0; i < gameVersion.ServerInfo.Length; i++)
                {
                    if (gameVersion.ServerInfo[i].Version == version)
                    {
                        List<Patches> patches = gameVersion.ServerInfo[i].Patches;
                        if (patches != null && patches.Count > 0)
                        {
                            serverHotPatches = patches[patches.Count - 1].patches;
                            hotDesc = patches[patches.Count - 1].Desc;
                        }
                        break;
                    }
                }
            }
        }

        // 检查需要去下载的补丁
        private void CheckDownloadPatches()
        {
            downLoadPatchs.Clear();
            for (int i = 0; i < serverHotPatches.Count; i++)
            {
                serverHotPatchDic.Add(serverHotPatches[i].Name, serverHotPatches[i]);
                AddDownloadPatch(serverHotPatches[i]);
            }
        }

        private void AddDownloadPatch(Patch patch)
        {
            string savePath = $"{PathUtlis.LocalAssetBundlePath}/{patch.Name}";
            if (!File.Exists(savePath))
            {
                downLoadPatchs.Add(patch);
            }
            else
            {
                if (patch.MD5 != MD5Utils.GenerateMD5(savePath))
                {
                    downLoadPatchs.Add(patch);
                }
            }
        }

        public void StartHot(Action hotCompeleteHandler,Action<List<Patch>> hotFailHandler)
        {
            this.hotCompeleteHandler = hotCompeleteHandler;
            this.hotFailHandler = hotFailHandler;
            corMono.StartCoroutine(StartLoad());
        }

        private IEnumerator StartLoad(List<Patch> patches = null)
        {
            if (patches == null)
            {
                patches = downLoadPatchs;
            }

            if (!Directory.Exists(PathUtlis.LocalAssetBundlePath))
                Directory.CreateDirectory(PathUtlis.LocalAssetBundlePath);
            
            List<ABDownloadItem> downloadItems = new List<ABDownloadItem>();
            for (int i = 0; i < patches.Count; i++)
            {
                downloadItems.Add(new ABDownloadItem(PathUtlis.LocalAssetBundlePath,patches[i].Url,patches[i].Size));
            }

            isLoading = true;
            for (int i = 0; i < downloadItems.Count; i++)
            {
                ABDownloadItem item = downloadItems[i];
                curDownloadItem = item;
                yield return corMono.StartCoroutine(item.StartDownload((success) =>
                {
                    if (success)
                    {
                        Patch patch = FindPatch(item.FileName);
                        if (patch != null)
                        {
                            if(!alreadyPatchLists.Contains(patch)) alreadyPatchLists.Add(patch);
                        }
                    }
                    else
                    {
                        Debug.LogError($"{item.FileName} 下载失败,尝试重新下载");
                    }
                    item.Destroy();
                }));
            }

            // 重新比较文件md5,避免文件下载失败
            yield return VerifyMD5(downLoadPatchs);
        }

        //校验下载后的文件
        private IEnumerator VerifyMD5(List<Patch> patches)
        {
            List<Patch> downPatchList = new List<Patch>();
            for (int i = 0; i < patches.Count; i++)
            {
                Patch patch = patches[i];
                string savePath = $"{PathUtlis.LocalAssetBundlePath}/{patch.Name}";
                if (!File.Exists(savePath))
                {
                    downPatchList.Add(patch);
                }
                else
                {
                    if (patch.MD5 != MD5Utils.GenerateMD5(savePath))
                    {
                        downPatchList.Add(patch);
                    }
                }
            }
            
            if (downPatchList.Count > 0)
            {
                reloadCount++;
                if (reloadCount < 5)
                {
                    yield return corMono.StartCoroutine(StartLoad(downPatchList));
                }
                else
                {
                    isLoading = false;
                    if (null != hotFailHandler) hotFailHandler(downPatchList);
                }
            }
            else
            {
                isLoading = false;
                if (null != hotCompeleteHandler) hotCompeleteHandler();
            }
        }

        private Patch FindPatch(string name)
        {
            Patch patch = null;
            serverHotPatchDic.TryGetValue(name, out patch);
            return patch;
        }

        public float GetProgress()
        {
            float loadedSize = alreadyPatchLists.Sum(x => x.Size);
            float curloadSize = curDownloadItem.GetCurProgress() * curDownloadItem.Size;
            float progress = (loadedSize + curloadSize) / hotAllSize;
            return progress;
        }
    }
}

UI测试效果图

还有个问题,这样下载下来的资源会直接被别人拿走使用,为了数据的安全,我们可以对资源进行加密处理,我使用的是AES,也没有什么难点,就是在一键生成AB包后使用AES对文件加密,然后加载资源的时候使用 字节数组加载,LoadFromMemory的缺点就是多占一份没存,对于没存吃紧的就不适合用了,或者参考:Unity3D加密Assetbundle(不占内存)

    private void DecryptAssetBundle()
    {
        string abPath = Path.Combine(PathUtlis.AssetBundlePath, path);
        // 解密被加载的AB包
        byte[] result = AESUtils.AESFileDecryptToByte(abPath,"ENCRYPT_KEY");
        if (result == null)
        {
            Debug.LogError($"AES Decrypt {abPath} file fail");  
            return;
        }
        AssetBundle asset = AssetBundle.LoadFromMemory(result);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/398746.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VS2008生产动态库、静态库调用案例

静态库创建静态库打开VS2008创建项目如下&#xff1a;选择静态库创建文件// add.h #ifndef _ADD_H_ #define _ADD_H_#include <stdio.h>#ifdef __cplusplus extern "C" { #endif__declspec(dllexport) int myAdd(int a, int b);#ifdef __cplusplus } #endif#e…

Linux:IO库函数

目录标准库IO函数一、fopen二、fwrite三、fread四、fseek五、fclose在编写程序时&#xff0c;离不开IO操作&#xff0c;最常见的IO操作就是用printf函数进行打印&#xff0c;本文主要介绍的是封装后的IO库函数。 标准库IO函数 常使用的IO库函数如下&#xff1a; 函数作用fop…

u盘系统文件删除后的五种恢复方法

U盘是我们日常生活中使用较为普遍的移动存储设备&#xff0c;由于其便携性和易用性广受人们的欢迎。然而&#xff0c;在我们使用U盘的过程中&#xff0c;经常会出现误删文件的情况&#xff0c;例如本来要作为启动盘的u盘&#xff0c;误删里面的系统文件怎么办&#xff1f;当U盘…

MySQL的同步数据Replication功能

MySQL提供了Replication功能&#xff0c;可以实现将一个数据库的数据同步到多台其他数据库。前者通常称之为主库&#xff08;master&#xff09;&#xff0c;后者则被称从库&#xff08;slave&#xff09;。MySQL复制过程采用异步方式&#xff0c;但延时非常小&#xff0c;秒级…

C51---超声波测距

1.主要实现功能&#xff1a;当手靠近传感器时&#xff0c;灯亮&#xff1b;手离开&#xff0c;灯灭 2.器件&#xff1a;51单片机、HC-SR04超声波测距传感器 3.代码&#xff1a; #include "reg52.h" //距离小于10cm&#xff0c;D5亮&#xff0c;D6灭&#xff0c;反之…

Lombok使用@Builder无法build父类属性

文章目录问题描述解决方案使用示例lombok Builder注解和build父类属性问题1、简介2.使用3、Builder注解对类做了什么&#xff1f;问题描述 实体类使用Lombok的Builder来实现Builder模式&#xff0c;但是如果使用了extend继承&#xff0c;则子类无法通过Builder来Build父类属性…

深度学习应用技巧4-模型融合:投票法、加权平均法、集成模型法

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下&#xff0c;深度学习中的模型融合。它是将多个深度学习模型或其预测结果结合起来&#xff0c;以提高模型整体性能的一种技术。 深度学习中的模型融合技术&#xff0c;也叫做集成学习&#xff0c;是指同时使用多个…

CentOS7.5(1804)安装vsftpd(ftp)

1.准备安装包 1. vsftpd-3.0.2-29.el7_9.x86_64.rpm 2. ftp-0.17-67.el7.x86_64.rpm 可以自行下载,也可从我的博客中下载,下载传送门点 这里 2.安装vsftpd #1. 上传文件到服务器上,比如/home目录 #2. 执行以下命令安装 rpm -ivh vsftpd-3.0.2-29.el7_9.x86_64.rpm #3. 启动vsf…

极限的无穷小和无穷大

目录 无穷小&#xff1a; 无穷大&#xff1a; 无穷小&#xff1a; 举几个无穷小量的例子&#xff1a; 以0为极限的意思就是无穷小。 注&#xff1a;无穷小是变量&#xff0c;不能把很小很小的数混为一谈。 2&#xff1a;0是可以作为无穷小的唯一的一个数。 我们进行证明&…

完整教程:使用Spring Boot实现大文件断点续传及文件校验

一、简介 随着互联网的快速发展&#xff0c;大文件的传输成为了互联网应用的重要组成部分。然而&#xff0c;由于网络不稳定等因素的影响&#xff0c;大文件的传输经常会出现中断的情况&#xff0c;这时需要重新传输&#xff0c;导致传输效率低下。 为了解决这个问题&#xff…

【敏捷开发】jenkins「CI持续集成 CD持续部署」

文章目录前言一、安装jenkins1. 部署中的痛点2. 什么是jenkins3. jenkins的安装和配置&#xff08;1&#xff09;下载&#xff08;2&#xff09;安装二、上传到运行服务器1. jenkins构建服务器流程2. 安装jenkins常用插件3. 通过freestyle构建项目4. 将构建服务器上的代码上传到…

ASA材料3D打印服务 抗紫外线材料3D打印服务 抗紫外线模型制作-CASAIM中科院广州电子

3D打印技术又称增材制造&#xff0c;通常是采用数字技术材料打印机来实现的&#xff0c;常在模具制造、工业设计等领域被用于制造模型&#xff0c;后逐渐用于一些产品的直接制造。随着 3D 打印逐渐成为主流生产流程的一部分&#xff0c;ASA抗紫外线材料应运而生。中科院广州电子…

Ubuntu 搭建NextCloud私有云盘【内网穿透远程访问】

文章目录1.前言2.本地软件安装2.1 nextcloud安装2.2 cpolar安装3.本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置4.公网访问测试5. 结语1.前言 对于爱好折腾的电脑爱好者来说&#xff0c;Linux是绕不开的、必须认识的系统&#xff08;大部分服务器都是采用Linux操作系统&a…

华为OD机试题,用 Java 解【数组二叉树】问题

华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典使用说明 参加华为od机试,一定要注意不…

将生成的NYUv2边界GT加载到dataloader中并进行训练

由上一篇我们可以知道&#xff0c;我们生成了一个label_img文件夹&#xff0c;里面存放的是索引对应图片的filename&#xff0c;每个filename里面存放的是GT的40个通道的边缘GT。train里面是这样&#xff0c;test里面也是这样。 加载数据我们要到train文件的dataloader中&…

Azure AD 与 AWS 单一帐户SSO访问集成,超详细讲解,包括解决可能出现的错误问题

本教程介绍如何将 AWS Single-Account Access 与 Azure Active Directory (Azure AD) 相集成。 将 AWS Single-Account Access 与 Azure AD 集成后&#xff0c;可以&#xff1a; 在 Azure AD 中控制谁有权访问 AWS Single-Account Access。让用户使用其 Azure AD 帐户自动登录…

SwiftUI 常用组件和属性(SwiftUI初学笔记)

本文为初学SwiftUI笔记。记录SwiftUI常用的组件和属性。 组件 共有属性(View的属性) Image("toRight").resizable().background(.red) // 背景色.shadow(color: .black, radius: 2, x: 9, y: 15) //阴影.frame(width: 30, height: 30) // 宽高 可以只设置宽或者高.…

2023年上半年软考中/高级一起报名考试+备考学习

软考是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资格考试。 系统集成…

Springboot——自定义Filter使用测试总结

文章目录前言自定义过滤器并验证关于排除某些请求的方式创建测试接口请求测试验证异常过滤器的执行流程注意事项资料参考前言 在Java-web的开发领域&#xff0c;对于过滤器和拦截器用处还是很多&#xff0c;但两者的概念却极易混淆。 过滤器和拦截器都是采用AOP的核心思想&am…

【微服务】—— 初识微服务

文章目录1. 什么是微服务1.1 微服务的特性自主专用性1.2 微服务的优势敏捷性灵活扩展轻松部署技术自由可重复使用的代码弹性2. 微服务技术栈3. 微服务架构演进3.1 单体架构3.2 分布式架构服务治理3.3 微服务微服务结构微服务技术对比企业需求1. 什么是微服务 微服务是一种开发软…