xLua 游戏热更新 - Unity3D

结合官方文档的 xLua 学习笔记,实现 Lua 和 c# 之间的相互调用

什么是热更新

简单来说就是当游戏某个功能出现 bug,或者修改了某个功能,或者增加了某个功能的时候,我们不需要重新下载安装安装包,就可以更新游戏内容。

热更新的好处:不用浪费流量重新下载,不用通过商店审核更加快速,不用重新安装玩家可以更快体验到更新的内容

热更新方案

这些热更新方案都是基于 Lua 语言的,也可以叫做 lua 插件。

  • ulua(不再维护,转到主要维护 tolua)
  • tolua(基于 tolua 开发了 luaframework)
  • slua 代码质量好,性能比 tolua 低

xLua 简介

xLua 是 Unity3D 下的 Lua 编程解决方案,作者是腾讯的车雄生前辈,xLua 全平台支持用 Lua 修复 C# 代码的 bug,借助 xLua,这些 Lua 代码可以方便的和 C# 相互调用。

2016年12月末,xLua 刚刚实现新的突破:全平台支持用 Lua 修复 C# 代码 bug。

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

  • 接入成本高,有的项目已经用 C# 写完了,这时要接入需要把需要热更的地方用 Lua 重新实现;
  • 即使一开始就接入了,也存在同时用两种语言开发难度较大的问题;
  • Lua 性能不如 C#;

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

  • 平时用 C# 开发;
  • 运行也是 C#,性能秒杀 Lua;
  • 有 bug 的地方下发个 Lua 脚本 fix 了,下次整体更新时可以把 Lua 的实现换回正确的 C# 实现,更新时甚至可以做到不重启游戏;

导入 xLua

  • GitHub Cloen or download
  • 解压后,将 Assets 文件夹下的所有文件复制到自己工程的 Assets 文件夹下
    • XLua 里包含文档和教程,可自行选择是或否需要

HelloWorld - 执行字符串

using UnityEngine;
using XLua; // xLua

namespace XLuaTest {
    public class Helloworld : MonoBehaviour {

        private LuaEnv luaEnv; // 一个 LuaEnv 实例对应 Lua 虚拟机,出于开销的考虑,建议全局唯一。

        private void Start() {
            luaEnv = new LuaEnv(); // Lua 环境
            luaEnv.DoString("print('hello world lua')"); // 运行 Lua 程序,String 内容需要符合 Lua 语法规则,输出带有 Lua 标识,LUA: hello world lua
            luaEnv.DoString("CS.UnityEngine.Debug.Log('hello world')"); // 在 Lua 中调用 c# API
        }

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

加载 Lua 文件

因为 Resource 只支持有限的后缀,Load<TextAsset> 会自动为读取的文件添加 txt 的后缀,但又为了区分是 lua 文件,所以将 lua 文件命名为 HelloWorld.lua.txt

HelloWorld.lua.txt

print('HelloWorld -- Lua')

利用 TextAsset 加载

using UnityEngine;
using XLua;

public class HelloWorldByFile : MonoBehaviour {

    private void Start() {
        // lua 文件名为: HelloWorld.lua.txt
        TextAsset ta = Resources.Load<TextAsset>("HelloWorld.lua");
        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString(ta.text); // 运行 Lua 程序
        luaEnv.Dispose();
    }
}

利用 Loader 加载

用 lua 的 require 函数,require 实际上是调一个个的 loader 去加载,有一个成功就不再往下尝试,全失败则报文件找不到。

建议的加载 Lua 脚本方式是:整个程序就一个 DoString(“require ‘main’”),然后在 main.lua 加载其它脚本(类似 lua脚本的命令行执行:lua main.lua)。

using UnityEngine;
using XLua;

public class HelloWorldByFile : MonoBehaviour {

    private void Start() {
        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString("require 'HelloWorld'"); // 利用 Loader 加载
        luaEnv.Dispose();
    }
}

自定义 Loader

因为系统自带的 Loader 需要 lua 文件放在 Resources 文件夹下,那要是我的 Lua 文件是下载回来的,或者放在自定义的目录里面,怎么办?问得好,xLua 的自定义 Loader 可以满足这些需求。

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

namespace Tutorial {
    public class CustomLoader : MonoBehaviour {

        LuaEnv luaenv = null;

