INTRODUCCION A LA PROGRAMACION EN X WINDOW

Artículo 14: XSNAKE: EJEMPLO COMPLETO (I).

Autor: (c) Santiago Romero.
Revista: Programación Actual (Prensa Técnica) nº 32, Noviembre-1999



Comentados tantos aspectos importantes de X Lib y de otras librerías de X Window, veamos algunos de nuestros conocimientos aplicados sobre un mismo programa que haga uso de X Lib, sus primitivas gráficas, los eventos, Imlib, una estructura organizada, y un funcionamiento algo más completo: el juego Xsnake.


Hasta ahora hemos visto todos los aspecto de X Lib mediante sencillos ejemplos que mostraban de una manera muy básica el funcionamiento de determinadas funciones o los posibles campos o aplicaciones de determinadas estructuras. Lo que vamos a hacer ahora es aglutinar todo lo aprendido en un sólo programa que requiera del uso de eventos para gestionar correctamente el bucle del programa, y que requiera además hacer uso de las primitivas gráficos, pixmaps, pantallas virtuales, dibujo de texto, control del teclado, y manejo de Xlib en general. Nada mejor para esto que un sencillo juego: X snake.


XSNAKE

Xsnake es la adaptación a Xlib e Imlib del clásico juego arcade Snake, donde una serpiente recorre la pantalla en busca de las frutas necesarias para subir de nivel, evitando chocar contra las paredes y con su propia cola. Cada fruta le otorga puntos y le alarga la cola un poco más, de modo que en ciertos momentos puede pasarse realmente mal esquivando nuestra cola para tratar de recoger las últimas frutas necesarias para pasar de nivel. Es un juego muy sencillo pero adictivo, y por su fácil implementación ha sido el elegido para mostrar todas las cualidades de Xlib.


imagen xsnake

En el listado 1 tenemos todas las definiciones de constantes y variables usadas en el programa, como direcciones (en lugar de usar números dentro de nuestro código es mucho más útil usar constantes que no hagan necesario el memorizado de dichos números), códigos de teclas (scancodes recibidos en los eventos KeyPress), las dimensiones del tablero de juego y de los bloques gráficos, etc. También están declaradas allí todas las variables globales del programa, como se verá cuando comentemos el mismo.


LISTADO 1: XSNAKE.H

/* Xsnake.h -> Fichero de cabecera para XSnake. (c) 1999 Santiago Romero Iglesias y Antonio Fernandez Fandos */ #define VELOCIDAD 70000 #define INCREMENTO 10 #define MAX_COMIDA 5 #define xmin 0 #define xmax 79 #define ymin 0 #define ymax 39 #define true 1 #define false 0 #define boolean char #define VACIO 0 #define SERPIENTE 1 #define PARED 2 #define COMIDA1 3 #define COMIDA2 4 #define COMIDA3 5 #define COMIDA4 6 #define ANCHURA_BLOQUE 7 #define ALTURA_BLOQUE 9 #define DIR_IZQ 1 #define DIR_DER 2 #define DIR_ARR 3 #define DIR_ABJO 4 /* Codigos de teclas, verlos todos en el fichero /usr/X11R6/lib/X11/xkb/keycodes/xfree86 */ #define ESC_KEY 9 #define ARRIBA 98 #define ABAJO 104 #define IZQUIERDA 100 #define DERECHA 102 /*--- Variables globales utilizadas ---------------------*/ Pixmap sprites[4]; long amarillo, verde, negro, tiempo=0, segundos=0; char done = 0, tecla = 0, comida; char pantalla[xmax+1][ymax+1], cola[xmax+1][ymax+1]; struct serpiente { int cabserx, cabsery, colaserx, colasery, espera; char izquierda, derecha, arriba, abajo; int puntos; }; /* estructuras para el cronometrado de tiempo */ struct timeval start, end; long time_dif; /*--- Declaraciones de funciones ------------------------*/ void InicializaMatrices( void ); void InicializaSerpiente( struct serpiente * ); void DibujaTablero( Display *, Pixmap, Drawable, GC ); void LeerMovimiento( char, struct serpiente * ); void Movimiento( Display *, Drawable, Drawable, GC, struct serpiente * ); void DibujaSerpiente( Display *, Pixmap, Window, GC, struct serpiente * ); void EliminaCola( Display *, Pixmap, Window, GC, struct serpiente * ); void BorraSerpiente( Display *, Pixmap, Window, GC, struct serpiente * ); void ActualizaPuntos( Display *, Pixmap, Window, GC, struct serpiente * ); void DibujaComida( Display *, Drawable, Drawable, GC ); void BorraComida( Display *, Drawable, Drawable, GC, int, int ); void GeneraComida( Display *, Drawable, Drawable, GC ); unsigned long ColorPorNombre( Display *, char *); void RedibujaPantalla( Display *, Drawable, GC, Pixmap, int, int, int, int ); void DibujaBitmap( Display *, Drawable, GC, Pixmap, int, int ); void DibujaBloque( Display *, Drawable, Drawable, GC, int, int, char );
El resto del programa (el código en sí mismo) está disponible en el listado 2, incluyendo todas las funciones de inicialización, movimiento, dibujado de gráficos, y gestión de eventos. Sin duda no hay nada más descriptivo para un programador que un sencillo ejemplo que ilustre como utilizar determinadas funciones, de modo que este programa tiene doble función, ya que además de permitirnos aprender muchas técnicas básicas, se podrá ver todo lo aprendido de forma correctamente aplicada.


