Вы находитесь на странице: 1из 9

Utilizando Swig para embeber Python en un motor de

juegos
Python, AnaCondaGL y Swig.
Introducción
La versatilidad de un motor gráfico para juegos pueden ser aumentada muchas veces si le
damos la posibilidad al programador de poder utilizar scripts para la implementación de la
inteligencia artificial de los personajes, para programar la interfaz de usuario o para cualquier
otra tarea donde pese más la flexibilidad del lenguaje a la velocidad de ejecución.
Hoy día muchos juegos utilizan algún tipo de lenguaje de scripts para la realización de estas
tareas, tal es el caso del Freedom Force que se encuentra repleto de scripts Python. Y quizás
fue este último caso el que me empujó finalmente a investigar la implementación de esta
facilidad en mi motor AnaCondaGL.
Este documento trata de cómo se puede incorporar Python en un motor de juegos en C++
utilizando la librería Swig.
¿Por qué incluir un lenguaje script en mi motor?
Existen muchísimas razones por las cuales es conveniente dotar nuestro motor de dicha
característica. El lenguaje C/C++ es ideal para programar juegos, de hecho es conveniente que
el corazón del mismo esté codificado en este lenguaje, sin embargo debemos notar que a pesar
de sus ventajas:
§ Alta performance.
§ Programación de bajo nivel.
§ Disponible en muchas plataformas.
Posee varias desventajas:
§ El ciclo compilación/depuración de ciertos módulos puede ser extenso y frustrante.
§ Su extensión y modificación se vuelve complicada.
§ No es interactivo.
Y es muy desventajoso cuando deseamos implementar:
§ Interfaz de usuarios.
§ Programación de alto nivel.
§ Integración de módulos.
Por otro lado la inclusión de Python en nuestro motor nos traerá las siguientes ventajas:
§ Flexibilidad.
§ Interactividad.
§ Prototipado rápido (módulos que luego de ser probados y de ser necesario podrán ser
convertidos en C++ sin la necesidad de modificar objetos que lo utilicen).
Es por esto que mezclar estos dos componentes es obtener lo mejor de los dos mundos,
además no penalizamos en tiempo de ejecución a el código que no haga uso de esta facilidad,
de hecho es posible que ciertos juegos que utilicen nuestro motor ni siquiera posean un script
en Python.
¿Y por que específicamente Python?
Python es un excelente lenguaje de script, es orientado a objetos lo que permitirá hacer uso de
ciertas funcionalidades C++ de modo mas convencional, está muy probado y tiene mucho
tiempo en el ruedo, ya fue utilizado en juegos comerciales AAA exitosamente, existe mucha
documentación y muchos tutoriales, tiene una gran cantidad de módulos.
Primero lo primero
Cada lenguaje de script posee su propio juego de tipos de datos y su propia sintaxis, por lo
tanto es necesario generar código extra, llamado glue-code, para permitirle al mismo acceder al
modelo de objetos de nuestro motor.

