9. Evitemos los parpadeos. Cuarto juego (aproximación "c"): Miniserpiente 3.

Contenido de este apartado:


9.1. Idea básica: el doble buffer.

Nuestros juegos, a pesar de que aún son sencillos, empiezan a tener problemas de parpadeos. Podríamos sincronizar el refresco de la pantalla con el momento de dibujar nuestras figuras, y el problema se reduciría un poco, pero seguiría existiendo. Y es tanto más grave cuantos más elementos dibujamos en pantalla (especialmente en el caso de Pascal, en que hemos usado una rutina "nuestra" para dibujar los sprites, en vez de rutinas "prefabricadas").

Por eso, en la práctica se suele hacer una aproximación ligeramente distinta a la que hemos usado hasta ahora: no escribiremos directamente en pantalla, sino que prepararemos todo lo que va a aparecer, lo dibujaremos en una "pantalla no visible" y en el último momento volcaremos toda esta "pantalla no visible" a la vez. Esta técnica es lo que se conoce como el empleo de un "doble buffer" (en inglés "double buffering").

En informática, un "buffer" es una memoria intermedia en la que se guarda la información antes de llegar a su destino definitivo. Un ejemplo típico es el "buffer" de impresora.

En nuestro caso, esto nos permite preparar toda la información tranquilamente, en un sitio que todavía no será su destino definitivo. Cuando todo está listo, es el momento en el que se vuelca a pantalla, todo a la vez, minimizando los parpadeos.

La forma de conseguir esto con Allegro es algo parecido a:

   BITMAP *bmp = create_bitmap(320, 200);    // Creamos el bitmap auxiliar en memoria
   clear_bitmap(bmp);                        // Lo borramos
   putpixel(bmp, x, y, color);               // Dibujamos en él
   blit(bmp, screen, 0, 0, 0, 0, 320, 200);  // Finalmente,volcamos a pantalla
 

Los cambios en nuestro programa no son grandes: creamos un nuevo bitmap llamado "pantallaOculta", dibujamos en él la nave, los marcianos y el disparo, volcamos a pantalla al final de cada secuencia completa de dibujado, y esta vez no nos molestamos en esperar a que realmente haya "información nueva" en pantalla: volvemos a dibujar aunque realmente sea lo mismo que ya había antes. En cualquier caso, ahora no debería haber parpadeos o estos deberían ser mínimos.


9.2 Miniserpiente 3 en C. 

Pocos cambios...
 

/*----------------------------*/
/*  Intro a la programac de   */
/*  juegos, por Nacho Cabanes */
/*                            */
/*    ipj09c.c                */
/*                            */
/*  Noveno ejemplo: juego de  */
/*  "miniSerpiente" (aprox C) */
/*                            */
/*  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
 
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  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;
 
  /* Nuestros sprites */
BITMAP *ladrilloFondo, *comida, *jugador;
BITMAP *pantallaOculta;   /* Y la pantalla oculta */
 
 
typedef
  char tipoSprite[ANCHOSPRITE][ALTOSPRITE];
                         /* El sprite en si: matriz de 10x10 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]  ]);
 
  pantallaOculta = create_bitmap(320, 200);
}
 
 
/* -------------- Rutina de dibujar el fondo ------------- */
 
