(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -0,0 +1,131 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
namespace Barotrauma.Particles
{
class Decal
{
public readonly DecalPrefab Prefab;
private Vector2 position;
public readonly Sprite Sprite;
private float fadeTimer;
public float FadeTimer
{
get { return fadeTimer; }
set { fadeTimer = MathHelper.Clamp(value, 0.0f, LifeTime); }
}
public float FadeInTime
{
get { return Prefab.FadeInTime; }
}
public float FadeOutTime
{
get { return Prefab.FadeOutTime; }
}
public float LifeTime
{
get { return Prefab.LifeTime; }
}
public Color Color
{
get;
set;
}
public Vector2 WorldPosition
{
get
{
Vector2 worldPos = position
+ clippedSourceRect.Size.ToVector2() / 2 * scale
+ hull.Rect.Location.ToVector2();
if (hull.Submarine != null) { worldPos += hull.Submarine.DrawPosition; }
return worldPos;
}
}
private Hull hull;
private float scale;
private Rectangle clippedSourceRect;
public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull)
{
Prefab = prefab;
this.hull = hull;
//transform to hull-relative coordinates so we don't have to worry about the hull moving
position = worldPosition - hull.WorldRect.Location.ToVector2();
Vector2 drawPos = position + hull.Rect.Location.ToVector2();
Sprite = prefab.Sprites[Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced)];
Color = prefab.Color;
Rectangle drawRect = new Rectangle(
(int)(drawPos.X - Sprite.size.X / 2 * scale),
(int)(drawPos.Y + Sprite.size.Y / 2 * scale),
(int)(Sprite.size.X * scale),
(int)(Sprite.size.Y * scale));
Rectangle overFlowAmount = new Rectangle(
(int)Math.Max(hull.Rect.X - drawRect.X, 0.0f),
(int)Math.Max(drawRect.Y - hull.Rect.Y, 0.0f),
(int)Math.Max(drawRect.Right - hull.Rect.Right, 0.0f),
(int)Math.Max((hull.Rect.Y - hull.Rect.Height) - (drawRect.Y - drawRect.Height), 0.0f));
clippedSourceRect = new Rectangle(
Sprite.SourceRect.X + (int)(overFlowAmount.X / scale),
Sprite.SourceRect.Y + (int)(overFlowAmount.Y / scale),
Sprite.SourceRect.Width - (int)((overFlowAmount.X + overFlowAmount.Width) / scale),
Sprite.SourceRect.Height - (int)((overFlowAmount.Y + overFlowAmount.Height) / scale));
position -= new Vector2(Sprite.size.X / 2 * scale - overFlowAmount.X, -Sprite.size.Y / 2 * scale + overFlowAmount.Y);
this.scale = scale;
}
public void Update(float deltaTime)
{
fadeTimer += deltaTime;
}
public void StopFadeIn()
{
Color *= GetAlpha();
fadeTimer = Prefab.FadeInTime;
}
public void Draw(SpriteBatch spriteBatch, Hull hull, float depth)
{
Vector2 drawPos = position + hull.Rect.Location.ToVector2();
if (hull.Submarine != null) { drawPos += hull.Submarine.DrawPosition; }
drawPos.Y = -drawPos.Y;
spriteBatch.Draw(Sprite.Texture, drawPos, clippedSourceRect, Color * GetAlpha(), 0, Vector2.Zero, scale, SpriteEffects.None, depth);
}
private float GetAlpha()
{
if (fadeTimer < Prefab.FadeInTime)
{
return fadeTimer / Prefab.FadeInTime;
}
else if (fadeTimer > Prefab.LifeTime - Prefab.FadeOutTime)
{
return (Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime;
}
return 1.0f;
}
}
}
@@ -0,0 +1,75 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma.Particles
{
class DecalManager
{
public PrefabCollection<DecalPrefab> Prefabs { get; private set; }
public DecalManager()
{
Prefabs = new PrefabCollection<DecalPrefab>();
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Decals))
{
LoadFromFile(configFile);
}
}
public void LoadFromFile(ContentFile configFile)
{
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { return; }
bool allowOverriding = false;
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
allowOverriding = true;
}
foreach (XElement sourceElement in mainElement.Elements())
{
var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
string name = element.Name.ToString().ToLowerInvariant();
if (Prefabs.ContainsKey(name))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing decal prefab '{name}' using the file '{configFile.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate decal prefab '{name}' found in '{configFile.Path}'! Each decal prefab must have a unique name. " +
"Use <override></override> tags to override prefabs.");
continue;
}
}
Prefabs.Add(new DecalPrefab(element, configFile), allowOverriding || sourceElement.IsOverride());
}
}
public void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull)
{
if (!Prefabs.ContainsKey(decalName.ToLowerInvariant()))
{
DebugConsole.ThrowError("Decal prefab " + decalName + " not found!");
return null;
}
DecalPrefab prefab = Prefabs[decalName];
return new Decal(prefab, scale, worldPosition, hull);
}
}
}
@@ -0,0 +1,73 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Particles
{
class DecalPrefab : IPrefab, IDisposable
{
public readonly string Name;
public string OriginalName { get { return Name; } }
private string _identifier;
public string Identifier
{
get
{
if (_identifier == null)
{
_identifier = Name.ToLowerInvariant();
}
return _identifier;
}
}
public string FilePath { get; private set; }
public ContentPackage ContentPackage { get; private set; }
public void Dispose()
{
foreach (Sprite spr in Sprites)
{
spr.Remove();
}
Sprites.Clear();
}
public readonly List<Sprite> Sprites;
public readonly Color Color;
public readonly float LifeTime;
public readonly float FadeOutTime;
public readonly float FadeInTime;
public DecalPrefab(XElement element, ContentFile file)
{
Name = element.Name.ToString();
FilePath = file.Path;
ContentPackage = file.ContentPackage;
Sprites = new List<Sprite>();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase))
{
Sprites.Add(new Sprite(subElement));
}
}
Color = new Color(element.GetAttributeVector4("color", Vector4.One));
LifeTime = element.GetAttributeFloat("lifetime", 10.0f);
FadeOutTime = Math.Min(LifeTime, element.GetAttributeFloat("fadeouttime", 1.0f));
FadeInTime = Math.Min(LifeTime - FadeOutTime, element.GetAttributeFloat("fadeintime", 0.0f));
}
}
}
@@ -0,0 +1,479 @@
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;
public delegate void OnChangeHullHandler(Vector2 position, Hull currentHull);
public OnChangeHullHandler OnChangeHull;
private Vector2 position;
private Vector2 prevPosition;
private Vector2 velocity;
private float rotation;
private float prevRotation;
private float angularVelocity;
private Vector2 dragVec = Vector2.Zero;
private int dragWait = 0;
private Vector2 size;
private Vector2 sizeChange;
private Color color;
private bool changeColor;
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 Hull currentHull;
private List<Gap> hullGaps;
private bool hasSubEmitters;
private List<ParticleEmitter> subEmitters = new List<ParticleEmitter>();
private float animState;
private int animFrame;
private float collisionUpdateTimer;
public bool HighQualityCollisionDetection;
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 = MathHelper.Clamp(value, Prefab.StartDelayMin, prefab.StartDelayMax); }
}
public Vector2 Size
{
get { return size; }
set { size = value; }
}
public Hull CurrentHull
{
get { return currentHull; }
}
public ParticlePrefab Prefab
{
get { return prefab; }
}
public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false)
{
this.prefab = prefab;
spriteIndex = Rand.Int(prefab.Sprites.Count);
animState = 0;
animFrame = 0;
dragWait = 0;
dragVec = Vector2.Zero;
currentHull = Hull.FindHull(position, hullGuess);
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);
totalLifeTime = prefab.LifeTime;
lifeTime = prefab.LifeTime;
startDelay = Rand.Range(prefab.StartDelayMin, prefab.StartDelayMax);
size = prefab.StartSizeMin + (prefab.StartSizeMax - prefab.StartSizeMin) * Rand.Range(0.0f, 1.0f);
sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f);
color = prefab.StartColor;
changeColor = prefab.StartColor != prefab.EndColor;
velocityChange = prefab.VelocityChangeDisplay;
velocityChangeWater = prefab.VelocityChangeWaterDisplay;
HighQualityCollisionDetection = false;
OnChangeHull = 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<Gap>() : currentHull.ConnectedGaps;
}
if (prefab.RotateToDirection)
{
this.rotation = MathUtils.VectorToAngle(new Vector2(velocity.X, -velocity.Y));
prevRotation = rotation;
}
DrawOnTop = drawOnTop;
}
public bool Update(float deltaTime)
{
if (startDelay > 0.0f)
{
startDelay -= deltaTime;
return true;
}
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 * deltaTime;
velocity.Y += velocityChangeWater.Y * deltaTime;
if (prefab.WaterDrag > 0.0f)
{
ApplyDrag(prefab.WaterDrag, deltaTime);
}
}
else
{
velocity.X += velocityChange.X * deltaTime;
velocity.Y += velocityChange.Y * deltaTime;
if (prefab.Drag > 0.0f)
{
ApplyDrag(prefab.Drag, deltaTime);
}
}
size.X += sizeChange.X * deltaTime;
size.Y += sizeChange.Y * deltaTime;
if (changeColor)
{
color = Color.Lerp(prefab.EndColor, prefab.StartColor, lifeTime / prefab.LifeTime);
}
if (prefab.Sprites[spriteIndex] is SpriteSheet)
{
animState += deltaTime;
int frameCount = ((SpriteSheet)prefab.Sprites[spriteIndex]).FrameCount;
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 false; }
if (hasSubEmitters)
{
foreach (ParticleEmitter emitter in subEmitters)
{
emitter.Emit(deltaTime, position, currentHull);
}
}
if (!prefab.UseCollision) { return true; }
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 true;
}
private bool CollisionUpdate()
{
if (currentHull == null)
{
Hull collidedHull = Hull.FindHull(position);
if (collidedHull != null)
{
if (prefab.DeleteOnCollision) return false;
OnWallCollisionOutside(collidedHull);
}
}
else
{
Rectangle hullRect = currentHull.WorldRect;
Vector2 collisionNormal = Vector2.Zero;
if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height)
{
if (prefab.DeleteOnCollision) return false;
collisionNormal = new Vector2(0.0f, 1.0f);
}
else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y)
{
if (prefab.DeleteOnCollision) return false;
collisionNormal = new Vector2(0.0f, -1.0f);
}
else if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X)
{
if (prefab.DeleteOnCollision) return false;
collisionNormal = new Vector2(1.0f, 0.0f);
}
else if (velocity.X > 0.0f && position.X + prefab.CollisionRadius * size.X > hullRect.Right)
{
if (prefab.DeleteOnCollision) return false;
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 != (collisionNormal.X != 0.0f)) continue;
if (gap.IsHorizontal)
{
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;
}
else
{
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 (!gapFound)
{
OnWallCollisionInside(currentHull, collisionNormal);
}
else
{
Hull newHull = Hull.FindHull(position, currentHull);
if (newHull != currentHull)
{
currentHull = newHull;
hullGaps = currentHull == null ? new List<Gap>() : currentHull.ConnectedGaps;
OnChangeHull?.Invoke(position, currentHull);
}
}
}
}
return true;
}
private void ApplyDrag(float dragCoefficient, float deltaTime)
{
if (velocity.LengthSquared() < dragVec.LengthSquared())
{
velocity = Vector2.Zero;
return;
}
if (Math.Abs(velocity.X) < 0.0001f && Math.Abs(velocity.Y) < 0.0001f) return;
//TODO: some better way to handle particle drag
//this doesn't work that well because the drag vector is only updated every 0.5 seconds, allowing the particle to accelerate way more than it should
//(e.g. a falling particle can freely accelerate for 0.5 seconds before the drag takes effect)
dragWait--;
if (dragWait <= 0)
{
dragWait = 30;
float speed = velocity.Length();
dragVec = (velocity / speed) * Math.Min(speed * speed * dragCoefficient * deltaTime, 1.0f);
}
velocity -= dragVec;
}
private void OnWallCollisionInside(Hull prevHull, Vector2 collisionNormal)
{
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 + prefab.CollisionRadius * size.X);
}
else
{
position.X = Math.Min(position.X, prevHullRect.Right - prefab.CollisionRadius * size.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 + prefab.CollisionRadius * size.Y);
}
else
{
position.Y = Math.Min(position.Y, prevHullRect.Y - prefab.CollisionRadius * size.Y);
}
velocity.X *= (1.0f - prefab.Friction);
velocity.Y = Math.Sign(collisionNormal.Y) * Math.Abs(velocity.Y) * prefab.Restitution;
}
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 - prefab.CollisionRadius;
velocity.X *= (1.0f - prefab.Friction);
velocity.Y = -velocity.Y * prefab.Restitution;
}
else if (position.Y > center.Y)
{
position.Y = hullRect.Y + prefab.CollisionRadius;
velocity.X *= (1.0f - prefab.Friction);
velocity.Y = -velocity.Y * prefab.Restitution;
}
if (position.X < center.X)
{
position.X = hullRect.X - prefab.CollisionRadius;
velocity.X = -velocity.X * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction);
}
else if (position.X > center.X)
{
position.X = hullRect.X + hullRect.Width + prefab.CollisionRadius;
velocity.X = -velocity.X * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction);
}
velocity *= prefab.Restitution;
}
public void UpdateDrawPos()
{
drawPosition = Timing.Interpolate(prevPosition, position);
drawRotation = Timing.Interpolate(prevRotation, rotation);
}
public void Draw(SpriteBatch spriteBatch)
{
Vector2 drawSize = size;
if (prefab.GrowTime > 0.0f && totalLifeTime - lifeTime < prefab.GrowTime)
{
drawSize *= ((totalLifeTime - lifeTime) / prefab.GrowTime);
}
if (prefab.Sprites[spriteIndex] is SpriteSheet)
{
((SpriteSheet)prefab.Sprites[spriteIndex]).Draw(
spriteBatch, animFrame,
new Vector2(drawPosition.X, -drawPosition.Y),
color * (color.A / 255.0f),
prefab.Sprites[spriteIndex].Origin, drawRotation,
drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
}
else
{
prefab.Sprites[spriteIndex].Draw(spriteBatch,
new Vector2(drawPosition.X, -drawPosition.Y),
color * (color.A / 255.0f),
prefab.Sprites[spriteIndex].Origin, drawRotation,
drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
}
}
}
}
@@ -0,0 +1,184 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma.Particles
{
class ParticleEmitter
{
private float emitTimer;
private float burstEmitTimer;
public readonly ParticleEmitterPrefab Prefab;
public ParticleEmitter(XElement element)
{
Prefab = new ParticleEmitterPrefab(element);
}
public ParticleEmitter(ParticleEmitterPrefab prefab)
{
System.Diagnostics.Debug.Assert(prefab != null, "The prefab of a particle emitter cannot be null");
Prefab = prefab;
}
public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f)
{
emitTimer += deltaTime * amountMultiplier;
burstEmitTimer -= deltaTime;
if (Prefab.ParticlesPerSecond > 0)
{
float emitInterval = 1.0f / Prefab.ParticlesPerSecond;
while (emitTimer > emitInterval)
{
Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier);
emitTimer -= emitInterval;
}
}
if (burstEmitTimer > 0.0f) { return; }
burstEmitTimer = Prefab.EmitInterval;
for (int i = 0; i < Prefab.ParticleAmount * amountMultiplier; i++)
{
Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier);
}
}
private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier)
{
angle += Rand.Range(Prefab.AngleMin, Prefab.AngleMax);
Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
Vector2 velocity = dir * Rand.Range(Prefab.VelocityMin, Prefab.VelocityMax) * velocityMultiplier;
position += dir * Rand.Range(Prefab.DistanceMin, Prefab.DistanceMax);
var particle = GameMain.ParticleManager.CreateParticle(Prefab.ParticlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop);
if (particle != null)
{
particle.Size *= Rand.Range(Prefab.ScaleMin, Prefab.ScaleMax) * sizeMultiplier;
particle.HighQualityCollisionDetection = Prefab.HighQualityCollisionDetection;
}
}
public Rectangle CalculateParticleBounds(Vector2 startPosition)
{
Rectangle bounds = new Rectangle((int)startPosition.X, (int)startPosition.Y, (int)startPosition.X, (int)startPosition.Y);
for (float angle = Prefab.AngleMin; angle <= Prefab.AngleMax; angle += 0.1f)
{
Vector2 velocity = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Prefab.VelocityMax;
Vector2 endPosition = Prefab.ParticlePrefab.CalculateEndPosition(startPosition, velocity);
bounds = new Rectangle(
(int)Math.Min(bounds.X, endPosition.X - Prefab.DistanceMax),
(int)Math.Min(bounds.Y, endPosition.Y - Prefab.DistanceMax),
(int)Math.Max(bounds.X, endPosition.X + Prefab.DistanceMax),
(int)Math.Max(bounds.Y, endPosition.Y + Prefab.DistanceMax));
}
bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - bounds.X, bounds.Height - bounds.Y);
return bounds;
}
}
class ParticleEmitterPrefab
{
public readonly string Name;
public readonly ParticlePrefab ParticlePrefab;
public readonly float AngleMin, AngleMax;
public readonly float DistanceMin, DistanceMax;
public readonly float VelocityMin, VelocityMax;
public readonly float ScaleMin, ScaleMax;
public readonly float EmitInterval;
public readonly int ParticleAmount;
public readonly float ParticlesPerSecond;
public readonly bool HighQualityCollisionDetection;
public readonly bool CopyEntityAngle;
public readonly bool DrawOnTop;
public ParticleEmitterPrefab(XElement element)
{
Name = element.Name.ToString();
ParticlePrefab = GameMain.ParticleManager.FindPrefab(element.GetAttributeString("particle", ""));
if (element.Attribute("startrotation") == null)
{
AngleMin = element.GetAttributeFloat("anglemin", 0.0f);
AngleMax = element.GetAttributeFloat("anglemax", 0.0f);
}
else
{
AngleMin = element.GetAttributeFloat("angle", 0.0f);
AngleMax = AngleMin;
}
AngleMin = MathHelper.ToRadians(MathHelper.Clamp(AngleMin, -360.0f, 360.0f));
AngleMax = MathHelper.ToRadians(MathHelper.Clamp(AngleMax, -360.0f, 360.0f));
if (element.Attribute("scalemin") == null)
{
ScaleMin = 1.0f;
ScaleMax = 1.0f;
}
else
{
ScaleMin = element.GetAttributeFloat("scalemin", 1.0f);
ScaleMax = Math.Max(ScaleMin, element.GetAttributeFloat("scalemax", 1.0f));
}
if (element.Attribute("distance") == null)
{
DistanceMin = element.GetAttributeFloat("distancemin", 0.0f);
DistanceMax = element.GetAttributeFloat("distancemax", 0.0f);
}
else
{
DistanceMin = DistanceMax = element.GetAttributeFloat("distance", 0.0f);
}
if (DistanceMax < DistanceMin)
{
var temp = DistanceMin;
DistanceMin = DistanceMax;
DistanceMax = temp;
}
if (element.Attribute("velocity") == null)
{
VelocityMin = element.GetAttributeFloat("velocitymin", 0.0f);
VelocityMax = element.GetAttributeFloat("velocitymax", 0.0f);
}
else
{
VelocityMin = VelocityMax = element.GetAttributeFloat("velocity", 0.0f);
}
if (VelocityMax < VelocityMin)
{
var temp = VelocityMin;
VelocityMin = VelocityMax;
VelocityMax = temp;
}
EmitInterval = element.GetAttributeFloat("emitinterval", 0.0f);
ParticlesPerSecond = element.GetAttributeInt("particlespersecond", 0);
ParticleAmount = element.GetAttributeInt("particleamount", 0);
HighQualityCollisionDetection = element.GetAttributeBool("highqualitycollisiondetection", false);
CopyEntityAngle = element.GetAttributeBool("copyentityangle", false);
DrawOnTop = element.GetAttributeBool("drawontop", false);
}
}
}
@@ -0,0 +1,265 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Particles
{
enum ParticleBlendState
{
AlphaBlend, Additive//, Distortion
}
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;
}
}
private Particle[] particles;
public readonly PrefabCollection<ParticlePrefab> Prefabs = new PrefabCollection<ParticlePrefab>();
private Camera cam;
public Camera Camera
{
get { return cam; }
set { cam = value; }
}
public ParticleManager(Camera cam)
{
this.cam = cam;
MaxParticles = GameMain.Config.ParticleLimit;
}
public void LoadPrefabs()
{
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles))
{
LoadPrefabsFromFile(configFile);
}
}
public void LoadPrefabsFromFile(ContentFile configFile)
{
var particleElements = new Dictionary<string, XElement>();
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { return; }
bool allowOverriding = false;
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
allowOverriding = true;
}
foreach (XElement sourceElement in mainElement.Elements())
{
var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
string name = element.Name.ToString().ToLowerInvariant();
if (Prefabs.ContainsKey(name) || particleElements.ContainsKey(name))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing particle prefab '{name}' using the file '{configFile.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate particle prefab '{name}' found in '{configFile.Path}'! Each particle prefab must have a unique name. " +
"Use <override></override> tags to override prefabs.");
continue;
}
}
particleElements.Add(name, element);
}
foreach (var kvp in particleElements)
{
Prefabs.Add(new ParticlePrefab(kvp.Value, configFile), allowOverriding);
}
}
public void RemovePrefabsByFile(string configFile)
{
Prefabs.RemoveByFile(configFile);
}
public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null)
{
return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess);
}
public Particle CreateParticle(string prefabName, Vector2 position, Vector2 velocity, float rotation=0.0f, Hull hullGuess = null)
{
ParticlePrefab prefab = FindPrefab(prefabName);
if (prefab == null)
{
DebugConsole.ThrowError("Particle prefab \"" + prefabName+"\" not found!");
return null;
}
return CreateParticle(prefab, position, velocity, rotation, hullGuess);
}
public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false)
{
if (particleCount >= MaxParticles || prefab == null) 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));
Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist);
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();
particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop);
particleCount++;
return particles[particleCount - 1];
}
public List<ParticlePrefab> GetPrefabList()
{
return Prefabs.ToList();
}
public ParticlePrefab FindPrefab(string prefabName)
{
return Prefabs.Find(p => p.Identifier == prefabName);
}
private void RemoveParticle(int index)
{
particleCount--;
Particle swap = particles[index];
particles[index] = particles[particleCount];
particles[particleCount] = swap;
}
public void Update(float deltaTime)
{
MaxParticles = GameMain.Config.ParticleLimit;
for (int i = 0; i < particleCount; i++)
{
bool remove = false;
try
{
remove = !particles[i].Update(deltaTime);
}
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<ParticlePrefab, int> CountActiveParticles()
{
Dictionary<ParticlePrefab, int> activeParticles = new Dictionary<ParticlePrefab, int>();
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)
{
ParticlePrefab.DrawTargetType drawTarget = inWater ? ParticlePrefab.DrawTargetType.Water : ParticlePrefab.DrawTargetType.Air;
for (int i = 0; i < particleCount; i++)
{
var particle = particles[i];
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.DrawOnTop)
{
if (isOutside != inSub.Value)
{
continue;
}
}
else if (isOutside == inSub.Value)
{
continue;
}
}
particles[i].Draw(spriteBatch);
}
}
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--; }
Particle swap = particles[particleCount];
particles[particleCount] = null;
particles[i] = swap;
}
}
}
}
}
@@ -0,0 +1,294 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Particles
{
class ParticlePrefab : IPrefab, IDisposable, ISerializableEntity
{
public enum DrawTargetType { Air = 1, Water = 2, Both = 3 }
public readonly List<Sprite> Sprites;
public void Dispose()
{
GameMain.ParticleManager?.RemoveByPrefab(this);
foreach (Sprite spr in Sprites)
{
spr.Remove();
}
Sprites.Clear();
}
public string Name
{
get;
private set;
}
public string FilePath
{
get;
private set;
}
public string OriginalName { get { return Name; } }
public string Identifier { get { return Name; } }
public ContentPackage ContentPackage
{
get;
private set;
}
public string DisplayName
{
get;
private set;
}
[Editable(0.0f, float.MaxValue), Serialize(5.0f, false, description: "How many seconds the particle remains alive.")]
public float LifeTime { get; private set; }
[Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")]
public float StartDelayMin { get; private set; }
[Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")]
public float StartDelayMax { get; private set; }
//movement -----------------------------------------
private float angularVelocityMin;
public float AngularVelocityMinRad { get; private set; }
[Editable, Serialize(0.0f, false)]
public float AngularVelocityMin
{
get { return angularVelocityMin; }
private set
{
angularVelocityMin = value;
AngularVelocityMinRad = MathHelper.ToRadians(value);
}
}
private float angularVelocityMax;
public float AngularVelocityMaxRad { get; private set; }
[Editable, Serialize(0.0f, false)]
public float AngularVelocityMax
{
get { return angularVelocityMax; }
private set
{
angularVelocityMax = value;
AngularVelocityMaxRad = MathHelper.ToRadians(value);
}
}
private float startRotationMin;
public float StartRotationMinRad { get; private set; }
[Editable, Serialize(0.0f, false, description: "The minimum initial rotation of the particle (in degrees).")]
public float StartRotationMin
{
get { return startRotationMin; }
private set
{
startRotationMin = value;
StartRotationMinRad = MathHelper.ToRadians(value);
}
}
private float startRotationMax;
public float StartRotationMaxRad { get; private set; }
[Editable, Serialize(0.0f, false, description: "The maximum initial rotation of the particle (in degrees).")]
public float StartRotationMax
{
get { return startRotationMax; }
private set
{
startRotationMax = value;
StartRotationMaxRad = MathHelper.ToRadians(value);
}
}
[Editable, Serialize(false, false, description: "Should the particle face the direction it's moving towards.")]
public bool RotateToDirection { get; private set; }
[Editable, Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through air.")]
public float Drag { get; private set; }
[Editable, Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through water.")]
public float WaterDrag { get; private set; }
private Vector2 velocityChange;
public Vector2 VelocityChangeDisplay { get; private set; }
[Editable, Serialize("0.0,0.0", false, description: "How much the velocity of the particle changes per second.")]
public Vector2 VelocityChange
{
get { return velocityChange; }
private set
{
velocityChange = value;
VelocityChangeDisplay = ConvertUnits.ToDisplayUnits(value);
}
}
private Vector2 velocityChangeWater;
public Vector2 VelocityChangeWaterDisplay { get; private set; }
[Editable, Serialize("0.0,0.0", false, description: "How much the velocity of the particle changes per second when in water.")]
public Vector2 VelocityChangeWater
{
get { return velocityChangeWater; }
private set
{
velocityChangeWater = value;
VelocityChangeWaterDisplay = ConvertUnits.ToDisplayUnits(value);
}
}
[Editable(0.0f, 10000.0f), Serialize(0.0f, false, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")]
public float CollisionRadius { get; private set; }
[Editable, Serialize(false, false, description: "Does the particle collide with the walls of the submarine and the level.")]
public bool UseCollision { get; private set; }
[Editable, Serialize(false, false, description: "Does the particle disappear when it collides with something.")]
public bool DeleteOnCollision { get; private set; }
[Editable(0.0f, 1.0f), Serialize(0.5f, false, description: "The friction coefficient of the particle, i.e. how much it slows down when it's sliding against a surface.")]
public float Friction { get; private set; }
[Editable(0.0f, 1.0f)]
[Serialize(0.5f, false, description: "How much of the particle's velocity is conserved when it collides with something, i.e. the \"bounciness\" of the particle. (1.0 = the particle stops completely).")]
public float Restitution { get; private set; }
//size -----------------------------------------
[Editable, Serialize("1.0,1.0", false, description: "The minimum initial size of the particle.")]
public Vector2 StartSizeMin { get; private set; }
[Editable, Serialize("1.0,1.0", false, description: "The maximum initial size of the particle.")]
public Vector2 StartSizeMax { get; private set; }
[Editable]
[Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")]
public Vector2 SizeChangeMin { get; private set; }
[Editable]
[Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")]
public Vector2 SizeChangeMax { get; private set; }
[Editable]
[Serialize(0.0f, false, description: "How many seconds it takes for the particle to grow to it's initial size.")]
public float GrowTime { get; private set; }
//rendering -----------------------------------------
[Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")]
public Color StartColor { get; private set; }
[Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The color of the particle at the end of its lifetime.")]
public Color EndColor { get; private set; }
[Editable, Serialize(DrawTargetType.Air, false, description: "Should the particle be rendered in air, water or both.")]
public DrawTargetType DrawTarget { get; private set; }
[Editable, Serialize(ParticleBlendState.AlphaBlend, false, description: "The type of blending to use when rendering the particle.")]
public ParticleBlendState BlendState { get; private set; }
//animation -----------------------------------------
[Editable(0.0f, float.MaxValue), Serialize(1.0f, false, description: "The duration of the particle's animation cycle (if it's animated).")]
public float AnimDuration { get; private set; }
[Editable, Serialize(true, false, description: "Should the sprite animation be looped, or stay at the last frame when the animation finishes.")]
public bool LoopAnim { get; private set; }
//----------------------------------------------------
public readonly List<ParticleEmitterPrefab> SubEmitters = new List<ParticleEmitterPrefab>();
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
//----------------------------------------------------
public ParticlePrefab(XElement element, ContentFile file)
{
Name = element.Name.ToString();
FilePath = file.Path;
ContentPackage = file.ContentPackage;
DisplayName = TextManager.Get("particle." + Name, true) ?? Name;
Sprites = new List<Sprite>();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
Sprites.Add(new Sprite(subElement));
break;
case "spritesheet":
case "animatedsprite":
Sprites.Add(new SpriteSheet(subElement));
break;
case "particleemitter":
case "emitter":
case "subemitter":
SubEmitters.Add(new ParticleEmitterPrefab(subElement));
break;
}
}
//if velocity change in water is not given, it defaults to the normal velocity change
if (element.Attribute("velocitychangewater") == null)
{
VelocityChangeWater = VelocityChange;
}
if (element.Attribute("angularvelocity") != null)
{
AngularVelocityMin = element.GetAttributeFloat("angularvelocity", 0.0f);
AngularVelocityMax = AngularVelocityMin;
}
if (element.Attribute("startsize") != null)
{
StartSizeMin = element.GetAttributeVector2("startsize", Vector2.One);
StartSizeMax = StartSizeMin;
}
if (element.Attribute("sizechange") != null)
{
SizeChangeMin = element.GetAttributeVector2("sizechange", Vector2.Zero);
SizeChangeMax = SizeChangeMin;
}
if (element.Attribute("startrotation") != null)
{
StartRotationMin = element.GetAttributeFloat("startrotation", 0.0f);
StartRotationMax = StartRotationMin;
}
if (CollisionRadius <= 0.0f) CollisionRadius = Sprites.Count > 0 ? 1 : Sprites[0].SourceRect.Width / 2.0f;
}
public Vector2 CalculateEndPosition(Vector2 startPosition, Vector2 velocity)
{
//endPos = x + vt + 1/2 * at^2
return startPosition + velocity * LifeTime + 0.5f * VelocityChangeDisplay * LifeTime * LifeTime;
}
}
}