How to code Gran Trak 10 (1974) – 3

Gran Trak 10 is yet another Atari arcade game! One of, if not the first car racing game to be released. Another car game I considered to code this week was speed race which was released a month later, but due to its popularity, Gran Trak took the spot. It’s also Steve Wozniak’s favourite game, so I felt obliged 😀

Gran Trak 10 is a single player game where the player controls a car along a race track, the idea is to hit as many checkpoints as possible before the time runs out. The game is controlled by a steering wheel, gears, and brakes.

What we’re gonna code!

Well to start off with, I don’t have a steering wheel so I’m going to have to substitute that movement to simple arrow keys, I’m sure someone could make a pull request to the code to add the option for steering wheel usage.

We’re going to find some clever way to create our own race tracks and be able to store these tracks in a JSON file so we can import different tracks to our game and share them with each other. If we were going to manually enter the x and y position of every wall on the track we would be here forever.

The Car!

What we need

  • A car that moves in the direction it’s facing.
  • A gear system.
  • Text to display the current gear.

The Player/Car class

class Player {
  constructor() {
  	this.respawn();
  }
  
  respawn() {
    this.x = width / 2 + 10;
    this.y = 25;
    this.speed = 1;
    this.angle = 0;
    this.rotateAmount = 0;
    this.r = 7.5;
    
    this.gear = 1;
  }
  
  
  update() {
    this.handleCurrentGearAffectOnSpeed();
    this.goTheWayWereFacing();
    this.angle += this.rotateAmount;
  }
  
  handleCurrentGearAffectOnSpeed() {
    if (this.speed < this.gear) {
    	this.speed += 0.02;
    } else if (this.speed > this.gear) {
    	this.speed -= 0.02;
    }
  }
  
  goTheWayWereFacing() {
    this.x += this.speed * cos(this.angle);
    this.y += this.speed * sin(this.angle);
  }
  
  
  draw() {
    rectMode(CENTER);
    
    push();
    translate(this.x, this.y);
    rotate(this.angle);
    rect(0, 0, 15, 15);
    pop();
    
    push();
    fill(255);
    textSize(40);
    text(this.gear, 110, 150);
    pop();

  }

}



handleCurrentGearAffectOnSpeed()

The handleCurrentGearAffectOnSpeed function handles the current gear’s effect on the speed if you hadn’t noticed ;). So it’s pretty simple, I’ve mapped the speed relative to the gear, so if you’re in second you can go up to speed 2. If you drop down from 4th to 2nd gear and you’re at a speed of 4 that will decrease steadily until you reach a speed of 2.

goTheWayWereFacing()

So this function makes the car go the way it’s facing. I’ve created a diagram to show how the maths works for moving the car, but to be honest, I drew it and just me now looking at It I’m confused myself so I would recommend taking a watch of this 3-minute youtube video if you don’t understand the maths behind it.

Sketch.js

A lot of what we see here is the same as in the previous tutorials. We create the player object, we update and draw it each frame. A keypressed handler has been added so if you click the right/left keyboard key you’ll turn the car. If you hit the up and down arrow you shift up and down in gears.

let player;
const ROTATE_AMOUNT = 0.1;

function setup() {
  createCanvas(400, 400);
  player = new Player();
}

function draw() {
  background(0);
  player.update();
  player.draw();
}


function keyPressed() {
  if (keyCode == LEFT_ARROW) {
    player.rotateAmount = -ROTATE_AMOUNT;
  } else if (keyCode == RIGHT_ARROW) {
    player.rotateAmount = ROTATE_AMOUNT;
  } else if (keyCode == UP_ARROW && player.gear < 4) {
    player.gear++;
  } else if (keyCode == DOWN_ARROW && player.gear > 1) { 
    player.gear--;  
  }
}


function keyReleased() {
  if (keyCode == RIGHT_ARROW  || keyCode == LEFT_ARROW)  {
    player.rotateAmount = 0;
  } 
}

The Race Track

