实现LUA脚本同步处理事件:LUA的coroutine

需求

    受WOW的影响,LUA越来越多地被应用于游戏中。脚本被用于游戏中主要用于策划编写游戏规则相关。实际运用中,
我们会将很多宿主语言函数绑定到LUA脚本中,使脚本可以更多地控制程序运行。例如我们可以绑定NPCDialog之类的函数
到LUA中,然后策划便可以在脚本里控制游戏中弹出的NPC对话框。
    我们现在面临这样的需求:对于宿主程序而言,某些功能是不能阻塞程序逻辑的(对于游戏程序尤其如此),但是为
了方便策划,我们又需要让脚本看起来被阻塞了。用NPCDialog举个例子,在脚本中有如下代码 :

    ret = NPCDialog( " Hello bitch " )
   
if ret == OK then print( " OK " ) end


    对于策划而言,NPCDialog应该是阻塞的,除非玩家操作此对话框,点击OK或者关闭,不然该函数不会返回。而对于
宿主程序C++而言,我们如何实现这个函数呢:

 

 

    static   int do_npc_dialog( lua_State * L )
   
{
       
const char *content = lua_tostring( L, -1 );
       
        lua_pushnumber( ret );
       
return 1;
    }


    显然,该函数不能阻塞,否则它会阻塞整个游戏线程,这对于服务器而言是不可行的。但是如果该函数立即返回,那
么它并没有收集到玩家对于那个对话框的操作。
    综上,我们要做的是,让脚本感觉某个操作阻塞,但事实上宿主程序并没有阻塞。

 

事件机制

    一个最简单的实现(对于C程序员而言也许也是优美的),就是使用事件机制。我们将对话框的操作结果作为一个事件。
脚本里事实上没有哪个函数是阻塞的。为了处理一些“阻塞”函数的处理结果,脚本向宿主程序注册事件处理器(同GUI事件
处理其实是一样的),例如脚本可以这样:

    function onEvent( ret )
       
if ret == OK then print( " OK " ) end
    end
   
-- register event handler
    SetEventHandler(
" onEvent " )
    NPCDialog(
" Hello bitch " )


    宿主程序保存事件处理器onEvent函数名,当玩家操作了对话框后,宿主程序回调脚本中的onEvent,完成操作。
    事实上我相信有很多人确实是这么做的。这样做其实就是把一个顺序执行的代码流,分成了很多块。但是对于sleep
这样的脚本调用呢?例如:

 

 

    -- do job A
    sleep(
10 )
   
-- do job B
    sleep(
10 )
   
-- do job C
   


    那么采用事件机制将可能会把代码分解为:

    function onJobA
       
-- do job A
        SetEventHandlerB(
" onJobB " )
        sleep(
10 )
    end
    function onJobB
       
-- do job B
        SetEventHandlerC(
" onJobC " )
    end
    function onJobC
       
-- do job C
    end
   
-- script starts here
    SetEventHandlerA(
" onJobA " )
    sleep(
10 )


    代码看起来似乎有点难看了,最重要的是它不易编写,策划估计会抓狂的。我想,对于非专业程序员而言,程序的
顺序执行可能理解起来更为容易。

 

SOLVE IT

    我们的解决方案,其实只有一句话:当脚本执行到阻塞操作时(如NPCDialog),挂起脚本,当宿主程序某个操作完
成时,让脚本从之前的挂起点继续执行。
    这不是一种假想的功能。我在刚开始实现这个功能之前,以为LUA不支持这个功能。我臆想着如下的操作:
    脚本:
    ret = NPCDialog("Hello bitch")
    if ret == 0 then print("OK") end
    宿主程序:

    static   int do_npc_dialog( lua_State * L )
   
{
       
        lua_suspend_script( L );
       
    }


    某个地方某个操作完成了:
    lua_resume_script( L );
    当我实现了这个功能后,我猛然发现,实际情况和我这里想的差不多(有点汗颜)。

 


认识Coroutine

    coroutine是LUA中类似线程的东西,但是它其实和fiber更相似。也就是说,它是一种非抢占式的线程,它的切换取决
