热更新
1.什么是热更新?
热更新,顾名思义就是就是让开发者,可以在不用发布新版本的下,对已经发布的游戏的内容进行更改。
打个比方,如果你发布的游戏出现了一个恶劣的bug需要紧急修复,但是一个游戏新版本发布到送审结束往往需要经历很长的时间,(尤其是国内这种烂环境),但是往往不可能等到那么久时间,所以热更新就出现了。
2.热更新的特点
快速迭代:开发者可以快速修复bug或添加新功能,而不必等待应用商店的审核流程。
用户体验:用户无需重新下载整个应用程序即可获得更新,这可以减少等待时间和数据使用。
即时更新:可以在发现问题时立即推送更新,减少问题对用户的影响。
3.热更新的技术
目前主流的热更新是Xlua,因为这个是腾讯开发出来的,很多企业想和腾讯沾点关系,所以都采用了这个,XLua需要学习Lua语言,也需要Lua虚拟机,成本比较高。
另一种就是支持使用C#进行热更新ILRuntime,不过需要适配ILRuntime和mono虚拟机,适配困难,在性能上也要比起今天讲的要差一些。
HybridCLR就是今天讲的热更新技术之一,在性能使用都要比前两者来的方便的多。
HybridCLR
官方介绍:HybridCLR | HybridCLR
安装教程:快速上手 | HybridCLR
由于对于上手和本地热更新的例子在官方已经有比较详细的安装和操作说明,所以我不在多做解释。
远程热更新
不过官方的上手例子是本地热更新的,没有远程热更新,而往往在这个过程中其实是要踩很多坑的。所以这里我就拿我目前学习到的知识,讲述远程热更新应该如何操作。
1.使用Addressable打包
首先开始前打开包管理器,然后安装Addressable插件,然后在windows[窗口]/Asset Management[资产管理]中看到Addressable这个选项了,选择Group[组]就会弹出一个窗口,创建一个默认组,你就对你的资源进行资产管理了
如果你学习过AB包,即Assets Bundle那么就很好理解,Addressable是Unity官方在Assets Bundle上的新版替代。没学习过也不要紧,这对于你接下来使用Addressable不够成影响。
Addressable中,你可以就将大部分资产(除了C#脚本不能直接打包,因为这些是C#是编译型语言,需要编译后才能运作,这也是早期热更新采用动态语言Lua的原因)拖拽或者在资产上面的小按钮addressable来将资产安排到组中。你也可以右键[Create New Group]添加属于自己的组,进行分类管理,或者添加标签,建议所有的资产都简化或者修改一下名字便于之后的调用(建议一定要添加一个所有资源都共享的标签,这对之后热更新文件有非常大的作用)。
然后打开上图的Manage Profiles,就会打开配置界面
第一个是本地配置,第二是远程配置路线,一般第一个不用管,而第二个远程Remote中在实际开发中Remote.BulidPath则需要修改为你实际上远程用于存放资源的位置,不过学习阶段你改为任意一个目录下加[BuildTarget],这个目录是之后资源打包后放置的路径,通常这个路径是和Assets同级的目录,完成打包操作后,右键Assets文件Show in Explorer[在资源管理器中查看]快速查看生成的目录。
对于远程第二个则是资源获取的路径,如果是正式的开发那么就是你的服务器地址也就是和Remote.BulidPath一样的路径,不过一般的学生党咋可能有自己的服务器?不用担心,Addressable帮你准备了一个
之后就会打开一个窗口,里面有各种关于本地服务器的设置,启动时需要点击Enable,但是我不推荐使用这个,因为管理很不方便而且对于后续的文件下载情况无法很好的观察。我推荐使用HFS来进行这个操作,你只需要把生成的资源文件拖拽到对应的目录下就可以轻松的使用了,只要把一眼丁真那句话替换为你实际开发过程中的IP地址与端口号。
这些准备工作完...别急,还有一样不要忘记了,点击每个组,把你需要的设置为远程连接的组改为你设置好的远程。不然默认所有组都是通过本地的。
这样所有前置工作都算完成了,之后只需要Addressable的界面中点击Build -> New Bulid -> Default Build Script即可完成资产的打包,然后,把你的资产文件也就是你设置的Remote.BulidPath对应的文件下,比如你写的是ServerData/[BuildTarget],那么就是[windows是StandaloneWindows64]整个文件拖到HFS中,这样就完成了资源远程的存放。
2.使用Addressable获取资产
当资产被添加到组后,在脚本中使用代码
Addressables.LoadAssetAsync<资产的类型>("资产的名称");
就可以获取到对应类型的资产,请注意,这是一个异步方法,本质是为了提高运行的效率和CPU的利用率,因此在实际使用时记得处理异步线程的等待关系,不然会导致资产还在获取中,但是你尝试调用就会出现空异常(踩过坑的教训)
该方法返回的是一个AsyncOperationHandle结构体(其实几乎大部分Addressable的静态方法返回的都是AsyncOperationHandle,所以你可以都用这个接收)
AsyncOperationHandle的有几个常用的参数
- IsDone: 一个布尔值,表示操作是否完成。
- OperationException: 如果操作失败,包含异常信息。
- PercentComplete: 操作的完成进度。
- Result: 操作的结果对象。
- Status: 操作的状态,类型为 AsyncOperationStatus。
- Task: 返回一个 Task 对象,用于异步等待。
我们可以通过IsDone或者Status判断这个异步操作获取资源的操作是否已经完成,然后进一步进行操作,当然还有更简单的方式就是
Addressables.LoadAssetAsync<Sprite>("A2").Completed += (obj) =>
{
GetComponent<SpriteRenderer>().sprite = obj.Result;
};
Completed就是操作执行完成后会调用的一系列事件,我们可以添加一个lambda表达式,让它获取资源成功后进行一些操作,比如这里的获取图像资源后替换当前的图像。不得不说Addressable相比与Assets Bundle的强大之处就在于对于资产它可以自动完成依赖关系的引用和加载,而Assets Bundle还需要编写一个工具类处理依赖关系
3.打包HybirdCLR热更新程序集
既然已经可以对各种资产进行实例化了,那么如何才能让不支持热更新的程序集被打包?
HybirdCLR在编译好热更新的程序集后,通常会在: 你的项目文件\HybridCLRData\HotUpdateDlls\StandaloneWindows64的目录下生成一个HotUpdate.dll文件,将它移动到你的热更新程序集所在文件,然后修改文件后缀名为dll.bytes,这样这个文件就可以被正常打包了。
4.获取并解析热更新程序集
完成对于热更新程序集的打包后,通过代码
Addressables.LoadAssetAsync<TextAsset>("HotUpdate.dll").Completed += (obj) =>
{
Assembly assembly = Assembly.Load(obj.Result.bytes);
};
就可以获取到程序集然后使用,之后会涉及到反射的知识,如果你不会反射请去学习反射。
利用反射,我们就可以解析出来内部的程序然后使用它。但是对于这里需要有很多要注意的地方,下例代码将进行说明
public class HotTest
{
public static string Run()
{
Debug.Log("HotUpdate Progress 0.01");
return "A2";
}
}
var TypeTask = new TaskCompletionSource<Type>();
Addressables.LoadAssetAsync<TextAsset>("HotUpdate.dll").Completed += (obj) =>
{
Assembly assembly = Assembly.Load(obj.Result.bytes);
TypeTask.SetResult(assembly.GetType("HotTest"));
};
Type types = await TypeTask.Task;
string ImgName = (string)types.GetMethod("Run").Invoke(null, null);
这段代码是获取热更新程序集并调用一个方法,获取返回到的图像名称。
TaskCompletionSource是用来协助等待异步线程结束的,因为Addressables.LoadAssetAsync是一个异步的方法,如果你贸然在Completed的lambda表达式就进行ImgName的赋值,由于加载程序集到反射调用都需要花费一定的时间,这样当你在外面下一步调用Debug.Log(ImgName);就会得到一个Null,而且你会在这句话后面看到输出的HotUpdate...这些内容。因此需要进行等待并将值返回到types成功赋值后,才继续执行后面的代码。
其它的知识点
1.加载场景
Addressable可以打包场景,然后通过代码获取调用来更新场景
Addressables.LoadSceneAsync("Main");
这段代码的作用和异步加载场景差不多,区别在于它是从Addressable包中获取的场景然后加载,这个方法其实有另一个第二个默认参数LoadSceneMode.Single,Single意味着关闭其它所有场景值加载这一个场景,Additive则是以添加的方式增加一个场景。
2.获取更新的状况
正常情况下,玩家进入游戏时,肯定是需要先判断是否有新的内容然后决定需不需要进行下载操作,所有我们需要实现一段功能用于判断热更新的内容是否有变化然后进行下载缓存到本地,这样下次进入游戏时就不需要再从服务器上直接下载了。
在进行操作之前,我们需要打开Addressable的设置
并勾选了Build Remote Catalog这个选项,这和之后的判断是否有更新有很大关系,然后你打包之后,就会多出两个文件
这两个文件就包含了包的各种信息内容。
IEnumerator LoadAsset()
{
AsyncOperationHandle<long> DownLoadRes = Addressables.GetDownloadSizeAsync("All");
yield return DownLoadRes;
Debug.Log($"File Size: {DownLoadRes.Result.ToString()}");
if (DownLoadRes.Status == AsyncOperationStatus.Succeeded)
{
if(DownLoadRes.Result <= 0)
{
Debug.Log("没有需要更新的内容");
StartGame();
}
else
{
Debug.Log("存在需要更新的内容");
StartCoroutine(DownLoadResFunc());
}
}
Addressables.Release(DownLoadRes);
}
使用协程的方式来进行,因为这个操作需要消耗一定的时间,当然异步也是可以的。
GetDownloadSizeAsync("All");方法的作用在于它会去下载我们之前设置好然后生成的Catalog文件,然后比对包含这个标签的所有文件,如果出现不一致的地方(比如多了一些文件或者少了,有的文件发生变化都可以判断出来,这也是为什么需要把所有的包都添加一个标签,以便于判断)就会把不同的信息(在这里返回出来就是不同内容的大小,如果没有差异,那么返回的就是0),之后我们就可以进行进一步的下载操作了。
StartCoroutine(DownLoadResFunc());就是执行下载的功能。
Release是用来释放资源的,当操作结束后,需要及时把不会用上的给释放掉避免占用内存,毕竟你已经判断好是否更新和做下载操作后就不会再用上(毕竟总不可能你游戏在进行时还能要更新吧?)所以需要释放这个资源。
IEnumerator DownLoadResFunc()
{
AsyncOperationHandle NewPack = Addressables.DownloadDependenciesAsync("All");
yield return NewPack;
if (NewPack.Status == AsyncOperationStatus.Succeeded) Addressables.Release(NewPack);
}
这段代码就是下载资源的,并且它只会下载发生变化的资源而不会所有的资源都进行下载。
完成以上操作后,你就可以下一段代码转到正式的场景然后开始游戏了。
3.制作一个窗口便于开发
每次进行程序的修改都要重新构建然后到对应文件下修改移动HotUpdate是一件很麻烦的事情,所以如果可以的话,我们应该尝试做个工具让程序自己完成把文件修改移植到对应的热更新程序集文件下。
using UnityEngine;
using UnityEditor;
using System.IO;
#if UNITY_EDITOR
public class AutoBuildDll : EditorWindow
{
private string hotUpdatePath = "/HotUpdate/Dll";
private string fileName = "HotUpdate.dll";
private readonly string Aps = Application.dataPath;
[MenuItem("HybridCLR/Auto Build DLL")] //添加一个菜单项到顶部导航栏
public static void ShowWindow()
{
//获取现有的开放窗口或者新建一个
GetWindow(typeof(AutoBuildDll), false, "Auto Build DLL");
}
private void OnGUI()
{
//使用成员变量显示设置选项
hotUpdatePath = EditorGUILayout.TextField("覆盖路径", hotUpdatePath);
fileName = EditorGUILayout.TextField("文件名称", fileName);
if (GUILayout.Button("执行文件复写"))
{
//执行覆盖操作
string readDllPath = Path.Combine(
Path.GetDirectoryName(Aps) ?? string.Empty,
"HybridCLRData/HotUpdateDlls/StandaloneWindows64",
fileName
);
string hotUpdateFullPath = Path.Combine(
hotUpdatePath,
fileName
);
hotUpdateFullPath = Aps + "/" + hotUpdateFullPath;
if (File.Exists(readDllPath))
{
File.Copy(readDllPath, Path.ChangeExtension(hotUpdateFullPath, ".dll.bytes"), true);
Debug.Log("覆盖dll.bytes文件完成!");
}
else Debug.LogError("DLL文件不存在: " + readDllPath);
}
}
}
#endif
之后只需要到导航栏就可以打开自己写好的窗口,再也不用手动替换那么麻烦了(需要注意的是最好把这个脚本放在Editor文件夹下避免打包的时候导致编码错误,即使你并不会打包包含这个的内容)
完整的代码和实现结果
以下这个教程中涉及到的知识的完整代码部分
using System;
using System.Collections;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System.Reflection;
public class LoadDll : MonoBehaviour
{
void Start() => StartCoroutine(LoadAsset());
IEnumerator LoadAsset()
{
AsyncOperationHandle<long> DownLoadRes = Addressables.GetDownloadSizeAsync("All");
yield return DownLoadRes;
Debug.Log($"File Size: {DownLoadRes.Result.ToString()}");
if (DownLoadRes.Status == AsyncOperationStatus.Succeeded)
{
if(DownLoadRes.Result <= 0)
{
Debug.Log("没有需要更新的内容");
StartGame();
}
else
{
Debug.Log("存在需要更新的内容");
StartCoroutine(DownLoadResFunc());
}
}
Addressables.Release(DownLoadRes);
}
IEnumerator DownLoadResFunc()
{
AsyncOperationHandle NewPack = Addressables.DownloadDependenciesAsync("All");
yield return NewPack;
if (NewPack.Status == AsyncOperationStatus.Succeeded) Addressables.Release(NewPack);
StartGame();
}
public async void StartGame()
{
var TypeTask = new TaskCompletionSource<Type>();
Addressables.LoadAssetAsync<TextAsset>("HotUpdate.dll").Completed += (obj) =>
{
Assembly assembly = Assembly.Load(obj.Result.bytes);
TypeTask.SetResult(assembly.GetType("HotTest"));
};
Type types = await TypeTask.Task;
string ImgName = (string)types.GetMethod("Run").Invoke(null, null);
var LoadSceneTask = Addressables.LoadSceneAsync("Main");
var LoadSprite = Addressables.LoadAssetAsync<Sprite>(ImgName);
await Task.WhenAll(LoadSceneTask.Task, LoadSprite.Task);
if (LoadSceneTask.Status == AsyncOperationStatus.Succeeded &&
LoadSprite.Status == AsyncOperationStatus.Succeeded)
{
var obj = new GameObject();
obj.AddComponent<SpriteRenderer>().sprite = LoadSprite.Result;
obj.transform.localScale = new Vector3(4, 4, 4);
}
}
}
这是一个测试热更新的程序,作用就是在进入游戏时先判断有没有更新,有就执行下载资源,否则就直接跳转到Main场景,然后通过读取热更新程序集中的代码获取我们展示的是哪个图片然后实例化一个对象展示这个图片。
我们来测试一下:这是所有打包的资源
我将两张新的图片加入到项目并打包,然后生成项目,此时的HotTest中对应的是返回值是D2,所以最后我们看到的是D2的图像
然后我修改了代码返回D1
然后使用HybirdCLR重新生成程序集,之后在用之前写的工具导航自动把程序集文件进行替换。需要说明的是,如果是图片的添加和删除可以直接打包,但是对于程序集你可能需要先清除一下然后重新打包,因为有可能因为缓存问题导致你的程序集还是原来的那个程序集导致热更新失败
然后你可以看到构建出来的项目再次打开后就是热更新之后的图片了,而我们并没有对构建出来的项目做任何修改。在HFS你也看到你的资源下载信息(这里Script被下载就说明发现了Script改变了然后就下载更新本地了)
至此,一段基础的热更新教程就结束了,之后你可以运用你的知识,尝试做出更对对于热更新的开发,并用于你的项目中。