前言
热更新,是游戏开发中十分重要的一个环节。热更新,指用户不重启应用的前提下能实现代码逻辑的更新。因此,本文主要介绍腾讯开源的热更新方案Xlua的使用,以及在项目框架中对Xlua进行支持。
Xlua 教程
Lua代码的本质实际上是一个字符串,Xlua插件让系统能够去执行这段代码并且和C#之间能够相互通信。
1、加载Lua文件
对Unity游戏而言,可以通过资源加载的方式加载TextAsset资源后执行lua代码:
public void DoScript(string luaName)
{
TextAsset luaScript = AssetsManager.Instance.LoadAsset<TextAsset>(luaName);
var scriptEnv = luaEnv.NewTable();
// 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
LuaTable meta = luaEnv.NewTable();
meta.Set("__index", luaEnv.Global);
scriptEnv.SetMetaTable(meta);
scriptEnv.Set("LuaTable", scriptEnv);
scriptEnv.Set("LuaName", luaName);
meta.Dispose();
luaEnv.DoString(luaScript.text, luaScript.name, scriptEnv);
}
同样也可以通过require 方法加载lua文件,require实际上是调用一个个的loader去加载,有一个加载成功就不继续往下尝试,全失败则找不到文件。因此,我们可以自定义加载的loader:
/// <summary>
/// 自定义加载
/// </summary>
/// <param name="file">lua代码</param>
/// <returns></returns>
public byte[] AddLoader(ref string file)
{
TextAsset luaScript = AssetsManager.Instance.LoadAsset<TextAsset>(file);
return System.Text.Encoding.UTF8.GetBytes(luaScript.text);
}
2、C# 访问Lua
在C# 中可以通过泛型方法 luaenv.Global.Get(str)获取Lua中全局变量的值,
Lua调用侧的返回值处理规则:C#函数的返回值(如果有的话)算一个返回值,out算一个返回值,ref算一个返回值,然后从左往右对应lua的多返回值。
LuaTable表的映射:
- 映射到普通的class或struct。这种映射方式属于值拷贝,因此Lua或C#中对值进行改变相互不会有影响
- 映射到接口interface。这种方式需要生产依赖代码,属于引用拷贝,Lua或C#中对值的修改会相互影响
- 对于轻量级的table表可以直接映射为Dictionary<>或者List<>
- 直接映射到LuaTable类。这种方式不需要生成代码,但是比接口映射的方式慢一个数量级,没有类型检查
Lua中function的映射
- 映射到delegate,官方建议采用这种方式,性能好,而且类型安全,但是要生成代码。对于多返回值的,用out 或者 ref参数接收
- 映射到LuaFunction,不用生成代码,但是性能不好,类型不安全。
3、Lua访问C#
Lua中没有new关键字,所有C#相关的都放到CS下,包括构造函数,静态成员属性、方法如在Lua中新建一个对象:
local obj=CS.UnityEngine.GameObject("[Asset Pool]")
上述lua代码就等同于在C#中的
GameObject obj= new GameObject("[Asset Pool]")
对于一些静态类,可以使用全局或者全局变量先引用后访问,减少代码量,还能提高性能:
local GameObject = CS.UnityEngine.GameObject
GameObject.Find('[Asset Pool]')
输入输出参数规则:
Lua调用侧的参数处理规则:C#的普通参数算一个输入形参,ref 修饰的算一个输入参数,out 参数不
算,然后从左往右对应lua调用侧的实参列表。
4、XLua的配置
- 打标签。
Xlua用白名单来指明生成哪些代码,通过白名单attribute来配置,如从lua想调用C#中的某个类,希望生成适配代码,就打上LuaCallCSharp标签
[LuaCallCSharp]
public class LuaManager
{
}
2、对于一些系统类和第三方组件我们可以通过静态列表来打标签。在项目中配置Xlua导出文件配置。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Playables;
using UnityEngine.UI;
using XLua;
/// <summary>
/// xlua自定义导出
/// </summary>
public static class XLuaCustomExport
{
/// <summary>
/// dotween的扩展方法在lua中调用
/// </summary>
[LuaCallCSharp]
[ReflectionUse]
public static List<Type> dotween_lua_call_cs_list = new List<Type>()
{
typeof(DG.Tweening.AutoPlay),
typeof(DG.Tweening.AxisConstraint),
typeof(DG.Tweening.Ease),
typeof(DG.Tweening.LogBehaviour),
typeof(DG.Tweening.LoopType),
typeof(DG.Tweening.PathMode),
typeof(DG.Tweening.PathType),
typeof(DG.Tweening.RotateMode),
typeof(DG.Tweening.ScrambleMode),
typeof(DG.Tweening.TweenType),
typeof(DG.Tweening.UpdateType),
typeof(DG.Tweening.DOTween),
typeof(DG.Tweening.DOVirtual),
typeof(DG.Tweening.EaseFactory),
typeof(DG.Tweening.Tweener),
typeof(DG.Tweening.Tween),
typeof(DG.Tweening.Sequence),
typeof(DG.Tweening.TweenParams),
typeof(DG.Tweening.Core.ABSSequentiable),
typeof(DG.Tweening.Core.TweenerCore<Vector3, Vector3, DG.Tweening.Plugins.Options.VectorOptions>),
typeof(DG.Tweening.TweenCallback),
typeof(DG.Tweening.TweenExtensions),
typeof(DG.Tweening.TweenSettingsExtensions),
typeof(DG.Tweening.ShortcutExtensions),
typeof(PlayableDirector),
//typeof(DG.Tweening.ShortcutExtensions43),
//typeof(DG.Tweening.ShortcutExtensions46),
//typeof(DG.Tweening.ShortcutExtensions50),
//dotween pro 的功能
//typeof(DG.Tweening.DOTweenPath),
//typeof(DG.Tweening.DOTweenVisualManager),
};
}
XLua.ReflectionUse
一个C#类型类型加了这个配置,xLua会生成link.xml阻止il2cpp的代码剪裁。
对于扩展方法,必须加上LuaCallCSharp或者ReflectionUse才可以被访问到。
建议所有要在Lua访问的类型,要么加LuaCallCSharp,要么加上ReflectionUse,这才能够保证在各平台都能正常运行。
Unity项目中使用Xlua
在游戏开发中有部分经常更改的逻辑会采用Lua来进行开发,因此我们可以LuaBehaviour继承Monobehaviour,讲关键的生命周期函数映射到Lua,那样就可以使用Lua来进行一些纯Lua的开发工作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
using System;
[System.Serializable]
public class Injection
{
public string name;
public GameObject value;
}
[System.Serializable]
public class InjectionLuaScript
{
public string name;
public LuaBehaviour value;
}
[CSharpCallLua]
public delegate void CSCallLuaAction(params object[] args);
[CSharpCallLua]
public delegate object[] CallLuaFunction(params object[] args);
/// <summary>
/// 纯Lua开发使用
/// </summary>
[LuaCallCSharp]
public class LuaBehaviour : MonoBehaviour
{
[SerializeField]
private TextAsset luaScript;
[SerializeField]
private Injection[] injections;
[SerializeField]
private InjectionLuaScript[] otherScript;
private Action luaAwake;
private Action luaStart;
private Action luaEnable;
private Action luaUpdate;
private Action luaDisable;
private Action luaDestory;
public LuaTable scriptEnv;
private void Awake()
{
var luaEnv = LuaManager.Instance.luaEnv;
scriptEnv = luaEnv.NewTable();
// 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
LuaTable meta = luaEnv.NewTable();
meta.Set("__index", luaEnv.Global);
scriptEnv.SetMetaTable(meta);
meta.Dispose();
scriptEnv.Set("self", this);
foreach (var injection in injections)
{
scriptEnv.Set(injection.name, injection.value);
}
foreach (var injection in otherScript)
{
scriptEnv.Set(injection.name, injection.value);
}
luaEnv.DoString(luaScript.text, luaScript.name, scriptEnv);
luaAwake = scriptEnv.Get<Action>("Awake");
scriptEnv.Get("Start", out luaStart);
scriptEnv.Get("OnEnable", out luaEnable);
scriptEnv.Get("Update", out luaUpdate);
scriptEnv.Get("OnDisable", out luaDisable);
scriptEnv.Get("OnDestroy", out luaDestory);
if (luaAwake != null)
{
luaAwake();
}
}
// Start is called before the first frame update
void Start()
{
if (luaStart != null)
{
luaStart();
}
}
private void OnEnable()
{
if (luaEnable != null)
{
luaEnable();
}
}
// Update is called once per frame
void Update()
{
if (luaUpdate != null)
{
luaUpdate();
}
}
private void OnDisable()
{
if (luaDisable != null)
{
luaDisable();
}
}
public void CallLuaFunction(string name, params object[] args)
{
var call = scriptEnv.Get<CSCallLuaAction>(name);
if (call != null)
{
call(args);
}
}
public void CallLuaFunction(string name)
{
var call = scriptEnv.Get<CSCallLuaAction>(name);
if (call != null)
{
call(null);
}
}
private void OnDestroy()
{
if (luaDestory != null)
{
luaDestory();
}
luaAwake = null;
luaStart = null;
luaEnable = null;
luaUpdate = null;
luaDisable = null;
luaDestory = null;
scriptEnv.Dispose();
injections = null;
}
}
同样对于UI窗口组件我们可以在基类UIBase中添加对Lua的支持。
using UnityEngine;
using XLua;
using System;
[LuaCallCSharp]
public class LuaBase : UIBase
{
public TextAsset luaScript;
[LuaCallCSharp]
public delegate void CSCallLuaDelegate(params object[] args);
[SerializeField]
private Injection[] injections;
[SerializeField]
private InjectionLuaScript[] otherScript;
private Action luaInit;
private CSCallLuaDelegate luaShow;
private CSCallLuaDelegate luaHide;
private Action luaStart;
private Action luaEnable;
private Action luaUpdate;
private Action luaDisable;
private Action luaDestory;
public LuaTable scriptEnv;
private bool isInitLua = false;
public override void Init()
{
base.Init();
initLua();
}
private void initLua()
{
if (!isInitLua)
{
var luaEnv = LuaManager.Instance.luaEnv;
scriptEnv = luaEnv.NewTable();
// 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
LuaTable meta = luaEnv.NewTable();
meta.Set("__index", luaEnv.Global);
scriptEnv.SetMetaTable(meta);
meta.Dispose();
scriptEnv.Set("self", this);
foreach (var injection in injections)
{
scriptEnv.Set(injection.name, injection.value);
}
foreach (var injection in otherScript)
{
scriptEnv.Set(injection.name, injection.value);
}
luaEnv.DoString(luaScript.text, luaScript.name, scriptEnv);
luaInit = scriptEnv.Get<Action>("Init");
luaShow = scriptEnv.Get<CSCallLuaDelegate>("Show");
luaHide = scriptEnv.Get<CSCallLuaDelegate>("Hide");
luaStart= scriptEnv.Get<Action>("Start");
luaEnable = scriptEnv.Get<Action>("OnEnable");
luaUpdate = scriptEnv.Get<Action>("Update");
luaDisable = scriptEnv.Get<Action>("OnDisable");
luaDestory = scriptEnv.Get<Action>("OnDestroy");
}
if (luaInit != null)
{
luaInit();
}
}
void Start()
{
if (luaStart != null)
{
luaStart();
}
}
private void OnEnable()
{
if (luaEnable != null)
{
luaEnable();
}
if(luaUpdate!=null)
{
Scheduler.Instance.UpdateEvent += luaUpdate;
}
}
public override void Show(params object[] args)
{
if (luaShow != null)
{
luaShow(args);
}
base.Show(args);
}
public override void Hide(params object[] args)
{
if (luaHide != null)
{
luaHide(args);
}
base.Hide(args);
}
private void OnDisable()
{
if (luaDisable != null)
{
luaDisable();
}
if (luaUpdate != null)
{
Scheduler.Instance.UpdateEvent -= luaUpdate;
}
}
public void CallLuaFunction(string name, params object[] args)
{
var call = scriptEnv.Get<CSCallLuaDelegate>(name);
if (call != null)
{
call(args);
}
}
public void CallLuaFunction(string name)
{
var call = scriptEnv.Get<CSCallLuaDelegate>(name);
if (call != null)
{
call();
}
}
private void OnDestroy()
{
if (luaDestory != null)
{
luaDestory();
}
luaInit = null;
luaStart = null;
luaEnable = null;
luaUpdate = null;
luaDisable = null;
luaDestory = null;
luaShow = null;
luaHide = null;
scriptEnv.Dispose();
injections = null;
}
}
这样有一些常用切频繁修改的窗口我们甚至可以使用纯Lua的开发方式。