How to code Space Race! (1973) – 2

One year on from pong came Space Race (also made by Atari). Space Race is very simple, it’s a two player game, each player controls a rocket. The idea is to get to the top of the map having avoided the space debris to gain a point. The bar in the middle represents the time left of the game, the player with the most points at the end of the game wins!

This tutorial is going to be much more to the point than my previous two, so If there are concepts that I don’t explain and you don’t understand, chances are I’ve covered them in my previous tutorials. But, I’m sure you’ll get the gist without needing the previous tutorials, ya clever bugger.

Ship movement

Let’s get those ships moving, they have two options, go up or go down.

Sketch.js

let leftShip;
let rightShip; 

function setup() {
  createCanvas(400, 400);
  leftShip = new Ship(width * 0.33);
  rightShip = new Ship(width * 0.66);
}

function draw() {
  background(0);
  
  leftShip.update();
  rightShip.update();
  
  leftShip.display();
  rightShip.display();
}


function keyPressed() {
  if (keyCode == UP_ARROW) {
    rightShip.isUp = true;
    rightShip.isDown = false;
  } else if (keyCode == DOWN_ARROW) {
  	rightShip.isDown = true;
    rightShip.isUp = false;
  }
  
  
  if (keyCode == 87) {
  	// keycode is 'w'
    leftShip.isUp = true;
    leftShip.isDown = false;
  } else if (keyCode == 83) {
  	// keycode is 's'
    leftShip.isDown = true;
    leftShip.isUp = false;
  }
}
function keyReleased() {
  if (keyCode == UP_ARROW) {
    rightShip.isUp = false;
  } else if (keyCode == DOWN_ARROW) {
    rightShip.isDown = false;
  }
  
  if (keyCode == 87) {
    leftShip.isUp = false;
  } else if (keyCode == 83) {
    leftShip.isDown = false;
  }
}


So we have our basic black background, we create these Ship objects which take x as a constructor argument and then each iteration we are updating these ships and drawing them. And also we have a keyPressed() handler which checks to see whether player one clicks ‘w’ or ‘s’ and player two clicks “up” or “down”, then we set some flags within the Ship class.

Ship.js

class Ship {

  constructor(x) {
    this.x = x;
    this.score = 0;
    this.respawn();
    this.r = 10;
  }

  respawn() {
    this.y = height - 20;
    this.isUp = false
    this.isDown = false;
  }
 
  update() {
    if (this.isUp && this.y > 0) {
      this.up();
    } else if (this.isDown && this.y < height - 20) {
      this.down();
    }
    
    if (this.hasPlayerScoredAPoint()) {
      this.score ++;
      this.respawn();
    }
  }
  
  display() {
    ellipse(this.x, this.y, this.r * 2, this.r * 2);
  }
  
  
  up() {
    this.y--;
  }
  
  down() {
   this.y++;
  }
  
  hasPlayerScoredAPoint() {
    if (this.y <= 0) {
      return true;
    }
    return false;
  }
}


So the ship class lets us keep track of the players x and y locations as well as their current score and whether they are going up isUp or down isDown. In the update() function (this is getting called within the draw() function of sketch class, so it is getting called 60 times per second) it updates the ships current position, so checks whether it is going up/down, if so, makes it go up/down by calling the up()/down() functions. It also checks whether the player has scored a point by going off the top of the screen, if it does then it increases the ship’s score and resets the ship to the starting position.

Adding debris


The obstacles spawn either to the left of the screen so -width to 0 x position or to the right of the screen width to width * 2 at a random location for both x and y.

So if the debris has spawned to the right of the screen, it’ll come left, if it has spawned to the left of the screen, it’ll come right. Then when it is no longer in view it will reset.

I’ve probably explained that awfully, have a look at the diagram I created below, should make stuff a bit clearer.

Debris.js

class Debris {

  
  constructor() {
    this.r = 5;
    this.resetDebris();
  }
  
  resetDebris() {
  	
    this.y = random(height - 70);
    
    let spawnLeftSide = random(1) < 0.5;
    
    if (spawnLeftSide) {
    	this.x = random(-width, 0);	
    	this.isGoingLeft = false;
    } else {
    	this.x = random(width, width * 2);
      this.isGoingLeft = true;
    }
    
    
  }
  
  update() {
  	if (this.isGoingLeft) {
    	this.x --;
    } else {
    	this.x ++;
    }
    
    if (this.isOffScreen()) {
    	this.resetDebris();
    }
  }
  
  
  isOffScreen() {
    if (this.isGoingLeft && this.x < -5) {
    	return true;
    } else if(!this.isGoingLeft && this.x > width + 5) {
    	return true;
    }
    return false;
  }
  
  display() {
  	ellipse(this.x, this.y, this.r * 2, this.r * 2);
  }
  
  hasHitShip(ship) {
  	if (dist(this.x, this.y, ship.x, ship.y) < this.r + ship.r) {
    	return true;
    }
    return false
  }

}

Sketch.js

I’ve added some really, really helpful comments so you know what code has been added at a glance.

let leftShip;
let rightShip; 
let allDebris = [];


// how hard do you want to make it? :D
const NUM_DEBRIS = 30;

function setup() {
  createCanvas(400, 400);
  leftShip = new Ship(width * 0.33);
  rightShip = new Ship(width * 0.66);
  
  // create the debris objects
  for (let i = 0; i < NUM_DEBRIS; i++) {
  	allDebris.push(new Debris());
  }
}

function draw() {
  background(0);
  
  leftShip.update();
  rightShip.update();
  
  leftShip.display();
  rightShip.display();
  
  // sexy function call
  updateDebrisAndCheckCollisions();
  
}


