#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