sábado, 24 de noviembre de 2012

Animación de Imágenes en Java

votar

En esta entrada del blog, voy a enseñarte cómo hacer imágenes con animación en Java. Para ello, lo primero que vamos a necesitar son...lo has adivinado: imágenes. Pueden ser fotos, o pueden ser dibujos sencillos, pero eso sí, tienen que mostrar movimientos consecutivos; por ejemplo, un pájaro subiendo y bajando las alas, o una cara abriendo y cerrando los ojos.

Si sabes utilizar Photoshop, Gimp o cualquier otro programa por el estilo, enhorabuena, dale rienda suelta a tu imaginación para crear las imágenes que desees (y ya puestos, compártelas con nosotros).

Si por el contrario, eres como yo, que a lo más que llegas es a dibujar un círculo con Paint, pues nada, le pones dos circulitos más pequeños dentro para simular ojos abiertos y dos líneas para simular ojos cerrados y ya tienes una carita que parpadea. Ahora guardas los dos dibujos como .png y listo.

También puedes añadirle una imagen de fondo que ocupe toda la pantalla: una foto en formato .jpg, otro dibujo o un fondo liso, lo que quieras.

Lo que vamos a hacer con estas imágenes es, primero, crear un ArrayList para contenerlas, y después, asignarles a cada una una duración determinada en milisegundos. Por último, vamos a mostrarlas en pantalla en un bucle para conseguir ese efecto de animación.

Para ello vamos a crear una clase, Animación (qué sorpresa de nombre, ¿eh?) que contendrá los métodos necesarios. Despues haremos otra para probarla, y originales como somos la llamaremos AnimaciónTest. PruebaDeAnimación tampoco está mal, pero es que es más largo. Dentro de esta clase AnimaciónTest vamos a utilizar un objeto de la clase PantallaCompleta, que creamos en una entrada anterior y que utilizamos, nunca lo adivinarías, para visualizar el resultado en una pantalla completa . Aquí puedes ver el código de esa clase.

Para que luego no te líes con el código, te voy a explicar el significado de algunas variables que verás en él:
  • frames, que en inglés viene siendo fotogramas, es el nombre del ArrayList
  • actualFrame es el número de índice de un elemento de frames
  • tiempoAnimación es el tiempo que dura toda la animación
  • tiempoTotal es la suma de los milisegundos de duración que hemos asignado a cada imagen
Clase 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;
  }
 }
}
Clase AnimaciónTest.java

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

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(displayMode);
    }

    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(DisplayMode displayMode) {
        pc = new PantallaCompleta();
        try {
            pc.setFullScreen(displayMode, new JFrame());
            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
            Graphics g =
                pc.getFullScreenWindow().getGraphics();
            draw(g);
            g.dispose();

            // 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);
    }

}
Yo he utilizado una imagen de fondo y tres dibujos de una cara, pero tú puedes usar el número de imágenes que quieras. Fíjate que cada imagen se puede añadir más de una vez, en distinto orden y con distinto tiempo asignado, para crear distintas animaciones. Tú puedes jugar con ello hasta encontrar la que te gusta.

En otro momento te enseñaré a añadirle otros efectos y a utilizar la clase BufferStrategy para  evitar que la imagen parpadee.

¡Si tienes alguna duda, ya sabes dónde estoy!

sábado, 17 de noviembre de 2012

Thread Pools en Java

votar

En otras entradas he explicado que son los hilos o threads, cómo crearlos y cómo funcionan. En esta entrada voy a tratar el tema de las thread pools, literalmente “piscinas de hilos”. ¿Por qué se llaman así? Bueno...ni idea. Pero ya que le han dado ese nombre, voy a utilizarlo en mi metáfora para explicarte en que consisten.

Imagínate una thread pool como un grupo de nadadores vagueando al borde de una piscina. De vez en cuando llega un entrenador con la orden de que uno de los nadadores tiene que recorrer la piscina de arriba abajo. A veces todos los nadadores reciben esas órdenes, un entrenador por cada nadador. En ocasiones hay más entrenadores que nadadores, y los últimos en llegar tienen que esperar a que vayan regresando los nadadores para ir dándoles de nuevo la orden de recorrer la piscina. Y otras veces sólo algunos nadadores están nadando mientras los otros están jugando a las cartas, esperando a que llegue algún entrenador.

