Unity2020 几种常用热更新方案的优劣及XLua实战

Posted 小程小程,永不消沉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity2020 几种常用热更新方案的优劣及XLua实战相关的知识,希望对你有一定的参考价值。

了解热更新之前,首先得知道为啥要用热更新?
为了省去用户自行更新客户端的步骤,从而改善用户体验。

一、几种常用热更新解决方案的区别

以下列举的这些解决方案都是C#环境下的Lua代码解释器(Lua虚拟机)。
1、uLua
Lua插件原生版本,不会产生静态代码,反射机制,效率低下,速度慢,gcalloc频繁,已停止更新维护
2、xLua

视频 xLua基本原理原则 https://www.zhihu.com/zvideo/1380896883473170433

3、ToLua
tolua(基于tolua开发了luaframework)
ToLua LuaFramework 使用实战[1]-代码热更新
ToLua LuaFramework 使用实战[2]-资源热更新

4、slua
代码质量好,性能比tolua低

5、其他
C#light、LSharp同一个作者(商用比较少)
参考此博客:https://blog.csdn.net/guofeng526/article/details/52662994

二、Lua语法讲解及编辑器下载

lua语法是通用的,可以根据菜鸟课程学。
https://www.runoob.com/lua/lua-basic-syntax.html

方便学习和直接运行Lua代码,先安装Lua编辑器:Luaforwindows
https://github.com/rjpcomputing/luaforwindows/releases
第一个是命令行形式。
第二个是编辑器形式,推荐用第二个:

三、XLua热更新实战

1、xLua特性总结

xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。

目前Unity下的Lua热更新方案大多都是要求要热更新的部分一开始就要用Lua语言实现,不足之处在于:

1、接入成本高,有的项目已经用C#写完了,这时要接入需要把需要热更的地方用Lua重新实现;

2、即使一开始就接入了,也存在同时用两种语言开发难度较大的问题;

3、Lua性能不如C#;

xLua热补丁技术支持在运行时把一个C#实现(函数,操作符,属性,事件,或者整个类)替换成Lua实现,意味着你可以:

1、平时用C#开发;

2、运行也是C#,性能秒杀Lua;

3、有bug的地方下发个Lua脚本fix了,下次整体更新时可以把Lua的实现换回正确的C#实现,更新时甚至可以做到不重启游戏;

这个新特性iosandroid,Window,Mac都测试通过了,目前在做一些易用性优化。

2、xlua支持下载,以及Unity 中搭建 xlua 环境

xlua支持下载:
https://codeload.github.com/Tencent/xLua/zip/refs/heads/master

https://github.com/Tencent/xLua

参考此博客:https://blog.csdn.net/u014361280/article/details/104826274

3、C#与Lua交互原理

可参考此博客:https://zhuanlan.zhihu.com/p/38322991
C#调用Lua:C#先调用Lua的dll文件(C语言写的库),dll文件执行Lua代码

2、Lua调用C#:Wrap方式:非反射机制,需要为源文件生成相应的wrap文件,当启动Lua虚拟机时,Wrap文件将会被自动注册到Lua虚拟机中,之后,Lua文件
将能够识别和调用C#源文件。

总结:Lua调用Wrap文件, Wrap文件调用C#源文件

4、Xlua与C#的互相调用实战

Xlua与C#的互相调用:

项目结构如下图:

三个lua脚本和C#脚本如下:

下面展示一些 LuaTest.lua

num = 10
name = "LuaTest"
boy = true

person = {name = "xiaoming",age = 20}

function add(a,b)
	a = a+b
	print(a)
end

--在Lua中new C#对象(创建游戏物体)
CS.UnityEngine.GameObject()

--Lua访问C#静态属性和方法
print(CS.UnityEngine.Time.deltaTime)

CS.UnityEngine.Time.timeScale = 0.5

local gameObject = CS.UnityEngine.GameObject

local cube = gameObject.Find("Cube")
cube.name = "cubeLua"

local Collider = cube.GetComponent(cube,"BoxCollider")
gameObject.Destroy(Collider)

print("-----LuaTest-----")

下面展示一些 HellowWorld.lua

num = 10
name = "LuaTest"
boy = true
print("HellowWorld")

下面展示一些 Lua_StreamingAssets.lua

print('Load Lua by StreamingAssets')

下面展示一些 LuaStudy

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

public class LuaStudy : MonoBehaviour
{
    private LuaEnv luaEnv;

    private void Start()
    {
        //创建lua运行环境
        luaEnv = new LuaEnv();
        LuaLoaderFun();
        VisitLua_Table();
        VisitLua_Function();
    }

    private void OnDestroy()
    {
        //释放lua环境
        luaEnv.Dispose();
    }

    /// <summary>
    /// 加载及调用lua脚本
    /// </summary>
    void LuaLoaderFun()
    {
        //执行lua的输出语句
        luaEnv.DoString("print('Hellow World 1')");

        //在lua中调用C#方法
        luaEnv.DoString("CS.UnityEngine.Debug.Log('Hellow World 2')");

        //加载运行Resources文件中的lua脚本
        TextAsset ta = Resources.Load<TextAsset>("HellowWorld.lua");
        luaEnv.DoString(ta.text);
        //加载运行helloworld.lua.txt  默认从Resources文件中进行加载
        luaEnv.DoString("require 'HellowWorld'");
       
        // 添加自定义的Loader方法
        //添加了一个自定义的Loader返回null并且DoString里面添加的是一个不存在的lua文件。它会返回错误。
        //luaEnv.AddLoader(MyLoader_1);
        //luaEnv.DoString("require 'xxx'");

        // 添加自定义的Loader方法
        //添加了一个自定义的Loader返回lua语句的二进制并且DoString里面添加的是一个不存在的lua文件。它会执行自定义的Loader的输出。
        //luaEnv.AddLoader(MyLoader_2);
        //luaEnv.DoString("require 'xxx'");

        //通过自定义Loader加载指定目录的Lua脚本
        luaEnv.AddLoader(MyLoader_3);
        luaEnv.DoString("require 'Lua_StreamingAssets'");
    }