LISTADO 2: XSNAKE.C

/*=========================================================== Xsnake.c -> Version XWindow del juego SNAKE (clásico de los primeros videojuegos arcade). El juego consiste en mover una serpiente por la pantalla sin chocarse con las paredes ni con la cola, recogiendo frutas que aumentan el tamaño de esta. Compilar mediante: gcc xsnake.c -o xsnake -I/usr/X11R6/include -I/usr/local/include -L/usr/X11/lib -L/usr/local/lib -lX11 -lXext -ljpeg -lz -lm -lImlib Lanzar desde el mismo directorio que las imagenes xsnake*.jpg . (c) 1999 Santiago Romero Iglesias y Antonio Fernandez Fandos =========================================================*/ #include <X11/Xlib.h> #include <X11/Xutil.h> #include <stdio.h> #include <X11/extensions/shape.h> #include <Imlib.h> #include <sys/time.h> #include <unistd.h> #include "xsnake.h" /*=== Funcion principal main() ==========================*/ int main() { Display *display; Window window, rootwin; int pant, anchura, altura; Pixmap pixmap, mask; GC gc; ImlibData *id; ImlibImage *imagen; XSetWindowAttributes attr; XEvent evento; Font fuente; char key; struct serpiente serpiente1; XSizeHints myhints; display = XOpenDisplay( NULL ); window = XcreateWindow(etc..); pant = DefaultScreen(display); XSelectInput( display, window, KeyPressMask | ExposureMask ); XStoreName(display, window, "X-Snake"); pixmap = XCreatePixmap( display, window, ANCHURA_BLOQUE*(xmax+1), ALTURA_BLOQUE*(ymax+1)+40, DefaultDepth(display, pant)); gc = XCreateGC( display, pixmap, 0, NULL ); XSetForeground(display, gc, 0 ); XFillRectangle( display, pixmap, gc, 0, 0, ANCHURA_BLOQUE*(xmax+1), ALTURA_BLOQUE*(ymax+1)+40 ); negro = ColorPorNombre( display, "black"); amarillo = ColorPorNombre(display, "yellow"); verde = ColorPorNombre( display, "green"); fuente = XLoadFont( display, "9x15" ); XSetFont( display, gc, fuente ); /* cargamos las imagenes JPG para el juego en pixmaps */ imagen=Imlib_load_image(id, "xsnake0.jpg"); Imlib_render( id, imagen, ANCHURA_BLOQUE, ALTURA_BLOQUE ); sprites[0] = (Pixmap) Imlib_move_image( id, imagen ); imagen=Imlib_load_image(id, "xsnake1.jpg"); Imlib_render( id, imagen, ANCHURA_BLOQUE, ALTURA_BLOQUE ); /* igual para el resto de imagenes */ srand( time(NULL) ); DibujaTablero( display, pixmap, window, gc ); InicializaMatrices(); InicializaSerpiente( &serpiente1 ); DibujaSerpiente(display, pixmap, window, gc, &serpiente1 ); ActualizaPuntos(display, pixmap, window, gc, &serpiente1 ); XMapWindow( display, window ); while ( !done ) { /* coger el tiempo actual */ gettimeofday(&start, NULL); /* a continuacion miramos si hay algun evento en la cola de eventos. Si lo hay, lo recogemos y lo interpretamos. Si no lo hay, se repite el bucle. */ while (QLength(display) > 0) { XNextEvent(display, &evento); if (evento.xany.window != window) continue; switch(evento.type) { case Expose: RedibujaPantalla(display, window, gc, pixmap, evento.xexpose.x, evento.xexpose.y, evento.xexpose.width, evento.xexpose.height ); XFlush(display); break; case KeyPress: key = XKeycodeToKeysym(display, evento.xkey.keycode, 0); if( evento.xkey.keycode == ESC_KEY ) done = 1; tecla = evento.xkey.keycode; break; } } if( tecla != 0 ) LeerMovimiento ( tecla, &serpiente1 ); GeneraComida( display, pixmap, window, gc ); Movimiento( display, pixmap, window, gc, &serpiente1 ); DibujaSerpiente(display, pixmap, window, gc, &serpiente1 ); BorraSerpiente(display, pixmap, window, gc, &serpiente1 ); /* coger el tiempo actual */ gettimeofday(&end, NULL); /* calculamos el tiempo que ha pasado desde el anterior fotograma, para poder cronometrar el estado del juego */ if( end.tv_usec < start.tv_usec ) end.tv_usec += 1000000; time_dif = end.tv_usec - start.tv_usec; XSync(display, False); usleep(VELOCIDAD-time_dif); /* implementamos un cronometro de useg a segundos */ tiempo += VELOCIDAD; if( tiempo > 1000000 ) { tiempo -= 1000000; segundos++; } } sleep(1); XUnmapWindow(display, window); XUnloadFont(display, fuente); XFreePixmap(display, pixmap); XCloseDisplay(display); exit( 0 ); } /*--- Inicializa los valores básicos para la serpiente.------------------*/ void InicializaSerpiente( struct serpiente *serp ) { serp->cabserx = serp->colaserx =xmax/2; serp->cabsery = serp->colasery =ymax/2; serp->espera = INCREMENTO/2; serp->puntos = 0; serp->izquierda = serp->arriba = serp->abajo = false; serp->derecha = true; pantalla[serp->colaserx][serp->colasery] = SERPIENTE; cola[serp->colaserx][serp->colasery] = DIR_DER; } /*--------------------------------------------------------- Dibuja el tablero tanto en pantalla como en el pixmap que hace de pantalla virtual. --------------------------------------------------------*/ void DibujaTablero( Display *display, Pixmap pixmap, Drawable window, GC gc ) { int x, y; for( x=0; x<=xmax; x++ ) { DibujaBloque( display, pixmap, window, gc, x, ymin, PARED ); DibujaBloque( display, pixmap, window, gc, x, ymax, PARED ); } for( y=0; y<=ymax; y++ ) { DibujaBloque( display, pixmap, window, gc, xmin, y, PARED ); DibujaBloque( display, pixmap, window, gc, xmax, y, PARED ); } XSetForeground(display, gc, verde); XDrawString( display, pixmap, gc, 10, ((ALTURA_BLOQUE*ymax)+40)-5, "PUNTOS: ", 8 ); XDrawString( display, window, gc, 10, ((ALTURA_BLOQUE*ymax)+40)-5, "PUNTOS: ", 8 ); XDrawString( display, pixmap, gc, ((ANCHURA_BLOQUE*(xmax-1))/2)-15, ((ALTURA_BLOQUE*ymax)+40)-5, "X-SNAKE", 7 ); XDrawString( display, window, gc, ((ANCHURA_BLOQUE*(xmax-1))/2)-15, ((ALTURA_BLOQUE*ymax)+40)-10, "X-SNAKE", 7 ); } /*--- Actualiza abajo los puntos que lleva el jugador. ----------------*/ void ActualizaPuntos( Display *display, Pixmap pixmap, Drawable window, GC gc, struct serpiente *serp ) { char puntos[7]; sprintf(puntos, "%d", serp->puntos); XSetForeground(display, gc, negro); XFillRectangle( display, window, gc, 105, ((ALTURA_BLOQUE*ymax)+40)-20, 6*9, 20 ); /* etc... sigue con mas escrituras: ver listado completo en CD. */ } /*--- Realiza la accion apropiada segun la pulsacion de direccion ------*/ void LeerMovimiento( char keycode, struct serpiente *serp ) { if( keycode == ARRIBA && serp->abajo == false ) { serp->arriba = true; serp->derecha = serp->izquierda = serp->abajo = false; } /* igual para el resto de direcciones, else if( keycode==DERECHA..) */ tecla = 0; } /*--- Realiza la accion apropiada segun la pulsacion de direccion ------*/ void Movimiento( Display *display, Pixmap pixmap, Window window, GC gc, struct serpiente *serp ) { int dircola, cabserantx, cabseranty; char dato; cabserantx = serp->cabserx; cabseranty = serp->cabsery; if ( serp->izquierda == true ) { if ( serp->cabserx > xmin+1 ) { serp->cabserx = (serp->cabserx)-1; dircola = DIR_IZQ; } else done = 1; } /* igual para el resto de direcciones (ver listado CD)*/ /* ahora comprobamos choque con pared y con comida */ if( pantalla[serp->cabserx][serp->cabsery] == SERPIENTE ) done = 1; else { if( pantalla[serp->cabserx][serp->cabsery] >= COMIDA1 ) { BorraComida( display, pixmap, window, gc, serp->cabserx, serp->cabsery ); comida--; serp->espera = serp->espera + INCREMENTO; serp->puntos = serp->puntos + 10; ActualizaPuntos(display, pixmap, window, gc, serp ); } pantalla[serp->cabserx][serp->cabsery] = SERPIENTE; cola[cabserantx][cabseranty] = dircola; } } /*--- Dibuja un bloque ----------------------------------*/ void DibujaBloque( Display *display, Drawable pixmap, Drawable window, GC gc, int x, int y, char tipo ) { DibujaBitmap( display, pixmap, gc, sprites[tipo], x*ANCHURA_BLOQUE, y*ALTURA_BLOQUE); DibujaBitmap( display, window, gc, sprites[tipo], x*ANCHURA_BLOQUE, y*ALTURA_BLOQUE); } /*--- Funcion que redibuja la pantalla necesaria ----------------*/ void RedibujaPantalla( Display *display, Drawable drawable, GC gc, Pixmap pixmap, int x, int y, int anchura, int altura ) { XCopyArea(display, pixmap, drawable, gc, x, y, anchura, altura, x, y); } /*--- Funcion que dibuja un bitmap ----------------------*/ void DibujaBitmap( Display *display, Drawable drawable, GC gc, Pixmap pixmap, int x, int y ) { XCopyArea(display, pixmap, drawable, gc, 0, 0, ANCHURA_BLOQUE, ALTURA_BLOQUE, x, y); }

