Introducción a la integración de scripts en un motor de juegos

Integración con Python

Para poder comunicar C++ con Python deberemos hacer uso de la librería Python/C API que puede descargarse desde el sitio oficial (se encuentra dentro del paquete de instalación oficial del lenguaje).

Para esto, en primer medida deberemos incluir el archivo cabecera correspondiente:

[código C++]

| #include <python.h>

En dicho archivo cabecera se encuentra el código correspondiente al enlace con la librería utilizada por el motor de script, por lo tanto no será necesario un enlace explícito en nuestro código.

1. Ejecutar un script desde C++

Ahora, en nuestro primer programa C++/Python ejecutaremos código python embebido dentro del programa:

[código C++]

| int main(int argc, char* argv[])
| {
| Py_Initialize();
| PyRun_SimpleString("print 'El lenguaje de script Python'");
| Py_Finalize();
|
| return 0;
| }

El siguiente paso será ejecutar un archivo de script realizado en Python:

[código C++]

| int main(int argc, char* argv[])
| {
| Py_Initialize();
|
| FILE * fp = fopen("script.py", "r");
| int iLength = _filelength(_fileno(fp));
| char * pScriptBuffer = new char[iLength];
| fread(pScriptBuffer, iLength, 1, fp);
| pScriptBuffer[16] = '\0';
| fclose(fp);
|
| PyRun_SimpleString(pScriptBuffer);
|
| Py_Finalize();
|
| return 0;
| }

Dentro del script “script.py” pueden existir definidas funciones y clases sin embargo se ejecutará código global, por ejemplo:

[código Python]

| def prueba():
| print "Código perteneciente a prueba()"
| return


# ------------------------------------------
# main
# ------------------------------------------
print "lo que sea"

Ejecutando dicho script sólo se escribirá en pantalla “lo que sea”.

Hasta aquí el modo de uso es bastante sencillo, sin embargo no podemos acceder al potencial del uso del script embebido si no le podemos pasar parámetros al mismo ni obtener resultados de él.

2. Ejecutar una función de un script desde C++ pasando un parámetro

Para poder ejecutar funciones dentro de script los pasos a realizar son un poco más complejos, deberemos hacer uso de más funciones del API de integración. El siguiente código permite ejecutar una función dentro de un determinado script, pasándole parámetros y recibiendo lo que devuelva la misma.

[código C++]

| PyObject * pName, * pModule, * pDict, * pFunc, * pValue, * pArgs;
| // Inicializamos el intérprete
| Py_Initialize();
|
| // Obtenemos el nombre del script en un objeto Python
| pName = PyString_FromString("elscript");
|
| // Cargo el modulo correspondiente al script
| pModule = PyImport_Import(pName);
|
| // Si encontré el script pModule debe ser distinto de NULL
| if (pModule != NULL)
| {
| // Obtengo el diccionario del módulo
| pDict = PyModule_GetDict(pModule);
|
| // Busco la función solicitada dentro del diccionario
| pFunc = PyDict_GetItemString(pDict, "prueba");
|
| // Verifico que la función es pueda ser invocada
| if (pFunc && PyCallable_Check(pFunc))
| {
| // Creo una tupla para contener un solo valor
| pArgs = PyTuple_New(1);
|
| // Convierto un número a un entero Python
| pValue = PyInt_FromLong(10);
|
| // Seteo el valor en la tupla
| PyTuple_SetItem(pArgs, 0, pValue);
|
| // Ejecuta la función
| pValue = PyObject_CallObject(pFunc, pArgs);
|
| // Si se ejecutó correctamente…
| if (pValue != NULL)
| {
| printf("Resultado: %ld\n", PyInt_AsLong(pValue));
| Py_DECREF(pValue);
| }
| }
| Py_DECREF(pModule);
| }
| Py_DECREF(pName);
| Py_Finalize();

Veamos que es lo que hicimos:

. Inicializamos el intérprete.
. Transformamos un string que contiene el nombre del script a un objeto Python.
. Obtenemos una referencia al diccionario del script. Este diccionario no permitirá acceder a las funciones que contiene el script en cuestión.
. Buscamos la función a ejecutar dentro del diccionario.
. Verificamos que este objeto encontrado es realmente una función (es decir si el objeto puede ser invocado).
. Creamos una tupla que podrá contener uno o mas valores (que será luego pasada como argumento al script).
. Insertamos los valores en la tupla (realizando la conversión previa requerida del tipo de dato C al tipo de dato Python).
. Invocamos la función.
. Convertimos el tipo de dato devuelto de Python a C.
. Mostramos el resultado.
. Decrementamos las referencias.
. Destruimos el intérprete.

Nota: Las tuplas son como listas con la diferencia que son no modificables.

