Академический Документы
Профессиональный Документы
Культура Документы
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.
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.
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 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;
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);
} // end namespace
#endif // _PYSCRIPT_H_
#include "PyScript.h"
#include "./include/python/python.h"
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;
Py_Initialize();
init_anacondagl();
pName = PyString_FromString(m_strScriptFileName.c_str());
if (pModule != NULL)
{
// Obtengo el diccionario del módulo
pDict = PyModule_GetDict(pModule);
if (pValue != NULL)
{
Py_DECREF(pValue);
}
Py_DECREF(pModule);
Py_DECREF(pName);
Py_Finalize();
return true;
}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bool PyScript::BeginScript()
{
m_bExitThread = 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