See Ken’s slides from his React Next talk.
Introducing React Game Kit
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 ( // Child components get this.context.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 ( // Child components get this.context.scale ) } }
Most screens you are targeting will have a 16:9 aspect ratio.
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.
class Sprite extends Component { render() { return (![]({this.props.src})) } }
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 ( );
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.
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] }) } } } }
react-game-kit provides a TileMap component to simplify this process.
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)`, }; }
Why not just make it one big image?
if (tile.index === 2) { return ![]({src}); } return ![]({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 ( // Children get this.context.engine ) } }
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 ; } }
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 ( { this.body = b; }} > ); } }
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 } }
Now go forth and build some dope games in React, and check out the source code for react-game-kit.