Ahora sustituye “entrenador” por “tarea” y “nadador” por “hilo” y ya tienes una thread pool.

“Vale”, te preguntarás, “¿y por qué hacer una thread pool? ¿No es mejor ir creando los hilos según los necesitemos?”. Pues eso va a depender del programa en cuestión. Ten en cuenta que cada hilo que creas ocupa lugar en la memoria. Si tienes un programa que sólo va a tener tres o cuatro tareas distintas, pues bueno, creas tres o cuatro hilos. Pero, ¿y si es un programa con cientos o miles de tareas?. Cientos de hilos ocuparán bastante memoria. Y lo que es peor: tú tendrás que escribir el código para crear todos esos hilos.Créeme, es buena práctica separar en ese caso las tareas de los hilos en tu código. Y tú trabajarás menos.

Además, está la cuestión de la eficiencia. Ya sé lo que estás pensando: “¿No es más eficiente tener un hilo para cada tarea, en lugar de tener tareas esperando que haya un hilo libre?” Volvamos a nuestro ejemplo de la piscina.

Supón que en la piscina hay cien nadadores y que todos reciben la orden de nadar a la vez. ¿Has tratado alguna vez de nadar en una piscina abarrotada? ¿Crees que alguno de los nadadores conseguiría un récord de velocidad? Pues tu programa lo mismo. El sistema en el que se ejecute tendrá que repartir sus recursos entre todos los hilos a la vez.

Hagamos ahora que los nadadores sean sólo diez, con las mismas cien tareas. Es cierto que hay algunas tareas que tendrán que esperar a que los nadadores vayan regresando para realizarse, pero como los nadadores pueden ir y volver mucho más rápido, las primeras tareas serán “visto y no visto” y el tiempo de espera de las últimas será similar al producido si todas tuviesen un hilo propio. Y eso si todas las órdenes se reciben a la vez, porque si los entrenadores van llegando de forma separada, el tiempo de espera de los últimos no será mayor que el de los primeros.

Bueno, ya sabemos lo que es una thread pool, ¿pero cómo se utiliza?. Ah, aquí entran en juego los “Ejecutores”. No, no es una película de Schawzenegger. Los ejecutores en java son simplemente los que se encargan de manejar la ejecución de las tareas.

Java tiene una clase, Executors, con métodos estáticos para la creación de thread pools, y tres interfaces:
  • Executor, que simplemente ejecuta las tareas
  • ExecutorService, subinterfaz de la anterior que añade nuevos métodos para controlar las tareas.
  • ScheduledExecutorService, subinterfaz de ExecutorService que soporta la ejecución de tareas programadas.
Todas ellas forman parte del paquete java.util.concurrent.

A continuación, te voy a mostrar un ejemplo sencillísimo de thread pool. Vamos a crear dos clases, una que implementa la interfaz Runnable, porque los ejecutores trabajan con objetos "runnable" o "callable", y otra que contiene el método principal y donde crearemos una thread pool de 10 hilos.

Tarea.java

public class Tarea implements Runnable{
   private int sleepTime;
   private String name;
   public Tarea(String name){
    this.name = name;//le asignamos un nombre a cada tarea.
    sleepTime = 1000;
 }
   
   public void run(){
 try{
    System.out.printf("El hilo de la tarea "+this.name+" va a dormir durante %d milisegundos.\n",sleepTime);
    Thread.sleep(sleepTime);//hacemos que cada hilo duerma durante 1 segundo
 }catch(InterruptedException exception){
    exception.printStackTrace();
 }
 System.out.println("Este hilo ya ha dormido bastante");
 }
}


EjemploThreadPool.java

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EjemploThreadPool{
   public static void main (String args[]){
    System.out.println("Comienza la ejecución");
    ExecutorService ex = Executors.newFixedThreadPool(10);
 Tarea t;
 for(int i = 0;i<200; i++){
    t = new Tarea(""+i);
 
    ex.execute(t);
 }
    ex.shutdown();
    
   }
}


¿Qué es lo que hace este programa? No gran cosa. Se limita a escribir en la consola que el hilo que está ejecutando cada tarea, a las que hemos asignado un número, va a dormir durante 1 segundo. Pero, ¿y lo que has aprendido?

