Unity的热更工程

有关Unity的热更工程

一. 什么是游戏热更

https://www.zhihu.com/question/29575814
https://blog.csdn.net/enweitech/article/details/84643308
https://www.jianshu.com/p/5e4699a942ea(重要)
https://blog.csdn.net/penchaoo/article/details/53648911

1. 什么是热更新?

  1. 举例来说

    游戏上线后,玩家下载第一个版本(70M左右或者更大),在运营的过程中,如果需要更换UI显示,或者修改游戏的逻辑,这个时候,如果不使用热更新,就需要重新打包,然后让玩家重新下载(浪费流量和时间,体验不好)。

    热更新可以在不重新下载客户端的情况下,更新游戏的内容。

    热更新一般应用在手机网游上。

  2. 为什么C#脚本不可以直接更新?

    C#是一门编程语言,它运行之前需要进行编译,而这个编译的过程在移动平台无法完成,所以当我们游戏的逻辑更改,C#代码发生改变的时候,我们就需要重新在开发环境下编译,然后重新打包,然后让玩家去下载更新最新的版本。

    这个体验差:包下载需要的时间长,而且很多资源没有更新,也需要重新下载,浪费流量。

  3. 热更新有哪些实现方式?
    1.使用Lua脚本编写游戏的UI或者其他的逻辑

     Lua是一个精悍小巧的脚本语言,可以跨平台运行解析,而且不需要编译的过程
    

    2.使用C#Light
    3.使用C#反射技术

  4. 什么是AssetBundle?

    Unity提供了一个资源更新技术,就是通过AssetBundle,我们可以通过AssetBundle更新游戏UI,也可以把脚本或者其他代码当成资源打包成AssetBundle然后更新到客户端。
    在所有的热更新技术中都需要AssetBundle

    关于游戏打包(AB包)与SDK
    https://www.jianshu.com/p/e86e106304d3
    https://www.zhihu.com/question/28609209
    https://blog.csdn.net/mango9126/article/details/74908794

2. C#为什么不能热更新

https://www.cnblogs.com/timeObjserver/p/8647383.html

准确的说,C#在安卓上可以实现热更新,但在苹果上却不能。
在安卓上可以通过C#的语言特性-反射机制实现动态代码加载从而实现热更新。

具体做法是:将需要频繁更改的逻辑部分独立出来做成DLL,在主模块调用这些DLL,主模块代码是不修改的,只有作为业务(逻辑)模块的DLL部分需要修改。游戏运行时通过反射机制加载这些DLL就实现了热更新。

但苹果对反射机制有限制,不能实现这样的热更。为什么限制反射机制?安全起见,不能给程序太强的能力,因为反制机制实在太过强大,会给系统带来安全隐患。

3. 大版本更新与热更

https://blog.csdn.net/qq_14914623/article/details/81112719

游戏上线后,遇见bug或者需要更新内容(包括资源,玩法,数值调整,游戏脚本等)的时候,一般有2种做法。第一种,发个新包,然后让玩家下载新的版本;第二种,在游戏内更新,游戏启动时去下载需要更新的资源。第一种我们一般称为游戏大版本更新,第二种称为热更新。

一般来说,不能通过小版本更新解决的问题,才会用到大版本更新,例如,不可热更的游戏代码。而热更新是建立在每次大版本更新的基础上进行的更新,因此,这2种更新方式应该互相承接。

我们可以通过MD5文件和更新文件以及版本号来实现大版本更新和热更新。游戏的版本号由大版本号+资源版本号构成,每一次大版本更新,大版本号增加,资源版本号重置。每一次热更新,大版本号不变,资源版本号增加。

MD5文件(MD5File)记录每一个资源的MD5值。 例如:

a.txt,1630d23f45464df6071a9948dd1592bf
b.texture,f9c985a8f2a86292a024c4ed21ed33fb

