Intro a la programación de juegos

Por Nacho Cabanes

Contenido Indice Cambios Enlaces Autor

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" (aprox A) */
/*                            */
/*  Comprobado con:           */
/* - MinGW DevStudio 2.05 */
/* (gcc 3.4.2) y Allegro */ /* 4.03, Windows XP */ /*----------------------------*/
#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

/* Ahora las imagenes de cada elemento */ #define LADRILLO "#" #define COMIDA "X" #define JUGADOR "O"
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 tamaño 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 33
char mapa[MAXFILAS][MAXCOLS]={
"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"
};
int numFrutas = 8;


/* -------------- 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') textout(screen, font, LADRILLO, i*ESCALA, j*ESCALA, palette_color[14]); if (mapa[j][i] == 'F') textout(screen, font, COMIDA, i*ESCALA, j*ESCALA, palette_color[10]); } }

/* ------------------------------------------------ */ /* */ /* -------------- 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 */
dibujaFondo();
/* Valores iniciales */ posX = POS_X_INI; posY = POS_Y_INI;
incX = INC_X_INI; incY = INC_Y_INI;

/* Parte repetitiva: */ do { dibujaFondo(); textout(screen, font, JUGADOR, posX*ESCALA, posY*ESCALA, palette_color[15]);
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;
/* Pequeña pausa antes de seguir */ rest ( PAUSA );
} while (TRUE); /* Repetimos indefininamente */ /* (la condición 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.

 Muuuuy parecido a la versión en C, como era de esperar:

(*----------------------------*)
(*  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 tamaño de mapa y de pantalla *) const 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 *) MAXFILAS = 20; MAXCOLS = 33;
mapa: array[1..MAXFILAS, 1..MAXCOLS] of char = ( 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', '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', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' );
(* Ahora las imagenes de cada elemento *) LADRILLO = '#'; COMIDA = 'X'; JUGADOR = 'O';
const numFrutas:word = 8;


(* -------------- 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 begin setColor(15); outTextXY( (i-1)*ESCALA, (j-1)*ESCALA, LADRILLO ); end; if mapa[j,i] = 'F' then begin setColor(10); outTextXY( (i-1)*ESCALA, (j-1)*ESCALA, COMIDA ); end; end;
end;

(* ------------------------------------------------ *) (* *) (* -------------- Cuerpo del programa ------------- *) var gd,gm, error : integer;

BEGIN gd := D8bit; gm := m320x200; initgraph(gd, gm, '');
 (* Si falla bajo Windows, probar gd:=0; gm:=0; *)

(* 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; setColor(14); outTextXY( (posX-1)*ESCALA, (posY-1)*ESCALA, JUGADOR );
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;
(* Pequeña pausa antes de seguir *) delay ( PAUSA );
until FALSE; (* Repetimos indefininamente *) (* (la condición 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 esa la parte que sí podremos parar cuando queramos, durante un cierto tiempo o por completo. Resumamos 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 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 tamaño 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) { }
}

Contenido Indice Cambios Enlaces Autor
 
Nacho Cabanes, 2005
Última versión en www.pobox.com/users/ncabanes