于任务本身,也就是取决你,你决定它们什么时候发生切换。建议你阅读lua manual了解更多。
    coroutine支持的典型操作有:lua_yield, lua_resume,也就是我们需要的挂起和继续执行。
    lua_State似乎就是一个coroutine,或者按照LUA文档中的另一种说法,就是一个thread。我这里之所以用’似乎‘是
因为我自己也无法确定,我只能说,lua_State看起来就是一个coroutine。
    LUA提供lua_newthread用于手工创建一个coroutine,然后将新创建的coroutine放置于堆栈顶,如同其他new出来的
对象一样。网上有帖子说lua_newthread创建的东西与脚本里调用coroutine.create创建出来的东西不一样,但是根据我
的观察来看,他们是一样的。lua_newthread返回一个lua_State对象,所以从这里可以看出,“lua_State看起来就是一个
coroutine”。另外,网上也有人说创建新的coroutine代价很大,但是,一个lua_State的代价能有多大?当然,我没做过
测试,不敢多言。
    lua_yield用于挂起一个coroutine,不过该函数只能用于coroutine内部,看看它的参数就知道了。
    lua_resume用于启动一个coroutine,它可以用于coroutine没有运行时启动之,也可以用于coroutine挂起时重新启动
之。lua_resume在两种情况下返回:coroutine挂起或者执行完毕,否则lua_resume不返回。
    lua_yield和lua_resume对应于脚本函数:coroutine.yield和coroutine.resume,建议你写写脚本程序感受下coroutine,
例如:

    function main()
        print(
" main start " )
        coroutine.yield()
        print(
" main end " )
    end
    co
= coroutine.create( main );
    coroutine.resume(co)


REALLY SOLVE IT

 

    你可能会想到,我们为脚本定义一个main,然后在宿主程序里lua_newthread创建一个coroutine,然后将main放进去,
当脚本调用宿主程序的某个’阻塞‘操作时,宿主程序获取到之前创建的coroutine,然后yield之。当操作完成时,再resume
之。
    事实上方法是对的,但是没有必要再创建一个coroutine。如之前所说,一个lua_State看上去就是一个coroutine,
而恰好,我们始终都会有一个lua_State。感觉上,这个lua_State就像是main coroutine。(就像你的主线程)
    思路就是这样,因为具体实现时,还是有些问题,所以我罗列每个步骤的代码。
    初始lua_State时如你平时所做:

    lua_State * L = lua_open();
    luaopen_base( L );


    注册脚本需要的宿主程序函数到L里:

    lua_pushcfunction( L, sleep );
    lua_setglobal( L,
" my_sleep " );


    载入脚本文件并执行时稍微有点不同:

    luaL_loadfile( L, " test.lua " );
lua_resume( L,
0 ); /* 调用resume */


    在你的’阻塞‘函数里需要挂起coroutine:

    return lua_yield( L, 0 );


    注意,lua_yield函数非常特别,它必须作为return语句被调用,否则会调用失败,具体原因我也不清楚。而在这里,
它作为lua_CFunction的返回值,会不会引发错误?因为lua_CFunction约定返回值为该函数对于脚本而言的返回值个数。
实际情况是,我看到的一些例子里都这样安排lua_yield,所以i do what they do。

 

    在这个操作完成后(如玩家操作了那个对话框),宿主程序需要唤醒coroutine:

    lua_resume( L, 0 );

 

 

    大致步骤就这些。如果你要单独创建新的lua_State,反而会搞得很麻烦,我开始就是那样的做的,总是实现不了自己
预想中的效果。

 

8.13补充

   可能有时候,我们提供给脚本的函数需要返回一些值给脚本,例如NPCDialog返回操作结果,我们只需要在宿主程序里lua_resume

之前push返回值即可,当然,需要设置lua_resume第二个参数为返回值个数。

2.9.2010
    lua_yield( L, nResults )第二个参数指定返回给lua_resume的值个数。如下:

   lua_pushnumber( L,  3  );
   
return  lua_yield( L,  1  );
 ..
   
int  ret  =  lua_resume( L,  0  );
   
if ( ret  ==  LUA_YIELD )
   
{
         lua_Number r 
= luaL_checknumber( L, -1 );
   }

你可能感兴趣的:(LUA)