Breaking Down Space Invaders – A possible implementation in dot net

Breaking Down Space Invaders – A possible implementation in dot net

I have wanted to write a space invaders clone for a while, and thought to myself that it wasn’t really difficult at all, and I should write a blog about it. I am going to try and minimize the BS in order to keep the entry short enough and to the point.

Sections

Initial Paint Initialize Constants
Timer Keystrokes Drawable Invader
Player Score Board Game Board Vector

Initial Discussion
The space invaders game is comprised of the following items that can be broken up into classes.

  • ScoreBoard – the display of the users score in textual form
  • Invader – an instance of an enemy craft
  • Player – the instance of the player craft
  • PlayerMissle – the instance of the player missle
  • InvaderMissle – the instance of the invader missle
  • GameBoard – the game border and bounds

In order to keep the game code simple, we are going to use a base class called Drawable that contains default implementations of useful functions and properties. The functions we will need in each are:

  • Collision – detect if this object collides with something
  • Draw – draw this object
  • Update – update this object for the current state

The properties that will be needed are:

  • Width ( integer )
  • Height ( integer )
  • Position ( Pointf )

From these starting functions and properties we should be able to get a basic game going that we can customize later. The next thing we need to do in order to get close to having something visible on screen is to provide a default drawing function that takes a Graphics object as a minimum to start as it’s parameter. We may want to also add the Form control as a first parameter in case we need to share anything with the object for drawing. The default implementation will simply draw a rectangle in order to give us an idea of size and position of our objects on screen until we have more complete implementations with images.

public void Draw ( Form1 parent, Graphics g ){
	g.DrawRectangle ( Pens.White, this.Position.X, this.Position.Y, this.Width, this.Height ) ;
}

Our default collision function will do nothing, and our update function will also do nothing. In order to assist us, we could provide debugging information just in case we override and find that we are still having our default implementation being called by accident.

public function Collision(){
	System.Diagnostics.Debugger.Log(0,"","default Collision function\n");
}
public function Collision(){
	System.Diagnostics.Debugger.Log(0,"","default Update function\n");
}

Our next order of business is to create all of the classes that implement the drawable class which I will leave as an exercise of the reader, but I will post one here as an example:

public class Player : Drawable{
	public override void Draw ( Form1 frm, Graphics g ){}
	public override void Collision (){}
	public override void Update (){}
}

The next two functions we need to create are our drawing routine which will simply go through each of the items we need and Draw them to screen. There are more than one way to do this, we could set up a function to Draw to a Graphics context that is provided that we can later send either a Bitmap or the Form Graphics object which will allow us to provide a form of buffering. Or we can simply draw everything in the Paint event. For now, we are simply going to use the paint event. The process is basically to go through each of our objects and paint them in order of “layer”. I use the term layer here because, everything that is painted after the first item will logically be layered on top in the event that the item is directly above even though we aren’t specifically creating a layer object.


Paint Function

public void Paint ( object sender, PaintEventArgs e){
	g.Draw(this,e.Graphics); // draw game board
	sb.Draw(this,e.Graphics); // draw score board
	p.Draw(this,e.Graphics); // draw player
	if ( pm != null ){ // one shot wonder, if the missel
		//goes off screen or hits target then eraticate it and set to null
		pm.Draw(this,e.Graphics); // draw missel
	}
	for each ( Invader tmp in invaders){
		if ( !tmp.Destroyed()){ // check if the invader still exists
			tmp.Draw(this,e.Graphics);
		}
	}
}

The next thing we need to do which can be implementation specific is to create an initializegame function that will set up all of the classes as new class instances, and also set up the parameters such as width, height, starting position.

Initialize Game

