Introducing React Game Kit

@emmabrillhart

See Ken’s slides from his React Next talk.

Introducing React Game Kit

character-gif

React Game Kit is Formidable’s newest release, written by the one and only Ken Wheeler. Since Ken is busy killing it in Israel at ReactNext, I’m giving a rundown of what React Game Kit is and why you should use it. Ken’s slides from his React Next talk can be found here, and I highly recommend you take a look at them.

Game Dev Basics

Should Can you build a game with React?

You sure can!

Why would you build a game with React?
* The same game code can work on the web, iOS & Android
* You primarily write React code
* You dont feel like learning Unity
* You can hot reload game logic

What is a game?

“A form of play or sport, especially a competitive one played according to rules and decided by skill, strength, or luck.”

Today we are going to learn how to make a 2d platformer game with ReactJS.

Basic Concepts

Game Loop

A programmatic loop that gets input, updates game state and draws the game.

Tick

Each step of the loop.

Update Function

A function called on each tick where game logic is checked.

Stage

The main game container to which game entities are added.

Sprite

An often animated bitmap graphic derived from a larger tiled image of states and steps.

TileMap

A large graphic created by rendering a matrix of position indexes derived from a smaller set of common tiles.

Physics Engine

A class that simulates physical systems.

Rigid Body Physics Engine

A physics engine that assumes that physical bodies are not elastic or fluid.

Physics World

A class that provides a set of conditions that the simulation abides by.

Physics Body

A class that acts as an entity inside the physics world.

This sounds hard.
But it doesn’t have to be!

Introducing: react-game-kit. A collection of ReactJS components and utilities that help you make awesome games. It’s pretty fun, all of the slides here are built with it. Oh, and it works on React Native too!

The Loop

How does the loop work?

requestAnimationFrame

let animationFrame;

const loop = () => {
  // Update logic
  animationFrame = requestAnimationFrame(loop);
}

animationFrame = requestAnimationFrame(loop);

How can I implement this in React with react-game-kit?

class Game extends Component {
  render() {
    return (
      <Loop>
        // Child components get this.context.loop
      </Loop>
    )
  }
}

Wait, how does context work?

class Example extends Component {
  static contextTypes = {
    loop: PropTypes.object,
  };

  loop = () => {
    //Do stuff here
  };

  componentDidMount() {
    this.loopID = this.context.loop.subscribe(this.loop);
  }

  componentWillUnmount() {
    this.context.loop.unsubscribe(this.loopID);
  }
}

Scaling

How can we size and scale our game?

transform: scale()
Rounding errors on subpixel floats mean we have to manually round & scale. react-game-kit provides a Stage component to help with this.

class Game extends Component {
  render() {
    return (
      <Loop>
        <Stage>
          // Child components get this.context.scale
        </Stage>
      </Loop>
    )
  }
}

Most screens you are targeting will have a 16:9 aspect ratio.

class Game extends Component {
  render() {
    return (
      <Loop>
        <Stage width={1024} height={576}>
          // Child components get this.context.scale
        </Stage>
      </Loop>
    )
  }
}
getWrapperStyles() {
  const x = Math.round(this.state.x * this.context.scale);

  return {
    position: 'absolute',
    transform: `translate(${x}px, 0px) translateZ(0)`,
    transformOrigin: 'top left',
  };
}

That’s cool, but won’t my images be blurry?

Not if you pixelate them!

getImageStyles() {
  const scaledWidth = Math.round(this.props.width * this.context.scale);

  return {
    width: scaledWidth,
    imageRendering: 'pixelated'
  };
}

Sprites

So, how do sprites work?

Create a sprite map.

character-sprite
character-sprite-grid
character-gif
class Sprite extends Component {
  render() {
    return (
      <div style={{
        width: 64,
        height: 64,
        overflow: 'hidden',
        position: 'relative'
      }}>
        <img src={this.props.src} style={this.getImageStyles()}
      </div>
    )
  }
}
getImageStyles() {
  const left = this.state.step * tileWidth;
  const top = this.state.state * tileHeight;

  return {
    position: 'absolute',
    transform: `translate(-${left}px, -${top}px)`,
  };
}

react-game-kit provides a Sprite component to simplify this process.

return (
  <Sprite
    repeat={true}
    src="assets/character-sprite.png"
    scale={this.context.scale * 2}
    state={0}
    steps={[9, 9, 0, 4, 5]}
  />
);

Tilemaps

What in the world is a tilemap?

Tile maps use a tile atlas to use a few graphics to create a “level”.

Tile maps have tiles and layers.

boardwalk tile
buildings

Ok, so what does the map look like?

const tileMap = {
  rows: 4,
  columns: 8,
  layers: [
    [
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      1, 1, 1, 1, 1, 1, 1, 1,
    ],
  ],
};

Parsing a tile map

