/* 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.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Pina3D.Particles { public class Emitter : Emission, ICloneable { protected List mRegularEmissions; protected List mMyEmissionsForces; protected Emission mRegularEmissionType; protected float mRotation; protected Vector3 mMinTrajectory; protected Vector3 mMaxTrajectory; protected float mMinMuzzleVelocity; protected float mMaxMuzzleVelocity; protected float mMinSpawnRadius; protected float mMaxSpawnRadius; protected Vector3 mDisplaceAxes; protected float mSpawnRate; protected float mSpawnCounter = 0f; protected Dictionary> mScriptedEmissions; protected float mTimeOfBirth = -1; protected List mActiveScriptedEmissions; protected bool mPaused = false; protected bool mOn = true; protected bool mStarved = false; protected bool mSuicidal = false; protected int mEmissionCap = -1; protected int mDieAfter = -1; protected int mSpawnCount = 0; protected int mParticleCount = 0; protected int mEmitterCount = 0; protected RenderQueue.BlendMode mBlendMode = RenderQueue.BlendMode.Additive; public float SpawnRate { set { if (value > 0) mSpawnRate = value; } get { return mSpawnRate; } } public override int ParticleCount { get { return mParticleCount; } } public override int EmitterCount { get { return 1 + mEmitterCount; } } internal List RegularEmissions { get { return mRegularEmissions; } } /// /// Returns true if this emitter has ever tried to emit a non-scripted particle but the system was over budget /// public bool Starved { get { return mStarved; } } /// /// Pausing stops all updating, freezing all emissions in place /// public bool Pause { set { mPaused = value; } get { return mPaused; } } /// /// An inactive emitter still updates, but does not emit anything /// public bool Active { set { mOn = value; } get { return mOn; } } /// /// Sets or gets the prototypical regular emission /// public Emission RegularEmissionType { set { mRegularEmissionType = value; } get { return mRegularEmissionType; }} /// /// Sets or gets the inital minimum direction values for everything that this emitter emits /// public Vector3 MinTrajectory { set { mMinTrajectory = value; } get { return mMinTrajectory; } } /// /// Sets or gets the inital maximum direction values for everything that this emitter emits /// public Vector3 MaxTrajectory { set { mMaxTrajectory = value; } get { return mMaxTrajectory; } } public float MinSpawnRadius { set { mMinSpawnRadius = value; } get { return mMinSpawnRadius; } } public float MaxSpawnRadius { set { mMaxSpawnRadius = value; } get { return mMaxSpawnRadius; } } public bool DisplaceInX { set { mDisplaceAxes.X = value ? 1f : 0f; } get { return mDisplaceAxes.X == 1f; } } public bool DisplaceInY { set { mDisplaceAxes.Y = value ? 1f : 0f; } get { return mDisplaceAxes.Y == 1f; } } public bool DisplaceInZ { set { mDisplaceAxes.Z = value ? 1f : 0f; } get { return mDisplaceAxes.Z == 1f; } } public float MinMuzzleVelocity { set { mMinMuzzleVelocity = value; } get { return mMinMuzzleVelocity; } } public float MaxMuzzleVelocity { set { mMaxMuzzleVelocity = value; } get { return mMaxMuzzleVelocity; } } public RenderQueue.BlendMode BlendMode { set { mBlendMode = value; } get { return mBlendMode; } } /// /// Limit this emitter's regular emissions to this number (does not affect Off/On state) /// public int EmissionCap { set { mEmissionCap = value; } get { return mEmissionCap; } } /// /// Set this emitter to kill itself after it has emitted this number of regular emissions /// public int DieAfter { set { mDieAfter = value; } get { return mDieAfter; } } /// /// True if this emitter has been flagged for death but still has living children and must stay alive until they are all dead /// public bool Suicidal { get { return mSuicidal; } } public List Forces { get { return mMyForces; } } public List EmissionForces { get { return mMyEmissionsForces; } } public override Matrix Transform { set { mTransform = value; UpdateChildTransforms(); } get { return mTransform; } } /// /// Scales all particles at render time. Render positions will not be affected by scale. /// public override float RenderScale { set { mRenderScale = value; UpdateChildTransforms(); } get { return mRenderScale; } } /// /// Rotates all particles (in screen space) at render time. Render positions will not be affected. /// public override float RenderRotation { set { mRenderRotation = value; UpdateChildTransforms(); } get { return mRenderRotation; } } /// /// Default constructor /// /// Used for bookeeping /// True if this emitter does not die over time /// If not immortal, how long it takes to implicitly die /// Initial minimum velocity direction for all emissions from this emitter /// Initial maximum velocity direction for all emissions from this emitter /// Initial minimum speed for all emissions from this emitter /// Initial maximum speed for all emissions from this emitter /// The rate at which this emitter spawns regular emissions public Emitter(string name, Vector3 position, Vector3 minTrajectory, Vector3 maxTrajectory, float minMuzzleVelocity, float maxMuzzleVelocity, bool immortal, float lifeSpan, float spawnRate) : base (name, position, immortal, lifeSpan) { mMinTrajectory = minTrajectory; mMaxTrajectory = maxTrajectory; mMinMuzzleVelocity = minMuzzleVelocity; mMaxMuzzleVelocity = maxMuzzleVelocity; mMinSpawnRadius = 0f; mMaxSpawnRadius = 0f; mDisplaceAxes = Vector3.One; mActiveScriptedEmissions = new List(); mRegularEmissions = new List(); mScriptedEmissions = new Dictionary>(); mMyEmissionsForces = new List(); mSpawnRate = spawnRate; } private Emitter() // Todo: remove? { } /// /// Adds one delayed emission /// /// Type of emission to spawn /// The time to wait (in seconds) before spawning this emission public void AddScriptedEmission(Emission emission, float time) { // Make a new list if this is the first event to happen at the specified time if (!mScriptedEmissions.ContainsKey(time)) mScriptedEmissions.Add(time, new List()); List spawnAtTime = mScriptedEmissions[time]; spawnAtTime.Add(emission); } /// /// Forces all emissions from this emitter to be affected by a clone of the specified Force. If already subscribed, then do nothing. /// /// public virtual void SubscribeEmissionsToForce(Force force) { if (!mMyEmissionsForces.Contains(force)) mMyEmissionsForces.Add(force); } /// /// Removes the effects of the specified Force on this emitter's emissions. If not subscribed, then do nothing. /// /// public virtual void UnsubscribeEmissionsFromForce(Force force) { if (mMyEmissionsForces.Contains(force)) mMyEmissionsForces.Remove(force); } /// /// Checks to see if this emitter has subscribed its emissions to the given force /// /// /// public bool SubscribesEmissionsToForce(Force force) { return (mMyEmissionsForces.Contains(force)); } /// /// Kills all emissions fromt this emitter, even immortal ones. /// public void KillAllEmissions() { for (int i = 0; i < mRegularEmissions.Count; i++) { mRegularEmissions[i].Alive = false; } // TODO: Scripted Emissions } public void UpdateChildTransforms() { for (int i = 0; i < mRegularEmissions.Count; i++) { mRegularEmissions[i].Transform = mTransform; mRegularEmissions[i].RenderScale = mRenderScale; mRegularEmissions[i].RenderRotation = mRenderRotation; } // TODO: Scripted Emissions } /// /// Returns a new instance almost exactly like this one /// /// public override object Clone() { Emitter retEm = new Emitter(); retEm.mName = mName + "_clone"; retEm.mPosition = new Vector3(mPosition.X, mPosition.Y, mPosition.Z); retEm.mVelocity = new Vector3(mVelocity.X, mVelocity.Y, mVelocity.Z); retEm.mAge = mAge; retEm.mLifeExpectancy = mLifeExpectancy; retEm.mDieAfter = mDieAfter; retEm.mEmissionCap = mEmissionCap; retEm.mMyModifiers = mMyModifiers; if (mMyForces != null) { retEm.mMyForces = new List(); for (int i = 0; i < mMyForces.Count; i++) retEm.mMyForces.Add((Force)mMyForces[i].Clone()); } if (mMyEmissionsForces != null) { retEm.mMyEmissionsForces = new List(); for (int i = 0; i < mMyEmissionsForces.Count; i++) retEm.mMyEmissionsForces.Add((Force)mMyEmissionsForces[i].Clone()); } retEm.Alive = mAlive; retEm.mImmortal = mImmortal; retEm.mSpawnRate = mSpawnRate; retEm.mRegularEmissions = new List(); retEm.mMinTrajectory = mMinTrajectory; retEm.mMaxTrajectory = mMaxTrajectory; retEm.mMinMuzzleVelocity = mMinMuzzleVelocity; retEm.mMaxMuzzleVelocity = mMaxMuzzleVelocity; retEm.mMinSpawnRadius = mMinSpawnRadius; retEm.mMaxSpawnRadius = mMaxSpawnRadius; retEm.mDisplaceAxes = mDisplaceAxes; if (mRegularEmissionType != null && mRegularEmissionType.Name != this.Name) retEm.mRegularEmissionType = (Emission)mRegularEmissionType.Clone(); // TODO: Scripted Emissions retEm.mScriptedEmissions = new Dictionary>(); retEm.mActiveScriptedEmissions = new List(); //foreach (Particle particle in mActiveScriptedEmissions) // retEm.mActiveScriptedEmissions.AddAfter(retEm.mActiveScriptedEmissions.Last, (Particle)particle.Clone()); retEm.mAlive = mAlive; retEm.mPaused = mPaused; retEm.mOn = mOn; retEm.mStarved = false; retEm.BlendMode = mBlendMode; retEm.mTransform = mTransform; retEm.mRenderScale = mRenderScale; retEm.mRenderRotation = mRenderRotation; return retEm; } /// /// Updates all emissions from this emitter /// public override void Update() { // See if we need a birthday if (mTimeOfBirth == -1) mTimeOfBirth = Pina.ElapsedSeconds; if (!mPaused) { int nextTickParticleCount = 0; int nextTickEmitterCount = 0; // Update this emitter first base.Update(); // Stay alive until all emissions are also dead bool hasLivingChild = false; for (int i = 0; i < mRegularEmissions.Count; i++) { hasLivingChild = hasLivingChild || mRegularEmissions[i].Alive; if (hasLivingChild) break; } // TODO: Scripted emissions //foreach (Emission emission in mScriptedEmissions) //{ // hasLivingChild = hasLivingChild || emission.Alive; // if (hasLivingChild) // break; //} if (!mAlive && hasLivingChild) { mOn = false; mAlive = true; mSuicidal = true; } if (mSuicidal && !hasLivingChild) { mAlive = false; } // Check for DieAfter case if (mDieAfter != -1 && mSpawnCount >= mDieAfter) { mOn = false; mSuicidal = true; } if (mOn) { mSpawnCounter += Pina.ElapsedSeconds; float scale = 1.0f; // Create new regular emissions (unless total particle budget or emitter cap has been hit) while (mSpawnCounter > 0 && !Suicidal) { if (Sparx.ParticleCount > Sparx.ParticleBudget) mStarved = true; else if (mEmissionCap == -1 || (this.EmitterCount + this.ParticleCount) <= mEmissionCap) { mSpawnCount++; // Calculate firing speed float speed; if (mMinMuzzleVelocity > mMaxMuzzleVelocity) speed = mMinMuzzleVelocity; else speed = scale * (mMinMuzzleVelocity + Sparx.RandomFloat * (mMaxMuzzleVelocity - mMinMuzzleVelocity)); // Calculate firing trajectory Vector3 trajectory = new Vector3( mMinTrajectory.X + Sparx.RandomFloat * (mMaxTrajectory.X - mMinTrajectory.X), mMinTrajectory.Y + Sparx.RandomFloat * (mMaxTrajectory.Y - mMinTrajectory.Y), mMinTrajectory.Z + Sparx.RandomFloat * (mMaxTrajectory.Z - mMinTrajectory.Z)); if (trajectory == Vector3.Zero) { // No trajectory means that particles will not spawn properly, but untuitively it should mean that they just don't move trajectory = Vector3.UnitZ; speed = 0; } Vector3.Normalize(ref trajectory, out trajectory); Vector3.Multiply(ref trajectory, speed, out trajectory); // Clone the regular emission, but update its position and velocity to this instance's current values Emission theClone = null; if (mRegularEmissionType is Emitter) { Emitter clone = (Emitter)mRegularEmissionType.Clone(); clone.Position3D += mPosition; clone.Velocity3D += trajectory; clone.Transform = mTransform; clone.RenderScale = mRenderScale; clone.RenderRotation = mRenderRotation; theClone = clone; mRegularEmissions.Add(clone); } else if (mRegularEmissionType is Particle) { Particle clone = (Particle)mRegularEmissionType.Clone(); clone.Position3D += mPosition; clone.Velocity3D += trajectory; clone.Transform = mTransform; clone.RenderScale = mRenderScale; clone.RenderRotation = mRenderRotation; theClone = clone; mRegularEmissions.Add(clone); Pina.AddRenderable(clone, mBlendMode); } // Displace the spawned emissions float radius = mMinSpawnRadius; if (mMinSpawnRadius < mMaxSpawnRadius) radius += Sparx.RandomFloat * (mMaxSpawnRadius - mMinSpawnRadius); if (radius > 0) { Vector3 disp = new Vector3( (Sparx.RandomFloat * 2f - 1f) * mDisplaceAxes.X, (Sparx.RandomFloat * 2f - 1f) * mDisplaceAxes.Y, (Sparx.RandomFloat * 2f - 1f) * mDisplaceAxes.Z); Vector3.Normalize(ref disp, out disp); Vector3.Multiply(ref disp, radius, out disp); theClone.Position3D += disp; } if (theClone != null) { // Assign necessary forces to this new emission for (int i = 0; i < mMyEmissionsForces.Count; i++) { Force force = mMyEmissionsForces[i]; theClone.SubscribeToForce(force); Gravitational gForce = force as Gravitational; Vortex vForce = force as Vortex; Spin sForce = force as Spin; if (gForce != null && gForce.Center == null) gForce.Center = this; if (vForce != null && vForce.Center == null) vForce.Center = this; if (sForce != null && sForce.Center == null) sForce.Center = this; } } } mSpawnCounter -= mSpawnRate; } // See if we need to do a scripted emission } // Update all regular emissions for (int i = 0; i < mRegularEmissions.Count; i++) { mRegularEmissions[i].Update(); nextTickParticleCount += mRegularEmissions[i].ParticleCount; nextTickEmitterCount += mRegularEmissions[i].EmitterCount; } // Update all scripted emissions for (int i = 0; i < mActiveScriptedEmissions.Count; i++) { mActiveScriptedEmissions[i].Update(); nextTickParticleCount += mActiveScriptedEmissions[i].ParticleCount; nextTickEmitterCount += mActiveScriptedEmissions[i].EmitterCount; } mParticleCount = nextTickParticleCount; mEmitterCount = nextTickEmitterCount; // Kill all dead emissions for (int i = mRegularEmissions.Count - 1; i >= 0; i--) { if (!mRegularEmissions[i].Alive) mRegularEmissions.RemoveAt(i); } // TODO: kill scripted emissions } } } }