C++和lua层交互的一些设计回顾

Page content

项目中通过c++和lua的交互,将大量的业务逻辑交给了非专业的编程人员开发。结合工具的强大的导出能力,将相当大的一部分和玩法和玩法设计相关的工作,交给了技术策划和使用工具的策划。本文稍微回顾下这种设计,以及它实际上和以往c++调用脚本做事情非常不一样的思路。

C++和lua交互的设计思路

跨语言调用,是在某个点执行特定的脚本以实现跨语言的调用,在c++中执行脚本逻辑。这种执行的交互接口如何设计,通过怎样的接口让脚本的编写者几乎不用过多的关注上层实现。我们这里采用解耦方式是通过event。脚本层先注册每个event的执行函数,c++层发生特定的行为时产生event传递给脚本层处理,脚本层只需要关注发生了什么事情,事情的具体内容通过event参数传递过来。

[C++ logic] -- event ->  [lua function]

用事件而不是函数调用的方式,将两种语言的之间的交互耦合降到最低。事件使得接口简单且固定,这点相对传统的回调还是有比较大的优势。而且lua层只需要关注发生了什么,要做什么。实际上对为什么发生都不用过多的关注,无需知道上层的实现细节。

某些需要成对出现的event如果没有处理好,就会导致异常。以某个行为start为例,如果没有对应的stop或者finish,lua逻辑很难处理清理和结束。没有合理的清理结束,甚至会发生重入的问题。实践上lua本身属于非专业编码人员的工作,对重入的防护是无法做出很好保证,他们通常只做正确的事,对于意外的处理相当缺乏,而且实践看来,这里无法要求过高。event还有个问题,如果玩法以来特定的event,这个event如果无法触发,玩法会卡住。甚至如果脚本设置了存档和淘汰相关的逻辑,导致特定的事件到来时,已经无法传递给脚本了。脚本下次加载时,可能再也无法获得对应的event了。只能经由其他event,然后执行状态检查,检查某个重要的event是否发生了,因为event丢失,这里应该对应的是该event带来的特定状态。

回调的相对好处是,只要c++业务层保证各时点都能执行,完整的时点接口提供健壮的 onStart / onStop 保证 lua层业务的初始化和清理得以正常。如果事情变成event,就需要lua层关注每个event的配套,一旦漏写,或者不知道event具体的产生意味着什么,就会出问题。回调会有更强烈的语义,用错这点上会好一点。当然回调也无法规避很多问题,这里不展开。

值得注意这里的lua,实际上只是在c++上套一层壳。只是使用lua逻辑组合了c++层提供的lua接口。其完整的交互流程为:

[C++ LOGIC] -- EVENT -> lua script 
						[EVENT 1] LUA function1   -> call sets of c++ register interface  -> [C++ FUNCTION]
						[EVENT 2] LUA function2
						[EVENT 3] LUA function3

事实上整个lua就变成了各种事件驱动的回调函数。event最终执行的还是c++代码,c++代码本身可能是公共的业务接口,又有可能再次触发event。甚至多个event的回调可能会增删某些数据,会导致各种问题。而且经历了event特别是跨语言的多层调用,实际上很难完全控制。迭代器失效以及死循环都是比较麻烦的事情。event会随着项目和玩法的迭代快速的扩张,这种问题又会变得无法避免和需要很小心的控制。

写在最后

目前通过c++层调用lua脚本做了相当大的一部分业务逻辑,解放了后台开发。不过设计上也确实存在不少问题。一个核心原因是event本身不完备,核心玩法其实比较需要完备的event,如开始和结束。但是玩法层本身无法保证一定有结束。会带来一些问题。这种无法保证可以在实现上尽量避免。我们一旦对event做出了更多的承诺,lua层的逻辑出问题的概率就少很多。event做出承诺,本身和玩法或者游戏本身的特点有关,也是需要权衡。

还有最大的一个问题是,c++ -> lua -> c++ 这样的三层调用,还有可能会再次产生event,甚至有可能产生递归死循环。这种间接的调用随着系统和业务规模的复杂化,会无法避免的导致一些问题。接口实现上需要控制的足够小心,才能降低问题的概率。相对风险来说,目前看收益更大。实际上出现风险lua还可以热更掉,也并非很可怕的事情。

本文主要是回顾下这种设计,因为业务的敏感性,也不展开了。主要是这种思路还是有不少启发。最近有点忙,强迫自己写点分享,后面再修整下。此刻主要是介绍一些思路,过于潦草。