Warfare Unleashed Implementing Gameplay

Exclusive offer: get 50% off this eBook here
SFML Game Development

SFML Game Development — Save 50%

Learn how to use SFML 2.0 to develop your own feature-packed game with this book and ebook

$26.99    $13.50
by Artur Moreira Henrik Vogelius Hansson Jan Haller | July 2013 | Open Source

In this article created by Jan Haller, Henrik Vogelius Hansson, and Artur Moreira, authors of SFML Game Development, we are going to populate the game world, and implement the core part of the game; the actual gameplay with enemies, weapons, battles, and goodies. We are going to cover the following topics:

  • Enemy aircraft controlled by a simple artificial intelligence

  • Projectiles such as a machine gun or missiles

  • Pickups that improve the player's equipment

  • Collision detection and response between entities in the scene graph

  • The world's update cycle and automatic removal of entities

     

(For more resources related to this topic, see here.)

Equipping the entities

The SceneNode base class was inherited by the Entity class. Entities are the central part of this chapter. It's all about the interaction between entities of different kinds. Before starting to implement all those interactions, it is reasonable to think about crucial properties our entities need to have.

Introducing hitpoints

Since, we are preparing our airplanes for the battlefield, we need to provide them with new specific attributes. To our class definition of Entity, we add a new member variable that memorizes the current hitpoints. Hitpoints ( HP ) are a measure for the hull integrity of an entity; the entity is destroyed as soon as the hitpoints reach or fall below zero.

In addition to the member variable, we provide member functions that allow the modification of the hitpoints. We do not provide direct write access, however, the hitpoints can be decreased (the plane is damaged) or increased (the plane is repaired). Also, a destroy() function instantly destroys the entity.

class Entity : public SceneNode
{
public:
explicit Entity(int hitpoints);
void repair(int points);
void damage(int points);
void destroy();
int getHitpoints() const;
bool isDestroyed() const;
...
private:
int mHitpoints;
...
};

The implementation is as expected: repair() adds the specified hitpoints, damage() subtracts them, and destroy() sets them to zero.

Storing entity attributes in data tables

In our game, there are already two different airplanes with different attributes. For this chapter, we introduce a third one to make the game more interesting. With an increasing amount of new aircraft types, attributes such as speed, hitpoints, used texture, or fire rate may vary strongly among them. We need to think of a way to store those properties in a central place, allowing easy access to them.

What we clearly want to avoid are case differentiations in every Aircraft method, since this makes the local logic code less readable, and spreads the attributes across different functions. Instead of if/else cascades or switch statements, we can store the attributes in a central table, and just access the table every time we need an attribute.

Let's define the type of such a table entry in the case of an airplane. We choose the simplest way, and have a structure AircraftData with all members public. This type is defined in the file DataTables.hpp.

struct AircraftData
{
int hitpoints;
float speed;
Textures::ID texture;
};

While AircraftData is a single table entry, the whole table is represented as a sequence of entries, namely std::vector<AircraftData>.

Next, we write a function that initializes the table for different aircraft types. We begin to define a vector of the correct size (Aircraft::TypeCount is the last enumerator of the enum Aircraft::Type, it contains the number of different aircraft types). Since the enumerators are consecutive and begin at zero, we can use them as indices in our STL container. We thus initialize all the attributes for different airplanes, and eventually return the filled table.

std::vector<AircraftData> initializeAircraftData()
{
std::vector<AircraftData> data(Aircraft::TypeCount);
data[Aircraft::Eagle].hitpoints = 100;
data[Aircraft::Eagle].speed = 200.f;
data[Aircraft::Eagle].texture = Textures::Eagle;
data[Aircraft::Raptor].hitpoints = 20;
data[Aircraft::Raptor].speed = 80.f;
data[Aircraft::Raptor].texture = Textures::Raptor;
...
return data;
}

The global function initializeAircraftData() is declared in DataTables.hpp and defined in DataTables.cpp. It is used inside Aircraft.cpp, to initialize a global constant Table. This constant is declared locally in the .cpp file, so only the Aircraft internals can access it. In order to avoid name collisions in other files, we use an anonymous namespace.

namespace
{
const std::vector<AircraftData> Table = initializeAircraftData();
}

Inside the Aircraft methods, we can access a constant attribute of the own plane type using the member variable mType as index. For example, Table[mType].hitpoints denotes the maximal hitpoints of the current aircraft.

Data tables are only the first step of storing gameplay constants. For more flexibility, and to avoid recompiling the application, you can also store these constants externally, for example, in a simple text file or using a specific file format. The application initially loads these files, parses the values, and fills the data tables accordingly.

Nowadays, it is very common to load gameplay information from external resources. There are text-based formats such as YAML or XML, as well as, many application-specific text and binary formats. There are also well-known C++ libraries such as Boost.Serialize (www.boost. org) that help with loading and saving data structures from C++.

