Skip to content

3.5Lua虚拟机

CGGG edited this page Sep 10, 2021 · 4 revisions

概述

本项目的游戏流程逻辑实现使用lua语言编写,使用xLua来实现lua和C#原生代码之间的相互调用。

游戏流程逻辑指如剧情、触发器管理、过场演出、调用战斗等等功能。

使用lua的好处在于不需要懂复杂的语法和项目的底层知识,即可对游戏进行开发和修改。面向游戏策划友好。

本项目所有的lua文件提取自DOS版《金庸群侠传》,区别在于原版使用字节码的方式,使用由网友godka提供的工具自动转义为含逻辑的lua文件

lua文件编写方式

lua调用游戏中的内容,均通过C#提供指令函数来实现。一个典型的lua实现样例(jyx2/data/lua/jygame/ka11.lua)如下:

Talk(1, "有什么要我帮忙的,尽管说出来.", "talkname1", 0);
if AskJoin () == true then goto label0 end;
    do return end;
::label0::
    Talk(0, "胡大哥肯随我闯荡江湖否?", "talkname0", 1);
    if TeamIsFull() == false then goto label1 end;
        Talk(1, "你的队伍已满,我无法加入.", "talkname1", 0);
        do return end;
::label1::
        Talk(1, "好.我就随你一走.", "talkname1", 0);
        Talk(0, "胡大哥肯随我闯荡江湖帮这个忙,那再好也不过了.", "talkname0", 1);
        DarkScence();
        ModifyEvent(-2, -2, 0, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2);
        jyx2_ReplaceSceneObject("","NPC/胡斐","");
        LightScence();
        Join(1);
        AddEthics(1);
do return end;

具体使用方法已经在 这里说明,在此不再赘述。

Lua指令的实现

所有指令均在 Jyx2LuaBridge.cs中定义,每个static函数为一个指令。
xlua的机制保证在editor模式下如果没有生成Lua Wrap,则用反射的方式调用。如果有生成Lua Wrap,则用原生代码调用的方式。

Lua调用中的线程关系

Lua虚拟机在项目第一个场景启动前被初始化:BeforeSceneLoad.ColdBind()中的LuaManager.Init()方法

此初始化将载入main.lua文件(在lua虚拟机中注册所有的C#函数编写的指令)

在游戏过程中,所有的调用均通过LuaExecutor.csExecuteLua()函数调用。

        public static void ExecuteLua(string luaContent, Action callback = null, string path = "")
        {

            //BY CG:JYX2的特殊情况,有空文件
            if (luaContent.Equals("do return end;"))
            {
                //Debug.Log("识别到空的lua文件,直接跳过:" + path);
                callback?.Invoke();
                return;
            }

            var luaEnv = LuaManager.GetLuaEnv() as LuaEnv;

            Debug.Log("执行lua: " + path);

            _executing = true;
            Loom.RunAsync(() =>
            {
                try
                {
                    luaEnv.DoString(luaContent);
                    Debug.Log("lua执行完毕: " + path);
                }
                catch (Exception e)
                {
                    Debug.LogError("lua执行错误:" + e.ToString());
                    Debug.LogError(e.StackTrace);
                }

                currentLuaContext = null;
                _executing = false;
                if (callback != null)
                {
                    Loom.QueueOnMainThread(_ => { callback(); }, null);
                }
            });
        }

注意:

  • 这里每次调用都使用了一个异步方法Loom.RunAsync来让lua执行在线程池里
  • 使用Loom.QueueOnMainThread来将执行完毕的回调在UI线程(主线程)运行

这样做的原因是;我们在执行lua的过程中,可能会反复调用进行UI展示(比如呼出对话框、选择框等),而lua逻辑实际是需要阻塞在“呼出”逻辑的那一行。
所以需要有一个线程将lua的执行“挂起”。

而lua执行完毕的回调需要在主线程的原因是,后续的C#代码可能需要立刻访问UI元素,在Unity的体系下,如果不在主线程访问UI元素则会报错。

为了在C#实现的lua指令中实现“挂起”的功能,我们需要用到多线程编程的信号量的概念。一个典型的例子如下

static public void ShowEthics()
{
    RunInMainThrad(() => {
        MessageBox.Create("你的道德指数为" + runtime.Player.Pinde, Next);
    });
    Wait();
}

这里我们封装了Wait()Next()方法,可以进一步查看其实现:

        static Semaphore sema = new Semaphore(0, 1);

        static private void Wait()
        {
            sema.WaitOne();
        }

        static private void Next()
        {
            sema.Release();
        }
补充说明:xlua的特性亦提供util.async_to_sync的方法,来将一个带回调的C#函数封装为一个可以同步方式来调用的lua函数。
如果有兴趣可以自行调整代码。