Fíjate que al método execute es al que hay que darle un objeto runnable, y que el método shutdown se asegura de cerrar al finalizar las tareas. Ambos métodos pertenecen a la interfaz ExecutorService. Para crear  la trhead pool hemos empleado el método newFixedThreadPool(), al que se le da un número específico de hilos. Pero existen más, como el newCachedThreadPool(), ambos de la clase Executors.

Para ampliar conocimientos, te recomiendo el tutorial sobre concurrencia de Oracle, y el libro "Java Threads", de S. Oaks y H. Wong, publicado por O´Reilly.

Y como siempre, si tienes alguna duda o comentario, ya sabes donde estamos.

sábado, 10 de noviembre de 2012

Cómo crear una pantalla completa para juegos en Java

votar
Supongamos que has estado aprendiendo Java durante algún tiempo; ya sabes todo lo que hay que saber sobre clases, objetos, métodos, polimorfismo, threads...Pero ahora quieres hacer algo realmente espectacular, como un juego (no para presumir, por supuesto, sólo para ampliar conocimientos). Y no un juego cualquiera, no una pequeña applet ni un juego embebido en una frame, sino un juego a pantalla completa, sin menús, ni barras horizontales o laterales que distraigan al jugador. Pero, ¿por dónde empezar?

Bueno, evidentemente, excede los límites de cualquier entrada de un blog el explicarte cómo hacer un juego en Java, pero lo que sí puedo hacer es mostrarte cómo crear una pantalla a tamaño completo para él. Antes de nada, vamos a aclarar algunos conceptos que necesitas saber (si ya los sabes, pues te vas directamente al código y listo).

Al diseñar un juego, hay que tener en cuenta que se tiene que poder ver en distintos ordenadores, de los que no vas a saber la resolución de los monitores ni la capacidad de la tarjeta de vídeo.¿Qué es la resolución? La pantalla de un monitor está dividida en pequeños puntitos llamados píxeles. El número de píxeles horizontales y verticales es la resolución de esa pantalla. Por ejemplo, un monitor con 800 píxeles horizontales y 600 píxeles verticales tiene una resolución de 800x600. Ten en cuenta que los píxeles se empiezan a contar desde 0, es decir 0 es el primer píxel y 799 el último.

Si tú pones una resolución en la pantalla de tu juego, pero el monitor tiene otra, la imagen no será tan nítida. Lo ideal es que el jugador pueda elegir la resolución según su monitor. Las resoluciones más utilizadas hoy en día son 800x600, 1024x768 y 1280x1024. En el código que verás después, hemos puesto una resolución fija, para no hacerlo muy extenso, pero tú puedes poner la de tu propio monitor si no es la misma.

Otro detalle en el que hay que fijarse, es en el Display Mode, o capacidad de bits de un monitor, que determina el número de colores que ese monitor puede mostrar. En nuestro ejemplo vamos a dejarlo en 32, que son 2 elevado a 32  colores (¿tienes una calculadora científica a mano? ¿no? bueno, son muchos colores) Si tu monitor es un poco más antiguo o prefieres aumentar la rapidez, puedes usar 16 bit, que son (esta me la sé) 65.536 colores.

Por último tenemos el Refresh Rate, que viene siendo la rapidez con la que la pantalla se está "repintando" una y otra vez. Lo que a ti te parece una imagen fija, en realidad no lo es, porque la luz de los píxeles se desvanece y el monitor está continuamente "encendiéndolos" de nuevo.

Ahora que ya hemos aclarado estos aspectos, para que sepas lo que son cuando los veas en el código, te voy a explicar lo que vamos a hacer. Vamos a crear una ventana, como las típicas de Windows, pero sin ninguna decoración, es decir, sin menús ni barras, ni botoncito de ningún tipo. En un juego normal, por supuesto, tienes la opción de salir cuando quieras, pero aquí, para favorecer la claridad del código y de lo que quiero enseñarte, vamos a dejar que esa pantalla se mantenga unos segundos y luego se desvanezca, que no es cuestión de que te quedes para siempre con ella. Si deseas que tenerla durante más o menos segundos, sólo tienes que cambiar el valor que yo le he puesto.

Hay algunos sistemas que no dejan que cambies el display que tienen por defecto, por lo que tendremos que  asegurarnos primero de que ése no es el caso. 

