Module 8: Make obstacles
Objective: use arrays and loops to manage groups of data
- One, two, three, infinity...
- Make an obstacle!
- From one to many
- Adding items to arrays
- Loops, loops, loops
- 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!
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:
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.
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!