Blog / Javascript

How to develop 2D JavaScript games with HTML5 and React

Learn in this easy step by step guide how to create simple, fun, 2D JavaScript games with HTML5 and React

Will MaygerWill Mayger
March 24, 2019
Article

It’s easier than you think.

Developing a game can be daunting, especially for us Web developers / JavaScript developers.

Just a heads up, this is quite a long read. If you just want to see the tutorial scroll down until you see a large heading saying “2D JavaScript game and HTML5 game coding tutorial”.

It’s outside our usual realm of coding so is almost unknown territory, but good news, it’s not as hard as it may seem!

We are all used to coding in JavaScript, HTML5, React, React Native, VueJS, AngularJS, VanillaJS, and so on…

We usually use these frameworks to make making UI’s or web apps with animations, transitions, and event listeners nice and easy. It’s not that much different when creating simple 2D JavaScript games or 2D HTML5 games.

If you are used to using a framework like React you will have to change the way you may normally write your applications to allow for game development, but I will help walk you through this.

Many people would now start talking about how to use canvas, and combine it with JavaScript. Whilst this is performant, it is not the easiest to understand in comparison to standard HTML5 elements like a div or a span.

Many people might also mention specific libraries known as game engines, to help you develop the physics, and other things you may need in your game, we won’t be using any here, we will be keeping it nice and simple.

For this reason, in this step by step guide, we will be using regular HTML5 elements in conjunction with JavaScript to build our 2D game.

I keep mentioning 2D games specifically, this is because 3D games, whilst similar, do have another layer of complexity with things such as Unity. I highly recommend starting off with 2D (and this tutorial!).

So in this JavaScript game / HTML5 game tutorial, we will be using a few tools I live by, all of which are built for normal web development. We will be using React, React Hooks, JavaScript, CSS3 and HTML5. If you don’t know React or React Hooks very well, you don’t need to worry as I will be explaining what each part is doing (even the React). So after you will not only be able to develop games with JavaScript and HTML5, you will be able to say, with confidence, that you can create React Hook games too!

After you have done with this tutorial on how to develop 2D JavaScript games and 2D HTML5 games, you will be able to take the concepts and apply it to any framework you want.

Personally, I have used this with React, React Native and VanillaJS.

Very briefly, the kind of game we will be creating will be a platformer, a little bit like flappy bird.

Let’s begin with some concepts!

Lifecycle

Those who are used to the lifecycle of frameworks like React or Vue will know that when you update a variable in the state object, the component will then re-render with the changes made to the state object, trying to be as efficient as possible, by only replacing the nodes that have been affected. This is known as reactivity.

Now, for the game we will be developing (a 2D platform game) all you need to do, is the exact opposite of reactivity, when creating 2D JavaScript games and 2D HTML5 games like in this guide.

This means you want nothing to happen when you update a variable, we can do this multiple ways, including closures, using this, or let and var variables. Basically any other way, other than using the state object.

Whilst this will probably feel a little counter intuitive, and this certainly does not apply to all games, but it is important that you understand why this is done for many games, and even some animations.

Which takes us nicely onto why we are doing this which is… painting.

Painting frames

No, i’m not talking about painting a pretty picture, literally speaking anyway.

When you create 2D games such as platformers, using just about any language, you will need to be using some form of timer, to plan out how fast the stage / platform is moving and to ensure that the users actions are in sync with the rest of the game.

If you don’t do this then the game will be laggy (if using reactivity) and it will not behave how you would expect, causing games that can’t actually be completed, and creating users that won’t want to try.

To sync up our game properly we will use something known as painting frames, or re-painting. I am sure you have heard of FPS or frames per second.

All FPS means, for us, is the most amount frames, or re-renders you can have per second, for a certain client, such as a browser like Google Chrome.

You can choose to use the max for best performance or less for better resource allocation.

We will be using window.requestAnimationFrame since we are using JavaScript, HTML5 and React. This function handles the FPS for us and provides us with a handy function that will only be called if the frame rate on the browser will allow it.

All we need to do is provide it with a function to call that will have all the information that will rendered on that paint.

For example if the users action is to jump, and they press the jump button, we will save this action as a variable and then provide it to the window.requestAnimationFrame, then each render triggered by window.requestAnimationFrame will make the users character jump, one frame at a time, until we stop the jump.

