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

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

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