        void Start() {
            luaenv = new LuaEnv();
            // public void LuaEnv.AddLoader(CustomLoader loader)
            luaenv.AddLoader(MyLoader);
            luaenv.DoString("require 'HelloWorld'"); // 利用 Loader 加载
        }

        /// <summary>
        /// 自定义 Loader
        /// public delegate byte[] CustomLoader(ref string filepath);
        /// </summary>
        private byte[] MyLoader(ref string filePath) {
            string absParh = "D:/" + filePath + ".lua.txt"; // lua 文件放在自定义目录 D 盘根目录下
            return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(absParh)); // 将 Lua 程序转换为了字节数组
        }

        void OnDestroy() {
            luaenv.Dispose();
        }
    }
}

C# 访问 Lua

这里指的是 C# 主动发起对 Lua 数据结构的访问。

  • 访问 lua 全局数据,特别是 table 以及 function,代价比较大,建议尽量少做

    • 比如在初始化时把要调用的 lua function 获取一次(映射到delegate)后,保存下来,后续直接调用该delegate 即可。table 也类似。
  • 如果 lua 实现的部分都以 delegate 和 interface 的方式提供,使用方可以完全和 xLua 解耦:由一个专门的模块负责 xlua 的初始化以及 delegate、interface 的映射,然后把这些 delegate 和i nterface 设置到要用到它们的地方。

获取一个全局基本数据类型

因为 lua 中 的 number 包括 int float double,所以利用 C# 访问时,需自行选择对应的类型进行获取

a = 100
b = 'Hello World'
c = true
int a = luaenv.Global.Get<int>("a");
float a2 = luaenv.Global.Get<float>("a");
string b = luaenv.Global.Get<string>("b");
bool c = luaenv.Global.Get<bool>("c");

访问一个全局的 table

映射到 class

这种方式下 xLua 会帮你 new 一个实例,并把对应的字段赋值过去。table 的属性可以多于或者少于 class 的属性。可以嵌套其它复杂类型。要注意的是,这个过程是值拷贝,如果 class 比较复杂代价会比较大。而且修改 class 的字段值不会同步到 table,反过来也不会。

这个功能可以通过把类型加到 GCOptimize 生成降低开销。

person = {
    name='cy',
    age=18,
    666, -- 无对应键,不会映射
}
public class Person {
    public string name;
    public int age;
}

// 访问一个全局的 table,,映射到 class
Person p = luaenv.Global.Get<Person>("person");
print(p.name + " - " + p.age);
// 修改 class 的字段值不会同步到 table
p.age = 23;
luaenv.DoString("print(person.age)");  // LUA: 18
映射到 interface(推荐)

这种方式是引用方式的映射,即在 c# 中修改,也会同步到 lua 的 table 中。

需要在接口上添加 [CSharpCallLua] 标签

在表中定义方法时需显示指定 self 参数,引号方式则不需要

person = {
    name='cy',
    age=18,
    Study=function(self,hour) -- 需要显示指定 self 参数
        return '已学习'..hour..'小时'
    end
}
-- 默认带一个 self 的参数,代表当前 table
function person:Eat(food) 
    return '吃饭:'..food
end
[CSharpCallLua]
public interface IPerson {
    string name { get; set; }
    int age { get; set; }
    string Study(int hour);
    string Eat(string food);
}

// 访问一个全局的 table,映射到 interface
IPerson p2 = luaenv.Global.Get<IPerson>("person"); // 映射到有对应字段的class,by value
print(p2.name + " - " + p2.age);
p2.name = "interface cy";
luaenv.DoString("print(person.name)"); // 引用方式,会同步到 table 中,输出 LUA: interface cy
print(p2.Study(2)); // p2.Study(p2,2); xLua 调用时会自动将当前对象填充为第一个参数,所以需要在 Lua 中显示的定义第一个参数 self
print(p2.Eat("Apple"));
映射到 Dictionary<>,List<>

更加轻量级的方式,有很多的限制,如果不想定义 class 或者 interface 的话,可以考虑用这个,前提 table 下 key 和 value 的类型都是一致的。

Dictionary 无法映射没有键的情况,方法映射也无法达到预期效果。

List 只能映射值。

person = {
    name='cy',
    age=18,
    66,'哈哈',
    Study=function(self,hour) -- 需要显示指定 self 参数
        return '已学习'..hour..'小时'
    end
}
-- 默认带一个 self 的参数,代表当前 table
function person:Eat(food) 
    return '吃饭:'..food
end

// 映射到 Dictionary<>
Dictionary<string, object> dict = luaenv.Global.Get<Dictionary<string, object>>("person");
foreach (string key in dict.Keys) {
    print(key + "-" + dict[key]); // age-18 Eat-function:11 Study-function:13 name-cy
}

// 映射到 List<>
List<object> list = luaenv.Global.Get<List<object>>("person");
foreach (object o in list) {
    print(o); // 66 哈哈
} 
映射到 LuaTable 类

LuaTable 类,是 xLua 定义好的一个 C# 类,这种方式好处是不需要生成代码,但也有一些问题,比如慢,比映射到 interface 要慢一个数量级,而且没有类型检查。

person = {
    name='cy',
    age=18
}
LuaTable tab = luaenv.Global.Get<LuaTable>("person");
print(tab.Get<string>("name") + tab.Get<int>("age"));

访问一个全局的 function

映射到 delegate(推荐)
-- 全局函数
function fun()
    print('Hello World')
end

function fun2(a,b)
    print(a+b)
    return a+b
end

function fun3(a,b)
    return a+b,a+1,b+2
end
[CSharpCallLua]
public delegate int Add(int a, int b);

// 多返回值,利用 out 参数
[CSharpCallLua]
public delegate int Fun3(int a, int b,out int resA,out int resB);

// ---------------------------------------

// 利用 ACtion
Action act = luaenv.Global.Get<Action>("fun"); // 映射到一个 delgate,要求 delegate 加到生成列表,否则返回 null,建议用法
act(); // LUA:Hello World

// 带参数
Add add = luaenv.Global.Get<Add>("fun2");
print(add(3, 4)); // LUA:7      7

// 多返回值
Fun3 fun3 = luaenv.Global.Get<Fun3>("fun3");
int resA, resB;
int res = fun3(3, 4, out resA, out resB); // 利用 out 参数接收多返回值
print(res + "-" + resA + "-" + resB); // 7-4-6
映射到 LuaFunction

存在性能问题

function fun3(a,b)
    return a+b,a+1,b+2
end
LuaFunction lfun = luaenv.Global.Get<LuaFunction>("fun3");
object[] os = lfun.Call(3, 4);
foreach (object o in os) {
    print(o); // 7  4  6
}

完整 Demo

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

namespace Tutorial {
    public class CSCallLua : MonoBehaviour {

