Flappy Bird With React-Native-Game-Engine and Matter.js

Recreating the annoying viral hit game

Tamas Szikszai
Sep 4 · 11 min read

Flappy Bird is one of those games that most people know about, even if they have never played it. It was originally released in May 2013 by a Vietnamese developer, Dong Nguyen, but didn’t become popular until early 2014, when it skyrocketed into the number one position of the iOS App Store.

The game is undeniably addictive and annoying at the same time, but the bigger question is: can it be made in React Native? The answer is obviously yes, otherwise this post wouldn’t exist!

TL;DR #1: Prefer watching it in a video format?

TL;DR #2: Just want the code? Here you go: https://github.com/lepunk/react-native-videos/tree/master/FlappyBird

To recreate this game I decided to use the react-native-game-engine (RNGE) again. If you haven’t done it yet, consider reading my previous post as an introduction to the engine. However, unlike Snake, Flappy Bird features some basic physics — the react-native-game-engine alone won’t cut it.

Enter Matter.js, a well established 2d physics engine for Javascript:

npm install react-native-game-engine matter-js --save

Just like that we have everything we need to make a basic version of Flappy Bird.

As usual, let’s start by setting up some constants:

import { Dimensions } from 'react-native';

export default Constants = {
MAX_WIDTH: Dimensions.get("screen").width,
MAX_HEIGHT: Dimensions.get("screen").height,
GAP_SIZE: 200, // gap between the two parts of the pipe
PIPE_WIDTH: 100 // width of the pipe
}

Next we need to set up our “entities” and “systems” for RNGE. At first let’s just create our world and draw a “Bird” which will be represented by a bright red square.

Our App.js will look something like this:

import React, { Component } from 'react';
import { StyleSheet, View, } from 'react-native';
import Matter from "matter-js";
import { GameEngine } from "react-native-game-engine";
import Bird from './Bird';
import Constants from './Constants';


export default class App extends Component {
constructor(props){
super(props);

this.state = {
running: true
};

this.gameEngine = null;

this.entities = this.setupWorld();
}

setupWorld = () => {
let engine = Matter.Engine.create({ enableSleeping: false });
let world = engine.world;

let bird = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 4, Constants.MAX_HEIGHT / 2, 50, 50);

Matter.World.add(world, [bird]);


return {
physics: { engine: engine, world: world },
bird: { body: bird, size: [50, 50], color: 'red', renderer: Bird},
}
}


render() {
return (
<View style={styles.container}>
<GameEngine
ref={(ref) => { this.gameEngine = ref; }}
style={styles.gameContainer}
running={this.state.running}
entities={this.entities}>
<StatusBar hidden={true} />
</GameEngine>
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
gameContainer: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
});

The important parts here are in the setupWorld method.

let engine = Matter.Engine.create({ enableSleeping: false });
let world = engine.world;
let bird = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 4, Constants.MAX_HEIGHT / 2, 50, 50);Matter.World.add(world, [bird]);

We create a new Matter “engine”. Then we create a bird “body” wìth a 50x50 virtual size, centered vertically and in the first 25% of the screen horizontally. Finally we add the bird body to the world we created.

Important to note that Matter won’t handle the rendering of our bird, it will only calculate it’s position on the screen. The rendering will happen on each tick by React Native and RNGE, defined in setupWorld’s return statement:

return {
physics: { engine: engine, world: world },
bird: { body: bird, size: [50, 50], color:'red',renderer: Bird},
}

In order to see something we need to create a Bird.js component, which will look something like this:

There’s nothing fancy here. The only thing I want to point out are x and y values. We’re extracting this from the bird’s body prop, which is managed by Matter.js.

Great. After over a 100 lines of code we have a red square on a white screen. Now what?

The world we created in Matter has a gravity by default, which is set at 1.0. In order to make it work, we need to call the update method on the engine periodically so Matter can recalculate each body’s position. As discussed in my previous post RNGE provides a convenient way to call a set of functions periodically, called “systems”.

Let’s modify our App.js by adding an include:

import Physics from './Physics';

and adding the Physics system to our GameEngine

<GameEngine
ref={(ref) => { this.gameEngine = ref; }}
style={styles.gameContainer}
running={this.state.running}
systems={[Physics]}
entities={this.entities}>
</GameEngine>

All we need now is a Physics.js file that will be called on every tick

Now we’re getting somewhere!

Ok, let’s add a ceiling and a floor. Change our setupWorld() method in App.js to this:

