Building a Browser Game Part 3: Setting Up Your Gamespace

So far, we’ve touched on how to draw on an HTML canvas and how to create a simple animation loop with requestAnimaitonFrame(). Paired with our foundational JavaScript knowledge, we are now in a prime spot to start building our game! In this installment, we’ll be setting up everything that we’ll need to start dropping game objects into our game. Now that we’ve covered the very basic rendering tools, we’ll be getting a bit more specific and actually design our game. I’m basing mine off a classic game which made a resurgence in the late 90s thanks to NOKIA. I’m calling it Slugger. Here is our broad checklist for today:

1: Build and draw our gamespace. We will want this to be responsive and easily adjusted using different variables.

2: Build our animation loop and update functions. We’re going to add some FPS and pausing controls so debugging and balancing will be easier moving forward.

3: Add some Event Listeners for user inputs and utility functions.

In this section, we’ll set up our game arena; the place where all of our game objects will spawn and interact. This will build on what we talked about in Part 1 so feel free to glance back at that for a more thorough explanation of some of what we’ll be using. If you’re following along from the last two parts, we’ll remove almost all the code in or script.js file except the canvas and context declarations.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

Before we write any more code, let's think about the deliverables we are looking for with our particular game space. We will want a square canvas with a variable number of tiles or cells within it (think like a chessboard). We will also want an easy way to see the individual cells when the background is drawn. We’ll get started by defining some variables and empty functions.

const sideLength = 600;
const numOfCells = 16;
const scale = sideLength / numOfCells:
const backgroundColor = "#c3faa2";
function resizeCanvas() {
console.log("resizeCanvas() called"
};
function drawBackground() {
console.log("resizeCanvas() called"
};
function cellPosToCanvasPos(positionArray) {
console.log("resizeCanvas() called"
};

We’ll look at those variables first. Because we are making a square gamespace, we will be eventually setting out canvas height and width to this sideLength variable. We will also need to generate the cell grid so we have to know how many cells per side we’ll need. Think of it as the number of rows and columns. We’ll set the scale as the sideLength divided by numOfCells. This will be the size in pixels of each of our cells. Finally, pick a base color for our background.

I often like to think through the functions I’ll be needing and write them down. It's fine to leave them empty or pop a conosle.log(“[name of function] called”) so I have a road map of what I still need to build and how everything will talk to each other. Before we start writing our functions, we’ll also add an init() function which we will run when the DOM has been loaded.

function init() {
resizeCanvas();
drawBackground();
// Any other functions we'll need before the game starts.
// We'll also start our animation loop here.
};
window.addEventListener("load", init);

We add an Event Listener on “load” so that our init() function will be called once our page has been loaded up. I know that one of the first things that will need to happen will be to resize the canvas and then to draw the background so I start out by throwing those two functions right a the beginning. If this init() function becomes more complicated as we go, it will be easy to debug because we will be able to adjust the order or comment out functions for testing. Test as you go!

Time to write these functions. If you’re coding along with me, I highly recommend thinking about how you’d build these functions before looking at how I’ve done it. More than likely you might come up with something more efficient. There are tons of ways to skin this cat.

The first function we write will resize the canvas. Starting out with a softball.

function resizeCanvas() {
canvas.height = sideLength;
canvas.width = sideLength;
};

It is important to change the height and width properties on the canvas object and not on the canvas.style DOM style attribute. Setting the dimensions on the canvas object itself will change the number of pixels on the canvas (to 600 in this case). Changing the width and height on the style CSS object (canvas.style) will just stretch the original canvas size to fit the new dimensions rather than changing the pixel grid itself. This is also why we aren’t setting the size in the CSS file.

A green 16 by 16 grid with alternating lighter shades.
A green 16 by 16 grid with alternating lighter shades.
This is what we’re trying to make.

Next, let's look at how we’ll draw the background. Our goal will be to render the image to the left using our numOfCells, scale, and backgroundColor variables. We can alternate cells with an additional opaque white layer to get the lighter green effects. To start, let's set the color and fill the entire background.

function drawBackground() {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, sideLength, sideLength);
}

We’ll use nested for loops to integrate through the rows and columns of the gamespace. To make the pattern more tileable and repeatable, I decided to draw them in 2 by 2 cell chunks which we will draw over the initial background layer. We can set the opaque white color rgba(255, 255, 255, 0.2). Because our first layer is drawn and we’ll only be adding the opaque layer, we can set the fillSyle outside the for loops. Finally, we can find the starting corners of each tile for fillRect() by multiplying the index of the for loop with the scale variable.

function drawBackground() {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, sideLength, sideLength);
ctx.fillStyle = "rgba(255,255,255, 0.2)";
for (let i = 0; i < numOfCells; i += 2) {
for (let j = 0; j < numOfCells; j += 2) {
ctx.fillRect(i * scale, j * scale, scale, scale);
ctx.fillRect((i + 1) * scale, (j + 1) * scale, scale, scale);
}
}
}

The last function here is going to be a utility function that will help us understand where game objects are in our grid. Our game board is currently 16 cells wide and tall so we could write out a position on the board as [2,5] and would know we’re talking about the second column and the fifth row. But our canvas is working on a 600px by 600px grid. When we want to draw to the grid, we’ll need it to be on that scale. How can we convert our manageable 16 by 16 grid to the 600px by 600px coordinates and get the center of each cell?

function cellPosToCanvasPos(positionArray) {
const x = Math.floor(positionArray[0] * scale + scale * 0.5);
const y = Math.floor(positionArray[1] * scale + scale * 0.5);
return [x, y];
}

This way we can set the locations of our game objects by cell and this function will find the [x,y] coordinates of the center of that cell on our canvas. For example, [3,6] would become [131, 243].

There are a few nice things about how we’ve set up this canvas. We could easily write a function to change the game size to change based on screen width. And we can play with the number of cells to find out what will make the most fun or challenging game. We can set the background to any color or even an image and will still be able to see the individual cells.

This time we’ll be building a loop similar to what we did in Part 2, just with a few more bells and whistles. In the last part, we had a recursive requestAnimationFrame() loop which once started would keep going and would call itself as frequently as possible. This time, we want to be able to just the frames per second and pause the game for debugging. Just like when we started drawing the gamespace, let's start with some useful variables and rough out our functions. Note how we instantiate but do not assign a handful of variables for use in a moment.

let isPaused = false;
const fps = 5;
const fpsInterval = 1000 / fps;
let now, then, delta;
function startGame() {
console.log("startGame() called");
}
function update() {
console.log("update() called");
}

In our update function, our goal is to only have the render happen after our fpsInterval. This way we can control the speed of the game. We’ll do this by calling the update function in requestAnimationFrame() every game tick, but only render() after the fpsInterval has passed. We get the change in time between renders (delta) by finding the difference between our current time at render (now) and the time of our last render (then), and then comparing delta to our desired fpsInterval. If the change in time is equal or greater than our desired fpsInterval, we can render the next frame. We are also going to only want to call our update() functation again if the game is not paused. window.performance.now() is a good way of getting the accurate current time.

function update() {
now = window.performance.now();
delta = now - then;
if (delta >= fpsInterval) {
then = now - (delta % fpsInterval);
drawBackground();
//Move, Draw, Update Game Ojbect etc
}
!isPaused && requestAnimationFrame(update);
}

And now we’ll build our startGame() function to set “then” for the first time as well as call update() for the first time. Later on, if we have anything else that we want to happen right at the beginning of the game, we can throw that in here. We’ll also pop startGame() at the end of init() so the game starts up right away.

function startGame(fps) {
then = window.performance.now();
requestAnimationFrame(update());
}
//...some of our code...//function init() {
resizeCanvas();
drawBackground();
startGame(); //<--Newly added
}

