320 lines
15 KiB
C#
320 lines
15 KiB
C#
#nullable enable
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Xml.Linq;
|
|
|
|
namespace Barotrauma.Particles
|
|
{
|
|
class ParticleEmitterProperties : ISerializableEntity
|
|
{
|
|
|
|
private const float MinValue = int.MinValue,
|
|
MaxValue = int.MaxValue;
|
|
|
|
public string Name => nameof(ParticleEmitter);
|
|
|
|
private float angleMin, angleMax;
|
|
|
|
public float AngleMinRad { get; private set; }
|
|
public float AngleMaxRad { get; private set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float AngleMin
|
|
{
|
|
get => angleMin;
|
|
set
|
|
{
|
|
angleMin = value;
|
|
AngleMinRad = MathHelper.ToRadians(MathHelper.Clamp(value, -360.0f, 360.0f));
|
|
}
|
|
}
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float AngleMax
|
|
{
|
|
get => angleMax;
|
|
set
|
|
{
|
|
angleMax = value;
|
|
AngleMaxRad = MathHelper.ToRadians(MathHelper.Clamp(value, -360.0f, 360.0f));
|
|
}
|
|
}
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float DistanceMin { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float DistanceMax { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float VelocityMin { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float VelocityMax { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, IsPropertySaveable.Yes)]
|
|
public float ScaleMin { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, IsPropertySaveable.Yes)]
|
|
public float ScaleMax { get; set; }
|
|
|
|
|
|
[Editable(), Serialize("1,1", IsPropertySaveable.Yes)]
|
|
public Vector2 ScaleMultiplier { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float EmitInterval { get; set; }
|
|
|
|
[Editable(ValueStep = 1, MinValueInt = 0, MaxValueInt = 1000), Serialize(0, IsPropertySaveable.Yes, description: "The number of particles to spawn per frame, or every x seconds if EmitInterval is set.")]
|
|
public int ParticleAmount { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 1000.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float ParticlesPerSecond { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 10.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes, description: "If larger than 0, a particle is spawned every x pixels across the ray cast by a hitscan weapon.")]
|
|
public float EmitAcrossRayInterval { get; set; }
|
|
|
|
[Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes, description: "Delay before the emitter becomes active after being created.")]
|
|
public float InitialDelay { get; set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes)]
|
|
public bool HighQualityCollisionDetection { get; set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes)]
|
|
public bool CopyEntityAngle { get; set; }
|
|
|
|
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")]
|
|
public bool CopyEntityDir { get; set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for status effects. Makes the emitter copy the angle from the target of the effect instead of the entity applying the effect.")]
|
|
public bool CopyTargetAngle { get; set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for particles spawned by another particle. Makes the emitter copy the scale of the parent particle.")]
|
|
public bool CopyParentParticleScale { get; set; }
|
|
|
|
[Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)]
|
|
public Color ColorMultiplier { get; set; }
|
|
|
|
[Editable, Serialize(1f, IsPropertySaveable.Yes)]
|
|
public float LifeTimeMultiplier { get; set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the particle be drawn as a tracer (a line from a weapon to the point it hit)? Only supported on hitscan projectiles and repair tools. Defaults to true on hitscan projectiles.")]
|
|
public bool UseTracerPoints { get; set; }
|
|
|
|
[Editable, Serialize(ParticleDrawOrder.Default, IsPropertySaveable.Yes)]
|
|
public ParticleDrawOrder DrawOrder { get; set; }
|
|
|
|
[Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float Angle
|
|
{
|
|
get => AngleMin;
|
|
set => AngleMin = AngleMax = value;
|
|
}
|
|
|
|
[Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float Distance
|
|
{
|
|
get => DistanceMin;
|
|
set => DistanceMin = DistanceMax = value;
|
|
}
|
|
|
|
[Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float Velocity
|
|
{
|
|
get => VelocityMin;
|
|
set => VelocityMin = VelocityMax = value;
|
|
}
|
|
|
|
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
|
|
|
|
public ParticleEmitterProperties(XElement element)
|
|
{
|
|
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
|
|
|
//backwards compatibility
|
|
if (element.GetAttributeBool("drawontop", false))
|
|
{
|
|
DrawOrder = ParticleDrawOrder.Foreground;
|
|
}
|
|
}
|
|
}
|
|
|
|
class ParticleEmitter
|
|
{
|
|
private float emitTimer;
|
|
private float burstEmitTimer;
|
|
private float initialDelay;
|
|
|
|
public readonly ParticleEmitterPrefab Prefab;
|
|
|
|
public ParticleEmitter(ContentXElement 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, Color? colorMultiplier = null, ParticlePrefab? overrideParticle = null, bool mirrorAngle = false, Tuple<Vector2, Vector2>? tracerPoints = null)
|
|
{
|
|
if (GameMain.Client?.MidRoundSyncing ?? false) { return; }
|
|
|
|
if (initialDelay < Prefab.Properties.InitialDelay)
|
|
{
|
|
initialDelay += deltaTime;
|
|
return;
|
|
}
|
|
|
|
emitTimer += deltaTime * amountMultiplier;
|
|
burstEmitTimer -= deltaTime;
|
|
|
|
if (Prefab.Properties.EmitAcrossRayInterval > 0.0f && tracerPoints != null)
|
|
{
|
|
Vector2 dir = tracerPoints.Item2 - tracerPoints.Item1;
|
|
if (dir.LengthSquared() > 0.001f)
|
|
{
|
|
float dist = dir.Length();
|
|
dir /= dist;
|
|
for (float z = 0.0f; z < dist; z += Prefab.Properties.EmitAcrossRayInterval)
|
|
{
|
|
Vector2 pos = tracerPoints.Item1 + dir * z;
|
|
Emit(pos, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: null);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Prefab.Properties.ParticlesPerSecond > 0)
|
|
{
|
|
float emitInterval = 1.0f / Prefab.Properties.ParticlesPerSecond;
|
|
while (emitTimer > emitInterval)
|
|
{
|
|
Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: tracerPoints);
|
|
emitTimer -= emitInterval;
|
|
}
|
|
}
|
|
|
|
if (burstEmitTimer > 0.0f) { return; }
|
|
|
|
burstEmitTimer = Prefab.Properties.EmitInterval;
|
|
for (int i = 0; i < Prefab.Properties.ParticleAmount * amountMultiplier; i++)
|
|
{
|
|
Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: tracerPoints);
|
|
}
|
|
}
|
|
|
|
private void Emit(Vector2 position, Hull? hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab? overrideParticle = null, bool mirrorAngle = false, Tuple<Vector2, Vector2>? tracerPoints = null)
|
|
{
|
|
var particlePrefab = overrideParticle ?? Prefab.ParticlePrefab;
|
|
if (particlePrefab == null)
|
|
{
|
|
DebugConsole.AddWarning($"Could not find the particle prefab \"{Prefab.ParticlePrefabName}\".",
|
|
contentPackage: Prefab.ContentPackage);
|
|
return;
|
|
}
|
|
|
|
Vector2 velocity = Vector2.Zero;
|
|
if (!MathUtils.NearlyEqual(Prefab.Properties.VelocityMax * velocityMultiplier, 0.0f) || !MathUtils.NearlyEqual(Prefab.Properties.DistanceMax, 0.0f))
|
|
{
|
|
angle += Rand.Range(Prefab.Properties.AngleMinRad, Prefab.Properties.AngleMaxRad) * (mirrorAngle ? -1 : 1);
|
|
Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
|
|
velocity = dir * Rand.Range(Prefab.Properties.VelocityMin, Prefab.Properties.VelocityMax) * velocityMultiplier;
|
|
position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax);
|
|
}
|
|
|
|
var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess,
|
|
particlePrefab.DrawOrder != ParticleDrawOrder.Default ? particlePrefab.DrawOrder : Prefab.DrawOrder,
|
|
lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: Prefab.Properties.UseTracerPoints ? tracerPoints : null);
|
|
|
|
if (particle != null)
|
|
{
|
|
particle.Size *= Rand.Range(Prefab.Properties.ScaleMin, Prefab.Properties.ScaleMax) * sizeMultiplier;
|
|
particle.Size *= Prefab.Properties.ScaleMultiplier;
|
|
particle.HighQualityCollisionDetection = Prefab.Properties.HighQualityCollisionDetection;
|
|
if (colorMultiplier.HasValue)
|
|
{
|
|
particle.ColorMultiplier = colorMultiplier.Value.ToVector4();
|
|
}
|
|
else if (Prefab.Properties.ColorMultiplier != Color.White)
|
|
{
|
|
particle.ColorMultiplier = Prefab.Properties.ColorMultiplier.ToVector4();
|
|
}
|
|
}
|
|
}
|
|
|
|
public Rectangle CalculateParticleBounds(Vector2 startPosition)
|
|
{
|
|
Rectangle bounds = new Rectangle((int)startPosition.X, (int)startPosition.Y, (int)startPosition.X, (int)startPosition.Y);
|
|
if (Prefab.ParticlePrefab == null) { return bounds; }
|
|
|
|
for (float angle = Prefab.Properties.AngleMinRad; angle <= Prefab.Properties.AngleMaxRad; angle += 0.1f)
|
|
{
|
|
Vector2 velocity = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Prefab.Properties.VelocityMax;
|
|
Vector2 endPosition = Prefab.ParticlePrefab.CalculateEndPosition(startPosition, velocity);
|
|
|
|
Vector2 endSize = Prefab.ParticlePrefab.CalculateEndSize();
|
|
float spriteExtent = 0.0f;
|
|
foreach (Sprite sprite in Prefab.ParticlePrefab.Sprites)
|
|
{
|
|
if (sprite is SpriteSheet spriteSheet)
|
|
{
|
|
spriteExtent = Math.Max(spriteExtent, Math.Max(spriteSheet.FrameSize.X * endSize.X, spriteSheet.FrameSize.Y * endSize.Y));
|
|
}
|
|
else
|
|
{
|
|
spriteExtent = Math.Max(spriteExtent, Math.Max(sprite.size.X * endSize.X, sprite.size.Y * endSize.Y));
|
|
}
|
|
}
|
|
|
|
bounds = new Rectangle(
|
|
(int)Math.Min(bounds.X, endPosition.X - Prefab.Properties.DistanceMax - spriteExtent / 2),
|
|
(int)Math.Min(bounds.Y, endPosition.Y - Prefab.Properties.DistanceMax - spriteExtent / 2),
|
|
(int)Math.Max(bounds.X, endPosition.X + Prefab.Properties.DistanceMax + spriteExtent / 2),
|
|
(int)Math.Max(bounds.Y, endPosition.Y + Prefab.Properties.DistanceMax + spriteExtent / 2));
|
|
}
|
|
|
|
bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - bounds.X, bounds.Height - bounds.Y);
|
|
return bounds;
|
|
}
|
|
}
|
|
|
|
class ParticleEmitterPrefab
|
|
{
|
|
public readonly Identifier ParticlePrefabName;
|
|
|
|
public ParticlePrefab? ParticlePrefab
|
|
{
|
|
get
|
|
{
|
|
ParticlePrefab.Prefabs.TryGet(ParticlePrefabName, out var prefab);
|
|
return prefab;
|
|
}
|
|
}
|
|
|
|
public readonly ParticleEmitterProperties Properties;
|
|
|
|
public readonly ContentPackage? ContentPackage;
|
|
|
|
public ParticleDrawOrder DrawOrder => Properties.DrawOrder != ParticleDrawOrder.Default ?
|
|
Properties.DrawOrder :
|
|
(ParticlePrefab?.DrawOrder ?? ParticleDrawOrder.Default);
|
|
|
|
public ParticleEmitterPrefab(ContentXElement element)
|
|
{
|
|
if (element == null) { throw new ArgumentNullException(nameof(element)); }
|
|
Properties = new ParticleEmitterProperties(element!);
|
|
ParticlePrefabName = element.GetAttributeIdentifier("particle", "");
|
|
ContentPackage = element.ContentPackage;
|
|
}
|
|
|
|
public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties)
|
|
{
|
|
Properties = properties;
|
|
ParticlePrefabName = prefab.Identifier;
|
|
}
|
|
}
|
|
}
|