Dealing with collisions

With so many bullets flying around and having the Enemy objects to check collisions against, it is important that there be a separate class that does this collision checking for us. This way we know where to look if we decide we want to implement a new way of checking for collisions or optimize the current code. The Collision.h file contains a static method that checks for collisions between two SDL_Rect objects:

const static int s_buffer = 4;

static bool RectRect(SDL_Rect* A, SDL_Rect* B)
{
  int aHBuf = A->h / s_buffer;
  int aWBuf = A->w / s_buffer;

  int bHBuf = B->h / s_buffer;
  int bWBuf = B->w / s_buffer;

  // if the bottom of A is less than the top of B - no collision
  if((A->y + A->h) - aHBuf <= B->y + bHBuf)  { return false; }

  // if the top of A is more than the bottom of B = no collision
  if(A->y + aHBuf >= (B->y + B->h) - bHBuf)  { return false; }

  // if the right of A is less than the left of B - no collision
  if((A->x + A->w) - aWBuf <= B->x +  bWBuf) { return false; }

  // if the left of A is more than the right of B - no collision
  if(A->x + aWBuf >= (B->x + B->w) - bWBuf)  { return false; }

  // otherwise there has been a collision
 return true;
}

The function makes use of a buffer, which is a value that is used to make the rectangles slightly smaller. In a game such as Alien Attack, exact collision on bounding rectangles would be slightly unfair and also not much fun. With the buffer value, more direct hits are needed before they will be registered as a collision. Here the buffer is set to 4; this will take a fourth off of each side of the rectangle.

The Player class will not handle its own collisions. This requires a way to separate out the player from the rest of the GameObject instants when the level is loaded. The Level class now stores a pointer to Player:

Player* m_pPlayer;

With a public getter and setter:

Player* getPlayer() { return m_pPlayer; }
void setPlayer(Player* pPlayer) { m_pPlayer = pPlayer; }

The LevelParser instance sets this pointer when it loads in Player from the level file:

pGameObject->load(std::unique_ptr<LoaderParams>(new LoaderParams(x, y, width, height, textureID, numFrames,callbackID, animSpeed)));

if(type == "Player") // check if it's the player
{
  pLevel->setPlayer(dynamic_cast<Player*>(pGameObject));
}

pObjectLayer->getGameObjects()->push_back(pGameObject);

Another addition to Level is that it holds a separate std::vector of TileLayer* which are tile layers that the game will check against for collisions. This value is passed in from the .tmx file and any TileLayer that needs to be checked for collisions must set collidable as a property in the tiled application.

Dealing with collisions

This also requires a slight alteration in LevelParser::parseLevel when checking for object layers, just in case the layer does contain properties (in which case data would no longer be the first child element):

else if(e->FirstChildElement()->Value() == std::string("data") || (e->FirstChildElement()->NextSiblingElement() != 0 && e->FirstChildElement()->NextSiblingElement()->Value() == std::string("data")))
{
  parseTileLayer(e, pLevel->getLayers(), pLevel->getTilesets(), 
  pLevel->getCollisionLayers());
}

The LevelParser instance can now add collision layers to the collision layers array in parseTileLayer:

// local temporary variable
bool collidable = false;

// other code…

for(TiXmlElement* e = pTileElement->FirstChildElement(); e != NULL; e = e->NextSiblingElement())
{
  if(e->Value() == std::string("properties"))
  {
    for(TiXmlElement* property = e->FirstChildElement(); property != NULL; property = property->NextSiblingElement())
    {
      if(property->Value() == std::string("property"))
      {
        if(property->Attribute("name") == std::string("collidable"))
        {
          collidable = true;
        }
      }
    }
  }

  if(e->Value() == std::string("data"))
  {
    pDataNode = e;
  }
}

// other code…

// push into collision array if necessary
if(collidable)
{
  pCollisionLayers->push_back(pTileLayer);
}

pLayers->push_back(pTileLayer);

Creating a CollisionManager class

The class responsible for checking and handling all of these collisions is the CollisionManager class. Here is its declaration:

class CollisionManager
{
public:

  void checkPlayerEnemyBulletCollision(Player* pPlayer);
  void checkPlayerEnemyCollision(Player* pPlayer, const 
  std::vector<GameObject*> &objects);
  void checkEnemyPlayerBulletCollision(const 
  std::vector<GameObject*> &objects);
  void checkPlayerTileCollision(Player* pPlayer, const 
  std::vector<TileLayer*> &collisionLayers);
};

Looking at the source code you will see that these functions are pretty big, yet they are relatively simple. They loop through each object that requires a collision test, create a rectangle for each, and then pass it to the static RectRect function defined in Collision.h. If a collision occurred then it calls the collision function for that object. The checkEnemyPlayerBulletCollision and checkPlayerEnemyCollision functions perform an extra check to see if the object is actually of Enemy type:

if(objects[i]->type() != std::string("Enemy") || !objects[i]->updating())
{
 continue;
}

If it is not, then it does not check the collision. This is why it is important that the Enemy subtypes do not override the type function or if they do, their type must also be added to this check. This condition also checks whether the object is updating or not; if it is not, then it is offscreen and does not need to be checked against for collision.

Checking for collision against tiles requires a similar method to working out where to start drawing the tiles from, which was implemented in the TileLayer::render function. Here is the checkPlayerTileCollision definition:

void CollisionManager::checkPlayerTileCollision(Player* pPlayer, 
  const std::vector<TileLayer*> &collisionLayers)
{
  // iterate through collision layers
  for(std::vector<TileLayer*>::const_iterator it = 
  collisionLayers.begin(); it != collisionLayers.end(); ++it)
  {
    TileLayer* pTileLayer = (*it);
    std::vector<std::vector<int>> tiles = pTileLayer-
    >getTileIDs();

    // get this layers position
    Vector2D layerPos = pTileLayer->getPosition();

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

    // calculate position on tile map
    x = layerPos.getX() / pTileLayer->getTileSize();
    y = layerPos.getY() / pTileLayer->getTileSize();

    // if moving forward or upwards
    if(pPlayer->getVelocity().getX() >= 0 || pPlayer-
    >getVelocity().getY() >= 0)
    {
      tileColumn = ((pPlayer->getPosition().getX() + pPlayer-
      >getWidth()) / pTileLayer->getTileSize());
      tileRow = ((pPlayer->getPosition().getY() + pPlayer-
      >getHeight()) 
      / pTileLayer->getTileSize());
      tileid = tiles[tileRow + y][tileColumn + x];
    }
    else if(pPlayer->getVelocity().getX() < 0 || pPlayer-
    >getVelocity().getY() < 0) // if moving backwards or downwards
    {
      tileColumn = pPlayer->getPosition().getX() / pTileLayer-
      >getTileSize();
      tileRow = pPlayer->getPosition().getY() / pTileLayer-
      >getTileSize();
      tileid = tiles[tileRow + y][tileColumn + x];
    }
    if(tileid != 0) // if the tile id not blank then collide
    {
      pPlayer->collision();
    }
  }
}
..................Content has been hidden....................

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