In order to learn the basics of HTML5 game development I created a simple maze game.
The source code is available here:
- Support all (modern) browsers and devices. – I see cross-platform compatibility as one of the major advantages of HTML5 games so I thought I would try and make my game work on as many devices as possible without creating any device-specific code.
The user interface consists of:
- A HTML5 canvas for rendering the maze.
- Two div tags that hold the interactive buttons. One div holds buttons that appear before the canvas and one holds the buttons that appear after it.
- Four IMG tags that hold a reference to SVG objects that draw the arrows that can be clicked on screen.
Supporting Multiple Resoltions:
When I was designing the interface I decided that the Canvas displaying the maze must always remain square while the buttons would change position to make good use of the remaining space.
When the browser window is in portrait mode all four arrow buttons appear below the canvas and when the window is landscape mode two arrow buttons appear on either side of the canvas.
Detecting Changes in Resolution:
In order to listen to these browser events we use the addEventListener function when the page is loaded:
window.addEventListener('resize', windowResized, false);
window.addEventListener('orientationchange', windowResized, false);
These function calls register ‘windowResized()’ as a callback function for the window resize events.
What this means is that when either of these events occur our windowResized function will be executed.
Resizing and Repositioning User Interface Elements:
The ‘windowResized’ funciton that repositions the GUI elements can be found in the layoutManager.js source file:
The file starts with a number of variables being declare to store a reference to each of the UI elements.
There is a boolean value named horizontal, which is true if the screen is wider horizontally than it is tall (landscape orientation) and false if the screen is taller than it is wide (portrait).
The orientation is determined like this:
var horizontal = (Math.min(winWidth,winHeight) == winHeight);
Now that we have determined the screen orientation, we can arrange the interface elements accordingly. I will explain how the code works for the horizontal orientation. The portrait mode is very similar and is explained through comments in the source code.
When the screen is aligned horizontally the canvas will appear in the middle of the screen with two arrow buttons on either side (See screenshot).
The first thing we do is determine what size the canvas should be. We want the canvas to be a square that fills as much space as possible. When the screen is in landscape mode the largest possible square that can fit on the screen will be the same height as the window. We will try and make the square as large as possible but we will include a check to ensure that enough space remains to display the arrow buttons.
newCanvasSize = Math.min(winHeight,(0.84*winWidth));
I decided that each half the UI should occupy at least 8% of the screen for a total of 16%. The canvas size is set to the smaller between the full height of the window and 84% of of the width. This means that if the window is too narrow to allow the full size of the canvas it will be small enough to accommodate the UI.
Next we set the size of our ‘control panels’. These are the DIVs that hold the buttons.
var cPanelWidth = ((winWidth-newCanvasSize)/2)+"px";
var cPanelHeight = "100%";
The width of the control panels is set to the remaining width of the window divided by 2 (one cPanel on either side). The height is set to 100% so that the panels will be as tall as possible.
Next we apply the calculated heights of the cPanels to the DIV tags’ style. This will manipulate how they are positioned on screen by altering their CSS properties:
controlsPanelBefore.style.width = cPanelWidth;
controlsPanelBefore.style.height = cPanelHeight;
controlsPanelAfter.style.width = cPanelWidth;
controlsPanelAfter.style.height = cPanelHeight;
When the screen is arranged horizontally we want two of the buttons to appear on the left side of the canvas and two to appear on the right side. In order to allow for this the HTML file that defines the user interface elements has two DIV tags before and after the Canvas with the IDs “controlsPanelBefore” and “controlsPanelAfter”.
Moving HTML elements from one DIV to another is simple. Just use appendChild to append the object to the destination DIV:
Now we apply the width and height styles to the buttons:
var buttonWidth = cPanelWidth;
var buttonHeight = "50%";
for(var i = 0; i<allButtons.length; i++)
allButtons[i].style.width = buttonWidth;
allButtons[i].style.height = buttonHeight;
The last thing that is specific to the landscape orientation is to move the left edge of the canvas to the right edge of the leftmost control panel. This is necessary because the positions are set to absolute in the CSS in order to allow more control over their positioning:
canvas.style.left = cPanelWidth;
Finally we resize the canvas. In order to resize the canvas we adjust canvas.style.size. Adjusting the size through the CSS like this allows us to scale and resize the canvas without changing the size that is used within the canvas for drawing. The logical size used for drawing within the canvas remains what we set it to in the HTML:
<canvas id="gameCanvas" width="800" height="800"></canvas>
Now we change the scaling of the canvas without affecting the drawing size like this:
canvas.style.height = newCanvasSize+"px";
canvas.style.width = newCanvasSize+"px";
In order to generate the maze I used a Depth First Search. A common way to implement this is through recursive backtracking. Since it is unknown how much stack space is available across all devices I decided to convert the algorithm to run iteratively and used a list as a stack for backtracking.
The code that is used to generate the maze can be found here:
More information on how to generate mazes can be found in this Wikipedia article:
The depth-first search approach I used is fast and simple but it tends not to produce very difficult mazes. See the screen shot here for an example of how long these corridors can be!
I decided to use a simple algorithm since the focus of the project was developing the interface.
Since I was worried that rendering the maze using the Canvas line drawing methods may be slow I decided to render the maze only once when it was generated. When the man moves the renderer draws the maze’s background colour over his old position and then redraws the man in his new position. This is a simple implementation of the Dirty Rectangle technique. It is made simple by the fact that there is only one object that can move in this game.
The code for drawing the lines of the maze to the canvas can be found here:
It is fairly straightforward but there was one small detail that was problematic. This is the code for rendering a north-south line:
When I was working on it it was very slow to render the maze. I had omitted the ctx.beginPath(); and ctx.closePath(); lines. I’m not sure of the particulars but forgetting these lines caused my lines to render very slowly!
The code for moving the man on screen and rendering him can be found here:https://github.com/goshdarngames/HTML5-Maze/blob/master/html5-maze/lib/gameplay/man.js
Handling Player Input
The player can input directions to the man through either on-screen buttons or keyboard arrows. I decided to use jQuery to handle the input because from what I’ve read it is a bit of a headache trying to support input from all browsers since there are some subtle differences in how the browsers dispatch events.
The code for handling input events is defined alongside the input object in the index HTML file.
The input handling is simple. When an input event is dispatched by the browser a ‘moveMan’ function is called with a single parameter, which is a string stating what direction we are to move the man.
The Game Loop:
The update and render cycle is driven by window.requestAnimationFrame(callback_function).
In HTML5 games the browser decides when it would like to update the display. We use the requestAnimationFrame function to register one of our functions to be executed when the browser wants to redraw the canvas.
We register the function in browserevents.js when the page is loaded:
Now the animationFrame function (defined in gameLoop.js) will be called when the browser wants to redraw the canvas:
manMoved = false;
The manMoved variable is a flag stating if the man has moved – it is used here so that the canvas is only redrawn if it has actually changed.
Finally we call window.requestAnimationFrame(animationFrame); again so that the function will be executed the next time the browser is ready to update the canvas. This will cause our game to loop as long as the browser wants to keep updating the canvas.
The game was running perfectly so I decided to share it on Reddit. Users started reporting all sorts of crazy bugs that hadn’t cropped up in my testing. Leave it to users to find the edge cases for you.
I decided not to fix the bugs in the code but instead detail what the problem was in this section. Only the first bug was fixed since I saw it as an opportunity to add a ‘bread crumb’ feature to the maze.
Outline of Player Remaining After Move
This bug may be evidence that I need my glasses prescription updated. When the player moves their old position was not completely cleared. This is puzzling to me as I used the same position and size to draw the rectangle with the background colour. To fix it I used a simple hack and made the clearing background colour rectangle 3 pixels larger than the man’s position. This might cause some problems on very small displays.
While I was there I decided to render a slightly smaller rectangle after clearing the background. This serves as the ‘breadcrumb trail’ that shows the player where they have already visited in the maze.
Fast Input Causes the Old Position to Remain:
Due to the way the player’s input is handled, fast input can cause the man to move twice before being rendered. When the code renders the man in his new position it only clears the previous position. When designing the renderer it was assumed that the man could only move once in each cycle.
The problem is that the function for moving the man is called directly by the input object. This means that if the user taps fast enough two input events can happen before the game renders.
There are two ways to fix this error:
- Change the renderer so that it clears the entire screen before rendering the updated scene. This ensures that anything left behind on the canvas from the previous game state is removed before drawing but rendering the scene will be slower.
- Add an event queue to store all inputs before updating the man’s position. This way we can process all the input events that have occurred since the last update at once.
One user found a new challenge in trying to completely fill the maze with these left-over squares. I like to think of it as ‘meta-game’ as that sounds more professional than ‘bug-riddled game’.
Double-Tapping Input Elements Causes Browser To Zoom:
The default behaviour of many mobile browsers is to zoom in when the user double clicks a HTML element.
This can be quite frustrating if the user is trying to move in a straight line quickly.
I’m not sure how to fix this at present but I will look into it for my next game!
Final Breadcrumb Persists Through Game Reset:
When the maze resets the final breadcrumb remains.
This is caused by the fact that I hacked in the breadcrumb feature 5 minutes after releasing the game.
It happens because the code that moves the man back to the starting position after he reaches the goal alters his position variables directly rather than using the ‘moveMan’ function.
Display Problems On Some Devices:
Sometimes when the screen is in portrait mode the buttons will not all fit on screen.
I am not sure why this happens. The buttons are resized in the layout manager to be slightly smaller than the available width. I have found that CSS positioning can be tricky to get just right.
I have found that creating a user interface that serves all possible browsers and screen sizes is difficult. The game effectively has two GUIs – one for horizontal orientation and one for vertical. It might be simpler and more practical for many games to pick an aspect ratio and stick with it.