using Barotrauma.Extensions; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; namespace Barotrauma.Particles { class Particle { private ParticlePrefab prefab; private string debugName = "Particle (uninitialized)"; public delegate void OnChangeHullHandler(Vector2 position, Hull currentHull); public OnChangeHullHandler OnChangeHull; public OnChangeHullHandler OnCollision; private Vector2 position; private Vector2 prevPosition; private Vector2 velocity; private float rotation; private float prevRotation; private float angularVelocity; private float collisionIgnoreTimer = 0; private Vector2 size; private Vector2 sizeChange; private Color color; private bool changeColor; private bool UseMiddleColor; private int spriteIndex; private float totalLifeTime; private float lifeTime; private float startDelay; private Vector2 velocityChange; private Vector2 velocityChangeWater; private Vector2 drawPosition; private float drawRotation; private Vector2 colliderRadius; private Hull currentHull; private List hullGaps; private bool hasSubEmitters; private readonly List subEmitters = new List(); private float animState; private int animFrame; private float collisionUpdateTimer; private bool changesSize; public bool HighQualityCollisionDetection; public Vector4 ColorMultiplier; public float VelocityChangeMultiplier; public bool DrawOnTop { get; private set; } public ParticlePrefab.DrawTargetType DrawTarget { get { return prefab.DrawTarget; } } public ParticleBlendState BlendState { get { return prefab.BlendState; } } public float StartDelay { get { return startDelay; } set { startDelay = Math.Max(value, 0.0f); } } public Vector2 Size { get { return size; } set { size = value; } } public Hull CurrentHull { get { return currentHull; } } public ParticlePrefab Prefab { get { return prefab; } } public override string ToString() { return debugName; } public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { this.prefab = prefab; #if DEBUG debugName = $"Particle ({prefab.Name})"; #else //don't instantiate new string objects in release builds debugName = prefab.Name; #endif spriteIndex = Rand.Int(prefab.Sprites.Count); animState = 0; animFrame = 0; currentHull = prefab.CanEnterSubs ? Hull.FindHull(position, hullGuess) : null; size = prefab.StartSizeMin + (prefab.StartSizeMax - prefab.StartSizeMin) * Rand.Range(0.0f, 1.0f); if (tracerPoints != null) { size = new Vector2(Vector2.Distance(tracerPoints.Item1, tracerPoints.Item2), size.Y); position = (tracerPoints.Item1 + tracerPoints.Item2) / 2; } RefreshColliderSize(); sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f); changesSize = !sizeChange.NearlyEquals(Vector2.Zero); this.position = position; prevPosition = position; drawPosition = position; velocity = MathUtils.IsValid(speed) ? speed : Vector2.Zero; if (currentHull?.Submarine != null) { velocity += ConvertUnits.ToDisplayUnits(currentHull.Submarine.Velocity); } this.rotation = rotation + Rand.Range(prefab.StartRotationMinRad, prefab.StartRotationMaxRad); prevRotation = rotation; angularVelocity = Rand.Range(prefab.AngularVelocityMinRad, prefab.AngularVelocityMaxRad); if (prefab.LifeTimeMin <= 0.0f) { totalLifeTime = prefab.LifeTime * lifeTimeMultiplier; lifeTime = prefab.LifeTime * lifeTimeMultiplier; } else { totalLifeTime = Rand.Range(prefab.LifeTimeMin, prefab.LifeTime) * lifeTimeMultiplier; lifeTime = totalLifeTime * lifeTimeMultiplier; } startDelay = Rand.Range(prefab.StartDelayMin, prefab.StartDelayMax); UseMiddleColor = prefab.UseMiddleColor; color = prefab.StartColor; changeColor = prefab.StartColor != prefab.EndColor; ColorMultiplier = Vector4.One; velocityChange = prefab.VelocityChangeDisplay; velocityChangeWater = prefab.VelocityChangeWaterDisplay; HighQualityCollisionDetection = false; VelocityChangeMultiplier = 1.0f; OnChangeHull = null; OnCollision = null; subEmitters.Clear(); hasSubEmitters = false; foreach (ParticleEmitterPrefab emitterPrefab in prefab.SubEmitters) { subEmitters.Add(new ParticleEmitter(emitterPrefab)); hasSubEmitters = true; } if (prefab.UseCollision) { hullGaps = currentHull == null ? new List() : currentHull.ConnectedGaps; } if (prefab.RotateToDirection) { this.rotation = MathUtils.VectorToAngle(new Vector2(velocity.X, -velocity.Y)); prevRotation = rotation; } DrawOnTop = drawOnTop; this.collisionIgnoreTimer = collisionIgnoreTimer; } public enum UpdateResult { Normal, Delete } public UpdateResult Update(float deltaTime) { if (startDelay > 0.0f) { startDelay -= deltaTime; return UpdateResult.Normal; } prevPosition = position; prevRotation = rotation; //over 3 times faster than position += velocity * deltatime position.X += velocity.X * deltaTime; position.Y += velocity.Y * deltaTime; if (prefab.RotateToDirection) { if (velocityChange != Vector2.Zero || angularVelocity != 0.0f) { Vector2 relativeVel = velocity; if (currentHull?.Submarine != null) { relativeVel -= ConvertUnits.ToDisplayUnits(currentHull.Submarine.Velocity); } rotation = MathUtils.VectorToAngle(new Vector2(relativeVel.X, -relativeVel.Y)); } } else { rotation += angularVelocity * deltaTime; } bool inWater = (currentHull == null || (currentHull.Submarine != null && position.Y - currentHull.Submarine.DrawPosition.Y < currentHull.Surface)); if (inWater) { velocity.X += velocityChangeWater.X * VelocityChangeMultiplier * deltaTime; velocity.Y += velocityChangeWater.Y * VelocityChangeMultiplier * deltaTime; if (prefab.WaterDrag > 0.0f) { ApplyDrag(prefab.WaterDrag, deltaTime); } } else { velocity.X += velocityChange.X * VelocityChangeMultiplier * deltaTime; velocity.Y += velocityChange.Y * VelocityChangeMultiplier * deltaTime; if (prefab.Drag > 0.0f) { ApplyDrag(prefab.Drag, deltaTime); } } if (changesSize) { size.X += sizeChange.X * deltaTime; size.Y += sizeChange.Y * deltaTime; RefreshColliderSize(); } if (UseMiddleColor) { if (lifeTime > totalLifeTime * 0.5f) { color = Color.Lerp(prefab.MiddleColor, prefab.StartColor, (lifeTime / totalLifeTime - 0.5f) * 2.0f); } else { color = Color.Lerp(prefab.EndColor, prefab.MiddleColor, lifeTime / totalLifeTime * 2.0f); } } else { if (changeColor) { color = Color.Lerp(prefab.EndColor, prefab.StartColor, lifeTime / totalLifeTime); } } if (prefab.Sprites[spriteIndex] is SpriteSheet) { animState += deltaTime; int frameCount = ((SpriteSheet)prefab.Sprites[spriteIndex]).FrameCount; if (prefab.LoopAnim) { animFrame = (int)(Math.Floor(animState / prefab.AnimDuration * frameCount) % frameCount); } else { animFrame = (int)Math.Min(Math.Floor(animState / prefab.AnimDuration * frameCount), frameCount - 1); } } lifeTime -= deltaTime; if (lifeTime <= 0.0f || color.A <= 0 || size.X <= 0.0f || size.Y <= 0.0f) { return UpdateResult.Delete; } if (hasSubEmitters) { foreach (ParticleEmitter emitter in subEmitters) { emitter.Emit(deltaTime, position, currentHull, particleRotation: rotation, sizeMultiplier: emitter.Prefab.Properties.CopyParentParticleScale ? Math.Max(size.X, size.Y) : 1.0f); } } if (collisionIgnoreTimer > 0f) { collisionIgnoreTimer -= deltaTime; if (collisionIgnoreTimer <= 0f) { currentHull ??= Hull.FindHull(position); } return UpdateResult.Normal; } if (!prefab.UseCollision) { return UpdateResult.Normal; } if (HighQualityCollisionDetection) { return CollisionUpdate(); } else { collisionUpdateTimer -= deltaTime; if (collisionUpdateTimer <= 0.0f) { //more frequent collision updates if the particle is moving fast collisionUpdateTimer = 0.5f - Math.Min((Math.Abs(velocity.X) + Math.Abs(velocity.Y)) * 0.01f, 0.45f); return CollisionUpdate(); } } return UpdateResult.Normal; } private UpdateResult CollisionUpdate() { if (currentHull == null) { Hull collidedHull = Hull.FindHull(position); if (collidedHull != null) { if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } OnWallCollisionOutside(collidedHull); } } else { Rectangle hullRect = currentHull.WorldRect; Vector2 collisionNormal = Vector2.Zero; if (velocity.Y < 0.0f && position.Y - colliderRadius.Y < hullRect.Y - hullRect.Height) { collisionNormal = new Vector2(0.0f, 1.0f); } else if (velocity.Y > 0.0f && position.Y + colliderRadius.Y > hullRect.Y) { collisionNormal = new Vector2(0.0f, -1.0f); } if (collisionNormal != Vector2.Zero) { bool gapFound = false; foreach (Gap gap in hullGaps) { if (gap.Open <= 0.9f || gap.IsHorizontal) { continue; } if (gap.WorldRect.X > position.X || gap.WorldRect.Right < position.X) { continue; } float hullCenterY = currentHull.WorldRect.Y - currentHull.WorldRect.Height / 2; int gapDir = Math.Sign(gap.WorldRect.Y - hullCenterY); if (Math.Sign(velocity.Y) != gapDir || Math.Sign(position.Y - hullCenterY) != gapDir) { continue; } gapFound = true; break; } if (prefab.DeleteOnCollision && !gapFound) { OnCollision?.Invoke(position, currentHull); return UpdateResult.Delete; } handleCollision(gapFound, collisionNormal); } collisionNormal = Vector2.Zero; if (velocity.X < 0.0f && position.X - colliderRadius.X < hullRect.X) { collisionNormal = new Vector2(1.0f, 0.0f); } else if (velocity.X > 0.0f && position.X + colliderRadius.X > hullRect.Right) { collisionNormal = new Vector2(-1.0f, 0.0f); } if (collisionNormal != Vector2.Zero) { bool gapFound = false; foreach (Gap gap in hullGaps) { if (gap.Open <= 0.9f || !gap.IsHorizontal) { continue; } if (gap.WorldRect.Y < position.Y || gap.WorldRect.Y - gap.WorldRect.Height > position.Y) { continue; } int gapDir = Math.Sign(gap.WorldRect.Center.X - currentHull.WorldRect.Center.X); if (Math.Sign(velocity.X) != gapDir || Math.Sign(position.X - currentHull.WorldRect.Center.X) != gapDir) { continue; } gapFound = true; break; } if (prefab.DeleteOnCollision && !gapFound) { OnCollision?.Invoke(position, currentHull); return UpdateResult.Delete; } handleCollision(gapFound, collisionNormal); } void handleCollision(bool gapFound, Vector2 collisionNormal) { if (!gapFound) { OnWallCollisionInside(currentHull, collisionNormal); } else { Hull newHull = Hull.FindHull(position, currentHull); if (newHull != currentHull) { currentHull = newHull; hullGaps = currentHull == null ? new List() : currentHull.ConnectedGaps; OnChangeHull?.Invoke(position, currentHull); } } } } return UpdateResult.Normal; } private void RefreshColliderSize() { if (!prefab.UseCollision) { return; } colliderRadius = new Vector2(prefab.CollisionRadius); if (!prefab.InvariantCollisionSize) { colliderRadius *= size; } } private void ApplyDrag(float dragCoefficient, float deltaTime) { Vector2 relativeVel = velocity; if (currentHull?.Submarine != null) { relativeVel = velocity - ConvertUnits.ToDisplayUnits(currentHull.Submarine.Velocity); } float speed = relativeVel.Length(); relativeVel /= speed; float drag = speed * speed * dragCoefficient * 0.01f * deltaTime; if (drag > speed) { relativeVel = Vector2.Zero; } else { speed -= drag; relativeVel *= speed; } velocity = relativeVel; if (currentHull?.Submarine != null) { velocity += ConvertUnits.ToDisplayUnits(currentHull.Submarine.Velocity); } } private void OnWallCollisionInside(Hull prevHull, Vector2 collisionNormal) { if (prevHull == null) { return; } Rectangle prevHullRect = prevHull.WorldRect; Vector2 subVel = prevHull?.Submarine != null ? ConvertUnits.ToDisplayUnits(prevHull.Submarine.Velocity) : Vector2.Zero; velocity -= subVel; if (Math.Abs(collisionNormal.X) > Math.Abs(collisionNormal.Y)) { if (collisionNormal.X > 0.0f) { position.X = Math.Max(position.X, prevHullRect.X + colliderRadius.X); } else { position.X = Math.Min(position.X, prevHullRect.Right - colliderRadius.X); } velocity.X = Math.Sign(collisionNormal.X) * Math.Abs(velocity.X) * prefab.Restitution; velocity.Y *= (1.0f - prefab.Friction); } else { if (collisionNormal.Y > 0.0f) { position.Y = Math.Max(position.Y, prevHullRect.Y - prevHullRect.Height + colliderRadius.Y); } else { position.Y = Math.Min(position.Y, prevHullRect.Y - colliderRadius.Y); } velocity.X *= (1.0f - prefab.Friction); velocity.Y = Math.Sign(collisionNormal.Y) * Math.Abs(velocity.Y) * prefab.Restitution; } OnCollision?.Invoke(position, currentHull); velocity += subVel; } private void OnWallCollisionOutside(Hull collisionHull) { Rectangle hullRect = collisionHull.WorldRect; Vector2 center = new Vector2(hullRect.X + hullRect.Width / 2, hullRect.Y - hullRect.Height / 2); if (position.Y < center.Y) { position.Y = hullRect.Y - hullRect.Height - colliderRadius.Y; velocity.X *= (1.0f - prefab.Friction); velocity.Y = -velocity.Y * prefab.Restitution; } else if (position.Y > center.Y) { position.Y = hullRect.Y + colliderRadius.Y; velocity.X *= (1.0f - prefab.Friction); velocity.Y = -velocity.Y * prefab.Restitution; } if (position.X < center.X) { position.X = hullRect.X - colliderRadius.X; velocity.X = -velocity.X * prefab.Restitution; velocity.Y *= (1.0f - prefab.Friction); } else if (position.X > center.X) { position.X = hullRect.X + hullRect.Width + colliderRadius.X; velocity.X = -velocity.X * prefab.Restitution; velocity.Y *= (1.0f - prefab.Friction); } OnCollision?.Invoke(position, currentHull); velocity *= prefab.Restitution; } public void UpdateDrawPos() { drawPosition = Timing.Interpolate(prevPosition, position); drawRotation = Timing.Interpolate(prevRotation, rotation); } public void Draw(SpriteBatch spriteBatch) { if (startDelay > 0.0f) { return; } Vector2 drawSize = size; if (prefab.GrowTime > 0.0f && totalLifeTime - lifeTime < prefab.GrowTime) { drawSize *= MathUtils.SmoothStep((totalLifeTime - lifeTime) / prefab.GrowTime); } Color currColor = new Color(color.ToVector4() * ColorMultiplier); Vector2 drawPos = new Vector2(drawPosition.X, -drawPosition.Y); if (prefab.Sprites[spriteIndex] is SpriteSheet sheet) { sheet.Draw( spriteBatch, animFrame, drawPos, currColor * (currColor.A / 255.0f), prefab.Sprites[spriteIndex].Origin, drawRotation, drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth); } else { prefab.Sprites[spriteIndex].Draw(spriteBatch, drawPos, currColor * (currColor.A / 255.0f), prefab.Sprites[spriteIndex].Origin, drawRotation, drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth); } /*if (GameMain.DebugDraw && prefab.UseCollision) { GUI.DrawLine(spriteBatch, drawPos - Vector2.UnitX * colliderRadius.X, drawPos + Vector2.UnitX * colliderRadius.X, Color.Gray); GUI.DrawLine(spriteBatch, drawPos - Vector2.UnitY * colliderRadius.Y, drawPos + Vector2.UnitY * colliderRadius.Y, Color.Gray); }*/ } } }