GameContainer

GameContainer is responsible for starting up the game once the user taps the screen. It will do this using requestAnimationFrame()--one of the custom timers implemented in React Native.

requestAnimationFrame() is similar to setTimeout(), but the former will fire after all the frame has flushed, whereas the latter will fire as quickly as possible (over 1000x per second on a iPhone 5S); therefore, requestAnimationFrame() is more suited for animated games as it deals only with frames.

As happens with most animated games, we need to create a loop to animate the sprites in the screen by calculating the next position of each element on each frame. This loop will be created by a function named nextFrame() inside our GameContainer:

nextFrame() {
if (this.props.gameOver) return;
    var elapsedTime = new Date() - this.time;
    this.time = new Date();
    this.props.tick(elapsedTime);
this.animationFrameId = 
      requestAnimationFrame(this.nextFrame.bind(this));
}

This function will be aborted if the property gameOver is set to true. Otherwise, it will trigger the action tick() (which calculates how the sprites should be moved on to the next frame, based on the elapsed time) and finally calls itself through requestAnimationFrame(). This will keep the loop in the game to animate the moving sprites.

Of book, this nextFrame() should be called at the start for the first time, so we will also create a start() function inside GameContainer to get the game started:

start() {
cancelAnimationFrame(this.animationFrameId);
    this.props.start();
    this.props.bounce();
    this.time = new Date();
    this.setState({ gameOver: false });
this.animationFrameId = 
      requestAnimationFrame(this.nextFrame.bind(this));
}

The start function makes sure there is no animation started by calling cancelAnimationFrame(). This will prevent any double animations being performed when the user resets the game.

Then, the functions trigger the start() action, which will just set a flag in the store to notice the game has started.

We want to start the game by moving the parrot up, so the user has the time to react. For this, we also call the bounce() action.

Finally, we start the animation loop by passing the already known nextFrame() function as a callback of requestAnimationFrame().

Let's also review the render() method we will use for this container:

render() {
    const {
      rockUp,
      rockDown,
      ground,
      ground2,
      parrot,
      isStarted,
      gameOver,
      bounce,
      score
    } = this.props;

    return (
      <TouchableOpacity
onPress={
          !isStarted || gameOver ? this.start.bind(this) : 
            bounce.bind(this)
        }
        style={styles.screen}
activeOpacity={1}
      >
        <Image
          source={require(“../../images/bg.png")}
          style={[styles.screen, styles.image]}
        />
        <RockUp
          x={rockUp.position.x * W} //W is a responsiveness factor 
                                    //explained in the 'constants' section
          y={rockUp.position.y}
          height={rockUp.size.height}
          width={rockUp.size.width}
        />
        <Ground
          x={ground.position.x * W}
          y={ground.position.y}
          height={ground.size.height}
          width={ground.size.width}
        />
        <Ground
          x={ground2.position.x * W}
          y={ground2.position.y}
          height={ground2.size.height}
          width={ground2.size.width}
        />
        <RockDown
          x={rockDown.position.x * W}
          y={rockDown.position.y * H} //H is a responsiveness factor  
                                      //explained in the 'constants' 
                                      //section
          height={rockDown.size.height}
          width={rockDown.size.width}
        />
        <Parrot
          x={parrot.position.x * W}
          y={parrot.position.y * H}
          height={parrot.size.height}
          width={parrot.size.width}
        />
        <Score score={score} />
        {!isStarted && <Start />}
        {gameOver && <GameOver />}
        {gameOver && isStarted && <StartAgain />}
      </TouchableOpacity>
    );
  }

It may be lengthy, but actually, it's a simple positioning of all the visible elements on the screen while wrapping them in a <TouchableOpacity /> component to capture the user tapping no matter in which part of the screen. This <TouchableOpacity /> component is actually not sending any feedback to the user when they tap the screen (we disabled it by passing activeOpacity={1} as a prop) since this feedback is already provided by the parrot bouncing on each tap.

Note

We could have used React Native's <TouchableWithoutFeedback /> for this matter, but it has several limitations which would have harmed our performance.

The provided onPress attribute just defines what the app should do when the user taps on the screen:

  • If the game is active, it will bounce the parrot sprite
  • If the user is on the game over screen it will restart the game by calling the start() action

All other children in the render() method are the graphic elements in our game, specifying for each of them, their position and size. It's also important to note several points:

  • There are two <Ground /> components because we need to continuously animate it in the x axis. They will be positioned one after the other horizontally to animate them together so when the end of the first <Ground /> component is shown on screen, the beginning of the second will follow creating the sense of continuum.
  • The background is not contained in any custom component but in <Image />. This is because it doesn't need any special logic being a static element.
  • Some positions are multiplied by factor variables (W and H). We will take a deeper look at these variables in the constants section. At this point, we only need to know that they are variables helping in the absolute positioning of the elements taking into account all screen sizes.
  • Let's now put all these functions together to build up our <GameContainer />:
    /*** src/components/GameContainer.js ***/
    
    import React, { Component } from “react";
    import { connect } from “react-redux";
    import { bindActionCreators } from “redux";
    import { TouchableOpacity, Image, StyleSheet } from “react-native";
    
    import * as Actions from “../actions";
    import { W, H } from “../constants";
    import Parrot from “./Parrot";
    import Ground from “./Ground";
    import RockUp from “./RockUp";
    import RockDown from “./RockDown";
    import Score from “./Score";
    import Start from “./Start";
    import StartAgain from “./StartAgain";
    import GameOver from “./GameOver";
    
    class Game extends Component {
    constructor() {
        super();
        this.animationFrameId = null;
        this.time = new Date();
      }
    
      nextFrame() {
         ...
      }
    
      start() {
         ...
      }
    
    componentWillUpdate(nextProps, nextState) {
        if (nextProps.gameOver) {
          this.setState({ gameOver: true });
          cancelAnimationFrame(this.animationFrameId);
        }
      }
    
    shouldComponentUpdate(nextProps, nextState) {
        return !nextState.gameOver;
      }
    
      render() {
    
         ...
    
      }
    }
    
    const styles = StyleSheet.create({
      screen: {
        flex: 1,
        alignSelf: “stretch",
        width: null
      },
      image: {
        resizeMode: “cover"
      }
    });
    
    function mapStateToProps(state) {
      const sprites = state.gameReducer.sprites;
      return {
    parrot: sprites[0],
        rockUp: sprites[1],
        rockDown: sprites[2],
        gap: sprites[3],
        ground: sprites[4],
        ground2: sprites[5],
        score: state.gameReducer.score,
        gameOver: state.gameReducer.gameOver,
        isStarted: state.gameReducer.isStarted
      };
    }
    function mapStateActionsToProps(dispatch) {
      return bindActionCreators(Actions, dispatch);
    }
    
    export default connect(mapStateToProps, mapStateActionsToProps)(Game);

We added three more ES6 and React lifecycle methods to this component:

  • super(): The constructor will save an attribute named animationFrameId to capture the ID for the animation frame in which the nextFrame function will run and also another attribute named time will store the exact time at which the game was initialized. This time attribute will be used by the tick() function to calculate how much the sprites should be moved.
  • componentWillUpdate(): This function will be called every time new props (positions and sizes for the sprites in the game) are passed. It will detect when the game must be stopped due to a collision so the game over screen will be displayed.
  • shouldComponentUpdate(): This performs another check to avoid re-rendering the game container if the game has ended.

The rest of the functions are Redux related. They are in charge of connecting the component to the store by injecting actions and attributes:

  • mapStateToProps(): This gets the data for all the sprites in the store and injects them into the component as props. The sprites will be stored in an array and therefore they will be accessed by index. On top of these, the Score, a flag noting if the current game is over, and a flag noting if the game is in progress will also be retrieved from the state and injected into the component.
  • mapStateActionsToProps(): This will inject the three available actions (tick, bounce, and start) into the component so they can be used by it.

    Note

    Accessing the sprites data by index is not a recommended practice as indexes can change if the number of sprites grows, but we will use it like this in this app for simplicity reasons.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.80.3