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.
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);
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(); } } }
3.138.106.233