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) { } }