setupWorld = () => {
let engine = Matter.Engine.create({ enableSleeping: false });
let world = engine.world;

let bird = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 4, Constants.MAX_HEIGHT / 2, 50, 50);
let floor = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 2, Constants.MAX_HEIGHT - 25, Constants.MAX_WIDTH, 50, { isStatic: true });
let ceiling = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 2, 25, Constants.MAX_WIDTH, 50, { isStatic: true });

Matter.World.add(world, [bird, floor, ceiling]);

return {
physics: { engine: engine, world: world },
bird: { body: bird, size: [50, 50], color: 'red', renderer: Bird},
floor: { body: floor, size: [Constants.MAX_WIDTH, 50], color: "green", renderer: Wall },
ceiling: { body: ceiling, size: [Constants.MAX_WIDTH, 50], color: "green", renderer: Wall },
}
}

All we’re doing is adding two new bodies to the Matter world: floor and ceiling. They are similar to bird, except their isStatic attribute is true, which tells Matter that physics does not affect them.

Their renderer is called Wall — let’s implement it:

import React, { Component } from "react";
import { View } from "react-native";

export default class Wall extends Component {
render() {
const width = this.props.size[0];
const height = this.props.size[1];
const x = this.props.body.position.x - width / 2;
const y = this.props.body.position.y - height / 2;

return (
<View
style={{
position: "absolute",
left: x,
top: y,
width: width,
height: height,
backgroundColor: this.props.color
}} />
);
}
}

The code for Wall is actually exactly the same as Bird — they could technically be the same component — but I like to keep things separate.

At this point we have a red square free-falling to a green rectangle. Let’s implement something more exciting. If the user taps anywhere on the screen we want our bird to change direction for a moment and start bouncing upwards.

In order to achieve this we have to modify Physics.js:

import Matter from "matter-js";

const Physics = (entities, { touches, time }) => {
let engine = entities.physics.engine;
let bird = entities.bird.body;

touches.filter(t => t.type === "press").forEach(t => {
Matter.Body.applyForce( bird, bird.position, {x: 0.00, y: -0.10});
});

Matter.Engine.update(engine, time.delta);

return entities;
};

export default Physics;

OK, what’s happening here?

  • RNGE conveniently passes in all the touch events to each of the systems passed in to the engine.
  • Since we only care about “press” events (taps) we need to filter these events.
  • Whenever we come across a tap we apply a force to our Bird’s center with -0.10 (slightly upwards).

Things are shaping up nicely. Let’s add some obstacles!

In Flappy Bird the obstacles are represented by pipes. One from the top and one from the bottom with a constant gap between them. I will add a total of two sets of pipes (four pipe pieces in total) on the screen initially. One at the right side of the screen (Constants.MAX_WIDTH-Constants.PIPE_WIDTH / 2) and on one screen further towards the right (Constants.MAX_WIDTH * 2-(Constants.PIPE_WIDTH / 2)

Next we move these pipe pieces one pixel towards the left on each tick. If a set of pipe moves out of the screen we bump them one screen's worth of pixels to the right.

The pipe’s Y position should be random so we define two functions in App.js (but outside of the component definition as these could be later reused).

export const randomBetween = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}

export const generatePipes = () => {
let topPipeHeight = randomBetween(100, (Constants.MAX_HEIGHT / 2) - 100);
let bottomPipeHeight = Constants.MAX_HEIGHT - topPipeHeight - Constants.GAP_SIZE;

let sizes = [topPipeHeight, bottomPipeHeight]

if (Math.random() < 0.5) {
sizes = sizes.reverse();
}


return sizes;
}

All this really does is generate an array of two numbers: one for the height of the top pipe, one for the bottom pipe.

We can now update our setupWorld method:

setupWorld = () => {
let engine = Matter.Engine.create({ enableSleeping: false });
let world = engine.world;

let bird = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 4, Constants.MAX_HEIGHT / 2, 50, 50);
let floor = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 2, Constants.MAX_HEIGHT - 25, Constants.MAX_WIDTH, 50, { isStatic: true });
let ceiling = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 2, 25, Constants.MAX_WIDTH, 50, { isStatic: true });

let [pipe1Height, pipe2Height] = generatePipes();

