loop_in_codes

低调做技术__欢迎移步我的独立博客 codemaro.com 微博 kevinlynx

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

author : Kevin Lynx

需求

    受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,反而会搞得很麻烦,我开始就是那样的做的,总是实现不了自己
预想中的效果。

相关下载:
    例子程序中,我给了一个sleep实现。脚本程序调用sleep时将被挂起,宿主程序不断检查当前时间,当时间到时,resume
挂起的coroutine。下载例子

 

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 );
   }

posted on 2008-08-12 16:02 Kevin Lynx 阅读(12795) 评论(14)  编辑 收藏 引用 所属分类: 通用编程lua

评论

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2008-08-12 20:47 陈梓瀚(vczh)

新的脚本都能够在非外部函数执行过程的的任意时间暂停并保留现场,LUA应该有吧?至少我自己做的那个是有的。  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2008-08-13 09:00 Kevin Lynx

@陈梓瀚(vczh)
就我所查阅的文档来看,似乎没有。coroutine.yiled/resume可能算是吧。  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2008-08-13 15:54 sirius.gnu@gmail.com

通过把触发标签和回掉函数捆绑(比如游戏中的按钮和点击按钮的回掉函数封装成一个table),感觉上要比用协程清晰,因为如果没有回掉,策划极可能把一个函数写过1k行。  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2008-08-13 23:03 大日如来

回调是逻辑上最清晰的一种办法了,协程不应该用在这个地方。  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2008-08-14 09:06 Kevin Lynx

看来很多人都偏向于回调啊。我刚开始也打算用回调,但是leader说这样很麻烦。我们原有的脚本系统就是采用挂起的方式。如果采用回调,那么对于sleep这样的操作你们是怎么做的?
  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2009-02-09 12:10 lilo

感谢博主,这篇文章写的很详细,解决了我困扰很久的问题。  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2009-05-20 14:22 wuditom100

似乎解决了困惑我几天的疑惑,以前执行脚本时老是阻塞掉宿主程序  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2009-06-27 22:22 owlcn

看了博主的sleep示例,有个疑问,请问这个test.lua是可重入的么,如果c的主循环里又调用了test.lua,会不会造成异常呢?  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2009-06-28 18:24 Kevin Lynx

@owlcn
应该不支持重入。因为都是对同一个lua_State操作。   回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2010-08-23 16:36 krezip

标准lua是不支持从lua外部resume的,不知道博主现在怎么解决这个问题的,我这两天也为这个所困扰  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2011-03-19 22:24 caphone_wang

sleep 用coroutine  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2011-08-24 18:43 xybz

c++的程序员看timerEvent才会非常高兴;而且进而给你用STL抽象一个eventor出来:D  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2014-08-26 07:38 xiaolong

直接在lua里封装协程的启动和关闭 C++只是负责启动和改变参数 lua相应不就行了  回复  更多评论   

# re: 实现LUA脚本同步处理事件:LUA的coroutine 2015-05-12 21:12 gxy0rita

博主你好,看到这篇文章我很受启发。我正在构思一个游戏对话系统,希望实现的效果是脚本每执行一条“talk”语句,就能停下来等待用户点击后再执行下一句
本文介绍的方法应该适用于这个情景吧?  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理