Chapter 18: Particle Systems and Shaders

In this chapter, we will look at what a particle system is and then go ahead and code one into our game. We will scratch the surface of the topic of OpenGL shaders and see how writing code in another language (GLSL), that can be run directly on the graphics card, can lead to smooth graphical effects that might otherwise be impossible. As usual, we will also use our new skills and knowledge to enhance the current project.

In this chapter, we will cover the following topics:

  • Building a particle system
  • OpenGL shaders and GLSL
  • Using shaders in the Thomas Was Late game

Building a particle system

Before we start coding, it will be helpful to see exactly what it is that we are trying to achieve.

Take a look at the following diagram:

The previous illustration is a screenshot of the particle effect on a plain background. We will use this effect in our game. We will spawn one of these effects each time the player dies.

The way we achieve this effect is as follows:

  1. First, we spawn 1,000 dots (particles), one on top of the other, at a chosen pixel position.
  2. Each frame of the game moves each of the 1,000 particles outwards at a predetermined but random speed and angle.
  3. Repeat step two for two seconds and then make the particles disappear.

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 the SFML Drawable class so that our particle system can take care of drawing itself.

Coding the Particle class

The Particle class will be a simple class that represents just one particle from a thousand particles. Let's get coding.

Coding Particle.h

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 coordinate of the particle, while the other will represent the horizontal and vertical speed.

Important note

When you have a rate of change (speed) in more than one direction, the combined values also define a direction. This is called velocity. Hence,Vector2f is called m_Velocity.

We also have several public functions. First is the constructor. It takes a Vector2f and uses this to let it know which 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 in position and find out its position, respectively.

All of these functions will make complete sense when we code them.

Coding the Particle.cpp file

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 "Particle.h"

Particle::Particle(Vector2f direction)

