Chapter 9. Creating Conan the Caveman

In the previous chapter, the creation of Alien Attack demonstrated that the framework is now at a point where it can be used to quickly create a 2D side scrolling shooter. Other genres are also simple to make with most of the changes once again being contained within the object classes.

In this chapter, we will cover:

  • Adapting the previous code base for a new game
  • More precise tile-collision detection
  • Handling jumping
  • Possible additions to the framework

This chapter will use the framework to create a platform game, Conan the Caveman. Here is a screenshot of the finished game level:

Creating Conan the Caveman

Here's another screenshot with more enemies:

Creating Conan the Caveman

As with the previous chapter, this chapter is not a step-by-step guide to creating Conan the Caveman, rather it is an overview of the most important aspects of the game. The project for the game is available in the source code downloads.

Setting up the basic game objects

In some ways this game is more complicated than Alien Attack, whereas in other ways it is simpler. This section will cover the changes that were made to the Alien Attack source code: what was altered, what was removed, and what was added.

No more bullets or bullet collisions

Conan the Caveman does not use projectile weapons, and therefore, there is no longer a Bullet class and the CollisonManager class no longer needs to have a function that checks for collisions between them; it only checks for the Player and Enemy collisions:

class CollisionManager
{
public:

  void checkPlayerEnemyCollision(Player* pPlayer, const 
  std::vector<GameObject*>&objects);
};

Game objects and map collisions

Almost all objects will need to collide with the tile map and react accordingly. The GameObject class now has a private member that is a pointer to the collision layers; previously only the Player class had this variable:

std::vector<TileLayer*>* m_pCollisionLayers;

GameObject also now has a function to set this variable:

void setCollisionLayers(std::vector<TileLayer*>* layers) { m_pCollisionLayers = layers; }

The Player class would previously have this set at the end of the LevelParser::parseLevel function, as follows:

pLevel->getPlayer()->setCollisionLayers(pLevel->getCollisionLayers());

This is no longer needed, as each GameObject gets their m_pCollisionLayers variables set on creation in the object-layer parsing:

// load the object
pGameObject->load(std::unique_ptr<LoaderParams>(new LoaderParams(x, y, width, height, textureID, numFrames,callbackID, animSpeed)));
// set the collision layers
pGameObject->setCollisionLayers(pLevel->getCollisionLayers());

ShooterObject is now PlatformerObject

The shooter-specific code from Alien Attack has been stripped out of ShooterObject and the class is renamed to PlatformerObject. Anything that all game objects for this game will make use of is within this class:

class PlatformerObject : public GameObject
{
public:

  virtual ~PlatformerObject() {}

  virtual void load(std::unique_ptr<LoaderParams> const &pParams);

  virtual void draw();
  virtual void update();

  virtual void clean() {}
  virtual void collision() {}

  virtual std::string type() { return "SDLGameObject"; }

protected:

  PlatformerObject();

  bool checkCollideTile(Vector2D newPos);

  void doDyingAnimation();

  int m_bulletFiringSpeed;
  int m_bulletCounter;
  int m_moveSpeed;

  // how long the death animation takes, along with a counter
  int m_dyingTime;
  int m_dyingCounter;

  // has the explosion sound played?
  bool m_bPlayedDeathSound;

  bool m_bFlipped;

  bool m_bMoveLeft;
  bool m_bMoveRight;
  bool m_bRunning;

  bool m_bFalling;
  bool m_bJumping;
  bool m_bCanJump;

  Vector2D m_lastSafePos;

  int m_jumpHeight;
};

There are some variables and functions from Alien Attack that are still useful, plus a few new functions. One of the most important additions is the checkCollideTile function, which takes Vector2D as a parameter and checks whether it causes a collision:

bool PlatformerObject::checkCollideTile(Vector2D newPos)
{
  if(newPos.m_y + m_height>= TheGame::Instance()->getGameHeight() 
  - 32)
  {
    return false;
  }
  else
  {
    for(std::vector<TileLayer*>::iterator it = m_pCollisionLayers
    ->begin(); it != m_pCollisionLayers->end(); ++it)
    {
      TileLayer* pTileLayer = (*it);
      std::vector<std::vector<int>> tiles = pTileLayer
      ->getTileIDs();

      Vector2D layerPos = pTileLayer->getPosition();

      int x, y, tileColumn, tileRow, tileid = 0;

      x = layerPos.getX() / pTileLayer->getTileSize();
      y = layerPos.getY() / pTileLayer->getTileSize();

      Vector2D startPos = newPos;
      startPos.m_x += 15;
      startPos.m_y += 20;
      Vector2D endPos(newPos.m_x + (m_width - 15), (newPos.m_y) + 
      m_height - 4);

      for(int i = startPos.m_x; i < endPos.m_x; i++)
      {
        for(int j = startPos.m_y; j < endPos.m_y; j++)
        {
          tileColumn = i / pTileLayer->getTileSize();
          tileRow = j / pTileLayer->getTileSize();

          tileid = tiles[tileRow + y][tileColumn + x];

          if(tileid != 0)
          {
            return true;
          }
        }
      }
    }

    return false; 
  }
}