Es cierto que ahora hemos ganado un poco de funcionalidad, sin embargo aún no es posible ejecutar funciones de nuestra API (definida en el motor o en nuestra aplicación) dentro del script Python. Para esto Python deberá “conocer” la existencia de estas funciones. Y para esto deberemos crear un módulo, extendiendo el lenguaje.

3. Ejecutar una función C++ desde un script

Creando un módulo

Crear un módulo desde C es sencillo, en principio la idea es definir una estructura del tipo PyMethodDef que contenga el nombre de cada una de nuestras funciones (este nombre será el utilizado por Python), la función real que realizará la conversión de parámetros y ejecutará finalmente la función deseada, el tipo de llamada que deseamos realizar y finalmente un texto explicativo que resumen el propósito de la función.

[código C++]

| static PyMethodDef prueba_funcion[] = {
| {"funcion", py_funcion, METH_VARARGS, "funcion de prueba"},
| {NULL, NULL}};

Dentro de esta estructura estamos declarando la función py_function que debe estar previamente definida:

[código C++]

| PyObject * py_funcion (PyObject *self, PyObject *args)
| {
| long i;
| if (!PyArg_ParseTuple(args, "l", &i))
| return NULL;
|
| return Py_BuildValue("l", funcion(i));
| }

En esta función lo que hicimos fue convertir los parámetros de la función que se encuentra en una tupla (en este caso sólo un número entero) al tipo de dato C en cuestión (un long), luego devolvemos al script el valor retornado por la función utilizando Py_BuildValue.
Ahora, deberemos crear una función en C que tome un número long como parámetro y devuelva otro.

[código C++]

| long funcion(long l)
| {
| printf("el valor es: %d\n", lValue);
| return 10;
| }

Habiendo ya definido estas funciones, deberemos crear el módulo. Para ello deberemos ejecutar el siguiente código tras inicializar el intérprete:

[código C++]

| PyImport_AddModule("prueba");
| Py_InitModule("prueba", prueba_funcion);

La primera línea crea un módulo llamado “prueba” y la segunda lo inicializa indicando cual es la tabla de métodos que contiene.
Ahora nuestro script en Python podrá realizar una llamada a “funcion” importando previamente al módulo “prueba”:

[código Python]

| import prueba
| print "el valor es: ", prueba.funcion(1)

Hemos agregado nueva funcionalidad a nuestro programa, ya es posible mapear funciones del mismo hacia el script y ejecutar a nuestro placer cualquier script en Python.

El cuerpo del programa completo que ejecutado el código Python que utiliza nuestra función C++ sería el siguiente:

[código C++]

| int main(int argc, char* argv[])
| {
| PyObject * pName, * pModule, * pDict, * pFunc, * pValue, * pArgs;
|
| // Inicializamos el intérprete
| Py_Initialize();
|
| // Agrego un módulo a Python
| PyImport_AddModule("prueba");
|
| // Relaciono el módulo a las funciones proxy relacionadas
| Py_InitModule("prueba", prueba_funcion);
|
| FILE * fp = fopen("script.py", "rb");
| int iLength = _filelength(_fileno(fp));
| char * pScriptBuffer = new char[iLength+1];
| fread(pScriptBuffer, iLength, 1, fp);
| pScriptBuffer[iLength] = '\0';
| fclose(fp);
|
| PyRun_SimpleString(pScriptBuffer);
|
| Py_Finalize();
|
| }

4. Ejecutar el método de una clase desde un script

El último paso será invocar desde un script Python un objeto en C++, al fin y al cabo este será una de las pruebas más parecidas al tipo de uso real que haremos del sistema de script.

A primera vista podemos apreciar cual va a ser nuestro problema, el sistema de registro de funciones callback permite registrar funciones globales y no métodos de clases. El método de una clase no puede registrarse como una función global convencional pues la clase a la cual pertenece el método forma parte de su prototipo. Luego, si bien Python es un lenguaje orientado a objetos no aceptará de manera directa objetos C++ como nativos.

Pero vayamos por partes, en primer lugar crearemos la clase en C++ que deseamos instanciar desde Python:

[código C++]

| class ClaseFoo
| {
| short m_sValor;
| public:
| bool SetValor(short sValor) { m_sValor = sValor; return true; };
| short GetValor() { return m_sValor; };
| }

Ahora, la idea será crear el código necesario para que desde Python se pueda instanciar un objeto tipo ClaseFoo e invocar cualquier de sus métodos.

[código Python]

| import Prueba
|
| obj = Prueba.new()
| Prueba.SetValor(obj, 1)
| unValor = Prueba.GetValor(obj)
| Prueba.delete(obj)
| return unValor

También deberíamos poder recibir en una función Python una referencia a un objeto en particular y utilizarlo para invocar métodos a partir de ella.

[código Python]

| import Prueba
|
| def funcion_x (ref):
| Prueba.SetValor(ref, 1)
| return Prueba.GetValor(ref)