layers.forEach((l, index) => {
  const layer = [];
  for (let r = 0; r < rows; r++) { // Loop over rows
    for (let c = 0; c < columns; c++) { // Loop over columns
      const gridIndex = (r * columns) + c; // Get index in grid
      if (layer[gridIndex] !== 0) { // If it isn't 0
        layer.push({
          row: r,
          column: c,
          tileIndex: layer[gridIndex]
        })
      }
    }
  }
}
getTileStyles(column, row, size) {
  const left = column * size;
  const top = row * size;

  return {
    height: size,
    width: size,
    overflow: 'hidden',
    position: 'absolute',
    transform: `translate(${left}px, ${top}px)`,
  };
}

react-game-kit provides a TileMap component to simplify this process.

<TileMap
  src="assets/boardwalktile.png"
  tileSize={128}
  columns={24}
  rows={4}
  layers={[
    [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    ],
  ]}
/>
<TileMap
  style={{ top: Math.floor(-63 * this.context.scale) }}
  src="assets/buildings.png"
  rows={1}
  columns={6}
  tileSize={512}
  layers={[
    [1, 2, 3, 4, 5, 6],
  ]}
/>

Why not just make it one big image?

<TileMap
  src="atlas.png"
  columns={4}
  rows={2}
  renderTile={(tile, src, styles) => {
    if (tile.index === 2) {
      return <Body><img style={styles} src={src} /></Body>;
    }
    return <img style={styles} src={src} />;
  )}
  layers={[
    [
      0, 0, 2, 0,
      2, 0, 1, 1,
    ]
  ]}
/>

Physics Engine

You probably don’t need a physics engine.

// Update loop
const update = () => {
  if (rightKeyPressed) {
    character.x += 1;
  }
  if (leftKeyPressed) {
    character.x -= 1;
  }
};

But let’s say you do want physics. react-game-kit provides physics helpers provided by matter-js.

class Game extends Component {
  render() {
    return (
      <Loop>
        <Stage width={1024} height={576}>
          <World>
            // Children get this.context.engine
          </World>
        </Stage>
      </Loop>
    )
  }
}
class WorldExample extends Component {
  physicsInit = (engine) => {
    const ground = Matter.Bodies.rectangle(
      512, 448,
      1024, 64,
      {
        isStatic: true,
      },
    );

    Matter.World.addBody(engine.world, ground);
  }
  render() {
    return <World onInit={this.physicsInit}/>;
  }
}

When using matter-js physics, it’s important to do physics updates after the world has updated.

class WorldChild extends Component {
  update = () => {
    //Logic goes here
  }

  componentDidMount() {
    Matter.Events.on(this.context.engine, 'afterUpdate', this.update);
  }

  componentWillUnmount() {
    Matter.Events.off(this.context.engine, 'afterUpdate', this.update);
  }
}

Using physics bodies

class WorldChild extends Component {
  //...
  render() {
    return (
      <Body
        args={[0, 384, 64, 64, { inertia: Infinity }]}
        ref={(b) => { this.body = b; }}
      >
        <MySprite/>
      </Body>
    );
  }
}
class WorldChild extends Component {
  move = (body, x) => {
    Matter.Body.setVelocity(body, { x, y: 0 });
  };

  update = () => {
    const { body } = this.body;
    if (keys.isDown(keys.LEFT)) {
      this.move(body, -5);
    } else if (keys.isDown(keys.RIGHT)) {
      this.move(body, 5);
    }
  };
  //...
}

Performant use of physics data for positioning

mobx

import { observable } from 'mobx';

class GameStore {
  @observable characterPosition = { x: 0, y: 0 };
}

export default new GameStore();
import { observer } from 'mobx-react';

@observer
class WorldChild extends Component {
  move = (body, x) => {
    Matter.Body.setVelocity(body, { x, y: 0 });
  };

  update = () => {
    const { body } = this.body;
    if (keys.isDown(keys.LEFT)) {
      this.move(body, -5);
    } else if (keys.isDown(keys.RIGHT)) {
      this.move(body, 5);
    }
    store.characterPosition = body.position;
  };
  //...
}
import { observer } from 'mobx-react';

@observer
class Enemy extends Component {
  getStyle() {
    const {x, y} = store.characterPosition;

    return {
      position: 'absolute',
      transform: `translate(-${x}px, -${y}px)`,
    }
  }

  render() {
    return <div style={this.getStyle()} />
  }
}

Now go forth and build some dope games in React, and check out the source code for react-game-kit.


We Are Formidable

Formidable is a Seattle-based consultancy and open source shop, with an emphasis on Node.js and React.js. We deploy a mixture of consulting, staff augmentation, and training to level up teams and solve engineering problems. Whether it’s transitioning walmart.com to React, moving speedtest.net off Flash, or helping a startup build and scale an MVP, we’re ready to help teams of any size.

Interested in hiring or working for us? Get in touch or view our careers page.