using Godot; using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; namespace Particles { public class ParticleSimulation { public Vector2 ScreenSize; private Dictionary _particles = new Dictionary(); private List _particleTypes = new List(); private Dictionary _particleTypeCounts = new Dictionary(); private List tasks = new List(); public List LastParticlesAdded { get; private set; } = new List(); public List LastParticlesRemoved { get; private set; } = new List(); private int _idCount = 0; private const int MaxParticles = 1200; private const int MaxParticleTypes = 10; private const float HealthDelta = 0.002f; private const float NegativeHealthMultiplier = 1.5f; public void Initialize() { for (var i = 0; i < MaxParticleTypes; i++) CreateRandomParticleType(); for (var i = 0; i < MaxParticles; i++) CreateRandomParticle(); } public void Update() { LastParticlesRemoved.Clear(); LastParticlesAdded.Clear(); tasks.Clear(); foreach (var id in _particles.Keys) tasks.Add(Task.Factory.StartNew(UpdateParticle, id)); Task.WaitAll(tasks.ToArray()); var deletedParticle = false; foreach (var p in _particles) { var particle = p.Value; if (deletedParticle == false && particle.Health == 0f) { if (GD.Randf() < 0.2f) { LastParticlesRemoved.Add(p.Key); deletedParticle = true; continue; } } var position = particle.Position; particle.Velocity = particle.Velocity.Clamped(3f * 4f); position += particle.Velocity; particle.Velocity *= 0.855f; // friction if (position.x > ScreenSize.x) position.x -= ScreenSize.x; else if (position.x < 0) position.x += ScreenSize.x; if (position.y > ScreenSize.y) position.y -= ScreenSize.y; else if (position.y < 0) position.y += ScreenSize.y; particle.AddAverageSpeedValue(particle.Position.DistanceTo(position)); if (particle.AverageSpeed < 0.3f) particle.Health -= HealthDelta * NegativeHealthMultiplier; else particle.Health += HealthDelta; if (deletedParticle == false && particle.Health == 0f) { if (GD.Randf() < 0.2f) { LastParticlesRemoved.Add(p.Key); deletedParticle = true; continue; } } particle.Position = position; } foreach (var id in LastParticlesRemoved) { _particles.Remove(id); } if (_particles.Count < MaxParticles) { if (GD.Randf() < 0.2f) { CreateRandomParticle(); } } } private void RemoveParticleType(ParticleType type) { _particleTypes.Remove(type); foreach (var t in _particleTypes) { t.RemoveRelationship(type); } } private void CreateRandomParticleType() { var type = new ParticleType() { Hue = (float) GD.RandRange(0, 1) }; _particleTypes.Add(type); _particleTypeCounts[type] = 0; foreach (var type1 in _particleTypes) foreach(var type2 in _particleTypes) type1.AddRelationship(type2, new ParticleRelationshipProps(16, (float)GD.RandRange(20, 60), (float)GD.RandRange(-0.35 * 2, 0.35 * 2))); } private void CreateRandomParticle() { var randomIndex = (int) (GD.Randi() % _particleTypes.Count); var type = _particleTypes[randomIndex]; CreateParticle(type); } private void CreateParticle(ParticleType type) { var particle = new Particle(_idCount, type) { Position = GetRandomParticlePosition(), Health = 1f }; LastParticlesAdded.Add(_idCount); _particles.Add(_idCount, particle); _idCount++; _particleTypeCounts[type]++; } private Vector2 GetRandomParticlePosition() { var position = new Vector2( (float) GD.RandRange(0, ScreenSize.x), (float) GD.RandRange(0, ScreenSize.y)); return position; } private Vector2 GetScreenWrapPosition(Vector2 p1, Vector2 p2) { var newPosition = p2; if (p2.x > (p1.x + (ScreenSize.x / 2f))) newPosition.x = p2.x - ScreenSize.x; else if (p2.x < (p1.x - (ScreenSize.x / 2f))) newPosition.x = p2.x + ScreenSize.x; if (p2.y > (p1.y + (ScreenSize.y / 2f))) newPosition.y = p2.y - ScreenSize.y; else if (p2.y < (p1.y - (ScreenSize.y / 2f))) newPosition.y = p2.y + ScreenSize.y; return newPosition; } public Particle GetParticle(int id) { return _particles[id]; } private Task UpdateParticle(object i) { var id = (int)i; var particle1 = _particles[id]; var closeCount = 0; foreach (var p2 in _particles) { var particle2 = p2.Value; if (particle1 == particle2) continue; var position = GetScreenWrapPosition(particle1.Position, particle2.Position); var distanceSquared = particle1.Position.DistanceSquaredTo(position); if (distanceSquared > (60f * 60f)) continue; var direction = particle1.Position.DirectionTo(position); if (distanceSquared < (70f * 70f)) closeCount++; // collision force float distance; if (distanceSquared < (16f * 16f)) { distance = particle1.Position.DistanceTo(position); var collisionForce = 1f / (0.3f + Mathf.Pow(Mathf.E, -(distance - 11f))) - 1f / 0.3f; particle1.Velocity += direction * collisionForce; } // particle relationship force var props = particle1.Type.GetRelationship(particle2.Type); if (props.Force != 0f && distanceSquared >= props.MinRadius * props.MinRadius && distanceSquared <= props.MaxRadius * props.MaxRadius) { distance = particle1.Position.DistanceTo(position); var slope = props.Force / ((props.MaxRadius - props.MinRadius) / 2f); var particleForce = -slope * Mathf.Abs(distance - (props.MinRadius + props.MaxRadius) / 2f) + props.Force; particle1.Velocity += direction * particleForce; } } if (closeCount < 4 || closeCount > 33) particle1.Health -= HealthDelta * NegativeHealthMultiplier; else particle1.Health += HealthDelta; return Task.CompletedTask; } } }