版本文件(VersionFile)记录每一个更新文件的资源版本号及新的MD5值,资源版本号对应玩家去服务器上哪个版本库(路径)里下载资源,MD5值用于服务器的资源和本地资源是否一样,避免重复下载。 例如:

 a.txt,1630d23f45464df6071a9948dd1592bf,0.1
 c.txt,2312xd23f45464df6071a9948dd1592b,0.2

大版本更新流程

  1. 清除之前的Md5文件和版本文件
  2. 打包所有的资源
  3. 计算每个资源的MD5值,创建新的Md5文件,将所有资源的资源名称和对应的MD5值保存在MD5文件中。

热更新流程

  1. 打包所有资源
  2. 计算每个资源的MD5值,并和MD5文件中记录的MD5值做比较,将MD5值发生变化的资源和被删除的资源记录下来.
  3. 判断是否存在版本文件: 如果不存在版本文件,即现在是在大版本后的第一次热更新,创建一个版本文件,将所有MD5值发生变化的资源的名称和其版本号记录在版本文件中,格式为为【文件名,新的MD5值,当前资源版本号】。 如果已经存在版本文件,即现在是在上一次热更新之后继续热更新。首先读取上一次的版本文件,然后遍历本次MD5值发生变化的资源,如果资源名称在版本文件中存在,则将版本文件中该资源版本号置为当前版本;如果资源名称在版本文件中不存在,则在版本文件中添加新的记录,存放其资源名称和当前资源版本号;如果版本文件中存在被删除的资源名称,将该资源的记录从版本文件中移除。
  4. 上传需要更新的资源和版本文件(VersionFile)到服务器
  5. 删除原来的MD5文件,将所有资源的MD5值保存在新的MD5文件中。

玩家进入游戏后,首先判断是否需要更新版本,如果是大版本更新,则提示需要重新下载安装包;如果是小版本更新,则先下载版本文件(VersionFile),然后根据版本文件中的资源名和版本号去对应的地址下载资源。

服务器下载目录结构

0.1(资源版本号文件夹)
 
-------a.txt
-------VersionFile.txt
 
0.2(资源版本号文件夹)
 
-------a.txt
-------c.txt
-------VersionFile.txt
 
0.3(资源版本号文件夹)
 
-------a.txt
-------d.txt
-------VersionFile.txt

假设VersionFile.txt内容为:

a.txt,1630d23f45464df6071a9948dd1592bf,0.3
 
c.txt,d23f45464df6071a9948dd1592bfw2sb,0.2
 
d.txt,30d23f45464df6071a9948dd1592bfx2,0.3

如果玩家的版本是0.1,则玩家需要去0.3的目录下下载a.txt和d.txt,去0.2的目录下下载c.txt.。

如果玩家的版本是0.2,则玩家只需要去0.3的目录下下载a.txt和d.txt。

通常通过上次下载的版本文件与这次版本文件做对比,生成需要下载的文件列表,然后去服务器下载即可。
在这里插入图片描述

4. 解压包

https://blog.csdn.net/m0_37920739/article/details/89215693

大家都知道大部分游戏打开的时候都是需要解压游戏资源(这个过程不消耗流量),比如王者荣耀,阴阳师这些游戏。其实这个就是Application.streamingAssetsPath的资源转移到沙盒路径上(Application.persistentDataPath),为什么需要转移资源呢?因为Application.streamingAssetsPath这个路径只支持读取,不支持写入,但是沙盒路径是可读可写的,所以需要将游戏资源转移到沙盒路径,然后资源有变换的话,就可以随便的替换。

