Página Anterior - Medición del rendimiento | Página Actual - 13 - Reorganización del código Java | Página Siguiente - Fotogramas |
Índice del curso |
(c) Alexander Hristov
En este punto el programa Invaders.java está convirtiéndose en una masa de código entremezclada; nada que ver con la programación orientada a objetos o con un buen estilo de programación. Si no se tiene experiencia, es normal haber recorrido este camino y llegar a esta situación. Lo importante es saber detenerse, como vamos a hacer nosotros aquí, y ver si existe alguna mejor forma de organizar el código. Con el tiempo y según se vaya acumulando experiencia en la programación en Java o cualquier otro lenguaje orientado a objetos, las decisiones que vamos a tomar aquí las habríamos tomado al principio en base a los comportamientos y tipos de "cosas" que preveamos que habrá en el programa final.
Inicialmente vamos a pensar que "cosas" (entidades) distintas tenemos en el programa :
Tenemos una parte del programa - relativamente independiente de las demás - que se dedica a gestionar un "caché de sprites". Esa parte del programa, que tiene una serie de datos propios (los sprites cargados) y comportamientos propios (leer y proporcionar sprites) es candidata típica para convertirse en una clase independiente. Cuando se define una clase nueva, es buena idea pensar qué responsabilidades o interacciones tendrá esa clase con el resto del sistema. En este caso la clase es muy sencilla y su única interacción es "dar gráficos" a quien lo necesite. Por lo tanto su único método público será el siguiente:
public BufferedImage getSprite(String nombre) {
Una vez que tenemos claros los servicios que proporcionará la clase, debemos preguntarnos qué necesitará internamente para llevar a cabo su cometido. En este caso, lógicamente la clase necesitará el mapa de sprites y el método para cargar una imagen en memoria. Con todo esto, la clase SpriteCache quedaría como sigue:
SpriteCache.java |
1 /** 2 * Curso Básico de desarrollo de Juegos en Java - Invaders 3 * 4 * (c) 2004 Planetalia S.L. - Todos los derechos reservados. Prohibida su reproducción 5 * 6 * http://www.planetalia.com 7 * 8 */ 9 package version13; 10 11 import java.awt.image.BufferedImage; 12 import java.net.URL; 13 import java.util.HashMap; 14 15 import javax.imageio.ImageIO; 16 17 public class SpriteCache { 18 private HashMap sprites; 19 20 public SpriteCache() { 21 sprites = new HashMap(); 22 } 23 24 private BufferedImage loadImage(String nombre) { 25 URL url=null; 26 try { 27 url = getClass().getClassLoader().getResource(nombre); 28 return ImageIO.read(url); 29 } catch (Exception e) { 30 System.out.println("No se pudo cargar la imagen " + nombre +" de "+url); 31 System.out.println("El error fue : "+e.getClass().getName()+" "+e.getMessage()); 32 System.exit(0); 33 return null; 34 } 35 } 36 37 public BufferedImage getSprite(String nombre) { 38 BufferedImage img = (BufferedImage)sprites.get(nombre); 39 if (img == null) { 40 img = loadImage("res/"+nombre); 41 sprites.put(nombre,img); 42 } 43 return img; 44 } 45 } 46
En segundo lugar tenemos el sitio donde se desarrolla la acción. Vamos a llamar a este sitio "el escenario" (Stage en inglés). El escenario es el que de alguna forma "coordina" las cosas que ocurren en el juego - el escenario es el que sabe cuántos monstruos hay, cuantas balas, en qué nivel estamos, etc... Una característica muy importante del escenario es que es precisamente él el que tiene la referencia al único objeto SpriteCache que existe en el programa. Podríamos pensar que ya tenemos una clase "escenario" que es la propia Invaders.java, pero lo cierto es que si queremos tener una serie de funcionalidades que nos sirvan para otros juegos, será mejor separarlas en una clase o en una interfaz distinta. En este caso hemos optado por crear una interfaz que indique las características que tiene el escenario.
Vamos a definir la interfaz Stage de la siguiente forma:
Stage.java |
1 /** 2 * Curso Básico de desarrollo de Juegos en Java - Invaders 3 * 4 * (c) 2004 Planetalia S.L. - Todos los derechos reservados. Prohibida su reproducción 5 * 6 * http://www.planetalia.com 7 * 8 */ 9 package version13; 10 11 import java.awt.image.ImageObserver; 12 13 public interface Stage extends ImageObserver { 14 public static final int WIDTH=640; 15 public static final int HEIGHT=480; 16 public static final int SPEED=10; 17 public SpriteCache getSpriteCache(); 18 19 } 20
Como se puede ver, de momento esta interfaz lo único que contiene son las constantes generales del juego y un método para acceder al caché de sprites
Adicionalmente tenemos el monstruo. Está claro que si en el futuro vamos a añadir más, todos ellos serán en esencia lo mismo, si acaso en distintas posiciones, o con distintas velocidades. Pensemos por un momento qué características comunes van a tener nuestros monstruos en el juego para poder definir una clase apropiada. Por un lado, todos los monstruos van a tener una posición sobre la pantalla. Además, todos van a tener un gráfico que periódicamente van a tener que pintar sobre ella. Dado que es posible que queramos definir monstruos diferentes, cada uno tendrá un tamaño que no será necesariamente el mismo para todos. Por último, todos van a "hacer algo" cuando les toque el turno. Puede ser que elijan moverse, disparar, desaparecer, teleportarse, atacar al jugador, o cualquier cosa que se nos ocurra más adelante. Como ahora mismo no sabemos qué va a ser esa cualquier cosa, vamos a definir un método genérico llamado act() en el que cada monstruo elegirá lo que quiere hacer. Lo bonito de este sistema es que el programa principal se limitará a llamar a monstruo.act() y cada uno actuará según el comportamiento programado, sin que el programa principal tenga que saber nada al respecto.
Por lo tanto y resumiendo, los monstruos :
Pensemos ahora un poco más y veremos que realmente estas características las tienen no solo los monstruos sino el propio jugador (su nave) o los disparos de la misma. Por lo tanto hemos llegado a una clase que es capaz de encapsular prácticamente cualquier cosa "activa" o "viva" sobre el escenario y a esta clase la vamos a llamar Actor. Por supuesto, podríamos haber hecho este razonamiento al principio del curso, pero el objetivo era llegar hasta él de forma natural, como lo haría una persona sin mucha experiencia y que está aprendiendo.
Con lo dicho hasta ahora, la clase Actor quedaría como sigue:
Actor.java |
1 /** 2 * Curso Básico de desarrollo de Juegos en Java - Invaders 3 * 4 * (c) 2004 Planetalia S.L. - Todos los derechos reservados. Prohibida su reproducción 5 * 6 * http://www.planetalia.com 7 * 8 */ 9 package version13; 10 11 import java.awt.Graphics2D; 12 import java.awt.image.BufferedImage; 13 14 public class Actor { 15 protected int x,y; 16 protected int width, height; 17 protected String spriteName; 18 protected Stage stage; 19 protected SpriteCache spriteCache; 20 21 public Actor(Stage stage) { 22 this.stage = stage; 23 spriteCache = stage.getSpriteCache(); 24 } 25 26 public void paint(Graphics2D g){ 27 g.drawImage( spriteCache.getSprite(spriteName), x,y, stage ); 28 } 29 30 public int getX() { return x; } 31 public void setX(int i) { x = i; } 32 33 public int getY() { return y; } 34 public void setY(int i) { y = i; } 35 36 public String getSpriteName() { return spriteName; } 37 public void setSpriteName(String string) { 38 spriteName = string; 39 BufferedImage image = spriteCache.getSprite(spriteName); 40 height = image.getHeight(); 41 width = image.getWidth(); 42 } 43 44 public int getHeight() { return height; } 45 public int getWidth() { return width; } 46 public void setHeight(int i) {height = i; } 47 public void setWidth(int i) { width = i; } 48 49 public void act() { } 50 } 51
Esta es la clase genérica, y ahora vamos a definir una clase descendiente de ella llamada Monster que se limitará a moverse y a actuar como hasta ahora lo hacía nuestro único monstruo:
Monster.java |
1 /** 2 * Curso Básico de desarrollo de Juegos en Java - Space Invaders 3 * 4 * (c) 2004 Planetalia S.L. - Todos los derechos reservados. Prohibida su reproducción 5 * 6 * http://www.planetalia.com 7 * 8 */ 9 10 package version13; 11 public class Monster extends Actor { 12 protected int vx; 13 14 public Monster(Stage stage) { 15 super(stage); 16 setSpriteName("bicho.gif"); 17 } 18 19 public void act() { 20 x+=vx; 21 if (x < 0 || x > Stage.WIDTH) 22 vx = -vx; 23 } 24 25 public int getVx() { return vx; } 26 public void setVx(int i) {vx = i; } 27 28 } 29
Y finalmente nos toca reescribir la clase original - Invaders y aprovecharemos para añadir la posibilidad de que existan muchos monstruos. Tods ellos los almacenaremos en una lista llamada "actors":
Invaders.java |
1 package version13; 2 /** 3 * Curso Básico de desarrollo de Juegos en Java - Invaders 4 * 5 * (c) 2004 Planetalia S.L. - Todos los derechos reservados. Prohibida su reproducción 6 * 7 * http://www.planetalia.com 8 * 9 */ 10 11 import java.awt.Canvas; 12 import java.awt.Color; 13 import java.awt.Dimension; 14 import java.awt.Graphics2D; 15 import java.awt.event.WindowAdapter; 16 import java.awt.event.WindowEvent; 17 import java.awt.image.BufferStrategy; 18 import java.util.ArrayList; 19 20 import javax.swing.JFrame; 21 import javax.swing.JPanel; 22 23 public class Invaders extends Canvas implements Stage { 24 25 private BufferStrategy strategy; 26 private long usedTime; 27 28 private SpriteCache spriteCache; 29 private ArrayList actors; 30 31 public Invaders() { 32 spriteCache = new SpriteCache(); 33 34 JFrame ventana = new JFrame("Invaders"); 35 JPanel panel = (JPanel)ventana.getContentPane(); 36 setBounds(0,0,Stage.WIDTH,Stage.HEIGHT); 37 panel.setPreferredSize(new Dimension(Stage.WIDTH,Stage.HEIGHT)); 38 panel.setLayout(null); 39 panel.add(this); 40 ventana.setBounds(0,0,Stage.WIDTH,Stage.HEIGHT); 41 ventana.setVisible(true); 42 ventana.addWindowListener( new WindowAdapter() { 43 public void windowClosing(WindowEvent e) { 44 System.exit(0); 45 } 46 }); 47 ventana.setResizable(false); 48 createBufferStrategy(2); 49 strategy = getBufferStrategy(); 50 requestFocus(); 51 } 52 53 public void initWorld() { 54 actors = new ArrayList(); 55 for (int i = 0; i < 10; i++){ 56 Monster m = new Monster(this); 57 m.setX( (int)(Math.random()*Stage.WIDTH) ); 58 m.setY( i*20 ); 59 m.setVx( (int)(Math.random()*20-10) ); 60 actors.add(m); 61 } 62 } 63 64 public void updateWorld() { 65 for (int i = 0; i < actors.size(); i++) { 66 Actor m = (Actor)actors.get(i); 67 m.act(); 68 } 69 } 70 71 public void paintWorld() { 72 Graphics2D g = (Graphics2D)strategy.getDrawGraphics(); 73 g.setColor(Color.black); 74 g.fillRect(0,0,getWidth(),getHeight()); 75 for (int i = 0; i < actors.size(); i++) { 76 Actor m = (Actor)actors.get(i); 77 m.paint(g); 78 } 79 80 g.setColor(Color.white); 81 if (usedTime > 0) 82 g.drawString(String.valueOf(1000/usedTime)+" fps",0,Stage.HEIGHT-50); 83 else 84 g.drawString("--- fps",0,Stage.HEIGHT-50); 85 strategy.show(); 86 } 87 88 public SpriteCache getSpriteCache() { 89 return spriteCache; 90 } 91 92 public void game() { 93 usedTime=1000; 94 initWorld(); 95 while (isVisible()) { 96 long startTime = System.currentTimeMillis(); 97 updateWorld(); 98 paintWorld(); 99 usedTime = System.currentTimeMillis()-startTime; 100 try { 101 Thread.sleep(SPEED); 102 } catch (InterruptedException e) {} 103 } 104 } 105 106 public static void main(String[] args) { 107 Invaders inv = new Invaders(); 108 inv.game(); 109 } 110 } 111
Fijémonos especialmente en los métodos initWorld(), updateWorld() y paintWorld(). El primero de ellos es nuevo y su propósito es inicializar el escenario. En este caso, se construyen distintos monstruos y se añaden a la lista de actores existentes. Los métodos updateWorld() y paintWorld() tienen los mismos papeles que antes, pero ahora son mucho más limpios y no dependen para nada del tipo de actores con los que están tratando. Cada uno de estos métodos se limita a recorrer la lista de actores diciéndoles "píntate!", "actúa", etc... y cada actor concreto, dependiendo de cómo están sobreescritos sus métodos, actúa en consecuencia
Actor.java | Invaders.java | Monster.java | SpriteCache.java |
Stage.java |
Página Anterior - Medición del rendimiento | Página Actual - 13 - Reorganización del código Java | Página Siguiente - Fotogramas |
Índice del curso |
(c) 2004 Planetalia S.L. Todos los derechos reservados. Prohibida su reproducción