{

    // Determine the 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 of 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 how, 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 we explained previously, 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.

Coding the ParticleSystem class

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. Before we do, however, let's talk a little bit more about OOP and the SFML Drawable class.

Exploring SFML's Drawable class and OOP

The Drawable class has just one function. It has no variables either. Furthermore, its one and only function is pure virtual. This means that, if we inherit from Drawable, we must implement its one and only function. The purpose, as a reminder from Chapter 14, Abstraction and Code Management – Making Better Use of OOP, is that we can then use our class that inherits from drawable as a polymorphic type. Put more simply, anything that SFML allows us to do with a Drawable object, we will be able to do with our class that inherits from it. The only requirement is that we must provide a definition for the pure virtual function, draw.

Some classes that inherit from Drawable already include Sprite and VertexArray (among others). Whenever we have used Sprite or VertexArray, we passed them to the draw function of the RenderWindow class.

The reason that we have been able to draw every object we have ever drawn, in this entire book, is because they have all been inherited from Drawable. We can use this knowledge to our advantage.

We can inherit from Drawable with any object we like, as long as we implement the pure virtual draw function. This is also a straightforward process. Consider a hypothetical SpaceShip class. The header file (SpaceShip.h) of the SpaceShip class that inherits from Drawable would look like this:

class SpaceShip : public Drawable

{

private:

    Sprite m_Sprite;

    // More private members

public:

    virtual void draw(RenderTarget& target,

        RenderStates states) const;

    // More public members

};

In the previous code, we can see the pure virtual draw function and a Sprite instance. Notice there is no way to access the private Sprite outside of the class – not even a getSprite function!

The SpaceShip.cpp file would look something like this:

void SpaceShip::SpaceShip

{

    // Set up the spaceship

}

void SpaceShip::draw(RenderTarget& target, RenderStates states) const

{

    target.draw(m_Sprite, states);

}

// Any other functions

In the previous code, notice the simple implementation of the draw function. The parameters are beyond the scope of this book. Just note that the target parameter is used to call draw and passes in m_Sprite as well as states, the other parameter.

Tip

While it is not necessary to understand the parameters to take full advantage of Drawable, in the context of this book, you might be intrigued. You can read more about SFML Drawable on the SFML website here: https://www.sfml-dev.org/tutorials/2.5/graphics-vertex-array.php.

In the main game loop, we could now treat a SpaceShip instance as if it were a Sprite or any other class that inherits from Drawable, like so:

SpaceShip m_SpaceShip;

// create other objects here

// ...

// In the draw function

// Rub out the last frame

m_Window.clear(Color::Black);

// Draw the spaceship

m_Window.draw(m_SpaceShip);

// More drawing here

// ...

// Show everything we have just drawn

m_Window.display();

It is because SpaceShip is a Drawable that we can treat it like it was a Sprite or VertexArray and, because we overrode the pure virtual draw function, everything just works as we want it to. You will use this approach in this chapter to draw the particle system.

While we are on the subject of OOP, let's look at an alternative way of encapsulating the drawing code into the game object that we will use in the next project.

An alternative to inheriting from Drawable

It is also possible to keep all the drawing functionality within the class that is the object to be drawn by implementing our own function, within our class, perhaps by using the following code:

void drawThisObject(RenderWindow window)

{

    window.draw(m_Sprite)

}

The previous code assumes that m_Sprite represents the visual appearance of the current class we are drawing, as it has throughout this and the previous project. Assuming that the instance of the class that contains the drawThisObject function is called playerHero and further assuming we have an instance of RenderWindow called m_Window, we could then draw the object from the main game loop with this code:

 playerHero.draw(m_Window);

In this solution, we pass the RenderWindow, m_Window, into the drawThisObject function as a parameter. The drawThisObject function then uses RenderWindow to draw the Sprite, m_Sprite.

If we have a more complicated set of game objects, then passing a reference of RenderWindow to the object to be drawn, each frame, so it can draw itself, is a good tactic.

We will use this tactic in the final project of this book, which we will start in the next chapter. Let's finish the particle system by coding the ParticleSystem class, which will inherit from Drawable.

Coding ParticleSystem.h

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. First, notice that we are inheriting from SFML's Drawable class. This is what will allow us to pass our ParticleSystem instance to m_Window.draw, because ParticleSystem is a Drawable. And, since we inherit from Drawable, we can override the draw function using the same function signature as the Drawable class uses internally. Shortly, when we use the ParticleSystem class, we will see the following code.

m_Window.draw(m_PS);

The m_PS object is an instance of our ParticleSystem class, and we will pass it directly to the draw function of the RenderWindow class, just like we have done for the Sprite, VertexArray, and RectangleShape instances. All this is made possible by the power of inheritance and polymorphism.

Tip

Don't add the m_Window.draw… code just yet; we have a bit more work to do first.

There is a vector named m_Particles of the Particle type. 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 m_IsRunning Boolean 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 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 the ParticleSystem is currently in use.

Let's code the function definitions to see what goes on inside ParticleSystem.

Coding the ParticleSystem.cpp file

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 so that we can code and discuss it in more detail. Add the first section of code, as follows:

#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 type of primitives 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.

Tip

If you want to know more about how the trigonometric functions (cos and sin) convert angles and speeds into a vector, then 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 into 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 1,000 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();

    }

    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 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 the code, 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;

    

    int currentVertex = 0;

    for (auto it = m_Particles.begin();

         it != m_Particles.end();

         it++)

    {

        m_Vertices[currentVertex++].color = Color::Yellow;

        it->setPosition(startPosition);

    }

}

This is the function we will call to start the particle system. 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 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 preceding code, we simply use target to call draw, passing m_Vertices and states as parameters. Remember that we will never call this function directly! Shortly, when we declare an instance of ParticleSystem, we will pass that instance to the RenderWindow draw function. The draw function we have just coded will be called internally from there.

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 in this chapter, so that we can determine the current state of the particle system.

Using the ParticleSystem object

Putting our particle

system to work is very straightforward, especially because we inherited from Drawable.

Adding a ParticleSystem object to the Engine class

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;

Now, we need to initialize the system.

Initializing ParticleSystem

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

    // Initialize 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.

Updating the particle system each frame

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.

Starting the particle system

Open the DetectCollisions.cpp file, which has the detectCollisions function in it. We left a comment in it when we originally coded it.

Identify the correct place from the context and add the following highlighted code:

// 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

// Have the characters' 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 fire tile. If either is the case, it checks whether the character's feet are in contact with it. When each of these if statements are 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.

Drawing the particle system

This is the best bit. See how easy it is to draw ParticleSystem. We pass our instance directly to the m_Window.draw function, after checking that the particle system is running.

Open the Draw.cpp file and add the following highlighted code in all the necessary places:

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 bob

        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 bob

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

}

