Fall Fury: Part 9 - Particle System

Particle System

During gameplay there are scenarios during which users need to be visually notified that something has happened, such as a collision with an obstacle or an enemy shell. One way to do this is by having explosion or item breaking simulation, which brings us to the next large component in FallFury—the sprite-based particle system. It doesn’t offer as much power as a full-fledged particle system would, but it allows for effects that fit well within the overall theme and layout of the game.

Check out the video for this article at http://channel9.msdn.com/Series/FallFury/Part-9-Particle-System .  For a complete, offline version of this series, you may download a nicely formatted PDF of all the articles.

The Core

ParticleSystem is the dedicated folder in the project that contains everything needed to render multiple textures at once and displace them to create the desired effect:

clip_image001

A single particle carries information regarding its size, position, velocity, color shading, rotation, circular velocity, and scale. As with any other rendered entity, it has a bounding box that can be used to detect its intersection with other elements on the screen. In FallFury, this functionality is not used.

Its structure is as follows:

#pragma once
#include "pch.h"
struct Particle
{
    Particle(float2 size);
    Particle(float2 size, float4 shading);
    float2 Size;
    float2 Position;
    float2 Velocity;
    float4 Shading;
    float Rotation;
    float RotationVelocity;
    float Scale;
    bool IsWithinScreenBoundaries(float x, float y, Windows::Foundation::Rect screenBounds);
    Windows::Foundation::Rect GetBoundingBox();
};

The Particle class also happens to have two constructors—one that sets the particle to have the default shading, effectively removing the effect, and one where shading is dynamic. Note that a particle on its own doesn’t do much—it neither carries the associated texture nor has an internal loop that can be used in any given application part to display it.

The next core class is ParticleSet. It is used as a container for all the particles associated with a specific effect. For example, if I want to create flying stuffing when the bear hits an obstacle, I create a new ParticleSet instance and define the necessary particle properties:

  • Lifespan – a particle set does not constantly animate. It displays the particles for a limited amount of time, and displaces them by the given velocity values, and then self-destructs.
  • Texture – all particles in a ParticleSet have the same texture. Going back to the stuffing example, there is a single PNG file used to render multiple variable-sized particles on collision.
  • IsAlive – this is the flag that shows whether the particle set should be rendered in the first place. If it is set to false then, regardless of the conditions, this ParticleSet instance is ignored.
  • ShouldScale – this flag determines whether particles will automatically increase their scale as they are being displaced, creating the effect of a particle approaching the screen. This effect is applied to each particle in the set.

The container class is used to update the particles through the Update function:

void ParticleSet::Update(float timeDelta)
{
    float quat = _lifespan / 0.016f;
    float decrement = 1.0f / quat;
    
    if (_totalTime <= _lifespan && _isAlive)
    {
        _totalTime += timeDelta;
        
        for (auto particle = _particles.begin(); particle != _particles.end(); particle++)
        {
            if (_shouldScale)
                particle->Scale += 0.2f;
            
            particle->Shading.a -= decrement;
            particle->Position = float2(particle->Position.x + particle->Velocity.x,
                            particle->Position.y + particle->Velocity.y);
            
            if (!_shouldScale)
                particle->Rotation += particle->RotationVelocity;
        }
    }
    else
    {
        _totalTime = 0.0f;
        _isAlive = false;
    }
}

As it relies on the _isAlive flag, the Update loop is only used when the particle displacement is activated via the Activate function:

void ParticleSet::Activate(float2 position, float2 velocity, bool randomize, bool scale)
{
    for (auto particle = _particles.begin(); particle != _particles.end(); particle++)
    {
        particle->Position = position;

        if (randomize)
            particle->Velocity = float2(RandFloat(-5.0f,5.0f), RandFloat(-5.0f, 5.0f));
        else
            particle->Velocity = float2(velocity.x + RandFloat(-0.6f, 0.6f), velocity.y + RandFloat(-0.6f, 0.6f));;
    }

    _shouldScale = scale;
    _isAlive = true;
}

When a set is activated, several user-defined parameters come into play. The position is set no matter what and is used to create the source point from which the particles start appearing. The velocity, on the other hand, can be randomized between the values of -5 and 5 pixels per update loop, on both the X-axis and the Y-axis. If randomized, large amount of particles will create an explosion that starts from the center point and expands towards all quadrants. When the velocity is not randomized, it creates a triangular expansion grid on which particles deviate from the center point in one of the given directions:

clip_image003

When it’s time to render the particles, the sprite batch associated with the current game screen is used to pass a texture for each particle registered in the set:

void ParticleSet::Render(SpriteBatch ^spriteBatch)
{
    for (auto particle = _particles.begin(); particle != _particles.end(); particle++)
        {
        if (GamePlayScreen::Manager->IsWithinScreenBoundaries(particle->Position))
                    spriteBatch->Draw(_texture.Get(), particle->Position, PositionUnits::DIPs,
                    particle->Size * particle->Scale, SizeUnits::Pixels, particle->Shading, particle->Rotation);
        }
}

Although there is no flag check inside the Render method that would make sure that the set is alive, this can be done outside of it by calling IsAlive, which will return the flag value:

bool ParticleSet::IsAlive()
{
    return _isAlive;
}

 

