Felix Kling

Felix Kling

1552716206

How to Create a two-player maze game with React Native?

#react-native #game-development #game-engine

What is GEEK

Buddha Community

John David

1552717291

Prerequisites

Basic knowledge of React Native is required.

We’ll be using the following package versions. If you encounter any issues getting the app to work, try using the following:

  • Yarn 1.13.0
  • React Native 0.58.4

We’ll also be using ngrok to expose the Pusher auth server to the internet. Lastly, you’ll need a Pusher app instance which has client events enabled.

App overview

As mentioned earlier, we will create a maze game in which two players have to navigate. When the app starts, they will be greeted with a login screen. This is where they enter a unique username and wait for an opponent:

Once there are two players, a Pusher event is manually triggered by accessing a specific route of the app’s server component. This event informs both users that an opponent is found. This serves as the cue for the app to automatically navigate to the game screen where the maze will be generated.

After that, the event for starting the game is also manually triggered. Once the app receives that, it will inform the players that they can start navigating the maze. The first player to reach the goal wins the game.

Here’s what the app looks like. The black square is the goal, the pink circle is the current player, and the blue circle is their opponent:

You can view the code on this GitHub repo.

Building the app

Start by cloning the repo:

    git clone https://github.com/anchetaWern/RNMaze.git
    cd RNMaze


Next, switch to the starter branch and install the dependencies:

    git checkout starter
    yarn


This branch has the styling and navigation already set up so we don’t have to go through them in this tutorial.

Next, update the .env file with your Pusher app credentials:

    PUSHER_APP_KEY="YOUR PUSHER APP KEY"
    PUSHER_APP_CLUSTER="YOUR PUSHER APP CLUSTER"


Login screen

Let’s start by adding the code for the login screen:

    // src/screens/Login.js
    import React, { Component } from "react";
    import { View, Text, TextInput, TouchableOpacity, Alert } from "react-native";

    import Pusher from "pusher-js/react-native";
    import Config from "react-native-config"; // for reading the .env file

    const pusher_app_key = Config.PUSHER_APP_KEY;
    const pusher_app_cluster = Config.PUSHER_APP_CLUSTER;
    const base_url = "YOUR HTTPS NGROK URL";

    class LoginScreen extends Component {
      static navigationOptions = {
        title: "Login"
      };

      state = {
        username: "",
        enteredGame: false
      };

      constructor(props) {
        super(props);
        this.pusher = null;
        this.myChannel = null; // the current user's Pusher channel
        this.opponentChannel = null; // the opponent's Pusher channel
      }

      // next: add render()
    }

Next, render the UI for the login screen. This asks for the user’s username so they can log in:

    render() {
      return (
        <View style={styles.wrapper}>
          <View style={styles.container}>
            <View style={styles.main}>
              <View>
                <Text style={styles.label}>Enter your username</Text>
                <TextInput
                  style={styles.textInput}
                  onChangeText={username => this.setState({ username })}
                  value={this.state.username}
                />
              </View>

              {!this.state.enteredGame && (
                <TouchableOpacity onPress={this.enterGame}>
                  <View style={styles.button}>
                    <Text style={styles.buttonText}>Login</Text>
                  </View>
                </TouchableOpacity>
              )}

              {this.state.enteredGame && (
                <Text style={styles.loadingText}>Loading...</Text>
              )}
            </View>
          </View>
        </View>
      );
    }

Once the Login button is clicked, the enterGame function is executed. This authenticates the user with Pusher via an auth endpoint in the app’s server component (we’ll create this later). From the prerequisites section, one of the requirements is that your Pusher app instance needs to have client events enabled. This authentication step is a required step for using the client events feature. This allows us to trigger events directly from the app itself:

    enterGame = async () => {
      const myUsername = this.state.username;

      if (myUsername) {
        this.setState({
          enteredGame: true // show login activity indicator
        });

        this.pusher = new Pusher(pusher_app_key, {
          authEndpoint: `${base_url}/pusher/auth`,
          cluster: pusher_app_cluster,
          auth: {
            params: { username: myUsername }
          },
          encrypted: true
        });

        // next: add code for subscribing the user to their own channel

      }
    };

Next, subscribe the user to their own channel. The username they entered is used for this. This channel is what’s used by their opponent to pass messages to them in realtime:

    this.myChannel = this.pusher.subscribe(`private-user-${myUsername}`);
    this.myChannel.bind("pusher:subscription_error", status => {
      Alert.alert(
        "Error",
        "Subscription error occurred. Please restart the app"
      );
    });

    // next: add code for when subscription succeeds