Así que vamos a hacer dos clases, la primera será la que tenga el constructor de la pantalla, y la segunda será la que tenga el método main, donde probaremos a ver si funciona. Con ellas crearemos una pantalla completa con un color de fondo naranja y una frase en negro, que se mantendrá durante 10 segundos.

PantallaCompleta.java

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

public class PantallaCompleta{
   private GraphicsDevice gd;//tarjeta gráfica
   public PantallaCompleta(){
      //hay que usar los medios del propio sistema
      GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
   gd = env.getDefaultScreenDevice();
 }
 
 public void setFullScreen(DisplayMode dm, JFrame ventana){
    ventana.setUndecorated(true);
    ventana.setResizable(false);
    gd.setFullScreenWindow(ventana);
    
    //comprobamos que el sistema soporta el cambio
    if(dm != null && gd.isDisplayChangeSupported()){
       try{
      gd.setDisplayMode(dm);
    }catch(IllegalArgumentException e){}
  }
 }
 
 public Window getFullScreenWindow(){
    return gd.getFullScreenWindow();
 }
 
 //restauramos los valores previos
 public void restoreScreen(){
    Window w = gd.getFullScreenWindow();
    if(w != null){
       w.dispose();
  }
  gd.setFullScreenWindow(null);
 }
}

TestPantallaCompleta.java

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

public class TestPantallaCompleta extends JFrame{
   public static void main(String[]args){
      //Pon aquí los valores que prefieras
      DisplayMode dm = new DisplayMode(1024,768,32,DisplayMode.REFRESH_RATE_UNKNOWN);
   TestPantallaCompleta test = new TestPantallaCompleta();
   test.run(dm);
 }
 
 public void run (DisplayMode dm){
    setBackground(Color.ORANGE);
    setForeground(Color.BLACK);
    setFont(new Font("Dialog", Font.PLAIN,48));
    PantallaCompleta pc = new PantallaCompleta();
    try{
       pc.setFullScreen(dm, this);
    try{
      //ponemos el tiempo que queremos que dure
      Thread.sleep(10000);
    }catch(InterruptedException ex){}
  }finally{//pase lo que pase, se restaura a sus antiguos valores
     pc.restoreScreen();
  }
 }
 
 public void paint(Graphics g){
    if (g instanceof Graphics2D){
       Graphics2D g2 = (Graphics2D) g;
    //difuminamos los bordes del texto
    g2.setRenderingHint(
       RenderingHints.KEY_TEXT_ANTIALIASING,
       RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    }     
           //el texto va a aparecer 300px a la derecha y 400px hacia abajo
    g.drawString("Aquí va a ir mi juego",300,400);
 }
}

Eso de "antialiasing" que ves al final del código, no es que no dejes a los jugadores tener un alias, sino que sirve para que el texto aparezca como escalonado, como escrito en cuadraditos.

A continuación te dejo una captura de pantalla para que veas cómo queda con los valores que yo le he asignado.



Si quieres más información sobre cómo crear juegos en Java, te recomiendo el libro Developing Games in Java, de David Brackeen, Bret Barker y Lawrence Vanhelsuwe, editorial New Riders Publishing. El código que te muestro aquí es una versión del que aparece en este libro. No sé si hay traducción en castellano, pero como creo que es muy interesante es seguro que en otras entradas os muestre otros ejemplos sacados de él.

Además he encontrado unos tutoriales en vídeo en Youtube, que su autor no lo dice, pero yo juraría que están basados en este libro. Están en inglés, pero aunque no lo entiendas, puede que el código que muestra te sea útil. Podéis encontralos en esta página.

domingo, 4 de noviembre de 2012

Drag and Drop: cómo funciona

votar
¡Después de unos cuantos meses de ausencia, por fin he vuelto! No, no me he ido de vacaciones (que más quisiera), sino que el imponderable deber de tener que trabajar para vivir se interpuso entre este blog y yo. ¡Y mira que os pongo anuncios para que clickéis y me libréis de ese problema! Pero, nada, no os dáis por enterados. ;-)

Hablando ahora ya en serio, esta entrada esta dedicada al Drag and Drop. Para los que no sepáis lo que es, os diré que no se trata del nombre de un grupo pop, sino de ese efecto de arrastrar elementos de un lugar a otro, como cuando arrastramos ficheros de una carpeta al escritorio del ordenador.