You could create similar functionality, using various other things such as setInterval, and recursive async functions.

I have found that for 2D game development with JavaScript, HTML5 and React, window.requestAnimationFrame generally works the best, and is best practice.

painting = re-rendering for each frame

Styling and assets

Styling and assets are incredibly similar to what you would do in normal web development compared with 2D JavaScript games and 2D HTML5 games.

There are a few key differences though, the first are tiles.

Tiles

Tiles are each square of your map / platform that is probably going to be the size of your in game character, for example a tile could be 40px by 40px.

If your character moves to the left by one, this means it will be moving 40 pixels to the left as that is one tile.

Layers

This will depend on the type of game you are building, to get an idea of the kind of game you would need layers for, take a look at my portfolio site here at https://willmayger.co.uk/.

The idea of layers are that you layer the game up assets in order to easily create new maps and new scenery.

For example, you would need to start with your base layer, this might be some grass. Then you may want to add a few trees, houses and ponds…

Sprites

Sprites and spriting are an efficient way of storing lots of assets in a single file, meaning your browser will only have to download one file rather than five, for example. Then you move the image to different parts hiding the rest of the image so you get each part as its own image.

This used to be the best way, and is still best practice, however, most browsers and devices are more than capable of downloading multiple assets at once, meaning the need for spriting is much less than it used to be.

For the game we will be making, we won’t be using sprites, this is simply because it will not really affect the performance of the 2D JavaScript game / 2D HTML5 game very much at all and I think it is easier to work with single assets, because it is easier for humans to read.

Event listeners

Event listeners are something we are all probably familiar with, it is very common to use them in just about any kind of front end development. What an event listener looks like:

    window.addEventListener('event', callback);

You can attach them to events such as the window resizing, the user scrolling, pressing a certain key, clicking on a certain element like a button.

For our game, we will be making use of the event listeners to make it easy to jump using the spacebar.

Now the bit you’ve been waiting for, the tutorial!

2D JavaScript game and HTML5 game coding tutorial

Make sure your NodeJS environment is up to date on your computer. Update or install create-react-app. Open up your favourite terminal and code editor (for me its hyper and Visual Studio Code).

Now you’re ready to begin!

Setup

Start off by initializing a brand new, empty react project by typing the following into your terminal like so:

    npx create-react-app js-html-game-tutorial

Once that has finished downloading the internet… move your terminal directory inside the js-html-game-tutorial folder like so:

    cd js-html-game-tutorial

Open this directory up in your code editor now as well. You should see a file system a little like the following straight off the bat:

create-react-app code architecture

Let’s change this to a slightly more friendly layout that is easier to both read and to use.

So lets change our folder structure to look a little more like this:

wills code architecture

Try to just follow that architecture for the minute by adding in empty files and folders with the names that I have added in.

Here is the entire architecture so far broken down:

 - js-html-game-tutorial
  - src
    - assets
    - components
      - engine
        - index.js
        - engine.module.scss
        - engine.test.js
    - helpers
      - index.js
    - hooks
      - index.js
    - views
      - home
        - index.js
    - globals.scss
    - index.js
    - serviceWorker.js
    - .gitignore
    - package.json
    - README.md

Now we have are base architecture set up for our 2D JavaScript / 2D HTML5 game, let’s install a few packages that we will need.

In no particular order, the first is node-sass this is so that our sass files & css module will be compatible with our project by simply importing them into each JavaScript file respectively.

So run this in your terminal to install it yarn add --dev node-sass.

Next, install ‘prop-types’ with yarn add --dev prop-types. This is how we type check any property or prop passed into a react component.

Finally, for the time being, you will need to install eslint I would recommend that you install this one globally because it will make installing it on all projects nice and easy.

Globally: yarn global add eslint

Then in the scope of the project run eslint --init After running that command you will be presented with a few choices. My recommendations here are the following:

How would you like to use ESLint? To check syntax, find problems, and enforce code style What type of modules does your project use? JavaScript modules (import/export) Which framework does your project use? React Where does your code run? Browser How would you like to define a style for your project? Use a popular style guide Which style guide do you want to follow? Airbnb (https://github.com/airbnb/javascript) What format do you want your config file to be in? JavaScript

After completing these questions that will help set up your eslinter for JavaScript and React say yes when you get asked if you want the script to install all the packages for you.

You may now have to fix an issue with eslint, due to an incompatibility, it is very quick and easy to fix.

Delete package-lock.json and the yarn.lock Delete node_modules In your package.json, remove the one dependency that is exactly called “eslint” (not the other eslint dependencies) Run yarn to install the packages again, with the eslint now aligned with that of the create-react-app

Initial code

Now we have our project set up, we need to get it working to do this let’s start by opening up the root index.js file in our code editor.

Replace the code within that file so it reads like the following code snippet

import React from 'react';
import ReactDOM from 'react-dom';
import './globals.scss';
import Home from './views/home';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<Home />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Here we have simply switched the old index.css file to our new globals.scss sass file, and changed the App component to our Home component that is located within the views directory.

Having a view folder is very useful if you are using react router, or splitting up your application into separate parts.

Next let’s get our home component using the JavaScript / HTML5 game engine we will be creating so we can get stuck into the game code.

Open up src > views > home > index.js and the reference to the game engine component in like so:

import React from 'react';
import Engine from '../../components/engine';

export default function Home() {
  return (<Engine />);
}

Finally add in a very basic component in the game engine to get our application to run.

src > components > engine > index.js

import React from 'react';

export default function Engine() {
  return (
    <h1>
      2D JavaScript Game engine / 2D HTML5 game engine
    </h1>
  );
}

Now run yarn start in your terminal and you should see something like the following

app working

Perfect!

That is our project setup to start building our game on.

Onto the game code

To keep this blog post nice and easy to follow, we will be using colours rather than assets, later on you can simply swap colours for images instead.

Now, let’s start by creating something a little easier to visualize by adding in a blue background to our game and creating a yellow character, using JSX/HTML5 and css.

Edit the css module file (src > components > engine > engine.module.scss).

.container {
  position: relative;
  background-color: #568cd2;
  height: 100vh;
  width: 100vw;
}

.character {
  position: absolute;
  display: block;
  height: 100px;
  width: 100px;
  bottom: 0;
  left: 0;
  background-color: #ffd600;
}

The container must be set to position: relative; and the character set to position: absolute;, this is so we can move the character freely within the container element.

Then in the engine component: src > components > engine > index.js

import React from 'react';
import styles from './engine.module.scss';

export default function Engine() {
  return (
    <div
      className={styles.container}
    >
      <span
        className={styles.character}
      />
    </div>
  );
}

Here we are importing the styles as a css module meaning we can reference the classes in the className property in the JSX / HTML5.

You should then have something fairly basic that looks like a blue screen with a yellow square in the bottom left of the screen.

character and background

Okay, now, we need to add some event listeners to be able to make our game character jump, when the spacebar has been pressed.

For this we will be using a hook that will only be triggered once in its entire lifetime, much like componentDidMount is used.

I often like to start by deciding how we want to be able to use this in our code, in my opinion, this hook should be no more than one line and should take only two parameters. It should replace the window.addEventListener function.

In our engine I want it to look like the following, so add this into your code now:

import React from 'react';
import styles from './engine.module.scss';
import { useDidMount } from '../../hooks';

export default function Engine() {
  const handleKeyPress = (e) => {};

  useEvent('keyup', handleKeyPress);

  return (
    <div
      className={styles.container}
    >
      <span
        className={styles.character}
      />
    </div>
  );
}

So, we have decided how we want to use our new hook, let’s make it.

Go to src > hooks and create a new file called useEvent.js.

In this file we need to add the following bit of code.

import { useEffect } from 'react';

export default function useEvent(event, handler) {
  useEffect(() => {
    // initiate the event handler
    window.addEventListener(event, handler);

    // this will clean up the event every time the component is re-rendered
    return function cleanup() {
      window.removeEventListener(event, handler);
    };
  });
}

With React hooks, it is generally best practice to name them starting with the word use. Also, where possible you should always return a function that will clean up your code on unmounting to save resources.

It is very important to cleanup if you need to use event listeners, because if you don’t have a cleanup method, or if statements the event will be added again and again after each render, this will eventually use a lot of resources slowing the users computer severely.

Now in the file src > hooks > index.js add the following code so we can reference our new hook easily.

import useEvent from './useEvent;

export {
  useEvent,
};

Let’s take a look at our event handler.

Back in the engine index.js file replace the function expression const handleKeyPress = (e) => {}; with the following code.

  const handleKeyPress = (e) => {
    // the ' ' char actually represents the space bar key.
    if (e.key === ' ') {
      console.log('You pressed the spacebar key!');
    }
  };

Now we need to test this code out, on the tab where your localhost is running, open an inspector window (in chrome), now press the spacebar. If you see the log, success!

So we now have our stage, character and our event listener.

Now we need to start making a JavaScript, HTML5 & React game out of this.

First things first, let’s make our game stage move. This means the stage will need to move from the right to the left making it seem like are character is moving from left to right.

To do this we will need to create a stage, HTML5 / JSX element within our container, our character will sit inside of this container as well. We will make it move using the inline style transform: translate(x, y).

Remember the painting concept? This is where we will use it, to create, almost a timer that will be the first rule of our game, everything will run off of that timer.

To create the timer we will use the framerate of the browser. Let’s take a look at the code of how this is done.

import React, { useState, useEffect } from 'react';
import styles from './engine.module.scss';
import { useEvent } from '../../hooks';

function CreateEngine(setState) {
  this.settings = {
    tile: 100, // width of one tile
  };

  // current stage position
  this.stage = 0;

  // function that will be continuously ran
  this.repaint = () => {
    // move the stage by one tile
    this.stage += this.settings.tile;

    // set state for use in the component
    setState({ stage: this.stage });

    // start repaint on next frame
    return requestAnimationFrame(this.repaint);
  };

  // trigger initial paint
  this.repaint();
  return () => ({
  });
}

export default function Engine() {
  // game state
  const [gameState, setGameState] = useState({ stage: 0 });

  // trigger game to start
  const [start, setStart] = useState(false);

  // if game is running
  const [started, setStarted] = useState(false);

  // instance of game engine
  const [engine, setEngine] = useState(null);

  const handleKeyPress = (e) => {
    // the ' ' char actually represents the space bar key.
    if (e.key === ' ') {
      // start the game when the user first presses the spacebar
      if (!started && !start) {
        setStart(true);
      }

      // if the game has not been initialized return
      if (engine === null) return;

      // otherwise jump
      // engine.jump();
    }
  };

  useEvent('keyup', handleKeyPress);

  useEffect(() => {
    if (start) {
      setStarted(true);
      setStart(false);
      // create a new engine and save it to the state to use
      setEngine(
        new CreateEngine(
          // set state
          state => setGameState(state),
        ),
      );
    }
  });

  return (
    <div
      className={styles.container}
    >
      <div
        className={styles.stage}
        style={{
          transform: `translate(${gameState.stage}px, 0px)`, // move stage
        }}
      >
        <span
          className={styles.character}
          style={{
            transform: `translate(-${gameState.stage}px, 0px)`, // move char in opposite direction
          }}
        />
      </div>
    </div>
  );
}

In this code we are doing a few things, and is the main portion of our game. First we are creating a new engine instance where it takes a function to set the state as an argument and then we save this instance. It is a closure so the methods and variables are safe inside, we will build on this closure next to create the jump.

Next we start the painting cycle so it will repeatedly move the stage position like a counter. Finally we are moving each element using the transition element where its position is calculated by JavaScript.

Try to work your way through the code to really understand what is going on.

Now let’s make our character jump! (This is where we start filling in our closure)

import React, { useState, useEffect } from 'react';
import styles from './engine.module.scss';
import { useEvent } from '../../hooks';

function CreateEngine(setState) {
  this.settings = {
    tile: 10, // width of one tile
  };

  // current stage position
  this.stage = 0;
  this.jump = false;
  this.direction = 'up';
  this.position = 0;
  this.max = this.settings.tile * 30;

  const doJump = () => {
    // if not jumping, reset and return
    if (!this.jump) {
      this.position = 0;
      this.direction = 'up';
      return;
    }

    // if finished jumping, reset and return
    if (this.direction === 'down' && this.position <= 0) {
      this.jump = false;
      this.position = 0;
      this.direction = 'up';
      return;
    }

    // if the jump is at its max, start falling
    if (this.position >= this.max) this.direction = 'down';

    // depending on the direction increment the jump.
    if (this.direction === 'up') {
      this.position += this.settings.tile;
    } else {
      this.position -= this.settings.tile;
    }
  };

  // function that will be continuously ran
  this.repaint = () => {
    // move the stage by one tile
    this.stage += this.settings.tile;

    // check and perform jump
    doJump();

    // set state for use in the component
    setState({ stage: this.stage, jump: this.position });

    // start repaint on next frame
    return requestAnimationFrame(this.repaint);
  };

  // trigger initial paint
  this.repaint();
  return () => ({
    jump: () => {
      // if jump is not active, trigger jump
      if (!this.jump) {
        this.jump = true;
      }
    },
  });
}

export default function Engine() {
  // game state
  const [gameState, setGameState] = useState({ stage: 0, jump: 0 });

  // trigger game to start
  const [start, setStart] = useState(false);

  // if game is running
  const [started, setStarted] = useState(false);

  // instance of game engine
  const [engine, setEngine] = useState(null);

  const handleKeyPress = (e) => {
    // the ' ' char actually represents the space bar key.
    if (e.key === ' ') {
      // start the game when the user first presses the spacebar
      if (!started && !start) {
        setStart(true);
      }

      // if the game has not been initialized return
      if (engine === null) return;

      // otherwise jump
      engine.jump();
    }
  };

  useEvent('keyup', handleKeyPress);

  useEffect(() => {
    if (start) {
      setStarted(true);
      setStart(false);
      // create a new engine and save it to the state to use
      setEngine(
        new CreateEngine(
          // set state
          state => setGameState(state),
        ),
      );
    }
  });

  return (
    <div
      className={styles.container}
    >
      <div
        className={styles.stage}
        style={{
          transform: `translate(${gameState.stage}px, 0px)`, // move stage
        }}
      >
        <span
          className={styles.character}
          style={{
            transform: `translate(-${gameState.stage}px, -${gameState.jump}px)`, // move char in opposite direction
          }}
        />
      </div>
    </div>
  );
}

Once the jump is triggered by the spacebar press calling the closure’s jump method, it is dealt with by a series of logical events in the doJump function. This will produce a fairly smooth jump transition. Now if you wish to make it more realistic you may want to play around with physics libraries or do your own research into this.

Now that we can jump, we need to add something(s) in that we can jump over! Checkout this next bit of code. It works by looping over an array of coordinate like numbers that plot out where obstacles / blocks will be in our 2D JavaScript game / 2D HTML5 game.

import React, { useState, useEffect } from 'react';
import styles from './engine.module.scss';
import { useEvent } from '../../hooks';

const BLOCKS = [
  140,
  250,
  390,
];

// this is in comparison to the rest of the game
// 2 is twice the speed
// 1 is the same speed
const JUMP_VELOCITY = 1.4;

function CreateEngine(setState) {
  this.settings = {
    tile: 10, // width of one tile
  };

  // current stage position
  this.stage = 0;
  this.jump = false;
  this.direction = 'up';
  this.position = 0;
  this.max = this.settings.tile * 40;
  this.blocks = BLOCKS.map(b => (b * this.settings.tile));

  const doJump = () => {
    // if not jumping, reset and return
    if (!this.jump) {
      this.position = 0;
      this.direction = 'up';
      return;
    }

    // if finished jumping, reset and return
    if (this.direction === 'down' && this.position <= 0) {
      this.jump = false;
      this.position = 0;
      this.direction = 'up';
      return;
    }

    // if the jump is at its max, start falling
    if (this.position >= this.max) this.direction = 'down';

    // depending on the direction increment the jump.
    if (this.direction === 'up') {
      this.position += (this.settings.tile * JUMP_VELOCITY);
    } else {
      this.position -= (this.settings.tile * JUMP_VELOCITY);
    }
  };

  // function that will be continuously ran
  this.repaint = () => {
    // move the stage by one tile
    this.stage += this.settings.tile;

    // check and perform jump
    doJump();

    // set state for use in the component
    setState({
      stage: this.stage,
      jump: this.position,
      blocks: this.blocks,
    });

    // start repaint on next frame
    return requestAnimationFrame(this.repaint);
  };

  // trigger initial paint
  this.repaint();
  return () => ({
    jump: () => {
      // if jump is not active, trigger jump
      if (!this.jump) {
        this.jump = true;
      }
    },
  });
}

export default function Engine() {
  // game state
  const [gameState, setGameState] = useState({
    stage: 0,
    jump: 0,
    blocks: [],
  });

  // trigger game to start
  const [start, setStart] = useState(false);

  // if game is running
  const [started, setStarted] = useState(false);

  // instance of game engine
  const [engine, setEngine] = useState(null);

  const handleKeyPress = (e) => {
    // the ' ' char actually represents the space bar key.
    if (e.key === ' ') {
      // start the game when the user first presses the spacebar
      if (!started && !start) {
        setStart(true);
      }

      // if the game has not been initialized return
      if (engine === null) return;

      // otherwise jump
      engine.jump();
    }
  };

  useEvent('keyup', handleKeyPress);

  useEffect(() => {
    if (start) {
      setStarted(true);
      setStart(false);
      // create a new engine and save it to the state to use
      setEngine(
        new CreateEngine(
          // set state
          state => setGameState(state),
        ),
      );
    }
  });

  return (
    <div
      className={styles.container}
    >
      <div
        className={styles.stage}
        style={{
          transform: `translate(-${gameState.stage}px, 0px)`, // move stage
        }}
      >
        <span
          className={styles.character}
          style={{
            transform: `translate(${gameState.stage}px, -${gameState.jump}px)`, // move char in opposite direction
          }}
        />
        {
          gameState.blocks.map(
            block => (
              <span
                className={styles.block}
                key={block}
                style={{
                  transform: `translate(${block}px, 0px)`, // move stage
                }}
              />
            ),
          )
        }
      </div>
    </div>
  );
}

Great, we now have a stage, a character that can jump, and obstacles to jump over. The only thing left to do is make sure that the user can either win or lose our game.

To do this we need to detect if our character hits an obstacle, if they pass all without hitting any, they have won, otherwise they have failed.

This is actually fairly easy because our character is within the stage, just like our obstacles. We just have to check if they are at the same coordinates / position.

Let’s do it!

import React, { useState, useEffect } from 'react';
import styles from './engine.module.scss';
import { useEvent } from '../../hooks';

const BLOCKS = [
  140,
  250,
  390,
];

const charWidth = 100;
const charHeight = 100;

const blockWidth = 80;
const blockHeight = 200;

// this is in comparison to the rest of the game
// 2 is twice the speed
// 1 is the same speed
const JUMP_VELOCITY = 1.4;

function CreateEngine(setState) {
  this.settings = {
    tile: 10, // width of one tile
  };

  // current stage position
  this.game = 'start';
  this.stage = 0;
  this.jump = false;
  this.direction = 'up';
  this.position = 0;
  this.max = this.settings.tile * 40;
  this.blocks = BLOCKS.map(b => (b * this.settings.tile));

  const checkBlocks = () => {
    const charXPos = this.stage + 200;
    const charYPos = this.position;

    // if the char has past all blocks
    if (charXPos > this.blocks[this.blocks.length - 1] + 200 && this.position <= 0) {
      this.game = 'win';
    }

    this.blocks.forEach((block) => {
      // if char hits a block
      if (
        charXPos + charWidth >= block
        && charYPos <= blockHeight
        && charYPos + charHeight >= 0
        && charXPos <= block + blockWidth
      ) {
        this.game = 'fail';
      }
    });
  };

  const doJump = () => {
    // if not jumping, reset and return
    if (!this.jump) {
      this.position = 0;
      this.direction = 'up';
      return;
    }

    // if finished jumping, reset and return
    if (this.direction === 'down' && this.position <= 0) {
      this.jump = false;
      this.position = 0;
      this.direction = 'up';
      return;
    }

    // if the jump is at its max, start falling
    if (this.position >= this.max) this.direction = 'down';

    // depending on the direction increment the jump.
    if (this.direction === 'up') {
      this.position += (this.settings.tile * JUMP_VELOCITY);
    } else {
      this.position -= (this.settings.tile * JUMP_VELOCITY);
    }
  };

  // function that will be continuously ran
  this.repaint = () => {
    // move the stage by one tile
    this.stage += this.settings.tile;

    // check if char has hit a block
    checkBlocks();

    // check and perform jump
    doJump();

    // set state for use in the component
    setState({
      stage: this.stage,
      jump: this.position,
      blocks: this.blocks,
      status: this.game,
    });

    // stop the game if the game var has been set to false
    if (this.game !== 'start') {
      // reset and stop
      this.game = 'start';
      this.stage = 0;
      this.jump = false;
      this.direction = 'up';
      this.position = 0;
      return null;
    }

    // start repaint on next frame
    return requestAnimationFrame(this.repaint);
  };

  // trigger initial paint
  this.repaint();
  return () => ({
    jump: () => {
      // if jump is not active, trigger jump
      if (!this.jump) {
        this.jump = true;
      }
    },
  });
}

const initialState = {
  stage: 0,
  jump: 0,
  blocks: [],
  status: 'start',
};

export default function Engine() {
  // game state
  const [gameState, setGameState] = useState(initialState);

  // trigger game to start
  const [start, setStart] = useState(false);

  // if game is running
  const [started, setStarted] = useState(false);

  // instance of game engine
  const [engine, setEngine] = useState(null);

  const handleKeyPress = (e) => {
    // the ' ' char actually represents the space bar key.
    if (e.key === ' ') {
      // start the game when the user first presses the spacebar
      if (!started && !start) {
        setStart(true);
      }

      // if the game has not been initialized return
      if (engine === null) return;

      // otherwise jump
      engine.jump();
    }
  };

  useEvent('keyup', handleKeyPress);

  useEffect(() => {
    if (start) {
      setStarted(true);
      setStart(false);
      // create a new engine and save it to the state to use
      setEngine(
        new CreateEngine(
          // set state
          state => setGameState(state),
        ),
      );
    }

    if (gameState.status === 'fail' && started) {
      setStarted(false);
      alert('You lost! Try again?');
      setGameState(initialState);
      setStart(true);
    }

    if (gameState.status === 'win' && started) {
      setStarted(false);
      alert('You won! Play again?');
      setGameState(initialState);
      setStart(true);
    }
  });

  return (
    <div
      className={styles.container}
    >
      <div
        className={styles.stage}
        style={{
          transform: `translate(-${gameState.stage}px, 0px)`, // move stage
        }}
      >
        <span
          className={styles.character}
          style={{
            transform: `translate(${gameState.stage + 200}px, -${gameState.jump}px)`, // move char in opposite direction
            height: charHeight,
            width: charWidth,
          }}
        />
        {
          gameState.blocks.map(
            block => (
              <span
                className={styles.block}
                key={block}
                style={{
                  transform: `translate(${block}px, 0px)`, // move stage
                  height: blockHeight,
                  width: blockWidth,
                }}
              />
            ),
          )
        }
      </div>
    </div>
  );
}

Final working game

And there you have your first 2D JavaScript game, 2D HTML5 game, 2D React game with react hooks.

You can take the idea of this and apply it to any web framework or technology because the principals work the same.

My advice would be to understand every part of what this code does, then adapt it for your own 2D game, and then improve on it and make it better.

I am currently building a 2D React Native using these methods, its slightly more advanced, because I need it to do more and I need it to be able to configure multiple levels easily as well. I will release a post on this game when I have released it to the various app stores.

For the time being you can find all the working code for this project on Github and CodeSandbox.

Github: https://github.com/WillMayger/2d-javascript-html5-react-game-tutorial

CodeSandbox: https://codesandbox.io/s/github/WillMayger/2d-javascript-html5-react-game-tutorial

Thanks for reading!

More posts coming soon, until then follow me on Twitter, Linkedin and Facebook.

Foxi - Budget Planner & Tracker

Foxi

Budget Planner & Tracker

More money in your pocket by the end of the month.

Free to use and no account needed.

Get started now.

Get the app

Some graphics used on this post were made using icons from flaticon.

Latest Posts

Learn React, JavaScript and TypeScript

Learn React, JavaScript and TypeScript

Join the platform that top tier companies are using.
Master the skills you need to succeed as a software engineer and take your career to the next level with Pluralsight.

Start here

Become an expert in ReactJS, TypeScript, and JavaScript.

Here you will find my personal recomendations to you, for full disclosure I earn a small commission from some of these links, but I only recommend what I trust and personally use.

Good things are coming, don't miss out!

Good things are coming, don't miss out!

Follow me on Twitter to stay up to date and learn frontend, React, JavaScript, and TypeScript tips and tricks!

Are you a novice, intermediate or expert react engineer?

Find out here by taking my fun, interactive, quick quiz which takes approximately 1 - 3 minutes. How well will you will do?

Foxi - Budget Planner & Tracker

Foxi

Budget Planner & Tracker

More money in your pocket by the end of the month.

Free to use and no account needed.

Get started now.

Get the app