When the subscription succeeds, we wait for the opponent-found event to get triggered by the server. When this happens, we determine who among the players is the first player (Player One) and assign the ball color based on that. From here, we also subscribe to the opponent’s channel. Once it succeeds, we navigate to the game screen with a few data we’re going to need:

    this.myChannel.bind("pusher:subscription_succeeded", () => {

      this.myChannel.bind("opponent-found", data => {
        const opponentUsername =
          myUsername == data.player_one ? data.player_two : data.player_one;

        const isPlayerOne = myUsername == data.player_one ? true : false;

        const ballColor = (isPlayerOne) ? 'pink' : 'blue'; // pink ball always goes to the first player

        Alert.alert("Opponent found!", `Use the ${ballColor} ball`);

        this.opponentChannel = this.pusher.subscribe(
          `private-user-${opponentUsername}`
        );
        this.opponentChannel.bind("pusher:subscription_error", data => {
          console.log("Error subscribing to opponent's channel: ", data);
        });

        this.opponentChannel.bind("pusher:subscription_succeeded", () => {
          this.props.navigation.navigate("Game", {
            pusher: this.pusher,
            isPlayerOne: isPlayerOne,
            myUsername: myUsername,
            myChannel: this.myChannel,
            opponentUsername: opponentUsername,
            opponentChannel: this.opponentChannel
          });
        });

        this.setState({
          username: "",
          enteredGame: false // hides the login activity indicator
        });
      });
    });

Game screen

Now we’re ready to add the code for the Game screen. Start by importing the packages, components, and helpers we need:

    // src/screens/Game.js
    import React, { PureComponent } from "react";
    import { View, Text, Alert, ActivityIndicator } from "react-native";

    import Matter from "matter-js"; // physics engine, collision detection
    import { GameEngine } from "react-native-game-engine"; // rendering game objects

    import Circle from '../components/Circle'; // renderer for the balls
    import Rectangle from '../components/Rectangle'; // renderer for the maze walls

    import CreateMaze from '../helpers/CreateMaze'; // for generating the maze
    import GetRandomPoint from '../helpers/GetRandomPoint'; // for getting a random point in the grid

    // the hardcoded width and height contraints of the app
    import dimensions from '../data/constants';
    const { width, height } = dimensions;

Next, create the constants file (src/data/constants.js) we used above:

    const constants = {
      width: 360,
      height: 686.67
    }

    export default constants;

Next, go back to the game screen (src/screens/Game.js) and initialize the physics settings for the ball as well as the goal size:

    const BALL_SIZE = Math.floor(width * .02);
    const ballSettings = {
      inertia: 0,
      friction: 0,
      frictionStatic: 0,
      frictionAir: 0,
      restitution: 0,
      density: 1
    };

    const GOAL_SIZE = Math.floor(width * .04); 

You can find what each of the ball settings does here.

Next, create the maze. As you’ll see later, this generates a composite body which makes up the walls of the maze:

    const GRID_X = 15; // the number of cells in the X axis
    const GRID_Y = 18; // the number of cells in the Y axis

    const maze = CreateMaze(GRID_X, GRID_Y);

Next, create the Game component. Inside the constructor, get the navigation params that were passed earlier from the Login screen:

    export default class Game extends PureComponent {

      static navigationOptions = {
        header: null
      };

      state = {
        isMazeReady: false, // whether to show the maze or not
        isGameFinished: false // whether someone has already reached the goal or not
      }

      constructor(props) {
        super(props);

        const { navigation } = this.props;

        this.pusher = navigation.getParam('pusher');
        this.myUsername = navigation.getParam('myUsername');
        this.opponentUsername = navigation.getParam('opponentUsername');

        this.myChannel = navigation.getParam('myChannel');
        this.opponentChannel = navigation.getParam('opponentChannel');
        this.isPlayerOne = navigation.getParam('isPlayerOne');

        // next: add code for adding the entities
      }
    }

Next, we need to construct the object containing the entities to be rendered by the React Native Game Engine. In this game, there are four entities we need to render, three of them are physical (two balls, one goal), while the other is logical (physics). Since there is a need to mirror the objects (and their positions) in both player instances, we first generate the objects in player one’s instance. Once the objects are generated, we send the object’s position to player two via Pusher:

    this.entities = {};

    if (this.isPlayerOne) {
      const ballOneStartPoint = GetRandomPoint(GRID_X, GRID_Y); // generate a random point to put the pink ball
      const ballTwoStartPoint = GetRandomPoint(GRID_X, GRID_Y); // generate a random point to put the blue ball

      const ballOne = this._createBall(ballOneStartPoint, 'ballOne'); // create the pink ball (for player one)
      const ballTwo = this._createBall(ballTwoStartPoint, 'ballTwo'); // create the blue ball (for player two)

      this.myBall = ballOne;
      this.myBallName = 'ballOne'; 
      this.opponentBall = ballTwo;
      this.opponentBallName = 'ballTwo';

      const goalPoint = GetRandomPoint(GRID_X, GRID_Y); // generate a random goal point
      const goal = this._createGoal(goalPoint); // create the goal box

      const { engine, world } = this._addObjectsToWorld(maze, ballOne, ballTwo, goal); // add all the objects to the world

      this.entities = this._getEntities(engine, world, maze, ballOne, ballTwo, goal); // get the entities of the game

      this._setupPositionUpdater(); // call the interval timer for updating the opponent of the current user's ball position
      this._setupGoalListener(engine); // setup the sensor for listening if a ball has touched the goal

      // send the position of the generated objects to player two
      this.opponentChannel.trigger('client-generated-objects', {
        ballOneStartPoint,
        ballTwoStartPoint,
        goalPoint
      });
    }

