Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Build a game using device sensors in React Native

  • Wern Ancheta
July 25th, 2019
You will need Node and React Native installed on your machine. Some knowledge of React Native development is expected.

In this tutorial, we’ll take a look at how you can get the device’s accelerometer data to create a simple dodge game.

Most modern smartphones are equipped with sensors such as the gyroscope, accelerometer, and magnetometer. These sensors are responsible for getting the data required for apps like the compass and your health app.

Prerequisites

You will need a good level of understanding of React Native, and familiarity with building and running apps in your development environment to follow this tutorial.

The following package versions are used to create the app:

  • Node 11.2
  • Yarn 1.13
  • React Native 0.59

If you have trouble running the app later on, try to use the versions above.

You will also need a real device for testing the app as you can’t really tilt in an emulator.

App overview

The app that we will create is a simple game of dodge. Blocks will be falling from the top part of the screen. The player will then have to slightly tilt their device to the left or to the right to move the ball so they can dodge the falling blocks.

Tilting the device to the right will make the ball go to the right, while tilting it to the left does the opposite. If the ball goes off all the way to the left or right where the player can’t see it, it automatically goes back to the middle part of the screen. The bottom part of the screen is where the floor is.

Once a block collides with the floor, it means that the player has successfully evaded it and their score will be incremented. At any point in the game, the player can also click on the RESET button to restart the game. We will be using React Native Sensors to get the sensor data, React Native Game Engine to implement the game, and MatterJS as the physics engine.

Here’s what the app will look like:

You can view the code used in this tutorial on its GitHub repo.

Bootstrapping the app

I’ve prepared a repo which you can clone in order to get the exact same package versions that I used for creating the app. Execute the following commands to bootstrap the app:

    git clone https://github.com/anchetaWern/RNSensorDemo.git
    cd RNSensorDemo
    git checkout starter
    yarn
    react-native eject

React Native Sensors is a native module, so you have to follow the additional steps in setting it up on their website.

Building the app

Once you’ve bootstrapped the app, update the App.js file at the root of the project directory and add the following. This will import all the packages we’ve installed:

    import React, { Component } from "react";
    import { StyleSheet, Text, View, Dimensions, Button, Alert } from "react-native";
    import {
      accelerometer,
      setUpdateIntervalForType,
      SensorTypes
    } from "react-native-sensors"; // for getting sensor data

    import { GameEngine } from "react-native-game-engine"; // for implementing the game
    import Matter from "matter-js"; // for implementing game physics (gravity, collision)

    import randomInt from "random-int"; // for generating random integer
    import randomColor from "randomcolor"; // for generating random hex color codes

Next, import the components for rendering the blocks and the ball. We will be creating these later:

    import Circle from "./src/components/Circle";
    import Box from "./src/components/Box";

Each of the blocks won’t be falling at the same rate, otherwise, it would be impossible for the player to dodge them all. MatterJS is responsible for implementing game physics. This way, all of the objects in the game (ball, blocks, and floor) will have their own physical attributes. One of the physical attributes which we can assign is the frictionAir. This allows us to define the air resistance of the object. The higher the value of this attribute, the faster it will travel through space. The getRandomDecimal helper allows us to generate a random value to make the blocks fall faster or slower. We will also create this later:

    import getRandomDecimal from "./src/helpers/getRandomDecimal";

Next, get the device’s height and width. We will be using those to calculate either the position or the dimensions of each of the objects. Below, we also calculate for the middle part of the screen. We’ll use this later on as the initial position for the ball, as well as the position it goes back to if it goes out of the visible area:

    const { height, width } = Dimensions.get('window');

    const BALL_SIZE = 20; // the ball's radius
    const DEBRIS_HEIGHT = 70; // the block's height
    const DEBRIS_WIDTH = 20; // the block's width

    const mid_point = (width / 2) - (BALL_SIZE / 2); // position of the middle part of the screen

Next, declare the physical attributes of the ball and blocks. The main difference between these two objects is that the ball is static. This means it cannot move on its own. It has to rely on the device’s accelerometer in order to calculate its new position. While the blocks are non-static, which means that they can be affected by physical phenomena such as gravity. This allows us to automatically make the blocks fall without actually doing anything:

    const ballSettings = {
      isStatic: true
    };

    const debrisSettings = { // blocks physical settings
      isStatic: false
    };

Next, create the bodies to be used for each of the objects. For now, we’re only creating the bodies for the ball and the floor. Because the blocks needs to have varying physical attributes and positioning, we’ll generate their corresponding bodies when the component is mounted:

    const ball = Matter.Bodies.circle(0, height - 30, BALL_SIZE, {
      ...ballSettings, // spread the object
      label: "ball" // add label as a property
    });

    const floor = Matter.Bodies.rectangle(width / 2, height, width, 10, {
      isStatic: true,
      isSensor: true,
      label: "floor"
    });

The code above uses the Matter.Bodies.Circle and Matter.Bodies.Rectangle methods from MatterJS to create a body with circular and rectangular frame. Both methods expect the x and y position of the body for the first and second arguments. While the third argument for the Circle is the radius, and the third and fourth argument for the Rectangle is the width and height of the body. The last argument is an object containing the object’s physical settings. A label is also added so we can easily tell each object apart when they collide.

Next, set the update interval for a specific sensor type. In this case, we’re using the accelerometer and we want to update every 15 milliseconds. This means that the function for getting the accelerometer data will only fire off every 15 milliseconds:

    setUpdateIntervalForType(SensorTypes.accelerometer, 15);

Note: For production apps, play around with the interval to come up with the best value to balance between the ball’s responsiveness and battery drain. 15 is just an arbitrary value I came up with during testing.

Next, create the main app component and initialize the state. The state is mainly used for setting the ball’s position and keeping track of the score:

    export default class App extends Component {

      state = {
        x: 0, // the ball's initial X position
        y: height - 30, // the ball's initial Y position
        isGameReady: false, // game is not ready by default
        score: 0 // the player's score
      }

      // next: add constructor

    }

Next, add the constructor. This contains the code for initializing the objects (also called entities) in the game and setting up the collision handler:

    constructor(props) {
      super(props);

      this.debris = [];

      const { engine, world } = this._addObjectsToWorld(ball);
      this.entities = this._getEntities(engine, world, ball);

      this._setupCollisionHandler(engine);

      this.physics = (entities, { time }) => {
        let engine = entities["physics"].engine; // get the reference to the physics engine
        engine.world.gravity.y = 0.5; // set the gravity of Y axis
        Matter.Engine.update(engine, time.delta); // move the game forward in time
        return entities;
      };
    }

    // next: add componentDidMount

Once the component is mounted, we subscribe to get the accelerometer data. In this case, we only need to get the data in the x axis because the ball is constrained to move only within the x axis. From there, we can set the ball’s current position by using the body’s setPosition method. All we have to do is add x to the current value of x in the state. This gives us the new position to be used for the ball:

    componentDidMount() {
      accelerometer.subscribe(({ x }) => {

        Matter.Body.setPosition(ball, {
          x: this.state.x + x, 
          y: height - 30 // should be constant
        });

        this.setState(state => ({
          x: x + state.x
        }), () => {
          // next: add code for resetting the ball's position if it goes out of view
        });

      });

      this.setState({
        isGameReady: true
      });
    }

    // next: add componentWillUnmount

If the ball goes off to the part of the screen which the user cannot see, we want to the bring it back to its initial position. That way, they can start controlling it again. this.state.x contains the current position of the ball, so we can simply check if its less than 0 (disappeared off to the left part of the screen) or greater than the device's width (disappeared off to the right part of the screen):

    if (this.state.x < 0 || this.state.x > width) {
      Matter.Body.setPosition(ball, {
        x: mid_point,
        y: height - 30
      });

      this.setState({
        x: mid_point
      });
    }

Next, unsubscribe from getting the accelerometer data once the component is unmounted. We don’t want to continuously drain the user’s battery if it’s no longer needed:

    componentWillUnmount() {
      this.accelerometer.stop();
    }

    // next: _addObjectsToWorld

Next, add the code for adding the objects to the world. Earlier, we already created the objects for the ball and the floor. But we’re still yet to create the objects for the blocks. The physics engine is still unaware of the ball and floor object, so we have to add them to the world. Here’s the code for that:

    _addObjectsToWorld = (ball) => {
      const engine = Matter.Engine.create({ enableSleeping: true });
      const world = engine.world;

      let objects = [
        ball,
        floor
      ];

      // create the bodies for the blocks
      for (let x = 0; x <= 5; x++) {
        const debris = Matter.Bodies.rectangle(
          randomInt(1, width - 30), // x position
          randomInt(0, 200), // y position
          DEBRIS_WIDTH,
          DEBRIS_HEIGHT,
          {
            frictionAir: getRandomDecimal(0.01, 0.5),
            label: 'debris'
          }
        );

        this.debris.push(debris);
      }

      objects = objects.concat(this.debris); // add the blocks to the array of objects 
      Matter.World.add(world, objects); // add the objects

      return {
        engine,
        world
      }
    }

    // next: add _getEntities

In the above code, we’re using MatterJS to create the physics engine. enableSleeping is set to true so that the engine will stop updating and collision tracking objects that have come to rest. This setting is mostly used as a performance boost. Once the engine is created, we create six rectangle bodies. These are the blocks (or debris) that will fall from the top part of the screen. Their initial y position and frictionAir will vary depending on the random numeric value that’s generated. Once all the blocks are generated, we add it to the array of objects and add them to the world.

Next, add the code for getting the entities to be rendered by React Native Game Engine. Note that each of these corresponds to a MatterJS object (ball, floor, and blocks). Each entity has a body, size, and renderer. The color we assigned to the gameFloor and debris is just passed to its renderer as a prop. As you’ll see in the code for the Box component later, the color is assigned as the background color:

    _getEntities = (engine, world, ball) => {
      const entities = {
        physics: {
          engine,
          world
        },

        playerBall: {
          body: ball,
          size: [BALL_SIZE, BALL_SIZE], // width, height
          renderer: Circle
        },

        gameFloor: {
          body: floor,
          size: [width, 10],
          color: '#414448',
          renderer: Box
        }
      };

      for (let x = 0; x <= 5; x++) { // generate the entities for the blocks
        Object.assign(entities, {
          ['debris_' + x]: {
            body: this.debris[x],
            size: [DEBRIS_WIDTH, DEBRIS_HEIGHT],
            color: randomColor({
              luminosity: 'dark', // only generate dark colors so they can be easily seen
            }),
            renderer: Box
          }
        });
      }

      return entities;
    }

    // next: _setupCollisionHandler

Next, add the code for setting up the collision handler. In the code below, we listen for the collisionStart event. This event is triggered when any of the objects in the world starts colliding. event.pairs stores the information on which objects have started colliding. If a block hits the floor, it means the player have successfully evaded it. We don’t really want to generate new objects as the game proceeds so we simply reuse the existing objects. We can do this by setting a new initial position, that way, it can start falling again. In the case that the ball hit a block, we loop through all the blocks and set them as a static object. This will have a similar effect to gravity being turned off, so the blocks are actually frozen in mid air. At this point, the game is considered over:

    _setupCollisionHandler = (engine) => {
      Matter.Events.on(engine, "collisionStart", (event) => {
        var pairs = event.pairs;

        var objA = pairs[0].bodyA.label;
        var objB = pairs[0].bodyB.label;

        if(objA === 'floor' && objB === 'debris') {
          Matter.Body.setPosition(pairs[0].bodyB, { // set new initial position for the block
            x: randomInt(1, width - 30),
            y: randomInt(0, 200)
          });

          // increment the player score
          this.setState(state => ({
            score: state.score + 1
          }));
        }

        if (objA === 'ball' && objB === 'debris') {
          Alert.alert('Game Over', 'You lose...');
          this.debris.forEach((debris) => {
            Matter.Body.set(debris, {
              isStatic: true
            });
          });
        }
      });
    }
    // next: add render

Next, render the UI. The GameEngine component from React Native Game Engine is used to render the entities that we’ve generated earlier. Inside it is the button for resetting the game, and a text for showing the player’s current score:

    render() {
      const { isGameReady, score } = this.state;

      if (isGameReady) {
        return (
          <GameEngine
            style={styles.container}
            systems={[this.physics]}
            entities={this.entities}
          >
            <View style={styles.header}>
              <Button
                onPress={this.reset}
                title="Reset"
                color="#841584"
              />
              <Text style={styles.scoreText}>{score}</Text>
            </View>
          </GameEngine>
        );
      }
      return null;
    }

    // next: add reset

Here’s the code for resetting the game:

    reset = () => {
      this.debris.forEach((debris) => { // loop through all the blocks
        Matter.Body.set(debris, {
          isStatic: false // make the block susceptible to gravity again
        });
        Matter.Body.setPosition(debris, { // set new position for the block
          x: randomInt(1, width - 30),
          y: randomInt(0, 200)
        });
      });

      this.setState({ 
        score: 0 // reset the player score
      });
    }

Lastly, here are the styles:

    const styles = StyleSheet.create({
      container: {
        flex: 1,
        backgroundColor: '#F5FCFF',
      },
      header: {
        padding: 20,
        alignItems: 'center'
      },
      scoreText: {
        fontSize: 25,
        fontWeight: 'bold'
      }
    });

Box component

Here’s the code for the Box component:

    // src/components/Box.js
    import React, { Component } from "react";
    import { View } from "react-native";

    const Box = ({ body, size, color }) => {
      const width = size[0];
      const height = size[1];

      const x = body.position.x - width / 2;
      const y = body.position.y - height / 2;

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

    export default Box;

Circle component

Here’s the code for the Circle component:

    // src/components/Circle.js
    import React, { Component } from "react";
    import { View, StyleSheet, Dimensions } from "react-native";

    const { height, width } = Dimensions.get('window');

    const BODY_DIAMETER = Math.trunc(Math.max(width, height) * 0.05);
    const BORDER_WIDTH = Math.trunc(BODY_DIAMETER * 0.1);

    const Circle = ({ body }) => {
      const { position } = body;
      const x = position.x - BODY_DIAMETER / 2;
      const y = position.y - BODY_DIAMETER / 2;
      return <View style={[styles.head, { left: x, top: y }]} />;
    };

    export default Circle;

    const styles = StyleSheet.create({
      head: {
        backgroundColor: "#FF5877",
        borderColor: "#FFC1C1",
        borderWidth: BORDER_WIDTH,
        width: BODY_DIAMETER,
        height: BODY_DIAMETER,
        position: "absolute",
        borderRadius: BODY_DIAMETER * 2
      }
    });

Random decimal helper

Here’s the code for generating a random decimal:

    // src/helpers/getRandomDecimal.js
    const getRandomDecimal = (min, max) => {
      return Math.random() * (max - min) + min;
    }

    export default getRandomDecimal;

Running the app

At this point, you should be able to run the app and play the game:

    react-native run-android
    react-native run-ios

Conclusion

In this tutorial, you learned how to get the device’s accelerometer data from a React Native app and use it to control the ball.

You can view the code used in this tutorial on its GitHub repo.

Clone the project repository
  • Android
  • Gaming
  • iOS
  • JavaScript
  • React
  • React Native
  • no pusher tech

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.