首次启动游戏先把汇总文件迁移过去,然后生成汇总文件的秘钥,如果汇总文件版本一致就不需要单个检查了,如果对不上就到服务器下载新的汇总文件,然后去一个一个的比对文件秘钥。检查热更新的时候,如果优先级最高的路径有资源,这样此路径资源MD5和服务器的MD5比对,如果优先级最高的路径没有这个资源,就使用备用资源路径的MD5值,注意事项(沙盒路径是可以使用File,Directory这些API的,但是Application.streamingAssetsPath这个路径是不允许使用File,Directory这些API的,因为Application.streamingAssetsPath不是一个绝对路径),之后如果资源发现更新的,就下载到沙盒目录。资源加载的使用先去判断沙盒是否存在资源(File.Exists()),不存在的话就去加载备用路径的资源,这样首次启动游戏就不需要解压游戏资源了。

二. Unity3D 热更新方案

https://blog.csdn.net/guofeng526/article/details/52662994
https://blog.csdn.net/u010019717/article/details/50853207

二. 使用ILRunTime进行热更

在这里插入图片描述

在这里插入图片描述

参考ILRuntime:

https://github.com/Ourpalm/ILRuntime

文档:

https://ourpalm.github.io/ILRuntime/public/v1/guide/index.html

其他参考文章:

https://www.cnblogs.com/zblade/p/9041400.html
https://blog.csdn.net/linshuhe1/article/details/101539307?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase

1. 实际上是基于Unity dll的热更

https://blog.csdn.net/baidu_28955655/article/details/52661698(原理)
https://blog.csdn.net/pdw_jsp/article/details/50698729(框架)
https://www.bbsmax.com/A/KE5QVq9jdL/(实现流程)
https://www.cnblogs.com/zblade/p/9089105.html(系列文章)
https://blog.csdn.net/qq_37310110/article/details/89514056

更本质就是用到反射

AssetBundle assetBundle = AssetBundle.LoadFromFile(path);//加载AssetBundle
TextAsset textAsset = assetBundle.LoadAsset<TextAsset>("A");//加载AssetBundle中的A
Assembly assembly = Assembly.Load(textAsset.bytes);//加载托管程序集
Type item = assembly.GetType("A.Class1");//获取程序集指定类
Component comparer = gameObject.AddComponent(item);//添加到游戏对象上
FieldInfo fieldInfo = comparer.GetType().GetField("text");//使用反射获取实例的字段
fieldInfo.SetValue(comparer, text);//给字段赋值
//先把DLL以TextAsset类型取出来,在把bytes给Assembly.Load方法读取准备进入反射操作
Assembly aly = System.Reflection.Assembly.Load(((TextAsset)www.assetBundle.mainAsset).bytes);

//获取DLL下全部的类型
foreach (var i in aly.GetTypes())
{
	//调试代码
	Debug.Log(i.Name);
	str += "\r\n" + i.Name;
	
	//添加组件到当前GameObject下面
	Component c = this.gameObject.AddComponent(i);
	
	//然后类名是MyClass,就把文本引用赋值给MyClass.platefaceText属性.
	if (i.Name == "MyClass") 
	{
	    FieldInfo info = c.GetType().GetField("platefaceText");
	    info.SetValue(c, myAgeText);
	}
}

2. 使用Python搭建服务器

  1. 下载安装Python,设置环境变量Path:在这里插入图片描述
  2. 创建一个文件夹作为服务器,然后在这个文件夹的最外层目录下打开git(目录结构:D:\Data\mg_hotfix\pc,应该在Data目录打开git),然后python -m SimpleHttpServer 80(2.7)在这里插入图片描述
    或python3.9:python -m http.server 80在这里插入图片描述

开启服务器。
3. 在这里插入图片描述

3. 热更工程如何与项目工程对接

热更工程在Rider右上角点击在这里插入图片描述
后会在热更工程目录\Hotfix~\MGHotfix\bin\Editor包下产生dll文件和pbd文件在这里插入图片描述
将这两个文件拷贝到刚才创建的服务器目录中(如一中的图)。

然后在unity中的MainCamera中MGMainCameraMono中找到LoginSetting,然后在登入的服务器的设置中写上自己本地环回地址:
在这里插入图片描述

4. 示例代码中的问题解决

https://blog.csdn.net/a13631290405/article/details/81192068

