Building A Browser Game Part 4: Welcome to the Function Factory

Finally, the day has come. It has taken us some time to nail down the basics of using an HTML canvas element in Part 1, build an animation loop in Part 2, and using both to set up our gamespace in Part 3. Today’s installation will hopefully be a bit more satisfying. We get to make a thing that does things and we can see those things change! In this part, we are building the game object which the player controls; our titular Slug.

Almost all of our code today is going to be in the function we’re going to write. We’re building a function called createSlug() which will return an object that will be our slug. This kind of function is called a Factory Function and operates similarly to declaring a class and using a constructor. There is quite a debate between Object-Oriented Programming and Functional Programming Paradigms going on. We won’t get too deep into it in this article but it is something any new programmer should be aware of and thinking about. Since JavaScript ES6, we would have the option to use Class Declarations if we really wanted to. But because JavaScript is a functional programming language, our classes are essential fancy functions anyway. We can skip all the steps that JavaScript applies to classes by writing them as factory functions from the get-go.

For our createSlug() function we will pass in the starting x and y positions as arguments. Right away, we will set those arguments as key-value pairs in the object we’ll return.

function createSlug(startingX, startingY) {
return {
x: startingX,
y: startingY
}
}
let slug = createSlug(2 , 4);

This is the basic structure of our slug object. We can add more attributes and even set functions as attributes which we’ll call on our slug.

Let's think about what is going to make up our slug. We currently only have an x and y positions. We’ll want our slug to be a bit longer, so we’ll keep our current x and y positions as the head of the slug but instead store it as the first element in an array of [x, y] positions. We’ll call these positions segments (as in segments of the body) and let's create two more in the same column based on those starting coordinates.

function createSlug(startingX, startingY) {
return {
segmentPositions = [
[x , y],
[x, y - 1],
[x, y - 2]
]
}
}
let slug = createSlug( 2 , 4);

We’ll also change the parameters in our constructor a bit to default to coordinates in the center of the screen. That way, if we pass no arguments, our slug will start in the middle right away.

function createSlug(
x = Math.floor(numOfCells / 2),
y = Math.floor(numOfCells / 2)
) {
return {
segmentPositions: [
[x, y],
[x, y + 1],
[x, y + 2],
],
}
}
let sulg = createSlug(); //We don't need to pass anything in.

Later on, when our slug moves or grows, we can change this array.

We also will need to know which direction our slug is moving and what color to use to draw it. We won’t need to pass in a direction parameter so we can just assume it will always begin by moving “north.” For our color, let's put that as the first parameter so that if we want to change the color when we create an instance of the object, we can still use the default x and y values. This is what I have so far.

function createSlug(
color = "salmon", //< We've added this
x = Math.floor(numOfCells / 2),
y = Math.floor(numOfCells / 2)
) {
return {
color: color, //< We've added this
direction: "north", //< We've added this
segmentPositions: [
[x, y],
[x, y + 1],
[x, y + 2],
],
}
}

Finally, let’s add our first function! We’ll want a method called update() that will be used to organize all the methods we’ll want our slug to run each time a game tick happens. We’ll call this using slug.update() in or global update() function. I like to plug everything in from the top down like this so that as we build we can start to see what is and isn’t working as expected. We can create this function by setting the key to the name of our function and the value as an anonymous arrow function. All the functions we’ll be writing today will be methods of our slug and will be defined like this.

function createSlug(
color = "salmon",
x = Math.floor(numOfCells / 2),
y = Math.floor(numOfCells / 2)
) {
return {
color: color,
direction: "north",
segmentPositions: [
[x, y],
[x, y + 1],
[x, y + 2],
],
update: () => {
console.log("Updating Slug");
//We'll replace this with more functions later
}
}
}
let slug = createSlug();
//...Some more of our codefunction update() {
now = window.performance.now();
delta = now - then;
if (delta > fpsInterval) {
then = now - (delta % fpsInterval);
drawBackground();
slug.update(); //<--This is our new line!<-------------
}
!isPaused && requestAnimationFrame(update);
}

Let's get rendering. We want to create a drawSlug() method which will draw our slug as a series of lines each time we update our game. Let's create the function with a console.log() and plug it into our update() method just to see what's working.

function createSlug(
color = "salmon",
x = Math.floor(numOfCells / 2),
y = Math.floor(numOfCells / 2)
) {
return {
color: color,
direction: "north",
segmentPositions: [
[x, y],
[x, y + 1],
[x, y + 2],
],
update: () => {
console.log("Updating Slug");
this.drawSlug(); //<--This is our new line--
},
drawSlug: () => { //<-- This is our new function--
console.log("Drawing Slug");
},
}
}

We want drawSlug() to create a path that begins at our first segment position and then moves to each subsequent position. We’ll need to give it our color with strokeStyle and set lineWidth based on 0.8 x the scale of our grid. This way, if we ever change the size of our grid, our slug will scale along with it. First, we’ll create path connecting all the segmentPositions. We can start that by using beginPath() without any arguments, moveTo(x, y) the first segment in our array, and then lineTo(x, y) each addition segment using forEach(). We can also change the way the corners and ends of our line segments will look by setting both lineCap and lineJoin to a value of “round.” Once we have created the path, we can draw it on our canvas using stroke(). Finally, we’ll need to convert each segmentPosition from our grid scale to the canvas’s scale. Luckily, we thought ahead and made our cellPosToCanvasPos() function when setting up our gamespace last time! We’re so smart, I love us…

drawSlug: () => {
ctx.strokeStyle = this.color;
ctx.lineWidth = scale * 0.8;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.beginPath();
this.segmentPositions.forEach((segmentPosition, index) => {
const position = cellPosToCanvasPos(segmentPosition);
if (index === 0) {
ctx.moveTo(position[0], position[1]);
} else {
ctx.lineTo(position[0], position[1]);
}
});
ctx.stroke();
},

If we’ve done everything correctly, our gamespace should look something like this:

If we add segments to our segmentPositions array, as long as they're adjacent to the last position and not already in our array, we should be able to render a slug of nearly any length.

Let's make a utility function on our slug class. We’ll call this one findNextPosition() and it will use our slug’s direction to find what position the first segment is going to be in the next tick. It will be used to check if we’re going to collide with anything and to move. We can use a switch statement to make this happen fairly easily. If we pass no argument into the function, this will default to using our slug’s current direction. For each case of our direction, we return whichever coordinates the first segment would be in if it moves.

findNextPosition: (direction = this.direction) => {
const firstSegment = this.segmentPositions[0];
let newSegment = [];
switch (direction) {
case "north":
newSegment[0] = firstSegment[0];
newSegment[1] = firstSegment[1] - 1;
break;
case "west":
newSegment[0] = firstSegment[0] - 1;
newSegment[1] = firstSegment[1];
break;
case "south":
newSegment[0] = firstSegment[0];
newSegment[1] = firstSegment[1] + 1;
break;
case "east":
newSegment[0] = firstSegment[0] + 1;
newSegment[1] = firstSegment[1];
break;
}
return newSegment;
}

Let's make another useful function. When our slug receives a direction input of “north,” “south,” “east,” or “west,” we are going to need to make sure it is 1a valid input before setting our slug’s direction. We can check the validity of an input by finding the next position given the potential input and checking if that position is already included in our segments. This will prevent us from trying to move backward or turning directly into our body on input. Because we are comparing the values in nested arrays, we’ll want to write our own comparison function and use some() to check. Finally, if the potential next position is valid, we can set our direction to the input direction.

handleMovementInput: (direction) => {
const nextPosition = this.findNextPosition(direction);
const canMoveThere = !this.segmentPositions.some(
position =>
position[0] == nextPosition[0] && position[1] ==
nextPosition[1]
);
if (canMoveThere) {this.direction = direction}
}

In the last part, we thought ahead and set up most of the work to allow for user input! We added keydown event listeners to our arrows and WSAD keys, all that needs to be done is to replace our console.log()s with code that changes our slug’s direction attribute. Our switch statement should now look something like this:

switch (key) {
//...Our input cases for "p" and " " is here...
case "w":
case "arrowup":
slug.handleMovementInput("north"); // This line is changed
break;
case "s":
case "arrowdown":
slug.handleMovementInput("south"); // This line is changed
break;
case "a":
case "arrowleft":
slug.handleMovementInput("west"); // This line is changed
break;
case "d":
case "arrowright":
slug.handleMovementInput("east"); // This line is changed
break;
}

This is the fun part. Most of the setup we’ve done up to this point has been in preparation for this step: actually moving our slug. Because we’ve laid out our foundation, this is actually very simple. We are going to make a moveSlug() function that removes the last element of our segment positions and adds our next position to the front. We can get that next position with findNextPosition(). We will then pop slug.moveSlug() into our slug’s update method before we draw it.

moveSlug: () => {
this.segmentPositions.pop();
this.segmentPositions.unshift(this.findNextPosition());
}
//...Some of our codeupdate: () => { //This is update in our slug factory function
this.moveSlug(); //<< We add this
this.drawSlug();
}

What are you waiting for? Go test it out! Make sure you use your “p” key to toggle pause. You can also play with pressing “space” while paused to slow everything down and progress frame by frame.

The last thing on the agenda for the day is handling collisions. If our slug collides with the edge of the gamespace or itself, it’ll be game over. We can also use this function later to check collisions with other game objects. Lets call it checkCollision(). For now, if it collides with itself or the edge, we’ll just alert “Game Over.” We can add some additional game over features later on. To make our if statement more readable, I like to store the conditional checks we make as boolean variables. One will check if the next position will be included in our existing segments and the other will check if it will move beyond the edge of our board. Lastly, we’ll call checkCollision() before moveSlug() in the slug’s update function.

checkCollision: () => {
const nextPosition = this.findNextPosition();
const nextSegmentPositions = [...this.segmentPositions];
nextSegmentPositions.pop(); const willCollideWithSelf = nextSegmentPositions.some(
position =>
position[0] == nextPosition[0] && position[1] == nextPosition[1]
);
const willCollideWithEdge =
nextPosition[0] < 0 ||
nextPosition[0] > numOfCells ||
nextPosition[1] < 0 ||
nextPosition[1] > numOfCells;\
if (willCollideWithSelf || willCollideWithEdge) {
isPaused = true;
alert("Game Over")
}
}
//...Some of our codeupdate: () => { //This is the update function on the slug class
this.checkCollision(); //<< We add this
this
.moveSlug();
this.drawSlug();
}

Ther we have it. If everything is working correctly now, we can zoom around with our little slug all over the arena. You can play with changing the size of the canvas or the number of cells and everything will scale correctly. We even can play with the slug’s speed by changing our fps. Next time, we are going to mess around with adding another game object, handling all of its behavior, and tying up a couple of loose ends here and there. If you’ve followed along with me, we are very close to having something that truly resembles a game!