        LuaEnv luaenv = null;

        #region Lua Code
        string script = @"
        a = 100
        b = 'Hello World'
        c = true

        person = {
            name='cy',
            age=18,
            66,'哈哈',
            Study=function(self,hour) -- 需要显示指定 self 参数
                return '已学习'..hour..'小时'
            end
        }
        -- 默认带一个 self 的参数,代表当前 table
        function person:Eat(food) 
            return '吃饭:'..food
        end

        -- 全局函数
        function fun()
            print('Hello World')
        end

        function fun2(a,b)
            print(a+b)
            return a+b
        end

        function fun3(a,b)
            return a+b,a+1,b+2
        end
        ";
        #endregion

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

        [CSharpCallLua]
        public interface IPerson {
            string name { get; set; }
            int age { get; set; }
            string Study(int hour);
            string Eat(string food);
        }

        [CSharpCallLua]
        public delegate int Add(int a, int b);

        // 多返回值,利用 out 参数
        [CSharpCallLua]
        public delegate int Fun3(int a, int b,out int resA,out int resB);

        void Start() {
            luaenv = new LuaEnv();
            luaenv.DoString(script);

            // 1. 获取 Lua 里面的全局变量
            int a = luaenv.Global.Get<int>("a"); // 因为 lua 中 的 number 包括 int float double,所以利用 C# 访问时,需自行选择对应的类型进行获取
            string b = luaenv.Global.Get<string>("b");
            bool c = luaenv.Global.Get<bool>("c");
            Debug.Log("a = " + a + ", b = " + b + ", c = " + c);

            // 2. 访问一个全局的 table,映射到 class
            Person p = luaenv.Global.Get<Person>("person");
            print(p.name + " - " + p.age);
            p.age = 23;
            luaenv.DoString("print(person.age)");

            // 3. 访问一个全局的 table,映射到 interface
            IPerson p2 = luaenv.Global.Get<IPerson>("person"); // 映射到有对应字段的class,by value
            print(p2.name + " - " + p2.age);
            p2.name = "interface cy";
            luaenv.DoString("print(person.name)"); // LUA: interface cy
            print(p2.Study(2)); // p2.Study(p2,2); xLua 调用时会自动将当前对象填充为第一个参数,所以需要在 Lua 中显示的定义第一个参数 self
            print(p2.Eat("Apple"));

            // 4. 映射到 Dictionary<>
            Dictionary<string, object> dict = luaenv.Global.Get<Dictionary<string, object>>("person");
            foreach (string key in dict.Keys) {
                print(key + "-" + dict[key]); // age-18 Eat-function:11 Study-function:13 name-cy
            }

            // 5. 映射到 List<>
            List<object> list = luaenv.Global.Get<List<object>>("person");
            foreach (object o in list) {
                print(o); // 66 哈哈
            }

            // 6. 映射到 LuaTable
            LuaTable tab = luaenv.Global.Get<LuaTable>("person");
            print(tab.Get<string>("name") + tab.Get<int>("age"));

            // 7. 访问全局函数
            Action act = luaenv.Global.Get<Action>("fun"); // 映射到一个 delgate,要求 delegate 加到生成列表,否则返回 null,建议用法
            act(); // LUA:Hello World
            // 带参数
            Add add = luaenv.Global.Get<Add>("fun2");
            print(add(3, 4)); // LUA:7      7
            // 多返回值
            Fun3 fun3 = luaenv.Global.Get<Fun3>("fun3");
            int resA, resB;
            int res = fun3(3, 4, out resA, out resB); // 利用 out 参数接收多返回值
            print(res + "-" + resA + "-" + resB); // 7-4-6

            // 8. 映射到 LuaFunction
            LuaFunction lfun = luaenv.Global.Get<LuaFunction>("fun3");
            object[] os = lfun.Call(3, 4);
            foreach (object o in os) {
                print(o);
            }
        }