This is quite a large function, but it is essentially the same as how Alien Attack checked for tile collisions. One difference is the y position check:

if(newPos.m_y + m_height >= TheGame::Instance()->getGameHeight() - 32)
{
  return false;
}

This is used to ensure that we can fall off the map (or fall into a hole) without the function trying to access tiles that are not there. For example, if the object's position is outside the map, the following code would try to access tiles that do not exist and would therefore fail:

tileid = tiles[tileRow + y][tileColumn + x];

The y value check prevents this.

The Camera class

In a game such as Alien Attack, precise map-collision detection is not terribly important; it is a lot more important to have precise bullet, player, and enemy collisions. A platform game, however, needs very precise map collision requiring the need for a slightly different way of moving the map, so that no precision is lost when scrolling.

In Alien Attack, the map did not actually move; some variables were used to determine which point of the map to draw and this gave the illusion of the map scrolling. In Conan the Caveman, the map will move so that any collision detection routines are relative to the actual position of the map. For this a Camera class was created:

class Camera
{
public:

  static Camera* Instance()
  {
    if(s_pCamera == 0)
    {
      s_pCamera = new Camera();
    }

    return s_pCamera;
  }

  void update(Vector2D velocity);

  void setTarget(Vector2D* target) { m_pTarget = target; }
  void setPosition(const Vector2D& position) { m_position = 
  position; }

  const Vector2D getPosition() const;


private:

  Camera();
  ~Camera();

  // the camera's target
  Vector2D* m_pTarget;

  // the camera's position
  Vector2D m_position;

  static Camera* s_pCamera;
};

typedef Camera TheCamera;

This class is very simple, as it merely holds a location and updates it using the position of a target, referred to the pointer as m_pTarget:

const Vector2DCamera::getPosition() const
{
{
  if(m_pTarget != 0)
  {
    Vector2D pos(m_pTarget->m_x - (TheGame::Instance()
    ->getGameWidth() / 2), 0);

    if(pos.m_x< 0)
    {
      pos.m_x = 0;
    }

    return pos;
  }

  return m_position;
}

This could also be updated to include the y value as well, but because this is a horizontal-scrolling game, it is not needed here and so the y is returned as 0. This camera position is used to move the map and decide which tiles to draw.

Camera-controlled map

The TileLayer class now needs to know the complete size of the map rather than just one section of it; this is passed in through the constructor:

TileLayer(int tileSize, int mapWidth, int mapHeight, const std::vector<Tileset>& tilesets);

LevelParser passes the height and width in as it creates each TileLayer:

void LevelParser::parseTileLayer(TiXmlElement* pTileElement, std::vector<Layer*> *pLayers, const std::vector<Tileset>* pTilesets, std::vector<TileLayer*> *pCollisionLayers)
{
TileLayer* pTileLayer = new TileLayer(m_tileSize, m_width, m_height, *pTilesets);

The TileLayer class uses these values to set its row and column variables:

TileLayer::TileLayer(int tileSize, int mapWidth, int mapHeight, const std::vector<Tileset>& tilesets) : m_tileSize(tileSize), m_tilesets(tilesets), m_position(0,0), m_velocity(0,0)
{
  m_numColumns = mapWidth;
  m_numRows = mapHeight;

  m_mapWidth = mapWidth;
}

With these changes, the tile map now moves according to the position of the camera and skips any tiles that are outside the viewable area:

void TileLayer::render()
{
  int x, y, x2, y2 = 0;

  x = m_position.getX() / m_tileSize;
  y = m_position.getY() / m_tileSize;

  x2 = int(m_position.getX()) % m_tileSize;
  y2 = int(m_position.getY()) % m_tileSize;

  for(int i = 0; i < m_numRows; i++)
  {
    for(int j = 0; j < m_numColumns; j++)
    {
      int id = m_tileIDs[i + y][j + x];

      if(id == 0)
      {
        continue;
      }

      // if outside the viewable area then skip the tile
      if(((j * m_tileSize) - x2) - TheCamera::Instance()
      ->getPosition().m_x < -m_tileSize || ((j * m_tileSize) - x2) 
      - TheCamera::Instance()->getPosition()
      .m_x > TheGame::Instance()->getGameWidth())
      {
        continue;
      }

      Tileset tileset = getTilesetByID(id);

      id--;

      // draw the tile into position while offsetting its x 
      position by 
      // subtracting the camera position
      TheTextureManager::Instance()->drawTile(tileset.name, 
      tileset.margin, tileset.spacing, ((j * m_tileSize) - x2) - 
      TheCamera::Instance()->getPosition().m_x, ((i * m_tileSize) 
      - y2), m_tileSize, m_tileSize, (id - (tileset.firstGridID - 
      1)) / tileset.numColumns, (id - (tileset.firstGridID - 1)) % 
      tileset.numColumns, TheGame::Instance()->getRenderer());
    }
  }

The Player class

The Player class now has to contend with jumping as well as moving, all while checking for map collisions. The Player::update function has undergone quite a change:

void Player::update()
{
  if(!m_bDying)
  {
    // fell off the edge
    if(m_position.m_y + m_height >= 470)
    {
      collision();
    }

    // get the player input
    handleInput();

    if(m_bMoveLeft)
    {
      if(m_bRunning)
      {
        m_velocity.m_x = -5;
      }
      else
      {
        m_velocity.m_x = -2;
      }
    }
    else if(m_bMoveRight)
    {
      if(m_bRunning)
      {
        m_velocity.m_x = 5;
      }
      else
      {
        m_velocity.m_x = 2;
      }
    }
    else
    {
      m_velocity.m_x = 0;
    }

    // if we are higher than the jump height set jumping to false
    if(m_position.m_y < m_lastSafePos.m_y - m_jumpHeight)
    {
      m_bJumping = false;
    }

    if(!m_bJumping)
    {
      m_velocity.m_y = 5;
    }
    else
    {
      m_velocity.m_y = -5;
    }

    handleMovement(m_velocity);
  }
  else
  {
    m_velocity.m_x = 0;
    if(m_dyingCounter == m_dyingTime)
    {
      ressurect();
    }
    m_dyingCounter++;

    m_velocity.m_y = 5;
  }
  handleAnimation();
}

As movement is such an important part of this class, there is a function that is dedicated to handling it:

void Player::handleMovement(Vector2D velocity)
{
  // get the current position
  Vector2D newPos = m_position;

  // add velocity to the x position
  newPos.m_x  = m_position.m_x + velocity.m_x;

  // check if the new x position would collide with a tile
  if(!checkCollideTile(newPos))
  {
    // no collision, add to the actual x position
    m_position.m_x = newPos.m_x;
  }
  else
  {
    // collision, stop x movement
    m_velocity.m_x = 0;
  }

  // get the current position after x movement
  newPos = m_position;

  // add velocity to y position
  newPos.m_y += velocity.m_y;

  // check if new y position would collide with a tile
  if(!checkCollideTile(newPos))
  {
    // no collision, add to the actual x position
    m_position.m_y = newPos.m_y;
  }
  else
  {
    // collision, stop y movement
    m_velocity.m_y = 0;

    //  we collided with the map which means we are safe on the 
    ground,
    //  make this the last safe position
    m_lastSafePos = m_position;

    // move the safe pos slightly back or forward so when 
    resurrected we are safely on the ground after a fall
    if(velocity.m_x > 0)
    {
      m_lastSafePos.m_x -= 32;
    }
    else if(velocity.m_x < 0)
    {
      m_lastSafePos.m_x += 32;

    }

    // allow the player to jump again
    m_bCanJump = true;

    // jumping is now false
    m_bJumping = false;
  }

Tip

Notice that x and y checking has been split into two different parts; this is extremely important to make sure that an x collision doesn't stop y movement and vice versa.

The m_lastSafePos variable is used to put the player back into a safe spot after they are respawned. For example, if the player was to fall off the edge of the platform in the following screenshot and therefore land on the spikes below, he would be respawned at pretty much the same place as in the screenshot:

The Player class

Finally, the handle input function now sets Boolean variables for moving to the right-hand side and left-hand side or jumping:

void Player::handleInput()
{
  if(TheInputHandler::Instance()->isKeyDown(SDL_SCANCODE_RIGHT) && 
  m_position.m_x < ((*m_pCollisionLayers->begin())->getMapWidth() 
  * 32))
  {
    if(TheInputHandler::Instance()->isKeyDown(SDL_SCANCODE_A))
    {
      m_bRunning = true;
    }
    else
    {
      m_bRunning = false;
    }

    m_bMoveRight = true;
    m_bMoveLeft = false;
  }
  else if(TheInputHandler::Instance()
  ->isKeyDown(SDL_SCANCODE_LEFT) && m_position.m_x > 32)
  {
    if(TheInputHandler::Instance()->isKeyDown(SDL_SCANCODE_A))
    {
      m_bRunning = true;
    }
    else
    {
      m_bRunning = false;
    }

    m_bMoveRight = false;
    m_bMoveLeft = true;
  }
  else
  {
    m_bMoveRight = false;
    m_bMoveLeft = false;
  }

  if(TheInputHandler::Instance()->isKeyDown(SDL_SCANCODE_SPACE) 
  && m_bCanJump && !m_bPressedJump)
  {
    TheSoundManager::Instance()->playSound("jump", 0);
    if(!m_bPressedJump)
    {
      m_bJumping = true;
      m_bCanJump = false;
      m_lastSafePos = m_position;
      m_bPressedJump = true;
    }
  }

  if(!TheInputHandler::Instance()->isKeyDown(SDL_SCANCODE_SPACE) 
  && m_bCanJump)
  {
    m_bPressedJump = false;
  }
}

This is all fairly self-explanatory apart from the jumping. When the player jumps, it sets the m_bCanJump variable to false, so that on the next loop the jump will not be called again, due to the fact that jump can only happen when the m_bCanJump variable is true; (landing after the jump sets this variable back to true).

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

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