7. Mapas. Cuarto juego (aproximación "a"): MiniSerpiente 1.
Contenido de este apartado:
7.1. Ideas generales.
El juego de la serpiente es casi tan sencillo de crear como el de las motos de luz: una figura va avanzando por la pantalla; si choca con la pared exterior, con su propia "cola" o con algún otro obstáculo, muere. La única complicación adicional es que en la mayoría de versiones de este juego también va apareciendo comida, que debemos atrapar; esto nos dará puntos, pero también hará que la serpiente sea más grande, y por eso, más fácil chocar con nuestra propia cola.
Esta novedad hace que sea algo más difícil de programar. En primer lugar porque la serpiente va creciendo, y en segundo lugar porque puede que en una posición de la pantalla exista un objeto contra el que podemos chocar pero no debemos morir, sino aumentar nuestra puntuación.
Podríamos volver a usar el "truco" de mirar en los puntos de la pantalla, y distinguir la comida usando un color distinto al de los obstáculos. Pero esto no es lo que se suele hacer. No serviría si nuestro fondo fuera un poco más vistoso, en vez de ser una pantalla negra. En lugar de eso, es más cómodo memorizar un "mapa" con las posibles posiciones de la pantalla, e indicando cuales están vacías, cuales ocupadas por obstáculos y cuales ocupadas por comida.
Podría ser algo así:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X X X
X X X X
X XXX F X X X
X X X
X X F X
X X X X
X F X XXX X
X X X
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
En este ejemplo, las X serían las casillas "peligrosas", con las que no debemos chocar si no queremos morir; las F serían las frutas que podemos recoger para obtener puntos extra.
El hecho de consultar este mapa en vez del contenido de la pantalla nos permite dibujar esos obstáculos, esas frutas, el fondo y nuestra propia serpiente con otras imágenes más vistosas.
Vamos a aplicarlo. Tomaremos la base del juego de las motos de luz, porque la idea básica coincide: el objeto se debe seguir moviendo aunque no toquemos ninguna tecla, y en cada paso se debe comprobar si hemos chocado con algún obstáculo. A esta base le añadiremos el uso de un mapa, aunque todavía será poco vistoso...
La apariencia del juego, todavía muy pobre, será:
7.2 Miniserpiente 1 en C.
Sencillito. Muy parecido al anterior, pero no miramos en pantalla sino en nuestro "mapa":/*----------------------------*/ /* Intro a la programac de */ /* juegos, por Nacho Cabanes */ /* */ /* ipj07c.c */ /* */ /* Septimo ejemplo: juego de */ /* "miniSerpiente" */ /* */ /* Comprobado con: */ /* - Djgpp 2.03 (gcc 3.2) */ /* y Allegro 4.02 - MsDos */ /* - MinGW 2.0.0-3 (gcc 3.2) */ /* y Allegro 4.02 - Win */ /*----------------------------*/ #include <allegro.h> /* Posiciones X e Y iniciales */ #define POS_X_INI 16 #define POS_Y_INI 10 #define INC_X_INI 1 #define INC_Y_INI 0 /* Pausa en milisegundos entre un "fotograma" y otro */ #define PAUSA 350 /* Teclas predefinidas */ #define TEC_ARRIBA KEY_E #define TEC_ABAJO KEY_X #define TEC_IZQDA KEY_S #define TEC_DCHA KEY_D int posX, posY; /* Posicion actual */ int incX, incY; /* Incremento de la posicion */ /* Terminado: Si ha chocado o comida todas las frutas */ int terminado; /* La tecla pulsada */ int tecla; /* Escala: relacion entre tamao de mapa y de pantalla */ #define ESCALA 10 /* Ancho y alto de los sprites */ #define ANCHOSPRITE 10 #define ALTOSPRITE 10 /* Y el mapa que representa a la pantalla */ /* Como usaremos modo grafico de 320x200 puntos */ /* y una escala de 10, el tablero medira 32x20 */ #define MAXFILAS 20 #define MAXCOLS 32 char mapa[MAXFILAS][MAXCOLS]={ "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "X X X", "X F X X", "X F X F X", "X XXXXX X X", "X X X X", "X X X X X", "X X X X X", "X X X X", "X X X X", "X X X X", "X F X X", "X X X", "X X F X", "X X X X", "X X X X", "X X F X X", "X F X X X", "X X F X", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }; int numFrutas = 7; /* Nuestros sprites */ BITMAP *ladrilloFondo, *comida, *jugador; typedef char tipoSprite[ANCHOSPRITE][ALTOSPRITE]; /* El sprite en si: matriz de 30x30 bytes */ tipoSprite spriteLadrillo = {{0,2,2,2,2,2,2,2,2,0}, {2,1,1,1,1,1,1,1,1,2}, {2,1,1,1,1,1,1,1,1,2}, {2,1,1,1,1,1,1,1,1,2}, {2,1,1,1,1,1,1,1,1,2}, {2,1,1,1,1,1,1,1,3,2}, {2,1,1,1,1,1,1,3,3,2}, {2,1,1,1,1,1,3,3,2,2}, {2,2,2,2,2,2,2,2,2,0} }; tipoSprite spriteComida = {{0,0,0,2,0,0,0,0,0,0}, {0,0,2,2,0,0,2,2,0,0}, {0,4,4,4,2,2,4,4,0,0}, {4,4,4,4,4,2,4,4,4,0}, {4,4,4,4,4,4,4,4,4,0}, {4,4,4,4,4,4,4,4,4,0}, {4,4,4,4,4,4,4,4,4,0}, {4,4,4,4,4,4,4,4,4,0}, {0,4,4,4,4,4,4,4,0,0} }; tipoSprite spriteJugador = {{0,0,3,3,3,3,3,0,0,0}, {0,3,1,1,1,1,1,3,0,0}, {3,1,1,1,1,1,1,1,3,0}, {3,1,1,1,1,1,1,1,3,0}, {3,1,1,1,1,1,1,1,3,0}, {3,1,1,1,1,1,1,1,3,0}, {0,3,1,1,1,1,1,3,0,0}, {0,0,3,3,3,3,3,0,0,0} }; /* -------------- Rutina de crear los sprites ------------- */ void creaSprites() { int i, j; ladrilloFondo = create_bitmap(10, 10); clear_bitmap(ladrilloFondo); for(i=0; i<ANCHOSPRITE; i++) for (j=0; j<ALTOSPRITE; j++) putpixel(ladrilloFondo, i, j, palette_color[ spriteLadrillo[j][i] ]); comida = create_bitmap(10, 10); clear_bitmap(comida); for(i=0; i<ANCHOSPRITE; i++) for (j=0; j<ALTOSPRITE; j++) putpixel(comida, i, j, palette_color[ spriteComida[j][i] ]); jugador = create_bitmap(10, 10); clear_bitmap(jugador); for(i=0; i<ANCHOSPRITE; i++) for (j=0; j<ALTOSPRITE; j++) putpixel(jugador, i, j, palette_color[ spriteJugador[j][i] ]); } /* -------------- Rutina de dibujar el fondo ------------- */ void dibujaFondo() { int i, j; clear_bitmap(screen); for(i=0; i<MAXCOLS; i++) for (j=0; j<MAXFILAS; j++) { if (mapa[j][i] == 'X') draw_sprite(screen, ladrilloFondo, i*ESCALA, j*ESCALA); if (mapa[j][i] == 'F') draw_sprite(screen, comida, i*ESCALA, j*ESCALA); } } /* ------------------------------------------------ */ /* */ /* -------------- Cuerpo del programa ------------- */ int main() { allegro_init(); /* Inicializamos Allegro */ install_keyboard(); install_timer(); /* Intentamos entrar a modo grafico */ if (set_gfx_mode(GFX_SAFE, 320, 200, 0, 0) != 0) { set_gfx_mode(GFX_TEXT, 0, 0, 0, 0); allegro_message( "Incapaz de entrar a modo grafico\n%s\n", allegro_error); return 1; } /* ----------------------- Si todo ha ido bien: empezamos */ creaSprites(); dibujaFondo(); /* Valores iniciales */ posX = POS_X_INI; posY = POS_Y_INI; incX = INC_X_INI; incY = INC_Y_INI; /* Parte repetitiva: */ do { dibujaFondo(); draw_sprite (screen, jugador, posX*ESCALA, posY*ESCALA); terminado = FALSE; /* Si paso por una fruta: la borro y falta una menos */ if (mapa[posY][posX] == 'F') { mapa[posY][posX] = ' '; numFrutas --; if (numFrutas == 0) { textout(screen, font, "Ganaste!", 100, 90, palette_color[14]); terminado = TRUE; } } /* Si choco con la pared, se acabo */ if (mapa[posY][posX] == 'X') { textout(screen, font, "Chocaste!", 100, 90, palette_color[13]); terminado = TRUE; } if (terminado) break; /* Compruebo si se ha pulsado alguna tecla */ if ( keypressed() ) { tecla = readkey() >> 8; switch (tecla) { case TEC_ARRIBA: incX = 0; incY = -1; break; case TEC_ABAJO: incX = 0; incY = 1; break; case TEC_IZQDA: incX = -1; incY = 0; break; case TEC_DCHA: incX = 1; incY = 0; break; } } posX += incX; posY += incY; /* Pequea pausa antes de seguir */ rest ( PAUSA ); } while (TRUE); /* Repetimos indefininamente */ /* (la condicin de salida la comprobamos "dentro") */ readkey(); return 0; } /* Termino con la "macro" que me pide Allegro */ END_OF_MAIN();
7.3. Miniserpiente 1 en Pascal.
El único comentario es que en Free Pascal no existen rutinas incorporadas para manejo de Sprites. Se podrían imitar usando las órdenes "getImage" y "putImage", de la librería Graph, pero nosotros lo haremos directamente imitando la idea básica del funcionamiento de un sprite: para cada punto de la figura, se dibuja dicho punto en caso de que no sea transparente (color 0). Además lo haremos en Pascal puro, sin usar ni siquiera lenguaje ensamblador, que haría nuestro programa más rápido pero también menos legible. El resultado es que nuestro programa será mucho más lento que si tuviéramos rutinas preparadas para manejar Sprites o si empleáramos ensamblador, a cambio de que sea muy fácil de entender.
(*----------------------------*) (* Intro a la programac de *) (* juegos, por Nacho Cabanes *) (* *) (* ipj07p.pas *) (* *) (* Septimo ejemplo: juego de *) (* 'miniSerpiente' *) (* *) (* Comprobado con: *) (* - FreePascal 1.06 (Dos) *) (* - FreePascal 2.0 -Windows *) (*----------------------------*) uses graph, crt; (* Cambiar por "uses wincrt, ..." bajo Windows *) (* Posiciones X e Y iniciales *) const POS_X_INI = 17; POS_Y_INI = 11; INC_X_INI = 1; INC_Y_INI = 0; (* Pausa en milisegundos entre un 'fotograma' y otro *) PAUSA = 350; (* Teclas predefinidas *) TEC_ARRIBA = 'E'; TEC_ABAJO = 'X'; TEC_IZQDA = 'S'; TEC_DCHA = 'D'; var posX, posY: word; (* Posicion actual *) incX, incY: integer; (* Incremento de la posicion *) (* Terminado: Si ha chocado o comida todas las frutas *) terminado: boolean; (* La tecla pulsada *) tecla: char; (* Escala: relacion entre tamao de mapa y de pantalla *) const ESCALA = 10; (* Ancho y alto de los sprites *) ANCHOSPRITE = 10; ALTOSPRITE = 10; (* Y el mapa que representa a la pantalla *) (* Como usaremos modo grafico de 320x200 puntos *) (* y una escala de 10, el tablero medira 32x20 *) MAXFILAS = 20; MAXCOLS = 32; mapa: array[1..MAXFILAS, 1..MAXCOLS] of char = ( 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'X X X', 'X F X X', 'X F X F X', 'X XXXXX X X', 'X X X X', 'X X X X X', 'X X X X X', 'X X X X', 'X X X X', 'X X X X', 'X F X X', 'X X X', 'X X F X', 'X X X X', 'X X X X', 'X X F X X', 'X F X X X', 'X X F X', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' ); const numFrutas:word = 8; (* Nuestros sprites *) (*BITMAP *ladrilloFondo, *comida, *jugador;###*) type tipoSprite = array[1..ANCHOSPRITE,1..ALTOSPRITE] of byte; (* El sprite en si: matriz de 30x30 bytes *) const spriteLadrillo: tipoSprite = ((0,2,2,2,2,2,2,2,2,0), (2,1,1,1,1,1,1,1,1,2), (2,1,1,1,1,1,1,1,1,2), (2,1,1,1,1,1,1,1,1,2), (2,1,1,1,1,1,1,1,1,2), (2,1,1,1,1,1,1,1,1,2), (2,1,1,1,1,1,1,1,3,2), (2,1,1,1,1,1,1,3,3,2), (2,1,1,1,1,1,3,3,2,2), (2,2,2,2,2,2,2,2,2,0) ); const spriteComida: tipoSprite = ((0,0,0,2,0,0,0,0,0,0), (0,0,2,2,0,0,2,2,0,0), (0,4,4,4,2,2,4,4,0,0), (4,4,4,4,4,2,4,4,4,0), (4,4,4,4,4,4,4,4,4,0), (4,4,4,4,4,4,4,4,4,0), (4,4,4,4,4,4,4,4,4,0), (4,4,4,4,4,4,4,4,4,0), (4,4,4,4,4,4,4,4,4,0), (0,4,4,4,4,4,4,4,0,0) ); const spriteJugador: tipoSprite = ((0,0,3,3,3,3,3,0,0,0), (0,3,1,1,1,1,1,3,0,0), (3,1,1,1,1,1,1,1,3,0), (3,1,1,1,1,1,1,1,3,0), (3,1,1,1,1,1,1,1,3,0), (3,1,1,1,1,1,1,1,3,0), (3,1,1,1,1,1,1,1,3,0), (3,1,1,1,1,1,1,1,3,0), (0,3,1,1,1,1,1,3,0,0), (0,0,3,3,3,3,3,0,0,0) ); (* -------------- Rutina de dibujar sprites -------------- *) (* Simplemente dibuja un sprite definido anteriormente. *) (* Copia la sintaxis de "draw_sprite" de Allegro, pero es *) (* una rutina lenta, que solo usa Pascal puro, no seria *) (* adecuada en programas con muchos sprites *) procedure draw_sprite(imagen: tipoSprite; x, y: word); var i,j: word; begin for i := 1 to ANCHOSPRITE do for j := 1 to ALTOSPRITE do begin if imagen[j,i] <> 0 then putpixel(x+i-1, y+j-1, imagen[j,i]); end; end; (* -------------- Rutina de dibujar el fondo ------------- *) procedure dibujaFondo; var i, j: word; begin clearDevice; for i:= 1 to MAXCOLS do for j := 1 to MAXFILAS do begin if mapa[j,i] = 'X' then draw_sprite(spriteLadrillo, (i-1)*ESCALA, (j-1)*ESCALA); if mapa[j,i] = 'F' then draw_sprite(spriteComida, (i-1)*ESCALA, (j-1)*ESCALA); end; end; (* ------------------------------------------------ *) (* *) (* -------------- Cuerpo del programa ------------- *) var gd,gm, error : integer; BEGIN gd := D8bit; gm := m320x200; initgraph(gd, gm, ''); (* Intentamos entrar a modo grafico *) error := graphResult; if error <> grOk then begin writeLn('No se pudo entrar a modo grafico'); writeLn('Error encontrado: '+ graphErrorMsg(error) ); halt(1); end; (* ----------------------- Si todo ha ido bien: empezamos *) dibujaFondo; (* Valores iniciales *) posX := POS_X_INI; posY := POS_Y_INI; incX := INC_X_INI; incY := INC_Y_INI; (* Parte repetitiva: *) repeat dibujaFondo; draw_sprite (spriteJugador, (posX-1)*ESCALA, (posY-1)*ESCALA); terminado := FALSE; (* Si paso por una fruta: la borro y falta una menos *) if (mapa[posY,posX] = 'F') then begin mapa[posY,posX] := ' '; numFrutas := numFrutas - 1; if (numFrutas = 0) then begin setColor(14); outTextXY( 100, 90, 'Ganaste!' ); terminado := TRUE; end; end; (* Si choco con la pared, se acabo *) if (mapa[posY,posX] = 'X') then begin setColor(13); outTextXY( 100, 90, 'Chocaste!' ); terminado := TRUE; end; if terminado then break; (* Compruebo si se ha pulsado alguna tecla *) if keypressed then begin tecla := upcase(readkey); case tecla of TEC_ARRIBA: begin incX := 0; incY := -1; end; TEC_ABAJO: begin incX := 0; incY := 1; end; TEC_IZQDA: begin incX := -1; incY := 0; end; TEC_DCHA: begin incX := 1; incY := 0; end; end; end; posX := posX + incX; posY := posY + incY; (* Pequea pausa antes de seguir *) delay ( PAUSA ); until FALSE; (* Repetimos indefininamente *) (* (la condicin de salida la comprobamos 'dentro') *) readkey; closegraph; end.
7.4. Miniserpiente 1 en Java.
Eso de que el juego no se pare... en Java es algo más complicado: deberemos usar un "Thread", un hilo, que es la parte que sí podremos parar cuando queramos, durante un cierto tiempo o por completo. Vamos a resumir los cambios más importantes:
- En la declaración de la clase deberemos añadir "implements Runnable"
- Tendremos nuevos métodos (funciones): "start" pondrá en marcha la ejecución del hilo, "stop" lo parará, y "run" indicará lo que se debe hacer durante la ejecución del hilo.
- Dentro de este "run" haremos la pausa ("sleep") que necesitamos en cada "fotograma" del juego, y redibujaremos después, así:
try {
Thread.sleep(PAUSA);
} catch (InterruptedException e){
}
Los demás cambios, que no son muchos, son los debidos a la forma de trabajar de los Applets, que ya conocemos (funciones init, paint y las de manejo de teclado), y las propias del lenguaje Java (por ejemplo, va a ser más cómodo definir el mapa como un array de Strings que como un array bidimensional de caracteres).
La apariencia será casi idéntica a las anteriores:
/*----------------------------*/ /* Intro a la programac de */ /* juegos, por Nacho Cabanes */ /* */ /* ipj07j.java */ /* */ /* Septimo ejemplo: juego de */ /* "miniSerpiente" (aprox A) */ /* */ /* Comprobado con: */ /* - JDK 1.5.0 */ /*----------------------------*/ import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class ipj07j extends Applet implements Runnable, KeyListener { // Posiciones X e Y iniciales final int POS_X_INI = 16; final int POS_Y_INI = 10; final int INC_X_INI = 1; final int INC_Y_INI = 0; // Pausa en milisegundos entre un "fotograma" y otro final int PAUSA = 350; // Ahora las imagenes de cada elemento final String LADRILLO = "#"; final String COMIDA = "X"; final String JUGADOR ="O"; // Escala: relacion entre tamao de mapa y de pantalla final int ESCALA = 10; // Y el mapa que representa a la pantalla // Como usaremos modo grafico de 320x200 puntos // y una escala de 10, el tablero medira 32x20 final int MAXFILAS = 20; final int MAXCOLS = 32; String mapa[]={ "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "X X X", "X F X X", "X F X F X", "X XXXXX X X", "X X X X", "X X X X X", "X X X X XXXX", "X X X X", "X X X X", "X X X X", "X F X X", "X X X", "X X F X", "X X X X", "X X X X", "X X F X X", "X F X X X", "X X F X", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }; Thread hilo = null; // El "hilo" de la animacion int posX, posY; // Posicion actual int incX, incY; // Incremento de la posicion // Terminado: Si ha chocado o comido todas las frutas boolean terminado; int tecla; // La tecla pulsada // Y las teclas por defecto final char TEC_ARRIBA = 'e'; final char TEC_ABAJO = 'x'; final char TEC_IZQDA = 's'; final char TEC_DCHA = 'd'; int numFrutas = 8; // Inicializacion public void init() { // Valores iniciales posX = POS_X_INI; posY = POS_Y_INI; incX = INC_X_INI; incY = INC_Y_INI; terminado = false; requestFocus(); addKeyListener(this); } // Escritura en pantalla public void paint(Graphics g) { int i, j; // Primero borro el fondo en negro g.setColor( Color.black ); g.fillRect( 0, 0, 639, 479 ); // Ahora dibujo paredes y comida for(i=0; i<MAXCOLS; i++) for (j=0; j<MAXFILAS; j++) { g.setColor( Color.blue ); if (mapa[j].charAt(i) == 'X') g.drawString(LADRILLO, i*ESCALA, j*ESCALA+10); g.setColor( Color.green ); if (mapa[j].charAt(i) == 'F') g.drawString(COMIDA, i*ESCALA, j*ESCALA+10); } // Finalmente, el jugador g.setColor( Color.white ); g.drawString(JUGADOR, posX*ESCALA, posY*ESCALA+10); // Si no quedan frutas, se acabo g.setColor( Color.yellow ); if (numFrutas == 0) { g.drawString("Ganaste!", 100, 90); terminado = true; } // Si choco con la pared, se acabo g.setColor( Color.magenta ); if (mapa[posY].charAt(posX) == 'X') { g.drawString("Chocaste!", 100, 90); terminado = true; } if (terminado) hilo=null; } // La rutina que comienza el "Thread" public void start() { hilo = new Thread(this); hilo.start(); } // La rutina que para el "Thread" public synchronized void stop() { hilo = null; } // Y lo que hay que hacer cada cierto tiempo public void run() { Thread yo = Thread.currentThread(); while (hilo == yo) { try { Thread.sleep(PAUSA); } catch (InterruptedException e){ } posX += incX; posY += incY; // Si paso por una fruta: la borro y falta una menos if (mapa[posY].charAt(posX) == 'F') { // La borro en el mapa StringBuffer temp = new StringBuffer(mapa[posY]); temp.setCharAt(posX, ' '); mapa[posY] = temp.toString(); // y en el contador numFrutas --; } // En cualquier caso, redibujo repaint(); } } // Comprobacion de teclado public void keyTyped(KeyEvent e) { tecla=e.getKeyChar(); switch (tecla) { case TEC_ARRIBA: incX = 0; incY = -1; break; case TEC_ABAJO: incX = 0; incY = 1; break; case TEC_IZQDA: incX = -1; incY = 0; break; case TEC_DCHA: incX = 1; incY = 0; break; } repaint(); e.consume(); } public void keyReleased(KeyEvent e) { } public void keyPressed(KeyEvent e) { } }