Curso - Programación de un Space Invaders en Java

  Página Anterior - Corrigiendo los fallos de sonido Página Actual - 29 - Pequeñas optimizaciones  
  Índice del curso  

Pequeñas optimizaciones

(c) Alexander Hristov

En nuestra última versión, realizaremos algunas pequeñas optimizaciones que nos ayudarán a incrementar nuestros fotogramas por segundo, dañadas desde que implementamos el sonido y las texturas.

La primera optimización consiste en utilizar imágenes en formato "compatible". ¿Qué significa esto? Una imagen "compatible" es una imagen en memoria que tiene las mismas características (o al menos muy similares) al modo de vídeo nativo que se está mostrando en ese momento - en términos de bits por pixel, transparencia, etc. Las imágenes compatibles son mucho más rápidas de dibujar que cualquier otro formato. Ahora mismo lo que estamos haciendo es utilizar el formato que a ImageIO le apetezca devolvernos. Podemos optimizar esto de la siguiente forma:

  1. Leemos la imagen de disco utilizando ImageIO
  2. Creamos una imagen compatible del mismo tamaño que la imagen leida
  3. Pintamos la imagen recién cargada encima de la imagen compatible
De esta forma obtenemos una "versión compatible" de una imagen situada en el disco

Haremos todo esto en la clase SpriteCache, que es la que centraliza el acceso a las imágenes:


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 version29;
10    
11    import java.awt.Graphics;
12    import java.awt.GraphicsConfiguration;
13    import java.awt.GraphicsEnvironment;
14    import java.awt.Image;
15    import java.awt.Transparency;
16    import java.awt.image.BufferedImage;
17    import java.awt.image.ImageObserver;
18    import java.net.URL;
19    import javax.imageio.ImageIO;
20    
21    public class SpriteCache extends ResourceCache implements ImageObserver{
22      
23      protected Object loadResource(URL url) {
24        try {
25          return ImageIO.read(url);
26        } catch (Exception e) {
27          System.out.println("No se pudo cargar la imagen de "+url);
28          System.out.println("El error fue : "+e.getClass().getName()+" "+e.getMessage());
29          System.exit(0);
30          return null;
31        }
32      }
33      
34      public BufferedImage createCompatible(int width, int height, int transparency) {
35        GraphicsConfiguration gc = 
36          GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
37        BufferedImage compatible = gc.createCompatibleImage(width,height,transparency);
38        return compatible;
39      }
40      
41      public BufferedImage getSprite(String name) {
42        BufferedImage loaded = (BufferedImage)getResource(name);
43        BufferedImage compatible = createCompatible(loaded.getWidth(),loaded.getHeight(),Transparency.BITMASK); 
44        Graphics g = compatible.getGraphics();
45        g.drawImage(loaded,0,0,this);
46        return compatible;
47      }
48        
49      public boolean imageUpdate(Image img, int infoflags,int x, int y, int w, int h) {
50         return (infoflags & (ALLBITS|ABORT)) == 0;
51      }
52    }
53    

La segunda optimización consiste en librarnos del dibujo repetitivo de texturas dentro del bucle principal. En su lugar, procederemos de la siguiente forma: Nada mas arrancar el juego, creamos una imagen en memoria que tiene la misma anchura que la pantalla, pero con una altura igual a la de la pantalla mas el tamaño de la textura. Rellenamos esta imagen con el fondo que queremos utilizar. Durante el juego, simplemente copiamos una sub-imagen de esta imagen en memoria a la pantalla. El efecto de scroll se consigue variando la coordenada Y de inicio de la sub-imagen que pintamos. Esto nos permite limitar el uso de TexturePaint - que siempre es muy lento- a una única vez, y además fuera del bucle principal del juego:


1     package version29;
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.Cursor;
14    import java.awt.Dimension;
15    import java.awt.Font;
16    import java.awt.Graphics2D;
17    import java.awt.Point;
18    import java.awt.Rectangle;
19    import java.awt.TexturePaint;
20    import java.awt.Toolkit;
21    import java.awt.Transparency;
22    import java.awt.event.KeyEvent;
23    import java.awt.event.KeyListener;
24    import java.awt.event.WindowAdapter;
25    import java.awt.event.WindowEvent;
26    import java.awt.image.BufferStrategy;
27    import java.awt.image.BufferedImage;
28    import java.util.ArrayList;
29    
30    import javax.swing.JFrame;
31    import javax.swing.JPanel;
32    
33    public class Invaders extends Canvas implements Stage, KeyListener {
34      
35      private BufferStrategy strategy;
36      private long usedTime;
37      
38      private SpriteCache spriteCache;
39      private SoundCache soundCache;
40      private ArrayList actors; 
41      private Player player;
42 private BufferedImage background, backgroundTile; 43 private int backgroundY;
44 45 private boolean gameEnded=false; 46 47 public Invaders() { 48 spriteCache = new SpriteCache(); 49 soundCache = new SoundCache(); 50 51 52 JFrame ventana = new JFrame("Invaders"); 53 JPanel panel = (JPanel)ventana.getContentPane(); 54 setBounds(0,0,Stage.WIDTH,Stage.HEIGHT); 55 panel.setPreferredSize(new Dimension(Stage.WIDTH,Stage.HEIGHT)); 56 panel.setLayout(null); 57 panel.add(this); 58 ventana.setBounds(0,0,Stage.WIDTH,Stage.HEIGHT); 59 ventana.setVisible(true); 60 ventana.addWindowListener( new WindowAdapter() { 61 public void windowClosing(WindowEvent e) { 62 System.exit(0); 63 } 64 }); 65 ventana.setResizable(false); 66 createBufferStrategy(2); 67 strategy = getBufferStrategy(); 68 requestFocus(); 69 addKeyListener(this); 70 71 setIgnoreRepaint(true); 72 73 BufferedImage cursor = spriteCache.createCompatible(10,10,Transparency.BITMASK); 74 Toolkit t = Toolkit.getDefaultToolkit(); 75 Cursor c = t.createCustomCursor(cursor,new Point(5,5),"null"); 76 setCursor(c); 77 } 78 79 public void gameOver() { 80 gameEnded = true; 81 } 82 83 public void initWorld() { 84 actors = new ArrayList(); 85 for (int i = 0; i < 10; i++){ 86 Monster m = new Monster(this); 87 m.setX( (int)(Math.random()*Stage.WIDTH) ); 88 m.setY( i*20 ); 89 m.setVx( (int)(Math.random()*20-10) ); 90 91 actors.add(m); 92 } 93 94 player = new Player(this); 95 player.setX(Stage.WIDTH/2); 96 player.setY(Stage.PLAY_HEIGHT - 2*player.getHeight()); 97 98 soundCache.loopSound("musica.wav"); 99
100 backgroundTile = spriteCache.getSprite("oceano.gif"); 101 background = spriteCache.createCompatible( 102 Stage.WIDTH, 103 Stage.HEIGHT+backgroundTile.getHeight(), 104 Transparency.OPAQUE); 105 Graphics2D g = (Graphics2D)background.getGraphics(); 106 g.setPaint( new TexturePaint( backgroundTile, 107 new Rectangle(0,0,backgroundTile.getWidth(),backgroundTile.getHeight()))); 108 g.fillRect(0,0,background.getWidth(),background.getHeight()); 109 backgroundY = backgroundTile.getHeight(); 110
111 } 112 113 public void addActor(Actor a) { 114 actors.add(a); 115 } 116 117 public Player getPlayer() { 118 return player; 119 } 120 121 public void updateWorld() { 122 int i = 0; 123 while (i < actors.size()) { 124 Actor m = (Actor)actors.get(i); 125 if (m.isMarkedForRemoval()) { 126 actors.remove(i); 127 } else { 128 m.act(); 129 i++; 130 } 131 } 132 player.act(); 133 } 134 135 public void checkCollisions() { 136 Rectangle playerBounds = player.getBounds(); 137 for (int i = 0; i < actors.size(); i++) { 138 Actor a1 = (Actor)actors.get(i); 139 Rectangle r1 = a1.getBounds(); 140 if (r1.intersects(playerBounds)) { 141 player.collision(a1); 142 a1.collision(player); 143 } 144 for (int j = i+1; j < actors.size(); j++) { 145 Actor a2 = (Actor)actors.get(j); 146 Rectangle r2 = a2.getBounds(); 147 if (r1.intersects(r2)) { 148 a1.collision(a2); 149 a2.collision(a1); 150 } 151 } 152 } 153 } 154 155 public void paintShields(Graphics2D g) { 156 g.setPaint(Color.red); 157 g.fillRect(280,Stage.PLAY_HEIGHT,Player.MAX_SHIELDS,30); 158 g.setPaint(Color.blue); 159 g.fillRect(280+Player.MAX_SHIELDS-player.getShields(),Stage.PLAY_HEIGHT,player.getShields(),30); 160 g.setFont(new Font("Arial",Font.BOLD,20)); 161 g.setPaint(Color.green); 162 g.drawString("Shields",170,Stage.PLAY_HEIGHT+20); 163 164 } 165 166 public void paintScore(Graphics2D g) { 167 g.setFont(new Font("Arial",Font.BOLD,20)); 168 g.setPaint(Color.green); 169 g.drawString("Score:",20,Stage.PLAY_HEIGHT + 20); 170 g.setPaint(Color.red); 171 g.drawString(player.getScore()+"",100,Stage.PLAY_HEIGHT + 20); 172 } 173 174 public void paintAmmo(Graphics2D g) { 175 int xBase = 280+Player.MAX_SHIELDS+10; 176 for (int i = 0; i < player.getClusterBombs();i++) { 177 BufferedImage bomb = spriteCache.getSprite("bombUL.gif"); 178 g.drawImage( bomb ,xBase+i*bomb.getWidth(),Stage.PLAY_HEIGHT,this); 179 } 180 } 181 182 public void paintfps(Graphics2D g) { 183 g.setFont( new Font("Arial",Font.BOLD,12)); 184 g.setColor(Color.white); 185 if (usedTime > 0) 186 g.drawString(String.valueOf(1000/usedTime)+" fps",Stage.WIDTH-50,Stage.PLAY_HEIGHT); 187 else 188 g.drawString("--- fps",Stage.WIDTH-50,Stage.PLAY_HEIGHT); 189 } 190 191 192 public void paintStatus(Graphics2D g) { 193 paintScore(g); 194 paintShields(g); 195 paintAmmo(g); 196 paintfps(g); 197 } 198 199 public void paintWorld() { 200 Graphics2D g = (Graphics2D)strategy.getDrawGraphics();
201 g.drawImage( background, 202 0,0,Stage.WIDTH,Stage.HEIGHT, 203 0,backgroundY,Stage.WIDTH,backgroundY+Stage.HEIGHT,this);
204 for (int i = 0; i < actors.size(); i++) { 205 Actor m = (Actor)actors.get(i); 206 m.paint(g); 207 } 208 player.paint(g); 209 210 211 paintStatus(g); 212 strategy.show(); 213 } 214 215 public void paintGameOver() { 216 Graphics2D g = (Graphics2D)strategy.getDrawGraphics(); 217 g.setColor(Color.white); 218 g.setFont(new Font("Arial",Font.BOLD,20)); 219 g.drawString("GAME OVER",Stage.WIDTH/2-50,Stage.HEIGHT/2); 220 strategy.show(); 221 } 222 223 public SpriteCache getSpriteCache() { 224 return spriteCache; 225 } 226 227 public SoundCache getSoundCache() { 228 return soundCache; 229 } 230 231 public void keyPressed(KeyEvent e) { 232 player.keyPressed(e); 233 } 234 235 public void keyReleased(KeyEvent e) { 236 player.keyReleased(e); 237 } 238 public void keyTyped(KeyEvent e) {} 239 240 public void game() { 241 usedTime=1000; 242 initWorld(); 243 while (isVisible() && !gameEnded) { 244 long startTime = System.currentTimeMillis();
245 backgroundY--; 246 if (backgroundY < 0) 247 backgroundY = backgroundTile.getHeight();
248 updateWorld(); 249 checkCollisions(); 250 paintWorld(); 251 usedTime = System.currentTimeMillis()-startTime; 252 do { 253 Thread.yield(); 254 } while (System.currentTimeMillis()-startTime< 17); 255 } 256 paintGameOver(); 257 } 258 259 public static void main(String[] args) { 260 Invaders inv = new Invaders(); 261 inv.game(); 262 } 263 } 264

