Module 9: Randomization, collision detection, and score

Objective: explore JS's Math library, create utility functions and optimize code.

  1. Wrapping up the block dodger
  2. Randomize it!
  3. More random numbers
  4. Collision detection
  5. Optimization
  6. Last (but not least), adding score
  7. Next steps

Wrapping up the block dodger

Keanu Reeves programming meme

Woohoo! Here we are at module 9, the penultimate module of the course, and the one where we wrap up the block dodger game! Believe it or not, but we've covered all of the major points of basic programming, so this last module is less about introducing new concepts and more about practicing what we've already learned.

As you've noticed, we still have a few missing pieces standing in the way between where we are now and a full-blown block dodger, including generation of obstacle blocks, collision detection between obstacles and the player box, and a player score. We'll handle these features by making use of JS's built-in Math library, a new array function, and adding a few additional functions to keep our code organized.


Randomize it!

Random numbers are very important in game design (and programming in general!). They're an easy way to reduce predictability, simulate chance, and add a natural feel to applications. Most programming languages have a variety of handy ways to get random numbers, but JS offers fairly slim pickings: Math.random(), which returns a random decimal between 0 and 1. Let's put this to use in generating our falling obstacles: instead of hard-coding three obstacles to appear in the init() function as we did...

// Add a few obstacles to the array
obstacles.push(new Obstacle(10, 0, 1, 30, "#5959ee"));
obstacles.push(new Obstacle(100, 0, 1, 20, "#9999ff"));
obstacles.push(new Obstacle(200, 0, 2, 25, "#8877ef"));

...we'll remove those lines and head over to the update() function, where we'll use a conditional and Math.random() to determine whether, at a given frame, a new obstacle gets created or not:

// Generate a new obstacle every so often
if (Math.random() > 0.98) {
  obstacles.push(new Obstacle(canvas.width / 2, -20, 2, 20, "#8877ef"));
}

If my math is correct, this works out to about a 1 percent chance that on any given frame, an obstacle will be created. Try adjusting the 0.98 number and see how the frequency of obstacle generation changes--it's pretty sensitive, so consider adding decimal places for better precision.

See the Pen block dodger (part 8) by ggorlen (@ggorlen) on CodePen.


More random numbers

Now that we're using random numbers, let's take a moment to add a utility function to our program to produce random integers between two bounds. Random integers will be useful for randomizing the placement and properties of our obstacles. Utility functions are very general and reusable functions that handle simple tasks, saving a lot of trouble. The below function takes a range of input between a low and high number, and returns a random whole number between the two. Technically, the lower bound is inclusive and the upper bound is exclusive, meaning it's possible for the function to return the low number but not the high number.

// Returns a random number between two bounds
function randInt(lo, hi) {
  return Math.floor(Math.random() * (hi - lo) + lo);
}

If you're curious, the way this works is multiplying Math.random() by an integer x produces a random decimal between 0 and x. Subtracting the lower bound from the upper bound before this operation, then re-adding the lower bound to the result puts the number in the correct range. At this point, the number is still a decimal, so Math.floor() is used to chop off that decimal. Try it out with randInt(5, 15); and test the results.

But honestly, the details aren't too important here: it works; use it! Most programming languages offer this function as a standard library feature, so you wouldn't have to worry about the implementation details in those languages--don't sweat them here either!

Random number meme

Let's put this function to use! But before we do, we're about to be making a mess in our nice, clean update() function. It's a good idea to extrapolate this burgeoning obstacle-making code to a separate function, makeObstacle(), keeping the dirty details away from update().

// Generates some random values for an Obstacle and 
// adds the new Obstacle to the obstacles array
function makeObstacle() {
  let size = randInt(10, 20);
  let x = randInt(0, canvas.width - size);
  let y = 0 - size;
  let color = "hsl(200, " + randInt(0, 100) + "%, 60%)";
  let vy = (Math.random() + 1) * 30 / size;
  obstacles.push(new Obstacle(x, y, vy, size, color));
}

The above code looks complex, but all of the numbers and operations were found through old fashioned trial and error; you'll come up with your own tricks for finding the right "feel" for these kinds of things, which is why I'm endlessly encouraging you to tweak my examples (go ahead, play with the above code--when it gets too complex, revert to simple literal numbers!). For example, I wanted smaller objects to move faster than larger objects, so I made the velocity inversely proportional to size. The color variable uses hsl(), which is just another way to represent color; check the explanation here to learn more.

All right; now, we'll call makeObstacle() from the update() function, and we've got ourselves a pretty nifty looking near-game:

See the Pen block dodger (part 9) by ggorlen (@ggorlen) on CodePen.