let pipe1 = Matter.Bodies.rectangle( Constants.MAX_WIDTH - (Constants.PIPE_WIDTH / 2), pipe1Height / 2, Constants.PIPE_WIDTH, pipe1Height, { isStatic: true });
let pipe2 = Matter.Bodies.rectangle( Constants.MAX_WIDTH - (Constants.PIPE_WIDTH / 2), Constants.MAX_HEIGHT - (pipe2Height / 2), Constants.PIPE_WIDTH, pipe2Height, { isStatic: true });

let [pipe3Height, pipe4Height] = generatePipes();

let pipe3 = Matter.Bodies.rectangle( Constants.MAX_WIDTH * 2 - (Constants.PIPE_WIDTH / 2), pipe3Height / 2, Constants.PIPE_WIDTH, pipe3Height, { isStatic: true });
let pipe4 = Matter.Bodies.rectangle( Constants.MAX_WIDTH * 2 - (Constants.PIPE_WIDTH / 2), Constants.MAX_HEIGHT - (pipe4Height / 2), Constants.PIPE_WIDTH, pipe4Height, { isStatic: true });


Matter.World.add(world, [bird, floor, ceiling, pipe1, pipe2, pipe3, pipe4]);

return {
physics: { engine: engine, world: world },
bird: { body: bird, size: [50, 50], color: 'red', renderer: Bird},
floor: { body: floor, size: [Constants.MAX_WIDTH, 50], color: "green", renderer: Wall },
ceiling: { body: ceiling, size: [Constants.MAX_WIDTH, 50], color: "green", renderer: Wall },
pipe1: { body: pipe1, size: [Constants.PIPE_WIDTH, pipe1Height], color: "green", renderer: Wall },
pipe2: { body: pipe2, size: [Constants.PIPE_WIDTH, pipe2Height], color: "green", renderer: Wall },
pipe3: { body: pipe3, size: [Constants.PIPE_WIDTH, pipe3Height], color: "green", renderer: Wall },
pipe4: { body: pipe4, size: [Constants.PIPE_WIDTH, pipe4Height], color: "green", renderer: Wall }
}
}

Let’s update Physics.js to move these pipes:

import Matter from "matter-js";

const Physics = (entities, { touches, time }) => {
let engine = entities.physics.engine;
let bird = entities.bird.body;

touches.filter(t => t.type === "press").forEach(t => {
Matter.Body.applyForce( bird, bird.position, {x: 0.00, y: -0.10});
});

for(let i=1; i<=4; i++){
if (entities["pipe" + i].body.position.x <= -1 * (Constants.PIPE_WIDTH / 2)){
Matter.Body.setPosition( entities["pipe" + i].body, {x: Constants.MAX_WIDTH * 2 - (Constants.PIPE_WIDTH / 2), y: entities["pipe" + i].body.position.y});
} else {
Matter.Body.translate( entities["pipe" + i].body, {x: -1, y: 0});
}
}

Matter.Engine.update(engine, time.delta);

return entities;
};

export default Physics;

As I said, on every tick we move the pipes 1px to the left. If the pipe goes out of sight we move it to the right, giving the user the sense of an infinite number of pipes.

This is all well and good but not exactly challenging, considering the player can’t fail. Adding collision detection with Matter.js is simple.

In our setupWorld method add these lines

Matter.Events.on(engine, 'collisionStart', (event) => {
var pairs = event.pairs;
this.gameEngine.dispatch({ type: "game-over"});
});

This sets up an event listener, which will be triggered on the “collisionStart” event emitted by Matter. The listener will emit another event with type “game-over” using RNGE’s dispatch method

With this, all that's left to do is to listen to this event and display a game over screen if the user fails. You can listen to RNGE events by adding an onEvent prop to your <GameEngine>

Your final App.js should look something like this:

import React, { Component } from 'react';
import { Dimensions, StyleSheet, Text, View, StatusBar, Alert, TouchableOpacity } from 'react-native';
import Matter from "matter-js";
import { GameEngine } from "react-native-game-engine";
import Bird from './Bird';
import Wall from './Wall';
import Physics from './Physics';
import Constants from './Constants';

export const randomBetween = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}

export const generatePipes = () => {
let topPipeHeight = randomBetween(100, (Constants.MAX_HEIGHT / 2) - 100);
let bottomPipeHeight = Constants.MAX_HEIGHT - topPipeHeight - Constants.GAP_SIZE;

let sizes = [topPipeHeight, bottomPipeHeight]

if (Math.random() < 0.5) {
sizes = sizes.reverse();
}


return sizes;
}

