Skip to content
← Back to Community
Simple JS Game Tutorial
Profile icon
MattDESTROYER

Simple JS Game Tutorial

In this tutorial you will learn the fundamentals of game development and how to create a simple, yet fast, reusable/adaptable game from scratch with JavaScript.

Before we get into making an actual game, first we must know what a game requires.

Their are a few main things a game requires:

  • Input
  • A main loop
  • Rendering (which should generally occur in the main loop, but is somewhat of a seperate operation)

Alright, we know the basic concept of making a game, so let's get started!

First of all we need something to draw to, the HTML canvas is the best tool for this. In this tutorial we will only use JavaScript, however you could easily add a canvas tag in the body of your HTML file.

// create a canvas let canvas = document.createElement("canvas"); // add the canvas to the document (document.body || document.getElementsByTagName("body")[0]).append(canvas); // get canvas context (this is how we will actually draw on the canvas) let ctx = canvas.getContext("2d"); // a simple script to add some css to the document { // create a style tag let style = document.createElement("style"); // the css we want to add (removes margins and hides scroll bars) const css = "*{margin:0px;overflow:hidden;}"; // set the type of the style tag style.type = "text/css"; // set the rel of the style tag style.rel = "stylesheet"; // add the css to the style tag if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } // add the style tag to the head of the document (document.head || document.getElementsByTagName("head")[0]).appendChild(style); } // now that we have done that, we can make the canvas fill the screen (if you don't want the canvas to fill the screen then skip this and the css above) canvas.width = window.innerWidth; canvas.height = window.innerHeight;

Alright, next we're going to want to have access to user input. To do this we are going to add some event listeners to the document.

let Input = {}; // keydown event (occurs when the user pushes down a key on their keyboard) document.addEventListener("keydown", function(e) { e = e || window.event; // set Input[keyCode] to true Input[e.keyCode] = true; // set Input[key] to true Input[e.key.toString().toUpperCase()] = true; // store the keyCode of the last key pressed Input.keyCode = e.keyCode; // store the key of the last key pressed Input.key = e.key.toString() }); // keyup event (occurs when the user releases a key on their keyboard) document.addEventListener("keyup", function(e) { e = e || window.event; // set Input[keyCode] to false Input[e.keyCode] = false; // set Input[key] to false Input[e.key.toString().toUpperCase()] = false; }); // mousedown event (occurs when the user presses one of the mouse buttons) document.addEventListener("mousedown", function(e) { e = e || window.event; Input.buttons = e.buttons; Input.button = e.button; Input.mouseDown = true; }); // mouseup event (occurs when the user releases one of the mouse buttons) document.addEventListener("mouseup", function(e) { e = e || window.event; Input.buttons = e.buttons; Input.button = e.button; Input.mouseDown = false; }); // mousemove event (occurs when the user moves their cursor/mouse) document.addEventListener("mousemove", function(e) { e = e || window.event; let rect = canvas.getBoundingClientRect(); // store the previous mouse x and y positions Input.pmouseX = Input.mouseX; Input.pmouseY = Input.mouseY; // get the current mouse x and y positions Input.mouseX = e.clientX - rect.left; Input.mouseY = e.clientY - rect.top; });

Now to explain what we've just done a little. We have created an object called Input and used event listeners to feed it all sorts of information about user input. We can use Input to check if a key is pressed:

// the 'W' key (note all keys are converted to uppercase so that their is no difference depending on whether or not shift is pressed) if (Input.W) { console.log("The 'W' key was pressed"); } // the keyCode for the ' ' key (space bar) if (Input[32]) { console.log("The ' ' key (space bar) was pressed"); } if (Input.mouseDown) { console.log("Mouse button " + Input.button + " was pressed"); }

Now we've got user input and somewhere to render. All we have left to setup is the loop! The fastest and best method for this is using a recursive requestAnimationFrame function:

function loop() { window.requestAnimationFrame(loop); }

To start our game we need to call loop:

loop();

So far nothing happens, that's because we haven't added anything to our loop yet. There are three things we want to do in our loop (if you are familiar with Unity, this follows the same general structure); FixedUpdate, Update, and Render.

function loop() { FixedUpdate(); Update(); Render(); window.requestAnimationFrame(loop); }

Right now these functions are no different, but, we will make a key change that will differentiate FixedUpdate and Update.

let previousUpdateTime = Date.now(); function loop() { FixedUpdate(); if (Date.now() - previousUpdateTime > 15) { Update(); previousUpdateTime = Date.now(); } Render(); window.requestAnimationFrame(loop); }

Now we will only update the frame around every 16 milliseconds, which results in approximately 60 updates per second. Update is a consistant function, we will want to handle things like movement here, FixedUpdate will continue to run every frame which will make it good for handling things like collisions. Now it's good and all that we have these functions in our loop, but they don't actually exist yet. Let's start making our game! For this tutorial we will make a simple maze escape game. To make it easier we will use a grid (formed by a two dimensional array) as the actual maze and render the content of the grid. We won't actually need FixedUpdate to make this game, but it can definitely come in handy so don't forget about it!

// the size (in pixels) to render in const BLOCK_SIZE = 25; // a ten by ten grid let grid = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 2, 1, 0, 0, 0, 0, 0, 0, 1], [1, 0, 1, 0, 1, 0, 1, 0, 1, 1], [1, 0, 1, 0, 1, 0, 1, 0, 0, 1], [1, 0, 0, 0, 1, 1, 1, 1, 0, 1], [1, 1, 1, 0, 1, 0, 0, 0, 0, 1], [1, 0, 1, 0, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 1, 0, 0, 0, 1, 1, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ]; /* * Grid Key: * - 0 = air * - 1 = solid * - 2 = player spawn * - 3 = level finish */ // a camera object let camera = { x: 0, y: 0, update: function () { this.x = player.x + (BLOCK_SIZE - 2) / 2; this.y = player.y + (BLOCK_SIZE - 2) / 2; } }; // a player object let player = { x: 0, y: 0, xVel: 0, yVel: 0, moveToSpawn: function () { for (let y = 0; y < grid.length; y++) { for (let x = 0; x < grid[y].length; x++) { if (grid[y][x] === 2) { this.x = x * BLOCK_SIZE + 1; this.y = y * BLOCK_SIZE + 1; return true; } } } return false; }, // check if the player collides with a rectangle with the input information collides: function (x, y, w, h) { return this.x < x + w && this.x + BLOCK_SIZE - 2 > x && this.y < y + h && this.y + BLOCK_SIZE - 2 > y; }, update: function () { // gives the player velocity based on input // notice I do NOT use else if, this is because // using else if gives dominance to the first // condition... this way if the up arrow is // pressed, we will move up, but if the up AND // down arrow key is pressed, we WON'T move if (Input.W || Input.ARROWUP) { this.yVel--; } if (Input.S || Input.ARROWDOWN) { this.yVel++; } if (Input.A || Input.ARROWLEFT) { this.xVel--; } if (Input.D || Input.ARROWRIGHT) { this.xVel++; } // resistance this.xVel *= 0.8; this.yVel *= 0.8; // move this.x += this.xVel; let _reverse = this.xVel / Math.abs(this.xVel); for (let y = 0; y < grid.length; y++) { for (let x = 0; x < grid[y].length; x++) { if (this.collides(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)) { switch (grid[y][x]) { case 1: while (this.collides(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)) { this.x -= _reverse; } this.xVel = 0; break; case 3: console.log("level completed!"); break; } } } } this.y += this.yVel; _reverse = this.yVel / Math.abs(this.yVel); for (let y = 0; y < grid.length; y++) { for (let x = 0; x < grid[y].length; x++) { if (this.collides(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)) { switch (grid[y][x]) { case 1: while (this.collides(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)) { this.y -= _reverse; } this.yVel = 0; break; case 3: console.log("level completed!"); break; } } } } }, // draw a blue square at the player's position offset by the input offset render: function (xOffset = 0, yOffset = 0) { ctx.fillStyle = "blue"; ctx.fillRect(this.x - xOffset, this.y - yOffset, BLOCK_SIZE - 2, BLOCK_SIZE - 2); } }; // in this case we aren't really using this, but it can be useful function FixedUpdate() { } // in this case the player is the only thing we need to update, but when we have multiply interactive/moving objects, this function comes in handy function Update() { camera.update(); player.update(); } function Render() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < grid.length; y++) { for (let x = 0; x < grid[y].length; x++) { switch (grid[y][x]) { case 1: ctx.fillStyle = "black"; ctx.fillRect(x * BLOCK_SIZE - (player.x + (BLOCK_SIZE - 2) / 2) + canvas.width / 2, y * BLOCK_SIZE - (player.y + (BLOCK_SIZE - 2) / 2) + canvas.height / 2, BLOCK_SIZE, BLOCK_SIZE); break; case 3: ctx.fillStyle = "green"; ctx.fillRect(x * BLOCK_SIZE - (player.x + (BLOCK_SIZE - 2) / 2) + canvas.width / 2, y * BLOCK_SIZE - (player.y + (BLOCK_SIZE - 2) / 2) + canvas.height / 2, BLOCK_SIZE, BLOCK_SIZE); break; } } } player.render(camera.x - canvas.width / 2, camera.y - canvas.height / 2); }

(A short explanation of some of the things used in the code):

ctx is the context we got from our canvas. We use ctx to draw on our canvas. ctx has a number of properties to allow you to draw different things on the canvas. In the game above I use three: clearRect(x, y, width, height);, fillRect(x, y, width, height);, and fillStyle = colour;.

clearRect(x, y, width, height); is pretty self-explanatory, it clears a rectangular area on the canvas based on the input x, y, width and height.

fillRect(x, y, width, height); is also pretty self-explanatory, it fills a rectangular area on the canvas based on the input x, y, width and height and the current fillStyle.

fillStyle = colour; is a little different, it is not a function. Setting fillStyle to a (CSS) colour changes the colour that 'fill actions' like fillRect will use to fill areas of the canvas. fillStyle can be set to any CSS colour; "red", "rgb(255, 0, 0)", and "#FF0000" are all valid.

The CanvasRenderingContext2D (which we have named ctx) can do so much more, so check out the documentation on mdn if you want to find out how else you can use it.

Voters
Profile icon
HBthePencil
Profile icon
ermidoodle
Profile icon
Joy-port
Profile icon
DENASILFA1
Profile icon
lsikora
Profile icon
ruiwenge2
Profile icon
JBloves27
Profile icon
FnuHadia
Profile icon
MatthewAlfandre
Profile icon
VulcanWM
Comments
hotnewtop
Profile icon
cesardegrote

how do i add a background without it following the camera???

Profile icon
MattDESTROYER

@cesardegrote You can change the background colour of the canvas:

{ // create a style tag let style = document.createElement("style"); // the css we want to add (removes margins, hides scroll bars, and sets canvas background colour to blue) const css = "*{margin:0px;overflow:hidden;}canvas{background-color:blue;}"; // set the type of the style tag style.type = "text/css"; // set the rel of the style tag style.rel = "stylesheet"; // add the css to the style tag if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } // add the style tag to the head of the document (document.head || document.getElementsByTagName("head")[0]).appendChild(style); }

You can also just draw stuff in the Render function, which will not automatically draw stuff based on where the camera is.

Either of those options will create a static background that doesn't move when the camera does.

Profile icon
ruiwenge2

nice tutorial :)

Profile icon
MattDESTROYER

@ruiwenge2 Thanks :D

Profile icon
JBloves27

Great tutorial!

Profile icon
MattDESTROYER

@JBloves27 Thanks :)

Profile icon
JBloves27

No problem! Are you planning on creating more tutorials like these? @MattDESTROYER

Profile icon
MattDESTROYER

@JBloves27 Well I guess so... depends on if I get an idea that I think will help people and decide to make a tutorial :)

Profile icon
JBloves27

Ooh, okay! Feel free to message me if you want an idea, any help, or want to send me a link! @MattDESTROYER

Profile icon
MattDESTROYER

@JBloves27 I will keep that in mind :)

Profile icon
VulcanWM

Really nicely explained!

Profile icon
MattDESTROYER

@VulcanWM Thanks :D

Profile icon
VulcanWM
Profile icon
VulcanWM
Profile icon
VulcanWM

for me it shows that it starts in 12 hours.
that's 12pm for me as well
how does that work? @MattDESTROYER

Profile icon
VulcanWM

yeah that’s weird @MattDESTROYER

Profile icon
VulcanWM

Screenshot of my screen

0EA6F873-4E7E-4F55-998E-2C828F7E0714

@MattDESTROYER

Profile icon
VulcanWM
Profile icon
VulcanWM
Profile icon
IMayBeMe

Really good explanation of some concepts in addition to the code. Great tutorial!

Profile icon
MattDESTROYER

@IMayBeMe Thanks :D