Collision detection

Next, we really need collision detection, or some function that tells us if two boxes are overlapping. Collision detection can be a very complex topic, but luckily, it's pretty straightforward as it relates to rectangles. The idea is to check whether each rectangle's x coordinate is between the other rectangle's bounds on the x axis and repeat the process for the y axis. If it passes tests on both axes, they're intersecting somewhere. Here's the code:

// Returns true if a and b rectangles 
// are colliding, false otherwise
function collides(a, b) {
  return a.x <= b.x + b.size &&
         b.x <= a.x + a.size &&
         a.y <= b.y + b.size &&
         b.y <= a.y + b.size;
}

Looking at this for too long can make your head hurt, and it's written here a little less for readability and more for succinctness; this sort of stuff is best handled by a game framework with pre-written complex collision detection algorithms, by searching a programming help or reference websites like Stack Overflow, or avoidng games that have heavy amounts of collision detection and physics, such as platformers and shooters (at least for your first project). That is, assuming you're not a math buff (power to you if you are!). In the meantime, just plug in the one above and enjoy!

And, of course, we need to actually make a call to the collides() function and send it two boxes. But where? This is a great job for the loop we're using to move and render our obstacles--since we're already doing that, we'll just add a collision check inside the loop and re-initialize the game if a collision occurs:

// Check for a collision between the player and this obstacle.
// Reinitialize the game if there was a collision.
if (collides(player, obstacles[i])) {
  init();
  break;
}

The break; statement simply ends the for loop immediately; once a new game has been set up in the init() function, there will no longer be any obstacles in the newly-cleared obstacles array, and the game will crash (we'll talk more about bugs in the next module). Let's take a look at the whole update() function since it's quite large (it's also visible in the Codepen below). In all honesty, it might be high time to split this function into smaller helper functions, but we'll call it sufficient for now.

// Updates the animation state and draws a frame
function update() {
    
  // Generate a new obstacle every so often
  if (Math.random() > 0.95) {
    makeObstacle();
  }
  
  // Clear the entire canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // Update the position of the player
  player.move(mouseX);
  
  // Draw the player box
  ctx.fillStyle = player.color;
  ctx.fillRect(player.x, player.y, player.size, player.size);
  
  // Update and draw the obstacles
  for (let i = 0; i < obstacles.length; i++) {
    
    // Update this obstacle's position
    obstacles[i].move();
    
    // Check for a collision between the player and this obstacle.
    // Reinitialize the game if there was a collision.
    if (collides(player, obstacles[i])) {
      init();
      break;
    }
    
    // Draw the obstacle
    ctx.fillStyle = obstacles[i].color;
    ctx.fillRect(obstacles[i].x, obstacles[i].y, 
                 obstacles[i].size, obstacles[i].size);
  }
  
  // Ask the browser to move on to the next frame
  requestAnimationFrame(update);
}

At this point, try playing the game--I think it's pretty close to satisfactory!

See the Pen block dodger (part 10) by ggorlen (@ggorlen) on CodePen.


Optimization

There are only two remaining things we should take care of before calling this game done. One is a classic arcade game feature whose importance cannot be overstated: score! It's a simple addition, but putting a number on the player's performance adds a competitive feel and an addictive quality that makes it more than worth the small amount of trouble.

The second feature to think about is a very subtle optimization issue. What happens to obstacles that fly below the bottom of the screen? To find out if they're still there, we can add a console.log() statement and print the length of our obstacles array every time we make a new one:

// Generates some random values for an Obstacle and 
// adds the new Obstacle to the obstacles array
function makeObstacle() {
  let size = randInt(10, 20);
  let x = randInt(0, canvas.width - size);
  let y = 0 - size;
  let color = "hsl(200, " + randInt(0, 100) + "%, 60%)";
  let vy = (Math.random() + 1) * 30 / size;
  obstacles.push(new Obstacle(x, y, vy, size, color));

  console.log(obstacles.length);
}

If you're skilled enough at the game, you'll notice that the longer the player stays alive, the larger the obstacles array grows. If the player is really good, there could be hundreds or thousands of items in this array, every one of which is being moved, rendered (offscreen, mostly), and checked for collisions.

All of this extra work can slow your game down and bring the player's fun to a grinding halt. Luckily, the issue isn't too problematic in this simple game--the player will almost always lose before it becomes a problem and modern computers can handle these calculations well. Nonetheless, it illustrates a typical efficiency problem programmers face constantly, so let's go ahead and resolve it by removing obstacles from the array as soon as they pass the bottom of the screen. Coincidentally, this is also where I'd like to increase the player's score, which can be thought of as the number of obstacles the player has successfully dodged (you could also make the score time-based, for example).

JS offers many functions to remove an item from an array, pop() being the most common (it removes and returns the last item in the array). However, this won't help in our case, since the obstacle that just flew off screen could be anywhere in the array, so we'll have to resort to a somewhat more technical function, splice(), which takes two number parameters, an index to begin removal at and the number of elements to remove starting from that index. splice() also allows for adding items to the array in the same call, but we'll ignore those optional features. As always, have a look at a reference site, such as this one, to learn about what these methods are and how they work--memorization isn't necessary!

Another subtle thing to note is that inside of a loop, if we splice out an item, the loop counter variable i will be off by one and miss updating the next item in the loop. We could adjust this manually, but my preferred approach is to simply run the for loop backwards; we'll initialize the i index variable to the length of the array - 1, which points to the last item in the array, and count backwards to 0. This way, if any element in the array that i points to is removed, no problem; i will still point to the correct item on the next iteration:

for (let i = obstacles.length - 1; i >= 0; i--) { 
  // etc etc ...
}

And another handy point: we've got a lot of code in our for loop that refers to obstacles[i], so let's beautify our code by saving this current array element in a variable called o (short for obstacle) at the beginning of each iteration of the loop:

// Update and draw the obstacles
for (let i = obstacles.length - 1; i >= 0; i--) {
  
  // Save the obstacle in a variable
  let o = obstacles[i];

  // etc etc ...
}

Now, anywhere in our code that refers to obstacles[i] in this loop can be replaced with simply o. Let's put it together; here's the full loop body as it stands, complete with code to remove any off-screen obstacles:

// Update and draw the obstacles
for (let i = obstacles.length - 1; i >= 0; i--) {
  
  // Save the obstacle in a variable
  let o = obstacles[i];
  
  // Update this obstacle's position
  o.move();
  
  // Draw the obstacle
  ctx.fillStyle = o.color;
  ctx.fillRect(o.x, o.y, o.size, o.size);
  
  // Remove this obstacle from the obstacles array if off screen 
  if (o.y - o.size > canvas.height) {
    obstacles.splice(i, 1);
  }
  
  // Check for a collision between the player and this obstacle.
  // Reinitialize the game if there was a collision.
  else if (collides(player, o)) {
    init();
    break;
  }
}

Again, this loop code is getting a little large for my taste, but I'll leave refactoring it into its own function, maybe updateObstacle(), perhaps, for the reader.

See the Pen block dodger (part 11) by ggorlen (@ggorlen) on CodePen.


Last (but not least), adding score

We're almost there! To add a score, we need a--you guessed it!--number variable. We'll initialize it to 0, and whenever an obstacle reaches the bottom of the screen, increment that number by 1. We also need to display the score; this can be done on canvas, but my preference is to output it as HTML inside a <div> element, which avoids obstructing gameplay.

To do this, we'll need a new HTML element and some CSS to style it. Here's all of the HTML for the game (with the new <div> element added with an id of "score-container"):

<div class="container">
  <canvas id="paper" height=500 width=300></canvas>
  <div id="score-container">score: 0</div>
</div>

Here's all of the CSS for the game as well:

body {
  display: flex;
  background-color: #000;
  height: 95vh;
  width: 95vw;
}

.container {
  margin: auto;
}

#paper {
  background-color: #000;
  border: 1px solid #fff;
}

#score-container {
  font-family: monospace;
  text-align: center;
  color: #fff;
}

With those in place, back in our JS code, we'll need two new variables: a number variable, score, which keeps track of the score, and an object variable, scoreContainer, which will represent the score-container element from the HTML document. This object gives us a function to change its HTML content. Lastly, we'll want to increment the score variable and display it whenever it changes, which occurs in the init() and update() functions. Since this code is scattered throughout the application, I'll show you the final version, then let you find the changed lines and implement them in your version. Try searching for the new variables using your text editor to locate them. As programs grow larger, it can make good sense to split your JS code into multiple files!

See the Pen block dodger (part 12) by ggorlen (@ggorlen) on CodePen.

Next steps

Congratulations! You made it through quite a challenging tutorial. Please revisit it a few times, even if skimming, to take in the concepts; there is a LOT going on here. Creating your own game or application in the remaining weeks will be the best way to internalize everything you've seen. Another good idea is to find and play with very simple examples of each concept, as we did with loops.

Yet another programming meme

While you're welcome to leave this game as-is, I'd also like to suggest a few areas for improvement. I implemented a couple of these features in the final game, so you can start by copying those and adjusting them to your liking. You're welcome to try any of these out or ask for help in adding them to your own game:

The last module discusses starting your own project!