// sexy function
function updateDebrisAndCheckCollisions() {
  for (let i = 0; i < allDebris.length; i++) {
    allDebris[i].update();
  	allDebris[i].display();
    
    if (allDebris[i].hasHitShip(leftShip)) {
    	leftShip.respawn();
    } else if (allDebris[i].hasHitShip(rightShip)) {
    	rightShip.respawn();
    }
  }
}


function keyPressed() {
	if (keyCode == UP_ARROW) {
  	rightShip.isUp = true;
    rightShip.isDown = false;
  } else if (keyCode == DOWN_ARROW) {
  	rightShip.isDown = true;
    rightShip.isUp = false;
  }
  
  
  if (keyCode == 87) {
  	// keycode is 'w'
    leftShip.isUp = true;
    leftShip.isDown = false;
  } else if (keyCode == 83) {
  	// keycode is 's'
    leftShip.isDown = true;
    leftShip.isUp = false;
  }
}


function keyReleased() {
	if (keyCode == UP_ARROW) {
  	rightShip.isUp = false;
  } else if (keyCode == DOWN_ARROW) {
  	rightShip.isDown = false;
  }
  
  if (keyCode == 87) {
    leftShip.isUp = false;
  } else if (keyCode == 83) {
    leftShip.isDown = false;
  }
}

So each debris object is created in the setup() function and then updated and drawn in the updateDebrisAndCheckCollisions() function. So each debris is checked against both players to see whether it has hit the ship if so the player is reset to the bottom. Notice in the Debris class in the hasHitShip() function we use the p5.js dist() function which calculates the distance between two points. At the moment the ship is a circle and the debris is a circle, so if the distance is less than the ships radius and the debris radius then they’re intersecting each other and thus have hit!

Let’s add the score!

We already have the building blocks for the player’s score as when they move off the top of the screen our member variable in the ship class score gets incremented, all we need to do is implement the bugger so we can see it!

So this is just a case of creating a new class called score, and then passing it the players current score each frame. Note, there are definitely better ways to do this, for instance, you could create a handler so it only passes the current score to the score object when it changes, but this will do for now.

Score.js

class Score {

  constructor(x) {
    this.x = x;
    this.y = height - 20;
  }
  
  
  display(score) {
    fill(255);
    textAlign(CENTER);
    textSize(60);
    text(score, this.x, this.y);
  }
 
}

Sketch.js

Once again, I’ve added dazzling comments so you know what’s been added! I’m a strong believer that each comment is an apology for writing poor code, but this is an exception 🙂


// left and right score, one for each player
let leftScore; 
let rightScore;

function setup() {
  createCanvas(400, 400);
  leftShip = new Ship(width * 0.33);
  rightShip = new Ship(width * 0.66);
  for (let i = 0; i < NUM_DEBRIS; i++) {
  	allDebris.push(new Debris());
  }

  
  // creating the score objects
  leftScore = new Score(width * 0.33 - 40);
  rightScore = new Score(width * 0.66 + 40);

}

function draw() {
  background(0);
  
  leftShip.update();
  rightShip.update();
  
  leftShip.display();
  rightShip.display();
  
  updateDebrisAndCheckCollisions();
  
  
  // pass in the players current score
  leftScore.display(leftShip.score);
  rightScore.display(rightShip.score);
}

Adding the spaceship

I know what you’ve been thinking, I came here to code Space Race and ended up with a bunch of shitty circles, I get ya, lets add the image.

I used this image, you can copy it, it’s my shocking attempt at photoshop. Hopefully, you can just copy and paste it.

Trust me, it looks better on a black background haha..

Sketch.js

This is just a case of using the p5.js loadImage() function specifying the location of the image as an argument.

let leftScore; 
let rightScore;

let spaceshipImage; // this is gonna have our gorgeous ship
function setup() {
  createCanvas(400, 400);
  
  // get the spaceship image from your project-folder
  spaceshipImage = loadImage('spaceship.png');
  
  // pass the image into the ship object
  leftShip = new Ship(width * 0.33, spaceshipImage);
  rightShip = new Ship(width * 0.66, spaceshipImage);
  
  for (let i = 0; i < NUM_DEBRIS; i++) {
  	allDebris.push(new Debris());
  }

  leftScore = new Score(width * 0.33 - 40);
  rightScore = new Score(width * 0.66 + 40);


  
}


Ship.js

Set the imageMode() to the center so it’s more similar to the circle, as usually the reference point of the image is the top left, this set’s the reference point to the middle of the image, so we don’t need to change any of the hit detection!

class Ship {

  constructor(x, spaceshipImage) {
    this.x = x;
    this.score = 0;
    this.r = 15;
    this.respawn();
    // store the image in the ship object
    this.spaceshipImage = spaceshipImage;
  }
  
  update() {
  	if (this.isUp && this.y > 0) {
    	this.up();
    } else if (this.isDown && this.y < height - 20) {
    	this.down();
    }
    
    if (this.hasPlayerScoredAPoint()) {
      this.score ++;
      this.respawn();
    }
  }
  
  display() {
    
    // display our beautiful ship to the world!
    imageMode(CENTER);
    image(this.spaceshipImage, this.x, this.y, this.r*2, this.r*2);
  }
  

The timer!

As is a tradition with these tutorials, I believe the best way to learn code is to get stuck in, so I challenge you to implement the timer similar to what I’ve done above. If you get stuck go check out my GitHub repository with the solution I’ve done, see if you can do it better, I’m sure you can!

Next blog will be released next week on Sunday 17th February at 8pm!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s