As mentioned earlier we want the ability to dynamically create a track and then store and save that in a JSON file.

Creating the boundaries array

Boundary.js

I’ve created a new Boundary class which is just going to be used to store the x and y locations of the Boundary and draw it in the game.

class Boundary{
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.r = 5;
  }
  
  draw() {  
    ellipse(this.x, this.y, this.r*2, this.r*2);
  }
}

And in sketch.js define the new boundaries array and draw it each frame. mousePressed() adds a new object to the boundaries array at the mouse’s current x and y location.

let boundaries = [];
let player;
const ROTATE_AMOUNT = 0.1;

function setup() {
  createCanvas(400, 400);
  player = new Player();
}

function draw() {
  background(0);
  player.update();
  player.draw();
  
  for (let boundary of boundaries) {
    boundary.draw();
  }
}

function mousePressed() {
  boundaries.push(new Boundary(mouseX, mouseY)); 
}


Saving the track

Say you make an amazingly awesome track you want to share with your friends, we need to store it. So we will use p5.js built-in function for saving JSON, saveJSON.


function keyPressed() {
  
  // player clicks 's' it saves the boundaries to a file
  if (keyCode === 83) {
    saveJSON(boundaries, 'boundaries.json');
  } else if (keyCode == LEFT_ARROW) {
    player.rotateAmount = -ROTATE_AMOUNT;
  } else if (keyCode == RIGHT_ARROW) {
    player.rotateAmount = ROTATE_AMOUNT;
  } else if (keyCode == UP_ARROW && player.gear < 4) {
    player.gear++;
  } else if (keyCode == DOWN_ARROW && player.gear > 1) {
    player.gear--;  
  }
}

The data will look something like this:

[
  {
    "x": 21,
    "y": 29
  },
  {
    "x": 19,
    "y": 54
  },
  {
    "x": 19,
    "y": 81
  },
  {
    "x": 19,
    "y": 105
  },
  {
    "x": 19,
    "y": 134
  },
  {
    "x": 19,
    "y": 158
  },
  {
    "x": 18,
    "y": 188
  },

Loading the track

In p5.js the preload function gets called before setup(), it is used to handle asynchronous loading of files. Basically, setup() waits for preload() to finish executing. So it is perfect for our scenario, we don’t want to start the game until we’ve actually loaded the map.

The second argument of the loadJSON function is a callback. loadJSON(path, callback, [errorCallback]). So we supply it with the setupBoundaries function, which loops through the data and creates our boundaries objects and chucks them into our boundaries array.


function preload() { 
  loadJSON("boundaries.json", setupBoundaries);
}

function setupBoundaries(allBoundaries) {
  
  for (let boundary of Object.keys(allBoundaries)) {
    var currentBoundary = allBoundaries[boundary];
    let gameBoundary = new Boundary(currentBoundary.x, currentBoundary.y);
    boundaries.push(gameBoundary);
  } 
}

Here’s an example of a pretty poor map I created, looks a mess doesn’t it.

Hit detection

Obviously, we need the boundaries to have some meaning, so if a player hits a boundary it should reset their position.

So I’ve added a new function within the boundary class which takes care of this.

  
  checkCollision(player) {
    if (dist(player.x, player.y, this.x, this.y) < (player.r + this.r)) {
      player.respawn();
    }
  }

And we just call this in the boundary loop within sketch.js

  for (let boundary of boundaries) {
    boundary.checkCollision(player);
    boundary.draw();
  }

Luckily, we already had the respawn logic within the player class. I snuck that one in there, I’m sure you noticed, you clever bugger.

What else YOU can add

As is tradition with these tutorials I leave a little more to be implemented in the hope that you’d get stuck in with some code yourself, as the best way to learn is by doing!

There are a few things you could add:

  • Checkpoint system
  • Global countdown timer
  • Make it look more like a car
  • Copy the actual map of Gran Trak
  • Lap current time and best lap time (I have implemented) see below

Use your imagination and have fun with it!

One comment

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