If the second player is the one who’s logged in, the following event is triggered. This contains the positions for the two balls and the goal. Using this data, we construct player two’s world:

    this.myChannel.bind('client-generated-objects', ({ ballOneStartPoint, ballTwoStartPoint, goalPoint }) => {

      const ballOne = this._createBall(ballOneStartPoint, 'ballOne');
      const ballTwo = this._createBall(ballTwoStartPoint, 'ballTwo');
      const goal = this._createGoal(goalPoint);

      this.myBall = ballTwo;
      this.myBallName = 'ballTwo';
      this.opponentBall = ballOne;
      this.opponentBallName = 'ballOne';

      const { engine, world } = this._addObjectsToWorld(maze, ballOne, ballTwo, goal);

      this.entities = this._getEntities(engine, world, maze, ballOne, ballTwo, goal);

      this._setupPositionUpdater();
      this._setupGoalListener(engine);
    });

Next, we add physics to the world. By default, MatterJS applies gravity to the world. We don’t really want that so we set the gravity to zero for both X and Y axis:

    this.physics = (entities, { time }) => {
      let engine = entities["physics"].engine;
      engine.world.gravity = {
        x: 0,
        y: 0
      };
      Matter.Engine.update(engine, time.delta);
      return entities;
    };

    // next: add this.moveBall

Next, we add the system for moving the ball. This filters move events. This event is triggered when the user moves their finger across the screen. Note that this listens for that event on the entire screen so the user doesn’t actually need to place their finger directly on top of the ball in order to move it. As you can see, this function specifically targets this.myBall. this.myBall.position contains the current position of the ball, while move.delta contains the data on how much the finger was moved across the screen. We add that up to the ball’s current position in order to move it to that direction:

    this.moveBall = (entities, { touches }) => {
      let move = touches.find(x => x.type === "move");
      if (move) {
        // move.delta.pageX is negative if moving fingers to the left
        // move.delta.pageX is negative if moving fingers to the top
        const newPosition = {
          x: this.myBall.position.x + move.delta.pageX, 
          y: this.myBall.position.y + move.delta.pageY
        };
        Matter.Body.setPosition(this.myBall, newPosition);
      }

      return entities;
    };

    // next: listen for the start-game event

Next, listen for the event for starting the game. All we do here is show an alert and update the state so it shows the generated maze instead of the activity indicator:

    this.myChannel.bind('start-game', () => {
      Alert.alert('Game Start!', 'You may now navigate towards the black square.');
      this.setState({
        isMazeReady: true
      });
    });
    // next: listen for client-moved-ball

Next, listen for the event for moving the opponent’s ball:

    this.myChannel.bind('client-moved-ball', ({ positionX, positionY }) => {
      Matter.Body.setPosition(this.opponentBall, {
        x: positionX,
        y: positionY
      });
    });

That’s pretty much all there is to it for the Game screen. Let’s now go over the functions we used for constructing the world.

First is the function for creating a ball. This accepts the ball’s start point and the name you want to assign to it. The name is very important here because it’s what we use to determine which ball touched the goal:

    constructor(props) {
      // ...
    }

    _createBall = (startPoint, name) => {
      const ball = Matter.Bodies.circle(
        startPoint.x,
        startPoint.y,
        BALL_SIZE,
        {
          ...ballSettings,
          label: name
        }
      );
      return ball;
    }

Next is the function for creating the goal box. Not unlike the ball, we don’t need to add a whole lot of physics settings to the goal. That’s because it only acts as a sensor. It gets rendered to the world, but it doesn’t actually interact or affect the rest of it (For example, the ball shouldn’t bounce if it touches it, nor does it move because of the force applied by the ball). The key setting here is isSensor: true:

    _createGoal = (goalPoint) => {
      const goal = Matter.Bodies.rectangle(goalPoint.x, goalPoint.y, GOAL_SIZE, GOAL_SIZE, {
        isStatic: true,
        isSensor: true,
        label: 'goal'
      });
      return goal;
    }

