Conceptos y configuración
Desarrollar un juego de video en 2D con OpenGL no es tan complicado como podrías pensar. A través de una serie de artículos, mostraré cómo se desarrolla un juego sencillo, que sirva para dar una idea clara de las funciones y estructura básica de un juego. Por razones de espacio en la revista, en esta primera parte sólo introduciré algunos conceptos básicos y de configuración; y en próximas entregas continuaré con los diferentes aspectos a resolver, en el desarrollo de un juego 2D.
Estructura básica de un juego•Una parte de inicialización - Aquí se crea nuestra ventana principal, se cargan todos los archivos que necesita el juego (modelos 3D, imágenes, sonidos) y se crean e inicializan los diferentes subsistemas del juego (gráficas, input del usuario, inteligencia artificial, etcétera).
•Un ciclo principal - Este ciclo principal ejecuta la lógica del juego una y otra vez, hasta que el usuario sale del juego. La lógica básica dentro de este ciclo es:
•Actualización - Por ejemplo la posición de un enemigo, verificar si el usuario ha presionado algun botón, actualizar la posición de un cohete se disparó, verificar si un vehículo ha colisionado contra algún objeto del mundo.
•Render – Es la parte más importante, es decir, el mandar todas las gráficas a la pantalla.
Este ciclo se ejecuta muchas veces por segundo, y cada ejecución se conoce como Frame. Una medida del rendimiento de nuestro juego, es indicar cuantos FPS (frames-per-
second) ejecuta. Qué tan rápido corre un juego, depende de muchos factores, principalmente aspectos de hardware como procesador, memoria, tarjeta gráfica; aunque también, influyen aspectos de nuestra lógica de programación. Por lo general, en el desarrollo de un juego se intenta tener por lo menos 60fps, para que el movimiento de los objetos sea contínuo.
•Una parte de liberación de recursos – Ejecutada al finalizar el juego, aquí se libera la memoria obtenida de manera dinámica en todos los subsistemas del juego.
Conceptos básicos
Definamos algunos conceptos básicos que nos ayudarán a entender el código de nuestro juego 2D:
Framebuffer. OpenGL cuenta con una colección de búfers donde se almacena información que se presentará en la pantalla. El búfer principal, es el búfer de color, que es un arreglo bidimensional que almacena el color final de los pixeles. También existe el búfer de profundidad (Z-buffer), que almacena la información de profundidad (respecto a la cámara virtual) de cada pixel generado cuando se hacer un render de un objeto, esto sirve para determinar si cada pixel del objeto es visible al hacer la comparación de los pixeles generados contra la información que ya se tiene en el Z-Buffer.
En las aplicaciones gráficas se tienen cuando ménos dos framebuffers, uno para mostrar en la pantalla y otro para hacer un render en él, mientras el otro, se muestra en la pantalla. Al terminar de hacer un render en un búfer, se hace un intercambio, el búfer en el que se hizo el render es mostrado en la pantalla, mientras se hace render en el otro. Es necesario usar dos búfers, ya que de lo contrario, tendríamos un efecto visual desagradable al hacer render sobre el búfer en pantalla.
Render. Es el proceso de tomar una representación matemática tridimensional de un objeto y presentarlo como una imagen bidimensional en la pantalla. Para este proceso se toma toda la información relacionada al objeto como color, posición y textura.
Proyecciones. Las 3 etapas conceptuales de la arquitectura gráfica son: Aplicación, Geometría, y Rasterización. En la etapa de Geometría, se toman los objetos y sus datos asociados y se “proyectan” en la pantalla. Hay dos formas principales de hacerlo: para 2D se usa la proyección ortogonal, en ésta, la apariencia de los objetos es la misma, independientemente de la distancia desde donde se vean. Para 3D se utiliza la proyección en perspectiva; donde la apariencia de los objetos cambia, dependiendo de la distancia con respecto a la cámara virtual, que es como percibimos los objetos en la vida real.
Transformaciones. En un juego, el movimiento de los objetos se realiza por medio de transformaciones. Trasladar un proyectil de una posición a otra, rotar la cámara virtual, hacer más grande un objeto, son ejemplos de transformaciones.
Matrices. Para almacenar información acerca del tipo de proyección usada en un juego, e información de las transformaciones y parámetros actuales de la cámara virtual, OpenGL utiliza la Projection Matrix y la ModelView Matrix respectivamente.
Viewport. Podemos definir un área dentro de una ventana, a la cual va a afectar nuestro render. Generalmente, el viewport tiene el mismo tamaño de la ventana, pero si quisiéramos hacer una aplicación que muestre nuestros objetos desde varias perspectivas (como lo hace 3dsmax por ejemplo) tendríamos que definir diferentes viewports dentro de nuestra ventana.
Geometría. Todos los objetos que vemos en un juego 3D están compuestos por pequeños triángulos, que al ser representados en conjunto, determinan la forma del objeto. Las formas básicas que se mandan a la tarjeta gráfica son cuadrados, triángulos, líneas, y puntos. Cada una de estas primitivas tiene información asociada como vértices, normales, y coordenadas de textura.
Texturas. Son básicamente, imágenes que van a ser “pegadas” a nuestra geometría para darle la apariencia final. Una restricción importante para las texturas de un juego, es que su tamaño debe ser potencia de 2, por ejemplo 128x128, 256x256, 512x512 pixeles. Para determinar qué parte de la textura se pegará en cada triángulo de la geometría, se utilizan las coordenadas de textura. Al hacer el render, se deben pasar las coordenadas de textura por cada vértice de la geometría. OpenGL funciona como una máquina de estados, es decir, guarda la información actual de las propiedades de los objetos hasta que se le indican nuevas propiedades.
GL Utility Toolkit (GLUT)
GLUT es una librería que simplifica la creación y manejo de ventanas con soporte para OpenGL. En GLUT podemos definir nuestras callback functions, que son las funciones llamadas automáticamente cuando ocurren ciertos eventos como cuando se cambia de tamaño la ventana, cuando se hace clic con el mouse o cuando se recibe input del teclado.
GLUT se puede descargar en www.xmission.com/~nate/glut.html, y si quieres configurar Visual Studio para usar GLUT, puedes encontrar una guía para esto en csf11.acs.uwosh.edu/cs371/visualstudio/
El siguiente código es un ejemplo de un esqueleto que inicializa OpenGL y GLUT.
#include
#include
#include
/* Declaracion de callback functions, etc */
void SG_DisplayFunction();
void SG_KeyboardFunction( unsigned char key, int x, int y );
void SG_SizeFunction( int width, int height );
void SG_MouseFunction( int button, int state, int x, int y );
void InitializeProjection( int width, int height );
void InitializeOpenGL();
/* ... */
void InitializeOpenGL()
{
// Color que se utilizará cuando se limpie la pantalla (RGBA)
glClearColor( 0.0f, 0.4f, 0.9f, 1.0f );
// Parámetros para el uso del Z-buffer
glClearDepth( 1.0f );
glEnable( GL_DEPTH_TEST );
glDepthFunc( GL_LEQUAL );
glShadeModel( GL_SMOOTH ); // Tipo de shading
InitializeProjection( 800, 600 ); // Proyección
//...
}
void InitializeProjection( int width, int height )
{
glViewport( 0, 0, width, height ); // Viewport
glMatrixMode( GL_PROJECTION ); // Proyección
glLoadIdentity();
glOrtho( 0, width, 0, height, -10, 10 ); // 2D
/* Cambiar para poder procesar las transformaciones de la camara virtual */
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
}
void SG_DisplayFunction()
{
// Limpia el bufer de color y el Z-buffer
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
//...
glFlush(); // Termina los comandos de render pendientes
glutSwapBuffers(); // Intercambia el front y back buffer
}
void SG_SizeFunction( int width, int height )
{
InitializeProjection( width, height );
}
void SG_KeyboardFunction( unsigned char key, int x, int y ){ /*...*/ }
void SG_MouseFunction( int button, int state, int x, int y ){ /*...*/ }
int main( int argc, char** argv )
{
/* Código de inicialización de GLUT */
glutInitDisplayMode( GLUT_DOUBLE | GLUT_DEPTH |
GLUT_RGBA );
glutInitWindowPosition( 0, 0 );
glutInitWindowSize( 800, 600 );
glutInit( &argc, argv );
/* Creación de la ventana */
glutCreateWindow(“Juego 2D – SG 2007”);
/* Callback Functions */
// Función llamada cada vez que el usuario cambia
// de tamaño la ventana
glutReshapeFunc( SG_SizeFunction );
// Función llamada cuando la ventana necesita ser redibujada
glutDisplayFunc( SG_DisplayFunction );
// Función llamada cada que nuestra aplicación esta “libre”
glutIdleFunc( SG_DisplayFunction );
// Función llamada cada que se recibe input del teclado
glutKeyboardFunc( SG_KeyboardFunction );
// Función llamada cada que se recibe input del mouse
glutMouseFunc( SG_MouseFunction );
// Inicialización de OpenGL
InitializeOpenGL();
// El loop principal de la aplicación
glutMainLoop();
return 0;
}
Como se puede observar, el uso de GLUT permite simplificar a unas cuantas líneas, un código de inicialización que podría tomar el doble de líneas o más, usando la API de Windows.
Conclusión
En esta primera parte, revisamos los conceptos básicos para arrancar con el desarrollo de nuestro juego. En entregas posteriores veremos cómo crear el mundo gráfico de nuestro juego, mover los objetos, detectar colisiones, reproducir sonidos, y muchos otros aspectos involucrados en el desarrollo de un juego. ¡Hasta entonces!
Acerca del autor
Joel Villagrana, egresado de la Universidad de Guadalajara como Ingeniero en Computación, ha estado en contacto con la programación de videojuegos desde hace 4 años cuando estudió una maestría en Ambientes Virtuales en Inglaterra. Actualmente trabaja para IBM, pero en su tiempo libre sigue aprendiendo las nuevas técnicas de gráficas 3D. Participó en el libro “More OpenGL Game Programming”, publicado en 2005. Recientemente participó en Creanimax 2006. La información de estos artículos representa su punto de vista y no necesariamente el de IBM Corporation. joel.villagrana@gmail.com
- Log in to post comments