Before we start coding, it will be helpful to see exactly what it is we are trying to achieve. Take a look at the following screenshot:
This is a screenshot of the particle effect on a plain background. We will use the effect in our game.
The way we achieve the effect is as follows:
We will use a VertexArray
to draw all the dots and the primitive type of Point
to represent each particle visually. Furthermore, we will inherit from Drawable
so that our particle system can take care of drawing itself.
The Particle
class will be a simple class that represents just one of the 1,000 particles. Let's get coding.
Right-click Header Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) Header File (.h
) and then in the Name field, type Particle.h
. Finally, click the Add button. We are now ready to code the header file for the Particle
class.
Add the following code to the Particle.h
file:
#pragma once #include <SFML/Graphics.hpp> using namespace sf; class Particle { private: Vector2f m_Position; Vector2f m_Velocity; public: Particle(Vector2f direction); void update(float dt); void setPosition(Vector2f position); Vector2f getPosition(); };
In the preceding code, we have two Vector2f
objects. One will represent the horizontal and vertical coordinates of the particle and the other will represent the horizontal and vertical speed.
We also have a number of public functions. First is the constructor. It takes a Vector2f
, which will be used to let it know what direction/velocity this particle will have. This implies that the system, not the particle itself, will be choosing the velocity.
Next is the update
function, which takes the time the previous frame has taken. We will use this to move the particle by precisely the correct amount.
The final two functions, setPosition
and getPosition
, are used to move the particle into position and find out its position, respectively.
All these functions will make complete sense when we code them.
Right-click Source Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) C++ File (.cpp
) and then in the Name field, type Particle.cpp
. Finally, click the Add button. We are now ready to code the .cpp
file for the Particle
class.
Add the following code to Particle.cpp
:
#include "stdafx.h" #include "Particle.h" Particle::Particle(Vector2f direction) { // Determine the direction //m_Velocity = direction; m_Velocity.x = direction.x; m_Velocity.y = direction.y; } void Particle::update(float dtAsSeconds) { // Move the particle m_Position += m_Velocity * dtAsSeconds; } void Particle::setPosition(Vector2f position) { m_Position = position; } Vector2f Particle::getPosition() { return m_Position; }
All these functions use concepts we have seen before. The constructor sets up the m_Velocity.x
and m_Velocity.y
values using the passed-in Vector2f
object.
The update
function moves the horizontal and vertical positions of the particle by multiplying m_Velocity
by the elapsed time (dtAsSeconds
). Notice that to achieve this, we simply add the two Vector2f
objects together. There is no need to perform calculations for both the x and y members separately.
The setPosition
function, as previously explained, initializes the m_Position
object with the passed-in values. The getPosition
function returns m_Position
to the calling code.
We now have a fully functioning Particle
class. Next, we will code a ParticleSystem
class to spawn and control the particles.
The ParticleSystem
class does most of the work for our particle effects. It is this class that we will create an instance of in the Engine
class.
Right-click Header Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) Header File (.h
) and then in the Name field, type ParticleSystem.h
. Finally, click the Add button. We are now ready to code the header file for the ParticleSystem
class.
Add the code for the ParticleSystem
class to ParticleSystem.h
:
#pragma once #include <SFML/Graphics.hpp> #include "Particle.h" using namespace sf; using namespace std; class ParticleSystem : public Drawable { private: vector<Particle> m_Particles; VertexArray m_Vertices; float m_Duration; bool m_IsRunning = false; public: virtual void draw(RenderTarget& target, RenderStates states) const; void init(int count); void emitParticles(Vector2f position); void update(float elapsed); bool running(); };
Let's go through this a bit at a time. Firstly, notice that we are inheriting from Drawable
. This is what will enable us to pass our ParticleSystem
instance to m_Window.draw
, because ParticleSystem
is a Drawable
.
There is a vector named m_Particles
, of type Particle
. This vector will hold each and every instance of Particle
. Next we have a VertexArray
called m_Vertices
. This will be used to draw all the particles in the form of a whole bunch of Point
primitives.
The m_Duration
, float
variable is how long each effect will last. We will initialize it in the constructor function.
The Boolean m_IsRunning
variable will be used to indicate whether the particle system is currently in use or not.
Next, in the public section, we have the pure virtual function, draw
, that we will soon implement to handle what happens when we pass our instance of ParticleSystem
to m_Window.draw
.
The init
function will prepare the VertexArray
and the vector
. It will also initialize all the Particle
objects (held by the vector
) with their velocities and initial positions.
The update
function will loop through each and every Particle
instance in the vector
and call their individual update
functions.
The running
function provides access to the m_IsRunning
variable so that the game engine can query whether or not the ParticleSystem
is currently in use.
Let's code the function definitions to see what goes on inside ParticleSystem
.
Right-click Source Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) C++ File (
.cpp
) and then in the Name field, type ParticleSystem.cpp
. Finally, click the Add button. We are now ready to code the .cpp
file for the ParticleSystem
class.
We will split this file into five sections to code and discuss it better. Add the first section of code as shown here:
#include "stdafx.h" #include <SFML/Graphics.hpp> #include "ParticleSystem.h" using namespace sf; using namespace std; void ParticleSystem::init(int numParticles) { m_Vertices.setPrimitiveType(Points); m_Vertices.resize(numParticles); // Create the particles for (int i = 0; i < numParticles; i++) { srand(time(0) + i); float angle = (rand() % 360) * 3.14f / 180.f; float speed = (rand() % 600) + 600.f; Vector2f direction; direction = Vector2f(cos(angle) * speed, sin(angle) * speed); m_Particles.push_back(Particle(direction)); } }
After the necessary includes
, we have the definition of the init
function. We call setPrimitiveType
with Points
as the argument so that m_VertexArray
knows what types of primitive it will be dealing with. We resize m_Vertices
with numParticles
, which was passed in to the init
function when it was called.
The for
loop creates random values for speed and angle. It then uses trigonometric functions to convert those values into a vector, which is stored in the Vector2f
, direction
.
If you want to know more about how the trigonometric functions (cos
, sin
, and tan
) convert angles and speeds into a vector, you can take a look at this article series: http://gamecodeschool.com/essentials/calculating-heading-in-2d-games-using-trigonometric-functions-part-1/
The last thing that happens in the for
loop (and the init
function) is that the vector is passed in to the Particle
constructor. The new Particle
instance is stored in m_Particles
using the push_back
function. Therefore, a call to init
with a value of 1000
would mean we have one thousand instances of Particle
, with random velocity, stashed away in m_Particles
just waiting to blow!
Next, add the update
function to ParticleSysytem.cpp
:
void ParticleSystem::update(float dt) { m_Duration -= dt; vector<Particle>::iterator i; int currentVertex = 0; for (i = m_Particles.begin(); i != m_Particles.end(); i++) { // Move the particle (*i).update(dt); // Update the vertex array m_Vertices[currentVertex].position = (*i).getPosition(); // Move to the next vertex currentVertex++; } if (m_Duration < 0) { m_IsRunning = false; } }
The update
function is simpler than it looks at first glance. First of all, m_Duration
is reduced by the passed-in time, dt
. This is so we know when the two seconds have elapsed. A vector iterator, i
, is declared for use with m_Particles
.
The for
loop goes through each of the Particle
instances in m_Particles
. For each and every one it calls its update
function and passes in dt
. Each particle will update its position. After the particle has updated itself, the appropriate vertex in m_Vertices
is updated by using the particle's getPosition
function. At the end of each pass through, the for
loop currentVertex
is incremented, ready for the next vertex.
After the for
loop has completed, if(m_Duration < 0)
checks whether it is time to switch off the effect. If two seconds have elapsed, m_IsRunning
is set to false
.
Next, add the emitParticles
function:
void ParticleSystem::emitParticles(Vector2f startPosition) { m_IsRunning = true; m_Duration = 2; vector<Particle>::iterator i; int currentVertex = 0; for (i = m_Particles.begin(); i != m_Particles.end(); i++) { m_Vertices[currentVertex].color = Color::Yellow; (*i).setPosition(startPosition); currentVertex++; } }
This is the function we will call to start the particle system running. So, predictably, we set m_IsRunning
to true
and m_Duration
to 2
. We declare an iterator
, i
, to iterate through all the Particle
objects in m_Particles
, and then we do so in a for
loop.
Inside the for
loop, we set each particle in the vertex array to yellow and set each position to startPosition
, which was passed in as a parameter. Remember that each particle starts life in exactly the same position, but they are each assigned a different velocity.
Next, add the pure virtual draw function definition:
void ParticleSystem::draw(RenderTarget& target, RenderStates states) const { target.draw(m_Vertices, states); }
In the previous code, we simply use target
to call draw
, passing in m_Vertices
and states
. This is exactly as we discussed when talking about Drawable
earlier in the chapter, except we pass in our VertexArray
, which holds 1,000 point primitives instead of the hypothetical spaceship Sprite.
Finally, add the running function:
bool ParticleSystem::running() { return m_IsRunning; }
The running
function is a simple getter function that returns the value of m_IsRunning
. We will see where this is useful to determine the current state of the particle system.
To put our particle system to work is very straightforward, especially because we inherited from Drawable
.
Open Engine.h
and add a ParticleSystem
object, as shown in the following highlighted code:
#pragma once #include <SFML/Graphics.hpp> #include "TextureHolder.h" #include "Thomas.h" #include "Bob.h" #include "LevelManager.h" #include "SoundManager.h" #include "HUD.h" #include "ParticleSystem.h" using namespace sf; class Engine { private: // The texture holder TextureHolder th; // create a particle system ParticleSystem m_PS; // Thomas and his friend, Bob Thomas m_Thomas; Bob m_Bob;
Next, initialize the system.
Open the Engine.cpp
file and add the short highlighted code right at the end of the Engine
constructor:
Engine::Engine()
{
// Get the screen resolution and create an SFML window and View
Vector2f resolution;
resolution.x = VideoMode::getDesktopMode().width;
resolution.y = VideoMode::getDesktopMode().height;
m_Window.create(VideoMode(resolution.x, resolution.y),
"Thomas was late",
Style::Fullscreen);
// Initialize the full screen view
m_MainView.setSize(resolution);
m_HudView.reset(
FloatRect(0, 0, resolution.x, resolution.y));
// Inititialize the split-screen Views
m_LeftView.setViewport(
FloatRect(0.001f, 0.001f, 0.498f, 0.998f));
m_RightView.setViewport(
FloatRect(0.5f, 0.001f, 0.499f, 0.998f));
m_BGLeftView.setViewport(
FloatRect(0.001f, 0.001f, 0.498f, 0.998f));
m_BGRightView.setViewport(
FloatRect(0.5f, 0.001f, 0.499f, 0.998f));
// Can this graphics card use shaders?
if (!sf::Shader::isAvailable())
{
// Time to get a new PC
m_Window.close();
}
m_BackgroundTexture = TextureHolder::GetTexture(
"graphics/background.png");
// Associate the sprite with the texture
m_BackgroundSprite.setTexture(m_BackgroundTexture);
// Load the texture for the background vertex array
m_TextureTiles = TextureHolder::GetTexture(
"graphics/tiles_sheet.png");
// Initialize the particle system
m_PS.init(1000);
}// End Engine constructor
The VertexArray
and the vector
of Particle
instances are ready for action.
Open the Update.cpp
file and add the following highlighted code. It can go right at the end of the update
function:
// Update the HUD every m_TargetFramesPerHUDUpdate frames
if (m_FramesSinceLastHUDUpdate > m_TargetFramesPerHUDUpdate)
{
// Update game HUD text
stringstream ssTime;
stringstream ssLevel;
// Update the time text
ssTime << (int)m_TimeRemaining;
m_Hud.setTime(ssTime.str());
// Update the level text
ssLevel << "Level:" << m_LM.getCurrentLevel();
m_Hud.setLevel(ssLevel.str());
m_FramesSinceLastHUDUpdate = 0;
}
// Update the particles
if (m_PS.running())
{
m_PS.update(dtAsSeconds);
}
}// End of update function
All that is needed in the previous code is the call to update
. Notice that it is wrapped in a check to make sure the system is currently running. If it isn't running, there is no point updating it.
Open the DetectCollisions.cpp
file, which has the detectCollisions
function in it. We left a comment in it when we originally coded it back in Chapter 15, Building Playable Levels and Collision Detection.
Identify the correct place from the context and add the highlighted code, as shown:
// Is character colliding with a regular block
if (m_ArrayLevel[y][x] == 1)
{
if (character.getRight().intersects(block))
{
character.stopRight(block.left);
}
else if (character.getLeft().intersects(block))
{
character.stopLeft(block.left);
}
if (character.getFeet().intersects(block))
{
character.stopFalling(block.top);
}
else if (character.getHead().intersects(block))
{
character.stopJump();
}
}
// More collision detection here once
// we have learned about particle effects
// Has the character's feet touched fire or water?
// If so, start a particle effect
// Make sure this is the first time we have detected this
// by seeing if an effect is already running
if (!m_PS.running())
{
if (m_ArrayLevel[y][x] == 2 || m_ArrayLevel[y][x] == 3)
{
if (character.getFeet().intersects(block))
{
// position and start the particle system
m_PS.emitParticles(character.getCenter());
}
}
}
// Has the character reached the goal?
if (m_ArrayLevel[y][x] == 4)
{
// Character has reached the goal
reachedGoal = true;
}
First the code checks if the particle system is already running. If it isn't, it checks if the current tile being checked is either a water or a fire tile. If either is the case, it checks whether the character's feet are in contact. When each of these if
statements is true
, the particle system is started by calling the emitParticles
function and passing in the location of the center of the character as the coordinates to start the effect.
This is the best bit. See how easy it is to draw the ParticleSystem
. We pass our instance directly to the m_Window.draw
function after checking that the particle system is actually running.
Open the Draw.cpp
file and add the highlighted code in all the places shown in the following code:
void Engine::draw() { // Rub out the last frame m_Window.clear(Color::White); if (!m_SplitScreen) { // Switch to background view m_Window.setView(m_BGMainView); // Draw the background m_Window.draw(m_BackgroundSprite); // Switch to m_MainView m_Window.setView(m_MainView); // Draw the Level m_Window.draw(m_VALevel, &m_TextureTiles); // Draw thomas m_Window.draw(m_Thomas.getSprite()); // Draw thomas m_Window.draw(m_Bob.getSprite()); // Draw the particle system if (m_PS.running()) { m_Window.draw(m_PS); } } else { // Split-screen view is active // First draw Thomas' side of the screen // Switch to background view m_Window.setView(m_BGLeftView); // Draw the background m_Window.draw(m_BackgroundSprite); // Switch to m_LeftView m_Window.setView(m_LeftView); // Draw the Level m_Window.draw(m_VALevel, &m_TextureTiles); // Draw thomas m_Window.draw(m_Bob.getSprite()); // Draw thomas m_Window.draw(m_Thomas.getSprite()); // Draw the particle system if (m_PS.running()) { m_Window.draw(m_PS); } // Now draw Bob's side of the screen // Switch to background view m_Window.setView(m_BGRightView); // Draw the background m_Window.draw(m_BackgroundSprite); // Switch to m_RightView m_Window.setView(m_RightView); // Draw the Level m_Window.draw(m_VALevel, &m_TextureTiles); // Draw thomas m_Window.draw(m_Thomas.getSprite()); // Draw bob m_Window.draw(m_Bob.getSprite()); // Draw the particle system if (m_PS.running()) { m_Window.draw(m_PS); } } // Draw the HUD // Switch to m_HudView m_Window.setView(m_HudView); m_Window.draw(m_Hud.getLevel()); m_Window.draw(m_Hud.getTime()); if (!m_Playing) { m_Window.draw(m_Hud.getMessage()); } // Show everything we have just drawn m_Window.display(); }
Notice in the previous code that we have to draw the particle system in all of the left, right, and full-screen code blocks.
Run the game and move one of the character's feet over the edge of a fire tile. Notice the particle system burst into life:
Now for something else that is new.
3.129.70.244