Complete Guide to Building Games in the Browser
Deon Rich
Posted on December 9, 2021
Back in the early days when I first started out in web development, I eventually stumbled across HTML Canvas, and I was immediately hooked. This discovery would prompt me to create many projects, from things like art programs to photo applications, and eventually even games.
Often I've been revisiting HTML Canvas, and combining it with some of my newer skills like Node.js. Using the two, I've been able to create full blown online games, and honestly have impressed myself with how much I've improved since I first came across the technology, becoming a kind of "canvas connoisseur" if I do say so myself! 😌
I thought it was about time I shared some of the knowledge I've gained over the years, and figured this was a good way to do it. So, today Im going to share with you my process and methods for creating games from scratch using HTML Canvas, showing you everything you need to know to get started building and designing your own!
Since this post will be a little lengthy, heres an overview of all of the topics I'll be covering:
- What is HTML Canvas?
- Sizing the Game Area
- Creating a Rendering Pipeline
- Building Sprites
- Movement and Controls
- Collision Detection
- Events
- Putting it All Together
Just as a heads up, Im going to assume you have somewhat strong knowledge of Javascript and the DOM as I go through this guide, so I wont be explaining any syntax or basic concepts, only concepts related to HTML Canvas. Throughout this post I'll be explaining the key concepts used in my personal process, and then lastly in the final chapter I'll show a simple game I've created to showcase those concepts. But with that out of the way, lets get into it! 😎
What is HTML Canvas?
The HTML <canvas>
element, or Canvas API as it's also called, is a special HTML Element used for creating graphics, similar to its counterpart SVG which is also used for creating graphics on the web. Unlike SVG however, HTML Canvas is built for speed and the rendering of graphics programmatically on the fly.
The API consists of the <canvas>
element, which is used as a container for our graphics, and contains a whole plethora of properties and methods used for drawing things like shapes, images, text, 3d models and more onto it, as well as applying transformation on said elements.
Because its so simple, fast and versatile its applied in a wide range of web applications like games (2D and 3D), video chat, animations, art programs and everything in between. But before we start applying it for our game, let's get into how it works.
Getting Started
To get started with HTML Canvas, we'll need to first add it into our HTML. The <canvas>
element has no children, but text can be added between its tags to serve as the text to be shown to a user in the case that their browser dosen't support Canvas.
<canvas>Sorry, your browser dosen't support HTML Canvas!</canvas>
By default, the dimensions of the <canvas>
element are 300x150(px), but it can be resized in CSS. Note that this may alter the aspect ratio of the canvas's content, and it may be stretched as a result of resizing, but I'll get more into that later.
To breifly give you an introduction to the use of HTML Canvas (before we jump into the good stuff), I'll quickly go over each of the most important methods and properties you need to know to get started. Before we can render anything however, we first need to get a reference to the canvas's context using the canvasElement.getContext()
method.
// get a reference to our canvas's context
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
The canvasElement.getContext()
method takes 2 parameters. One is the context type, and another is the context attributes, however context attributes are irrelevant in our case, and can be ommited. The main two values for the context type are "2d"
and "webgl"
.
Passing it a "2d"
context specifies that we want a 2D rendering context, and canvasElement.getContext()
will return a CanvasRenderingContext2D
object, containing properties and methods for rendering 2D graphics onto our canvas. Passing "webgl"
, will return a WebGLRenderingContext
object, which contains properties and methods for rendering 3D graphics.
WebGL as you may have heard of before, is a Javascript API for rendering 3D graphics, and is a very popular choice for creating 3D games on the web. The API however is very complex, which is why people usually opt to use libraries like Three.js to interact with it.
In this guide, we'll be using a 2D rendering context which is much simpler, but if you're interested in either Three.js or WebGL, you can check out some of the resources linked at the end of this article.
Anyway, let's take a look at some properties and rendering methods..👇
Drawing Graphics
Quickly, lets go over the main rendering methods and properties we'll be using to create our game. If any of these properties or methods dont seem clear, they'll become clearer as we apply them later in this article:
beginPath()
: Starts a new "path", which means ties with previous renderings are cut. The path is all of the drawing commands that have been called up untill the current point. If you were to stroke a rectangle usingrect()
andstroke()
, and then fill a rectangle usingrect()
andfill()
, the stroked rectangle would get filled as well because both rectangles are part of the same path. Thus whenfill()
is called both rectangles are filled. This method prevents this from ocurring by starting a new path. Later you'll see this in action.stroke()
: Used to stroke (or line) the current path. Lines and shapes wont be stroked by default, so this should always be called explicitly.fill()
: Used to fill the current path. Lines and shapes wont be filled by default, so this should be always called explicitly.moveTo(x,y)
: Moves the pen (the current coordinates from which to start drawing from) tox
andy
coordinates.lineTo(x,y)
: Draws a line form the current pen coordinates tox
andy
coordinates. Line wont show unlessstroke()
is used after.rect(x,y,width,height)
: Draws a rectangle whose top left corner is located atx
andy
, and whose dimensions arewidth
andheight
. Wont show unlessstroke()
orfill()
are explicitly called after.strokeRect(x,y,width,height)
: Same asrect()
but strokes (lines) the rectangle in the same function call (no need to callstroke()
afterwards).fillRect(x,y,width,height)
: Same asstrokeRect()
, but fills the rectangle in the same function call instead of stroking it.clearRect(x,y,width,height)
: Same asfillRect()
, but fill clear out (erase) the area of space specified. This is often used to clear the canvas for the next frame, as you'll see later.drawImage(image,x,y,width,height)
: Renders a given image (image
) onto the canvas located at x and y width the givenwidth
andheight
. Usually anHTMLImageElement
created through theImage()
constructor is used as theimage
value.fillText(text,x,y)
: Creates text specified bytext
andx
andy
. Settings such as font and text alignment can be set using additional properties, which i wont go over here.arc(x,y,radius,startAngle,endAngle,direction)
: Draws an arc centered atx
andy
coordinates, which has a radius ofradius
, starts at the anglestartAngle
and ends at the angleendAngle
(both given in radians).To create a circle, setstartAngle
to 0 andendAngle
to2*Math.PI
.direction
specifies weather the arc is drawn counter clockwise, the default being clockwise.strokeStyle
: This property sets the color which will be used in strokes (lines). Can be any valid CSS color value. The new stroke color will be applied to everything drawn after its been set.fillStyle
: Sets the fill color. The new fill color will be applied to everything drawn after its been set.globalAlpha
: Sets the opacity. The new opacity will be applied to everything drawn after its been set.
These are the principle methods and properties that we'll be using to render our game onto the canvas, and draw each element from backgrounds, to characters and even on-screen controls for mobile users.
To keep this section as short as possible, i've only gone over the essencials. But, theres a ton of additional useful properties and methods you can use to acheive all kinds of stuff (some of which we will see later on). You can explore them here if you're interested.
Don't worry about it if it's hard to visualize any of these, as you'll see them in action further along. Any tips or other relevant information will be explained as this guide progresses. But now that we understand what we're using to create our game and a little about how to use it, let's look at the first step in implementing it.
Sizing the Game Area
The first step we need to take before drawing any game elements onto our canvas is to determine what kind of sizing we'll use, and how we want the game area to behave on different screen sizes. Before I show the methods I have for doing so, it's important we go over the width
and height
attributes of the canvas.
As before mentioned, the default dimensions of the canvas are 300x150, but this also serves as the default dimensions of the canvas's content. Using the width
and height
attributes we can change these inner content dimensions, and control how many units the canvas uses for rendering on it's x
and y
axis. Below is a further example and explination of how this works:
The canvas dimensions and it's content dimensions can also be set in one go, using it's width
and height
attributes in HTML:
<!-- canvas itself and its content is 300x250 -->
<canvas width="300" height="250">Sorry, your browser dosen't support HTML Canvas!</canvas>
What option you choose for sizing will determine how or weather or not you'll use these two attributes. But now that this concept is understood, let me show you what I think are the three best and most common methods for sizing your game.
Applying a Static Fit
A "Static Fit" (for the lack of a better term) is when you apply permanent default dimensions to your canvas, and then simply position it somewhere on your webpage. Generally, this is used for larger screens and desktops where you want other content to be visible without maximizing the game area, or you want to maintain the aspect ratio of the game area and dont care about maximizing it.
For some games, aspect ratio is important because if it's altered, the content of the game could squish or stretch. A good example of a static fit is the online game Tank Trouble (one i used to play quite a bit 😊). You can see they've simply positioned their game area onto the page, maintain it's aspect ratio, and keep it minimized as to keep the rest of the webpage content visible.
When applying this type of fit, you'll want to set the default content dimensions of the canvas once and never change them. They should have the same aspect ratio as the physical canvas dimensions in CSS, and whenever you want to scale the canvas, always keep it's aspect ratio and inner dimensions the same.
Applying a Theator Fit
The "theator fit" or "theator mode" method, is one of the most common ways used to maximize the size of content which must maintain its aspect ratio. This is when you stretch the height of the content to the full height of the device, and keep the width proporcionate, optionally filling in any gaps with black (or vice-versa, based on the client device dimenisons).
This method is usually applied when the game area has to maintain a certain aspect ratio and you want to maximize it as much as possible without cutting off any of the canvas. I could'nt find a good example in a game, but the Youtube video player serves as a good example. As seen when you try to fullscreen the video, black bars may cover the top or bottom of the video player in order to cover gaps not filled my the video itself.
Below, I show an example and full explination on how you can apply this:
Notice how the canvas adapts to the viewport changing, that way our game content wont end up stretching or being altered if a user resizes, and always provides the most optimum presentation possible. Similar to a static fit, you should set the content dimensions once and never change them, only changing the canvas's CSS dimensions to scale it, and maintain aspect ratio. It would also be smart to encourage your users to flip their device (if possible) to get as close to the aspect ratio of the game area as possible.
Applying a Fullscreen Fit
A fullscreen fit can be simple, or a tad more complicated. It's when you stretch the canvas dimension's (and it's content dimensions) to the exact dimensions of the user's device, as to leave no gaps. When using a fullscreen and theator fit, I would recommend fullscreening the canvas on touchscreen devices via the HTMLElement.requestFullscreen()
method to lock it in place, because the browser may scroll and move as the player interacts with the screen to play the game.
A fullscreen fit should usually only be applied when the dimensions of your game area dont matter, and or the full game area within the canvas dosen't have to be visible. A good example of this is the online game Narwhale.io, where the character is centered and the map moves into view naturally.
A small pitfall if this is that the sizing of your game may vary slightly in terms of how much of the map certain clients will see. If your game is drawn on a phone with smaller dimensions than a tablet, the content is drawn using the same amount of units for each rendering (meaning a 2x2 square on the phone is also 2x2 on the tablet), except the tablet uses more units since the canvas's content dimensions will adapt to its larger screen size. This means that users on larger screens will end up seeing significantly more of the map than users with smaller screens.
Depending on your game, this may not matter to you, but if it's somthing you care about, I have a solution. I found to get around this was not to use pixels (the default units) as units when sizing renderings, but instead inventing my own "viewport unit" based on the dimensions of the device. Think of it as using "%" units instead of "px" units in CSS.
When applying this in an online game I had started a while back which used a fullscreen fit, it proved very effective at maintaining consistent proporcionality between devices. Below, you can see two screenshots I took, comparing the the size of the game area relative to the screen of my phone, and my tablet.
Phone game area (720x1520):
Tablet game area (1920x1200):
Notice how the elements of the game dont seem smaller on the tablet screen, and the distance between the benches from the edge of the screen is almost the same. Of course, some devices will inevadablely see slightly more or less of the map than others, because of slight differences in aspect ratio, but it's nothing to worry about.
In short, if you use pixel units (the default units) with a fullscreen fit you'll probably see large changes in the sizing of the map between devices (which isn't a problem, if you dont care), but if you use "viewport" units, the sizing of your game will stay consistent. Here I show an example and explination of how to apply these percentage units if you're interested.
On another note, if you're on a touch screen device and you're interested in checking out the unfinished multiplayer game where I took the screenshots from, you can check that out here. You can also dig through the code if you'd like to see how I apply some of the techniques I talk about here.
But with all that out of the way, lets finally start building our game, shall we? 👇
Creating a Rendering Pipeline
Before creating any characters, objects or backgrounds, we first need to define an order and structure through which each of these entities will be rendered and managed. Since we're building our game from scratch and HTML Canvas provides no kind of framework, we'll have to define ourselves a structure. I call this my rendering pipeline.
Generally it'll look like this:
// get canvas 2D context object
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
// object for storing globally accessable states
const GLOBALS = {}
// Array where all props will be stored
const PROPS = [];
// Array where all characters will be stored
const CHARS = [];
// function for applying any initial settings
function init() {
}
// function for rendering background elements
function renderBackground() {
}
// function for rendering prop objects in PROPS
function renderProps() {
}
// function for rendering character objects in CHARS
function renderCharacters() {
}
// function for rendering onscreen controls
function renderControls() {
}
// main function to be run for rendering frames
function startFrames() {
// erase entire canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
// render each type of entity in order, relative to layers
renderBackground();
renderProps();
renderCharacters();
renderControls();
// rerun function (call next frame)
window.requestAnimationFrame(startFrames);
}
init(); // initialize game settings
startFrames(); // start running frames
This provides us with an orderly process we can use to draw all of the elements of our game. The process goes as follows:
We create a container for any states that we want to be accessable globally. For example, if we wanted to create a "pet" that follows our character around, we could store the character's coordinates in our global container, for the pet object to access and use as a relative point to follow.
Two arrays are defined, one which will store all objects for props (objects that move or can be interacted with by the user), and another that will store all objects for characters (objects controlled by the user). Objects for props and characters will be created using classes, as we'll see later.
An initialization function is defined, which will set any initial states, like setting the canvas's dimensions, colors, fonts, etc. This is typically where you'd put the logic for your initial canvas sizing, like discussed in the previous section, or perhaps register an event listener for adjusting the game area on resize (depending on your prefered sizing method).
A function for rendering the backdrop is defined. Here we can either render an image, or separate background elements (more on that later).
We create a function which will render each prop in the
PROPS
array.We create a function which will render each character in the
CHARS
array.We create a function which will render onscreen controls (for mobile devices) line buttons and joysticks, as well as any other displays like showing the number of lives.
The main function is created. This function when called will begin running frames. It starts by erasing the canvas using
clearRect()
, then calls the render function for each type of entity. If somthing is drawn onto the canvas, and then we draw somthing directly ontop of it, the first rendering will be covered, meaning we'll have to think in layers. Thus, the render function for each type of entity is called in a logical order (background-> objects-> characters-> controls), with the background being on the bottom layer, and the onscreen controls being on the top. The last thing this function dose is run a special methodrequestAnimationFrame
. This method takes in a function, which it will run as soon as the browser is ready to render new content, so we pass itstartFrames
. We call it in a loop, so that our frames run indefinitly (note thatrequestAnimationFrame
isn't blocking, like if you were to run afor
loop indefinitely, making the page unresponsive).We call
init
andstartFrames
, to initialize our game, and begin running frames.
Of course, you can personalize this process however you please if you're actively following along, as this is just my personal process and none of this is set in stone (though I'd recommed still using requestAnimationFrame
).
The purpose of this section was to explain that you should have some kind of structure for rendering your game, and managing states, animations and user interactions. This is the most important part, as it'll prevent our game from becoming a confusing, clunky mess in the long run.
Hopefully by now the fundamentals have more or less stuck, but now that the scafolding for our game is all set up, we can finally start filling our rendering pipeline with actual backgrounds, objects and characters (etc) to render!
Building Sprites
A "sprite" refers to any rendered object or character that can be moved around, interected with, or hold some type of state or animation. Some can be represented as objects, and others with functions, both of which should typically be stored in a separate file, to keep the file where your rendering pipeline lives clean. Usually I split these into three categories:
- Background Sprites
- Object Sprites
- Character Sprites
As implied, they function as a tangible rendered elements of the game, each of which serves a different purpose. Below, I explain exactly the application of these, and how to create each of them.
Background Sprites
When creating a background for your game (as we'll dive deeper into later) there are generally two options. The background can be rendered as a single pre-created image, or, it can be rendered as a collection of "background sprites". This can be multiple renderings, or a collection of multiple images.
Because background elements cant be interected with and hold no state, usually these are created as functions which do nothing but render the sprite, rather than objects with render methods and properties as used in character and object sprites (more on that later). These functions will take in an x
and y
coordinate as parameters (optionally any other parameters regarding display as well), and will simply be called inside the renderBackground()
function of our rendering pipeline.
Heres an example of a simple flower pot sprite ive created:
Here I draw each flower from scratch using rendering methods. If you do this its important that you draw each peice of the sprite relative to the x
and y
parameters that are passed into the function. I wouldn't recommend doing this however, as its tedious and using a prepared image generally will look alot better. I'm just using rendering methods to examplify their use.
Again, you can just as easily skip all of the building of the flower yourself, and simply use the drawImage(x,y)
method to draw a pre-build image or PNG (one that hopefully looks better than my flowers) onto the canvas for a single background sprite, or even skip everything all together and render a single image for the entire background in the renderBackground()
function.
Object Sprites
Object sprites are like background sprites, except they usually hold some kind of state (for user interactions), movement or have an animation, and may have access to certain global states in GLOBALS
, like a ceiling fan, or a door that opens and closes when the user touches it. Usually these are made with classes and objects, which are stored in the PROPS
array, which is later iterated through to render each object in it, inside the renderProps()
function.
Classes for object sprites will always carry a render()
method containing the commands to render the sprite, which will be accessed and called in renderProps
. Of course you dont have to call it that, but you should have some kind of standard method for rendering each prop. Inside the render()
method, state can be used to influence how the sprite is displayed. Similar to background sprites, these can also accept an x
and y
parameter, and any other additional parameters regarding interaction or display. Below I create animated spikes as an example of a simple object sprite:
The movement works because we're constantly changing the coordinates (our state) where the rendering is drawn, and because frames are running indefinitely, any changes we apply to the rendering will be immediately reflected since the sprite is being redrawn with new state repeatedly, which means it's up to us to control timing in animations. Again, my spikes are fairly ugly, and I'm just using render methods to demonstrate their use and prove the concept. You can make your sprites as pretty as you'd like.
This is a good example of the types of elements you should render as object sprites, such as obstacles or things a character can interact with or be affected by. As shown, typically you should make these as a class in which you'll specify its default functionality and give it a render
method, then simply envoke it whenever you need to place a new object sprite of the same type, keeping our code nice and DRY.
Character Sprites
Character sprites function essencially the same as object sprites, accept they usually have state thats controlled by outside factors like keyboard controls or a joystick, and are rendered on a higher layer than objects. Character sprites are created as objects from a standard class which have state and a render
method, and are added to the CHARS
array which will be iterated through in renderCharacters()
to call the render method of each existing character. This can include the player's character as well as other players, like in an online game.
Though they're similar, it's best to keep them separated, because usually you'll want your characters to be rendered on a higher layer than the objects and background.
In the next section I'll show how you can implement controls with a character sprite, and explain how to create different types of movements in your game like having the character look towards the mouse, making joysticks and using keyboard controls.
Movement and Controls
In this section I'll explain and demonstrate how to implement common controls and character movements that you'll typically apply in a 2D game, many of the methods I use to implement which can be used to create other types of controls and movements. Below I explain each one by one, and provide code and an example.
Implementing Joysticks
Joysticks are a common type of control used for mobile games and typically have two applications in regards to character actions: 1) Controlling a character's angle 2) Controlling a character's movement direction. Apart from display, a joystick's primary purpose is to keep track of each of these, so that we can apply them to whatever entity(s) it's controling. Typically it'll keep it's angle and x
and y
direction in GLOBALS
so that they're accessible to every sprite that needs it.
These properties will allow us to specify certain directions in which sprites will travel on the game area, and the angle at which they're facing. Usually these are applied as character controls, and dual-joysticks are used, one for the character's angle and one for the character's direction.
Below I show an example and full explination on how to create a joystick. I've made this example so that you can interact using a mouse or a touchscreen device. Try not to resize to much, as it could break the example:
As you can see iv'e created a class for creating joysticks just like I would've made an object sprite, then instanciate and render it inside renderControls()
. They technically aren't sprites however, because the joystick isn't an element of the game itself. I've rendered it simply as two circles, one serving as the thumbstick and one as the base (again, you can make these peices look however you want).
Within it's render
method I added logic to draw the thumbstick towards the user's finger, while keeping it inside of the base (the distance()
helper function, which measures the distance between two points, aids in this). Using one of the helper functions I've added to GLOBALS
(angle()
), the center coordinates of the base and the center coordinates of the thumbstick, Im also able to determine the angle between the two coordinates, and the x
and y
offset the thumbstick is from the base (the formula for which will be covered later), which is the information shown above the joystick. The formula used by angle()
and distance()
can be found commented in the above pen.
Note: The formula I've used to calculate the angle will output radians, because that's the unit used by the
rotate()
method of the canvas's context. This will be used later to rotate our character.
This information can be kept in GLOBALS
to later be accessed by our main character sprite, to control it's movements. Another important note to mention is how im giving the joystick class access to the mouse actions through GLOBALS
, and then implementing it's logic within it's own class instead of inside the event listeners for mouse/touch events. This keeps things much more orderly, rather than cramming the logic of each sprite who needs mouse events inside the same event listeners. This is why inside init()
I've registered mouse/touch event listeners which when fired just add that information to GLOBALS
to be globally accessible.
Joystick Movement
Below is an example of integrating a joystick with a character sprite to allow movement. Our character sprite is the red circle in the top left corner. Simply use the joystick to move it across the screen with your finger or mouse. Again, try not to resize the window:
Here our character sprite is accessing the charX
and charY
properties on GLOBALS
, which are being continuously updated by and based on our joystick's state. Again, this is better than directly updating our character sprite's coordinates inside of the joystick's class, because if we we're to add more sprites for the joystick to control, we would have to cram a ton of code inside it. Instead, any sprite can simply access the information via GLOBALS
and keep its implementation within it's own class.
The way this movement works is a bit different than implementing keyboard controls, because our character sprite is moving in very smooth angles rather than simply up or left. Here's how it works: First we declare charX
and charY
onto GLOBALS
, which serve as the horizontal and vertical amount of units a character will move over time.
Note: The way the joystick determines both of these values is by mesuring the
x
andy
offset of the thumbstick of the joystick from the center of it's base. These are determined by subtracting the base'sx
coordinate from the thumbstick'sx
coordinate to getcharX
, and then subtracting the base'sy
coordinate from the thumbstick'sy
coordinate to getcharY
. You might want to only use a fraction of the result of both of these calculations, like a 10%, because if the result of calculatingcharX
for exmaple is -56.09, your character will be moving alot of units to the left very quickly. You can fraction each of these however (as long as you fraction both the same). It's all about the ratio between the amount of units our character sprite is moving in one direction vs the other overtime, to apply movement at dynamic angles.
If a user had the joystick positioned at the bottom right (as far as it could go in each direction), this would mean our character would move diagonally downwards to the right. Why? Because since charX
and charY
are the same value (because the width and height of the joystick are equal and thus if the thumbstick is at the bottom right it has equal x
and y
offset) charX
and charY
would be set the same, and the character would move down and right at the same rate, causing our character to move diagonally downwards towards the right. Hopefully that made sense, but lucky for you implementing keyboard controls is 10x simpler.
Keyboard Controls
Unlike joystick controls keyboard movements are much simpler to implement. It's as simple as registering a keydown
and keyup
event inside init()
, and then keeping track of weather or not the keycodes you're listening for are pressed down or released. That information is kept inside GLOBALS
, where our character sprite has easy access, and depending on weather a key is currently being pressed, the x
and y
coordinates of the character sprite will be incremented or decremented by a certain amount (the character sprite's speed
property).
Below is an example of a character sprite (the red circle) which you can control using the arrow keys:
Simple right? As it's name implies, the speed
property of the character sprite controls how many units our character will move when it dose, allowing us to control how much ground it covers in each movement. The same can be done with joystick controls by either multiplying charX
and charY
to go faster, or dividing them to go slower (as long as they're multiplied or divided by the same factor).
Character Rotation
In the below example I've made a small rocketship character sprite using the drawImage()
method. Not only dose it move in the direction of the joystick, but also mimics the angle of the joystick. The joystick's angle is kept in GLOBALS.roation
, which our character sprite has easy access to for it to rotate that amount.
Below is an example and explination of how to apply rotation to a character sprite:
The key part here is the angle()
method on GLOBALS
, which takes in two sets of coordinates and returns the angle between them (in radians). The formula for which is atan2(y2 - y1, x2 - x1)
. We use angle()
to measure the angle between the center of the joystick's thumbstick, and the center of the joystick's base, and then put the result into GLOBALS.rotation
. This formula (and every other I cover in this post) has a ton of applications when building a game, and you can use it to control rotation with the mouse as well, by mesuring the angle between the mouse coordinates and the center of the screen.
The second primary part which allows this to work is the rotate()
method of the canvas's context, which is what's used to apply the rotation to the character sprite. The way this method works is by rotating the canvas from its top left corner by the specified amount of radians. The rotation only applies to everything drawn after the method is called. Of course this works alot differently than you would expect, but we can normalize it by doing the following:
We want to rotate the canvas by the center of the thing we want to rotate (our character sprite) rather then the top left corner. The way to do this is by using the
translate(x,y)
method to move the canvas's top left corner to the center coordinates of the thing we want to rotate, rotate our desired amount usingrotate(radians)
, undo the translation (translate(-x,-y)
), redraw our sprite with the applied canvas rotation, and then call theresetTransform()
method to reset the canvas transformations we just did, so that everything drawn after the thing we wanted to rotate dosen't become part of the rotation or translation. This process can be seen in the render method of our character sprite.
You can read more on the rotate()
and translate()
methods here.
Static vs Dynamic Movement
In 2D games typically there exists two major types of character movement:
Static Movement: This is when the map is fixed on the game area, and the character moves relative to the map. Like in Tank Trouble.
Dynamic Movement: This is when the character is fixed to the center of the screen, and the map moves relative to the character. Like in Narwhale.io.
So far the type of movement I've examplified has all been static movement, but in this section I'll give an example and explination on how to apply dynamic movement:
See how the "camera" follows our character as the map moves relative to it. The way we do this is by applying the joystick's x
and y
offset to coordinates which will serve as a map anchor, which our background sprites will be drawn relative to (and of course remove the charX
and charY
implementation from our main character sprite).
First what I do is store the joystick's x
and y
offset in GLOBALS.anchorX
and GLOBALS.anchorY
, and then declare GLOBALS.mapAnchor
to store the "map anchor" coordinates. Inside renderBackground()
before rendering each sprite, I update GLOBALS.mapAnchor.x
and GLOBALS.mapAnchor.y
by subtracting GLOBALS.anchorX
from GLOBALS.mapAnchor.x
, and subtracting GLOBALS.anchorY
from GLOBALS.mapAnchor.y
.
Here we subtract instead of add like we did in static movement, because the map has to flow the opposite direction from where the character is moving towards. Then finally, inside our Pot()
sprite, I add GLOBALS.mapAnchor.x
and GLOBALS.mapAnchor.y
to it's x
and y
coordinate, to draw the sprite relative to the current map anchor coordinates.
Collision Detection
Collision detection is typically an essencial when it comes to games, weather it's stopping a character from walking through a wall, or killing a character when a bullet hits them.
In this section I'll touch base on basic collision detection, and explain how to detect collision between squares, circles and rectangles as well as give examples.
Squares and Rectangles
Collision detection is all about using the dimensions and coordinates of two shapes, to mathematically determine weather or not both are in contact. Depending on the shapes themselves (square, circle, etc.), the way you'll determine this will vary.
Below I give an example and explination on how to apply collision detection with squares (same exact method can be used with rectangles of any size). Simply drag the pink square into the blue square, and the blue square will turn red when the pink square comes into contact in the following example:
The first thing I did was implement some logic for dragging the pink square. That isn't as relevant but if your interested in how I implemented it just take a look at the code. Next, onto the base class for our squares (which is the one the blue square instanciates) I attach an object inside the constructor called collisions
. On this object, we'll keep a sub object for every sprite we want to detect collision for, thus, I add pinkSquare
.
On pinksquare
(and every other collision object in collisions
) there exists a conditions
property which holds the boolean for mathematically determining contact with the pink square (which we're about to dig into), and then an inContact
property which specifies weather not the sprite is in contact, though here we aren't using this property. This is a good standard to use when implementing collision detection, since its simple and keeps things orderly.
Note:
collisions.pinkSquare.conditions
is a string rather than a boolean, so that we can revaluate it constantly witheval()
, and we dont have to hard code it intoif
statements.
Here is the pseudo-code for determining collision between two rectangles/squares, exactly the same structures as the boolean in collisions.pinkSquare.conditions
:
A = {x: ... y: ..., width: ..., height: ...}
B = {x: ..., y: ..., width: ..., height: ...}
if (A.x + A.width) > B.x
and A.x < (B.x + B.width)
and (A.y + A.height) > B.y
and A.y < (B.y + B.height)
...
Try visualizing this boolean in action as you read through it, and eventually you'll get the gist.
Circles
Determining contact between two circles (not ovals, we'll get to that) is very straight forward and simple to understand. Because Im lazy, just imagine the shapes in the above example I showed are now two circles. The pseudo code for collisions.pinkCircle.conditions
would be the following, to determine weather the two circles are in contact:
A = {x: ..., y: ..., radius: ...}
B = {x: ..., y: ..., radius: ...}
if
distance between A and B < (A.radius + B.radius)
...
Here, "distance between A and B" refers to the distance between the two points where each circle is centered. If you remember, you determine this with the handy-dandy formula: √(x2 − x1)^2 + (y2 − y1)^2
. Simple right?
These two examples are pretty simple, but collision detection with more complex and irregular shapes can require much more advanced methods of determining collisions. Of course here I've only gone over the basics, but if you do plan on implementing collision detection for complex shapes, I highly recommend you check out these resources:
- https://www.gamedeveloper.com/programming/advanced-collision-detection-techniques
- https://www.toptal.com/game/video-game-physics-part-ii-collision-detection-for-solid-objects
Events
Keeping track of certain events, such as when a door is opened or when the character is hit in a game is important. This allows us to globally listen for (and trigger) when certain things happen our game no matter what sprite we're in.
Below I've edited the previous example to use events to run two alert()
s every time the squares are in contact:
Basically the way I've implemented events is exactly how DOM events are implemented, by adding an addEventListener()
and dispatchEvent()
method to GLOBALS
. That way both methods are globally accessible.
GLOBALS
also includes an events
property, and each of its sub properties are the name of an existing event which points to an array where callbacks for that event will be stored. I simply call GLOBALS.addEventListener(name,callback)
to add a callback to the array for the specified event, and then call dispatchEvent(name)
to call each callback inside the array of the specified event name. And of course we can all new events by adding a new property to GLOBALS.events
. Easy-peasy! 🍋
Despite how simple it is, I just wanted to clarify that its important that you implement some kind of event system within your game, otherwise you'll be hardcoding alot of stuff, which isn't good practice.
Putting it All Together
Finally let me go down the list of each integral concept that makes this process possible. If any of what I explained previously seemed unclear, I recommend you try to examine where and how each concept is implemented within the code to gain a better understanding of how this all functions. Let us first again go down the list of each of the steps of the process:
- Choose a sizing for the game area (apply it in
init()
) - Create your rendering pipeline
- Fill your rendering pipeline with sprites (in their respective locations depending on the sprite)
- Apply movement and controls
- Apply collision detection
- Integrate events
Each of these concepts can be seen applied in this simple game I've created, named "Drift". The objective is to last as long as possible without being hit by a barrier or leaving the map, steering a car by tapping the left or right half of the game area or using arrow keys. I found it about as frustrating as playing Flappy Bird, my highest score being 33.
This game greatly examlifies the process and methods I've explained throughout, and hopefully with this guide and example to throw it all together, you'll be equiped with the knowledge to build your own games using HTML Canvas, and the techniques I've gone over in this post.
I hope you've managed to gain somthing from this article, and if you're still thirsty for more related to game development, definitely checkout the resources below.
Thanks for reading, and happy coding! 👍
Posted on December 9, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.