Hello Lua

在游戏开发中,Lua是一种比较热门的轻量级脚本语言。由C实现的Lua可以很好得和C/C++进行交互,同时它可以很方便地和和各种语言结合进行跨平台开发。
特别是在当前最流行的游戏Unity中,为了实现C#脚本热更新,Lua几乎是唯一的解决方案,各种基于Lua的库层出不穷。一直在写C#脚本,还是有必要学习Lua。

###Lua初见

一开始就直接打开Lua官方的Reference Manual
Lua Reference Manual

接下来下载好Lua的binary版本,开始学习基本的语法
Lua 基础教程

总体感觉Lua非常的轻巧
基础的数据类型也只有nil boolean number string function userdata thread table (整形和浮点都是number 想起了SQLite)
动态类型语言和Javascript一样
结尾不需要;和Python一样
函数可以是多返回值
没有自带的++ +=等运算符,写的时候特别难受…T_T

但是有metatable可以进行任意的扩展,这个metatable给我的感觉就像是JS中的prototype

###定个小目标

在熟悉了基本的语法和运算后,准备试试Lua的热更新。之前在github发现了一个GUI库imgui,imgui是一个immediate mode gui库。什么是immediate mode gui呢,就是和通常的事件回调式的GUI框架不同的,所有的UI逻辑是线性的,和Unity中的EditorGUI一样,需要不断的刷新。大概就是下面代码的样子

1
2
3
4
5
gui.Label("This is a label");
if(gui.button("This is a button"))
{
//process button click
}

工作中一直在写一些Unity的插件及编辑器脚本,挺喜欢IMGUI这种快速代码实现UI的模式,但是也是有缺点的,比如UI逻辑太复杂时候,有可能会影响绘制帧率造成卡顿。或者是太复杂的UI结构代码的可读性就大大降低。
现在我们有了Lua 那么我们是不是可以用IMGUI绘制一个文本编辑器,然后在里面编写Lua脚本,通过Lua调用 C++的imgui的方法来更新UI呢。
这样就像写Unity的Editor扩展一样,但是不需要重新编译整个项目,加载不同的Lua脚本就能实现热更新UI。

总结起来我们需要做一下几步

  • 在windows下开发 windows程序,调用imgui C++库
  • 配置Lua环境 执行Lua脚本
  • 编写Lua调用imgui的C函数,互调用操作
  • 在imgui中刷新Lua脚本

开始实现

创建工程
imgui在github上的repo有demo 我们直接拿来改就行了。选了用opengl进行渲染,而不是dx。毕竟有GLFW GLEW比DX的初始化快多了。

配置Lua
把几个Lua头文件的目录加入Include

  • Lua.h lualib.h 定义了Lua的基本方法
  • lauxlib.h 里面有一些比较常用的扩展方法
  • luaconf.h 用来控制模块的开关

Lib就只有一个lua53.lib

Lua和C++的互调用
处理Lua与C++的互调用都需要通过Lua C Api来进行

在Lua5.3中 需要维护一个lua_State 来进行互操作
Lua调用C++需要先定义对应的方法,所有方法都必须带有lua_State的参数,因为Lua和C交互是通过维护一个栈来实现的。Lua调用方法时会先把方法和参数一个个push入栈,对应的C方法会pop出参数执行完再把结果压入栈返回给Lua的脚本

1
2
3
4
5
6
7
8
9
10
11
12
//声明方法
static void luaTestFunc(lua_State *state)
{
//some implement
}

//初始化lua_State
lua_State * lstate = luaL_newstate();
//加载lua基础库
luaL_oepnlibs(lstate);
//注册方法
lua_register(lstate,"luaTestFunc",luaTestFunc);

接下来就可以直接在Lua中调用luaTestFunc()

github上有imgui lua的wrap 我们可以直接用,就不需声明那么多方法了

C++调用Lua方法
使用luaL_loadstring(lua_State*,const char * luacode) 来加载代码
加载完成后我们要把需要调用的方法push到栈顶,然后调用lua_call() 就可以执行了。需要注意的是调用了lua_call 之后我们使用lua_getglobal() push到栈顶的方法会被pop出栈。在imgui中每一帧循环都要调用imgui的绘制方法,所以如果要重复调用一个方法就需要在调用前讲其push到栈顶(不知道有没有其他的方法可以实现)。不这样操作可能会报错就会出现如

1
2
3
lua syntax error:attempt to call a nil value
lua syntax error:attempt to call a string value
lua syntax error:attempt to call a string value

这样的错误其实是调用lua_call 发现在栈顶的不是函数先报错 nil value,然后lua会把这个error string push到栈顶 所以下次调用call就会返回string value,无限循环下去…

更多的Lua C++的互调用可以参考
Lua C++互调用

** 重新加载编写好的Lua脚本 **
当我们更新了lua脚本之后需要重新加载,首先调用 lua_close() 回收lua_State,接着就是重新加载lua基本库和imgui的库。这一步加载库是比较耗时间的,不知道有没有其他的方式可以动态卸载掉加载的代码块,再重新加载Lua脚本。

###总结
Lua的栈操作还是不太习惯,总体感觉Lua是一个很简洁易用的脚本语言。模块管理,基本库,metadata等特性还需要再深入接触一下。
在调试互调用的时候还是踩了很多坑,需要好好了解下Lua C Api。基于Lua的imgui UI扩展还会继续完善完善。