La tercera optimización consiste en librarnos del cursor del ratón:


           . . .  
47      public Invaders() {
48        spriteCache = new SpriteCache();
49        soundCache = new SoundCache();
50        
51        
52        JFrame ventana = new JFrame("Invaders");
53        JPanel panel = (JPanel)ventana.getContentPane();
54        setBounds(0,0,Stage.WIDTH,Stage.HEIGHT);
55        panel.setPreferredSize(new Dimension(Stage.WIDTH,Stage.HEIGHT));
56        panel.setLayout(null);
57        panel.add(this);
58        ventana.setBounds(0,0,Stage.WIDTH,Stage.HEIGHT);
59        ventana.setVisible(true);
60        ventana.addWindowListener( new WindowAdapter() {
61          public void windowClosing(WindowEvent e) {
62            System.exit(0);
63          }
64        });
65        ventana.setResizable(false);
66        createBufferStrategy(2);
67        strategy = getBufferStrategy();
68        requestFocus();
69        addKeyListener(this);
70        
71        setIgnoreRepaint(true);
72        
73 BufferedImage cursor = spriteCache.createCompatible(10,10,Transparency.BITMASK); 74 Toolkit t = Toolkit.getDefaultToolkit(); 75 Cursor c = t.createCustomCursor(cursor,new Point(5,5),"null"); 76 setCursor(c);
77 } 78 . . .