int i = 0;
int irow = 0;
int icol = 0;
p = new Player(); //create a player and set position, width and height
p.Width = PLAYER_WIDTH ;
p.Height = PLAYER_HEIGHT ;
p.Position.X = ( this.ClientRectangle.Width / 2 ) - ( PLAYER_WIDTH / 2 ) ; // center of screen
p.Position.Y = this.ClientRectangle.Height - ( p.Height + GAME_OFFSET_Y) ; // bottom of game
sb = new ScoreBoard(); //create a score board width and height no needed
sb.Score = 0; // NOTE: this is a custom int property
sb.Position.X = 2;
sb.Position.Y = 2;
g = new GameBoard();
g.Position.X = GAME_OFFSET_X; // provide a little padding;
g.Position.Y = GAME_OFFSET_Y; // provide space for scoreboard
g.Width = this.ClientRectangle.Width - (2 * GAME_OFFSET_X); // maintain padding on right
g.Height = this.ClientRectangle.Height - (2 * GAME_OFFSET_Y); // maintain padding at the bottom
Invader.invaderclass invclass = Invader.invaderclass.class1 ;
invaders = new List();
for (i = 0; i < ENEMY_COUNT; i++){
	Vector v = new Vector(1, 0); // set up an initial vector/speed for enemy to go right one unit(pixel)
	switch ( irow ){ // set invader type based on row
		case 0 :
			invclass= Invader.invaderclass.class4 ;
		break ;
		case 1 :
			invclass= Invader.invaderclass.class3 ;
		break ;
		case 2 :
			invclass= Invader.invaderclass.class2 ;
		break ;
		case 3 :
			invclass= Invader.invaderclass.class1 ;
		break ;
	}
	Invader tmp = new Invader(v, invclass); // create invader
	tmp.Position.X = GAME_OFFSET_X + ( ENEMY_WIDTH / 2 )+ (icol * (ENEMY_OFFSET + 32));
	tmp.Position.Y = GAME_OFFSET_Y + (irow * (ENEMY_OFFSET + 32));
	tmp.Width = 32; // could make a constant
	tmp.Height = 32; // could make a constant
	invaders.Add(tmp); // add invader to invaders list
	icol++;
	if (icol == COLS) // keep track of columns and rows
	{
		icol = 0;
		irow++;
	}
}
bInit = true; // tell the paint that we are able to draw items
// Timer - the drawing event handler
t = new Timer();
t.Interval = 1000 / 30; // 30 times a second
 t.Tick += new EventHandler(t_Tick);
t.Enabled = true; // enable painting.

In the previous function we did a lot of work and a little extra from what we discussed, and there is room for improvements. Now that we have this function, however, lets fill in the things that are missing: contants.


Constants

public const int ENEMY_COUNT = 8 * 4;
public const int ENEMY_WIDTH = 32;
public const int ENEMY_HEIGHT = 32;
public const int PLAYER_WIDTH = 32;
public const int PLAYER_HEIGHT = 32;
public const int ROWS = 4;
public const int COLS = 8;
public const int ENEMY_OFFSET = 5;
public const int GAME_OFFSET_Y = 20;
public const int GAME_OFFSET_X = ENEMY_OFFSET + (ENEMY_WIDTH / 2);

The only remaining thing we need to do now is create our Timer Event function and we are well on our way to a starting game or at least putting something on screen. The basic responsibility of the timer function will be to update the movements and states of the enemies and player, as well as triggering the paint a the end of the method

Timer Event

