Make Your Own Sideways Scroller: Part 5 - Launching and Moving Obstacles


(Simon) #1

The code described in these lessons can be found at https://github.com/filmote/Steve


Launching an Obstacle

Steve the Dinosaur must avoid four type of obstacles - the single, double and triple cacti and a flying pterodactyl. These various obstacle types are enumerated in an enum called ObstacleType as shown below. The first three elements are self-explanatory - the two pterodactyl elements are used to represent the animal (I was about to say bird but they were in fact reptiles!) with its wing up and down. Later we will see how we animate the image but for now you can ignore the second element, Pterodactyl2.

enum ObstacleType {
  SingleCactus,
  DoubleCactus,
  TripleCactus,
  Pterodactyl1,
  Pterodactyl2,
  Count_AllObstacles = 4,
};

Unless otherwise specified, elements in an enumeration are assigned values starting from zero. In the enumeration above, the SingleCactus element has a value of 0 and the Pterodactly2 element has a value of 4. Although there are five elements, when we randomly launch objects there are only four types to choose from as the Pterodactyl1 and Pterodacytl2 elements describe the same thing. The element Count_AllObstacles is used to define the number of options available. Note that I have explicitly assigned it a value of 4. Enumerations do not have to have contiguous element values and multiple elements can have the same values.

The details of a single obstacle are stored in a structure as defined below. In addition to the obstacle’s position, the structure also contains the object type, an enabled flag and a reference to the image that will be used when rendering it. As mentioned earlier, structures are a great mechanism for capturing related data together.

struct Obstacle {
  int x;
  int y;
  ObstacleType type;
  bool enabled;
  const byte *image;
};

At any time during game play, two or even three obstacles may be visible on the screen. To cater for this, I have created an array of obstacles and initialised them with default values. Note that all of the obstacles are disabled by default.

#define NUMBER_OF_OBSTACLES  3

Obstacle obstacles[NUMBER_OF_OBSTACLES] = {
  { 0, 0, ObstacleType::Pterodactyl1, false, pterodactyl_1 },
  { 0, 0, ObstacleType::Pterodactyl1, false, pterodactyl_1 },
  { 0, 0, ObstacleType::Pterodactyl1, false, pterodactyl_1 },
};

When launching obstacles, we need to make sure that the obstacles are randomly placed but not too close together otherwise Steve may not be able to land between them and jump again. To facilitate this, we generate a random delay and store this into the variable obstacleLaunchCountdown which is decremented each pass of the main game loop. When this variable reaches zero, a simple loop passes through the obstacles[] array looking for the first inactive obstacle in the collection.

#define LAUNCH_DELAY_MIN   90
#define LAUNCH_DELAY_MAX   200

 
--obstacleLaunchCountdown;
  
if (obstacleLaunchCountdown == 0) {

  for (byte i = 0; i < NUMBER_OF_OBSTACLES; i++) {

    if (!obstacles[i].enabled) { 
      launchObstacle(i); 
      break;
    }

  }

  obstacleLaunchCountdown = random(LAUNCH_DELAY_MIN, LAUNCH_DELAY_MAX);
            
}

The actual code to launch a new obstacle is shown below. The input parameter, obstacleNumber, defines which of the three obstacles in the array to activate. To help the player get accustomed to the various obstacles, they are introduced slowly as the player’s score increases.

The first section of the routine calculates which of the elements in the ObstacleType enumeration can be chosen based on the player’s score. When the player’s score is less than 100, only the single cacti is valid. When the player’s score is less than 200, the single and double cacti images are valid - likewise a score less than 300 allows all three cacti obstacles to be chosen. Once the score exceeds 300 all obstacles including the pterodactyl are valid.

If a pterodactyl obstacle is chosen then a flying height is randomly selected between an upper and lower limit as defined by the two constants PTERODACTYL_UPPER_LIMIT and PTERODACTYL_LOWER_LIMIT. In contrast, cacti are all launched at ground level.

#define PTERODACTYL_UPPER_LIMIT     27
#define PTERODACTYL_LOWER_LIMIT     48

void launchObstacle(byte obstacleNumber) {


  // Randomly pick an obstacle ..

  ObstacleType randomUpper = ObstacleType::SingleCactus;
  
  switch (score) {

    case 0 ... 99: 
      randomUpper = ObstacleType::SingleCactus;
      break;

    case 100 ... 199: 
      randomUpper = ObstacleType::DoubleCactus;
      break;

    case 200 ... 299: 
      randomUpper = ObstacleType::TripleCactus;
      break;

    default:
      randomUpper = ObstacleType::Count_AllObstacles;
      break;
      
  }

  ObstacleType type = (ObstacleType)random(ObstacleType::SingleCactus, randomUpper + 1);


  // Launch the obstacle ..

  obstacles[obstacleNumber].type = type;
  obstacles[obstacleNumber].enabled = true;
  obstacles[obstacleNumber].x = WIDTH - 1;

  if (type == ObstacleType::Pterodactyl1) {

    obstacles[obstacleNumber].y = random(PTERODACTYL_UPPER_LIMIT, PTERODACTYL_LOWER_LIMIT);

  }
  else {

    obstacles[obstacleNumber].y = CACTUS_GROUND_LEVEL;

  }
 
}

It’s worth pointing out the use of ranges in the case statement in the above example. Ranges must be specified in the format shown with three decimal points between them and the ranges cannot overlap. Although this syntax is valid in the Arduino / Arduboy environment, it is not valid in most C++ implementations.

Moving Obstacles

The obstacles in our game move from right to left, one pixel per frame. The code below checks to see which of our obstacles are enabled and decrements their x coordinate by one. As the objects move out of view on the left hand side of the screen, they are disabled which allows them to be relaunched in the future.

We apply the same technique used to animate Steve’s feet to animate the pterodactyl’s wings.

void updateObstacles() {

  for (byte i = 0; i < NUMBER_OF_OBSTACLES; i++) {
    
    if (obstacles[i].enabled == true) {
      
      switch (obstacles[i].type) {

        case ObstacleType::Pterodactyl1:
        case ObstacleType::Pterodactyl2:
        
          if (arduboy.everyXFrames(2)) {
            if (obstacles[i].type == Pterodactyl1) { 
              obstacles[i].type = Pterodactyl2;
            }
            else {
              obstacles[i].type = Pterodactyl1;
            }
          }

          obstacles[i].x--;
          break;

        case ObstacleType::SingleCactus:
        case ObstacleType::DoubleCactus:
        case ObstacleType::TripleCactus:

          obstacles[i].x--;
          break;

      }
      

      // Has the obstacle moved out of view ?

      if (obstacles[i].x < -getImageWidth(obstacles[i].image)) {
        obstacles[i].enabled = false; 
      }

    }
    
  }
  
}

The code described in these lessons can be found at https://github.com/filmote/Steve

Prev Article > Make Your Own Sideways Scroller: Part 4 - Moving and Rendering Steve

Next Article > Make Your Own Sideways Scroller: Part 6 - Detecting Crashes and Saving Scoress


Make Your Own Sideways Scroller: Part 6 - Detecting Crashes and Saving Scores