Introducci贸n a la integraci贸n de scripts en un motor de juegos
Integraci贸n con Lua
Lua es un lenguaje de programaci贸n originalmente dise帽ado para extender aplicaciones, pero tambi茅n es utilizado frecuentemente como un sistema de scripts multiprop贸sito y de modo stand-alone. Combina una sintaxis procedural con tipos de datos poderosos como arrays asociativos. Es interpretado a partir de bytecodes y posee gesti贸n autom谩tica de memoria. Se encuentra implementado a partir de una peque帽a librer铆a escrita en lenguaje C y compila sin modificaciones en cualquier plataforma conocida.
Ha sido utilizado en juegos como Battle Mages, Vendetta, FarCry, Homeworld2 y PainKiller.
1. Ejecutar un script desde C++
Como con todo API de integraci贸n habr谩 un archivo cabecera (uno o m谩s) y una librer铆a (una o m谩s) que deberemos utilizar. En este caso los archivos cabecera son:
[c贸digo C++]
| extern "C"
| {
| #include <lua.h>
| #include <lauxlib.h>
| #include <lualib.h>
| }
Y las librer铆as que debemos enlazar son dos:
[c贸digo C++]
| #if _DEBUG
| #pragma comment(lib, "../lib/luad.lib")
| #pragma comment(lib, "../lib/lualibd.lib")
| #else
| #pragma comment(lib, "../lib/lua.lib")
| #pragma comment(lib, "../lib/lualib.lib")
| #endif
Ahora, veamos el primer ejemplo. La idea es ejecutar el siguiente script Lua:
[c贸digo Lua]
| io.write("Mensaje de texto\n");
El script en cuesti贸n deber谩 escribir un texto cualquier en pantalla. Veremos que c贸digo C++ nos permite ejecutar dichas l铆neas en Lua:
[c贸digo C++]
| int main(int argc, char* argv[])
| {
| // Abro el int茅rprete
| lua_State * pState = lua_open();
|
| // Cargo la librer铆a en entrada/salida
| luaopen_io(pState);
|
| // Cargo el archivo de script
| luaL_loadfile(pState, "../../scripts/script1.lua");
|
| // Ejecuto el script
| int iError = lua_pcall(pState, 0, 0, 0);
|
| // Si existe un error lo informo
| if (iError)
| {
| cout << lua_tostring(pState, -1) << endl;
| lua_pop(pState, 1);
| }
|
| // Cierro el int茅rprete
| lua_close(pState);
|
| return 0;
| }
Veamos cuales son las funciones m谩s importantes que utilizamos:
lua_open() : Abre el int茅rprete. Este paso es necesario para realizar cualquier operaci贸n con scripts. Nota que retorna un puntero al estilo fopen, que luego deber谩 ser utilizado para pasarle a otras funciones del API.
lua_close() : Cierra el int茅rprete.
luaopen_io() : Abre la librer铆a en entrada/salida. Este paso es necesario debido a que hacemos uso de esta librer铆a en Lua. M谩s adelante requeriremos utilizar otras librer铆as, tambi茅n podr铆amos no haber utilizado ninguna y esta funci贸n no habr铆a sido necesario invocar.
luaL_loadfile() : Todas las funciones que poseen una “L” may煤scula luego del prefijo “lua” pertenecen a la librer铆a auxiliar del Lua, el paquete de funciones que acompa帽a esta librer铆a son de alto nivel y no forman parte del n煤cleo del motor de script. En este caso, esta funci贸n cargar谩 un script lua (pero no lo ejecutar谩).
lua_pcall() : Ejecuta un script lua cargado por el motor. Es posible transferir argumentos a la invocaci贸n.
El valor de retorno de funciones como luaL_loadfile, lua_pcall y otras tantas es un n煤mero entero que representa un c贸digo de error. Cuando es cero significa que la operaci贸n se ha concretado de manera exitosa. Si miramos el c贸digo que hemos escrito, cuando el valor de retorno de lua_pcall es distinto a cero escribo en el stdout un string retornado por la funci贸n lua_tostring y luego realizo un lua_pop, esto tiene que ver con el modo de funcionamiento del pasaje de datos entre C/C++ y el motor de script Lua.
2. Ejecutar una funci贸n de un script desde C++ pasando un par谩metro
Antes de pasar al paso 2, introduciremos un ejemplo intermedio que ser谩 ver como realizar un pasaje de datos a un script Lua sin utilizar una funci贸n 驴c贸mo es eso? Como comentamos en el p谩rrafo anterior, el pasaje de datos entre C/C++ y Lua posee una particularidad, Lua define una pila virtual en la cual se pueden insertar y extraer datos (y no necesariamente del modo tradicional en que se maneja una pila). Para la manipulaci贸n de esta pila existen funciones espec铆ficas de Lua como lua_push, lua_pop y muchas otras. Esta pila es utilizada para la comunicaci贸n C++ hacia Lua y viceversa. Veamos:
Escribiremos el c贸digo Lua:
[c贸digo Lua]
| for i=0,table.getn(arg) do
| print(i,arg[i])
| end
En este c贸digo solicitamos la tabla “arg” al sistema y navegamos la misma imprimiendo su contenido. Veamos entonces como crear e ingresar datos en la tabla “arg”:
[c贸digo C++]
| int main(int argc, char* argv[])
| {
| // Abro el int茅rprete
| lua_State * pState = lua_open();
|
| // Cargo la librer铆a en entrada/salida
| openstdlibs(pState);
| // Cargo el archivo de script
| luaL_loadfile(pState, "../../scripts/script2.lua");
|
| // start array structure
| lua_newtable(pState);
|
| // set first element "0" to value 45
| lua_pushnumber(pState, 0 );
| lua_pushnumber(pState, 45 );
| lua_rawset(pState, -3 );
|
| // set second element "1" to value 99
| lua_pushnumber(pState, 1 );
| lua_pushnumber(pState, 99 );
| lua_rawset(pState, -3 );
| // set the number of elements minus 1 (index to the last array element)
| lua_pushliteral(pState, "n" );
| lua_pushnumber(pState, 1 );
| lua_rawset(pState, -3 );
|
| // set the name of the array that the script will access
| lua_setglobal(pState, "arg" );
|
| // Ejecuto el script
| int iError = lua_pcall(pState, 0, LUA_MULTRET, 0);
|
| // Si existe un error lo informo
| if (iError)
| {
| cout << lua_tostring(pState, -1) << endl;
| lua_pop(pState, 1);
| }
|
| // Cierro el int茅rprete
| lua_close(pState);
|
| return 0;
| }
Analicemos el c贸digo anterior:
[c贸digo C++]
| lua_newtable(pState);
La funci贸n crea una tabla nueva.
[c贸digo C++]
| lua_pushnumber(pState, 0 );
| lua_pushnumber(pState, 45 );
| lua_rawset(pState, -3 );
A continuaci贸n insertamos informaci贸n en la tabla, para esto primero insertamos el n煤mero 铆ndice, luego el contenido y finalmente le indicamos que tome estos n煤meros y lo inserte en la tabla. El n煤mero negativo 3 tiene relaci贸n a que estamos referenciando la primer posici贸n de la pila de manera relativa desde la posici贸n actual de inserci贸n (el hecho que el n煤mero sea negativo tiene que ver con esto, los n煤meros positivos referencian posiciones absolutas). Es decir, estamos posicionados en el tercer elemento, la indicaci贸n de un -3 referencia a tres posiciones hacia atr谩s, al primer elemento.
Nota: Es importante conocer como gestionar la pila mediante m茅todos del API que ofrece Lua, para esto es recomendable leer el siguiente cap铆tulo del libro “Lua 5.0 Reference Manual” que es posible acceder mediante el siguiente enlace: http://lua-users.org/wiki/FunctionsTutorial
[c贸digo C++]
| lua_setglobal(pState, "arg" );
Finalmente, fijamos el nombre a la tabla.
Luego, la invocaci贸n al script se realiza del modo usual como ya hemos visto en el paso 1.
3. Ejecutar una funci贸n C++ desde un script
Para poder ejecutar una funci贸n C++ desde Lua deberemos, as铆 como hemos hecho con Python, registrar una funci贸n tipo proxy para que la misma sea visible desde el script. La raz贸n por la cual deberemos utilizar una funci贸n tipo proxy y no podamos llamar directamente a la funci贸n de inter茅s tiene que ver con un prototipo espec铆fico que debe poseer toda funci贸n a registrar para que pueda ser llamada desde Lua. El prototipo en cuesti贸n es el siguiente:
[c贸digo C++]
| typedef int (*lua_CFunction) (lua_State *L);
Por ejemplo, si deseamos poder registrar una funci贸n llamada “proxy_a_foo” deberemos escribir:
[c贸digo C++]
| int proxy_a_foo(lua_State *pState)
| {
| // ...
| }
Como par谩metro la funci贸n recibir谩 el descriptor del int茅rprete abierto por medio de la funci贸n lua_open que utilizaremos para extraer los par谩metros a la funci贸n pasados haciendo uso de la pila.
Para registrar nuestra funci贸n haremos uso de una conveniente y popular macro:
[c贸digo C++]
| #define lua_register(L,n,f) (lua_pushstring(L, n), lua_pushcfunction(L, f), lua_settable(L, LUA_GLOBALSINDEX))
La macro recibe como primer par谩metro el descriptor del int茅rprete (como casi toda funci贸n lua), el segundo par谩metro es el nombre simb贸lico que se utilizar谩 desde lua para acceder a la funci贸n proxy y el tercer par谩metro es el puntero a la funci贸n proxy a invocar.
[c贸digo C++]
| lua_register(pState, "foo", proxy_a_foo);
Veamos el c贸digo de script lua que har谩 uso de nuestra funci贸n:
[c贸digo Lua]
| foo()
Ahora veamos el programa completo en C++:
[c贸digo C++]
| // Funci贸n que deseo invocar en C++
| void foo()
| {
| cout << "Mensajes desde C++" << endl;
| }
|
| // Funci贸n proxy a foo
| int proxy_a_foo(lua_State *pState)
| {
| foo();
| return 0;
| }
|
| int main(int argc, char* argv[])
| {
| // Abro el int茅rprete
| lua_State * pState = lua_open();
|
| // registro la funci贸n
| lua_register(pState, "lafunc", proxy_a_foo);
|
| // Cargo el archivo de script
| luaL_loadfile(pState, "../../scripts/script4.lua");
|
| // Ejecuto el script
| int iError = lua_pcall(pState, 0, 0, 0);
|
| // Si existe un error lo informo
| if (iError)
| {
| cout << lua_tostring(pState, -1) << endl;
| lua_pop(pState, 1);
| }
|
| // Cierro el int茅rprete
| lua_close(pState);
|
| return 0;
| }
Ahora, agreguemos el pasaje de un par谩metro, desde lua ahora escribiremos:
foo(5)
Y en C++, nuestra funci贸n proxy ahora deber谩 extraer el par谩metro desde la pila:
[c贸digo C++]
| // Funci贸n proxy a foo
| int proxy_a_foo(lua_State *pState)
| {
| int iValor = lua_tonumber(pState, -1);
| lua_pop(pState, 1);
| foo(iValor);
|
| return 0;
| }
Como sabemos que el valor a extraer se encuentra en el extremo superior de la pila y adem谩s sabemos que el par谩metro es num茅rico, entonces tomamos dicho valor haciendo uso de la funci贸n lua_tonumber. Adem谩s, como es nuestro deber dejar la pila limpia de todo par谩metro, extraemos el mismo utilizando la funci贸n lua_pop.
De manera an谩loga podremos realizar pasajes de par谩metros de cualquier tipo.
4. Ejecutar el m茅todo de una clase desde un script
El modo m谩s simple de acceder a objetos desde Lua es utilizando una metodolog铆a similar a la que hemos utilizado en Python, es decir, utilizando una referencia expl铆cita al objeto. Sin embargo, es posible mediante la escritura de m谩s c贸digo escribir una clase como si fuese una tabla Lua de modo incluya propiedades y funciones, m谩s tarde haciendo uso de ToLua veremos como acceder a m茅todos de clases de este modo.
Por ahora, podremos pasar al script Lua una tabla con la referencia al objeto que deseamos utilizar desde el script. Tambi茅n, introduciremos en nuestra clase m茅todos est谩ticos (que al no poseer puntero this son como funciones globales) que oficien de funciones proxy a los m茅todos reales. Veamos:
[c贸digo C++]
| class ClaseFoo
| {
| short m_sValor;
| public:
| bool SetValor(short sValor);
| short GetValor();
|
| // Funciones proxy para acceso a m茅todos desde Lua
| static int ls_SetValor(lua_State *pState);
| static int ls_GetValor(lua_State *pState);
| };
Notemos que los m茅todos est谩ticos cumplen con el prototipo solicitado por Lua para registrar funciones.
El contenido de la funci贸n ls_SetValor ser谩:
[c贸digo C++]
| int ClaseFoo::ls_SetValor(lua_State *pState)
| {
| int iValor = lua_tonumber(pState, -1);
| lua_pop(pState, 1);
|
| ClaseFoo * pObj = (ClaseFoo *) lua_touserdata(pState, -1);
| lua_pop(pState, 1);
|
| pObj->SetValor(iValor);
|
| return 0;
| }
Como podemos apreciar la idea es recuperar el primer valor recibido del stack como el valor a utilizar como par谩metro en el llamado al m茅todo, y el segundo valor obtenido del stack es el puntero al objeto que casteamos para luego poder realizar la invocaci贸n.
El c贸digo del cuerpo principal del programa es:
[c贸digo C++]
| int main(int argc, char* argv[])
| {
|
| // Abro el int茅rprete
| lua_State * pState = lua_open();
|
| // Cargo la librer铆a en entrada/salida
| openstdlibs(pState);
|
| lua_register(pState, "clasefoo_setvalor", ClaseFoo::ls_SetValor);
| lua_register(pState, "clasefoo_getvalor", ClaseFoo::ls_GetValor);
|
| // Cargo el archivo de script
| luaL_loadfile(pState, "../../scripts/script5.lua");
|
| // Creo una instancia de ClaseFoo
| ClaseFoo obj;
|
| // Ahora paso el puntero al objeto al script por medio del stack
| lua_newtable(pState);
|
| lua_pushnumber(pState, 0 );
| lua_pushlightuserdata(pState, &obj);
| lua_rawset(pState, -3 );
|
| lua_pushliteral(pState, "n" );
| lua_pushnumber(pState, 1 );
| lua_rawset(pState, -3 );
|
| lua_setglobal(pState, "obj");
|
| // Ejecuto el script
| int iError = lua_pcall(pState, 0, 0, 0);
|
| // Si existe un error lo informo
| if (iError)
| {
| cout << lua_tostring(pState, -1) << endl;
| lua_pop(pState, 1);
| }
|
| // Cierro el int茅rprete
| lua_close(pState);
|
| return 0;
| }
El c贸digo del script Lua:
[c贸digo Lua]
| -- Puntero a objeto en obj[0]
| print(obj[0])
|
| -- Invoco a funci贸n de objeto
| clasefoo_setvalor(obj[0], 5)
Al igual que con Python, no es dif铆cil notar que este acercamiento si bien es el que menos c贸digo de pegado requiere tiene sus contrariedades:
. P茅rdida de elegancia en la sintaxis
. Peligro de realizar un casting inv谩lido, fruto de un error en el script (esto podr铆amos evitarlo haciendo uso de RTTI para verificar que el cast a realizar sea v谩lido antes de invocar un m茅todo o acceder a una propiedad).





Comentarios (3)



Hehe, flor de articulo. Todavia no lo termine de leer, pero muy buen tema!
Epa! Crei que tenias abandonada la pagina pero no, estabas haciendo un super articulo. Buenisimo!
[...] Este art铆culo completa el que publiqu茅 la semana pasada y que puede acceder haciendo click aqu铆. [...]