Note that we must 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, it's time for something else that's new.

OpenGL, Shaders, and GLSL

The Open Graphics Library (OpenGL) is a programming library that handles 2D as well as 3D graphics. OpenGL works on all major desktop operating systems and there is also a version that works on mobile devices, known as OpenGL ES.

OpenGL was originally released in 1992. It has been refined and improved over more than twenty years. Furthermore, graphics card manufacturers design their hardware to make it work well with OpenGL. The point of mentioning this is not for the history lesson but to explain that it would be a fool's errand to try and improve upon OpenGL and use it in 2D (and 3D games) on the desktop, especially if we want our game to run on more than just Windows, which is the obvious choice. We are already using OpenGL because SFML uses OpenGL. Shaders are programs that run on the GPU itself. We'll find out more about them in the following section.

The programmable pipeline and shaders

Through OpenGL, we have access to what is called a programmable pipeline. We can send our graphics off to be drawn, each frame, with the RenderWindow instance's draw function. We can also write code that runs on the GPU that can manipulate each and every pixel independently, after the call to draw. This is a very powerful feature.

This extra code that runs on the GPU is called a shader program. We can write code to manipulate the geometry (position) of our graphics in a vertex shader. We can also write code that manipulates the appearance of every pixel individually in code. This is known as a fragment shader.

Although we will not be exploring shaders in any great depth, we will write some shader code using the GL Shader Language (GLSL) and we will get a glimpse of the possibilities that it offers.

In OpenGL, everything is a point, a line, or a triangle. In addition, we can attach colors and textures to this basic geometry, and we can also combine these elements to make the complex graphics that we see in today's modern games. These are collectively known as primitives. We have access to OpenGL primitives through the SFML primitives and VertexArray, as well as the Sprite and Shape classes.

In addition to primitives, OpenGL uses matrices. Matrices are a method and structure for performing arithmetic. This arithmetic can range from extremely simple high school-level calculations such as  moving (translating) a coordinate or it can be quite complex, such as performing more advanced mathematics, for example, to convert our game world coordinates into OpenGL screen coordinates that the GPU can use. Fortunately, it is this complexity that SFML handles for us behind the scenes. SFML also allows us to handle OpenGL directly.

Tip

If you want to find out more about OpenGL, you can get started here: http://learnopengl.com/#!Introduction. If you want to use OpenGL directly, alongside SFML, you can read this article to find out more: https://www.sfml-dev.org/tutorials/2.5/window-opengl.php.

An application can have many shaders. We can then attach different shaders to different game objects to create the desired effects. We will only have one vertex and one fragment shader in this game. We will apply it to every frame, as well as to the background.

However, when you see how to attach a shader to a draw call, it will be plain that it is trivial to have more shaders.

We will follow these steps:

  1. First, we need the code for the shader that will be executed on the GPU.  
  2. Then, we need to compile that code.
  3. Finally, we need to attach the shader to the appropriate draw function call in the draw function of our game engine.

GLSL is a language and it also has its own types, and variables of those types, which can be declared and utilized.  Furthermore, we can interact with the shader program's variables from our C++ code.

As we will see, GLSL has some syntax similarities to C++.

Coding a fragment shader

Here is the code from the rippleShader.frag file in the shaders folder. We don't need to code this because it is in the assets that we added back in Chapter 14, Abstraction and Code Management – Making Better Use of OOP:

// attributes from vertShader.vert

varying vec4 vColor;

varying vec2 vTexCoord;

// uniforms

uniform sampler2D uTexture;

uniform float uTime;

void main() {

    float coef = sin(gl_FragCoord.y * 0.1 + 1 * uTime);

    vTexCoord.y +=  coef * 0.03;

    gl_FragColor = vColor * texture2D(uTexture, vTexCoord);

}

The first four lines (excluding comments) are the variables that the fragment shader will use, but they are not ordinary variables. The first type we can see is varying. These are variables which are in scope between both shaders. Next, we have the uniform variables. These variables can be manipulated directly from our C++ code. We will see how we do this soon.

In addition to the varying and uniform types, each of the variables also has a more conventional type that defines the actual data, as follows:

  • vec4 is a vector with four values.
  • vec2 is a vector with two values.
  • sampler2d will hold a texture.
  • float is just like a float data type in C++.

