Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
SFML Game Development.pdf
Скачиваний:
194
Добавлен:
28.03.2016
Размер:
4.19 Mб
Скачать

Chapter 7

Collision detection and response

Now that our world is full of entities, let's implement interactions between them. Most interactions occur in the form of a collision; two airplanes collide and explode, projectiles of the player's Gatling gun perforate an enemy, and a pickup is collected by the player, and so on.

First, we write a function that computes the bounding rectangle of an entity. This is the smallest possible rectangle that completely contains the entity. As such, it represents an approximation of the entity's shape, which makes

computations simpler. Here is an example implementation: getWorldTransform() multiplies the sf::Transform objects from the scene root to the leaf. sf::Transform::transformRect() transforms a rectangle, and may enlarge

it if there is a rotation (since the rectangle has to remain axis-aligned). sf::Sprite::getGlobalBounds() returns the sprite's bounding rectangle relative to the aircraft.

[ 173 ]

www.it-ebooks.info

Warfare Unleashed – Implementing Gameplay

sf::FloatRect Aircraft::getBoundingRect() const

{

return getWorldTransform()

.transformRect(mSprite.getGlobalBounds());

}

To get a better imagination of the bounding rectangle, take a look at SceneNode.cpp in the online code base. You can uncomment the call to drawBoundingRect() inside

SceneNode::draw().

For our collision, we write a function that checks whether a collision between two entities occurs. Here, we simply check bounding rectangles of the entities for an overlap. This approach is not extremely accurate, but easily implemented, and good enough for many purposes.

There is a wide range of more elaborated collision detection algorithms. A popular algorithm is the Separating Axis Theorem, which checks for collisions between two

convex polygons. You can read more about it at www. metanetsoftware.com/technique/tutorialA.html.

Our function is implemented using the SFML method sf::FloatRect::intersects() which checks for rectangle intersection.

bool collision(const SceneNode& lhs, const SceneNode& rhs)

{

return lhs.getBoundingRect()

.intersects(rhs.getBoundingRect());

}

Note that we wrote the function for SceneNode and not Entity. This is because collision occurs inside the scene graph, so we avoid the downcasts. Scene nodes that do not have a physical representation have an empty bounding rectangle, which does not intersect with others.

Finding the collision pairs

Given the collision() function, we can determine in each frame, which pairs of entities collide. We store the pointers to the entities in std::pair<SceneNode*, SceneNode*>, for which we have created the SceneNode::Pair typedef. All collision pairs are stored in a std::set instance.

[ 174 ]

www.it-ebooks.info

Chapter 7

Basically, we need to compare every scene node with every other scene node to determine if a collision between the two occurs. To do this in a recursive way, we use two methods. The first one, checkNodeCollision(), evaluates a collision between *this with its children, and the function argument node.

The first three lines check if a collision occurs, and if the nodes are not identical

(we do not want an entity to collide with itself). By calling isDestroyed(), we exclude entities that have already been destroyed, and that are no longer part of the gameplay. If the four conditions are true, we insert the pair to our set. The STL

algorithm std::minmax() takes two arguments and returns a pair with first being the smaller, and second being the greater of the two arguments (where smaller means lower address in this case). Thus, std::minmax(a,b) and std::minmax(b,a) return always the same pair. This comes in very handy in our case—together with the sorted set, we automatically ensure that a collision between entities A and B is inserted only once (and not twice as A-B and B-A pairs).

void SceneNode::checkNodeCollision(SceneNode& node, std::set<Pair>& collisionPairs)

{

if (this != &node && collision(*this, node)

&&!isDestroyed() && !node.isDestroyed()) collisionPairs.insert(std::minmax(this, &node));

FOREACH(Ptr& child, mChildren) child->checkNodeCollision(node, collisionPairs);

}

The second part invokes the function recursively for all children of *this.

Now, we have checked the whole scene graph against one node, but we want to check the whole scene graph against all nodes. This is where our second function checkSceneCollision() comes into play. For the argument and all its children, a collision between the current node *this and the argument node sceneGraph is evaluated.