void dibujaFondo()
{
  int i, j;
 
  clear_bitmap(pantallaOculta);
 
    for(i=0; i<MAXCOLS; i++)
    for (j=0; j<MAXFILAS; j++) {
      if (mapa[j][i] == 'X')
        draw_sprite(pantallaOculta, ladrilloFondo, i*ESCALA, j*ESCALA);
      if (mapa[j][i] == 'F')
        draw_sprite(pantallaOculta, 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 (pantallaOculta, 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(pantallaOculta, font,
          "Ganaste!", 100, 90, palette_color[14]);
        terminado = TRUE;
      }
    }
 
 
    /* Si choco con la pared, se acabo */
    if (mapa[posY][posX] == 'X') {
      textout(pantallaOculta, font,
        "Chocaste!", 100, 90, palette_color[13]);
      terminado = TRUE;
    }
 
  /* En cualquier caso, vuelco la pantalla oculta */
  blit(pantallaOculta, screen, 0, 0, 0, 0, 320, 200);
 
    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 indefinidamente */
                 /* (la condicin de salida la comprobamos "dentro") */
 
  readkey();
  destroy_bitmap(pantallaOculta);
  destroy_bitmap(jugador);
  destroy_bitmap(comida);
  destroy_bitmap(ladrilloFondo);
  return 0;
 
}
 
                     /* Termino con la "macro" que me pide Allegro */
END_OF_MAIN();
 




9.3. Miniserpiente 3 en Pascal.

(Todavía no disponible: la biblioteca gráfica estándar de Free Pascal no permite usar un doble buffer)





9.4. Miniserpiente 3 en Java.

Los cambios no son grandes: crear un objeto de tipo "Image" con el mismo tamaño que la pantalla: dobleBuffer = createImage(500, 300); Sobre él iremos dibujando todos los elementos, y sólo cuando la imagen está completa es cuando la volcamos sobre la pantalla visible.

El fuente podría quedar:

/*----------------------------*/
/*  Intro a la programac de   */
/*  juegos, por Nacho Cabanes */
/*                            */
/*    ipj09j.java             */
/*                            */
/*  Noveno ejemplo: juego de  */
/*  "miniSerpiente" (aprox C) */
/*  (Contribucion por:        */
/*     Javi Gimenez)          */
/*                            */
/*  Comprobado con:           */
/*  - JDK 1.5.0               */
/*----------------------------*/
 
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.MemoryImageSource;
 
import javax.swing.JApplet ;
 
public class Serpiente extends JApplet 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
    Image ladrilloFondo;
 
    Image comida;
 
    Image jugador;
 
    // 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" };
 
    /* Ancho y alto de los sprites */
    final int ANCHOSPRITE = 10;
 
    final int ALTOSPRITE = 9;
 
    int 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 };
 
    int 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 };
 
    int 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, 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 };
 
    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;
 
    // Auxiliares para el doble buffer
    private Image dobleBuffer;
    private Graphics graphicsDobleBuffer;
 
    // Rutina de crear los sprites
    void creaSprites() {
        int i, j;
 
        // Doy colores ms adecuados a cada figura: ladrillos
        for (i = 0; i < ANCHOSPRITE; i++)
            for (j = 0; j < ALTOSPRITE; j++) {
                // El color 1 sera azul
                if (spriteLadrillo[i + j * ANCHOSPRITE] == 1)
                    spriteLadrillo[i + j * ANCHOSPRITE] = (255 << 24) | 255;
                // El color 2 sera verde
                if (spriteLadrillo[i + j * ANCHOSPRITE] == 2)
                    spriteLadrillo[i + j * ANCHOSPRITE] = (255 << 24)
                            | (255 << 8);
                // El color 3 sera cyan
                if (spriteLadrillo[i + j * ANCHOSPRITE] == 3)
                    spriteLadrillo[i + j * ANCHOSPRITE] = (255 << 24)
                            | (127 << 16) | (127 << 8) | 255;
                // El color 4 sera rojo
                if (spriteLadrillo[i + j * ANCHOSPRITE] == 4)
                    spriteLadrillo[i + j * ANCHOSPRITE] = (255 << 24)
                            | (255 << 16);
            }
        // y al final los asigno
        ladrilloFondo = createImage(new MemoryImageSource(ANCHOSPRITE,
                ALTOSPRITE, spriteLadrillo, 0, ANCHOSPRITE));
 
        // Lo mismo para la comida
        for (i = 0; i < ANCHOSPRITE; i++)
            for (j = 0; j < ALTOSPRITE; j++) {
                // El color 1 sera azul
                if (spriteComida[i + j * ANCHOSPRITE] == 1)
                    spriteComida[i + j * ANCHOSPRITE] = (255 << 24) | 255;
                // El color 2 sera verde
                if (spriteComida[i + j * ANCHOSPRITE] == 2)
                    spriteComida[i + j * ANCHOSPRITE] = (255 << 24)
                            | (255 << 8);
                // El color 3 sera cyan
                if (spriteComida[i + j * ANCHOSPRITE] == 3)
                    spriteComida[i + j * ANCHOSPRITE] = (255 << 24)
                            | (127 << 16) | (127 << 8) | 255;
                // El color 4 sera rojo
                if (spriteComida[i + j * ANCHOSPRITE] == 4)
                    spriteComida[i + j * ANCHOSPRITE] = (255 << 24)
                            | (255 << 16);
            }
        comida = createImage(new MemoryImageSource(ANCHOSPRITE, ALTOSPRITE,
                spriteComida, 0, ANCHOSPRITE));
 
        // Y lo mismo para el jugador
        for (i = 0; i < ANCHOSPRITE; i++)
            for (j = 0; j < ALTOSPRITE; j++) {
                // El color 1 sera azul
                if (spriteJugador[i + j * ANCHOSPRITE] == 1)
                    spriteJugador[i + j * ANCHOSPRITE] = (255 << 24) | 255;
                // El color 2 sera verde
                if (spriteJugador[i + j * ANCHOSPRITE] == 2)
                    spriteJugador[i + j * ANCHOSPRITE] = (255 << 24)
                            | (255 << 8);
                // El color 3 sera cyan
                if (spriteJugador[i + j * ANCHOSPRITE] == 3)
                    spriteJugador[i + j * ANCHOSPRITE] = (255 << 24)
                            | (127 << 16) | (127 << 8) | 255;
                // El color 4 sera rojo
                if (spriteJugador[i + j * ANCHOSPRITE] == 4)
                    spriteJugador[i + j * ANCHOSPRITE] = (255 << 24)
                            | (255 << 16);
            }
        jugador = createImage(new MemoryImageSource(ANCHOSPRITE, ALTOSPRITE,
                spriteJugador, 0, ANCHOSPRITE));
    }
 
    // Inicializacion
    public void init() {
 
        // Valores iniciales
        posX = POS_X_INI;
        posY = POS_Y_INI;
 
        incX = INC_X_INI;
        incY = INC_Y_INI;
 
        terminado = false;
        setSize(500, 300);
        dobleBuffer = createImage(500, 300);
        graphicsDobleBuffer = dobleBuffer.getGraphics();
 
        requestFocus();
        addKeyListener(this);
        creaSprites();
    }
 
    // Escritura en pantalla
    public void paint(Graphics graphics) {
 
        int i, j;
 
        // Primero borro el fondo en negro
        graphicsDobleBuffer.setColor(Color.black);
        graphicsDobleBuffer.fillRect(0, 0, 639, 479);
 
        // Ahora dibujo paredes y comida
        for (i = 0; i < MAXCOLS; i++)
            for (j = 0; j < MAXFILAS; j++) {
                if (mapa[j].charAt(i) == 'X')
                    graphicsDobleBuffer.drawImage(ladrilloFondo, i * ESCALA, j
                            * ESCALA, this);
                if (mapa[j].charAt(i) == 'F')
                    graphicsDobleBuffer.drawImage(comida, i * ESCALA, j
                            * ESCALA, this);
            }
 
        // Finalmente, el jugador
        graphicsDobleBuffer.drawImage (jugador, posX * ESCALA, posY * ESCALA,
                this);
 
        // Si no quedan frutas, se acabo
        graphicsDobleBuffer.setColor(Color.yellow);
        if (numFrutas == 0) {
            graphicsDobleBuffer.drawString ("Ganaste!", 100, 90);
            terminado = true;
        }
 
        // Si choco con la pared, se acabo
        graphicsDobleBuffer.setColor(Color.magenta);
        if (mapa[posY].charAt(posX) == 'X') {
            graphicsDobleBuffer.drawString("Chocaste!", 100, 90);
            terminado = true;
        }
        graphics.drawImage(dobleBuffer, 0, 0, this);
        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) {
    }
 
}