    /// <summary>
    /// 访问获取lua属性和表
    /// </summary>
    void VisitLua_Table() {

        //调用lua脚本才能访问属性
        luaEnv.DoString("require 'LuaTest'");

        //获取lua脚本中的全局变量
        int num = luaEnv.Global.Get<int>("num");//获取lua里面的全局变量num
        string name = luaEnv.Global.Get<string>("name");//获取lua里面的全局变量name
        Debug.Log("num:" + num + "  name:" + name);

        //访问lua中的table(映射到class)
        Person personTemp = luaEnv.Global.Get<Person>("person");
        Debug.Log(personTemp.name + "    " + personTemp.age);
        //这种方式的访问,修改age的值不会影响到lua里面的表的属性
        personTemp.name = "wang";
        personTemp.age = 50;
        //修改后试一下
        Person personTemp2 = luaEnv.Global.Get<Person>("person");
        Debug.Log(personTemp2.name + "    " + personTemp2.age);

        //访问lua中的table(映射到interface)
        //映射到interface修改p中的属性,lua中的原table也会发生变化
        IPerson personTemp3 = luaEnv.Global.Get<IPerson>("person");
        Debug.Log(personTemp2.name + "    " + personTemp2.age);

        //通过LuaTable访问table
        List<object> list = luaEnv.Global.Get<List<object>>("person");
        foreach (object o in list)
        {
            print(o);
        }

        //通过LuaTable类 比较慢
        LuaTable luaTable = luaEnv.Global.Get<LuaTable>("person");
        print(luaTable.Get<string>("name"));
        print(luaTable.Get<int>("age"));
    }

    /// <summary>
    /// 访问lua中的全局方法
    /// </summary>
    void VisitLua_Function()
    {
        //调用lua脚本才能访问属性
        luaEnv.DoString("require 'LuaTest'");

        //使用委托来获取Lua中的全局函数
        Add add = luaEnv.Global.Get<Add>("add");
        add(2, 4);

        //通过LuaFunction访问Lua中的全局函数
        LuaFunction luaFunction = luaEnv.Global.Get<LuaFunction>("add");
        luaFunction.Call(2, 4);
    }

    private byte[] MyLoader_1(ref string filePath)
    {
        print(filePath);
        return null;
    }

    private byte[] MyLoader_2(ref string filePath) {
        string s = "print(123)";
        return System.Text.Encoding.UTF8.GetBytes(s);
    }

    private byte[] MyLoader_3(ref string filePath) {
        string path = Application.streamingAssetsPath + "/" + filePath + ".lua.txt";
        return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(path));
    }

    class Person
    {
        public string name;
        public int age;
    }

	//打标签
    [CSharpCallLua]
    public interface IPerson
    {
        string name { get; set; }
        int age { get; set; }
    }
	//打标签
    [CSharpCallLua]
    delegate void Add(int a, int b);

}

5、xLua热更新框架使用

下载的xLua-master的 Assets\\XLua\\Doc 路径下有热更新相关教程文档

这个文档说明一定要仔细阅读https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/hotfix.md

使用方式
1、添加HOTFIX_ENABLE宏打开该特性(在Unity3D的File->Build Setting->Scripting Define Symbols下添加)。编辑器、各手机平台这个宏要分别设置!如果是自动化打包,要注意在代码里头用API设置的宏是不生效的,需要在编辑器设置。

(建议平时开发业务代码不打开HOTFIX_ENABLE,只在build手机版本或者要在编译器下开发补丁时打开HOTFIX_ENABLE)

2、打开unity安装路径D:\\Unity\\2020.3.0f1c1\\Editor\\Data\\MonoBleedingEdge\\lib\\mono\\unity\\Mono.Cecil.dll,把Mono.Cecil.dll拷贝到项目LuaTest\\Assets\\XLua\\Editor路径下

或者复制Tools文件到项目中

3、菜单栏中,执行xLua/Generat Code,会在新建Gen文件夹,下面生成一些wrap文件


4、把ExampleConfig脚本拖到XLua文件夹下,自行修改配置信息

5、构建手机包这个步骤会在构建时自动进行,编辑器下开发补丁需要手动执行"XLua/Hotfix Inject In Editor"菜单。注入成功会打印“hotfix inject finish!”或者“had injected!”。

6、热更新大致流程

1、游戏一开始启动时,检查远程服务器上是否有新的myHotfix.lua文件(例如:md5比对,自己加解密),有的话,下载下来,放在指定目录,没有的话,读取本地已有的myHotfix.lua文件,若文件不存在,则说明不需要热修复,一般这种情况是在项目刚发布的早期,没有进行打补丁;
2、项目发布版本前,需要在CustomGenConfig.cs中 加入需要添加[Hotfix]标签的类型,想要更灵活使用的话,可以自己写个配置表,读取配置中的类型,自动添加,***只有加入[Hotfix]标签的类型,才可以调用xlua.hotfix()

以上是关于Unity2020 几种常用热更新方案的优劣及XLua实战的主要内容,如果未能解决你的问题,请参考以下文章

Unity 热更新技术 | 热更新的基本概念原理及主流热更新方案介绍

Unity上面有啥好的热更新方案

Unity上面有啥好的热更新方案

unity3d的资源管理和热更新方案

unity3d的资源管理和热更新方案

unity热更新新方案,ILRuntime