Existe una excelente herramienta que nos facilita esta labor, llamada Swig
(http://www.swig.org). Swig no sólo nos ayuda a embeber Python en nuestro motor sino
además Perl, Tcl/Tk, Guile, MzScheme, Ruby y Java.
Además nos permite acceder a las siguientes funcionalidades del ANSI C++:
§ Todos los tipos de datos del C++.
§ Referencias.
§ Punteros a miembros.
§ Clases.
§ Herencia y herencia múltiple.
§ Sobrecarga de funciones y métodos.
§ Sobrecarga de operadores.
§ Miembros estáticos.
§ Espacios de nombre.
§ Plantillas.
Pero…¿Qué es concretamente el SWIG?
El Swig no es una librería, sino un precompilador. Tomando como entrada un archivo definido
por nosotros (.i) generará el código de necesario para permitir a Python (en nuestro caso)
acceder al motor. Este código generado será código C++ (wrapper functions) que luego será
compilado normalmente junto con el resto de la librería. También, para acceder a ciertas
funcionalidades, podemos requerir la generación de clases especiales en Python llamadas
Shadow Classes, estas clases son la contra parte de las clases C++ a la cuales queremos
acceder. De este modo podremos hacer referencia a ellas desde Python como si fuesen nativas.

¿Como configuramos el compilador para utilizar Swig?


Si utilizamos el compilador de Microsoft (Visual C++), podremos especificar que para cierto tipo
de archivos invoque un comando arbitrario. En nuestro caso, para el archivo AnaCondaSwig.i
especificaremos que ejecute el swig con los siguientes parámetros:
swig -c++ -python -shadow -o $(ProjDir)\$(InputName)_wrap.cxx $(InputPath)

El modificador -c++ indica que el código a generar es C++, el -python indica que el lenguaje
objetivo es Python, luego especificamos que genere shadow classes con -shadow y finalmente
especificamos el archivo de salida con -o.

¿Qué contienen los archivos .i?


Como mencionamos anteriormente, debemos especificar un archivo .i para indicarle a Swig que
parte de nuestro código queremos exportar para ser visto desde Python.
Supongamos que escribimos la siguiente clase en C++:
class foo
{
public:
void Print(const char * pszText);
};
Entonces, deberemos crear un archivo foo.i que indique:
%module foo
%{
#include “foo.h”
%}
%include “foo.h”
Hecho esto podremos compilar el archivo foo.i, a lo que el compilador C++ invocará Swig y
generará un extenso archivo llamado foo_wrap.cxx en lenguaje C++ y un archivo llamado
foomodule.py con la shadow class.
¿y ahora que?
Bueno, por el momento vimos como crear la interfaz de nuestro modelo de objetos que gracias
a Swig es muy sencillo. Si vimos en detalle el código del archivo i notamos que hay una
sentencia llamada “module foo”, esto significa que existirá un módulo con este nombre que
deberemos importar desde Python para poder hacer uso de él. ¿Es necesario crear un módulo
por clase?, respuesta: NO. Quizás nos convenga crear un único módulo por API o quizás un
módulo por subsistema de nuestro motor, AnaCondaGL hace uso de esta última opción, ya que
cuando cargamos un módulo no utilizamos recursos por los módulos no cargados.
Llegando a esta instancia quizás queramos saber como invocar un script Python desde nuestro
motor. Bueno para esto deberemos hacer uso del API C/Python. Pero antes es conveniente
hacer una distinción entre embeber y extender.
Embeber es utilizar el API C/Python para poder invocar scripts Python desde nuestro código
C/C++. Estos scripts podrán hacer uso o no de nuestro modelo de objetos.
Extender es crear módulos en C/C++ para poder ser utilizados desde Python.
La diferencia es o no sutil en función de por donde se mire pero principalmente radica en por
donde se comience a ejecutar el programa. Cuando extendemos el programa principal se
encuentra en Python que invoca módulo escritos en C/C++, cuando embebemos el programa
principal es C/C++ y utiliza scripts Python.
AnaCondaGL embebe Python. Es por esto que requerimos el API C/Python para poder cargar el
intérprete y llamar a los scripts correspondientes.
Para esto implementé la posibilidad de correr scripts Python desde la clase principal del motor y
además creé una clase llamada PyScript que permite la ejecución de scripts en su propio
thread. Veamos la cabecera de dicha clase:

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// AnaCondaGL : Librería para la programación de juegos.
// http://www.dedalus-software.com.ar
//
// AnaCondaGL es libre, puede utilizarla y/o modificarla bajo los
// términos de la licencia LGPL. Los términos completos de esta
// licencia puede encontrarlo en un archivo adjunto llamado lgpl.txt.
// Diego G. Ruiz - Dedalus Software
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

#ifndef _PYSCRIPT_H_
#define _PYSCRIPT_H_

#include <string>

namespace AnaCondaGL
{

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// PyScript
// Esta clase envuelve la ejecución de un método Python. Es posible realizar
// la ejecución dentro del mismo thread o crear un thread especial para el
// script.
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class __declspec(dllexport) PyScript
{
// Handle al Thread
HANDLE m_hThreadEngine;
unsigned long m_ulThreadId;

// Nombre del archivo donde se encuentra el script a ejecutar


std::string m_strScriptFileName;
// Nombre del método a ejecutar
std::string m_strMethodName;

// Indica si el script debe terminar


volatile bool m_bExitThread;

public:
PyScript();
PyScript(const char * pszScriptFileName, char * pszMethodName);

virtual ~PyScript();
// Ejecuta un script en el mism hilo de ejecución
bool Run();
bool Run(const char * pszScriptFileName, char * pszMethodName);

// Ejecuta un script en un hilo propio de ejecución


bool BeginScript();
bool BeginScript(const char * pszScriptFileName, char * pszMethodName);
// Detiene la ejecución del script
bool EndScript();

// Indica si el script se encuentra corriendo


bool IsRunning() { return m_bExitThread; };
static bool IsRunning(long pPyScript);

friend unsigned long WINAPI ThreadScriptFn(LPVOID pParam);


};

} // end namespace

#endif // _PYSCRIPT_H_

Ahora veamos el cuerpo de la clase:


// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// AnaCondaGL : Librería para la programación de juegos.
// http://www.dedalus-software.com.ar
//
// AnaCondaGL es libre, puede utilizarla y/o modificarla bajo los
// términos de la licencia LGPL. Los términos completos de esta
// licencia puede encontrarlo en un archivo adjunto llamado lgpl.txt.
// Diego G. Ruiz - Dedalus Software
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

#include "PyScript.h"
#include "./include/python/python.h"

// Función creada por Swig para registrar el módulo Python


extern "C" void init_anacondagl(void);

namespace AnaCondaGL
{

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Función del thrad
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
unsigned long WINAPI ThreadScriptFn(LPVOID pParam)
{
PyScript * pPyScript = (PyScript *) pParam;

pPyScript->Run();

return 1;
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
PyScript::PyScript()
{
m_strScriptFileName = "";
m_strMethodName = "";
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
PyScript::PyScript(const char * pszScriptFileName, char * pszMethodName)
{
m_strScriptFileName = pszScriptFileName;
m_strMethodName = pszScriptFileName;
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
PyScript::~PyScript()
{

}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bool PyScript::Run(const char * pszScriptFileName, char * pszMethodName)
{
m_strScriptFileName = pszScriptFileName;
m_strMethodName = pszScriptFileName;

return Run();

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bool PyScript::Run()
{
if (m_strScriptFileName == "" || m_strMethodName == "")
return false;

PyObject * pName = NULL;


PyObject * pModule = NULL;
PyObject * pDict = NULL;
PyObject * pFunc = NULL;
PyObject * pValue = NULL;
PyObject * pArgs = NULL;

Py_Initialize();

init_anacondagl();

pName = PyString_FromString(m_strScriptFileName.c_str());

// Cargo el modulo correspondiente al script


pModule = PyImport_Import(pName);

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, (char *) m_strMethodName.c_str());

// Verifico que la función es llamable


if (pFunc && PyCallable_Check(pFunc))
{
// Ejecuto la función Python pasandole
// un objeto como referencia
pArgs = PyTuple_New(1);

pValue = PyInt_FromLong((long) this);


PyTuple_SetItem(pArgs, 0, pValue);

pValue = PyObject_CallObject(pFunc, pArgs);

if (pValue != NULL)
{
Py_DECREF(pValue);
}

Py_DECREF(pModule);

Py_DECREF(pName);

Py_Finalize();

return true;

}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bool PyScript::BeginScript()
{
m_bExitThread = false;

if (m_strScriptFileName == "" || m_strMethodName == "")


return false;

m_hThreadEngine = CreateThread(NULL,
0,
ThreadScriptFn,
(void *) this,
0,
&m_ulThreadId);

if (m_hThreadEngine)
return true;
else
return false;
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bool PyScript::BeginScript(const char * pszScriptFileName, char * pszMethodName)
{
m_strScriptFileName = pszScriptFileName;
m_strMethodName = pszScriptFileName;

return BeginScript();
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Detiene la ejecución del script
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bool PyScript::EndScript()
{
// Determino que finalice el thread
m_bExitThread = true;

return true;
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Detiene la ejecución del script
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bool PyScript::IsRunning(long pPyScript)
{
return ((PyScript *) pPyScript)->IsRunning();
}

} // end namespace

No nos perdamos en detalles. Analicemos que es lo que pretende hacer la clase viendo como
podríamos utilizarla:
AnaCondaGL::PyScript script;
script.Run(“miscript”, “foo”);

En este caso creamos un objeto del tipo PyScript y lo ejecutamos (indicando que función del
script queremos correr). La ejecución se realizará en el mismo thread que el programa que ha
creado el objeto PyScript, sin embargo en ocasiones desearemos que esto no sea así, y que el
script corra en su propio thread, entonces tendremos que escribir:
AnaCondaGL::PyScript script;
script.BeginScript(“miscript”, “foo”);

Hecho esto la ejecución del thread principal continuará inmediatamente después de pasar por
BeginScript, para saber si está o no corriendo aún podremos hacer uso de IsRunning en su
versión de clase (estática) o de objeto. También podremos finalizar la ejecución del script con
EndScript pero esto quedará supeditado a que dicho script lea periódicamente el flag
m_bExitThread.
Ahora inspeccionemos un poco como está implementada la clase. Su parte mas interesante se
encuentra en la codificación del método Run, ya que allí es donde hacemos uso del API
C/Python, veamos:
Primeramente invocamos el método Py_Initialize() para inicializar el interprete Python,
luego invocamos una función llamada init_anacondagl, esta función se encuentra en el
wrapper creado por Swig y lo que hace es inicializar el módulo de anacondagl para que esté
disponible a los scripts que deseen utilizarlo.
La línea
pName = PyString_FromString(m_strScriptFileName.c_str());
lo que hace es una conversión de datos, de string a pystring (que es un objeto Python), luego
pModule = PyImport_Import(pName);
importa el módulo especificado por parámetro. Si lo encuentra intenta tomar el diccionario del
mismo:
pDict = PyModule_GetDict(pModule);
y
pFunc = PyDict_GetItemString(pDict, (char *) m_strMethodName.c_str());
busca la función especificada por parámetro dentro de él:
if (pFunc && PyCallable_Check(pFunc))
Si la encuentra y si verifica que dicha entrada del diccionario corresponde a un objeto
invocable, armo una tupla de un valor con el address al objeto this en su interior
pArgs = PyTuple_New(1);
pValue = PyInt_FromLong((long) this);
PyTuple_SetItem(pArgs, 0, pValue);
y finalmente realizo la llamada al script:
pValue = PyObject_CallObject(pFunc, pArgs);

El resto del código de la clase es trivial, BeginScript crea un thread que invoca una función
(amiga de la clase) que a su vez invoca al método Run. EndScript cambia el flag m_bExitThread
a true y luego IsRunning devuelve el estado de este flag.
¿En que momento invocar scripts?
Quizás ahora nos preguntemos esto, ¿Cuándo utilizamos python?. Prefiero dejar librado al
programador del juego esta decisión, ofreciendo todos los elementos necesarios para que
pueda hacerlo del modo que mas le plazca. En los tutoriales del motor utilizo Python para
definir el comportamiento de los personajes u objetos móviles.
En el futuro planeo realizar un plugin que permita crear GUIs y los scripts de comportamiento
de cada control también se definirán podrán ser escritos Python.
Es posible, que realizando un juego sencillo, alguien quiera utilizar 100% Python, siendo así el
ejecutable lo único que hará será crear una clase que herede AnaCondaGL::Game y luego de
inicializado el motor invoque un script pseudo principal donde a partir de allí comience a correr
el juego.
Conclusiones
Python es poderoso, incrementa muchísimo las prestaciones de nuestro motor, Swig nos las
hace fácil y nos permite publicar rápidamente nuestra API a los scripts.
Gran parte del código de un juego no requiere gran performance, el código que debe hacer el
trabajo duro permanece en C/C++ y hasta quizás Assembler pero la lógica, la inteligencia
artificial, las acciones del GUI, y un largo etcétera no requieren sacar el máximo provecho del
CPU, en estos casos es mejor correr la sábana hacia la cabeza y disfrutar de las bondades de
un lenguaje de script acelerando tiempos de desarrollo y permitiéndonos concentrar en la
jugabilidad que es al fin al cabo el componente mas importante de cualquier juego.

Diego G. Ruiz
diego@dedalus-software.com.ar

Referencia
Dedalus Software: http://www.dedalus-software.com.ar
Python: http://www.python.org
Swig: http://www.swig.org

Вам также может понравиться