using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma.Particles { enum ParticleBlendState { AlphaBlend, Additive//, Distortion } enum ParticleDrawOrder { Default, Foreground, Background } class ParticleManager { private const int MaxOutOfViewDist = 500; private int particleCount; public int ParticleCount { get { return particleCount; } } private int maxParticles; public int MaxParticles { get { return maxParticles; } set { if (maxParticles == value || value < 4) { return; } Particle[] newParticles = new Particle[value]; for (int i = 0; i < Math.Min(maxParticles, value); i++) { newParticles[i] = particles[i]; } particleCount = Math.Min(particleCount, value); particles = newParticles; maxParticles = value; var oldParticlesInCreationOrder = particlesInCreationOrder.ToList(); particlesInCreationOrder.Clear(); foreach (var particle in oldParticlesInCreationOrder) { if (particles.Contains(particle)) { particlesInCreationOrder.AddLast(particle); } } } } private Particle[] particles; /// /// Used for rendering the particles in the order in which they were created (starting from the most recent one) /// to avoid the order of the particles shuffling around when particles are removed from the pool. /// Linked list for fast additions and removals at the middle of the list. /// private readonly LinkedList particlesInCreationOrder = new LinkedList(); private Camera cam; public Camera Camera { get { return cam; } set { cam = value; } } public ParticleManager(Camera cam) { this.cam = cam; MaxParticles = GameSettings.CurrentConfig.Graphics.ParticleLimit; } public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess, collisionIgnoreTimer, tracerPoints: tracerPoints); } public Particle CreateParticle(string prefabName, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { ParticlePrefab prefab = FindPrefab(prefabName); if (prefab == null) { DebugConsole.ThrowError("Particle prefab \"" + prefabName + "\" not found!"); return null; } return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer, tracerPoints:tracerPoints); } public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, ParticleDrawOrder drawOrder = ParticleDrawOrder.Default, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } if (particleCount >= MaxParticles) { //maximum number of particles reached, and this is not a high-prio particle or something that should always draw // -> the particle won't be created, we can return early if (particleCount >= MaxParticles && prefab.Priority == 0 && !prefab.DrawAlways) { return null; } for (int i = 0; i < particleCount; i++) { if (particles[i].Prefab.Priority < prefab.Priority || (!particles[i].Prefab.DrawAlways && prefab.DrawAlways)) { RemoveParticle(i); break; } } if (particleCount >= MaxParticles) { return null; } } Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); Vector2 minPos = new Vector2(Math.Min(position.X, particleEndPos.X), Math.Min(position.Y, particleEndPos.Y)); Vector2 maxPos = new Vector2(Math.Max(position.X, particleEndPos.X), Math.Max(position.Y, particleEndPos.Y)); if (tracerPoints != null) { minPos = new Vector2( Math.Min(Math.Min(minPos.X, tracerPoints.Item1.X), tracerPoints.Item2.X), Math.Min(Math.Min(minPos.Y, tracerPoints.Item1.Y), tracerPoints.Item2.Y)); maxPos = new Vector2( Math.Max(Math.Max(maxPos.X, tracerPoints.Item1.X), tracerPoints.Item2.X), Math.Max(Math.Max(maxPos.Y, tracerPoints.Item1.Y), tracerPoints.Item2.Y)); } Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); if (!prefab.DrawAlways) { if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } } if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } Particle particle = particles[particleCount]; particle.Init(prefab, position, velocity, rotation, hullGuess, drawOrder, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; particlesInCreationOrder.AddFirst(particle); return particle; } public static List GetPrefabList() { return ParticlePrefab.Prefabs.ToList(); } public static ParticlePrefab FindPrefab(string prefabName) { ParticlePrefab.Prefabs.TryGet(prefabName, out ParticlePrefab prefab); return prefab; } private void RemoveParticle(int index) { particlesInCreationOrder.Remove(particles[index]); particleCount--; (particles[particleCount], particles[index]) = (particles[index], particles[particleCount]); } public void RemoveParticle(Particle particle) { for (int i = 0; i < particleCount; i++) { if (particles[i] == particle) { RemoveParticle(i); return; } } } public void Update(float deltaTime) { MaxParticles = GameSettings.CurrentConfig.Graphics.ParticleLimit; for (int i = 0; i < particleCount; i++) { bool remove; try { remove = particles[i].Update(deltaTime) == Particle.UpdateResult.Delete; } catch (Exception e) { DebugConsole.ThrowError("Particle update failed", e); remove = true; } if (remove) { RemoveParticle(i); } } } public void UpdateTransforms() { for (int i = 0; i < particleCount; i++) { particles[i].UpdateDrawPos(); } } public Dictionary CountActiveParticles() { Dictionary activeParticles = new Dictionary(); for (int i = 0; i < particleCount; i++) { if (!activeParticles.ContainsKey(particles[i].Prefab)) activeParticles[particles[i].Prefab] = 0; activeParticles[particles[i].Prefab]++; } return activeParticles; } public void Draw(SpriteBatch spriteBatch, bool inWater, bool? inSub, ParticleBlendState blendState, bool? background = false) { ParticlePrefab.DrawTargetType drawTarget = inWater ? ParticlePrefab.DrawTargetType.Water : ParticlePrefab.DrawTargetType.Air; foreach (var particle in particlesInCreationOrder) { if (particle.BlendState != blendState) { continue; } //equivalent to !particles[i].DrawTarget.HasFlag(drawTarget) but garbage free and faster if ((particle.DrawTarget & drawTarget) == 0) { continue; } if (inSub.HasValue) { bool isOutside = particle.CurrentHull == null; if (particle.DrawOrder != ParticleDrawOrder.Foreground && isOutside == inSub.Value) { continue; } } if (background.HasValue) { bool isBackgroundParticle = particle.DrawOrder == ParticleDrawOrder.Background; if (background.Value != isBackgroundParticle) { continue; } } particle.Draw(spriteBatch); } } public void ClearParticles() { particleCount = 0; particlesInCreationOrder.Clear(); } public void RemoveByPrefab(ParticlePrefab prefab) { if (particles == null) { return; } for (int i = particles.Length - 1; i >= 0; i--) { if (particles[i]?.Prefab == prefab) { if (i < particleCount) { particleCount--; } particlesInCreationOrder.Remove(particles[particleCount]); Particle swap = particles[particleCount]; particles[particleCount] = null; particles[i] = swap; } } } } }