Commit d99e64bf authored by Nathan Bean's avatar Nathan Bean
Browse files

Updated particle systems

parent 97c5f234
......@@ -9,4 +9,6 @@ Much of gameplay derives from how agents in the game world (players, enemies, pu
While game physics often correlate to the physics of the world we inhabit _they don't have to!_ In fact, most game physics approaches at least _simplify_ real-world physics models to allow for real-time processing, and some abandon real-world physics altogether.
In this chapter we'll examine several common approaches to implementing physics in games.
\ No newline at end of file
When games _do_ try to implement realistic physics, they typically start with _rigid body dynamics_, a simplification of real-world physics where objects are represented as a _rigid body_ and a _point mass_. In games, the rigid body is essentially the collision shape we covered in chapter 4, and a point mass represents the object's position as a vector, and the mass as a float.
In this chapter we'll examine how to implement rigid body physics in games.
\ No newline at end of file
---
title: "Physics Engines"
pre: "6. "
weight: 60
date: 2018-08-24T10:53:26-05:00
---
If we want to incorporate more robust and realistic physics than that we have just explored, we would be well-served to look at _physics engines_ designed for use in games. These are simulations built along the same lines as what we've discussed - essentially they represent the objects in the game world as rigid bodies, and provide the means for simulating them somewhat realistically.
## Farseer / Velcro Physics
One of the best-known of the physics engines developed for XNA was the Farseer Physics Engine, which was renamed to Velcro Physics when it was moved from CodePlex to GitHub.
\ No newline at end of file
......@@ -74,7 +74,7 @@ Thus, each frame you update the offset vector based on the offset and the player
public void Draw(GameTime gameTime)
{
// Player-synched scrolling
offset = Player.Position - PlayerOffset;
offset = PlayerOffset - Player.Position;
// Create the translation matrix representing the offset
Matrix transform = Matrix.CreateTranslation(offset.X, offset.Y, 0);
......
......@@ -4,6 +4,7 @@ pre: "1. "
weight: 1
date: 2020-03-20T10:53:05-05:00
---
Particle systems leverage thousands of very small sprites to create the illusion of fire, smoke, explosions, precipitation, waterfalls, and many other interesting effects. They are a staple in modern game engines both to create ambience (i.e. fires and smoke) and to enhance gameplay (glowing sparkles around objects that can be interacted with). In this section, we'll discuss the basics of how particle systems work, and iteratively build a particle system in MonoGame that can be used in your own games.
I've prepared a starter project in which we will build this particle system. Feel free to clone it from GithHub, here: [https://github.com/ksu-cis/particle-system-starter](https://github.com/ksu-cis/particle-system-starter).
\ No newline at end of file
Continuing our exploration of the SpriteBatch and Sprites, let's turn our attention to _particle systems_. Particle systems leverage thousands of very small sprites to create the illusion of fire, smoke, explosions, precipitation, waterfalls, and many other interesting effects. They are a staple in modern game engines both to create ambience (i.e. fires and smoke) and to enhance gameplay (glowing sparkles around objects that can be interacted with). In this section, we'll discuss the basics of how particle systems work, and iteratively build a particle system in MonoGame that can be used in your own games.
This approach is based on one that appeared in the original XNA samples, but has been modified for ease of use. You can see the original source updated for MonoGame on GithHub at https://github.com/CartBlanche/MonoGame-Samples/tree/master/ParticleSample
---
title: "The Particle"
pre: "2. "
weight: 2
date: 2020-03-20T10:53:05-05:00
---
At the heart of a particle system is a collection of particles - tiny sprites that move independently of one another, but when rendered together, create the interesting effects we are after. To draw each individual particle, we need to know where on the screen it should appear, as well as the texture we should be rendering, and any color effects we might want to apply. Moreover, each frame our particles will be moving, so we'll also want to be able to track information to make that process easier, like velocity, acceleration, and how long a particle has been "alive".
With thousands of particles in a system, it behooves us to think on efficiency as we write this representation. The [flyweight pattern](https://gameprogrammingpatterns.com/flyweight.html) is a great fit here - each particle in the system can be implemented as a flyweight. This means that we only store the information that is _specific to that particle_. Any information shared by _all_ particles will instead be stored in the `ParticleSystem` class, which we'll define separately.
We'll start with a fairly generic properties that are used by most particle systems:
```csharp
/// <summary>
/// A class representing a single particle in a particle system
/// </summary>
public class Particle
{
/// <summary>
/// The current position of the particle. Default (0,0).
/// </summary>
public Vector2 Position;
/// <summary>
/// The current velocity of the particle. Default (0,0).
/// </summary>
public Vector2 Velocity;
/// <summary>
/// The current acceleration of the particle. Default (0,0).
/// </summary>
public Vector2 Acceleration;
/// <summary>
/// The current rotation of the particle. Default 0.
/// </summary>
public float Rotation;
/// <summary>
/// The current angular velocity of the particle. Default 0.
/// </summary>
public float AngularVelocity;
/// <summary>
/// The current angular acceleration of the particle. Default 0.
/// </summary>
public float AngularAcceleration;
/// <summary>
/// The current scale of the particle. Default 1.
/// </summary>
public float Scale = 1.0f;
/// <summary>
/// The current lifetime of the particle (how long it will "live"). Default 1s.
/// </summary>
public float Lifetime;
/// <summary>
/// How long this particle has been alive
/// </summary>
public float TimeSinceStart;
/// <summary>
/// The current color of the particle. Default White
/// </summary>
public Color Color = Color.White;
/// <summary>
/// If this particle is still alive, and should be rendered
/// <summary>
public bool Active => TimeSinceStart < Lifetime;
}
```
Here we've created fields to hold all information unique to the particle, both for updating and drawing it. Feel free to add or remove fields specific to your needs; this sampling represents only some of the most commonly used options. Note that we don't define a texture here - all the particles in a particle system typically share a single texture (per the flyweight pattern), and that texture is maintained by the particle system itself.
We should also write an initialize function to initialize the values of a newly minted particle:
```csharp
/// <summary>
/// Sets the particle up for first use, restoring defaults
/// </summary>
public void Initialize(Vector2 where)
{
this.Position = where;
this.Velocity = Vector2.Zero;
this.Acceleration = Vector2.Zero;
this.Rotation = 0;
this.AngularVelocity = 0;
this.AngularAcceleration = 0;
this.Scale = 1;
this.Color = Color.White;
this.Lifetime = 1;
this.TimeSinceStart = 0f;
}
```
We can also provide some overloads of this method to allow us to specify additional parameters (avoiding setting them twice - once to the default value and once to the expected value). An easy way to keep these under control is to provide _default_ values. Unfortunately, we can only do this for values that can be determined at compile time (i.e. primitives), so the vectors cannot have a default value. Thus, we would need at least three overloads:
```csharp
/// <summary>
/// Sets the particle up for first use
/// </summary>
public void Initialize(Vector2 position, Vector2 velocity, float lifetime = 1, float scale = 1, float rotation = 0, float angularVelocity = 0, float angularAcceleration = 0)
{
this.Position = position;
this.Velocity = velocity;
this.Acceleration = Vector2.Zero;
this.Lifetime = lifetime;
this.TimeSinceStart = 0f;
this.Scale = scale;
this.Rotation = rotation;
this.AngularVelocity = angularVelocity;
this.AngularAcceleration = angularAcceleration;
this.Color = Color.White;
}
/// <summary>
/// Sets the particle up for first use
/// </summary>
public void Initialize(Vector2 position, Vector2 velocity, Vector2 acceleration, float lifetime = 1, float scale = 1, float rotation = 0, float angularVelocity = 0, float angularAcceleration = 0)
{
this.Position = position;
this.Velocity = velocity;
this.Acceleration = acceleration;
this.Lifetime = lifetime;
this.TimeSinceStart = 0f;
this.Scale = scale;
this.Rotation = rotation;
this.AngularVelocity = angularVelocity;
this.AngularAcceleration = angularAcceleration;
this.Color = Color.White;
}
/// <summary>
/// Sets the particle up for first use
/// </summary>
public void Initialize(Vector2 position, Vector2 velocity, Vector2 acceleration, Color color, float lifetime = 1, float scale = 1, float rotation = 0, float angularVelocity = 0, float angularAcceleration = 0)
{
this.Position = position;
this.Velocity = velocity;
this.Acceleration = acceleration;
this.Lifetime = lifetime;
this.TimeSinceStart = 0f;
this.Scale = scale;
this.Rotation = rotation;
this.AngularVelocity = angularVelocity;
this.AngularAcceleration = angularAcceleration;
this.Color = color;
}
```
You might wonder why we don't use a constructor for this initialization. The answer is because we'll want to reuse the same `Particle` instance multiple times - we'll see this soon, in the particle system. We'll turn our attention to that next.
\ No newline at end of file
---
title: "The Particle Structure"
pre: "2. "
weight: 2
date: 2020-03-20T10:53:05-05:00
---
At the heart of a particle system is a collection of particles - tiny sprites that move independently of one another, but when rendered together, create the interesting effects we are after. To draw each individual particle, we need to know where on the screen it should appear, as well as the texture we should be rendering, and any color effects we might want to apply. Morever, each frame our particles will be moving, so we'll also want to be able to track information to make that process easier, like velocity, acceleration, and how long a particle has been "alive".
With thousands of particles in a system, it behooves us to think on efficiency as we write this representation. The [flyweight pattern](https://gameprogrammingpatterns.com/flyweight.html) is a great fit here - each particle in the system can be implemented as a flyweight. Moreover, since we know we'll be iterating over the collection of particles each frame to both update and draw the particles, using the [data locality pattern](https://gameprogrammingpatterns.com/data-locality.html) to maximize the use of our cache makes sense as well. To fulfil these two requirements, we'll want to define a `struct` to represent our particles.
>**Why a struct?** Remember, in C# a struct is a _value_ type, so if we create an array of `struct` instances, each struct will be stored sequentially in memory. If we instead used a class, a _reference_ type, and create an array of `class` instances (objects), that array will hold references pointing to other locations in memory where those objects are located. So using a struct in this instance is a performance optimization.
Go ahead and create a file named _particle.cs_ and define a particle structure in it:
```csharp
/// <summary>
/// A struct representing a single particle in a particle system
/// </summary>
public struct Particle
{
/// <summary>
/// The current position of the particle
/// </summary>
public Vector2 Position;
/// <summary>
/// The current velocity of the particle
/// </summary>
public Vector2 Velocity;
/// <summary>
/// The current acceleration of the particle
/// </summary>
public Vector2 Acceleration;
/// <summary>
/// The current scale of the particle
/// </summary>
public float Scale;
/// <summary>
/// The current life of the particle
/// </summary>
public float Life;
/// <summary>
/// The current color of the particle
/// </summary>
public Color Color;
}
```
Here we've created fields to hold all information unique to the particle, both for updating and drawing it. Feel free to add or remove fields specific to your needs; this sampling represents only some of the most commonly used options.
Note that we don't define a texture here - all the particles in a particle system typically share a single texture (per the flyweight pattern), and that texture is maintained by the particle system itself. We'll turn our attention to that next.
\ No newline at end of file
---
title: "Example Particle Systems"
pre: "4. "
weight: 4
date: 2020-03-20T10:53:05-05:00
---
To create a particle system, we'll derive a class from the `ParticleSystem` class and override its `InitializeConstants()`, and possibly its `InitializeParticle()` and `UpdateParticle()` methods. Let's look at some examples:
### Rain Particle System
This is a simplistic implementation of rain that is spawned in a predefined rectangle and falls to the bottom of the screen. The texture we'll use is [this drop]({{<static "images/drop.png">}})
We start by defining a class extending the `ParticleSystem`:
```csharp
/// <summary>
/// A class embodying a particle system emulating rain
/// </summary>
public class RainParticleSystem : ParticleSystem
{
// TODO: Add Implementation
}
```
Inside this class, we'll define a private `Rectangle` field to represent where the rain begins:
```csharp
// The source of the rain
Rectangle _source;
```
And a boolean property to start and stop the rain:
```csharp
/// <summary>
/// Determines if it is currently raining or not
/// </summary>
public bool IsRaining { get; set; } = true;
```
We'll add a constructor that must also invoke the `ParticleSystem` constructor. We'll supply the `Rectangle` to use for the source, and hard-code a maximum amount of particles (this may need to be tweaked for larger/smaller rain effects - if there aren't enough particles there will be gaps in the rain):
```csharp
/// <summary>
/// Constructs the rain particle system
/// </summary>
/// <param name="game">The game this particle system belongs to</param>
/// <param name="source">A rectangle defining where the raindrops start</param>
public RainParticleSystem(Game game, Rectangle source) : base(game, 5000)
{
_source = source;
}
```
We override the `InitializeConstants()` to set the number of particles that should be spawned with an `AddParticles()` method call, and the name of the texture to use:
```csharp
/// <summary>
/// Initialize the particle system constants
/// </summary>
protected override void InitializeConstants()
{
// We'll use a raindrop texture
textureFilename = "opaque-drop";
// We'll spawn a large number of particles each frame
minNumParticles = 10;
maxNumParticles = 20;
}
```
Then we override the `InitializeParticle()` method of the base `ParticleSystem` to provide custom behavior for our rain particles. Basically, they just fall straight down. However, you could expand on this to add wind, etc.:
```csharp
/// <summary>
/// Initializes individual particles
/// </summary>
/// <param name="p">The particle to initialize</param>
/// <param name="where">Where the particle appears</param>
protected override void InitializeParticle(Particle p, Vector2 where)
{
base.InitializeParticle(p, where);
// rain particles fall downward at the same speed
p.Velocity = Vector2.UnitY * 260;
// rain particles have already hit terminal velocity,
// and do not spin, so we don't need to set the other
// physics values (they default to 0)
// we'll use blue for the rain
p.Color = Color.Blue;
// rain particles are small
p.Scale = 0.1f;
// rain particles need to reach the bottom of the screen
// it takes about 3 seconds at current velocity/screen size
p.Lifetime = 3;
}
```
Finally, we'll override the `Update()` method from `DrawableGameComponent` to add spawning new droplets every frame within our source rectangle:
```csharp
/// <summary>
/// Override the default DrawableGameComponent.Update method to add
/// new particles every frame.
/// </summary>
/// <param name="gameTime">An object representing the game time</param>
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
// Spawn new rain particles every frame
if(IsRaining) AddParticles(_source);
}
```
### Explosion Particle System
Another particle effect we see often in games is explosions. Let's create an effect that will let us create explosions at specific points on-screen as our game is running. We'll use [this explosion texture]({{<static "images/explosion.png">}}).
We again start by defining a new class derived from `ParticleSystem`:
```csharp
/// <summary>
/// A GameComponent providing a particle system to render explosions in a game
/// </summary>
public class ExplosionParticleSystem : ParticleSystem
{
// TODO: Add implementation
}
```
Our constructor will invoke the base `ParticleSystem` constructor, but we'll also ask for the maximum number of anticipated explosions the system needs to handle. As each explosion needs 20-25 particles, we'll multiply that value by 25 to determine how many particles the system needs to have:
```csharp
/// <summary>
/// Constructs a new explosion particle system
/// </summary>
/// <param name="game">The game to render explosions in</param>
/// <param name="maxExplosions">The anticipated maximum number of explosions on-screen at one time</param>
public ExplosionParticleSystem(Game game, int maxExplosions)
: base(game, maxExplosions * 25)
{
}
```
The explosion will use an explosion texture, 20-25 particles per explosion, and _additive blending_. This blend mode means if two particles overlap, their colors are added together. As more particle combine, the combined color gets closer to white, meaning the center of the explosion will be bright white, but as the particles spread out they will get redder (as the texture is red and yellow). We'll set these up by overriding the `ParticleSystem.InitializeConstants()` method:
```csharp
/// <summary>
/// Set up the constants that will give this particle system its behavior and
/// properties.
/// </summary>
protected override void InitializeConstants()
{
textureFilename = "explosion";
// We'll use a handful of particles for each explosion
minNumParticles = 20;
maxNumParticles = 25;
// Additive blending is very good at creating fiery effects.
blendState = BlendState.Additive;
DrawOrder = AdditiveBlendDrawOrder;
}
```
We'll also override `ParticleSystem.InitializeParticle()` to provide the default starting state for all particles:
```csharp
/// <summary>
/// Initializes the particle <paramref name="p"/>
/// </summary>
/// <param name="p">The particle to initialize</param>
/// <param name="where">Where the particle begins its life</param>
protected override void InitializeParticle(Particle p, Vector2 where)
{
base.InitializeParticle(p, where);
// Explosion particles move outward from the point of origin in all directions,
// at varying speeds
p.Velocity = RandomHelper.RandomDirection() * RandomHelper.NextFloat(40, 500);
// Explosions should be relatively short lived
p.Lifetime = RandomHelper.NextFloat(0.5f, 1.0f);
// Explosion particles spin at different speeds
p.AngularVelocity = RandomHelper.NextFloat(-MathHelper.PiOver4, MathHelper.PiOver4);
// Explosions move outwards, then slow down and stop because of air resistance.
// Let's set acceleration so that when the particle is at max lifetime, the velocity
// will be zero.
// We'll use the equation vt = v0 + (a0 * t). (If you're not familiar with
// this, it's one of the basic kinematics equations for constant
// acceleration, and basically says:
// velocity at time t = initial velocity + acceleration * t)
// We'll solve the equation for a0, using t = p.Lifetime and vt = 0.
p.Acceleration = -p.Velocity / p.Lifetime;
}
```
And we'll also override the `ParticleSystem.Update()` method, so we can use custom logic to change the color and scale of the particle over its lifetime:
```csharp
/// <summary>
/// We override the UpdateParticle() method to scale and colorize
/// explosion particles over time
/// </summary>
/// <param name="particle">the particle to update</param>
/// <param name="dt">the time elapsed between frames</param>
protected override void UpdateParticle(Particle particle, float dt)
{
base.UpdateParticle(particle, dt);
// normalized lifetime is a value from 0 to 1 and represents how far
// a particle is through its life. 0 means it just started, .5 is half
// way through, and 1.0 means it's just about to be finished.
// this value will be used to calculate alpha and scale, to avoid
// having particles suddenly appear or disappear.
float normalizedLifetime = particle.TimeSinceStart / particle.Lifetime;
// we want particles to fade in and fade out, so we'll calculate alpha
// to be (normalizedLifetime) * (1-normalizedLifetime). this way, when
// normalizedLifetime is 0 or 1, alpha is 0. the maximum value is at
// normalizedLifetime = .5, and is
// (normalizedLifetime) * (1-normalizedLifetime)
// (.5) * (1-.5)
// .25
// since we want the maximum alpha to be 1, not .25, we'll scale the
// entire equation by 4.
float alpha = 4 * normalizedLifetime * (1 - normalizedLifetime);
particle.Color = Color.White * alpha;
// make particles grow as they age. they'll start at 75% of their size,
// and increase to 100% once they're finished.
particle.Scale = particle.Scale * (.75f + .25f * normalizedLifetime);
}
```
And finally, we need to allow the game to place explosion effects, so we'll add a public method to do so:
```csharp
/// <summary>
/// Places an explosion at location <paramref name="where"/>
/// </summary>
/// <param name="where">The location of the explosion</param>
public void PlaceExplosion(Vector2 where) => AddParticles(where);
```
### PixieParticleSystem
Another common use for particle systems is to have them _emitted_ from an object in the game - i.e. the player, an enemy, or something the player can interact with. Let's explore this idea by making a particle system that emits colored sparks that fall to the ground, like pixie dust. For this particle system, we'll use [this particle texture with a circular gradient]({{<static "images/particle.png">}}).
Let's start by defining an interface that can serve as our emitter representation. With an emitter, the particle starts in the same place as the emitter, so need to know its location in the game world, so a `Vector2` we'll name `Position`. Also, if the emitter is moving, we need to know the velocity it is moving at, as the particle will also start with that as its initial velocity, so we'll add a second `Vector2` named `Velocity`:
```csharp
/// <summary>
/// An interface for the emitter of a particle system
/// </summary>
public interface IParticleEmitter
{
/// <summary>
/// The position of the emitter in the world
/// </summary>
public Vector2 Position { get; }
/// <summary>
/// The velocity of the emitter in the world
/// </summary>
public Vector2 Velocity { get; }
}
```
Then we start the particle system the same way as before, by defining a class that inherits from `ParticleSystem`:
```csharp
/// <summary>
/// A particle system that drops "pixie dust" from an emitter
/// </summary>
public class PixieParticleSystem : ParticleSystem
{
// TODO: Add implementation
}
```
We'll want a list of emitters of our `IParticleEmitter` class so we know where to spawn those particles (this way we can have multiple pixies!):
```csharp
/// <summary>
/// The emitter for this particle system
/// </summary>
public List<IParticleEmitter> Emitters { get; } = new List<IParticleEmitter>();
```
And we'll construct our particle system with an expected number of pixies to support (with each using around 200 particles):
```csharp
/// <summary>
/// Constructs a new PixieParticleSystem to support up to <paramref name="maxPixies"/> pixies
/// </summary>
/// <param name="game">The game this system belongs to</param>
/// <param name="maxPixies">The maximum number of pixies to support</param>
public PixieParticleSystem(Game game, int maxPixies): base(game, 200 * maxPixies) { }
```
We override `ParticleSystem.InitializeConstants()` to set up the particle system values:
```csharp
/// <summary>
/// Set up the constants that will give this particle system its behavior and
/// properties.
/// </summary>
protected override void InitializeConstants()
{
textureFilename = "particle";
minNumParticles = 2;
maxNumParticles = 5;
blendState = BlendState.Additive;
DrawOrder = AdditiveBlendDrawOrder;
}
```
And `ParticleSystem.InitializeParticle()` to initialize individual particles:
```csharp
/// <summary>
/// Initialize the particles
/// </summary>
/// <param name="p">The particle to initialize</param>
/// <param name="where">Where the particle initially appears</param>
protected override void InitializeParticle(Particle p, Vector2 where)
{
base.InitializeParticle(p, where);