using System.Drawing; using System.Numerics; using DaggerFramework.Rendering; namespace DaggerFramework { // TODO: add oneshot parameter. public class Particles2d : Drawable2d { public int MaxParticles => _maxParticles; public void BeginEmit(ParticleSettings settings) { _settings = settings; _maxParticles = _settings.MaxParticles; _particleIndex = _maxParticles - 1; InitializeParticles(); } public void Restart() { CleanupParticles(); InitializeParticles(); } public override void OnDraw(in Renderer renderer) { foreach (var particle in _particles) { if (!particle.Alive) continue; var t = particle.LifeTimeRemaining / particle.LifeTime; var scale = MathUtils.Lerp(_settings.ScaleEnd, _settings.ScaleBegin, t); var color = MathUtils.LerpColor(_settings.ColorEnd, _settings.ColorBegin, t); renderer.SetTransform(particle.Position, particle.Rotation); renderer.DrawRectangle(Vector2.One * scale, color); } } protected override void OnStart() { base.OnStart(); // Emit(); } protected override void OnUpdate(double dt) { base.OnUpdate(dt); var rate = (int)MathUtils.Lerp(1, _maxParticles, _settings.Explosiveness); for (int i = 0; i < rate; i++) { Emit(); } for (int i = 0; i < _maxParticles; i++) { var particle = _particles[i]; if (!particle.Alive) continue; if (particle.LifeTimeRemaining <= 0.0f) { particle.Alive = false; continue; } particle.LifeTimeRemaining -= (float)dt; particle.Velocity += _settings.Gravity * (float)dt; particle.Position += particle.Velocity * (float)dt; particle.Rotation += particle.AngularVelocity * (float)dt; particle.Velocity -= particle.Velocity * _settings.Damping * (float)dt; _particles[i] = particle; } } private void InitializeParticles() { _particles = new Particle[_maxParticles]; } private void Emit() { Particle particle = _particles[_particleIndex]; if (!(particle.LifeTimeRemaining <= 0)) return; particle.Alive = true; particle.Position = GetEmitPosition(); particle.Velocity = _settings.Direction * _settings.LinearVelocity; particle.Velocity += Vector2.One * _settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f); particle.AngularVelocity = _settings.AngularVelocity; particle.AngularVelocity += 1f * _settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f); particle.LifeTime = _settings.LifeTime; particle.LifeTimeRemaining = particle.LifeTime; _particles[_particleIndex] = particle; _particleIndex = --_particleIndex <= 0 ? _maxParticles - 1 : --_particleIndex; } private void CleanupParticles() => Array.Clear(_particles); private Vector2 GetEmitPosition() { // https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image var angle = _random.NextDouble() * Math.PI * 2; float radius = (float)Math.Sqrt(_random.NextDouble()) * _settings.EmitRadius; float x = Position.X + radius * (float)Math.Cos(angle); float y = Position.Y + radius * (float)Math.Sin(angle); return new Vector2(x, y); } private ParticleSettings _settings; private int _maxParticles; private Particle[] _particles; private int _particleIndex; // TODO: replace a random function for better distribution and performance. private LehmerRandom _random = new LehmerRandom(); } public struct ParticleSettings { public ParticleSettings() { } public float EmitRadius; public float LifeTime; public float Explosiveness; public int MaxParticles; public Vector2 Direction; public float LinearVelocity; public float AngularVelocity = 0.0f; public float AngularVelocityRandom; public float LinearVelocityRandom; public Vector2 Gravity; public float ScaleBegin = 16f; public float ScaleEnd = 0.0f; public Color ColorBegin = Color.White; public Color ColorEnd = Color.Black; public float Damping = 0.0f; } public struct Particle { public Particle() { } public Vector2 Position; public Vector2 Velocity; public float AngularVelocity; public float LifeTime; public float LifeTimeRemaining; public float Scale; public float Rotation; public bool Alive = true; } }