One possibility that has recently gained popularity consists of using script languages, most notably Lua (www.lua.org), in addition to C++. This has the advantage that not only constant data, but dynamic functionality can be outsourced and loaded during runtime.

Displaying text

We would like to add some text on the display, for example, to show the hitpoints or ammunition of different entities. Since this text information is supposed to be shown next to the entity, it stands to reason to attach it to the corresponding scene node. We therefore, create a TextNode class which inherits SceneNode as shown in the following code:

class TextNode : public SceneNode
{
public:
explicit TextNode(const FontHolder& fonts,
const std::string& text);
void setString(const std::string& text);
private:
virtual void drawCurrent(sf::RenderTarget& target,
sf::RenderStates states) const;
private:
sf::Text mText;
};

The implementation of the functions is not complicated. The SFML class sf::Text provides most of what we need. In the TextNode constructor, we retrieve the font from the resource holder and assign it to the text.

TextNode::TextNode(const FontHolder& fonts, const std::string& text)
{
mText.setFont(fonts.get(Fonts::Main));
mText.setCharacterSize(20);
setString(text);
}

The function to draw the text nodes just forwards the call to the SFML render target, as you know it from sprites.

void TextNode::drawCurrent(sf::RenderTarget& target, sf::RenderStates
states) const
{
target.draw(mText, states);
}

For the interface, mainly the following method is interesting. It assigns a new string to the text node, and automatically adapts to its size. centerOrigin() is a utility function we wrote; it sets the object's origin to its center, which simplifies positioning a lot.

void TextNode::setString(const std::string& text)
{
mText.setString(text);
centerOrigin(mText);
}

In the Aircraft constructor, we create a text node and attach it to the aircraft itself. We keep a pointer mHealthDisplay as a member variable and let it point to the attached node.

std::unique_ptr<TextNode> healthDisplay(new TextNode(fonts, ""));
mHealthDisplay = healthDisplay.get();
attachChild(std::move(healthDisplay));

In the method Aircraft::update(), we check for the current hitpoints, and convert them to a string, using our custom toString() function. The text node's string and relative position are set. Additionally, we set the text node's rotation to the negative aircraft rotation, which compensates the rotation in total. We do this in order to have the text always upright, independent of the aircraft's orientation.

mHealthDisplay->setString(toString(getHitpoints()) + " HP");
mHealthDisplay->setPosition(0.f, 50.f);
mHealthDisplay->setRotation(-getRotation());

Creating enemies

Enemies are other instances of the Aircraft class. They appear at the top of the screen and move downwards, until they fly past the bottom of the screen. Most properties are the same for the player and enemies, so we only explain the new aircraft functionality.

Movement patterns

By default, enemies fly downwards in a straight line. But it would be nice if different enemies moved differently, giving the feeling of a very basic artificial intelligence ( AI ). Thus, we introduce specific movement patterns. Such a pattern can be described as a sequence of directions to which the enemy airplane heads. A direction consists of an angle and a distance.

struct Direction
{
Direction(float angle, float distance);
float angle;
float distance;
};

Our data table for aircraft gets a new entry for the sequence of directions as shown in following code:

struct AircraftData
{
int hitpoints;
float speed;
Textures::ID texture;
std::vector<Direction> directions;
};

Let's implement a zigzag movement pattern for the Raptor plane. First, it steers for 80 units in 45 degrees direction. Then, the angle changes to -45 degrees, and the plane traverses 160 units back. Last, it moves again 80 units in +45 degrees direction, until it arrives at its original x position.

data[Aircraft::Raptor].directions.push_back(Direction( 45, 80));
data[Aircraft::Raptor].directions.push_back(Direction(-45, 160));
data[Aircraft::Raptor].directions.push_back(Direction( 45, 80));

For the Avenger plane, we use a slightly more complex pattern: it is essentially a zigzag, but between the two diagonal movements, the plane moves straight for 50 units.

data[Aircraft::Avenger].directions.push_back(Direction(+45, 50));
data[Aircraft::Avenger].directions.push_back(Direction( 0, 50));
data[Aircraft::Avenger].directions.push_back(Direction(-45, 100));
data[Aircraft::Avenger].directions.push_back(Direction( 0, 50));
data[Aircraft::Avenger].directions.push_back(Direction(+45, 50));

The following figure shows the sequence of directions for both planes; the Raptor plane is located on the left, Avenger on the right:

This way of defining movement is very simple, yet it enables a lot of possibilities. You can let the planes fly in any direction (also sideward or backwards); you can even approximate curves when using small intervals.

Now, we look at the logic we have to implement to follow these movement patterns. To the Aircraft class, we add two member variables: mTravelledDistance, which denotes the distance already travelled for each direction, and mDirectionIndex, to know which direction the plane is currently taking.

First, we retrieve the aircraft's movement pattern and store it as a reference to const named directions. We only proceed if there are movement patterns for the current type (otherwise the plane flies straight down).