根据教程从零开始遇到的第一个问题,“HotFix_Project是热更DLL工程,请用VS2015之类的C# IDE打开和进行编译,在编译前请确保至少打开过一次Unity的主工程,如果编译依然说找不到UnityEngine等dll,请手动重新指认一下”。我的热更DLL工程生成解决方案报错,找不到引用的UnityEngine的dll,所以需要重新指定。找到HotFix_Project工程文件夹下面,找到HotFix_Project.csproj打开编辑。
在这里插入图片描述
第一第二个红框就是生成之后热更dll的目录,后面的红框就是引用的dll,换成自己unity所在的dll目录就可以。改了之后就发现可以编译成功了。

第二个问题就是使用委托的问题。在热更里面,如果要把热更定义的委托给U3D那边使用,是需要在U3D那边先注册的,什么原因可以看下委托那边的教程,例如我在热更里面用了List.Sort(A)这个排序方法,A是我自己写的排序方法,这个时候其实是调用了U3D那边的,我A方法这个是个委托来的,需要在U3D那边注册,这里我一直弄不懂怎么注册,因为在教程里面只写了float,int,bool这些基础类型的注册,如果我委托的参数是自己定义的类,应该怎么注册呢?后面还是在QQ群里面寻找到了帮助,如果委托参数是继承MonoBehaviour的,例如我热更里面用到的排序方法

public int SortList(UI_BagItem p, UI_BagItem q)
{
return q.quality.CompareTo(p.quality);
}

UI_BagItem是继承MonoBehaviour的,所以需要在U3D工程那边这样注册:

appdomain.DelegateManager.RegisterFunctionDelegate<MonoBehaviourAdapter.Adaptor, MonoBehaviourAdapter.Adaptor, System.Int32>();// 最后一个类型是返回类型,具体可以先看下委托那个教程。

appdomain.DelegateManager.RegisterDelegateConvertor<System.Comparison<MonoBehaviourAdapter.Adaptor>>((act) =>
{
return new System.Comparison<MonoBehaviourAdapter.Adaptor>((x, y) =>
{
return ((System.Func<MonoBehaviourAdapter.Adaptor, MonoBehaviourAdapter.Adaptor, System.Int32>)act)(x, y);
});
});

这些我都是在Demo工程上进行的。MonoBehaviourAdapter.Adaptor这个类型也是作者写好的。加上这两句后,就算注册成功了。

如果这个参数不是MonoBehaviour而是自己定义的类应该如果注册呢,例如我在热更里面的排序方法是这样:

public int SortList2(TestSort p, TestSort q) {
return p.t1.CompareTo(q.t2);
}

TestSort是自己定义的类,什么都没继承的,这个时候把MonoBehaviourAdapter.Adaptor 换成ILTypeInstance就可以了。

5. 热更文件在手机中存放的位置

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三. Lua实现热更

https://www.cnblogs.com/kerven/p/8118223.html
https://blog.csdn.net/qq_35030499/article/details/83420902
https://www.cnblogs.com/muyuqianshan/p/6937096.html

四. InjectFix

https://blog.csdn.net/hnzmdlhc/article/details/102984177
https://github.com/Tencent/InjectFix/blob/master/Doc/quick_start.md

1.InjectFix文件在手机中存放位置

在这里插入图片描述
在这里插入图片描述

五. 手机连接unity看debug

adb forward tcp:34999 localabstract:Unity-com.mechanist.gok
在这里插入图片描述

六.ILRunTime笔记

ILRuntime官方Demo笔记
ILRuntime入门笔记

七.一些问题

使用ILRuntime遇到的一些问题

1. 热更层中使用到框架层的一些event描述的delegate,那么因为当前自动绑定生成工具不支持生成add_xxx, remove_xxx的绑定,所以需要手动修改工具。

在这里插入图片描述
MethodBindingGenerator.GenerateMethodWraperCode
在这里插入图片描述
在这里插入图片描述

2.如果需要用到GetComponent/s,AddComponent/s系列簇函数处理热更的mono,那么一定要实现相关的重定向

自动生成的GameObject/Transform的绑定文件对于GetComponent/s, AddComponent/s系列簇函数的重定向实现存在问题,都是当作框架层的mono去处理。所以需要参考作者的GetComponent, AddComponent去实现对于热更mono的特殊处理。
在这里插入图片描述
但是其实,这里重定向实现很复杂,而一般情况下,我们很少使用热更的mono(有的项目组直接禁止使用热更mono),这种情况下,我们可以使用自动生成的getcomponent的重定向(在UnityEngine_Gameobject_Binding.cs中)去处理框架层的mono,只在查找/添加热更mono的时候才去调用这个手写的重定向。最好是给处理热更mono的这个接口改个名字,比如GetComponentCustom

3.强转Action委托类型as报错,使用(Action)XXX的方式去转换

4.GetComponent无法获取基类组件类型,无法找到时,需要在MonoBehaviorAdapter中添加遍历获取基类逻辑

5. AddComponent需要限定好类型,不能用Type类型,会报错

6.ref out关键字谨慎使用,比如传递ref/out 静态变量时,会报错

7.重写父类方法时,不能使用virtual关键字,否则会执行两次,要使用override

8.Awake方法在AddComponent组件时调用一次,不论节点是否显示。此时需要在MonoBehaviorAdapter中加一层判断来规避此情况。

9.ILRuntime不支持proto2,需要换成proto3,同时由于ILRuntime不认为ILRT里面跑的数据类型是枚举,导致无法把默认值转换为枚举,不能使用枚举(比如消息类型)

10.打包时,主工程未用到但是热更工程用到的dll未被打包,添加link.xml文件,内容为

<linker>
<!-- The following assemblies contain namespaces that should be fully preserved
even when assembly stripping is used. Not excluding the assemblies below from
stripping can result in crashes or various exceptions.

<assembly fullname="Vuforia.UnityExtensions">
<namespace fullname="Vuforia" preserve="all"/>
</assembly>
-->

<assembly fullname="System">
<namespace fullname="System.Runtime.InteropServices" preserve="all"/>
<namespace fullname="System.Collections;" preserve="all"/>
<namespace fullname="System.Collections.Generic;" preserve="all"/>
<namespace fullname="System.Collections.IEnumerator;" preserve="all"/>
</assembly>

<assembly fullname="UnityEngine">
<type fullname="UnityEngine.Collider" preserve="all"/>
<type fullname="UnityEngine.SphereCollider" preserve="all"/>
<type fullname="UnityEngine.AudioSources" preserve="all"/>
</assembly>

<!-- XMLSerilizer-->
<assembly fullname="System.Xml">
<namespace fullname="System.Xml" preserve="all"/>
</assembly>

<!--JSONSerilizer-->

<assembly fullname="Newtonsoft.Json">
<namespace fullname="Newtonsoft.Json" preserve="all"/>
</assembly>
<assembly fullname="UnityEngine" preserve="all"/>
<assembly fullname="Assembly-CSharp" preserve="all"/>
</linker>

11.

ILRunTime热更新使用细节汇总

1.注意:虚函数是可以调用base.xxx的,但是要通过主工程的Adapter处理:
在这里插入图片描述

12. 在for循环遍历中尽量不要使用全局变量

ILRuntime使用全局变量:UnitTest_Performance50万 Elapsed time:1050ms, result = 445698416 ,Tick:10527445

ILRuntime使用局部变量:UnitTest_Performance50万 Elapsed time:534ms, result = 445698416 ,Tick:5436060
上面局部变量和全局变量的耗时差别很大,是因为局部变量是存储在栈中,而全局变量是存储在堆中,一定要注意在进行遍历一个列表时,在方法内先使用local xx = 全局变量,这样对于性能的提升是非常有效的

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值