void SceneNode::checkSceneCollision(SceneNode& sceneGraph, std::set<Pair>& collisionPairs)

{

checkNodeCollision(sceneGraph, collisionPairs);

FOREACH(Ptr& child, sceneGraph.mChildren) checkSceneCollision(*child, collisionPairs);

}

[ 175 ]

www.it-ebooks.info

Warfare Unleashed – Implementing Gameplay

Reacting to collisions

What we have seen now is how collision detection works. The other part is collision response, where collisions result in gameplay actions.

For every frame, we store all collided scene nodes in a set. Now we can iterate through this set of SceneNode* pairs, and dispatch on the categories of each collision partner. First, we write a helper function that returns true if a given pair matches two assumed categories. For example, we want to check if a pair represents a collision between the player aircraft and a dropped pickup. We do not want the order of the parameters type1 and type2 to influence the result, that's why we check if the first node matches the first category and the second node the second category, as well as vice versa. In the vice versa case, we swap the node pointers so that their order is the same as the arguments' order. Because the first parameter colliders is passed by reference, the caller will then have a consistent ordering (colliders.first matches type1 and colliders.second matches type2).

bool matchesCategories(SceneNode::Pair& colliders,

Category::Type type1, Category::Type type2)

{

unsigned int category1 = colliders.first->getCategory(); unsigned int category2 = colliders.second->getCategory();

if (type1 & category1 && type2 & category2)

{

return true;

}

else if (type1 & category2 && type2 & category1)

{

std::swap(colliders.first, colliders.second); return true;

}

else

{

return false;

}

}

Our actual dispatch function is now rather simple. We check the whole scene graph for collisions, and fill the set with collision pairs. Then, we iterate through the set and differentiate between the collisions categories.

[ 176 ]

www.it-ebooks.info

Chapter 7

void World::handleCollisions()

{

std::set<SceneNode::Pair> collisionPairs; mSceneGraph.checkSceneCollision(mSceneGraph, collisionPairs);

FOREACH(SceneNode::Pair pair, collisionPairs)

{

if (matchesCategories(pair,

Category::PlayerAircraft, Category::EnemyAircraft))

{

... // React to player-enemy collision

}

}

}

We have four combinations of categories which trigger a collision, as shown in the following diagram:

Correspondingly, we need four calls to matchesCategories() in order to react to all possible combinations. Note that the argument pair is passed by reference—possibly its members are swapped to match the category order. Therefore, we can be sure about the pointer's categories, and safely downcast from SceneNode* to the concrete entity.

[ 177 ]

www.it-ebooks.info

Warfare Unleashed – Implementing Gameplay

We begin with the collision between the two airplanes. In this case, we always destroy the enemy, and deal damage to the player, depending on the enemy's current hitpoints.

if (matchesCategories(pair,

Category::PlayerAircraft, Category::EnemyAircraft))

{

auto& player = static_cast<Aircraft&>(*pair.first); auto& enemy = static_cast<Aircraft&>(*pair.second);

player.damage(enemy.getHitpoints());

enemy.destroy();

}

Next, we handle the case where the player's aircraft collects a pickup by touching it. We apply the effect to the player and destroy the pickup.

else if (matchesCategories(pair, Category::PlayerAircraft, Category::Pickup))

{

auto& player = static_cast<Aircraft&>(*pair.first); auto& pickup = static_cast<Pickup&>(*pair.second);

pickup.apply(player);

pickup.destroy();

}

Last, we react to the collision between projectiles and aircraft. We only consider player projectiles that hit the enemy airplanes, and enemy projectiles that hit the player's airplane. Since the reaction is the same for both cases, we can unify them. We destroy the projectile, and deal the corresponding damage to the aircraft.

else if (matchesCategories(pair, Category::EnemyAircraft, Category::AlliedProjectile)

||matchesCategories(pair,

Category::PlayerAircraft, Category::EnemyProjectile))

{

auto& aircraft = static_cast<Aircraft&>(*pair.first); auto& projectile = static_cast<Projectile&>(*pair.second);

aircraft.damage(projectile.getDamage());

projectile.destroy();

}

[ 178 ]

www.it-ebooks.info

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]