sábado, 15 de diciembre de 2012

La clase BufferStrategy

votar
En una entrada anterior te había explicado cómo hacer una pequeña animación en Java. Ahora bien, es posible que te hayas dado cuenta de que no es todo lo perfecta que debiera, si no que se producen pequeños desajustes, o parpadeos, entre las imágenes, o que incluso a veces se pueden ver algo superpuestas.

Esto es así porque el programa está "dibujando" las imágenes a la vez que el sistema está continuamente refrescando la pantalla. Una forma de evitarlo es crear un espacio de almacenamiento donde el programa dibuje la imagen y no a muestre hasta que esté totalmente acabada. Exactamente esto es lo que hace la clase BufferStrategy. Además, se encarga que las imágenes no se muestren más rápido de la velocidad con la que el monitor se refresca, es decir, supongamos que tú tienes un juego en el que aparecen 150 escenas por segundo, pero tu monitor funciona a 75Hz, lo que significa que sólo puede mostrar 75 escenas por segundo, pues la clase BufferStrategy adapta el programa a esta velocidad. 

Lo bueno es que tú sólo tienes que implementarla, ella solita se encarga de manejar los recursos del sistema. Se puede utilizar tanto con Canvas como con Windows, y para crearla elige primero el número de lugares de almacenamiento o "buffers" que deseas y luego utiliza el método createBufferStrategy(), por ejempo así:

ventana.createBufferStrategy(2);

donde 2 es el número de buffers que creamos. Para invocarlo usaremos el método getBufferStrategy(). Por supuesto, hay que dibujar la imagen en el buffer y luego mostrarla. Esto se hace de la siguiente manera:

BufferStrategy buffer = ventana.getBufferStrategy();
Graphics g = buffer.getDrawGraphics();
draw (g);
g.dispose();
buffer.show();

Te dejo a continuación una actualización del código visto en la entrada sobre animación, PantallaCompleta.java, donde ya está implementada la clase BufferStrategy, y ya puestos, se añaden otras mejoras, como el método getCompatibleDisplayModes(), que te ayudará a conseguir el mejor rendimiento posible del sistema que estés utilizando. También te dejo los códigos de AnimaciónTest.java y Animación.java, para que los tengas todos a mano, aunque también los tienes en la entrada correspondiente.
Si aún no sabes crear una pantalla a modo completo, visita esta otra entrada.

PantallaCompleta.java

import java.awt.*;
import java.awt.image.BufferStrategy;
import javax.swing.JFrame;
import java.awt.image.BufferedImage;

public class PantallaCompleta{
   private GraphicsDevice gd;
   public PantallaCompleta(){
      //hay que usar los medios del propio sistema
      GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
   gd = env.getDefaultScreenDevice();
 }
 
 //devuelve una lista de los modos de visualización compatibles con las características de nuestro sistema
 public DisplayMode[] getCompatibleDisplayModes(){
    return gd.getDisplayModes();
 }
 
 //devuelve el primer modo compatible o nada si no encuentra ninguno
 public DisplayMode findFirstCompatibleMode(DisplayMode modes[]){
    DisplayMode goodModes[] = gd.getDisplayModes();
    for(int i = 0; i < modes.length; i++){
     for(int j = 0; j < goodModes.length; j++){
     if(displayModesMatch(modes[i], goodModes[j])){
      return modes[i];
    }
  }
    }
    return null;
  }
  
  //devuelve el modo de visualización actual
  public DisplayMode getCurrentDisplayMode(){
     return gd.getDisplayMode();
 }
 
 /**
     Determina si dos modos de visualización coinciden, es decir,
  si tienen la misma resolución, velocidad de actualización de
  pantalla, etc.
 */ 
 public boolean displayModesMatch(DisplayMode mode1, DisplayMode mode2){
    if(mode1.getWidth() != mode2.getWidth() || mode1.getHeight() != mode2.getHeight()){
     return false;
    }
    if (mode1.getRefreshRate() !=
        DisplayMode.REFRESH_RATE_UNKNOWN &&
     mode2.getRefreshRate() !=
     DisplayMode.REFRESH_RATE_UNKNOWN &&
     mode1.getRefreshRate() != mode2.getRefreshRate()){
      return false;
  }
  return true;
 }
      