La siguiente optimización trata con un problema que es posible que nos hayamos encontrado durante el juego. Si movemos la ventana a un segundo plano y la volvemos a poner en primer plano, veremos un parpadeo gris por unos instantes. No es muy problemático, pero es muy poco profesional. Lo que está ocurriendo es que el AWT está lanzando un evento de redibujo desde su propio hilo, mientras nosotros estamos haciendo otra cosa. Dado que AWT no utiliza un doble búfer, por unos instantes vemos una pantalla gris completamente borrada - la misma situación que teníamos nosotros al comenzar. Para evitar esto, debemos indicarle a AWT que ignore las peticiones de redibujo que provengan del sistema operativo, ya que nosotros mismos nos estamos haciendo cargo del dibujo de la pantalla. Esto se consigue llamando al método setIgnoreRepaint()


           . . .  
47      public Invaders() {
48        spriteCache = new SpriteCache();
49        soundCache = new SoundCache();
50        
51        
52        JFrame ventana = new JFrame("Invaders");
53        JPanel panel = (JPanel)ventana.getContentPane();
54        setBounds(0,0,Stage.WIDTH,Stage.HEIGHT);
55        panel.setPreferredSize(new Dimension(Stage.WIDTH,Stage.HEIGHT));
56        panel.setLayout(null);
57        panel.add(this);
58        ventana.setBounds(0,0,Stage.WIDTH,Stage.HEIGHT);
59        ventana.setVisible(true);
60        ventana.addWindowListener( new WindowAdapter() {
61          public void windowClosing(WindowEvent e) {
62            System.exit(0);
63          }
64        });
65        ventana.setResizable(false);
66        createBufferStrategy(2);
67        strategy = getBufferStrategy();
68        requestFocus();
69        addKeyListener(this);
70        
71 setIgnoreRepaint(true);
72 73 BufferedImage cursor = spriteCache.createCompatible(10,10,Transparency.BITMASK); 74 Toolkit t = Toolkit.getDefaultToolkit(); 75 Cursor c = t.createCustomCursor(cursor,new Point(5,5),"null"); 76 setCursor(c); 77 } 78 . . .

Finalmente, tenemos un problema con el juego y es que su velocidad varía un poco dependiendo de la velocidad del ordenador. Dado que la pausa que hacemos es siempre la misma, si el ordenador es rápido la actualización del escenario será rápida y el juego irá rápido. Sin embargo, si el ordenador es lento, consumirá mucho tiempo en la actualización del escenario y aún así estamos esperando la misma cantidad de tiempo en el bucle principal, con lo cual el juego va más lento. Como esto no es muy agradable lo corregiremos exigiendo que cada turno dure cierta cantidad fija de tiempo. Esta cantidad dependerá del número de fotogramas que queremos alcanzar. Por ejemplo si queremos 60 fts, cada turno debe durar 1000/60 = 16,66 milisegundos. De forma que en lugar de dormir siempre la misma cantidad de tiempo (10ms), en cada turno calcularemos cuánto hemos invertido en la actualización y redibujo de la pantalla y dormiremos el tiempo que nos quede:


           . . .  
240     public void game() {
241       usedTime=1000;
242       initWorld();
243       while (isVisible() && !gameEnded) {
244         long startTime = System.currentTimeMillis();
245         backgroundY--;
246         if (backgroundY < 0)
247           backgroundY = backgroundTile.getHeight();
248         updateWorld();
249         checkCollisions();
250         paintWorld();
251         usedTime = System.currentTimeMillis()-startTime;
252 do { 253 Thread.yield(); 254 } while (System.currentTimeMillis()-startTime< 17);
255 } 256 paintGameOver(); 257 } . . .

Bien, hemos llegado al final de este tutorial de Java. Ahora es tu turno para comenzar a hacer juegos. Si te ha gustado este curso de java, puedes poner un enlace a su índice para que otros lo puedan disfrutar también. Si tienes algún comentario o alguna corrección o sugerencia, puedes mandarme una nota desde aquí. Es posible que en el futuro añada más tutoriales y cursos sobre temas similares, en cuyo caso aparecerán en la página de cursos gratuitos de Planetalia.



Lista de archivos Java del programa en este paso

Actor.java Bomb.java Bullet.java Invaders.java
Laser.java Monster.java Player.java ResourceCache.java
SoundCache.java SpriteCache.java Stage.java  

Lista de recursos

bicho.gif bicho0.gif bicho1.gif bicho2.gif
bombD.gif bombDL.gif bombDR.gif bombL.gif
bombR.gif bombU.gif bombUL.gif bombUR.gif
disparo.gif disparo0.gif disparo1.gif disparo2.gif
explosion.wav misil.gif missile.wav musica.wav
nave.gif oceano.gif photon.wav Thumbs.db

  Página Anterior - Corrigiendo los fallos de sonido Página Actual - 29 - Pequeñas optimizaciones  
  Índice del curso  

(c) 2004 Planetalia S.L. Todos los derechos reservados. Prohibida su reproducción