The Sprites Module

The structure of the sprites module is formed by an array of sprites and several exported functions:

/*** src/sprites.js ***/

import sound from “react-native-sound";

const coinSound = new sound(“coin.wav", sound.MAIN_BUNDLE);
let heightOfRockUp = 25;
let heightOfRockDown = 25;
let heightOfGap = 30;
let heightOfGround = 20;

export const sprites = [
   ...
];

function prepareNewRockSizes() {
  ...
}

function getRockProps(type) {
  ...
}

export function moveSprites(sprites, elapsedTime = 1000 / 60) {
  ...
}

export function bounceParrot(sprites) {
  ...
}

function hasCollided(mainSprite, sprite) {
  ...
}

export function checkForCollision(mainSprite, sprites) {
  ...
}

export function getUpdatedScore(sprites, score) {
  ...
}

This module begins by loading the sound effect we will play when the parrot passes a set of rocks to give feedback to the user about the increment in their score.

Then, we define some heights for several sprites:

  • heightOfRockUp: This is the height of the rock which will appear in the upper part of the screen.
  • heightOfRockDown: This is the height of the rock which will show in the lower part of the screen.
  • heightOfGap: We will create an invisible view between the upper and the lower rock to detect when the parrot has passed each set of rocks so the score is updated. This this gap's height.
  • heightOfGround: This is a static value for the height of the ground.

Each other item in this module plays a role in moving or positioning the sprites on the screen.

The Sprites Array

This is the array in charge of storing all the sprite's positions and sizes at a given time. Why are we using an array for storing our sprites instead of a hash map (Object)? Mainly for extensibility; although a hash map would make our code noticeably more readable, if we want to add new sprites of an existing type (as it happens with the ground sprite in this app) we would need to use artificial keys for each of them despite being the same type. Using an array of sprites is a recurrent pattern in game development which allows to decouple the implementation from the list of sprites.

Whenever we want to move a sprite, we will update its position in this array:

export const sprites = [
  {

    type: “parrot",
    position: { x: 50, y: 55 },
    velocity: { x: 0, y: 0 },
    size: { width: 10, height: 8 }
  },
  {
    type: “rockUp",
    position: { x: 110, y: 0 },
    velocity: { x: -1, y: 0 },
    size: { width: 15, height: heightOfRockUp }
  },
  {
    type: “rockDown",
    position: { x: 110, y: heightOfRockUp + 30 },
    velocity: { x: -1, y: 0 },
    size: { width: 15, height: heightOfRockDown }
  },
  {
    type: “gap",
    position: { x: 110, y: heightOfRockUp },
    velocity: { x: -1, y: 0 },
    size: { width: 15, height: 30 }
  },
  {
    type: “ground",
    position: { x: 0, y: 80 },
    velocity: { x: -1, y: 0 },
    size: { width: 100, height: heightOfGround }
  },
  {
    type: “ground",
    position: { x: 100, y: 80 },
    velocity: { x: -1, y: 0 },
    size: { width: 100, height: heightOfGround }
  }
];

The array will store the initial values for positioning and sizing all the moving sprites in the game.

prepareNewRockSizes()

This function randomly calculates the size of the next upper and lower rock together with the height of the gap between them:

function prepareNewRockSizes() {
  heightOfRockUp = 10 + Math.floor(Math.random() * 40);
  heightOfRockDown = 50 - heightOfRockUp;
  heightOfGap = 30;
}

It's important to note that this function only calculates the heights for the new set of rocks but doesn't create them. This is just a preparation step.

getRockProps()

The helper functions to format the position and size attributes of a rock (or gap):

function getRockProps(type) {
  switch (type) {
    case “rockUp":
      return { y: 0, height: heightOfRockUp };
    case “rockDown":
      return { y: heightOfRockUp + heightOfGap, 
               height: heightOfRockDown };
    case “gap":
      return { y: heightOfRockUp, height: heightOfGap };
  }
}

moveSprites()

This is the main function as it calculates the new position for each sprite stored in the sprites array. Game development relies in physics to calculate the position for each sprite in each frame.

For example, if we want to move an object to the right side of the screen, we will need to update its x position a number of pixels. The more pixels we add to the object's x attribute for the next frame, the faster it will move (sprite.x = sprite.x + 5; moves sprite five times faster than sprite.x = sprite.x + 1;).

As we can see in the following example, the way we calculate the new position for each sprite is based on three factors: the current position of the sprite, the time that has passed since the last frame (elapsedTime), and the gravity/velocity of the sprite (i.e. sprite.velocity.y + elapsedTime * gravity).

Additionally, we will use the helper function getRockProps to get the new sizes and positions for the rocks. Let's take a look at how the moveSprites function looks like:

export function moveSprites(sprites, elapsedTime = 1000 / 60) {
  const gravity = 0.0001;
  let newSprites = [];

  sprites.forEach(sprite => {
    if (sprite.type === “parrot") {
      var newParrot = {
        ...sprite,
        position: {
          x: sprite.position.x,
          y:
            sprite.position.y +
            sprite.velocity.y * elapsedTime +
            0.5 * gravity * elapsedTime * elapsedTime
        },
        velocity: {
          x: sprite.velocity.x,
          y: sprite.velocity.y + elapsedTime * gravity
        }
      };
      newSprites.push(newParrot);
    } else if (
      sprite.type === “rockUp" ||
      sprite.type === “rockDown" ||
      sprite.type === “gap"
    ) {
      let rockPosition,
        rockSize = sprite.size;
      if (sprite.position.x > 0 - sprite.size.width) {
        rockPosition = {
          x: sprite.position.x + sprite.velocity.x,
          y: sprite.position.y
        };
      } else {
        rockPosition = { x: 100, y: getRockProps(sprite.type).y };
        rockSize = { width: 15, 
                     height: getRockProps(sprite.type).height };
      }
      var newRock = {
        ...sprite,
        position: rockPosition,
        size: rockSize
      };
      newSprites.push(newRock);
    } else if (sprite.type === “ground") {
      let groundPosition;
      if (sprite.position.x > -97) {
        groundPosition = { x: sprite.position.x + sprite.velocity.x,
                           y: 80 };
      } else {
        groundPosition = { x: 100, y: 80 };
      }
      var newGround = { ...sprite, position: groundPosition };
      newSprites.push(newGround);
    }
  });
  return newSprites;
}

Calculating the next position for a sprite is, most of the time, basic addition (or subtraction). Let's take, for example, how the parrot should move:

var newParrot = {
        ...sprite,
        position: {
          x: sprite.position.x,
          y:
            sprite.position.y +
            sprite.velocity.y * elapsedTime +
            0.5 * gravity * elapsedTime * elapsedTime
        },
        velocity: {
          x: sprite.velocity.x,
          y: sprite.velocity.y + elapsedTime * gravity
        }
     }

The parrot will only move vertically, basing its speed on gravity, so the x attribute will always stay fixed for it while the y attribute will change according to the function sprite.position.y + sprite.velocity.y * elapsedTime + 0.5 * gravity * elapsedTime * elapsedTime which, in summary, adds the elapsed time and the gravity in different factors.

The calculations for how the rocks should move are a little more complex, as we need to take into account every time the rocks disappear from the screen (if (sprite.position.x > 0 - sprite.size.width)). As they have been passed, we need to recreate them with different heights (rockPosition = { x: 100, y: getRockProps(sprite.type).y }).

We have the same behavior for the ground, in terms of having to recreate it once it abandons the screen completely (if (sprite.position.x > -97)).

bounceParrot()

The only task for this function is changing the velocity of the main character, so it will fly up reversing the effect of gravity. This function will be called whenever the user taps on the screen while the game is started:

export function bounceParrot(sprites) {
  var newSprites = [];
  var sprite = sprites[0];
  var newParrot = { ...sprite, velocity: { x: sprite.velocity.x,
                    y: -0.05 } };
  newSprites.push(newParrot);
  return newSprites.concat(sprites.slice(1));
}

It's a simple operation in which we take the parrot's sprite data from the sprites array; we change its velocity on the y axis to a negative value so that the parrot moves upwards.

checkForCollision()

checkForCollision() is responsible for identifying if any of the rigid sprites have collided with the parrot sprite, so the game can be stopped. It will use hasCollided() as a supporting function to perform the required calculations on each specific sprite:

function hasCollided(mainSprite, sprite) {
  /*** 
   *** we will check if 'mainSprite' has entered in the
   *** space occupied by 'sprite' by comparing their
   *** position, width and height 
   ***/

  var mainX = mainSprite.position.x;
  var mainY = mainSprite.position.y;
  var mainWidth = mainSprite.size.width;
  var mainHeight = mainSprite.size.height;

  var spriteX = sprite.position.x;
  var spriteY = sprite.position.y;
  var spriteWidth = sprite.size.width;
  var spriteHeight = sprite.size.height;

  /*** 
   *** this if statement checks if any border of mainSprite
   *** sits within the area covered by sprite 
   ***/

  if (
    mainX < spriteX + spriteWidth &&
    mainX + mainWidth > spriteX &&
    mainY < spriteY + spriteHeight &&
    mainHeight + mainY > spriteY
  ) {
    return true;
  }
}

export function checkForCollision(mainSprite, sprites) {
  /*** 
   *** loop through all sprites in the sprites array
   *** checking, for each of them, if there is a
   *** collision with the mainSprite (parrot)
   ***/

  return sprites.filter(sprite => sprite.type !== “gap").find(sprite => {
    return hasCollided(mainSprite, sprite);
  });
}