A partir del código Python escrito ya podemos determinar algunas cosas. La sintaxis que utilizaremos en el script para acceder a los métodos de los objetos C++ no será la más elegante que podríamos desear. Es decir, no utilizaremos la clásica convención:


| nombreObjeto.nombreMétodo()

sino:


| nombreClase.nombreMétodo(nombreObjeto)

Más adelante veremos como lograr la sintaxis a la cual estamos acostumbrados, aunque deberemos pagar un precio por ello de una indirección extra.
Pero empecemos a mirar el código C++ necesario para poder ejecutar el código Python que escribimos:

[código C++]

| class ClaseFoo
| {
|
| static PyMethodDef ClaseFoo::Methods[];
|
| };

Recordemos que los métodos estáticos son como funciones globales (al no requerir puntero this al objeto).

[código C++]

| PyMethodDef ClaseFoo::Methods[] = {
| {"new", ClaseFoo::_py_New, 1, ""},
| {"delete", ClaseFoo::_py_Delete, 1, ""},
| {"SetValor", ClaseFoo::_py_SetValor, 1, ""},
| {"GetValor", ClaseFoo::_py_GetValor, 1, ""},
| {NULL, NULL, 0, NULL}
| };

En el array de estructuras PyMethodDef indicamos cual será el nombre de la función (visible desde Python) y cual será la función real a la que se deberá llamar cuando se realice la invocación correspondiente.

Ahora, agregaremos la inicialización del módulo que haremos corresponder con la clase:

[código C++]

| class ClaseFoo
| {
|
| // Inicialización del módulo
| static void _py_InitModule();
|
| };
|
| void ClaseFoo::_py_InitModule()
| {
| PyImport_AddModule("ClaseFoo");
| Py_InitModule("ClaseFoo", ClaseFoo::Methods);
| }

Lo que hacemos en _py_InitModule es agregar el módulo utilizando el nombre y luego las definiciones de las funciones del módulo.

Ahora, agregaremos las funciones proxy para cada uno de los métodos:

[código C++]

| PyObject * ClaseFoo::_py_SetValor(PyObject *self, PyObject *args)
| {
| long lRefObject;
| short s;
| if (!PyArg_ParseTuple(args, "lh", &lRefObject, &s))
| return NULL;
|
| return Py_BuildValue("h", (( ClaseFoo *) lRefObject)->SetValor(s));
| }

El puntero a esta función lo obtenemos en una variable tipo long (que son 4 bytes al igual que los punteros a objetos) y luego lo casteamos para poder invocar el método correspondiente.
Es conveniente notar que en el parseo de la tupla indicamos con una l que el primer parámetro debe ser un long y con una h que el segundo parámetro debe ser un short (Ver la documentación de “Python/C API” para mas información).
De manera análoga podremos crear la función que envuelva la llamada a GetValor. El asunto ahora es ver como poder crear objetos nuevos y destruirlos desde Python. Para esto crearemos dos métodos estáticos más (_py_New y _py_Delete) que harán esta tarea:

[código C++]

| class ClaseFoo
| {
|
| static PyObject * _py_New(PyObject *self, PyObject *args);
| static PyObject * _py_Delete(PyObject *self, PyObject *args);
|
| };
|
| PyObject * ClaseFoo::_py_New(PyObject *self, PyObject *args)
| {
| CPrueba * pNewObj = new ClaseFoo();
| return Py_BuildValue("l", (long) pNewObj);
| }
|
| PyObject * ClaseFoo::_py_Delete(PyObject *self, PyObject *args)
| {
| long lRefObject;
| if (!PyArg_ParseTuple(args, "l", &lRefObject))
| return NULL;
| delete (( ClaseFoo *) lRefObject);
| return Py_BuildValue("l", 0);
| }

Con estos métodos es posible realizar la creación de objetos desde Python.

En el caso de la incorporación de esta metodología de uso a un motor o librería, es conveniente colocar la mayor parte de estos métodos en clases bases para evitar escribir tanto código para acceder a los objetos de C++.

También puede ser conveniente la creación de algún tipo de diccionario de objetos para que desde los scripts se pueda acceder fácilmente a los objetos creados en el sistema en un momento determinado.

Compartir:
  • Twitter
  • Facebook
  • MySpace
  • BarraPunto
  • del.icio.us

Pages: 1 2 3 4 5 6

Comments

  1. Novack
    December 1st, 2007 | 2:05

    Hehe, flor de articulo. Todavia no lo termine de leer, pero muy buen tema!

  2. Ni7ram
    December 1st, 2007 | 12:53

    Epa! Crei que tenias abandonada la pagina pero no, estabas haciendo un super articulo. Buenisimo!

  3. December 7th, 2007 | 22:24

    [...] Este artículo completa el que publiqué la semana pasada y que puede acceder haciendo click aquí. [...]