        void Update() {
            if (luaenv != null) {
                luaenv.Tick();
            }
        }

        void OnDestroy() {
            luaenv.Dispose();
        }
    }
}

Lua 调用 C#

所有 C# 相关的都放到 CS 下,包括构造函数,静态成员属性、方法。

new C# 对象

-- new C# 对象,构造游戏物体
local newGameObj = CS.UnityEngine.GameObject()
local newGameObj2 = CS.UnityEngine.GameObject('NewByLua')

访问 C# 静态属性,方法

如果需要经常访问的类,可以先用局部变量引用后访问,除了减少敲代码的时间,还能提高性能:

local GameObject = CS.UnityEngine.GameObject

GameObject.Find(“Main Camera”)

local GameObject = CS.UnityEngine.GameObject -- 性能优化
print('UnityEngine.Time.deltaTime:', CS.UnityEngine.Time.deltaTime) -- 读静态属性
CS.UnityEngine.Time.timeScale = 0.5 -- 写静态属性
local camera = GameObject.Find("Main Camera") -- 静态方法调用
camera.name = "update by lua" -- 修改摄像机的名字

访问 C# 成员属性,方法

local cameraCom= camera:GetComponent("Camera") -- 调用成员方法的时候,使用冒号,否则需要传递自身为第一个参数
-- local cameraCom= camera.GetComponent(camera,"Camera") -- 传递自身
GameObject.Destroy(cameraCom)

参数的输入输出属性(out,ref)

Lua 调用 c# 的参数处理规则:C# 的普通参数算一个输入形参,ref 修饰的算一个输入形参,out 不算,然后从左往右对应 lua 调用 C# 的实参列表;

Lua 调用 C# 的返回值处理规则:C# 函数的返回值(如果有的话)算一个返回值,out 算一个返回值,ref 算一个返回值,然后从左往右对应 lua 的多返回值。

lua
 xxx(t1,t2,t3)
 	return re1,t3,t4
 end
 
c#
 xxx(type t1,type t2,ref type t3,put type t4){
     return re1;
 }

泛化(模版)方法

不直接支持,可以通过 Extension methods 功能进行封装后调用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值