export default class App extends Component {
constructor(props){
super(props);

this.state = {
running: true
};

this.gameEngine = null;

this.entities = this.setupWorld();
}

setupWorld = () => {
let engine = Matter.Engine.create({ enableSleeping: false });
let world = engine.world;
world.gravity.y = 1.2;

let bird = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 4, Constants.MAX_HEIGHT / 2, 50, 50);
bird.restitution = 20;
let floor = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 2, Constants.MAX_HEIGHT - 25, Constants.MAX_WIDTH, 50, { isStatic: true });
let ceiling = Matter.Bodies.rectangle( Constants.MAX_WIDTH / 2, 25, Constants.MAX_WIDTH, 50, { isStatic: true });

let [pipe1Height, pipe2Height] = generatePipes();

let pipe1 = Matter.Bodies.rectangle( Constants.MAX_WIDTH - (Constants.PIPE_WIDTH / 2), pipe1Height / 2, Constants.PIPE_WIDTH, pipe1Height, { isStatic: true });
let pipe2 = Matter.Bodies.rectangle( Constants.MAX_WIDTH - (Constants.PIPE_WIDTH / 2), Constants.MAX_HEIGHT - (pipe2Height / 2), Constants.PIPE_WIDTH, pipe2Height, { isStatic: true });

let [pipe3Height, pipe4Height] = generatePipes();

let pipe3 = Matter.Bodies.rectangle( Constants.MAX_WIDTH * 2 - (Constants.PIPE_WIDTH / 2), pipe3Height / 2, Constants.PIPE_WIDTH, pipe3Height, { isStatic: true });
let pipe4 = Matter.Bodies.rectangle( Constants.MAX_WIDTH * 2 - (Constants.PIPE_WIDTH / 2), Constants.MAX_HEIGHT - (pipe4Height / 2), Constants.PIPE_WIDTH, pipe4Height, { isStatic: true });

Matter.World.add(world, [bird, floor, ceiling, pipe1, pipe2, pipe3, pipe4]);
Matter.Events.on(engine, 'collisionStart', (event) => {
var pairs = event.pairs;

this.gameEngine.dispatch({ type: "game-over"});

});

return {
physics: { engine: engine, world: world },
floor: { body: floor, size: [Constants.MAX_WIDTH, 50], color: "green", renderer: Wall },
ceiling: { body: ceiling, size: [Constants.MAX_WIDTH, 50], color: "green", renderer: Wall },
bird: { body: bird, size: [50, 50], color: 'red', renderer: Bird},
pipe1: { body: pipe1, size: [Constants.PIPE_WIDTH, pipe1Height], color: "green", renderer: Wall },
pipe2: { body: pipe2, size: [Constants.PIPE_WIDTH, pipe2Height], color: "green", renderer: Wall },
pipe3: { body: pipe3, size: [Constants.PIPE_WIDTH, pipe3Height], color: "green", renderer: Wall },
pipe4: { body: pipe4, size: [Constants.PIPE_WIDTH, pipe4Height], color: "green", renderer: Wall }
}
}

onEvent = (e) => {
if (e.type === "game-over"){
//Alert.alert("Game Over");
this.setState({
running: false
});
}
}

reset = () => {
this.gameEngine.swap(this.setupWorld());
this.setState({
running: true
});
}

render() {
return (
<View style={styles.container}>
<GameEngine
ref={(ref) => { this.gameEngine = ref; }}
style={styles.gameContainer}
systems={[Physics]}
running={this.state.running}
onEvent={this.onEvent}
entities={this.entities}>
<StatusBar hidden={true} />
</GameEngine>
{!this.state.running && <TouchableOpacity style={styles.fullScreenButton} onPress={this.reset}>
<View style={styles.fullScreen}>
<Text style={styles.gameOverText}>Game Over</Text>
</View>
</TouchableOpacity>}
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
gameContainer: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
gameOverText: {
color: 'white',
fontSize: 48
},
fullScreen: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'black',
opacity: 0.8,
justifyContent: 'center',
alignItems: 'center'
},
fullScreenButton: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
flex: 1
}
});

There you have it: an ugly, barely functional flappy bird clone.

Couple of “to do”s to make things better (if you feel up for it):

  • Currently the pipes are repeating. It should be easy to change their Y position each time we send them back to the right side of the screen
  • The bird should slightly rotate up / down depending the direction it is heading to
  • Some images would make things slightly more exciting (no offence to red squares)

Please feel free to send a PR/fork the repo if you feel like you can improve it.

Better Programming

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade