游戏项目中的热更新机制

🗨️字数统计=2.3k字 ⏳阅读时长≈8分钟

项目中的热更新机制

热更新技术是指可以在不关闭游戏客户端的情况下,动态的更新游戏本身的资源。类似的服务器硬盘的“热插拔”。

目前市面上的绝大部分游戏客户端都启用了热更新技术,热更新的好处是玩家不必重新下载完整的安装包也能体验到官方的新版本资料片,减少了用户流失。

项目中安卓系统与苹果系统在热更新的机制上有一个明显的差异:苹果系统不能热更C#代码。

同样都可以进行热更新的是:AssetBundle(prefab预制体和UI界面图集等)、Table(文本,项目中的excel表格转成的二进制文件)、Lua(Lua脚本语言)、Mapfile(场景地图文件,关卡编辑器生成,项目中独有的)。

通过校验本地文件的MD5与拉取到的MD5是否一样来进行热更。

代码热更新实现方式:

1、使用Lua脚本编写游戏的UI或者其他逻辑,Lua是一个精悍小巧的脚本语言,可以跨平台运行解析,而且不需要编译过程。(热更lua)

2、使用C#Light(热更L#)

3、使用C#反射技术(热更DLL)

热更C#代码(DLL)

针对C#代码热更新的机制做一个简单的概述,在安卓上可以通过C#的语言特性——反射机制实现动态代码加载从而实现热更新。

具体做法是:将需要频繁更改的逻辑部分独立出来做成DLL,在主模块调用这些DLL,主模块代码是不修改的,只有作为业务(逻辑)模块的DLL部分需要修改。游戏运行时通过反射机制加载这些DLL就实现了热更新。注:将这部分dll改个名字放在安卓包体中,可以防止一些别有用心的人做一些别有用心的事情。

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

关于C#在苹果系统上热更新的思考可以参考此处

Lua热更新

项目中使用的是ulua更新框架,不仅支持热更lua,还支持热更C#Light(L#),之所以把这两者放在一起讲是因为他们都支持安卓与苹果系统的热更新。

Lua热更新的原理:在客户端可以编写Lua的解析器,通过这个解析器,可以运行最新的Lua脚本,后期可以把控制游戏逻辑的代码都写成Lua脚本。

C#和lua中的类型可以一一对应。

Lua 和 C#中类型的对应

nil null

String System.String

number System.Double

boolean System.Boolean

table Lualnterface.LuaTable

function LuaInterface.LuaTable

Lua中通过表来实现面向对象的一个小例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

Enemy = {} --申明对象

loacal this = Enemy --申明this关键字代表当前对象

--定义并声明对象中的属性

Enemy.hp=100

Enemy.speed=12

--定义并声明对象中的方法

Enemy.Move = function()

print("移动")

end

function Enemy.Attact()

print(this.hp,"攻击")

this.Move()

end

--执行

Enemy.Attact()

Lua解析器示例

在C#中执行访问Lua代码
1
2
3
4
5
6
7
8
9
10
11
12
13
Lua lua = new Lua(); //创建Lua解析器

lua["num"] = 5; //定义一个 num

lua["str"] = "a string"; //定义一个字符串

lua.newTable("tab"); //创建一个表 tab={}

//取得Lua环境中的变量

double num = (double)lua["num"];

string str = (string)lua["str"];
在C#脚本中执行Lua脚本文件,或者脚本字符串
1
2
3
4
5
6
7
lua.DoFile("script.lua");//执行script.lua脚本

lua.DoString("num = 2");//执行lua代码

lua.DoString("str = 'a string'");

object[] retVals = lua.DoString("return num,str");

在热更新中,只需要写好解析Lua脚本的代码,然后C#代码不需要变动,只需要修改lua脚本就好,通过lua脚本控制游戏逻辑。

把一个C#方法注册进Lua的一个全局方法
1
2
3
4
5
6
7
8
9
10
11
//把一个类中的普通方法注册进去

Lua.RegisterFunction("NormalMethod",obj,obj.GetType().GetMethod("NormalMethod"))

lua.DoString("NormalMethod()")

//把一个类的静态方法注册进去

lua.RegisterFunction("StaticMethod",null,typeof(ClassName).GetMethod("StaticMethod"))

lua.DoString("StaticMethod()")
在Lua中使用C#脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require "luanet" --引入库,相当于 import

--加载CLR的类型、实例化CLR对象

luanet.load_assembly("System.Windows.Forms")

