Planetalia - Java Training

Training and Consulting

Home

Tutorial - Writing a Space Invaders game in Java

  Previous Page - Counting FPS Current Page - 13 - Refactoring the code   Next Page - Frames
  Return to the index of free Java tutorials  

Refactoring the code

(c) Alexander Hristov

At this point the program is quickly becoming a huge mess of code, and it is becoming increasingly clear that we must do something about it. If you have little experience programming Java or any other object oriented programming, it's normal to make this journey in the same way as we did it. The important thing is, however, to know when to stop and think whether there's a better way to do things. As you gain experience over time, you'll learn to make the decisions we are about to make now in the beginning of the development, before writing a single line of code.

Before starting to refactor let's think about what "things" we have in our game. Each of these "things" that displays a separate behaviour will become a class

In the first place, we have a part of the program - which is more or less independant of the others - that handles our "sprite cache". This part, which has a series of behaviours (providing sprites) and some state (the hashmap with the loaded sprites) is an ideal candidate for becoming an independant class. When creating a new class it's a good idea to think exactly what responsabilities the new class will havem, and then turn these responsabilities as public methods. Everything else should be hidden. In our case, our sprite cache class will have only one responsability - providing a sprite when needed. As such, it will have a single public method:

public BufferedImage getSprite(String nombre) {

The full code of the class is:


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    

In the scond place, we have the "stage" where the action is developing. The stage is the entity that coordinates everything that happens in the game - the stage knows how many monsters there are, how many shots, which is the current level, etc.. A very important property of the stage is that it is the only one that has a reference to the single instance of SpriteCache that exists in the program. We could argue that we already have a main class - Invaders.java - so there's no need to talk about a stage, but if we want to have a set of funnctionalities that are common to 2D games and not specifically tied to an "invaders" type of game, it's better to separate this set in a different class. In our case, we've chosen to create an interface called Stage as follows:


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    

As you can see, currently the stage only defines several global constants and a method for obtaining the SpriteCache instance.

Next we have the critter. It's clear that if we are going to add more critters in the future - and we will -, all of them will be basically the same, although each one will have a different position and speed. Let's think for a second about al the things that monsters have in common, in order to properly define a class for them: All monsters have a position, and all monsters have graphic that they show on the screen. Also, all monsters have a size, which need not be the same for all of them. Finally, all monsters "do" something from time to time - whether it's moving, firing, reproducing, dropping bombs, tracking the player or whatever. Currently our critter only moves, but we don't know what things will happen in the future, so we will define a generic method called act() that will be called on each turn, and each monster will decide what to do. The main game loop will not need to know any specifics.

So to summarize, we have the following:

  1. A position
  2. A size (height and width)
  3. A monster is able to draw itself on the screen
  4. A monster periodically does something

If we now think a bit more, we'll see that these things are not exclusive of monsters alone, but are also shared by almost all moving things in the game - be it the player, the shots, the falling bonuses, whatever. We have thus arrived at the conclusion that we are describing an even more generic class, that describes all "live" entities in the game, and so we will call this class Actor. Some programmers like to call such a generic class a Sprite, but historically "a sprite" has been associated with a graphic rather than with a complex set of behaviours, so we will use Actor instead. Of course, we could have defined this class at the beginning of this tutorial, but our aim was to arrive at a point where the existance of this class appears almost naturally and seems inevitable, especially for those readers who don't have much object oriented design experience.

Our Actor class has the following code


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    

Now that we have the generic class, we will define a new class that will represent all enemies and we'll call that class Monster. Monster extends Actor adding a simple "bouncing" movement:


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    

And finally we must rewrite the original class - Invaders taking into account everything that we've done up to this point. We will also use the rewrite to add multiple enemies instead of just one. All these enemies will be stored in a list called "actors":


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   

I'd like to point you to the methods initWorld(), updateWorld() and paintWorld(). The first of them is new and its purpose is to initialize the world, creating all the enemies and adding them to the list. The methods updateWorld() and paintWorld() have the same roles as before, but now are cleaner and do not depend upon the type of actor they are handling. The only work each of these method does is trasversing the lsit of actors and telling each of them "draw yourself!, "do whatever you want to do!", etc..., and each specific actor, depending on how its methods override the base methods proceeds accordingly.

Monstruitos Java



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


Full list of Java source files for this step

Actor.java Invaders.java Monster.java SpriteCache.java
Stage.java      

Full list of resources

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 test.gif
Thumbs.db      

  Previous Page - Counting FPS Current Page - 13 - Refactoring the code   Next Page - Frames
  Return to the index of free Java tutorials  

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