Next is the function for adding the objects to the world. Aside from the two balls and the goal, we also need to add the maze that we generated earlier:

    _addObjectsToWorld = (maze, ballOne, ballTwo, goal) => {
      const engine = Matter.Engine.create({ enableSleeping: false }); // enableSleeping tells the engine to stop updating and collision checking bodies that have come to rest
      const world = engine.world;

      Matter.World.add(world, [
        maze, ballOne, ballTwo, goal
      ]);

      return {
        engine,
        world
      }
    }

Next is the _getEntities function. This is responsible for constructing the entities object that we need to pass to the React Native Game Engine. This includes the physics, the two balls, the goal, and the maze walls. All of these objects (except for the physics), requires the body and renderer. All the other options are simply passed as a prop to the renderer to customize its style (bgColor, size, borderColor):

    _getEntities = (engine, world, maze, ballOne, ballTwo, goal) => {
      const entities = {
        physics: {
          engine,
          world
        },
        playerOneBall: {
          body: ballOne,
          bgColor: '#FF5877',
          borderColor: '#FFC1C1',
          renderer: Circle
        },
        playerTwoBall: {
          body: ballTwo,
          bgColor: '#458ad0',
          borderColor: '#56a4f3',
          renderer: Circle
        },
        goalBox: {
          body: goal,
          size: [GOAL_SIZE, GOAL_SIZE],
          color: '#414448',
          renderer: Rectangle
        }
      };

      const walls = Matter.Composite.allBodies(maze); // get the children of the composite body
      walls.forEach((body, index) => {

        const { min, max } = body.bounds;
        const width = max.x - min.x;
        const height = max.y - min.y;

        Object.assign(entities, {
          ['wall_' + index]: {
            body: body,
            size: [width, height],
            color: '#fbb050',
            renderer: Rectangle
          }
        });
      });

      return entities; 
    }

The _setupPositionUpdater function triggers the event for updating the current user’s ball position on their opponent’s side. We can actually do this inside the system for moving the ball (this.moveBall) but that gets called multiple times over a span of a few milliseconds so it’s not really recommended. Also, make sure to only execute it if no one has reached the goal yet:

    _setupPositionUpdater = () => {
      setInterval(() => {
        if (!this.state.isGameFinished) { // nobody has reached the goal yet
          this.opponentChannel.trigger('client-moved-ball', {
            positionX: this.myBall.position.x,
            positionY: this.myBall.position.y
          });
        }
      }, 1000);
    }

The _setupGoalListener is responsible for listening for collision events. These collision events are triggered from the engine so we’re attaching the listener to the engine itself. collisionStart gets fired at the very beginning of a collision. This provides data on the bodies which collided. The first body (bodyA) always contain the body which initiated the collision. In this case, it’s always one of the two balls. bodyB, on the other hand, contains the body which receives the collision. In this case, it’s the goal box. But since the goal box is set as a sensor (isSensor: true), it won’t actually affect the ball in any way. It will only register that it collided with the ball:

    _setupGoalListener = (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 == this.myBallName && objB == 'goal') {
          Alert.alert("You won", "And that's awesome!");
          this.setState({
            isGameFinished: true
          });
        } else if (objA == this.opponentBallName && objB == 'goal') {
          Alert.alert("You lose", "And that sucks!");
          this.setState({
            isGameFinished: true
          });
        }
      });
    }

Lastly, render the UI:

    render() {
      if (this.state.isMazeReady) {
        return (
          <GameEngine
            systems={[this.physics, this.moveBall]}
            entities={this.entities}
          >
          </GameEngine>
        );
      }

      return <ActivityIndicator size="large" color="#0064e1" />;
    }

Circle component

Here’s the code for the Circle component:

    // src/components/Circle.js
    import React from "react";
    import { View, Dimensions } from "react-native";
    import dimensions from '../data/constants';
    const { width, height } = dimensions;

    const BODY_DIAMETER = Math.floor(width * .02);
    const BORDER_WIDTH = 2;

    const Circle = ({ body, bgColor, borderColor }) => {
      const { position } = body;

      const x = position.x;
      const y = position.y;
      return <View style={[styles.head, {
        left: x, 
        top: y,
        backgroundColor: bgColor,
        borderColor: borderColor
      }]} />;

    };

    export default Circle;

    const styles = {
      head: {
        borderWidth: BORDER_WIDTH,
        width: BODY_DIAMETER,
        height: BODY_DIAMETER,
        position: "absolute",
        borderRadius: BODY_DIAMETER * 2
      }
    };

Rectangle component

