#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Microsoft.Xna.Framework;
namespace Nuclex.Graphics.SpecialEffects.Particles {
/// Manages a series of simulated particles
/// Type of the particles being simulated
///
///
/// A particle system comprises of the particles being simulated and their
/// affectors, which influence the behavior of the particles. Affectors can
/// simulate gravity, wind, decay and just about any physical behavior your
/// particles my display.
///
///
/// The design is optimized for a multi-threading scenario: affactors only
/// update particles and never delete or add particles, so multiple affectors
/// can be run in parallel or one affector can be run from multiple threads
/// on different segments of the particle array
///
///
/// Pruning (the process of removing dead particles) can be performed
/// post-update in a single, controlled step which could run in the background
/// while the game engine is doing other things.
///
///
public partial class ParticleSystem : IDisposable
where ParticleType : struct {
/// Delegate used for detecting dead particles to prune
/// Particle that should be checked
/// True if the particle should be kept in the list
public delegate bool PrunePredicate(ref ParticleType particle);
/// Initializes a new particle system
///
/// Maximum number of particles the system can support
///
public ParticleSystem(int maximumParticles) {
this.particles = new ParticleType[maximumParticles];
this.affectors = new AffectorCollection();
this.coalescableAffectors = this.affectors.CoalescableAffectors;
this.noncoalescableAffectors = this.affectors.NoncoalescableAffectors;
this.asyncPrune = new AsyncPrune(this);
this.asyncUpdate = new AsyncUpdate(this);
}
///
/// Immediately releases all resources owned by the particle system
///
public void Dispose() {
if(this.asyncUpdate != null) {
this.asyncUpdate.Dispose();
this.asyncUpdate = null;
}
if(this.asyncPrune != null) {
this.asyncPrune.Dispose();
this.asyncPrune = null;
}
this.particles = null;
this.coalescableAffectors = null;
this.noncoalescableAffectors = null;
}
/// Adds a new particle to the particle system
/// Particle that will be added to the system
///
/// If the particle system is full, the added particle will be silently discarded.
///
public void AddParticle(ParticleType particle) {
if(this.particleCount < this.particles.Length) {
this.particles[this.particleCount] = particle;
++this.particleCount;
}
}
/// Removes a particle from the particle system
/// Index of the particle that will be removed
public void RemoveParticle(int index) {
this.particles[index] = this.particles[this.particleCount - 1];
--this.particleCount;
}
/// Particles being simulated by the particle system
public ArraySegment Particles {
get {
return new ArraySegment(this.particles, 0, this.particleCount);
}
}
/// Runs the specified number of updates on the particle system
/// Number of updates that will be run
public void Update(int updates) {
update(updates, 0, this.particleCount);
}
/// Number of particles the particle system can manage
public int Capacity { get { return this.particles.Length; } }
/// Begins a multi-threaded update of the particles
///
/// Number of updates to perform. A single update will take full advantage
/// of multiple threads as well.
///
/// Number of threads to perform the updates in
///
/// Callback that will be invoked when the update has finished
///
///
/// User defined parameter that will be passed to the callback
///
/// An asynchronous result handle for the background operation
public IAsyncResult BeginUpdate(
int updates, int threads, AsyncCallback callback, object state
) {
Debug.Assert(
!this.asyncUpdate.IsRunning, "An asynchronous update is already running"
);
Debug.Assert(
!this.asyncPrune.IsRunning, "Can't update while an asynchronous prune is running"
);
this.asyncUpdate.Start(
updates, Math.Min(threads, this.particleCount), callback, state
);
return this.asyncUpdate;
}
/// Ends a multi-threaded particle system update
///
/// Asynchronous result handle returned by the BeginUpdate() method
///
public void EndUpdate(IAsyncResult asyncResult) {
if(!ReferenceEquals(asyncResult, this.asyncUpdate)) {
throw new ArgumentException(
"Async result does not belong to the particle system", "asyncResult"
);
}
if(!this.asyncUpdate.IsCompleted) {
this.asyncUpdate.AsyncWaitHandle.WaitOne();
}
if(this.asyncUpdate.AsyncException != null) {
throw this.asyncUpdate.AsyncException;
}
}
/// Prunes dead particles from the system
///
/// Delegate deciding which particles will be pruned
///
public void Prune(PrunePredicate pruneDelegate) {
for (int index = 0; index < this.particleCount; ) {
bool keep = pruneDelegate(ref this.particles[index]);
if (keep) {
++index;
} else {
this.particles[index] = this.particles[this.particleCount - 1];
--this.particleCount;
}
}
}
/// Begins a threaded prune of the particles
///
/// Method that evaluates whether a particle should be pruned from the system
///
///
/// Callback that will be invoked when the update has finished
///
///
/// User defined parameter that will be passed to the callback
///
/// An asynchronous result handle for the background operation
public IAsyncResult BeginPrune(
PrunePredicate pruneDelegate, AsyncCallback callback, object state
) {
Debug.Assert(
!this.asyncPrune.IsRunning, "An asynchronous prune is already running"
);
Debug.Assert(
!this.asyncUpdate.IsRunning, "Can't prune while an asynchronous update is running"
);
this.asyncPrune.Start(pruneDelegate, callback, state);
return this.asyncPrune;
}
/// Ends a multi-threaded prune of the particle system
///
/// Asynchronous result handle returned by the BeginPrune() method
///
public void EndPrune(IAsyncResult asyncResult) {
if(!ReferenceEquals(asyncResult, this.asyncPrune)) {
throw new ArgumentException(
"Async result does not belong to the particle system", "asyncResult"
);
}
if(!this.asyncPrune.IsCompleted) {
this.asyncPrune.AsyncWaitHandle.WaitOne();
}
if(this.asyncPrune.AsyncException != null) {
throw this.asyncPrune.AsyncException;
}
}
/// Affectors that are influencing the particles in this system
public AffectorCollection Affectors {
get { return this.affectors; }
}
/// Runs the specified number of updates on the particle system
/// Number of updates that will be run
/// Particle index at which updating will begin
/// Number of particles that will be updated
private void update(int updates, int start, int count) {
// Coalescable affectors can be executed in one go per affector because they
// will not produce side effects on each other or the non-coalescable ones.
for(int index = 0; index < this.coalescableAffectors.Count; ++index) {
this.coalescableAffectors[index].Affect(
this.particles, start, count, updates
);
}
// Process any non-coalescable affectors one after another
if(this.noncoalescableAffectors.Count > 1) { // multiple non-coalescables?
// Run all non-coalescable affectors in succession for the number of updates
// we were instructed to perform. This stepped approach is neccessary to
// ensure that the outcome of any cross-affector effects does not change.
for(int update = 0; update < updates; ++update) {
for(int index = 0; index < this.noncoalescableAffectors.Count; ++index) {
this.noncoalescableAffectors[index].Affect(
this.particles, start, count, 1
);
}
}
} else if(this.noncoalescableAffectors.Count == 1) { // only one non-coalescable
// Since there's only one non-coalescable affector, we can let it run
// all the updates in a single step
this.noncoalescableAffectors[0].Affect(
this.particles, start, count, updates
);
}
}
/// Stores the particles simulated by the system
private ParticleType[] particles;
/// Number of particles currently stored in the particle array
private int particleCount;
/// Affectors registered to the particle system
private AffectorCollection affectors;
/// Affectors that are coalescable into a single update
private List> coalescableAffectors;
/// Affectors that are not coalescable into a single update
private List> noncoalescableAffectors;
/// Manages the asynchronous pruning process
private AsyncPrune asyncPrune;
/// Manages the asynchronous updating process
private AsyncUpdate asyncUpdate;
}
} // namespace Nuclex.Graphics.SpecialEffects.Particles