COMPILACION DEL PROGRAMA

Para compilar el programa se debe ejecutar el siguiente comando:

gcc xsnake.c -o xsnake -I/usr/X11R6/include -I/usr/local/include -L/usr/X11/lib -L/usr/local/lib -lX11 -lXext -ljpeg -lz -lm -lImlib

Para realizar la compilación se ha creado un sencillo script llamado make con la anterior línea. Basta pues con ir al directorio del programa y ejecutar ./make, lo cual generará el binario xsnake (aunque mucho más eficiente hubiese sido un Makefile, no es make el propósito de este curso).

Como puede verse, requiere (en general) los siguientes recursos:

 XFree86
 XFree86-devel
 glibc-devel
 Imlib
 Imlib-devel
 libjpeg

Para su ejecución tan sólo hay que ir mediante una terminal de texto (xterm) al directorio del programa compilado y ejecutarlo con:


 ./xsnake

El programa debe ejecutarse en el mismo directorio que las imágenes para que éste pueda cargarlas. En un programa que fuese versión final, probablemente el programa de instalación las instalaría en /usr/games, desde donde las leería el programa.

Durante la ejecución controlaremos la serpiente mediante las flechas del cursor, y nuestro objetivo será, simplemente, el de conseguir la mayoría de puntos posible. Al salir del programa, éste nos dirá la puntuación máxima conseguida y el tiempo que se ha sobrevivido:


[sromero@compiler ejemplo]$ ./xsnake
XSnake - Version 1.0 para X Window.
 
 Has conseguido 240 puntos.
 Has sobrevivido 2 minuto(s) y 39 segundo(s).

Como se verá en el comentario del próximo artículo, en el juego se controla el tiempo para que la tasa de fotogramas por segundo (fps) sea fija, asi como para contar el tiempo de juego desde el inicio de la aplicación.


EN LA PROXIMA ENTREGA

En la próxima entrega comentaremos todas las técnicas utilizadas para el funcionamiento del juego. Entre otras:

Por supuesto, la sencillez del juego ha estado limitada al tamaño del artículo, pero nada nos impide crear juegos más amplios, más completos o de más calidad utilizando XImages en lugar de pixmaps (para más velocidad) y de cualquier temática que se nos ocurra. Como siempre, la limitación del programador es la imaginación. Con este ejemplo, en la próxima entrega esperamos despejar todas las dudas que pueda tener el lector antes de lanzarse a programar aplicaciones o juegos, o a aprender toolkits de Xwindow (como QT o GTK) y a complementarlos con el uso de Xlib, DGA, Xshm o Imlib.


Pulse aquí para bajarse los ejemplos y listados del artículo (8 Kb).


Santiago Romero


Volver a la tabla de contenidos.