 /**
     Se pone la pantalla a modo pantalla completa y se cambia
  el modo de visualización. En caso de que no hubiese ninguno
  compatible o no se pudiese cambiar, se utiliza el que tiene
  el sistema
 */
 public void setFullScreen(DisplayMode displaymode){
    JFrame ventana = new JFrame();
    ventana.setUndecorated(true);
    ventana.setIgnoreRepaint(true);
    ventana.setResizable(false);
    
    gd.setFullScreenWindow(ventana);
    
    //comprobamos que el sistema soporta el cambio
    if(displaymode != null && gd.isDisplayChangeSupported()){
       try{
      gd.setDisplayMode(displaymode);
    }catch(IllegalArgumentException e){}
  }
  ventana.createBufferStrategy(2);
 }
 
 public Graphics2D getGraphics(){
    Window window = gd.getFullScreenWindow();
    if(window != null){
       BufferStrategy strategy = window.getBufferStrategy();
    return(Graphics2D)strategy.getDrawGraphics();
  }else{
    return null;
  }
 }
 
 public void update(){
    Window window = gd.getFullScreenWindow();
    if(window != null){
       BufferStrategy strategy = window.getBufferStrategy();
    if(!strategy.contentsLost()){
      strategy.show();
    }
  }
  
  //sincronizamos con la ventana del sistema
  Toolkit.getDefaultToolkit().sync();
 }
 
 public Window getFullScreenWindow(){
    return gd.getFullScreenWindow();
 }
 
 public int getWidth(){
    Window window = gd.getFullScreenWindow();
    if(window != null){
       return window.getWidth();
  }else{
    return 0;
  }
 }
 
 public int getHeight(){
    Window window = gd.getFullScreenWindow();
    if(window != null){
     return window.getHeight();
    }else{
     return 0;
    }
 }
 
 //restauramos los valores previos
 public void restoreScreen(){
    Window w = gd.getFullScreenWindow();
    if(w != null){
       w.dispose();
  }
  gd.setFullScreenWindow(null);
 }
 
 /**
    Creamos una imagen compatible con el modo de visualización,
    que se almacena en la memoria del sistema
 */
 public BufferedImage createCompatibleImage(int w, int h, int transparency){
    Window window = gd.getFullScreenWindow();
    if(window != null){
      GraphicsConfiguration gc = window.getGraphicsConfiguration();
   return gc.createCompatibleImage(w, h, transparency);
  }
  return null;
 }
}


Animación.java



import java.awt.Image;
import java.util.ArrayList;

public class Animación{
   private ArrayList frames;
   private int actualFrame;
   private long tiempoAnimación;
   private long tiempoTotal;
   
   public Animación(){
      frames = new ArrayList();
   tiempoTotal = 0;
   start();
 }
 
 public synchronized void addFrame(Image image, long duración){
    tiempoTotal += duración;
    frames.add(new AnimFrame(image, tiempoTotal));
 }
 
 public synchronized void start(){
    tiempoAnimación = 0;
    actualFrame = 0;
 }
 
 public synchronized void update(long tiempoTranscurrido){
    if(frames.size()>1){
     tiempoAnimación += tiempoTranscurrido;
     if(tiempoAnimación >= tiempoTotal){
     tiempoAnimación = tiempoAnimación % tiempoTotal;
     actualFrame = 0;
  }
  while(tiempoAnimación > getFrame(actualFrame).endTime){
     actualFrame++;
  }
    }
 }
 
 public synchronized Image getImage(){
    if (frames.size() ==0){
       return null;
  }else{
     return getFrame(actualFrame).image;
  }
 }
 
 private AnimFrame getFrame(int i){
    return (AnimFrame)frames.get(i);
 }
 
 private class AnimFrame{
    Image image;
    long endTime;
    public AnimFrame(Image image, long endTime){
       this.image = image;
    this.endTime = endTime;
  }
 }
}