void Aircraft::updateMovementPattern(sf::Time dt)
{
const std::vector<Direction>& directions
= Table[mType].directions;
if (!directions.empty())
{

Second, we check if the current direction has already been passed by the plane (that is, the travelled distance is higher than the direction's distance). If so, the index is advanced to the next direction. The modulo operator allows a cycle; after finishing the last direction, the plane begins again with the first one.

float distanceToTravel
= directions[mDirectionIndex].distance;
if (mTravelledDistance > distanceToTravel)
{
mDirectionIndex
= (mDirectionIndex + 1) % directions.size();
mTravelledDistance = 0.f;
}

Now, we have to get a velocity vector out of the angle. First, we turn the angle by 90 degrees (by default, 0 degrees points to the right), but since our planes fly downwards, we work in a rotated coordinate system, such that we can use a minus to toggle between left/right. We also have to convert degrees to radians, using our function toRadian().

The velocity's x component is computed using the cosine of the angle multiplied with the maximal speed; analogue for the y component, where the sine is used. Eventually, the travelled distance is updated:

float radians
= toRadian(directions[mDirectionIndex].angle + 90.f);
float vx = getMaxSpeed() * std::cos(radians);
float vy = getMaxSpeed() * std::sin(radians);
setVelocity(vx, vy);
mTravelledDistance += getMaxSpeed() * dt.asSeconds();
}
}

Note that if the distance to travel is no multiple of the aircraft speed, the plane will fly further than intended. This error is usually small, because there are many logic frames per second, and hardly noticeable, since each enemy will only be in the view for a short time.

Spawning enemies

It would be good if enemies were initially inactive, and the world created them as soon as they come closer to the player. By doing so, we do not need to process enemies that are relevant in the distant future; the scene graph can concentrate on updating and drawing active enemies.

We create a structure nested inside the World class that represents a spawn point for an enemy.

struct SpawnPoint
{
SpawnPoint(Aircraft::Type type, float x, float y);
Aircraft::Type type;
float x;
float y;
};

A member variable World::mEnemySpawnPoints of type std::vector<SpawnPoint> holds all future spawn points. As soon as an enemy position enters the battlefield, the corresponding enemy is created and inserted to the scene graph, and the spawn point is removed.

The World class member function getBattlefieldBounds(), returns sf::FloatRect to the battlefield area, similar to getViewBounds(). The battlefield area extends the view area by a small rectangle at the top, inside which new enemies spawn before they enter the view. If an enemy's y coordinate lies below the battlefield's top member, the enemy will be created at its spawn point. Since enemies face downwards, they are rotated by 180 degrees.

void World::spawnEnemies()
{
while (!mEnemySpawnPoints.empty()
&& mEnemySpawnPoints.back().y
> getBattlefieldBounds().top)
{
SpawnPoint spawn = mEnemySpawnPoints.back();
std::unique_ptr<Aircraft> enemy(
new Aircraft(spawn.type, mTextures, mFonts));
enemy->setPosition(spawn.x, spawn.y);
enemy->setRotation(180.f);
mSceneLayers[Air]->attachChild(std::move(enemy));
mEnemySpawnPoints.pop_back();
}
}

Now, let's insert the spawn points. addEnemy() effectively calls mEnemySpawnPoints.push_back(), and interprets the passed coordinates relative to the player's spawn position. After inserting all spawn points, we sort them by their y coordinates. By doing so, spawnEnemies() needs to check only the elements at the end of the sequence instead of iterating through it every time.

void World::addEnemies()
{
addEnemy(Aircraft::Raptor, 0.f, 500.f);
addEnemy(Aircraft::Avenger, -70.f, 1400.f);
...
std::sort(mEnemySpawnPoints.begin(), mEnemySpawnPoints.end(),
[] (SpawnPoint lhs, SpawnPoint rhs)
{
return lhs.y < rhs.y;
});
}

Here is an example of the player facing four Avenger enemies. Above each, you see how many hitpoints it has left.

Adding projectiles

Finally, time to add what makes a game fun. Shooting down stuff is essential for our game. The code to interact with the W orld class is already defined, thanks to the actions in Player and to the existing Entity base class. All that's left is to define the projectiles themselves.

We start with the Projectile class. We have normal machine gun bullets and homing missiles represented by the same class. This class inherits from the Entity class and is quite small, since it doesn't have anything special that differentiates it from other entities apart from collision tests, which we will talk about later.

class Projectile : public Entity
{
public:
enum Type
{
AlliedBullet,
EnemyBullet,
Missile,
TypeCount
};
public:
Projectile(Type type,
const TextureHolder& textures);
void guideTowards(sf::Vector2f position);
bool isGuided() const;
virtual unsigned int getCategory() const;
virtual sf::FloatRect getBoundingRect() const;
float getMaxSpeed() const;
int getDamage() const;
private:
virtual void updateCurrent(sf::Time dt,
CommandQueue& commands);
virtual void drawCurrent(sf::RenderTarget& target,
sf::RenderStates states) const;
private:
Type mType;
sf::Sprite mSprite;
sf::Vector2f mTargetDirection;
};

Nothing fun or exciting here; we add some new helper functions such as the one to guide the missile towards a target. So let's have a quick look at the implementation. You might notice, we use the same data tables that we used in the Aircraft class to store data.

Projectile::Projectile(Type type, const TextureHolder& textures)
: Entity(1)
, mType(type)
, mSprite(textures.get(Table[type].texture))
{
centerOrigin(mSprite);
}

The constructor simply creates a sprite with the texture we want for the projectile. We will check out the guide function when we actually implement the behavior of missiles. The rest of the functions don't hold anything particularly interesting. Draw the sprite and return a category for the commands and other data needed.

To get an overview of the class hierarchy in the scene graph, here is an inheritance diagram of the current scene node types. The data table structures which are directly related to their corresponding entities are shown at the bottom of the following diagram:

Firing bullets and missiles

So let's try and shoot some bullets in the game. We start with adding two new actions in the Player class: Fire and LaunchMissile. We define the default key bindings for these to be the Space bar and M keys.

Player::Player()
{
// Set initial key bindings
mKeyBinding[sf::Keyboard::Left] = MoveLeft;
mKeyBinding[sf::Keyboard::Right] = MoveRight;
mKeyBinding[sf::Keyboard::Up] = MoveUp;
mKeyBinding[sf::Keyboard::Down] = MoveDown;
mKeyBinding[sf::Keyboard::Space] = Fire;
mKeyBinding[sf::Keyboard::M] = LaunchMissile;
// ...
}
void Player::initializeActions()
{
// ...
mActionBinding[Fire].action = derivedAction<Aircraft>(
std::bind(&Aircraft::fire, _1));
mActionBinding[LaunchMissile].action =derivedAction<Aircraft>(
std::bind(&Aircraft::launchMissile, _1));
}

So when we press the keys bound to those two actions, a command will be fired which calls the aircraft's fire() and launchMissile() functions. However, we cannot put the actual code that fires the bullet or missile in those two functions. The reason is, because if we could, we would have no concept of how much time has elapsed. We don't want to fire a projectile for every frame. We want there to be some cool down until the next time we fire a bullet, to accomplish that we need to use the delta time passed in the aircraft's update() function.

Instead, we mark what we want to fire by setting the Boolean flags mIsFiring or mIsLaunchingMissile to true in the Aircraft::fire() and the Aircraft::launchMissile() functions, respectively. Then we perform the actual logic in the update() function using commands. In order to make the code clearer to read, we have extracted it to its own function.

void Aircraft::checkProjectileLaunch(sf::Time dt, CommandQueue&
commands)
{
if (mIsFiring && mFireCountdown <= sf::Time::Zero)
{
commands.push(mFireCommand);
mFireCountdown += sf::seconds(1.f / (mFireRateLevel+1));
mIsFiring = false;
}
else if (mFireCountdown > sf::Time::Zero)
{
mFireCountdown -= dt;
}
if (mIsLaunchingMissile)
{
commands.push(mMissileCommand);
mIsLaunchingMissile = false;
}
}

We have a cool down for the bullets. When enough time has elapsed since the last bullet was fired, we can fire another bullet. The actual creation of the bullet is done using a command which we will look at later. After we spawn the bullet, we reset the countdown. Here, we use += instead of =; with a simple assignment, we would discard a little time remainder in each frame, generating a bigger error as time goes by. The time of the countdown is calculated using a member variable mFireCountdown in Aircraft. Like that, we can improve the aircraft's fire rate easily. So if the fire rate level is one, then we can fire a bullet every half a second, increase it to level two, and we get every third of a second. We also have to remember to keep ticking down the countdown member, even if the user is not trying to fire. Otherwise, the countdown would get stuck when the user released the Space bar.

Next is the missile launch. We don't need a countdown here, because in the Player class, we made the input an event-based (not real-time based) input.

bool Player::isRealtimeAction(Action action)
{
switch (action)
{
case MoveLeft:
case MoveRight:
case MoveDown:
case MoveUp:
case Fire:
return true;
default:
return false;
}
}

Since the switch statement does not identify LaunchMissile as a real-time input, the user has to release the M key before he can shoot another missile. The user wants to save his missiles for the moment he needs them.

So, let's look at the commands that we perform, in order to actually shoot the projectiles. We define them in the constructor in order to have access to the texture holder. This shows one of the strengths of lambda expressions in C++11.

Aircraft::Aircraft(Type type, const TextureHolder& textures)
{
mFireCommand.category = Category::SceneAirLayer;
mFireCommand.action =
[this, &textures] (SceneNode& node, sf::Time)
{
createBullets(node, textures);
};
mMissileCommand.category = Category::SceneAirLayer;
mMissileCommand.action =
[this, &textures] (SceneNode& node, sf::Time)
{
createProjectile(node, Projectile::Missile, 0.f, 0.5f,
textures);
};
}

Now, we can pass the texture holder to the projectiles without any extra difficulty, and we don't even have to keep an explicit reference to the resources. This makes the Aircraft class and our code a lot simpler, since the reference does not need to exist in the update() function.

The commands are sent to the air layer in the scene graph. This is the node where we want to create our projectiles. The missile is a bit simpler to create than bullets, that's why we call directly Aircraft::createProjectile(). So how do we create bullets then?

void Aircraft::createBullets(SceneNode& node, const TextureHolder&
textures) const
{
Projectile::Type type = isAllied()
? Projectile::AlliedBullet : Projectile::EnemyBullet;
switch (mSpreadLevel)
{
case 1:
createProjectile(node, type, 0.0f, 0.5f, textures);
break;
case 2:
createProjectile(node, type, -0.33f, 0.33f, textures);
createProjectile(node, type, +0.33f, 0.33f, textures);
break;
case 3:
createProjectile(node, type, -0.5f, 0.33f, textures);
createProjectile(node, type, 0.0f, 0.5f, textures);
createProjectile(node, type, +0.5f, 0.33f, textures);
break;
}
}

For projectiles, we provide different levels of fire spread in order to make the game more interesting. The player can feel that progress is made, and that his aircraft becomes more powerful as he is playing. The function calls createProjectile() just as it was done for the missile.

So how do we actually create the projectile and attach it to the scene graph?

void Aircraft::createProjectile(SceneNode& node,
Projectile::Type type, float xOffset, float yOffset,
const TextureHolder& textures) const
{
std::unique_ptr<Projectile> projectile(
new Projectile(type, textures));
sf::Vector2f offset(
xOffset * mSprite.getGlobalBounds().width,
yOffset * mSprite.getGlobalBounds().height);
sf::Vector2f velocity(0, projectile->getMaxSpeed());
float sign = isAllied() ? -1.f : +1.f;
projectile->setPosition(getWorldPosition() + offset * sign);
projectile->setVelocity(velocity * sign);
node.attachChild(std::move(projectile));
}

We create the projectile with an offset from the player and a velocity required by the projectile type. Also, depending on if this projectile is shot by an enemy or the player, we will have different directions. We do not want the enemy bullets to go upwards like the player's bullets or the other way around.

Implementing gunfire for enemies is now a tiny step; instead of calling fire() when keys are pressed, we just call it always. We do this by adding the following code to the beginning of the checkProjectileLaunch() function:

if (!isAllied())
fire();

Now we have bullets that fly and split the sky.

Homing missiles

What would a modern aircraft be if it hadn't got an arsenal of homing missiles? This is where we start to add intelligence to our missiles; they should be capable of seeking enemies autonomously.

Let's first look at what we need to implement on the projectile site. For homing missiles, the functions guideTowards() and isGuided(), as well as the variable mTargetDirection are important. Their implementation looks as follows:

bool Projectile::isGuided() const
{
return mType == Missile;
}
void Projectile::guideTowards(sf::Vector2f position)
{
assert(isGuided());
mTargetDirection = unitVector(position - getWorldPosition());
}

The function unitVector() is a helper we have written. It divides a vector by its length, thus, always returns a vector of length one. The target direction is therefore a unit vector headed towards the target.

In the function updateCurrent(), we steer our missile. We change the current missile's velocity by adding small contributions of the target direction vector to it. By doing so, the velocity vector continuously approaches the target direction, having the effect that the missile flies along a curve towards the target.

approachRate is a constant that determines, to what extent the target direction contributes to the velocity. newVelocity, which is the weighted sum of the two vectors, is scaled to the maximum speed of the missile. It is assigned to the missile's velocity, and its angle is assigned to the missile's rotation. We use +90 here, because the missile texture points upwards (instead of right).

void Projectile::updateCurrent(sf::Time dt,
CommandQueue& commands)
{
if (isGuided())
{
const float approachRate = 200.f;
sf::Vector2f newVelocity = unitVector(approachRate
* dt.asSeconds() * mTargetDirection + getVelocity());
newVelocity *= getMaxSpeed();
float angle = std::atan2(newVelocity.y, newVelocity.x);
setRotation(toDegree(angle) + 90.f);
setVelocity(newVelocity);
}
Entity::updateCurrent(dt, commands);
}

Note that there are many possibilities to guide a missile. Steering behaviors define a whole field of AI; they incorporate advanced mechanisms such as evasion, interception, and group behavior. Don't hesitate to search on the internet if you're interested.

Now, we have guided the missile to a certain position, but how to retrieve that position? We want our missile to pursuit the closest enemy. For this, we switch from Projectile to the World class, where we write a new function. First, we store all currently active (that is, already spawned and not yet destroyed) enemies in the member variable mActiveEnemies. With the command facility, this task is almost trivial:

void World::guideMissiles()
{
Command enemyCollector;
enemyCollector.category = Category::EnemyAircraft;
enemyCollector.action = derivedAction<Aircraft>(
[this] (Aircraft& enemy, sf::Time)
{
if (!enemy.isDestroyed())
mActiveEnemies.push_back(&enemy);
});

Next, we have to find the nearest enemy for each missile. We set up another command, now for projectiles, that iterates through the active enemies to find the closest one. Here, distance() is a helper function that returns the distance between the centers of two scene nodes.

Command missileGuider;
missileGuider.category = Category::AlliedProjectile;
missileGuider.action = derivedAction<Projectile>(
[this] (Projectile& missile, sf::Time)
{
// Ignore unguided bullets
if (!missile.isGuided())
return;
float minDistance = std::numeric_limits<float>::max();
Aircraft* closestEnemy = nullptr;
FOREACH(Aircraft* enemy, mActiveEnemies)
{
float enemyDistance = distance(missile, *enemy);
if (enemyDistance < minDistance)
{
closestEnemy = enemy;
minDistance = enemyDistance;
}
}

In case we found a closest enemy, we let the missile chase it.

if (closestEnemy)
missile.guideTowards(
closestEnemy->getWorldPosition());
});

After defining the second command, we push both to our queue, and reset the container of active enemies. Remember that the commands are not yet executed, they wait in the queue until they are invoked on the scene graph in World::update().

mCommandQueue.push(enemyCollector);
mCommandQueue.push(missileGuider);
mActiveEnemies.clear();
}

That's it, now we are able to fire and forget!

The result looks as follows:

Picking up some goodies

Now we have implemented enemies and projectiles. But even if the player shot enemy airplanes down, and had exciting battles, he wouldn't remark that his success changes anything. You want to give the player the feeling that he is progressing in the game. Usual for this game genre are power-ups that the enemies drop when they are killed. So let's go ahead and implement that in our game.

Now this is the same story as with the projectile. Most of the things we need have already been implemented; therefore, this will be quite easy to add. What we want is only an entity that, when the player touches it, applies an effect to the player and disappears. Not much work with our current framework.

class Pickup : public Entity
{
public:
enum Type
{
HealthRefill,
MissileRefill,
FireSpread,
FireRate,
TypeCount
};
public:
Pickup(Type type,
const TextureHolder& textures);
virtual unsigned int getCategory() const;
virtual sf::FloatRect getBoundingRect() const;
void apply(Aircraft& player) const;
protected:
virtual void drawCurrent(sf::RenderTarget& target,
sf::RenderStates states) const;
private:
Type mType;
sf::Sprite mSprite;
};

So, let's start looking at a few interesting parts. As usual, we have a data table, create a sprite and center it, so the constructor looks just as you would expect it. Let's investigate the apply() function, and how the data table is created. In apply(), a function object stored in the table is invoked with player as argument. The initializePickupData() function initializes the function objects, using std::bind() that redirects to the Aircraft member functions.

void Pickup::apply(Aircraft& player) const
{
Table[mType].action(player);
}
std::vector<PickupData> initializePickupData()
{
std::vector<PickupData> data(Pickup::TypeCount);
data[Pickup::HealthRefill].texture = Textures::HealthRefill;
data[Pickup::HealthRefill].action
= std::bind(&Aircraft::repair, _1, 25);
data[Pickup::MissileRefill].texture = Textures::MissileRefill;
data[Pickup::MissileRefill].action
= std::bind(&Aircraft::collectMissiles, _1, 3);
data[Pickup::FireSpread].texture = Textures::FireSpread;
data[Pickup::FireSpread].action
= std::bind(&Aircraft::increaseSpread, _1);
data[Pickup::FireRate].texture = Textures::FireRate;
data[Pickup::FireRate].action
= std::bind(&Aircraft::increaseFireRate, _1);
return data;
}

The pickups call already defined functions on the player aircraft that let us modify its state. These functions may repair it, refill it with missiles, or improve its firepower. It's nice when things just work out of the box.

That's how the scene looks when two pickups (health and fire rate) are floating in the air. You may notice that the player's Eagle plane shoots two bullets at once, which is the result of a previously collected fire spread pickup.

SFML Game Development Learn how to use SFML 2.0 to develop your own feature-packed game with this book and ebook
Published: June 2013
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

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.

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.

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);
}

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.

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.

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

Very straightforward, isn't it? You can easily implement new interactions by adding another if clause. Want to intercept enemy missiles? No problem; add a case for Category::AlliedProjectile and Category::EnemyProjectile. Allow friendly fire, and see enemy planes taking each other down? Just write a collision for two entities of category Category::EnemyAircraft.

An outlook on optimizations

Since we test all possible scene node combinations, the number of collision checks increases quadratically (by a power of two) with the number of scene nodes. This can become a performance bottleneck if we have very many entities. There are several ways to cope with this issue.

First, needless comparisons can be reduced. Recursion can be replaced with iteration; one possible solution is to write an iterator class that traverses scene graphs. This would avoid checking each combination twice, and checking a scene node for collision with itself.

for (SceneNode::iterator left = mSceneGraph.begin();
left != mSceneGraph.end(); ++left)
{
for (SceneNode::iterator right = std::next(left);
right != mSceneGraph.end(); ++right)
{
... // Collision detection
}
}

By storing pointers to entities that are interesting for collisions (instead of all scene nodes) in a separate container, we would reduce unnecessary checks too. We could even go further and directly store the entities with their full type. For example, we might have std::vector<Aircraft*> for the enemies and std::vector<Projectile*> for the allied bullets, so there would be no need for category dispatching.

Those approaches are a good start, but the time complexity is still quadratic. In a big world, it is clearly meaningless to check every possible pair of entities for collisions, since most of them are too far away. An optimization would base on locality. We only check entities that are close to each other. In order to achieve this, the world could be divided into a grid of equally sized cells. Each entity is assigned to a cell. For collision detection, only entities inside the same cell and the neighbor cells are checked, which drastically reduces the amount of required comparisons. Going this way further would lead to data structures such as quadtrees.

Concerning collision response, if there are many cases to consider, the dispatching could be done using a two-dimensional lookup table. The categories of both partners would serve as indices, and the table entries are function objects that implement the collision response for a concrete collider pair.

As nice as these optimizations sound, there is a price to pay—the implementation becomes more complicated. A decent amount of book keeping is required to keep everything synchronous, for example, the grid. Each time an entity moves, it might move to another cell, so we have to keep track of it. Newly created entities must be inserted, and destroyed entities must be removed from the right cell.

In conclusion, such optimizations are not only nice to have, but a bare necessity when the world and the number of entities grow. However, the implied book-keeping overhead does not pay off for smaller scenarios, which is a reason why we kept things simple in our game.

An interacting world

A lot of game logic has been implemented in the different entities, now we look at functionality that is defined in the World class. You have already seen the collision in the last section.

Cleaning everything up

During the game, entities are destroyed in battle, and have to be removed from the scene graph. We do not remove them instantly. Once in a frame, we iterate through the scene graph, check which nodes have been destroyed, and detach them from their parents. To find out whether a node has been destroyed, we write the virtual function SceneNode::isDestroyed(). By default, it returns false. A derived entity may specify a condition under which it returns true. Usually, this will be the case when the hitpoints are zero or less (that is, the entity is destroyed).

bool Entity::isDestroyed() const
{
return mHitpoints <= 0;
}

In addition, we add a virtual function that checks if a scene node should be removed from the scene graph. By default, this is true as soon as the node is destroyed.

bool SceneNode::isMarkedForRemoval() const
{
return isDestroyed();
}

However, this need not always be the case. Imagine an entity that has been destroyed, but still needs to reside for some time in the world, in order to drop a pickup, show an explosion animation, or similar. While isDestroyed() tells whether entities are logically dead and therefore, don't interact with the world anymore, isMarkedForRemoval() tells whether the scene node can be removed from the scene graph. The Aircraft class itself delays removal after destruction, to let enemies drop their pickups in the update() function. There, a special flag determines the return value.

bool Aircraft::isMarkedForRemoval() const
{
return mIsMarkedForRemoval;
}

The removal is performed by the following method. In the first part, std::remove_if() rearranges the children container, so that all active nodes are at the beginning, and the ones to remove at the end. The call to erase() actually destroys these SceneNode::Ptr objects. In the second part, the function is recursively called for all child nodes. std::mem_fn() creates a function object which returns true, if and only if, the member function passed as argument returns true.

void SceneNode::removeWrecks()
{
auto wreckfieldBegin = std::remove_if(mChildren.begin(),
mChildren.end(), std::mem_fn(&SceneNode::isMarkedForRemoval));
mChildren.erase(wreckfieldBegin, mChildren.end());
std::for_each(mChildren.begin(), mChildren.end(),
std::mem_fn(&SceneNode::removeWrecks));
}

This function can now be called in World::update(), and we automatically get rid of all nodes that request their removal.

Out of view, out of the world

Most entities that leave the current view become meaningless. Launched projectiles that have missed their enemy unwaveringly follow their path in the endless void. Enemies that fly past the screen continue to fly, although the player will never see them again, which can be costly performance-wise.

In order to reduce the amount of unnecessary entities, especially having our collision algorithm in mind, we want to remove entities that are located outside the view. Remember that getBattlefieldBounds() returns sf::FloatRect, which is slightly bigger than getViewBounds(). It also contains the area beyond the view, inside which the enemies spawn. We create a command that destroys all entities, of which the bounding rectangle doesn't intersect with the battlefield's bounding rectangle (that is, they are outside).

void World::destroyEntitiesOutsideView()
{
Command command;
command.category = Category::Projectile
| Category::EnemyAircraft;
command.action = derivedAction<Entity>(
[this] (Entity& e, sf::Time)
{
if (!getBattlefieldBounds()
.intersects(e.getBoundingRect()))
e.destroy();
});
mCommandQueue.push(command);
}

The final update

A lot of new logic code has found its way into the World class; the different functions are invoked from World::update(), which currently looks as follows. The function names are self-explanatory.

void World::update(sf::Time dt)
{
mWorldView.move(0.f, mScrollSpeed * dt.asSeconds());
mPlayerAircraft->setVelocity(0.f, 0.f);
destroyEntitiesOutsideView();
guideMissiles();
while (!mCommandQueue.isEmpty())
mSceneGraph.onCommand(mCommandQueue.pop(), dt);
adaptPlayerVelocity();
handleCollisions();
mSceneGraph.removeWrecks();
spawnEnemies();
mSceneGraph.update(dt, mCommandQueue);
adaptPlayerPosition();
}

Victory and defeat

Equipped with the Eagle aircraft, you can accept the challenge to maneuver through a mission. Depending on your skill, you may reach the end of the level and become a pilot legend. Or you fall victim to the enemy fleet and die in a horrible plane crash.

Anyway, the player should be informed by the game about his fate. In most games, there are victory and defeat conditions. In our airplane game, a mission is complete if you cross the level's border at the end. A mission is failed if your plane is destroyed. To display this information, we have written GameOverState that displays an appropriate message. Of course, this can be extended to show high scores, statistics, or save the game between multiple missions. But the basic principle remains the same. We reuse the states and GUI from the previous chapters. Since their implementation should be clear, we do not paste their code here. However, you are free to look at the original code base at any time.

Summary

This was probably the most difficult chapter up to now, as it combines all the game-related features we have developed in earlier chapters: resources, world, entities, input, and commands. Despite all the things to consider, it became apparent that the existing framework made a lot of new tasks simple to achieve.

In this chapter, you learned about essential gameplay mechanics and their interaction. We added projectiles to represent bullets and homing missiles. We let enemies spawn, follow certain movement patterns, fire in regular intervals, and drop pickups upon destruction. Collision detection and response was implemented, and we discussed performance considerations. Eventually, we managed the world's update cycle, and cleaned up destroyed entities.

Now that the game foundation has been built, we are ready to add more graphical content. In the next chapter, we are going to add a variety of visual effects to improve the appearance of our game.

Resources for Article :


Further resources on this subject:


SFML Game Development Learn how to use SFML 2.0 to develop your own feature-packed game with this book and ebook
Published: June 2013
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

About the Author :


Artur Moreira

Artur Moreira is a game development enthusiast who ultimately aims to start a new game development company in his home country. He has been programming games and game-related software for over 4 years. Most of the effort in that time was put in creating an open source game-making library with lots of flexibility and portability. The library is called Nephilim and is known for supporting all major desktop and mobile operating systems, making game development fully crossplatform and fast. Alongside this big project, he keeps making his own prototypes and games for educational and commercial purposes.

Aside from the programming, he also puts some focus in creative areas such as 3D modeling, digital painting, and music composing.

Henrik Vogelius Hansson

Henrik Vogelius Hansson has always been in love with both games and programming. He started his education fairly early and continued on into the indie scene with Defrost Games and their game Project Temporality. The next company that hired him was Paradox Development Studio where he got to work on titles such as Crusader Kings 2.

Beside the game companies, Henrik has also been very active in the SFML community and has even provided a binding for Ruby called rbSFML.

Jan Haller

Jan Haller is a Master's degree student of Electrical Engineering and Information Technology. In his free time, he occasionally develops games in C++. He is also interested in the creation of graphics and 3D models.

In 2008, Jan stumbled upon SFML. Since then, he has used the library for many of his works. One of his bigger projects was a 2D Jump'n'Run game, where the character has to find its way through different levels, defeating enemies and collecting various items. During the years, Jan has actively participated in the SFML community, getting a lot of insights into the development of SFML 2. He has also written a C++ library called Thor, which extends SFML by ready-to-use features such as particle systems or animations.

Books From Packt


Mastering UDK Game Development
Mastering UDK Game Development

HTML5 Game Development with GameMaker
HTML5 Game Development with GameMaker

Learning Stencyl 3.x Game Development: Beginner's Guide
Learning Stencyl 3.x Game Development: Beginner's Guide

Torque 3D Game Development Cookbook
Torque 3D Game Development Cookbook

jQuery Game Development Essentials
jQuery Game Development Essentials

Unity iOS Game Development Beginners Guide
Unity iOS Game Development Beginners Guide

Flash Game Development by Example
Flash Game Development by Example

Unity 3.x Game Development Essentials
Unity 3.x Game Development Essentials


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
j
p
D
R
r
t
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software