/* Sparx * Copyright August Zinsser 2007 * This program is distributed under the terms of the GNU General Public License */ using System; using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; using System.Xml.Serialization; using System.Diagnostics; namespace Pina3D.Particles { /// /// The particle engine component of Pina3D /// public static class Sparx { public enum DepthYRelationship { Proportional, Inverse, None }; private static bool mPaused = false; // Each emitter can also be seperately paused private static int mBudget; private static int mParticleCount; private static int mEmitterCount; private static bool mVerbose; // Asserts false if a warning pops up (only in debug) private static bool mVisible = true; private static bool mStarved; private static bool mStarving; private static bool mLayerDepthOutOfBounds; private static double mGlobalRotation; private static Matrix mWorldMatrix; private static float mDepthYRatio; private static Vector2 mScreenTranslate; private static Random mRandom = new Random(); private static List mEmitters; // TODO: change this to mRootEmitters private static List mAutoJigglies; // Particles in this list have their depth jiggled to combat z-fighting private static DepthYRelationship mDYR = DepthYRelationship.None; /// /// Get or Set the maximum number of particles to be spawned /// public static int ParticleBudget { set { mBudget = value; } get { return mBudget; } } /// /// Returns the number particles across all emitters that are currently alive /// public static int ParticleCount { get { return mParticleCount; } } /// /// Returns the total number of alive emitters /// public static int EmitterCount { get { return mEmitterCount; } } internal static List Emitters { get { return mEmitters; } } public static bool Verbose { set { mVerbose = value; } get { return mVerbose; } } /// /// True if at least one emitter wants to emit a particle but the system is over the budget /// public static bool Starving { get { return mStarving; } } /// /// True if any particle tries to draw at a layer depth < 0 or > 1 /// public static bool LayerDepthOutOfBounds { get { return mLayerDepthOutOfBounds; } } /// /// Stops drawing emissions, but still updates them /// public static bool Visible { set { mVisible = value; } get { return mVisible; } } /// /// Stops updating all emissions, but continues to draw them /// public static bool Paused { set { mPaused = value; } get { return mPaused; } } /// /// Radians that increase at a constant rate. Used by various Forces. /// public static double GlobalRotation { get { return mGlobalRotation; } } /// /// The amount per unit Y position to change layer depth of each particle. Useful for 2.5D games. /// public static float DepthYRatio { set { mDepthYRatio = value; } get { return mDepthYRatio; } } public static float RandomFloat { get { return (float)mRandom.NextDouble(); } } /// /// Visual 2D offset for particles. Useful when rendering in screen space. /// public static Vector2 ScreenTranslate { set { mScreenTranslate = value; } get { return mScreenTranslate; } } public static DepthYRelationship DepthYBehavior { set { mDYR = value; } get { return mDYR; } } /// /// Starts the particle engine /// /// The maximum number of particles allowed to spawn at a time /// If true, asserts alert of any violations of budget or bounds (only in debug mode) public static void Initialize(int particleBudget, bool verbose) { mEmitters = new List(); mWorldMatrix = Matrix.Identity; mBudget = particleBudget; mVerbose = verbose; mDepthYRatio = (Pina.FarPlane - Pina.NearPlane) / Pina.Graphics.PreferredBackBufferHeight; // TODO: Get rid of the autojigglies mAutoJigglies = new List(); } public static void FlagOutOfBounds() { if (mVerbose && !mLayerDepthOutOfBounds) Debug.Assert(false, "One or more particles has tried to draw outside of the layer depth range [0,1]. Consider decreasing the DepthYRatio or spawning new particle emitters closer to a layer depth of .5"); mLayerDepthOutOfBounds = true; } /// /// Adds a new emitter to the particle sytem. /// /// public static void AddEmitter(Emitter emitter) { mEmitters.Add(emitter); } /// /// Kills every particle and every emitter /// public static void Flush() { List deathRow = new List(); for (int i = mEmitters.Count - 1; i >= 0; i--) { mEmitters[i].KillAllEmissions(); mEmitters.RemoveAt(i); } Pina.RenderQueue.FlushParticles(); } /// /// Removes an emitter from the particle system. /// /// public static void RemoveEmitter(Emitter emitter) { if (mEmitters != null) mEmitters.Remove(emitter); } /// /// All registered auto jigglies will automatically have their layer depth jiggled to keep them from z-fighting /// /// public static void RegisterAutoJiggly(Particle particle) { mAutoJigglies.Add(particle); } /// /// Saves the given emitter and corresponding emissions/forces/modifiers attached to or spawned from it. /// /// The emitter to save (and all of its spawns and modifiers/forces) /// Where to save the file public static void SaveParticleEffect(string fileName, Emitter rootEmitter) { try { FileStream stream = File.Open(fileName, FileMode.Create); // Convert the object to XML data and put it in the stream XmlSerializer serializer = new XmlSerializer(typeof(SaveFileStruct)); serializer.Serialize(stream, GetSerializable(rootEmitter)); stream.Close(); } catch { } } /// /// Loads a particle effect from a file. /// /// .spx file to load from /// The root of the saved particle effect (with references to its spawns) public static Emitter LoadParticleEffect(string fileName) { //Open the file string fullpath = Path.Combine(StorageContainer.TitleLocation, fileName); if (File.Exists(fullpath)) { FileStream file = File.Open(fullpath, FileMode.Open, FileAccess.Read); XmlSerializer serializer = new XmlSerializer(typeof(SaveFileStruct)); SaveFileStruct sf = (SaveFileStruct)serializer.Deserialize(file); file.Close(); return CreateRoot(sf); } else { FileNotFoundException fnfe = new FileNotFoundException("Load file find failure"); throw fnfe; } } private static Emitter CreateRoot(SaveFileStruct saveFile) { Emitter retEm = CreateParticleEntity(saveFile) as Emitter; return retEm; } private static SparxEntity CreateParticleEntity(SaveFileStruct saveFile) { SparxEntity retEn = null; switch (saveFile.CommonAttributes.Type) { case SaveFileStruct.EntityType.Emitter: // Basic properties retEn = new Emitter ( saveFile.CommonAttributes.Name, Vector3.Zero, saveFile.EmitterAttributes.MinTrajectory, saveFile.EmitterAttributes.MaxTrajectory, saveFile.EmitterAttributes.MinMuzzleVelocity, saveFile.EmitterAttributes.MaxMuzzleVelocity, saveFile.EmitterAttributes.Immortal, saveFile.EmitterAttributes.LifeSpan, saveFile.EmitterAttributes.SpawnRate ); ((Emitter)retEn).RegularEmissionType = (Emission)CreateParticleEntity(saveFile.EmitterAttributes.RegularEmissionType); ((Emitter)retEn).MinSpawnRadius = saveFile.EmitterAttributes.MinSpawnRadius; ((Emitter)retEn).MaxSpawnRadius = saveFile.EmitterAttributes.MaxSpawnRadius; ((Emitter)retEn).DisplaceInX = saveFile.EmitterAttributes.DisplaceAxes.X != 0; ((Emitter)retEn).DisplaceInY = saveFile.EmitterAttributes.DisplaceAxes.Y != 0; ((Emitter)retEn).DisplaceInZ = saveFile.EmitterAttributes.DisplaceAxes.Z != 0; ((Emitter)retEn).BlendMode = saveFile.EmitterAttributes.BlendMode; ((Emitter)retEn).DieAfter = saveFile.EmitterAttributes.DieAfter; ((Emitter)retEn).EmissionCap = saveFile.EmitterAttributes.EmissionCap; // TODO: Scripted Emissions // Modifiers foreach (SaveFileStruct sfMod in saveFile.EmitterAttributes.Modifiers) ((Emitter)retEn).SubscribeToModifier((Modifier)CreateParticleEntity(sfMod)); // Forces foreach (SaveFileStruct sfFo in saveFile.EmitterAttributes.Forces) ((Emitter)retEn).SubscribeToForce((Force)CreateParticleEntity(sfFo)); foreach (SaveFileStruct sfEmFo in saveFile.EmitterAttributes.EmissionsForces) ((Emitter)retEn).SubscribeEmissionsToForce((Force)CreateParticleEntity(sfEmFo)); break; case SaveFileStruct.EntityType.Particle: // Basic Properties retEn = new Particle ( Vector3.Zero, saveFile.ParticleAttributes.LifeSpan, null, saveFile.ParticleAttributes.Width, saveFile.ParticleAttributes.Height, 0f ); ((Particle)retEn).Name = saveFile.CommonAttributes.Name; ((Particle)retEn).ParticleSprite.UVUpperLeft = saveFile.ParticleAttributes.UVUpperLeft; ((Particle)retEn).ParticleSprite.UVLowerRight = saveFile.ParticleAttributes.UVLowerRight; ((Particle)retEn).Immortal = saveFile.ParticleAttributes.Immortal; // Modifiers foreach (SaveFileStruct sfMod in saveFile.ParticleAttributes.Modifiers) ((Particle)retEn).SubscribeToModifier((Modifier)CreateParticleEntity(sfMod)); break; case SaveFileStruct.EntityType.Modifier: // Basic Properties retEn = new Modifier ( saveFile.CommonAttributes.Name, saveFile.ModifierAttributes.Interval, saveFile.ModifierAttributes.Delay, saveFile.ModifierAttributes.InitialTint, saveFile.ModifierAttributes.FinalTint, saveFile.ModifierAttributes.InitialScale, saveFile.ModifierAttributes.FinalScale, saveFile.ModifierAttributes.InitialOpacity, saveFile.ModifierAttributes.FinalOpacity, saveFile.ModifierAttributes.InitialAngularVelocity, saveFile.ModifierAttributes.FinalAngularVelocity ); break; case SaveFileStruct.EntityType.Current: // Basic Properties retEn = new Current ( saveFile.CommonAttributes.Name, saveFile.ForceAttributes.LifeSpan, saveFile.ForceAttributes.Delay, saveFile.ForceAttributes.DirectionOrDisplacement, saveFile.ForceAttributes.Magnitude ); break; case SaveFileStruct.EntityType.Directional: // Basic Properties retEn = new Directional ( saveFile.CommonAttributes.Name, saveFile.ForceAttributes.LifeSpan, saveFile.ForceAttributes.Delay, saveFile.ForceAttributes.DirectionOrDisplacement, saveFile.ForceAttributes.Magnitude ); break; case SaveFileStruct.EntityType.Gravitational: // Basic Properties retEn = new Gravitational ( saveFile.CommonAttributes.Name, saveFile.ForceAttributes.LifeSpan, saveFile.ForceAttributes.Delay, null, saveFile.ForceAttributes.DirectionOrDisplacement, saveFile.ForceAttributes.Magnitude ); break; case SaveFileStruct.EntityType.Vortex: // Basic Properties retEn = new Vortex ( saveFile.CommonAttributes.Name, saveFile.ForceAttributes.LifeSpan, saveFile.ForceAttributes.Delay, null, saveFile.ForceAttributes.DirectionOrDisplacement, saveFile.ForceAttributes.Magnitude, saveFile.ForceAttributes.Clockwise ); break; case SaveFileStruct.EntityType.Spin: // Basic Properties retEn = new Spin ( saveFile.CommonAttributes.Name, saveFile.ForceAttributes.LifeSpan, saveFile.ForceAttributes.Delay, null, saveFile.ForceAttributes.DirectionOrDisplacement, saveFile.ForceAttributes.Magnitude, saveFile.ForceAttributes.Clockwise ); break; } return retEn; } /// /// Returns a SaveFileStruct ready to be serialized by the xml serializer /// private static SaveFileStruct GetSerializable(Emitter rootEmitter) { SaveFileStruct sfRoot = new SaveFileStruct(); // Set the root's properties (and it's emissions') sfRoot = SerializeEmitter(rootEmitter); return sfRoot; } private static SaveFileStruct SerializeEmitter(Emitter emitter) { SaveFileStruct sf = new SaveFileStruct(); sf.CommonAttributes = new SerializableCommon(); sf.EmitterAttributes = new SerializableEmitter(); sf.ParticleAttributes = null; sf.ModifierAttributes = null; sf.ForceAttributes = null; sf.CommonAttributes.Name = emitter.Name; sf.CommonAttributes.Type = SaveFileStruct.EntityType.Emitter; sf.EmitterAttributes.LifeSpan = emitter.LifeExpectancy; sf.EmitterAttributes.Immortal = emitter.Immortal; sf.EmitterAttributes.MinTrajectory = emitter.MinTrajectory; sf.EmitterAttributes.MaxTrajectory = emitter.MaxTrajectory; sf.EmitterAttributes.MinMuzzleVelocity = emitter.MinMuzzleVelocity; sf.EmitterAttributes.MaxMuzzleVelocity = emitter.MaxMuzzleVelocity; sf.EmitterAttributes.MinSpawnRadius = emitter.MinSpawnRadius; sf.EmitterAttributes.MaxSpawnRadius = emitter.MaxSpawnRadius; sf.EmitterAttributes.DisplaceAxes = new Vector3( emitter.DisplaceInX ? 1f : 0f, emitter.DisplaceInY ? 1f : 0f, emitter.DisplaceInZ ? 1f : 0f); sf.EmitterAttributes.SpawnRate = emitter.SpawnRate; sf.EmitterAttributes.EmissionCap = emitter.EmissionCap; sf.EmitterAttributes.DieAfter = emitter.DieAfter; sf.EmitterAttributes.BlendMode = emitter.BlendMode; // Set the properties that emissions inherit from this emitter sf.EmitterAttributes.EmissionsForces = new List(); foreach (Force emissionsForce in emitter.EmissionForces) { sf.EmitterAttributes.EmissionsForces.Add(SerializeForce(emissionsForce)); } // Now go through all the emissions and set their properties SaveFileStruct sfRegEm = new SaveFileStruct(); Emitter regularEmitter = emitter.RegularEmissionType as Emitter; Particle regularParticle = emitter.RegularEmissionType as Particle; if (regularEmitter != null) { sfRegEm = SerializeEmitter(regularEmitter); } if (regularParticle != null) { sfRegEm = SerializeParticle(regularParticle); } sf.EmitterAttributes.RegularEmissionType = sfRegEm; // TODO: Scripted emissions // Get any modifiers attached to this emitter sf.EmitterAttributes.Modifiers = new List(); foreach (Modifier modifier in emitter.Modifiers) { sf.EmitterAttributes.Modifiers.Add(SerializeModifier(modifier)); } // Get any forces attached to this emitter sf.EmitterAttributes.Forces = new List(); foreach (Force force in emitter.Forces) { sf.EmitterAttributes.Forces.Add(SerializeForce(force)); } return sf; } private static SaveFileStruct SerializeParticle(Particle particle) { SaveFileStruct sf = new SaveFileStruct(); sf.CommonAttributes = new SerializableCommon(); sf.EmitterAttributes = null; sf.ParticleAttributes = new SerializableParticle(); sf.ModifierAttributes = null; sf.ForceAttributes = null; sf.CommonAttributes.Name = particle.Name; sf.CommonAttributes.Type = SaveFileStruct.EntityType.Particle; sf.ParticleAttributes.LifeSpan = particle.LifeExpectancy; sf.ParticleAttributes.UVUpperLeft = particle.ParticleSprite.UVUpperLeft; sf.ParticleAttributes.UVLowerRight = particle.ParticleSprite.UVLowerRight; sf.ParticleAttributes.Width = particle.Width; sf.ParticleAttributes.Height = particle.Height; sf.ParticleAttributes.Modifiers = new List(); foreach(Modifier modifier in particle.Modifiers) { sf.ParticleAttributes.Modifiers.Add(SerializeModifier(modifier)); } return sf; } private static SaveFileStruct SerializeModifier(Modifier modifier) { SaveFileStruct sf = new SaveFileStruct(); sf.CommonAttributes = new SerializableCommon(); sf.EmitterAttributes = null; sf.ParticleAttributes = null; sf.ModifierAttributes = new SerializableModifier(); sf.ForceAttributes = null; sf.CommonAttributes.Name = modifier.Name; sf.CommonAttributes.Type = SaveFileStruct.EntityType.Modifier; sf.ModifierAttributes.Interval = modifier.Interval; sf.ModifierAttributes.Delay = modifier.Delay; sf.ModifierAttributes.InitialAngularVelocity = modifier.InitialAngularVelocity; sf.ModifierAttributes.FinalAngularVelocity = modifier.FinalAngularVelocity; sf.ModifierAttributes.InitialOpacity = modifier.InitialOpacity; sf.ModifierAttributes.FinalOpacity = modifier.FinalOpacity; sf.ModifierAttributes.InitialScale = modifier.InitialScale; sf.ModifierAttributes.FinalScale = modifier.FinalScale; sf.ModifierAttributes.InitialTint = modifier.InitialTint; sf.ModifierAttributes.FinalTint = modifier.FinalTint; return sf; } private static SaveFileStruct SerializeForce(Force force) { SaveFileStruct sf = new SaveFileStruct(); sf.CommonAttributes = new SerializableCommon(); sf.EmitterAttributes = null; sf.ParticleAttributes = null; sf.ModifierAttributes = null; sf.ForceAttributes = new SerializableForce(); sf.CommonAttributes.Name = force.Name; sf.ForceAttributes.LifeSpan = force.LifeExpectancy; sf.ForceAttributes.Delay = force.Delay; sf.ForceAttributes.Magnitude = force.Magnitude; if (force is Current) { sf.CommonAttributes.Type = SaveFileStruct.EntityType.Current; sf.ForceAttributes.DirectionOrDisplacement = ((Current)force).Direction; } else if (force is Directional) { sf.CommonAttributes.Type = SaveFileStruct.EntityType.Directional; sf.ForceAttributes.DirectionOrDisplacement = ((Directional)force).Direction; } else if (force is Gravitational) { sf.CommonAttributes.Type = SaveFileStruct.EntityType.Gravitational; sf.ForceAttributes.DirectionOrDisplacement = ((Gravitational)force).CentralDisplacement; } else if (force is Vortex) { sf.CommonAttributes.Type = SaveFileStruct.EntityType.Vortex; sf.ForceAttributes.Clockwise = ((Vortex)force).Clockwise; sf.ForceAttributes.DirectionOrDisplacement = ((Vortex)force).CentralDisplacement; } else if (force is Spin) { sf.CommonAttributes.Type = SaveFileStruct.EntityType.Spin; sf.ForceAttributes.Clockwise = ((Spin)force).Clockwise; sf.ForceAttributes.DirectionOrDisplacement = ((Spin)force).CentralDisplacement; } return sf; } /// /// Updates the particle engine. Time is synched with Pina. /// /// Seconds since this function was last called public static void Update() { List deathRow = new List(); int nextTickParticleCount = 0; int nextTickEmitterCount = 0; bool isStarving = false; if (!mPaused) { mGlobalRotation = Pina.TotalSeconds; mGlobalRotation %= Math.PI * 2.0; for (int i = mEmitters.Count - 1; i >= 0; i--) { mEmitters[i].Update(); nextTickParticleCount += mEmitters[i].ParticleCount; nextTickEmitterCount += mEmitters[i].EmitterCount; isStarving |= mEmitters[i].Starved; // Particle engine must kill root emitters when they are ready to die // Emitters are responsible for killing anything they spawn if (!mEmitters[i].Alive) mEmitters.RemoveAt(i); } } mStarving = isStarving; mStarved |= mStarving; if (mVerbose) { if (!mStarved) Debug.Assert(!mStarving, "One or more particle systems is starving. Consider increasing particle budget or decreasing the amount of particles in use."); } mParticleCount = nextTickParticleCount; mEmitterCount = nextTickEmitterCount; // Jiggle any conflicting depths that are being watched // TODO: remove all this jiggly crap Dictionary mDepthSnapshot = new Dictionary(); List pDeathRow = new List(); foreach (Particle particle in mAutoJigglies) { //while (mDepthSnapshot.ContainsKey(particle.LayerDepth)) // particle.DepthJiggle(); //mDepthSnapshot.Add(particle.LayerDepth, particle); //if (!particle.Alive) // pDeathRow.Add(particle); } foreach (Particle particle in pDeathRow) mAutoJigglies.Remove(particle); } } }