The code inside the main function is executed. If we look closely at the code in main, we will see each of the variables in use. Exactly what this code does is beyond the scope of the book. In summary, however, the texture coordinates (vTexCoord) and the color of the pixels/fragments (glFragColor) are manipulated by several mathematical functions and operations. Remember that this executes for each pixel involved in the draw function that's called on each frame of our game. Furthermore, be aware that uTime is passed in as a different value for each frame. The result, as we will soon see, will be a rippling effect.

Coding a vertex shader

Here is the code from the vertShader.vert file. You don't need to code this. It was in the assets we added back in Chapter 14, Abstraction and Code Management – Making Better Use of OOP:

//varying "out" variables to be used in the fragment shader

varying vec4 vColor;

varying vec2 vTexCoord;

 

void main() {

    vColor = gl_Color;

    vTexCoord = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;

    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

}

First of all, notice the two varying variables. These are the very same variables that we manipulated back in the fragment shader. In the main function, the code manipulates the position of each and every vertex. How the code works is beyond the scope of this book, but there is some quite in-depth mathematics going on behind the scenes. If it interests you, then exploring GLSL further will be fascinating.

Now that we have two shaders (one fragment and one vertex), we can use them in our game.

Adding shaders to the engine class

Open the Engine.h file. Add the following highlighted line of code, which adds an SFML Shader instance called m_RippleShader to the Engine class:

// Three views for the background

View m_BGMainView;

View m_BGLeftView;

View m_BGRightView;

View m_HudView;

// Declare a sprite and a Texture for the background

Sprite m_BackgroundSprite;

Texture m_BackgroundTexture;

// Declare a shader for the background

Shader m_RippleShader;

// Is the game currently playing?

bool m_Playing = false;

// Is character 1 or 2 the current focus?

bool m_Character1 = true;

The engine object and all its functions now have access to m_RippleShader. Note that an SFML Shader object will be comprised of both shader code files.

Loading the shaders

Add the following code, which checks whether the player's GPU can handle shaders. The game will quit if it can't.

Tip

You will have to have an exceptionally old PC for this not to work. If you do have a GPU that doesn't handle shaders, please accept my apologies.

Next, we will add an else clause that loads the shaders if the system can handle them. Open the Engine.cpp file and add this code to the constructor:

// Can this graphics card use shaders?

if (!sf::Shader::isAvailable())

{

    // Time to get a new PC

    // Or remove all the shader related code L

    m_Window.close();

}

else

{

    // Load two shaders (1 vertex, 1 fragment)

    m_RippleShader.loadFromFile("shaders/vertShader.vert",

        "shaders/rippleShader.frag");

}

m_BackgroundTexture = TextureHolder::GetTexture(

    "graphics/background.png");

We are nearly ready to see our ripple effect in action.

Updating and drawing the shader

Open the Draw.cpp file. As we already discussed when we coded the shaders, we will update the uTime variable directly from our C++ code each frame. We will do so with the setParameter function.

Add the following highlighted code to update the shader's uTime variable and change the call to draw for m_BackgroundSprite, in each of the possible drawing scenarios:

void Engine::draw()

{

    // Rub out the last frame

    m_Window.clear(Color::White);

    // Update the shader parameters

    m_RippleShader.setUniform("uTime",

      m_GameTimeTotal.asSeconds());

    if (!m_SplitScreen)

    {

        // Switch to background view

        m_Window.setView(m_BGMainView);

        // Draw the background

        //m_Window.draw(m_BackgroundSprite);

        // Draw the background, complete with shader effect

        m_Window.draw(m_BackgroundSprite, &m_RippleShader);

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

        // Draw the background, complete with shader effect

        m_Window.draw(m_BackgroundSprite, &m_RippleShader);

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

        // Draw the background, complete with shader effect

        m_Window.draw(m_BackgroundSprite, &m_RippleShader);

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

}

It would be best to delete the lines of code that were commented out.

Run the game and you will get an eerie kind of molten rock. Experiment with changing the background image to have some fun:

That's it! Our fourth game is done.

Summary

In this chapter, we explored the concepts of particle systems and shaders. Although we looked at probably the simplest possible case for each, we still managed to create a simple explosion and an eerie molten rock effect.

In the next four chapters, we will look at more ways that we can improve our code using design patterns at the same time as building a Space Invaders game.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.172.132