En principio, todos los componentes de Swing soportan el Drag and Drop, pero algunos tienen instalada la forma de ejecutarlo por defecto y otros no. Para los primeros, basta con invocar el método setDragEnabled(true) y listo. Para los segundos, hay que escribir el código necesario para adaptarlo a tus necesidades.

Los componentes que tienen un mecanismo de arrastre por defecto son:
  • JColorChooser
  • JEditorPane
  • JFileChooser
  • JFormattedTextField
  • JList
  • JTable
  • JTextArea
  • JTextPane
  • JTree
En cuanto al mecanismo para "dejar caer" los datos que queremos transferir, en estos componentes no hay que hacer nada más:
  • JEditorPane
  • JFormattedTextField
  • JPasswordField
  • JTextArea
  • JTextField
  • JTextPane
  • JColorChooser
mietras que estos otros necesitan un poco más de código para funcionar:
  • JList
  • JTable
  • JTree
Para los componentes que no tienen un método de trasnferencia de datos por defecto, deberemos usar la clase TransferHandler.

A continuación voy a poneros un par de ejemplos, simplificados hasta el extremo, para que veáis el funcionamiento del Drag and Drop en Java. En el primero crearemos dos selectores de color y arrastraremos el color seleccionado en el primero hasta el segundo. Fijaos que no podemos arrastrar desde los cuadraditos pequeños, sino desde la vista previa. Este primer ejemplo es una adaptación del que podéis encontrar en la siguiente página.

import java.awt.BorderLayout;
import javax.swing.JColorChooser;
import javax.swing.JFrame;

public class DobleColor {
  public static void main(String args[]) {
    JFrame frame = new JFrame("Doble Selector de Color ");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JColorChooser izquierda = new JColorChooser();
    izquierda.setDragEnabled(true);
    frame.add(izquierda, BorderLayout.WEST);
    JColorChooser derecha = new JColorChooser();
    derecha.setDragEnabled(true);
    frame.add(derecha, BorderLayout.EAST);

    frame.pack();
    frame.setVisible(true);
  }
Podéis ver aquí cómo quedaría, tras haber arrastrado el color seleccionado desde el primero hasta el segundo.



En el siguiente ejemplo, vamos a arrastrar un texto desde una etiqueta o JLabel a un campo de texto o JTextField. Para ello utilizaremos los método setTransferHandler y getTransferHandler, además de crear un evento al presionar con el ratón, para que funcione el gesto de arrastrar.


import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.JFrame;

public class SimpleDnDs extends JFrame{
    //Vamos a crear componentes
    private JTextField textField;
    private JLabel label;
    private JPanel panel;
 
 public static void main (String args[]){
     SimpleDnDs dnd = new SimpleDnDs();
 }
     
    public SimpleDnDs(){ //constructor de SimpleDnDs
      JFrame frame = new JFrame("Drag and Drop");
      panel = new JPanel();   
      textField = new JTextField(40);
      label = new JLabel("Este es el texto a arrastrar");
   
   //especificamos qué tipo de dato vamos a transferir
      label.setTransferHandler(new TransferHandler("text"));
      MouseListener ml = new MouseAdapter(){
     //creamos el método para transferir
     //datos al presionar con el ratón
        public void mousePressed(MouseEvent e){
        JComponent jc = (JComponent)e.getSource();
        TransferHandler th = jc.getTransferHandler();
        th.exportAsDrag(jc, e, TransferHandler.COPY);
        }
      };
      label.addMouseListener(ml);
   panel.add(label);
   panel.add(textField);
   frame.add(panel); //añadimos el panel al marco
      frame.setSize(600,600); //le ponemos las medidas que queramos
      frame.setLayout(new FlowLayout()); 
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
      frame.setLocationRelativeTo(null);
      frame.setVisible(true); 
   }
}
Una vez ejecutado el programa, podréis arrastrar el texto desde la etiqueta al campo de texto. Es por supuesto muy sencillito, sólo para que veáis cómo funciona, y podáis crear vuestros propios programas, jugando con distintos componentes y tipos de datos a transferir.

Para ello, recomiendo a los que sepáis inglés que echéis un ojo al tutorial de Oracle sobre Drag and Drop. Y como siempre, si tenéis dudas o comentarios, ya sabéis cómo. Además, si queréis saber cuándo publico las siguientes entradas sin tener que estar pendientes del blog, os recomiendo que os suscribáis.