- •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
Every Pixel Counts – Adding Visual Effects
sf::RenderTarget& target = ...; target.draw(vertices);
The main reason to use sf::VertexArray instead of high-level classes such as sf::Sprite is performance. The rendering performance primarily depends on the number of draw calls, that is, the number of times the CPU invokes a draw routine on the graphics card. While sprites are easier to use and good enough for many cases, there are situations where we need to exploit the possibilities of vertex arrays.
Particle systems
Visual effects such as fire, rain, or smoke have one thing in common: they have a continuously changing nature and cannot be meaningfully described using a single sprite. Even an animated sprite is too limited for many cases, because such effects should come with certain randomness. Fire may have sparks flying in arbitrary directions; smoke may be blown away by the wind.
This is why we need another model to visualize these sorts of effects: particles. A particle is a tiny object that makes up a part of the whole effect; you can imagine it as a small sprite. Each particle by itself looks boring, only in combination do they lead to an emergent visual pattern such as fire.
A particle system is a component that manages the behavior of many particles to form the desired effect. Emitters continuously create new particles and add them to the system. Affectors affect existing particles with respect to motion, fade-out, scaling, and many other properties.
Given a particle texture, we could model each particle as a sprite; the particle system could contain std::vector<sf::Sprite>. The problem with this approach is that we have to draw each sprite separately. Since particle systems may easily consist of many thousands of particles, thousands of draw calls on the GPU are not unrealistic—per frame. Now consider that not only one effect must be rendered; depending on the game, the screen may contain dozens of particle systems. Clearly, we need a technique to reduce the amount of draw calls.
This is where vertex arrays come into play. We model each particle as an object with four vertices. The vertices of all particles are inserted into a single vertex array. This gives us a method to draw everything with only one draw call.
[ 192 ]
www.it-ebooks.info
Chapter 8
Particles and particle types
In our game, we want to create an effect for the burned propellant and the emitted smoke of homing missiles. Both can be handled in a similar way, the main difference is the color. Of course, it would also be possible to use different textures. The final result is shown in the following screenshot. You don't recognize the single particles anymore, the trace of the missiles looks like a continuous stream.
[ 193 ]
www.it-ebooks.info
Every Pixel Counts – Adding Visual Effects
We define a class for particles that stores the position, color, and the time until the particle disappears. The Particle::Type data type is used to differ between smoke and propellant effects.
struct Particle
{
enum Type
{
Propellant,
Smoke, ParticleCount
};
sf::Vector2f position; sf::Color color; sf::Time lifetime;
};
We also create data tables for particles, in order to easily change their attributes in a central place. The required structure is shown here, the rest is as you know it, from the entity data tables:
struct ParticleData
{
sf::Color color; sf::Time lifetime;
};
Particle nodes
To render the particles on screen, they need to be part of the scene graph. We will to create a class ParticleNode, which can be inserted into the scene and which acts as a particle system. The class definition looks as follows:
class ParticleNode : public SceneNode
{
public: |
|
|
|
ParticleNode( |
|
|
Particle::Type type, |
|
|
const TextureHolder& textures); |
|
void |
addParticle( |
|
|
sf::Vector2f position); |
|
Particle::Type |
getParticleType() const; |
|
virtual unsigned int |
getCategory() const; |
|
... |
|
|
|
|
|
|
[ 194 ] |
|
|
|
www.it-ebooks.info
Chapter 8
private: |
|
std::deque<Particle> |
mParticles; |
const sf::Texture& |
mTexture; |
Particle::Type |
mType; |
mutable sf::VertexArray |
mVertexArray; |
mutable bool |
mNeedsVertexUpdate; |
};
Many methods for drawing and updating are already known from other
SceneNode definitions, thus not listed here. getCategory() returns
Category::ParticleSystem, a separate category. getParticleType() returns the particle type (smoke or propellant) which is stored in mType.
A new addition is addParticle(), which looks up the data table and inserts a particle into the system:
void ParticleNode::addParticle(sf::Vector2f position)
{
Particle particle; particle.position = position; particle.color = Table[mType].color;
particle.lifetime = Table[mType].lifetime;
mParticles.push_back(particle);
}
In the update method, we first remove all particles of which the lifetime has expired. Since all particles have the same initial lifetime, older particles are stored
at the beginning of the container. Therefore, it is enough to remove the front element of mParticles as long as its lifetime is smaller or equal to zero (this is also the reason why we employed std::deque). In the middle part of the function, we decrease
the lifetime of each particle by the current frame time. Finally, every time the particle container is modified, we enable a flag to express that the render geometry must be recomputed:
void ParticleNode::updateCurrent(sf::Time dt, CommandQueue&)
{
while (!mParticles.empty()
&& mParticles.front().lifetime <= sf::Time::Zero) mParticles.pop_front();
FOREACH(Particle& particle, mParticles) particle.lifetime -= dt;
mNeedsVertexUpdate = true;
}
[ 195 ]
www.it-ebooks.info
Every Pixel Counts – Adding Visual Effects
The rendering part is shown next. The mVertexArray member is declared mutable, since it is not a part of the object's logical state. This allows optimizations: we only rebuild the vertex array if something has changed, and directly before drawing (instead of after each update). This way, if the particle system is updated multiple times in a row before being drawn, we do not needlessly compute the vertices each time.
After checking whether we need to recompute the vertices, we set the sf::RenderStates texture to our particle texture and draw the vertex array:
void ParticleNode::drawCurrent(sf::RenderTarget& target, sf::RenderStates states) const
{
if (mNeedsVertexUpdate)
{
computeVertices(); mNeedsVertexUpdate = false;
}
states.texture = &mTexture; target.draw(mVertexArray, states);
}
The rebuild of the vertex array is shown in the following code snippet. First, we save the texture's full and half sizes in variables, to determine the vertex positions more easily. For size, the constructor syntax is used rather than =, because a sf::Vector2i (vector of integers) is converted to sf::Vector2f (vector of floats). We clear the vertex array, removing all vertices in it, but keeping the memory allocated:
void ParticleNode::computeVertices() const
{
sf::Vector2f size(mTexture.getSize()); sf::Vector2f half = size / 2.f;
mVertexArray.clear();
For each particle, we compute the ratio between the remaining and total lifetime— this ratio in [0, 1] is used to set the particle's alpha value in [0, 255]. The alpha value determines the transparency; therefore our particles fade out continuously until they are completely invisible:
FOREACH(const Particle& particle, mParticles)
{
sf::Vector2f pos = particle.position; sf::Color c = particle.color;
[ 196 ]
www.it-ebooks.info