Unity 通用框架搭建(十)——使用Xlua热更新

本文详细介绍了如何在Unity游戏中使用腾讯的XLua框架进行热更新。内容涵盖Lua代码加载、C#与Lua交互、XLua配置以及在项目中的具体应用,展示了如何在不重启应用的情况下更新代码逻辑,提升开发效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

热更新,是游戏开发中十分重要的一个环节。热更新,指用户不重启应用的前提下能实现代码逻辑的更新。因此,本文主要介绍腾讯开源的热更新方案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表的映射:

  1. 映射到普通的class或struct。这种映射方式属于值拷贝,因此Lua或C#中对值进行改变相互不会有影响
  2. 映射到接口interface。这种方式需要生产依赖代码,属于引用拷贝,Lua或C#中对值的修改会相互影响
  3. 对于轻量级的table表可以直接映射为Dictionary<>或者List<>
  4. 直接映射到LuaTable类。这种方式不需要生成代码,但是比接口映射的方式慢一个数量级,没有类型检查

Lua中function的映射

  1. 映射到delegate,官方建议采用这种方式,性能好,而且类型安全,但是要生成代码。对于多返回值的,用out 或者 ref参数接收
  2. 映射到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的配置
  1. 打标签。
    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的开发方式。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值