Here’s the code for the Rectangle component:

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

    const Rectangle = ({ 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 Rectangle;

CreateMaze helper

The CreateMaze helper is really the main meat of this app because it’s the one which generates the maze that the players have to navigate. There are lots of maze generation algorithms out there. This helper uses the recursive backtracking algorithm. Here’s the way it works:

  1. Create a grid.
  2. Choose a random cell within the grid. In this case, we’re selecting the very first cell in the grid to start with.
  3. Check if there are any cells you can visit from the current cell.
  4. If there is, then:
    Pick a random neighbor.Put the neighbor on the stack.Mark the path from the current cell to the neighbor.1. If there are no more cells you can go to, mark the cell as “visited” and pop it out of the stack. The cell you’ve gone to prior to the cell that was popped out is now the current cell.
  5. Repeat steps 3 to 5 until all the cells in the grid have been visited or popped out of the stack.

Now that you have a general idea of how we’re going to implement this, it’s time to proceed with the code. Start by importing the things we need:

    // src/helpers/CreateMaze.js
    import Matter from 'matter-js';

    import GetRandomNumber from './GetRandomNumber';

    import dimensions from '../data/constants';
    const { width, height } = dimensions;

    // convenience variables:
    const TOP = 'T';
    const BOTTOM = 'B';
    const RIGHT = 'R';
    const LEFT = 'L';

Next, we represent the grid using an array. The CreateMaze class accepts the number of cells in the X and Y axis for its constructor. By default, it’s going to generate a 15x18 grid. this.grid contains a two-dimensional array of null values. This will be filled later with the directions in which a path is carved on each row in the grid:

    const CreateMaze = (gridX = 15, gridY = 18) => {

      this.width = gridX; 
      this.height = gridY; 

      this.blockWidth = Math.floor(width / this.width); // 24
      this.blockHeight = Math.floor(height / this.height); // 38

      this.grid = new Array(this.height)
      for (var i = 0; i < this.grid.length; i++) {
        this.grid[i] = new Array(this.width);
      }

      // next: initialize the composite body

    }

Next, initialize the composite body for containing the maze:

    const wallOpts = {
      isStatic: true,
    };

    this.matter = Matter.Composite.create(wallOpts);

Next, start carving the path. We’re going to start at the very first cell for this one, but you can pretty much start anywhere. Just remember that the grid is only 15x18 so you can only pick numbers between 0 to 14 for the X axis, and numbers between 0 to 17 for the Y axis:

    this.carvePathFrom(0, 0, this.grid);

carvePathFrom is a recursive function. It will call itself recursively until it has visited all the cells in the grid. It works by randomly picking which direction to go to first from the current cell. It then loops through those directions to determine if they can be visited or not. As you learned earlier, a cell can be visited if it can be accessed from the current cell and that it hasn’t already been visited. The getDirectionX and getDirectionY function checks if the next cell in the X or Y axis can be visited:

    carvePathFrom = (x, y, grid) => {

      const directions = [TOP, BOTTOM, RIGHT, LEFT]
        .sort(f => 0.5 - GetRandomNumber()); // whichever direction is closest to the random number is first in the list. 

      directions.forEach(dir => {
        const nX = x + this.getDirectionX(dir);
        const nY = y + this.getDirectionY(dir);
        const xNeighborOK = nX >= 0 && nX < this.width;
        const yNeighborOK = nY >= 0 && nY < this.height;

        // next: check if cell can be visited
      }); 
    }

Only when both these functions return either 0 or 1, and that the next cell hasn’t already been visited will it proceed to call itself again. Don’t forget to put the visited direction into the grid. This will tell the function that the specific cell has already been visited. We also need to add the direction opposite to the current direction as the next path:

    if (xNeighborOK && yNeighborOK && grid\[nY\][nX] == undefined) {
      grid\[y\][x] = grid\[y\][x] || dir;
      grid\[nY\][nX] = grid\[nY\][nX] || this.getOpposite(dir);
      this.carvePathFrom(nX, nY, grid);
    }

Here are the variables we used in the above function:

    const LEFT = 'L';

    // add these:
    const directionX = {
      'T': 0, // neutral because were moving in the X axs
      'B': 0,
      'R': 1, // +1 because were moving forward (we started at 0,0 so we move from left to right)
      'L': -1 // -1 because were moving backward
    };

    const directionY = {
      'T': -1, // -1 because were moving backward
      'B': 1, // +1 because were moving forward (we started at 0,0 so we move from top to bottom)
      'R': 0, // neutral because were moving in the Y axis
      'L': 0
    };

    // opposite directions
    const op = {
      'T': BOTTOM, // top's opposite is bottom
      'B': TOP,
      'R': LEFT,
      'L': RIGHT
    };

And here are the functions:

    this.matter = Matter.Composite.create(wallOpts);

    // add these:
    getDirectionX = (dir) => {
      return directionX[dir];
    }

    getDirectionY = (dir) => {
      return directionY[dir];
    }

    getOpposite = (dir) => {
      return op[dir];
    }

Now that we have the grid and the path in place, the next step is to construct the walls:

    this.carvePathFrom(0, 0, this.grid);

    // add these:
    for (var i = 0; i < this.grid.length; i++) { // rows
      for (var j = 0; j < this.grid[i].length; j++) { // columns
        Matter.Composite.add(this.matter, this.generateWall(j, i));
      }
    }

The generateWall function accepts the cell’s address in the X and Y axis. The first one is always going to be 0,0 since that’s the very first cell we visited. From there, we figure out which part of the cell do we draw the walls. We do that by checking if the current cell we are in isn’t part of the path. gridPoint is the cell we’re currently iterating over and it contains the direction of the path to be visited from there (either T, L, B or R). For example, if we’re visiting 0,0 and it contains B as part of the path then only the top, right, and left walls will be generated. Aside from that, we also need to consider the opposite. The getPointInDirection is key for that. This function is responsible for returning the direction (either T, L, B or R) of the next cell to visit, but it only does so if the address to the next cell in the given direction is greater than 0. So it’s just checking if we’ve actually moved forward in that specific direction:

    generateWall = (x, y) => {
      const walls = Matter.Composite.create({ isStatic: true });
      const gridPoint = this.grid\[y\][x];
      const opts = { 
        isStatic: true
      };

      const wallThickness = 5; // how thick the wall is in pixels

      const topPoint = this.getPointInDirection(x, y, TOP);
      if (gridPoint !== TOP && topPoint !== this.getOpposite(TOP)) {
        Matter.Composite.add(walls, Matter.Bodies.rectangle(this.blockWidth / 2, 0, this.blockWidth, wallThickness, opts));
      }
      const bottomPoint = this.getPointInDirection(x, y, BOTTOM);
      if (gridPoint !== BOTTOM && bottomPoint !== this.getOpposite(BOTTOM)) {
        Matter.Composite.add(walls, Matter.Bodies.rectangle(this.blockWidth / 2, this.blockHeight, this.blockWidth, wallThickness, opts));
      }
      const leftPoint = this.getPointInDirection(x, y, LEFT);
      if (gridPoint !== LEFT && leftPoint !== this.getOpposite(LEFT)) {
        Matter.Composite.add(walls, Matter.Bodies.rectangle(0, this.blockHeight / 2, wallThickness, this.blockHeight + wallThickness, opts));
      }
      const rightPoint = this.getPointInDirection(x, y, RIGHT);
      if (gridPoint !== RIGHT && rightPoint !== this.getOpposite(RIGHT)) {
        Matter.Composite.add(walls, Matter.Bodies.rectangle(this.blockWidth, this.blockHeight / 2, wallThickness, this.blockHeight + wallThickness, opts));
      }

      // next: create vector
    }

The final step before we return the walls is to actually put the walls in their proper position. In the code above, all we did was create Rectangle bodies and add it to the composite body. We haven’t actually specified the correct position for them (which cells they need to be added to). That’s where the Vector class comes in. We use it to change the position of the walls so that they’re on the cells where they need to be. For this, we simply multiply the cell address with the width or height of each cell in order to get their proper position. Then we use the translate method to actually move the walls to that position:

    const translate = Matter.Vector.create(x * this.blockWidth, y * this.blockHeight);
    Matter.Composite.translate(walls, translate);

    return walls;

Here’s the getPointInDirection function:

    getOpposite = (dir) => {
      // ...
    }

    getPointInDirection = (x, y, dir) => {
      const newXPoint = x + this.getDirectionX(dir);
      const newYPoint = y + this.getDirectionY(dir);

      if (newXPoint < 0 || newXPoint >= this.width) {
        return;
      }

      if (newYPoint < 0 || newYPoint >= this.height) {
        return;
      }

      return this.grid\[newYPoint\][newXPoint];
    }

GetRandomPoint helper

Here’s the code for getting a random point within the grid:

    // src/helpers/GetRandomNumber.js
    import dimensions from '../data/constants';
    const { width, height } = dimensions;
    import GetRandomNumber from './GetRandomNumber';

    const GetRandomPoint = (gridX, gridY) => {
      const gridXPart = (width / gridX);
      const gridYPart = (height / gridY);
      const x = Math.floor(GetRandomNumber() * gridX);
      const y = Math.floor(GetRandomNumber() * gridY);

      return {
        x: x * gridXPart + gridXPart / 2,
        y: y * gridYPart + gridYPart / 2
      }
    }

    export default GetRandomPoint;

GetRandomNumber helper

Here’s the code for generating random numbers:

    // src/helpers/GetRandomNumber.js
    var seed = 1;
    const GetRandomNumber = () => {
      var x = Math.sin(seed++) * 10000;
      return x - Math.floor(x);
    }

    export default GetRandomNumber;

Server

Now we’re ready to work with the server. Start by navigating to the app’s server directory and install the dependencies:

    cd server
    yarn


Next, update the .env file with your Pusher credentials:

    APP_ID="YOUR PUSHER APP ID"
    APP_KEY="YOUR PUSHER APP KEY"
    APP_SECRET="YOUR PUSHER APP SECRET"
    APP_CLUSTER="YOUR PUSHER APP CLUSTER"


Next, create a server.js file and add the following. This sets up the server and Pusher:

    var express = require("express");
    var bodyParser = require("body-parser");
    var Pusher = require("pusher");

    require("dotenv").config();

    var app = express();
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));

    // setup Pusher
    var pusher = new Pusher({
      appId: process.env.APP_ID,
      key: process.env.APP_KEY,
      secret: process.env.APP_SECRET,
      cluster: process.env.APP_CLUSTER
    });