void t_Tick(object sender, EventArgs e){
	int i = 0 ;
	p.Update(MoveState); // Update the player based on movement state
	if (p.Collision(this,this.ClientRectangle.Width,this.ClientRectangle.Height )) // check for collisions with the walls or game board
	{
		// if we are on the right hand side, adjust the player position
		if (p.Position.X + ( PLAYER_WIDTH / 2 )> this.ClientRectangle.Width / 2){
			 p.Position.X = this.ClientRectangle.Width - GAME_OFFSET_Y - PLAYER_WIDTH / 2;
		}else{ // if we are on the left hand side, adjust the player position
			p.Position.X = GAME_OFFSET_Y + PLAYER_WIDTH / 2 ;
		}
	}
	sb.Update(); // update the score board
	if (pm != null) // check for a missel and update it
	{
		pm.Update();
		Invader tmp = pm.Collision(this,invaders); // check if the missel hit a target or went offscreen
		if (tmp != null){ // check the class of invader hit on collision
			switch (tmp.thisClass){
				case Invader.invaderclass.class1:
					sb.Score += 10;
				break;
				case Invader.invaderclass.class2:
					sb.Score += 15;
				break;
				case Invader.invaderclass.class3:
					sb.Score += 20;
				break;
				case Invader.invaderclass.class4:
					sb.Score += 25;
				break;
			}
			pm = null;
		}else{
			if (pm.Position.Y < GAME_OFFSET_Y){ // if we went offscreen - destroy the missel
				pm = null;
			}
		}
	}
	bool hadCollision = false; // check all of the invaders that aren't already destroyed for collisions with the walls
	int destroyed = 0;
	for ( i = 0 ; i < ENEMY_COUNT; i++){
		invaders[i].Update();
		if (!invaders[i].Destroyed()){
			if (invaders[i].Collision(this, this.ClientRectangle.Width, this.ClientRectangle.Height)){
				hadCollision = true;
			}
		}else{
			destroyed++;
		}
	}
	if (destroyed == invaders.Count) // check if all invaders are destroyed and init a new board
	{ // level over
		InitBoard();
		t.Enabled = false;
	}
	if (hadCollision){ // if we had a collision with the walls, update the speed of the invaders and the direction, also drop all invaders down
		for (i = 0; i < ENEMY_COUNT; i++){
			invaders[i].v.x *= -1.20;
			if (invaders[i].v.x > 10){
				invaders[i].v.x = 10;
			}
			if (invaders[i].v.x < -10){
				invaders[i].v.x = -10;
			}
			invaders[i].Position.Y += 16;
		}
	}
	this.Invalidate(false); // redraw
	this.Update();
}

Capturing keystrokes to advance the player is the next order of business. I will leave it as an exercise to the reader to figure out how to attach a form keyup and form keydown event. But essentially what we will do is maintain a state variable on keypress and release in order to tell our game what to do at render and update time. Key Notes are that the left and right are updated on press and release, and the player missle object is created on space bar press.

Capturing Keystrokes:

private void Form1_KeyDown(object sender, KeyEventArgs e){
	switch (e.KeyCode){
		case Keys.Left:
			MoveState = movestate.left;
		break;
		case Keys.Right:
			MoveState = movestate.right;
		break;
		case Keys.Space :
			if (pm == null){
				pm = new PlayerMissle();
				pm.Position.X = p.Position.X;
				pm.Position.Y = p.Position.Y;
				pm.Width = 3;
				pm.Height = 3;
			}
		break;
	}
}
private void Form1_KeyUp(object sender, KeyEventArgs e){
	switch (e.KeyCode){
		case Keys.Left:
			if (MoveState == movestate.left){
				MoveState = movestate.none;
			}
		break;
		case Keys.Right:
			if (MoveState == movestate.right){
				MoveState = movestate.none;
			}
		break;
		case Keys.Space:

		break;
	}
}


Drawable Class


class Drawable{
	public Font font;
	public PointF Position;
	private int iwidth;
	private int iheight;
	public Drawable(){
		font = new Font("Verdana", 12, GraphicsUnit.Pixel);
	}
	public int Width{
		get{
			return iwidth;
		}
		set{
			iwidth = value;
		}
	}
	public int Height{
		get{
			return iheight;
		}
		set{
			iheight = value;
		}
	}
	public virtual void Draw(Graphics g){
		g.DrawRectangle(Pens.White, this.Position.X, this.Position.Y, this.Width, this.Height);
	}
	public virtual void Update(){}
	public virtual void Collision(){}
}


Invader Class