AnimaciónTest.java


import java.awt.*;
import javax.swing.ImageIcon;


public class AnimaciónTest {

    public static void main(String args[]) {

       //DisplayMode displayMode= new DisplayMode(1024, 768, 32,
                //DisplayMode.REFRESH_RATE_UNKNOWN);
        AnimaciónTest test = new AnimaciónTest();
        test.run();
    }
    private static final DisplayMode POSSIBLE_MODES[]={
     new DisplayMode(800, 600, 32, 0),
        new DisplayMode(800, 600, 24, 0),
        new DisplayMode(800, 600, 16, 0),
        new DisplayMode(640, 480, 32, 0),
        new DisplayMode(640, 480, 24, 0),
        new DisplayMode(640, 480, 16, 0)
    };

    private static final long DEMO_TIME = 10000;

    private PantallaCompleta pc;
    private Image bgImage;
    private Animación animación;

    public void loadImages() {
        // cargamos las imágenes
        bgImage = loadImage("images/background.jpg");
        Image cara1 = loadImage("images/cara1.png");
        Image cara2 = loadImage("images/cara2.png");
        Image cara3 = loadImage("images/cara3.png");

        // creamos la animación
        animación = new Animación();
        animación.addFrame(cara1, 250);
        animación.addFrame(cara2, 150);
        animación.addFrame(cara1, 150);
        animación.addFrame(cara2, 150);
        animación.addFrame(cara3, 200);
        animación.addFrame(cara2, 150);
    }


    private Image loadImage(String fileName) {
        return new ImageIcon(fileName).getImage();
    }


    public void run() {
        pc = new PantallaCompleta();
        try {
      DisplayMode displayMode = pc.findFirstCompatibleMode(POSSIBLE_MODES);
            pc.setFullScreen(displayMode);
            loadImages();
            animationLoop();
        }
        finally {
             pc.restoreScreen();
        }
    }


    public void animationLoop() {
        long tiempoInicio = System.currentTimeMillis();
        long tiempoActual = tiempoInicio;
        while (tiempoActual - tiempoInicio < DEMO_TIME) {
            long tiempoTranscurrido =
                System.currentTimeMillis() - tiempoActual;
            tiempoActual += tiempoTranscurrido;

            // actualizamos la animación
            animación.update(tiempoTranscurrido);

            // se dibuja en pantalla
            Graphics2D g =
                pc.getGraphics();
            draw(g);
            g.dispose();
   pc.update();

            // una pequeña pausa
            try {
                Thread.sleep(20);
            }
            catch (InterruptedException ex) { }
        }

    }


    public void draw(Graphics g) {
        // se dibuja el fondo
        g.drawImage(bgImage, 0, 0, null);

        // se dibuja la imagen y la centramos más o menos
        g.drawImage(animación.getImage(), 300, 200, null);
    }

}


Si quieres ver más a fondo la clase BufferStrategy, puedes visitar estas páginas:


Vale, están todas en inglés, así que, si no lo dominas, es una suerte que esté yo, ¿eh? ;-)

4 comentarios:

  1. buen blog y bien explicado, gracias ;)

    ResponderEliminar
  2. Ahi alguna manera de implementar un botón dentro de un frame o panel(aunque BufferStrategy solo es nativo para JFrame y windows, se puede implementar en un Jpanel) que este usando BufferStrategy??, lo he intetnado y no he podido, y no quiero tener que implementar una imagen de un boton y las clases de Listener Mouse. Gracias de antemano

    ResponderEliminar
  3. Gracias, pero ya lo solucione.

    Por si alguien mas tiene la misma duda que yo, busque un poco mas sobre en que consiste BufferStrategy y es únicamente la implementación de un DoubleBuffer para contenedores como JFrame, windows y canvas, por lo que en el caso de un JPanel podes usar un DoubleBuffer(true) común y silvestre y listo, obtienes lo mismo, aun así lo podes optimizar usando threads y paints parciales, si la implementación que le darán lo permite.

    ResponderEliminar