Next, add the route for authenticating users with Pusher:

    var users = []; // for storing the users who logs in

    app.post("/pusher/auth", function(req, res) {
      var socketId = req.body.socket_id;
      var channel = req.body.channel_name;
      var username = req.body.username;

      users.push(username);
      console.log(username + " logged in");

      var auth = pusher.authenticate(socketId, channel);
      res.send(auth);
    });

Next, add the route for triggering the event that an opponent was found:

    app.get("/opponent-found", function(req, res) {
      var unique_users = users.filter((value, index, self) => {
        return self.indexOf(value) === index;
      });

      var player_one = unique_users[0];
      var player_two = unique_users[1];

      console.log("opponent found: " + player_one + " and " + player_two);

      pusher.trigger(
        ["private-user-" + player_one, "private-user-" + player_two],
        "opponent-found",
        {
          player_one: player_one,
          player_two: player_two
        }
      );

      res.send("opponent found!");
    });

Next, add the route for triggering the event for starting the game:

    app.get("/start-game", function(req, res) {
      var unique_users = users.filter((value, index, self) => {
        return self.indexOf(value) === index;
      });

      var player_one = unique_users[0];
      var player_two = unique_users[1];

      console.log("start game: " + player_one + " and " + player_two);

      pusher.trigger(
        ["private-user-" + player_one, "private-user-" + player_two],
        "start-game",
        {
          start: true
        }
      );

      users = [];

      res.send("start game!");
    });

