/* 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);
}
}
}