[2] Space Invader

Introduction

We are going to implement a simple action game using the Sprite Manager I wrote some time ago.

Screen Shot

invader
Attribution: “Space Ship Shooter Pixel Art Assets” by ansimuz / CC0
You may download the file space_shooter_pack.zip at opengameart.org.
Alternatively, you may download an unmodified version of the zip file here.


Background Scrolling

We will use the file desert-backgorund-looped.png inside the zip file.

This is a looping background. That means if we cut the bottom row of the image, and then paste it to the top, we still have a continuous image. Hence the scrolling is as simple as:
  public void scrollImage(BufferedImage image)
  {
    int w=image.getWidth(null);
    int h=image.getHeight(null);
    int[] pixels = ((java.awt.image.DataBufferInt)
        image.getRaster().getDataBuffer()).getData();

    // save the bottomRow of the image before scrolling down
    int[] bottomRow = new int[w];
    System.arraycopy(pixels, (h-1)*w, bottomRow, 0, w);

    // scroll down the entire array by one row
    System.arraycopy(pixels, 0, pixels, w, w*(h-1));

    // copy the bottom row to the top row
    System.arraycopy(bottomRow, 0, pixels, 0, w);

  }
  
Note that we are using the same technique in Optimized flood filler to access the pixel array of the image. This is much faster than the getPixel() and setPixel() API.

A simple demonstration using the above code can be found here.

Ship Sprite

We will use the file ship.png inside the zip file.
ship

This is a 80×48 image containing 10 frames of 16×24 each. The bounding rectangle of the 10 frames in row major order is:
{ 0, 0,16,24},{16, 0,16,24},{32, 0,16,24},{48, 0,16,24},{64, 0,16,24},
{ 0,16,16,24},{16,16,16,24},{32,16,16,24},{48,16,16,24},{64,16,16,24}
The image actually contains animation frame for 3 different direction. If we are moving up, we will use the middle two frames.

If the ship is moving up or down, we will use the middle two frames. If the ship is moving left, we will use the four frames on the left. If the ship is moving right, we will use the four frames on the right.
Therefore we define the following region in ShipSprite.java
class ShipSprite extends Sprite
{
  // pre-defined source regions
  private int[][][] frameRect = // [direction][gesture][x,y,w,h]
    {
      {{16, 0,16,24}, {16,24,16,24}, { 0, 0,16,24}, { 0,24,16,24} },  // left
      {{48, 0,16,24}, {48,24,16,24}, {64, 0,16,24}, {64,24,16,24} },  // right
      { { 32, 0,16,24}, {32,24,16,24} },  // down
      { { 32, 0,16,24}, {32,24,16,24} }   // up
    };  
}


Note that ShipSprite is a child class of the Sprite class defined in the Sprite Manager
A very simple demonstration on how to use the ShipSprite class can be found here.

Enemy Sprite

We will use the file enemy-medium.png inside the zip file.
enemy-medium

This sprite is much simpler. It has only two frames no matter which direction it is moving. Hence the bounding rectangle in EnemySprite.java is defined as:
class EnemySprite extends Sprite
{
  // pre-defined source regions
  private int[][][] frameRect = // [direction][gesture][x,y,w,h]
    {
      {{ 0, 0,32,16}, {32,0,32,16} },  // left
      {{ 0, 0,32,16}, {32,0,32,16} },  // right
      {{ 0, 0,32,16}, {32,0,32,16} },  // down
      {{ 0, 0,32,16}, {32,0,32,16} }  // up
    };  
}


Enemy Movement

All enemies will be moving in the same direction. Hence we will use a static variable in the enemy class
class EnemySprite extends Sprite
{
  // all enemies are moving in the same direction, hence use a "static" variable
  static int direction=LEFT;
}
The moving rule is very simple (see animate() in EnemySprite.java):
    // if any one of the enemy hit the left hand side, all enemies start moving right
    if (x<=0) direction=RIGHT;
 
    // if any one of the enemy hit the right hand side, all enemies start moving left
    if (x>=225) direction=LEFT;
 
The hardcode of 225 is ugly, but it works for this simple demonstration.
A better idea is to pass the viewport dimension in the constructor of EnemySprite.
This is so simple that I would leave it as an exercise for readers.

Enemy Firebolt

We will use the file laser-bolts.png inside the zip file.
laser-bolts

Note that the enemy fire bolt and our laser bolt are stored in the same file. For fire bolt, we will use the upper two frames. See the bounding rectangle in FireboltSprite.java.
class FireboltSprite extends Sprite
{
  // pre-defined source regions
  private int[][][] frameRect = // [direction][gesture][x,y,w,h]
    {
      {{ 0, 0,16,16}, {14,0,16,16} },  // left
      {{ 0, 0,16,16}, {14,0,16,16} },  // right
      {{ 0, 0,16,16}, {14,0,16,16} },  // down
      {{ 0, 0,16,16}, {14,0,16,16} }  // up

    };  
}