Lastly, run the server on port 5000:

    var port = process.env.PORT || 5000;
    app.listen(port);

Running the app

Start by running the server:

    cd server
    node server
    ./ngrok http 5000


Next, update the the login screen with the ngrok URL:

    // src/screens/Login.js
    const base_url = "YOUR NGROK HTTPS URL";

Finally, run the app:

    react-native run-android


Since this app requires two players, I recommend that you set up the development server on one or two of the devices (or emulator). That way, you don’t need to physically connect the device to their computer (they only need to be connected to the same WI-FI network). This also helps avoid the confusion of where the React Native CLI will deploy the app if two devices are connected at the same time.

The screen above can be accessed by shaking the device (or executing adb shell input keyevent 82 in the terminal), select Dev Settings, and select Debug server host & port for device. Then enter your computer’s internal IP address in the box. The default port where the bundler runs is 8081.

Here’s the workflow I use for running the app:

  1. Create a grid.
  2. Choose a random cell within the grid. In this case, we’re selecting the very first cell in the grid to start with.
  3. Check if there are any cells you can visit from the current cell.
  4. If there is, then:
    Pick a random neighbor.Put the neighbor on the stack.Mark the path from the current cell to the neighbor.1. If there are no more cells you can go to, mark the cell as “visited” and pop it out of the stack. The cell you’ve gone to prior to the cell that was popped out is now the current cell.
  5. Repeat steps 3 to 5 until all the cells in the grid have been visited or popped out of the stack.

Conclusion

In this tutorial, you learned how to construct a maze using MatterJS. By using Pusher, we were able to update the position of each player within the maze in realtime.

But as you’ve seen, the game we created isn’t really production-ready. If you’re looking for a challenge, here are a few things that need additional work:

  • Yarn 1.13.0
  • React Native 0.58.4

Once you’ve implemented the above improvements, the game is basically ready for the app store.

You can find the code for this app on its GitHub repo.

Learn More

React - The Complete Guide (incl Hooks, React Router, Redux)

Modern React with Redux [2019 Update]

React Native - The Practical Guide

Styling in React Native

Learn React.js - Frontend Web Development for Beginners - Web Development in 2019

React Apps with the Google Maps API and google-maps-react

React Tutorial: Building and Securing Your First App

Simple User Authentication in React

Autumn  Blick

Autumn Blick

1598839687

How native is React Native? | React Native vs Native App Development

If you are undertaking a mobile app development for your start-up or enterprise, you are likely wondering whether to use React Native. As a popular development framework, React Native helps you to develop near-native mobile apps. However, you are probably also wondering how close you can get to a native app by using React Native. How native is React Native?

In the article, we discuss the similarities between native mobile development and development using React Native. We also touch upon where they differ and how to bridge the gaps. Read on.