Try throwing in some console.log()s into various parts of these functions and test how they’re working.

We’ve done some good work so far. The last step for today will be to add in some event listeners for all of our controls. We’re going to set our controls up to work with WSAD, arrow keys, and with some debugging tools in mind. When we press “p”, we’ll toggle pause, and while paused, we can press “space” to render the next update() without resuming.

Add an event listener with a type of “keydown” and an anonymous arrow function that passes in the event. Inside, we’ll then find whichever key was pressed and save it to a variable with event.key. We can then build a switch statement to handle each different key pressed. Notice how we make sure that the value of event.key is lower case. That way, if the user has caps lock on, the controls will still work. Starting out with the “p” and “space” controls:

document.addEventListener("keydown", event => {
const key = event.key.toLocaleLowerCase();
switch (key) {
case " ":
isPaused && requestAnimationFrame(update);
break;
case "p":
isPaused = !isPaused;
!isPaused && requestAnimationFrame(update);
console.log("Game Paused: " + isPaused);
break;
//Will add directon coltrols here
}
});

When we press “p”, isPaused will toggle, if we have unpaused the game we call update(), and we do a quick console.log() to let us know what is going on. When “space” is pressed, if the game is paused, we call update(). Pretty straightforward! For our directional inputs, let's just log the direction that we press for now. In the next section, we’ll write a function to handle the input, but we want to see that everything is working this time.

document.addEventListener("keydown", event => {
const key = event.key.toLocaleLowerCase();
switch (key) {
case " ":
isPaused && requestAnimationFrame(update);
break;
case "p":
isPaused = !isPaused;
!isPaused && requestAnimationFrame(update);
console.log("Game Paused: " + isPaused);
break;
case "w":
case "arrowup":
console.log("north");
break;
case "s":
case "arrowdown":
console.log("south");
break;
case "a":
case "arrowleft":
console.log("west");
break;
case "d":
case "arrowright":
console.log("east");
break;
}
});

As always, open up your dev console and give everything a whirl to see if it is working as expected. Using this setup, it is easy for us to add key inputs or change the functionality all in one place.

We tackled a decent amount in this one. We tied together the basics from the last two parts to more or less build a game engine. We have a gamespace suited for our game, a flexible update and render system, and support for different inputs. From here, we really have a foundation to build a variety of 2d games. In the next part, we’ll add our player game object and functionality.