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.

Welcome to the Function Factory

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.

Body Building

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);
}

Looking Slick

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.

A Glimpse Into the Future

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;
}

What Do I Do With This

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}
}

Pull the Lever

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;
}

Making Moves

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.

Hitting Our Heads Against Stuff

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!