class Invader : Drawable{
	public enum invaderclass { class1, class2, class3, class4 } ;
	public invaderclass thisClass;
	bool bDestroyed;
	public Vector v;
	private int midpoint;
	public Invader(invaderclass invclass) : base() {
		thisClass = invclass;
		v = new Vector(0.0,0.0);
		bDestroyed = false ;
	}
	public Invader(Vector v, invaderclass invclass) : base() {
		this.thisClass = invclass;
		this.v = v;
		bDestroyed = false;
	}
	public bool Destroyed ( ){
		return bDestroyed ;
	}
	public override void Update(){
		this.Position.X += (float)v.x;
		this.Position.Y += (float)v.y;
	}
	public void Draw(Form1 parent, System.Drawing.Graphics g){
		switch (thisClass){
			case invaderclass.class1:
				g.DrawImageUnscaled(parent.imageList1.Images[0], (int)this.Position.X - this.Width / 2, (int)this.Position.Y, this.Width, this.Height);
			break;
			case invaderclass.class2:
				g.DrawImageUnscaled(parent.imageList1.Images[1], (int)this.Position.X - this.Width / 2, (int)this.Position.Y, this.Width, this.Height);
			break;
			case invaderclass.class3:
				g.DrawImageUnscaled(parent.imageList1.Images[2], (int)this.Position.X - this.Width / 2, (int)this.Position.Y, this.Width, this.Height);
			break;
			case invaderclass.class4:
				g.DrawImageUnscaled(parent.imageList1.Images[3], (int)this.Position.X - this.Width / 2, (int)this.Position.Y, this.Width, this.Height);
			break;
		}
	}
	public bool Collision(Form1 parent, int width, int height){
		if (!this.Destroyed()){
			if (this.Position.X + (this.Width / 2) > width - Form1.GAME_OFFSET_X){
				return true ;
			}else if ( this.Position.X - ( this.Width / 2 ) < Form1.GAME_OFFSET_X){
				return true ;
			}else if (this.Position.Y - this.Height > height - Form1.GAME_OFFSET_Y){
				parent.GameOver();
				return true;
			}
			return false;
		}else{
			return false ;
		}
	}
	public void Hit(){
		bDestroyed = true;
	}
}


Player Class

class Player : Drawable {
	public Vector v = new Vector(0.0, 0.0);
	public void Update(Form1.movestate MoveState){
		if (MoveState == Form1.movestate.right){
			v.x += 1.0;
			if (v.x >= 5){
				v.x = 5;
			}
		}else if (MoveState == Form1.movestate.left){
			v.x -= 1.0;
			if (v.x <= -5){
				v.x = -5;
			}
		}else if ( v.x != 0 ){
			if (v.x > 0){
				v.x -= 1.0;
			}else{
				v.x += 1.0;
			}
		}
		this.Position.X += (float)v.x;
		this.Position.Y += (float)v.y;
	}
	public  void Draw(Form1 frm, Graphics g){
		//g.DrawRectangle(Pens.White,(this.Position.X - this.Width / 2), this.Position.Y, this.Width, this.Height);
		g.DrawImageUnscaled(frm.imageList1.Images[4], (int)this.Position.X, (int)this.Position.Y, this.Width, this.Height);
	}
	public bool Collision(Form1 parent, int width, int height){
		if (this.Position.X + (this.Width / 2) > width - Form1.GAME_OFFSET_X){
			return true;
		}else if (this.Position.X - (this.Width / 2) < Form1.GAME_OFFSET_X){
			return true;
		}
		return false;
	}
}


Score Board Class

class ScoreBoard : Drawable{
	public ScoreBoard():base(){}
	private int iScore;
	public int Score{
		get{
			return iScore;
		}
		set{
			iScore = value;
		}
	}
	public override void Draw(Graphics g){
		g.DrawString(iScore.ToString(), this.font, Brushes.White, this.Position);
	}
}


Game Board Class

class GameBoard : Drawable{
	public override void Draw(System.Drawing.Graphics g){
		base.Draw(g);
	}
}


Vector Class

class Vector{
	public double x = 0;
	public double y = 0;
	public Vector(double x, double y){
		this.x = x;
		this.y = y;
	}
}

I will leave it up to the reader to complete the rest of the functions themselves, or download the sources from our downloads section.

Space Invaders Source

There is also an exe available from the preceeding link.

ttessier

About ttessier

Professional Developer and Operator of SwhistleSoft
This entry was posted in C#.net, Uncategorized. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *