Module 8: Make obstacles

Objective: use arrays and loops to manage groups of data

  1. One, two, three, infinity...
  2. Make an obstacle!
  3. From one to many
  4. Adding items to arrays
  5. Loops, loops, loops
  6. Til next time...

One, two, three, infinity...

Here we go again! Welcome to module 8! Usually, we're trying to get rid of obstacles in life, but today we're going to make some.

Last we left the block dodger game, we had a player block which we were able to ease left and right using our mouse (pretty cool, eh?) and we used conditionals to prevent it from moving off the edge of the screen (even cooler?). But we don't have any blocks to dodge! Let's add some!

Moon landing meme

There is a saying that counting goes "one, two, three, infinity." In other words, once you can handle making the conceptual leap between one to two, and two to three, going further is a trivial repetition of that process. In our case, making our first object or two is pretty tough, but once we have a constructor function (see the previous module if you're not sure what that means) and two concepts we'll learn about today, arrays and loops, we can make and manage as many objects as our heart desires (or until we run the computer out of memory).

And, by the way, the objects we'll be making aren't going to be more player boxes (we only need one of those); they'll be obstacle boxes which will fall from the top of the screen. Did you guess that we'll need another constructor function to do that? Good!


Make an obstacle!

Let's start by deciding what properties and functions our new Obstacle class will need. Then we'll get a single obstacle on screen. Once that's working, we'll add a few obstacles and call it a day.

Actually, it turns out the Obstacle class is going to be very similar to the player Box class. It'll have the standard x, y, size, and color, plus one new property: a vy property, which represents the obstacle's velocity, or rate of change, on the y-axis. Obstacles will also need a move() function, just like the player box, but the obstacle's movement behavior will be different (and a bit simpler). Here's all of that in code:

// This class represents an obstacle the player must avoid
let Obstacle = function (x, y, vy, size, color) {
  this.x = x;
  this.y = y;
  this.vy = vy;
  this.size = size;
  this.color = color;
};

// Updates an obstacle's position
Obstacle.prototype.move = function () {
  this.y += this.vy;
};

Look familiar? It's not all that different from the player box class! Now we can use our new Obstacle constructor to actually make an obstacle and draw it. But before moving further, I notice that my code has multiple lines of code that are part of a single task that I'm going to want to be reusable (did someone say "function"?):

// Make a new player box
player = new Box(canvas.width / 2, canvas.height - 20, 20, "#ffffff");

// Set the mouse's initial value
mouseX = 0;
  
// Listen for mouse movements
document.addEventListener("mousemove", function (e) {
  mouseX = e.pageX - canvas.getBoundingClientRect().left;
});

So let's toss all of this, plus the line to create a new Obstacle inside of an init() (short for initialize) function and call it. Note that the call to the obstacle's constructor has an additional parameter for the vy, which I made 2 (try other numbers here to change the speed of the obstacle):

function init() {
  
  // Make a new player box
  player = new Box(canvas.width / 2, canvas.height - 20, 20, "#ffffff");
  
  // Make an obstacle
  obstacle = new Obstacle(canvas.width / 2, 0, 2, 30, "#ccccff");
  
  // Set the mouse's initial value 
  mouseX = canvas.width / 2;
  
  // Listen for mouse movements
  document.addEventListener("mousemove", function (e) {
    mouseX = e.pageX - canvas.getBoundingClientRect().left;
  });
}

// Initialize the game
init();

This function sets up the game state by giving default values to all of our variables. To use it, we need to create the obstacle variable and add movement and draw code to the update() function. With all that in place, here's our current state of affairs:

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

Did you catch that? An obstacle falls from the center of the screen and disappears (run it again if you missed it!). Try tweaking the parameters in the new Obstacle() call to the Obstacle's constructor and see what happens.


From one to many

Now we have an obstacle! We can't collide with it yet--we'll add that feature in the next module. Although our goal is to get a ton of obstacles, we broke the problem down into a smaller one: getting one obstacle on the screen, which we already knew how to do. This is a common strategy in programming: breaking large problems down into small ones, then proceeding towards the end result incrementally.

Onward, to arrays! Arrays are, like objects, a way to group related pieces of data in your program's memory. However, where objects' properties and functions are accessed by unique names, for example, box.x, array elements are accessed by an index number, for example, students[4]. This means that data in arrays is ordered numerically, while objects' data isn't. The practical implication of all this is that arrays are best suited for grouping one type of data, which can easily be identified by number, whereas objects are groups of many different types of data which need to be identified by name. For our block dodger game, we want to create a collection of Obstacle objects. The naive way to accomplish this is to use a bunch of variables, say, obstacle1, obstacle2, obstacle3 and so on, but this is clearly going to become impossible to manage very quickly.

Let's adjust our code to use an array instead of a single obstacle variable to see how this plays out. Start by pluralizing the obstacle variable--it'll store our array of obstacles. Next, in the init() function, we'll set the obstacles variable to an empty array (denoted by the empty [] brackets):

// Initializes the game
function init() {
  
  // Make a new player box
  player = new Box(canvas.width / 2, canvas.height - 20, 20, "#ffffff");
  
  // Make obstacles an empty array
  obstacles = [];
  
  // Set the mouse's initial value 
  mouseX = canvas.width / 2;
  
  // Listen for mouse movements
  document.addEventListener("mousemove", function (e) {
    mouseX = e.pageX - canvas.getBoundingClientRect().left;
  });
}

However, changing our variable from obstacle to obstacles invalidates our current update() function, and we've removed the code that creates a new obstacle. We need to populate our empty array with obstacle objects and then adjust our update() code to process (move, draw) each object in the array.


Adding items to arrays

Arrays have a ton of useful functions to add and remove items in various ways. To add an item to the end of an array, call the array's push() function and pass the item as its parameter. Here's an example of push() using an array of strings, instead of objects, for simplicity's sake:

// Declare a new empty array
let people = [];

// Add "Geraldine" to the array
people.push("Geraldine");
console.log(people);  // [ 'Geraldine' ]

// Add "Alex" to the array
people.push("Alex");
console.log(people);  // [ 'Geraldine', 'Alex' ]
console.log(people[1]);  // 'Alex'

Let's do the same in our init() function, except we'll add a few new Obstacle objects to the array instead of strings:

// Initializes the game
function init() {
  
  // Make a new player box
  player = new Box(canvas.width / 2, canvas.height - 20, 20, "#ffffff");
  
  // Make obstacles an empty array
  obstacles = [];
  
  // 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"));
  console.log(obstacles); 
  
  // Set the mouse's initial value 
  mouseX = canvas.width / 2;
  
  // Listen for mouse movements
  document.addEventListener("mousemove", function (e) {
    mouseX = e.pageX - canvas.getBoundingClientRect().left;
  });
}

Let's open the browser console and view the result of the console.log() call:

Console output image

This gives us a lot of handy information about the array! At the top, Array(3) tells us we have an array with three items in it. Right below, at index 0, is an Obstacle object with a variety of properties. Note that expanding the __proto__ on the object at index 2 shows us the functions associated with these objects: move and constructor and their parameters. Bottom line: we've successfully made an array and added some objects to it!

Loops, loops, loops

All right! Now, we have an array with items in it, but our update() code is still trying to find an obstacle, which no longer exists, to move and draw it. That won't work--we have three obstacles, and they're all in an array, so we need some way to access each one specifically in order to move and draw it.

We can solve this problem the naive way with the following code:

// Update and draw each obstacle by index
obstacles[0].move();
ctx.fillStyle = obstacles[0].color;
ctx.fillRect(obstacles[0].x, obstacles[0].y, 
             obstacles[0].size, obstacles[0].size);
obstacles[1].move();
ctx.fillStyle = obstacles[1].color;
ctx.fillRect(obstacles[1].x, obstacles[1].y, 
             obstacles[1].size, obstacles[1].size);
obstacles[2].move();
ctx.fillStyle = obstacles[2].color;
ctx.fillRect(obstacles[2].x, obstacles[2].y, 
             obstacles[2].size, obstacles[2].size);

This works, but yuck! And what if our array has a few dozen, or even a million items in it (a common computer science scenario!)? We'd have to type some three million lines of code--clearly, we need a new trick! Whenever you need to perform some action on each item in an array, or many other actions requiring repetition, use a for loop. For loops can be a little tricky syntactically at first, so type carefully and compare the below code to the above code (both accomplish the same task of moving and drawing each obstacle):

// Update and draw each obstacle with a for loop
for (let i = 0; i < obstacles.length; i++) {
  obstacles[i].move();
  ctx.fillStyle = obstacles[i].color;
  ctx.fillRect(obstacles[i].x, obstacles[i].y, 
               obstacles[i].size, obstacles[i].size);
}

This code is scalable. We don't need to worry about how many items are in the obstacles array: the loop initializes an index variable, i, to 0 and runs as long as i is less than the length (or number of items) of the obstacles array. On the first run through the block of code between the { }, i is 0, and the first element in the array is moved and drawn. Then, the i++ increment is run and i is equal to 1. Then, the block of code between the { } runs a second time and the second block is drawn. Then i++ runs again, and so on, until the loop counter reaches the length of the array and the loop stops.

Here's how to break down for loop syntax; the initialization code runs once at the beginning of the loop. That's usually where variables are declared for counting through the loop. The end conditional test runs before each loop, and if it's false, the loop stops executing, otherwise the body of the loop is executed. The third part of the for loop, labeled "post-body code," runs after each trip through the loop body, and is typically where loop control variables are incremented.

for (/*initialization code*/; /*end conditional*/; /*post-body code*/) {
  // Loop body, a block of code to execute on each trip through the loop
}

Let's take another simple example; run this code and play with it to convince yourself of how it works:

// Count to 100 with a loop
for (let i = 1; i <= 100; i++) {
  console.log(i);
}

Putting it all together, let's run the code and see what happens.

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

Celebration! There are three obstacles dropping from the top of the screen. The best news is that our array and loop are done, waiting to handle as many items as we care to throw at them.

Foxtrot programming joke

Til next time...

Whew! Great job with some tricky concepts here. As always, try tweaking the code, adding more obstacle objects, and playing with the numbers. Break it and fix it and keep playing with examples until it becomes clear how it all works--arrays and loops are pretty complicated at first, but they'll become second nature with enough exposure!

Next module is when the game comes together: we add utility functions to randomize how blocks are generated and check for collisions between the player and obstacles. We'll also need to add the ever-important score variable. We'll also touch up a few odds and ends and optimize the code here and there. After that, you'll have yourself a pretty solid and enjoyable block dodger game to mess around with. The main concepts will be applicable to all programming projects: variables to store data in memory, operators to manipulate the data, objects and arrays to organize and collect data, conditionals to enable the program to respond dynamically to different situations, functions to modularize and reuse code, and loops to automate repetitive tasks.

Don't worry about memorizing the syntax; just try to remember what kind of programming concept you need for a particular scenario, then look the syntax up as necessary. I've created a handy JS cheat sheet for just that purpose!

Programming meme