luanet.load_assembly("System.Drawing") --表示加载了一个dll,理解为加载了system.drawing的命名空间

Form = luanet.import_type("System.Windows.Forms.Form") --表示引入了System.Windows.Forms下的一个类Form

StartPosition = luanet.import_type("System.Windows.Forms.FormStartPosition")

print(Form)

print(StartPosition)

在Lua中使用C#中的类创建对象的时候,会自动匹配最合适的构造方法。

在Lua中访问C#中的属性和方法

Lua代码中,访问C#对象的属性的方法和访问table的键索引一样,比如obj.name 或者 obj[“name”]

Lua代码中,访问C#对象的普通函数的方式和调用table的函数一样,如:obj:method()

注:在Lua中访问C#中的方法 - 特殊情况

当函数中有out或者ref参数时,out参数和ref参数和函数的返回值一起返回,并且调用的时候,out参数不需要传参。

AssetBundle热更新

AssetBundle是一个特定于平台的资产压缩包,资产包括Models、Textures、Prefabs、Audio clips等,每个不同的平台打包出来的AssetBundle不同。AssetBundle的热更新是unity官方支持的。

选择需要打包的资源,在inspector视图底部,找到AssetBundle选项,默认是None,选项卡选择NEW,输入自定义的AssetBundle名称,后面第二个选项卡是AssetBundle的后缀名,可选可不选,如果你使用了后缀,那么加载AssetBundle的时候需要带上后缀名。如果创建的标签是window标签,那么所有window标签的资源会打到一起window AssetBundle。也就是说,Unity会自动把相同标签的资源打包成一个整体。

使用官方提供的API打AB以及加载AB,BuildPipeline.BuildAssetBundles这个API,参数一是打包后AssetBundle的存放路径,参数二是压缩方式,参数三是打包的平台。

1
public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

本地加载,如果你设置的AssertBundle有后缀名,比如后缀是unity3d,则AssertBundle名需要加后缀,如Capsule.unity3d

1
2
3
4
AssetBundle ab = AssetBundle.LoadFromFile("Assets/AssetBundles/Android/cube");
if(ab != null){
Instantiate(ab.LoadAsset<GameObject>(cube))
}

网络加载

1
2
3
4
5
6
7
8
9
10
11
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
Instantiate(cube);
Instantiate(sprite);
}

动态load资源的几种途径

  • 通过Resources模块,调用它的load函数:可以直接load并返回某个类型的Object,前提是要把这个资源放在Resource命名的文件夹下,Unity不关有没有场景引用,都会将其全部打入到安装包中。

  • 通过bundle的形式:即将资源打成 AssetBundle 放在服务器或本地磁盘,然后使用WWW模块get 下来,然后从这个bundle中load某个object。(AssetBundle热更的前提和基础)

  • 通过AssetDatabase.loadasset :这种方式只在editor范围内有效,游戏运行时没有这个函数,它通常是在开发中调试用的。

关于AssetBundle的分组,有必要仔细思考一下,在这里分享下之前公司经历过的一个项目的真实案例。在项目开发的前期,没有注意AssetBundle的分组策略,采用的是一个UI界面一个AssetBundle,在项目的后期却怎么也打不出来包,折腾了一两天之后才终于发现这是unity官方的bug,AssetBundle包有一个数量上的限制,不能突破这个限制,于是我们修改了AssetBundle的分组策略,改为了所有UI界面一个包才最终规避了这个问题。

可以参考官方提供的分组策略:

  • 逻辑实体分组:
    一个UI界面或者所有UI界面一个包;
    一个角色或者所有角色一个包;
    所有场景所共享的部分一个包。
  • 类型分组:比如Models一个包、Audio clips一个包等。
  • 使用分组:
    把在某一时间内使用的所有资源打成一个包;
    把同一关卡的所有资源打成一个包;
    一个场景一个包。
  • 原则
    经常更新的资源与不经常更新的资源拆分离为两个包;
    把需要同时加载的资源放在同一个包;
    把其他包共享的资源放在一个单独的包里面;
    把一些需要同时加载的小资源打包成一个包;
    如果对于同一个资源有两个版本,可以考虑通过后缀来区分。

热更文本(项目中的表格、json文件等)

一般的项目中,为了方便配置控制游戏中的数值以及文本,会使用excel表格填写数据,利用python脚本将其转换成json或者二进制文件供客户端读取。文本不需要编译过程,如果遇到了相关数据的改动,更新相关文件即可。

分享到