Let’s now take a look at the class that managed the particle flow—ParticleCore.

The Particle Manager

ParticleCore is the class that manages internal particle sets, and is also the proxy for set activation, update, and rendering. Here is its structure:

#pragma once
#include "pch.h"
#include "ParticleSet.h"
#include <list>
#include <map>

class ParticleCore
{
    public:
    ParticleCore();
    ParticleCore(Coding4Fun::FallFury::Screens::GameScreenBase^);
    virtual ~ParticleCore();
    
    void CreatePreCachedParticleSets();
    void ActivateSet(Platform::String^, float2);
    void ActivateSet(Platform::String^, float2, float2);
    void ActivateSet(Platform::String^, float2, bool);
    void ActivateSet(Platform::String^, float2, float2, bool);
    void ActivateSet(Platform::String^, float2, float2, bool, bool scale);
    void Update(float);
    void Render();
    
    private:
        std::list<ParticleSet>                                                    _renderParticleSets;
        std::map<Platform::String^, ParticleSet*>                                _particleSetCache;
        std::map<Platform::String^, Microsoft::WRL::ComPtr<ID3D11Texture2D>>    _textureCache;
        Coding4Fun::FallFury::Screens::GameScreenBase^                            _screenBase;
};

As previously mentioned, set activation can be done with some omitted parameters inferred by the system, such as randomization of velocity or particle scaling, which is the reason why you see multiple overloads for ActivateSet.

ParticleCore is also the container for pre-defined sets that have specific textures and properties that are dumped in the particle set cache (internal _particleSetCache). The cache is reusable and even though sets can be activated and destroyed, the cache remains intact for the duration of the game unless explicitly reset or modified. The texture cache is an addition to the particle set cache and is used as a helper container to temporarily store the textures used for individual particles.

Taking a look under the hood at the CreatePreCachedParticleSets function, you can see multiple sets that can be used in the game, each with different properties. Here is a snippet that shows how the bear stuffing explosion set is created:

auto smallExplosionSet = new ParticleSet(_textureCache["Stuffing"], LIFESPAN);
for (int i = 0; i < 20; i++)
{
    float size = RandFloat(50.0f, 100.0f);
    Particle particle(float2(size, size));
    smallExplosionSet->AddParticle(particle);
}

 

Once all the particles are in place for that specific set, it is added to the global particle set cache:

_particleSetCache["SmallExplosion"] = smallExplosionSet;

The particle set cache is not explicitly used to render anything on the screen. Rather, that task is delegated to the rendering cache. When a set is activated, the cache is inspected for the given key, its Activate function is called, marking it as alive, and the set itself is pushed on the rendering stack:

void ParticleCore::ActivateSet(Platform::String^ name, float2 position, float2 velocity, bool randomize, bool scale)
{
    ParticleSet set = ParticleSet(*_particleSetCache[name]);
    set.Activate(position, velocity, randomize, scale);
    _renderParticleSets.push_back(set);
}

 

The Render loop takes care of invoking the proper SpriteBatch drawing functions for each set that is in that stack:

void ParticleCore::Render()
{
    for (auto set = _renderParticleSets.begin(); set != _renderParticleSets.end(); ++set)
    {
        if ((*set).IsAlive())
        {
            (*set).Render(_screenBase->CurrentSpriteBatch);
        }
    }
}

 

Notice that, as previously mentioned, the set does not perform the life check on itself when rendering. Instead, the manager class performs this action. The same applies to the Update loop:

void ParticleCore::Update(float timeDelta)
{
    for (auto set = _renderParticleSets.begin(); set != _renderParticleSets.end();)
    {
        if ((*set).IsAlive())
        {
            (*set).Update(timeDelta);
            ++set;
        }
        else
        {
            set = _renderParticleSets.erase(set);
        }
    }
}

 

The textures for each set are individually loaded in the CreatePreCachedParticleSets. Each instance is internally pushed into the cache and also added to the sprite batch associated with the current game screen, where ParticleCore is used:

Loader->LoadTexture("DDS\\stuffing.dds", &_textureCache["Stuffing"], nullptr);
_screenBase->CurrentSpriteBatch->AddTexture(_textureCache["Stuffing"].Get());

 

Once everything is loaded, the ParticleCore is ready to go and you can use as many particles as necessary in any part of the application. In GamePlayScreen, particle sets are activated in many cases. For example, if the bear dies:

void GamePlayScreen::CheckBearHealth()
{
    if (GameBear->CurrentHealth <= 0)
    {
        m_particleSystem.ActivateSet("Buttons",GameBear->Position, true);
        GameBear->Kill();
        StopBackground();
    }
}

 

Conclusion

Implementing a sprite-based particle system is not complicated, but it requires depending on a number of assumptions. For example, when a particle set is created, you might consider the fact that some hardware can handle drawing only a given number of sprites at the same time. If a particle set is rendered on a desktop machine, there is no guarantee that the same set will successfully render on an ARM device. Therefore, plan accordingly. For each particle set, create a lifespan that fits the scenario without wasting resources on rendering unnecessary particles. As an additional failsafe, you might want to disable specific particle set types when a Direct3D feature level below 10.0 is detected.

Tags:

Follow the Discussion

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.