Planetalia - Java Training

Training and Consulting

Home

Tutorial - Writing a Space Invaders game in Java

  Previous Page - Fixing the sound Current Page - 29 - Small optimizations  
  Return to the index of free Java tutorials  

Small optimizations

(c) Alexander Hristov

In our last version, we will perform some optimizations that will help improve our framerate, greatly affected by now because of the sounds and textures we are using.

The first optimization consists in using images in a compatible format. What does this mean? A compatible immage is an image in memory that has the same (or at least very similar) characteristics as the native video mode we are using - in terms of bits per pixel, transparency, etc. Compatible images are much faster to draw than images in any other format. Right now we are using the images in whatever format ImageIO chooses to return them. We can optimize this by :

  1. Reading the image from disk using ImageIO
  2. Creating a compatible image of the same size as the loaded image
  3. Drawing the loaded image on the compatible image
This gives us a "compatible version" of an image located on disk

We will do all this in the SpriteCache class :


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    

The second optimization consists in getting rid of the repetitive texture painting in the main loop. How? When we start the game, we create an off-screen image that has the same width as the screen, but is higher than the screen in exactly the size of the background tile. We fill this image with our background texture. During the gameplay, we simply draw a sub-image of this background to the screen. The scrolling effect is achieved varying the Y coordinate of the sub-image we choose to draw. This allows us to use TexturePaint - which is very slow - just once, and outside the main loop:


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

Our third optimization will be getting rid of the mouse cursor:


           . . .  
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 . . .

Our next optimization deals with a problem you may have encountered while playing the game. If you move the game to the background, and then to the foreground again during play, you will see a gray flicker for a moment. It isn't very problematic, but it is rather unprofessional. What is happening here is that the AWT is launching a repaint event from its thread while we are doing something else. And since the AWT is not using our double buffer strategy but is rather drawing directly on the screen, for a moment we see a completely erased gray screen. To prevent this, we must tell the AWT to ignore repaint requests coming from the OS since it's we ourselves who are doing the repainting. This is achieved by calling the setIgnoreRepaint() method:


           . . .  
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 . . .

And finally, we have a problem with the current game and it is that the speed of the game is a bit dependant on the speed of the computer. If a faster computer completes a turn in less time, the game will run faster. This is not very pleasant, so we are going to correct it by requiring that each turn takes a fixed amount of time. This amount of time depends on the number of frames per second we want to get. For example, if we want 60 fps, each turn must take exactly 16,66 ms. So instead of sleeping always a fixed amount of time (10ms), each turn we will calculate how much time we have invested in updating and redrawing the world, and we will sleep the remaining time:


           . . .  
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 } . . .

So we have finally arrived at the end... Now is your turn to make games. If you like this tutorial and develop a game, link to the index page for others to enjoy too. If you have any comments, or want to correct my poor english :-), or want to suggest improvements, drop me a note here.



Do you want to be notified when new tutorials or lessons are published? Press here


Full list of Java source files for this step

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

Full list of resources

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

  Previous Page - Fixing the sound Current Page - 29 - Small optimizations  
  Return to the index of free Java tutorials  

(c) 2004 Planetalia S.L. All rights reserved. Unauthorized reproduction and/or mirroring is not permitted