For simplicity, we assume that all sprites have a rectangular shape (even though rocks grow thinner towards the end) because the calculation would be a lot more complex if we considered different shapes.

In summary, checkForCollision() is just looping through the sprites array to find any colliding sprite, hasCollided() checks for collisions based on the sprite size and position. In just an if statement, we compare the boundaries of a sprite and the parrot's sprite to see if any of those boundaries are occupying the same area of the screen.

getUpdatedScore()

The last function in the sprites module will check if the score needs to be updated based on parrot position relative to the gap position (the gap between the upper and the lower rock is also counted as a sprite):

export function getUpdatedScore(sprites, score) {
  var parrot = sprites[0];
  var gap = sprites[3];

  var parrotXPostion = parrot.position.x;
  var gapXPosition = gap.position.x;
  var gapWidth = gap.size.width;

  if (parrotXPostion === gapXPosition + gapWidth) {
    coinSound.play();
    score++;
    prepareNewRockSizes();
  }

  return score;
}

An if statement checks if the parrot's position in the x axis has surpassed the gap (gapXPosition + gapWidth). When this happens, we play the sound we created in the header of the module (const coinSound = new sound(“coin.wav", sound.MAIN_BUNDLE);) by calling its play() method. Moreover, we will increase the score variable and prepare a new set of rocks to be rendered when the current ones leave the screen.

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

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