- •Credits
- •Foreword
- •About the Authors
- •About the Reviewers
- •www.PacktPub.com
- •Table of Contents
- •Preface
- •Introducing SFML
- •Downloading and installation
- •A minimal example
- •A few notes on C++
- •Developing the first game
- •The Game class
- •Game loops and frames
- •Input over several frames
- •Vector algebra
- •Frame-independent movement
- •Fixed time steps
- •Other techniques related to frame rates
- •Displaying sprites on the screen
- •File paths and working directories
- •Real-time rendering
- •Adapting the code
- •Summary
- •Defining resources
- •Resources in SFML
- •Textures
- •Images
- •Fonts
- •Shaders
- •Sound buffers
- •Music
- •A typical use case
- •Graphics
- •Audio
- •Acquiring, releasing, and accessing resources
- •An automated approach
- •Finding an appropriate container
- •Loading from files
- •Accessing the textures
- •Error handling
- •Boolean return values
- •Throwing exceptions
- •Assertions
- •Generalizing the approach
- •Compatibility with sf::Music
- •A special case – sf::Shader
- •Summary
- •Entities
- •Aircraft
- •Alternative entity designs
- •Rendering the scene
- •Relative coordinates
- •SFML and transforms
- •Scene graphs
- •Scene nodes
- •Node insertion and removal
- •Making scene nodes drawable
- •Drawing entities
- •Connecting entities with resources
- •Aligning the origin
- •Scene layers
- •Updating the scene
- •One step back – absolute transforms
- •The view
- •Viewport
- •View optimizations
- •Resolution and aspect ratio
- •View scrolling
- •Zoom and rotation
- •Landscape rendering
- •SpriteNode
- •Landscape texture
- •Texture repeating
- •Composing our world
- •World initialization
- •Loading the textures
- •Building the scene
- •Update and draw
- •Integrating the Game class
- •Summary
- •Polling events
- •Window events
- •Joystick events
- •Keyboard events
- •Mouse events
- •Getting the input state in real time
- •Events and real-time input – when to use which
- •Delta movement from the mouse
- •Playing nice with your application neighborhood
- •A command-based communication system
- •Introducing commands
- •Receiver categories
- •Command execution
- •Command queues
- •Handling player input
- •Commands in a nutshell
- •Implementing the game logic
- •A general-purpose communication mechanism
- •Customizing key bindings
- •Why a player is not an entity
- •Summary
- •Defining a state
- •The state stack
- •Adding states to StateStack
- •Handling updates, input, and drawing
- •Input
- •Update
- •Draw
- •Delayed pop/push operations
- •The state context
- •Integrating the stack in the Application class
- •Navigating between states
- •Creating the game state
- •The title screen
- •Main menu
- •Pausing the game
- •The loading screen – sample
- •Progress bar
- •ParallelTask
- •Thread
- •Concurrency
- •Task implementation
- •Summary
- •The GUI hierarchy, the Java way
- •Updating the menu
- •The promised key bindings
- •Summary
- •Equipping the entities
- •Introducing hitpoints
- •Storing entity attributes in data tables
- •Displaying text
- •Creating enemies
- •Movement patterns
- •Spawning enemies
- •Adding projectiles
- •Firing bullets and missiles
- •Homing missiles
- •Picking up some goodies
- •Collision detection and response
- •Finding the collision pairs
- •Reacting to collisions
- •An outlook on optimizations
- •An interacting world
- •Cleaning everything up
- •Out of view, out of the world
- •The final update
- •Victory and defeat
- •Summary
- •Defining texture atlases
- •Adapting the game code
- •Low-level rendering
- •OpenGL and graphics cards
- •Understanding render targets
- •Texture mapping
- •Vertex arrays
- •Particle systems
- •Particles and particle types
- •Particle nodes
- •Emitter nodes
- •Affectors
- •Embedding particles in the world
- •Animated sprites
- •The Eagle has rolled!
- •Post effects and shaders
- •Fullscreen post effects
- •Shaders
- •The bloom effect
- •Summary
- •Music themes
- •Loading and playing
- •Use case – In-game themes
- •Sound effects
- •Loading, inserting, and playing
- •Removing sounds
- •Use case – GUI sounds
- •Sounds in 3D space
- •The listener
- •Attenuation factor and minimum distance
- •Positioning the listener
- •Playing spatial sounds
- •Use case – In-game sound effects
- •Summary
- •Playing multiplayer games
- •Interacting with sockets
- •Socket selectors
- •Custom protocols
- •Data transport
- •Network architectures
- •Peer-to-peer
- •Client-server architecture
- •Authoritative servers
- •Creating the structure for multiplayer
- •Working with the Server
- •Server thread
- •Server loop
- •Peers and aircraft
- •Hot Seat
- •Accepting new clients
- •Handling disconnections
- •Incoming packets
- •Studying our protocol
- •Understanding the ticks and updates
- •Synchronization issues
- •Taking a peek in the other end – the client
- •Client packets
- •Transmitting game actions via network nodes
- •The new pause state
- •Settings
- •The new Player class
- •Latency
- •Latency versus bandwidth
- •View scrolling compensation
- •Aircraft interpolation
- •Cheating prevention
- •Summary
- •Index
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