using System; using System.Collections.Generic; using System.Linq; using Duality; using Duality.Editor; using Duality.Resources; using Duality.Components; using Duality.Components.Renderers; using Duality.Drawing; namespace DualStickSpaceShooter { [RequiredComponent(typeof(Transform))] public class ParticleEffect : Renderer, ICmpUpdatable, ICmpInitializable { private ContentRef material = null; private Vector2 particleSize = new Vector2(16, 16); private Vector3 constantForce = Vector3.Zero; private float linearDrag = 0.3f; private float angularDrag = 0.3f; private float fadeInAt = 0.0f; private float fadeOutAt = 0.75f; private bool worldSpace = false; private bool disposeWhenEmpty = true; private List emitters = new List(); [DontSerialize] private float boundRadius = 0.0f; [DontSerialize] private RawList particles = null; [DontSerialize] private RawList vertexBuffer = null; public ContentRef ParticleMaterial { get { return this.material; } set { this.material = value; } } public Vector2 ParticleSize { get { return this.particleSize; } set { this.particleSize = value; } } public Vector3 ConstantForce { get { return this.constantForce; } set { this.constantForce = value; } } [EditorHintRange(0.0f, 1.0f)] public float LinearDrag { get { return this.linearDrag; } set { this.linearDrag = value; } } [EditorHintRange(0.0f, 1.0f)] public float AngularDrag { get { return this.angularDrag; } set { this.angularDrag = value; } } [EditorHintRange(0.0f, 1.0f)] public float FadeInAt { get { return this.fadeInAt; } set { this.fadeInAt = value; } } [EditorHintRange(0.0f, 1.0f)] public float FadeOutAt { get { return this.fadeOutAt; } set { this.fadeOutAt = value; } } public bool WorldSpace { get { return this.worldSpace; } set { this.worldSpace = value; } } public bool DisposeWhenEmpty { get { return this.disposeWhenEmpty; } set { this.disposeWhenEmpty = value; } } public List Emitters { get { return this.emitters; } set { this.emitters = value ?? new List(); } } public override float BoundRadius { get { return this.boundRadius * this.GameObj.Transform.Scale; } } protected bool IsEmpty { get { return this.particles == null || this.particles.Count == 0; } } public void AddParticles(ParticleEmitter emitter, int count) { // Lookup what sprite sheet we're using to get the number of available frames Texture tex = this.RetrieveTexture(); if (tex == null) return; Pixmap img = tex.BasePixmap.Res; if (img == null) return; // Gather data for emitting particles Vector3 effectPos = this.GameObj.Transform.Pos; float effectAngle = this.GameObj.Transform.Angle; float effectScale = this.GameObj.Transform.Scale; // Reserve memory for storing the new particles we're spawning if (this.particles == null) this.particles = new RawList(count); int oldCount = this.particles.Count; this.particles.Count = this.particles.Count + count; // Initialize all those new particles Particle[] particleData = this.particles.Data; for (int i = oldCount; i < this.particles.Count; i++) { // Initialize the current particle. emitter.InitParticle(ref particleData[i]); // So far, the particle is in local coordinates. Transform to world coordinates when required. if (this.worldSpace) { MathF.TransformCoord(ref particleData[i].Velocity.X, ref particleData[i].Velocity.Y, effectAngle, effectScale); MathF.TransformCoord(ref particleData[i].Position.X, ref particleData[i].Position.Y, effectAngle, effectScale); particleData[i].Position += effectPos; particleData[i].Angle += effectAngle; } } } private void RemoveParticle(int index) { this.particles.RemoveAt(index); } private Texture RetrieveTexture() { if (material.IsAvailable && material.Res.MainTexture.IsAvailable) return material.Res.MainTexture.Res; else return null; } private void UpdateEmitters() { for (int i = this.emitters.Count - 1; i >= 0; i--) { if (this.emitters[i] == null) continue; this.emitters[i].Update(this); } } public override void Draw(IDrawDevice device) { if (this.particles == null) return; Texture tex = this.RetrieveTexture(); if (tex == null) return; Vector2 particleHalfSize = this.particleSize * 0.5f; float objAngle; float objScale; Vector3 objPos; if (this.worldSpace) { objAngle = 0.0f; objScale = 1.0f; objPos = Vector3.Zero; } else { objAngle = this.GameObj.Transform.Angle; objScale = this.GameObj.Transform.Scale; objPos = this.GameObj.Transform.Pos; } Vector2 objXDot, objYDot; MathF.GetTransformDotVec(objAngle, objScale, out objXDot, out objYDot); if (this.vertexBuffer == null) this.vertexBuffer = new RawList(this.particles.Count * 4); this.vertexBuffer.Count = this.vertexBuffer.Count = this.particles.Count * 4; VertexC1P3T2[] vertexData = this.vertexBuffer.Data; Particle[] particleData = this.particles.Data; int particleCount = this.particles.Count; for (int i = 0; i < particleCount; i++) { ColorRgba color = particleData[i].Color; float alpha = (float)color.A / 255.0f; if (this.fadeOutAt < 1.0f) alpha *= MathF.Clamp((1.0f - particleData[i].AgeFactor) / this.fadeOutAt, 0.0f, 1.0f); if (this.fadeInAt > 0.0f) alpha *= MathF.Clamp(particleData[i].AgeFactor / this.fadeInAt, 0.0f, 1.0f); color.A = (byte)(alpha * 255.0f); Rect uvRect; tex.LookupAtlas(particleData[i].SpriteIndex, out uvRect); Vector3 particlePos = particleData[i].Position; MathF.TransformDotVec(ref particlePos, ref objXDot, ref objYDot); particlePos += objPos; float particleAngle = objAngle + particleData[i].Angle; float particleScale = objScale; device.PreprocessCoords(ref particlePos, ref particleScale); Vector2 xDot, yDot; MathF.GetTransformDotVec(particleAngle, particleScale, out xDot, out yDot); Vector2 edgeTopLeft = new Vector2(-particleHalfSize.X, -particleHalfSize.Y); Vector2 edgeBottomLeft = new Vector2(-particleHalfSize.X, particleHalfSize.Y); Vector2 edgeBottomRight = new Vector2(particleHalfSize.X, particleHalfSize.Y); Vector2 edgeTopRight = new Vector2(particleHalfSize.X, -particleHalfSize.Y); MathF.TransformDotVec(ref edgeTopLeft, ref xDot, ref yDot); MathF.TransformDotVec(ref edgeBottomLeft, ref xDot, ref yDot); MathF.TransformDotVec(ref edgeBottomRight, ref xDot, ref yDot); MathF.TransformDotVec(ref edgeTopRight, ref xDot, ref yDot); int vertexBaseIndex = i * 4; vertexData[vertexBaseIndex + 0].Pos.X = particlePos.X + edgeTopLeft.X; vertexData[vertexBaseIndex + 0].Pos.Y = particlePos.Y + edgeTopLeft.Y; vertexData[vertexBaseIndex + 0].Pos.Z = particlePos.Z; vertexData[vertexBaseIndex + 0].TexCoord.X = uvRect.X; vertexData[vertexBaseIndex + 0].TexCoord.Y = uvRect.Y; vertexData[vertexBaseIndex + 0].Color = color; vertexData[vertexBaseIndex + 1].Pos.X = particlePos.X + edgeBottomLeft.X; vertexData[vertexBaseIndex + 1].Pos.Y = particlePos.Y + edgeBottomLeft.Y; vertexData[vertexBaseIndex + 1].Pos.Z = particlePos.Z; vertexData[vertexBaseIndex + 1].TexCoord.X = uvRect.X; vertexData[vertexBaseIndex + 1].TexCoord.Y = uvRect.BottomY; vertexData[vertexBaseIndex + 1].Color = color; vertexData[vertexBaseIndex + 2].Pos.X = particlePos.X + edgeBottomRight.X; vertexData[vertexBaseIndex + 2].Pos.Y = particlePos.Y + edgeBottomRight.Y; vertexData[vertexBaseIndex + 2].Pos.Z = particlePos.Z; vertexData[vertexBaseIndex + 2].TexCoord.X = uvRect.RightX; vertexData[vertexBaseIndex + 2].TexCoord.Y = uvRect.BottomY; vertexData[vertexBaseIndex + 2].Color = color; vertexData[vertexBaseIndex + 3].Pos.X = particlePos.X + edgeTopRight.X; vertexData[vertexBaseIndex + 3].Pos.Y = particlePos.Y + edgeTopRight.Y; vertexData[vertexBaseIndex + 3].Pos.Z = particlePos.Z; vertexData[vertexBaseIndex + 3].TexCoord.X = uvRect.RightX; vertexData[vertexBaseIndex + 3].TexCoord.Y = uvRect.Y; vertexData[vertexBaseIndex + 3].Color = color; } device.AddVertices(this.material, VertexMode.Quads, vertexData, this.vertexBuffer.Count); } void ICmpUpdatable.OnUpdate() { // Update all existing particles Vector3 boundMax = Vector3.Zero; if (this.particles != null) { float timeMult = Time.TimeMult; float timePassed = Time.MsPFMult * timeMult; Vector3 boundRadiusOrigin = this.worldSpace ? this.GameObj.Transform.Pos : Vector3.Zero; Particle[] particleData = this.particles.Data; int particleCount = this.particles.Count; for (int i = particleCount - 1; i >= 0; i--) { particleData[i].Position += particleData[i].Velocity * timeMult; particleData[i].Angle += particleData[i].AngleVelocity * timeMult; particleData[i].Velocity += this.constantForce * 0.01f * timeMult; particleData[i].Velocity *= MathF.Pow(1.0f - (this.linearDrag * 0.1f), timeMult); particleData[i].AngleVelocity *= MathF.Pow(1.0f - (this.angularDrag * 0.1f), timeMult); particleData[i].AgeFactor += timePassed / particleData[i].TimeToLive; if (particleData[i].AgeFactor > 1.0f) this.RemoveParticle(i); boundMax.X = MathF.Max(boundMax.X, MathF.Abs(particleData[i].Position.X - boundRadiusOrigin.X)); boundMax.Y = MathF.Max(boundMax.Y, MathF.Abs(particleData[i].Position.Y - boundRadiusOrigin.Y)); boundMax.Z = MathF.Max(boundMax.Z, MathF.Abs(particleData[i].Position.Z - boundRadiusOrigin.Z)); } } this.boundRadius = boundMax.Length; if (this.worldSpace) this.boundRadius /= this.GameObj.Transform.Scale; this.boundRadius += this.particleSize.Length; // Update particle emission this.UpdateEmitters(); // Dispose when empty if (this.disposeWhenEmpty && this.IsEmpty) { this.GameObj.DisposeLater(); } } void ICmpInitializable.OnInit(Component.InitContext context) { if (context == InitContext.Activate) { // When activating, directly update particle emitters once, so there is already something to see. this.UpdateEmitters(); } } void ICmpInitializable.OnShutdown(Component.ShutdownContext context) {} } }