A brief introduction to React Native

Let’s briefly set the context first. We will briefly touch upon what React Native is and how it differs from earlier hybrid frameworks.

React Native is a popular JavaScript framework that Facebook has created. You can use this open-source framework to code natively rendering Android and iOS mobile apps. You can use it to develop web apps too.

Facebook has developed React Native based on React, its JavaScript library. The first release of React Native came in March 2015. At the time of writing this article, the latest stable release of React Native is 0.62.0, and it was released in March 2020.

Although relatively new, React Native has acquired a high degree of popularity. The “Stack Overflow Developer Survey 2019” report identifies it as the 8th most loved framework. Facebook, Walmart, and Bloomberg are some of the top companies that use React Native.

The popularity of React Native comes from its advantages. Some of its advantages are as follows:

  • Performance: It delivers optimal performance.
  • Cross-platform development: You can develop both Android and iOS apps with it. The reuse of code expedites development and reduces costs.
  • UI design: React Native enables you to design simple and responsive UI for your mobile app.
  • 3rd party plugins: This framework supports 3rd party plugins.
  • Developer community: A vibrant community of developers support React Native.

Why React Native is fundamentally different from earlier hybrid frameworks

Are you wondering whether React Native is just another of those hybrid frameworks like Ionic or Cordova? It’s not! React Native is fundamentally different from these earlier hybrid frameworks.

React Native is very close to native. Consider the following aspects as described on the React Native website:

  • Access to many native platforms features: The primitives of React Native render to native platform UI. This means that your React Native app will use many native platform APIs as native apps would do.
  • Near-native user experience: React Native provides several native components, and these are platform agnostic.
  • The ease of accessing native APIs: React Native uses a declarative UI paradigm. This enables React Native to interact easily with native platform APIs since React Native wraps existing native code.

Due to these factors, React Native offers many more advantages compared to those earlier hybrid frameworks. We now review them.

#android app #frontend #ios app #mobile app development #benefits of react native #is react native good for mobile app development #native vs #pros and cons of react native #react mobile development #react native development #react native experience #react native framework #react native ios vs android #react native pros and cons #react native vs android #react native vs native #react native vs native performance #react vs native #why react native #why use react native

Hire Dedicated React Native Developer

Have you ever thought of having your own app that runs smoothly over multiple platforms?

React Native is an open-source cross-platform mobile application framework which is a great option to create mobile apps for both Android and iOS. Hire Dedicated React Native Developer from top React Native development company, HourlyDeveloper.io to design a spectacular React Native application for your business.

Consult with experts:- https://bit.ly/2A8L4vz

#hire dedicated react native developer #react native development company #react native development services #react native development #react native developer #react native

Juned Ghanchi

1621573085

React Native App Developers India, React Native App Development Company

Expand your user base by using react-native apps developed by our expert team for various platforms like Android, Android TV, iOS, macOS, tvOS, the Web, Windows, and UWP.

We help businesses to scale up the process and achieve greater performance by providing the best react native app development services. Our skilled and experienced team’s apps have delivered all the expected results for our clients across the world.

To achieve growth for your business, hire react native app developers in India. You can count on us for all the technical services and support.

#react native app development company india #react native app developers india #hire react native developers india #react native app development company #react native app developers #hire react native developers

Hire Dedicated React Native Developers - WebClues Infotech

Being one of the emerging frameworks for app development the need to develop react native apps has increased over the years.

Looking for a react native developer?

Worry not! WebClues infotech offers services to Hire React Native Developers for your app development needs. We at WebClues Infotech offer a wide range of Web & Mobile App Development services based o your business or Startup requirement for Android and iOS apps.

WebClues Infotech also has a flexible method of cost calculation for hiring react native developers such as Hourly, Weekly, or Project Basis.

Want to get your app idea into reality with a react native framework?

Get in touch with us.

Hire React Native Developer Now: https://www.webcluesinfotech.com/hire-react-native-app-developer/

For inquiry: https://www.webcluesinfotech.com/contact-us/

Email: sales@webcluesinfotech.com

#hire react native developers #hire dedicated react native developers #hire react native developer #hiring a react native developer #hire freelance react native developers #hire react native developers in 1 hour

Factors affecting the cost of hiring a React Native developer in USA - TopDevelopers.co

Want to develop app using React Native? Here are the tips that will help to reduce the cost of react native app development for you.
Cost is a major factor in helping entrepreneurs take decisions about investing in developing an app and the decision to hire react native app developers in USA can prove to be fruitful in the long run. Using react native for app development ensures a wide range of benefits to your business. Understanding your business and working on the aspects to strengthen business processes through a cost-efficient mobile app will be the key to success.

#best react native development companies from the us #top react native app development companies in usa #cost of hiring a react native developer in usa #top-notch react native developer in usa #best react native developers usa #react native