For laser bolt, we will use the lower two frames.
class LaserSprite extends Sprite
{
  public static final int LEFT=0,RIGHT=1,DOWN=2,UP=3;

  // pre-defined source regions
  private int[][][] frameRect = // [direction][gesture][x,y,w,h]
    {
      {{ 0, 16,16,16}, {14,16,16,16} },  // left
      {{ 0, 16,16,16}, {14,16,16,16} },  // right
      {{ 0, 16,16,16}, {14,16,16,16} },  // down
      {{ 0, 16,16,16}, {14,16,16,16} }  // up

    };
}  


Probability of Enemy Fire

We will use a predefined probability table indexed by number of enemies left. If there are only one enemy left, the probability of firing is 1/5. If there are three enemy left, the probability of firing is 1/20. This probability is defined in Scroller.java
  java.util.Random random=new java.util.Random();
  boolean readyToFire(EnemySprite enemy)
  {
    // for example, if one enemy left, the probability of fire is 1/5
    // if 3 enemies left, the probability of fire is 1/20
    int[] probability=new int[] {0,5,10,20,40,80,160,320,640,1000,
      1000,1000,1000,1000,1000,1000,1000};
    return (random.nextInt(probability[enemyList.size()])==0);
  }

Limitation of our fire

To avoid killing your keyboard, you are allowed to make one fire at a time. That is, you can only fire again when the previous laser bolt has disappeared. If you really want to fire more frequently, set the variable autoFire to true. The above logic is implemented in Scroller.java
  int MAX_FIRE=1;
  int lastFire=0;
  boolean autoFire=false;

  boolean readyToFire()
  {
    if (ship==null) return false;
    if (!autoFire)
      return laserList.size()<MAX_FIRE;
    else
    {
      return ((lastFire++%10)==0);
    }
  }



Collision Detection

We will use a simple bounding rectangle approach to detect sprite collision. If the bounding rectangles don’t overlap, then we return immediately. Otherwise, we will do a pixel by pixel verification in the overlapped region. The detection is implemented inside both LaserSprite.java and FireSprite.java. The coding is identical other than the input parameter.
See the hit() function in these two files.

Game Control

The player will use the keyboard as the control interface.
  • Left Arrow : Move left
  • Right Arrow : Move right
  • Space : fire.
We will need to implement keyPressed() and keyReleased() of the KeyListener in Scroller.java. The implementation is simple, we just set and reset a global boolean variable if key is pressed or released.
  boolean spacePressed=false;
  boolean leftPressed=false;
  boolean rightPressed=false;
  
  @Override // KeyListener
  public void keyPressed(java.awt.event.KeyEvent e)
  {
    if (e.getKeyCode()==32) spacePressed=true;
    if (e.getKeyCode()==37) leftPressed=true;
    if (e.getKeyCode()==39) rightPressed=true;
//System.out.println(e);
  } 
 
  @Override // KeyListener
  public void keyReleased(java.awt.event.KeyEvent e)
  {
//System.out.println(e);
    if (e.getKeyCode()==32) spacePressed=false;
    if (e.getKeyCode()==37) leftPressed=false;
    if (e.getKeyCode()==39) rightPressed=false;
  } 
Those boolean variables are used in the control() method in Scroller.java.
  void control()
  {
    if (ship==null) return;  // already GAME OVER
    int w=backgroundImage.getWidth(null);
    if (leftPressed && ship.x>0) ship.setDirection(ship.LEFT);
    else if (rightPressed && ship.x<w-16) ship.setDirection(ship.RIGHT);
    else ship.setDirection(ship.UP);
    if (spacePressed && readyToFire()) fire();
 
  }


Overriding the Sprite class

You can see plenty of examples on overriding the Sprite class in this game. We have ShipSprite, EnemySprite, FireboltSprite, LaserSprite and ExplosionSprite. To override the Sprite class, the bare minimum is to implement the draw() and animate() function. The draw() function just draw the Sprite on a Graphics object. The animate() function control the self animation and the movement. For the simplest example, you may see ExplosionSprite.java. Depending on your needs, you may define more functions for different kinds of Sprite. For example, a collision detection function is implemented in LaserSprite.java and FireboltSprite.java as describe above.

Basic Flow

  1. The program entry point is main().
  2. A new instance of Scroller is created and its init() function is called
  3. After everything is setup, a SwingTimer is started which will call animateFrame() on every 30 ms


Source Code

To play the game, please visit the source code page to download everything.


Previous Entries [1] Klotski Next Entries [3] Color Matrix