summaryrefslogtreecommitdiff
path: root/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles
diff options
context:
space:
mode:
authorchai <215380520@qq.com>2024-06-03 10:15:45 +0800
committerchai <215380520@qq.com>2024-06-03 10:15:45 +0800
commitacea7b2e728787a0d83bbf83c8c1f042d2c32e7e (patch)
tree0bfec05c1ca2d71be2c337bcd110a0421f19318b /Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles
parent88febcb02bf127d961c6471d9e846c0e1315f5c3 (diff)
+ plugins project
Diffstat (limited to 'Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles')
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs77
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs59
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs43
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs152
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs83
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs222
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs49
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs68
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs33
44 files changed, 1635 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs
new file mode 100644
index 0000000..66c99e7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs
@@ -0,0 +1,17 @@
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ public static class FastRandomExtensions
+ {
+ public static void NextColor(this FastRandom random, out HslColor color, Range<HslColor> range)
+ {
+ var maxH = range.Max.H >= range.Min.H
+ ? range.Max.H
+ : range.Max.H + 360;
+ color = new HslColor(random.NextSingle(range.Min.H, maxH),
+ random.NextSingle(range.Min.S, range.Max.S),
+ random.NextSingle(range.Min.L, range.Max.L));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs
new file mode 100644
index 0000000..a3c1422
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles
+{
+ /// <summary>
+ /// Defines a part of a line that is bounded by two distinct end points.
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LineSegment : IEquatable<LineSegment>
+ {
+ internal readonly Vector2 _point1;
+ internal readonly Vector2 _point2;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LineSegment" /> structure.
+ /// </summary>
+ /// <param name="point1"></param>
+ /// <param name="point2"></param>
+ public LineSegment(Vector2 point1, Vector2 point2)
+ {
+ _point1 = point1;
+ _point2 = point2;
+ }
+
+ public static LineSegment FromPoints(Vector2 p1, Vector2 p2) => new LineSegment(p1, p2);
+ public static LineSegment FromOrigin(Vector2 origin, Vector2 vector) => new LineSegment(origin, origin + vector);
+
+ public Vector2 Origin => _point1;
+
+ public Vector2 Direction
+ {
+ get
+ {
+ var coord = _point2 - _point1;
+ return new Vector2(coord.X, coord.Y);
+ }
+ }
+
+ public LineSegment Translate(Vector2 t)
+ {
+ return new LineSegment(_point1 + t, _point2 + t);
+ }
+
+ public Vector2 ToVector()
+ {
+ var t = _point2 - _point1;
+ return new Vector2(t.X, t.Y);
+ }
+
+ public bool Equals(LineSegment other)
+ {
+ // ReSharper disable ImpureMethodCallOnReadonlyValueField
+ return _point1.Equals(other._point1) && _point2.Equals(other._point2);
+ // ReSharper restore ImpureMethodCallOnReadonlyValueField
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+
+ return obj is LineSegment & Equals((LineSegment) obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return (_point1.GetHashCode()*397) ^ _point2.GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return $"({_point1.X}:{_point1.Y} {_point2.X}:{_point2.Y})";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs
new file mode 100644
index 0000000..fac85f0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class AgeModifier : Modifier
+ {
+ [EditorBrowsable(EditorBrowsableState.Always)]
+ public List<Interpolator> Interpolators { get; set; } = new List<Interpolator>();
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var n = Interpolators.Count;
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(particle->Age, particle);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs
new file mode 100644
index 0000000..30d2802
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs
@@ -0,0 +1,52 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public class CircleContainerModifier : Modifier
+ {
+ public float Radius { get; set; }
+ public bool Inside { get; set; } = true;
+ public float RestitutionCoefficient { get; set; } = 1;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var radiusSq = Radius*Radius;
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var localPos = particle->Position - particle->TriggerPos;
+
+ var distSq = localPos.LengthSquared();
+ var normal = localPos;
+ normal.Normalize();
+
+ if (Inside)
+ {
+ if (distSq < radiusSq) continue;
+
+ SetReflected(distSq, particle, normal);
+ }
+ else
+ {
+ if (distSq > radiusSq) continue;
+
+ SetReflected(distSq, particle, -normal);
+ }
+ }
+ }
+
+ private unsafe void SetReflected(float distSq, Particle* particle, Vector2 normal)
+ {
+ var dist = (float) Math.Sqrt(distSq);
+ var d = dist - Radius; // how far outside the circle is the particle
+
+ var twoRestDot = 2*RestitutionCoefficient*
+ Vector2.Dot(particle->Velocity, normal);
+ particle->Velocity -= twoRestDot*normal;
+
+ // exact computation requires sqrt or goniometrics
+ particle->Position -= normal*d;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs
new file mode 100644
index 0000000..a56e072
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs
@@ -0,0 +1,59 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public sealed class RectangleContainerModifier : Modifier
+ {
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public float RestitutionCoefficient { get; set; } = 1;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ var left = particle->TriggerPos.X + Width*-0.5f;
+ var right = particle->TriggerPos.X + Width*0.5f;
+ var top = particle->TriggerPos.Y + Height*-0.5f;
+ var bottom = particle->TriggerPos.Y + Height*0.5f;
+
+ var xPos = particle->Position.X;
+ var xVel = particle->Velocity.X;
+ var yPos = particle->Position.Y;
+ var yVel = particle->Velocity.Y;
+
+ if ((int) particle->Position.X < left)
+ {
+ xPos = left + (left - xPos);
+ xVel = -xVel*RestitutionCoefficient;
+ }
+ else
+ {
+ if (particle->Position.X > right)
+ {
+ xPos = right - (xPos - right);
+ xVel = -xVel*RestitutionCoefficient;
+ }
+ }
+
+ if (particle->Position.Y < top)
+ {
+ yPos = top + (top - yPos);
+ yVel = -yVel*RestitutionCoefficient;
+ }
+ else
+ {
+ if ((int) particle->Position.Y > bottom)
+ {
+ yPos = bottom - (yPos - bottom);
+ yVel = -yVel*RestitutionCoefficient;
+ }
+ }
+ particle->Position = new Vector2(xPos, yPos);
+ particle->Velocity = new Vector2(xVel, yVel);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
new file mode 100644
index 0000000..4da6efa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public class RectangleLoopContainerModifier : Modifier
+ {
+ public int Width { get; set; }
+ public int Height { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var left = particle->TriggerPos.X + Width*-0.5f;
+ var right = particle->TriggerPos.X + Width*0.5f;
+ var top = particle->TriggerPos.Y + Height*-0.5f;
+ var bottom = particle->TriggerPos.Y + Height*0.5f;
+
+ var xPos = particle->Position.X;
+ var yPos = particle->Position.Y;
+
+ if ((int) particle->Position.X < left)
+ xPos = particle->Position.X + Width;
+ else
+ {
+ if ((int) particle->Position.X > right)
+ xPos = particle->Position.X - Width;
+ }
+
+ if ((int) particle->Position.Y < top)
+ yPos = particle->Position.Y + Height;
+ else
+ {
+ if ((int) particle->Position.Y > bottom)
+ yPos = particle->Position.Y - Height;
+ }
+ particle->Position = new Vector2(xPos, yPos);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs
new file mode 100644
index 0000000..f2d6d09
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs
@@ -0,0 +1,23 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class DragModifier : Modifier
+ {
+ public float DragCoefficient { get; set; } = 0.47f;
+ public float Density { get; set; } = .5f;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var drag = -DragCoefficient*Density*particle->Mass*elapsedSeconds;
+
+ particle->Velocity = new Vector2(
+ particle->Velocity.X + particle->Velocity.X*drag,
+ particle->Velocity.Y + particle->Velocity.Y*drag);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs
new file mode 100644
index 0000000..f94c689
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs
@@ -0,0 +1,13 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ /// <summary>
+ /// Defines a modifier which interpolates the color of a particle over the course of its lifetime.
+ /// </summary>
+ public sealed class ColorInterpolator : Interpolator<HslColor>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Color = HslColor.Lerp(StartValue, EndValue, amount);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs
new file mode 100644
index 0000000..8b58a8e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs
@@ -0,0 +1,12 @@
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class HueInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Color = new HslColor((EndValue - StartValue) * amount + StartValue, particle->Color.S, particle->Color.L);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs
new file mode 100644
index 0000000..0ec9866
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs
@@ -0,0 +1,26 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public abstract class Interpolator
+ {
+ protected Interpolator()
+ {
+ Name = GetType().Name;
+ }
+
+ public string Name { get; set; }
+ public abstract unsafe void Update(float amount, Particle* particle);
+ }
+
+ public abstract class Interpolator<T> : Interpolator
+ {
+ /// <summary>
+ /// Gets or sets the intial value when the particles are created.
+ /// </summary>
+ public virtual T StartValue { get; set; }
+
+ /// <summary>
+ /// Gets or sets the final value when the particles are retired.
+ /// </summary>
+ public virtual T EndValue { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs
new file mode 100644
index 0000000..0a26fcd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class OpacityInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Opacity = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs
new file mode 100644
index 0000000..44a72de
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class RotationInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Rotation = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs
new file mode 100644
index 0000000..aff9244
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class ScaleInterpolator : Interpolator<Vector2>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Scale = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator
new file mode 100644
index 0000000..bad04b1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class VelocityInterpolator : Interpolator<Vector2>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Velocity = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs
new file mode 100644
index 0000000..e9ed216
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs
@@ -0,0 +1,23 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class LinearGravityModifier : Modifier
+ {
+ public Vector2 Direction { get; set; }
+ public float Strength { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var vector = Direction*(Strength*elapsedSeconds);
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Velocity = new Vector2(
+ particle->Velocity.X + vector.X*particle->Mass,
+ particle->Velocity.Y + vector.Y*particle->Mass);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs
new file mode 100644
index 0000000..bd62d25
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs
@@ -0,0 +1,18 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public abstract class Modifier
+ {
+ protected Modifier()
+ {
+ Name = GetType().Name;
+ }
+
+ public string Name { get; set; }
+ public abstract void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator);
+
+ public override string ToString()
+ {
+ return $"{Name} [{GetType().Name}]";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs
new file mode 100644
index 0000000..9cc44d8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs
@@ -0,0 +1,14 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public sealed class OpacityFastFadeModifier : Modifier
+ {
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Opacity = 1.0f - particle->Age;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs
new file mode 100644
index 0000000..2b17058
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs
@@ -0,0 +1,18 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class RotationModifier : Modifier
+ {
+ public float RotationRate { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var rotationRateDelta = RotationRate*elapsedSeconds;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Rotation += rotationRateDelta;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs
new file mode 100644
index 0000000..ae9dc7b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class VelocityColorModifier : Modifier
+ {
+ public HslColor StationaryColor { get; set; }
+ public HslColor VelocityColor { get; set; }
+ public float VelocityThreshold { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var velocityThreshold2 = VelocityThreshold*VelocityThreshold;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var velocity2 = particle->Velocity.X*particle->Velocity.X +
+ particle->Velocity.Y*particle->Velocity.Y;
+ var deltaColor = VelocityColor - StationaryColor;
+
+ if (velocity2 >= velocityThreshold2)
+ VelocityColor.CopyTo(out particle->Color);
+ else
+ {
+ var t = (float) Math.Sqrt(velocity2)/VelocityThreshold;
+
+ particle->Color = new HslColor(
+ deltaColor.H*t + StationaryColor.H,
+ deltaColor.S*t + StationaryColor.S,
+ deltaColor.L*t + StationaryColor.L);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs
new file mode 100644
index 0000000..f089abc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class VelocityModifier : Modifier
+ {
+ public List<Interpolator> Interpolators { get; set; } = new List<Interpolator>();
+
+ public float VelocityThreshold { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var velocityThreshold2 = VelocityThreshold*VelocityThreshold;
+ var n = Interpolators.Count;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var velocity2 = particle->Velocity.LengthSquared();
+
+ if (velocity2 >= velocityThreshold2)
+ {
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(1, particle);
+ }
+ }
+ else
+ {
+ var t = (float) Math.Sqrt(velocity2)/VelocityThreshold;
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(t, particle);
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs
new file mode 100644
index 0000000..03a9999
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs
@@ -0,0 +1,30 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public unsafe class VortexModifier : Modifier
+ {
+ // Note: not the real-life one
+ private const float _gravConst = 100000f;
+
+ public Vector2 Position { get; set; }
+ public float Mass { get; set; }
+ public float MaxSpeed { get; set; }
+
+ public override void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var diff = Position + particle->TriggerPos - particle->Position;
+
+ var distance2 = diff.LengthSquared();
+
+ var speedGain = _gravConst*Mass/distance2*elapsedSeconds;
+ // normalize distances and multiply by speedGain
+ diff.Normalize();
+ particle->Velocity += diff*speedGain;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj
new file mode 100644
index 0000000..d82f26e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A high performance particle system to make MonoGame more awesome.</Description>
+ <PackageTags>monogame particle system</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs
new file mode 100644
index 0000000..042d8ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs
@@ -0,0 +1,24 @@
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct Particle
+ {
+ public float Inception;
+ public float Age;
+ public Vector2 Position;
+ public Vector2 TriggerPos;
+ public Vector2 Velocity;
+ public HslColor Color;
+ public float Opacity;
+ public Vector2 Scale;
+ public float Rotation;
+ public float Mass;
+ public float LayerDepth;
+
+ public static readonly int SizeInBytes = Marshal.SizeOf(typeof(Particle));
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs
new file mode 100644
index 0000000..2e410fa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleBuffer : IDisposable
+ {
+ private readonly ParticleIterator _iterator;
+ private readonly IntPtr _nativePointer;
+
+ // points to the first memory pos after the buffer
+ protected readonly unsafe Particle* BufferEnd;
+
+ private bool _disposed;
+ // points to the particle after the last active particle.
+ protected unsafe Particle* Tail;
+
+ public unsafe ParticleBuffer(int size)
+ {
+ Size = size;
+ _nativePointer = Marshal.AllocHGlobal(SizeInBytes);
+
+ BufferEnd = (Particle*) (_nativePointer + SizeInBytes);
+ Head = (Particle*) _nativePointer;
+ Tail = (Particle*) _nativePointer;
+
+ _iterator = new ParticleIterator(this);
+
+ GC.AddMemoryPressure(SizeInBytes);
+ }
+
+ public int Size { get; }
+
+ public ParticleIterator Iterator => _iterator.Reset();
+ // pointer to the first particle
+ public unsafe Particle* Head { get; private set; }
+
+ // Number of available particle spots in the buffer
+ public int Available => Size - Count;
+ // current number of particles
+ public int Count { get; private set; }
+ // total size of the buffer (add one extra spot in memory for margin between head and tail so the iterator can see that it's at the end)
+ public int SizeInBytes => Particle.SizeInBytes*(Size + 1);
+ // total size of active particles
+ public int ActiveSizeInBytes => Particle.SizeInBytes*Count;
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Marshal.FreeHGlobal(_nativePointer);
+ _disposed = true;
+ GC.RemoveMemoryPressure(SizeInBytes);
+ }
+
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Release the given number of particles or the most available.
+ /// Returns a started iterator to iterate over the new particles.
+ /// </summary>
+ public unsafe ParticleIterator Release(int releaseQuantity)
+ {
+ var numToRelease = Math.Min(releaseQuantity, Available);
+
+ var prevCount = Count;
+ Count += numToRelease;
+
+ Tail += numToRelease;
+ if (Tail >= BufferEnd) Tail -= Size + 1;
+
+ return Iterator.Reset(prevCount);
+ }
+
+ public unsafe void Reclaim(int number)
+ {
+ Count -= number;
+
+ Head += number;
+ if (Head >= BufferEnd)
+ Head -= Size + 1;
+ }
+
+ //public void CopyTo(IntPtr destination)
+ //{
+ // memcpy(destination, _nativePointer, ActiveSizeInBytes);
+ //}
+
+ //public void CopyToReverse(IntPtr destination)
+ //{
+ // var offset = 0;
+ // for (var i = ActiveSizeInBytes - Particle.SizeInBytes; i >= 0; i -= Particle.SizeInBytes)
+ // {
+ // memcpy(IntPtr.Add(destination, offset), IntPtr.Add(_nativePointer, i), Particle.SizeInBytes);
+ // offset += Particle.SizeInBytes;
+ // }
+ //}
+
+ //[DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
+ //public static extern void memcpy(IntPtr dest, IntPtr src, int count);
+
+ ~ParticleBuffer()
+ {
+ Dispose();
+ }
+
+ public class ParticleIterator
+ {
+ private readonly ParticleBuffer _buffer;
+
+ private unsafe Particle* _current;
+
+ public int Total;
+
+ public ParticleIterator(ParticleBuffer buffer)
+ {
+ _buffer = buffer;
+ }
+
+ public unsafe bool HasNext => _current != _buffer.Tail;
+
+ public unsafe ParticleIterator Reset()
+ {
+ _current = _buffer.Head;
+ Total = _buffer.Count;
+ return this;
+ }
+
+ internal unsafe ParticleIterator Reset(int offset)
+ {
+ Total = _buffer.Count;
+
+ _current = _buffer.Head + offset;
+ if (_current >= _buffer.BufferEnd)
+ _current -= _buffer.Size + 1;
+
+ return this;
+ }
+
+ public unsafe Particle* Next()
+ {
+ var p = _current;
+ _current++;
+ if (_current == _buffer.BufferEnd)
+ _current = (Particle*) _buffer._nativePointer;
+
+ return p;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs
new file mode 100644
index 0000000..ce6c782
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Serialization;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleEffect : Transform2, IDisposable
+ {
+ public ParticleEffect(string name = null)
+ {
+ Name = name;
+ Emitters = new List<ParticleEmitter>();
+ }
+
+ public void Dispose()
+ {
+ foreach (var emitter in Emitters)
+ emitter.Dispose();
+ }
+
+ public string Name { get; set; }
+ public List<ParticleEmitter> Emitters { get; set; }
+ public int ActiveParticles => Emitters.Sum(t => t.ActiveParticles);
+
+ public void FastForward(Vector2 position, float seconds, float triggerPeriod)
+ {
+ var time = 0f;
+ while (time < seconds)
+ {
+ Update(triggerPeriod);
+ Trigger(position);
+ time += triggerPeriod;
+ }
+ }
+
+ public static ParticleEffect FromFile(ITextureRegionService textureRegionService, string path)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream(textureRegionService, stream);
+ }
+ }
+
+ public static ParticleEffect FromStream(ITextureRegionService textureRegionService, Stream stream)
+ {
+ var options = ParticleJsonSerializerOptionsProvider.GetOptions(textureRegionService);
+ return JsonSerializer.Deserialize<ParticleEffect>(stream, options);
+ }
+
+ public void Update(float elapsedSeconds)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Update(elapsedSeconds, Position);
+ }
+
+ public void Trigger()
+ {
+ Trigger(Position);
+ }
+
+ public void Trigger(Vector2 position, float layerDepth = 0)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Trigger(position, layerDepth);
+ }
+
+ public void Trigger(LineSegment line, float layerDepth = 0)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Trigger(line, layerDepth);
+ }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs
new file mode 100644
index 0000000..1a6273b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Particles.Profiles;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Particles
+{
+ public unsafe class ParticleEmitter : IDisposable
+ {
+ private readonly FastRandom _random = new FastRandom(Math.Abs(Guid.NewGuid().GetHashCode()));
+ private float _totalSeconds;
+
+ [JsonConstructor]
+ public ParticleEmitter(string name, TextureRegion2D textureRegion, int capacity, TimeSpan lifeSpan, Profile profile)
+ {
+ if (profile == null)
+ throw new ArgumentNullException(nameof(profile));
+
+ _lifeSpanSeconds = (float)lifeSpan.TotalSeconds;
+
+ Name = name;
+ TextureRegion = textureRegion;
+ Buffer = new ParticleBuffer(capacity);
+ Offset = Vector2.Zero;
+ Profile = profile;
+ Modifiers = new List<Modifier>();
+ ModifierExecutionStrategy = ParticleModifierExecutionStrategy.Serial;
+ Parameters = new ParticleReleaseParameters();
+ }
+
+ public ParticleEmitter(TextureRegion2D textureRegion, int capacity, TimeSpan lifeSpan, Profile profile)
+ : this(null, textureRegion, capacity, lifeSpan, profile)
+ {
+ }
+
+ public void Dispose()
+ {
+ Buffer.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ ~ParticleEmitter()
+ {
+ Dispose();
+ }
+
+ public string Name { get; set; }
+ public int ActiveParticles => Buffer.Count;
+ public Vector2 Offset { get; set; }
+ public List<Modifier> Modifiers { get; }
+ public Profile Profile { get; set; }
+ public float LayerDepth { get; set; }
+ public ParticleReleaseParameters Parameters { get; set; }
+ public TextureRegion2D TextureRegion { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ParticleModifierExecutionStrategy ModifierExecutionStrategy { get; set; }
+
+ internal ParticleBuffer Buffer;
+
+ public int Capacity
+ {
+ get { return Buffer.Size; }
+ set
+ {
+ var oldBuffer = Buffer;
+ oldBuffer.Dispose();
+ Buffer = new ParticleBuffer(value);
+ }
+ }
+
+ private float _lifeSpanSeconds;
+ public TimeSpan LifeSpan
+ {
+ get { return TimeSpan.FromSeconds(_lifeSpanSeconds); }
+ set { _lifeSpanSeconds = (float) value.TotalSeconds; }
+ }
+
+ private float _nextAutoTrigger;
+
+ private bool _autoTrigger = true;
+ public bool AutoTrigger
+ {
+ get { return _autoTrigger; }
+ set
+ {
+ _autoTrigger = value;
+ _nextAutoTrigger = 0;
+ }
+ }
+
+ private float _autoTriggerFrequency;
+ public float AutoTriggerFrequency
+ {
+ get { return _autoTriggerFrequency; }
+ set
+ {
+ _autoTriggerFrequency = value;
+ _nextAutoTrigger = 0;
+ }
+ }
+
+ private void ReclaimExpiredParticles()
+ {
+ var iterator = Buffer.Iterator;
+ var expired = 0;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ if (_totalSeconds - particle->Inception < _lifeSpanSeconds)
+ break;
+
+ expired++;
+ }
+
+ if (expired != 0)
+ Buffer.Reclaim(expired);
+ }
+
+ public bool Update(float elapsedSeconds, Vector2 position = default(Vector2))
+ {
+ _totalSeconds += elapsedSeconds;
+
+ if (_autoTrigger)
+ {
+ _nextAutoTrigger -= elapsedSeconds;
+
+ if (_nextAutoTrigger <= 0)
+ {
+ Trigger(position, this.LayerDepth);
+ _nextAutoTrigger = _autoTriggerFrequency;
+ }
+ }
+
+ if (Buffer.Count == 0)
+ return false;
+
+ ReclaimExpiredParticles();
+
+ var iterator = Buffer.Iterator;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Age = (_totalSeconds - particle->Inception) / _lifeSpanSeconds;
+ particle->Position = particle->Position + particle->Velocity * elapsedSeconds;
+ }
+
+ ModifierExecutionStrategy.ExecuteModifiers(Modifiers, elapsedSeconds, iterator);
+ return true;
+ }
+
+ public void Trigger(Vector2 position, float layerDepth = 0)
+ {
+ var numToRelease = _random.Next(Parameters.Quantity);
+ Release(position + Offset, numToRelease, layerDepth);
+ }
+
+ public void Trigger(LineSegment line, float layerDepth = 0)
+ {
+ var numToRelease = _random.Next(Parameters.Quantity);
+ var lineVector = line.ToVector();
+
+ for (var i = 0; i < numToRelease; i++)
+ {
+ var offset = lineVector * _random.NextSingle();
+ Release(line.Origin + offset, 1, layerDepth);
+ }
+ }
+
+ private void Release(Vector2 position, int numToRelease, float layerDepth)
+ {
+ var iterator = Buffer.Release(numToRelease);
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ Vector2 heading;
+ Profile.GetOffsetAndHeading(out particle->Position, out heading);
+
+ particle->Age = 0f;
+ particle->Inception = _totalSeconds;
+ particle->Position += position;
+ particle->TriggerPos = position;
+
+ var speed = _random.NextSingle(Parameters.Speed);
+
+ particle->Velocity = heading * speed;
+
+ _random.NextColor(out particle->Color, Parameters.Color);
+
+ particle->Opacity = _random.NextSingle(Parameters.Opacity);
+
+ if(Parameters.MaintainAspectRatioOnScale)
+ {
+ var scale = _random.NextSingle(Parameters.Scale);
+ particle->Scale = new Vector2(scale, scale);
+ }
+ else
+ {
+ particle->Scale = new Vector2(_random.NextSingle(Parameters.ScaleX), _random.NextSingle(Parameters.ScaleY));
+ }
+
+ particle->Rotation = _random.NextSingle(Parameters.Rotation);
+ particle->Mass = _random.NextSingle(Parameters.Mass);
+ particle->LayerDepth = layerDepth;
+ }
+ }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs
new file mode 100644
index 0000000..7566e8d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs
@@ -0,0 +1,49 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Particles
+{
+ public static class ParticleExtensions
+ {
+ public static void Draw(this SpriteBatch spriteBatch, ParticleEffect effect)
+ {
+ for (var i = 0; i < effect.Emitters.Count; i++)
+ UnsafeDraw(spriteBatch, effect.Emitters[i]);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, ParticleEmitter emitter)
+ {
+ UnsafeDraw(spriteBatch, emitter);
+ }
+
+ private static unsafe void UnsafeDraw(SpriteBatch spriteBatch, ParticleEmitter emitter)
+ {
+ if(emitter.TextureRegion == null)
+ return;
+
+ var textureRegion = emitter.TextureRegion;
+ var origin = new Vector2(textureRegion.Width/2f, textureRegion.Height/2f);
+ var iterator = emitter.Buffer.Iterator;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var color = particle->Color.ToRgb();
+
+ if (spriteBatch.GraphicsDevice.BlendState == BlendState.AlphaBlend)
+ color *= particle->Opacity;
+ else
+ color.A = (byte) (particle->Opacity*255);
+
+ var position = new Vector2(particle->Position.X, particle->Position.Y);
+ var scale = particle->Scale;
+ var particleColor = new Color(color, particle->Opacity);
+ var rotation = particle->Rotation;
+ var layerDepth = particle->LayerDepth;
+
+ spriteBatch.Draw(textureRegion, position, particleColor, rotation, origin, scale, SpriteEffects.None, layerDepth);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs
new file mode 100644
index 0000000..20a4d51
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using MonoGame.Extended.Particles.Modifiers;
+
+namespace MonoGame.Extended.Particles
+{
+ using TPL = System.Threading.Tasks;
+
+ public abstract class ParticleModifierExecutionStrategy
+ {
+ public static readonly ParticleModifierExecutionStrategy Serial = new SerialModifierExecutionStrategy();
+ public static readonly ParticleModifierExecutionStrategy Parallel = new ParallelModifierExecutionStrategy();
+
+ internal abstract void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator);
+
+ internal class SerialModifierExecutionStrategy : ParticleModifierExecutionStrategy
+ {
+ internal override void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ for (var i = 0; i < modifiers.Count; i++)
+ modifiers[i].Update(elapsedSeconds, iterator.Reset());
+ }
+
+ public override string ToString()
+ {
+ return nameof(Serial);
+ }
+ }
+
+ internal class ParallelModifierExecutionStrategy : ParticleModifierExecutionStrategy
+ {
+ internal override void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ TPL.Parallel.ForEach(modifiers, modifier => modifier.Update(elapsedSeconds, iterator.Reset()));
+ }
+
+ public override string ToString()
+ {
+ return nameof(Parallel);
+ }
+ }
+
+ public static ParticleModifierExecutionStrategy Parse(string value)
+ {
+ if (string.Equals(nameof(Parallel), value, StringComparison.OrdinalIgnoreCase))
+ return Parallel;
+
+ if (string.Equals(nameof(Serial), value, StringComparison.OrdinalIgnoreCase))
+ return Serial;
+
+ throw new InvalidOperationException($"Unknown particle modifier execution strategy '{value}'");
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs
new file mode 100644
index 0000000..e19fbc2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs
@@ -0,0 +1,34 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleReleaseParameters
+ {
+ public ParticleReleaseParameters()
+ {
+ Quantity = 1;
+ Speed = new Range<float>(-1f, 1f);
+ Color = new Range<HslColor>(Microsoft.Xna.Framework.Color.White.ToHsl(), Microsoft.Xna.Framework.Color.White.ToHsl());
+ Opacity = new Range<float>(0f, 1f);
+ Scale = new Range<float>(1f, 1f);
+ Rotation = new Range<float>(-MathHelper.Pi, MathHelper.Pi);
+ Mass = 1f;
+ MaintainAspectRatioOnScale = true;
+ ScaleX = new Range<float>(1f, 1f);
+ ScaleY = new Range<float>(1f, 1f);
+ }
+
+ public Range<int> Quantity { get; set; }
+ public Range<float> Speed { get; set; }
+ public Range<HslColor> Color { get; set; }
+ public Range<float> Opacity { get; set; }
+ public Range<float> Scale { get; set; }
+ public Range<float> Rotation { get; set; }
+ public Range<float> Mass { get; set; }
+ public bool MaintainAspectRatioOnScale { get; set; }
+ public Range<float> ScaleX { get; set; }
+ public Range<float> ScaleY { get; set; }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs
new file mode 100644
index 0000000..e363713
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxFillProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f),
+ Random.NextSingle(Height*-0.5f, Height*0.5f));
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs
new file mode 100644
index 0000000..7d31984
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs
@@ -0,0 +1,31 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ switch (Random.Next(3))
+ {
+ case 0: // Left
+ offset = new Vector2(Width*-0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ break;
+ case 1: // Top
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*-0.5f);
+ break;
+ case 2: // Right
+ offset = new Vector2(Width*0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ break;
+ default: // Bottom
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*0.5f);
+ break;
+ }
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs
new file mode 100644
index 0000000..9f766dc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxUniformProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var value = Random.Next((int) (2*Width + 2*Height));
+
+ if (value < Width) // Top
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*-0.5f);
+ else
+ {
+ if (value < 2*Width) // Bottom
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*0.5f);
+ else
+ {
+ if (value < 2*Width + Height) // Left
+ offset = new Vector2(Width*-0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ else // Right
+ offset = new Vector2(Width*0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ }
+ }
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs
new file mode 100644
index 0000000..ef7f11e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs
@@ -0,0 +1,24 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class CircleProfile : Profile
+ {
+ public float Radius { get; set; }
+ public CircleRadiation Radiate { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var dist = Random.NextSingle(0f, Radius);
+
+ Random.NextUnitVector(out heading);
+
+ offset = Radiate == CircleRadiation.In
+ ? new Vector2(-heading.X*dist, -heading.Y*dist)
+ : new Vector2(heading.X*dist, heading.Y*dist);
+
+ if (Radiate == CircleRadiation.None)
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs
new file mode 100644
index 0000000..bea584e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs
@@ -0,0 +1,17 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class LineProfile : Profile
+ {
+ public Vector2 Axis { get; set; }
+ public float Length { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var vect = Axis*Random.NextSingle(Length*-0.5f, Length*0.5f);
+ offset = new Vector2(vect.X, vect.Y);
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs
new file mode 100644
index 0000000..04456d3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class PointProfile : Profile
+ {
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ offset = Vector2.Zero;
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs
new file mode 100644
index 0000000..bfde371
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs
@@ -0,0 +1,68 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public abstract class Profile
+ {
+ public enum CircleRadiation
+ {
+ None,
+ In,
+ Out
+ }
+
+ protected FastRandom Random { get; } = new FastRandom();
+
+ public abstract void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading);
+
+ public object Clone()
+ {
+ return MemberwiseClone();
+ }
+
+ public static Profile Point()
+ {
+ return new PointProfile();
+ }
+
+ public static Profile Line(Vector2 axis, float length)
+ {
+ return new LineProfile {Axis = axis, Length = length};
+ }
+
+ public static Profile Ring(float radius, CircleRadiation radiate)
+ {
+ return new RingProfile {Radius = radius, Radiate = radiate};
+ }
+
+ public static Profile Box(float width, float height)
+ {
+ return new BoxProfile {Width = width, Height = height};
+ }
+
+ public static Profile BoxFill(float width, float height)
+ {
+ return new BoxFillProfile {Width = width, Height = height};
+ }
+
+ public static Profile BoxUniform(float width, float height)
+ {
+ return new BoxUniformProfile {Width = width, Height = height};
+ }
+
+ public static Profile Circle(float radius, CircleRadiation radiate)
+ {
+ return new CircleProfile {Radius = radius, Radiate = radiate};
+ }
+
+ public static Profile Spray(Vector2 direction, float spread)
+ {
+ return new SprayProfile {Direction = direction, Spread = spread};
+ }
+
+ public override string ToString()
+ {
+ return GetType().ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs
new file mode 100644
index 0000000..d5ac2f8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs
@@ -0,0 +1,32 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class RingProfile : Profile
+ {
+ public float Radius { get; set; }
+ public CircleRadiation Radiate { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ Random.NextUnitVector(out heading);
+
+ switch (Radiate)
+ {
+ case CircleRadiation.In:
+ offset = new Vector2(-heading.X*Radius, -heading.Y*Radius);
+ break;
+ case CircleRadiation.Out:
+ offset = new Vector2(heading.X*Radius, heading.Y*Radius);
+ break;
+ case CircleRadiation.None:
+ offset = new Vector2(heading.X*Radius, heading.Y*Radius);
+ Random.NextUnitVector(out heading);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException($"{Radiate} is not supported");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs
new file mode 100644
index 0000000..6259210
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs
@@ -0,0 +1,20 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class SprayProfile : Profile
+ {
+ public Vector2 Direction { get; set; }
+ public float Spread { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var angle = (float) Math.Atan2(Direction.Y, Direction.X);
+
+ angle = Random.NextSingle(angle - Spread/2f, angle + Spread/2f);
+ offset = Vector2.Zero;
+ heading = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs
new file mode 100644
index 0000000..21bd7fc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class InterpolatorJsonConverter : BaseTypeJsonConverter<Interpolator>
+ {
+ public InterpolatorJsonConverter()
+ : base(GetSupportedTypes(), "Interpolator")
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Interpolator)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => typeof(Interpolator).GetTypeInfo().IsAssignableFrom(type) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs
new file mode 100644
index 0000000..8fc894b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+/// <summary>
+/// Converts a <see cref="ParticleModifierExecutionStrategy"/> value to or from JSON.
+/// </summary>
+public class ModifierExecutionStrategyJsonConverter : JsonConverter<ParticleModifierExecutionStrategy>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) =>
+ typeToConvert == typeof(ParticleModifierExecutionStrategy) ||
+ typeToConvert.GetTypeInfo().BaseType == typeof(ParticleModifierExecutionStrategy);
+
+ /// <inheritdoc />
+ public override ParticleModifierExecutionStrategy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = JsonSerializer.Deserialize<string>(ref reader, options);
+ return ParticleModifierExecutionStrategy.Parse(value);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, ParticleModifierExecutionStrategy value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue(value.ToString());
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs
new file mode 100644
index 0000000..5985e40
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class ModifierJsonConverter : BaseTypeJsonConverter<Modifier>
+ {
+ public ModifierJsonConverter()
+ : base(GetSupportedTypes(), "Modifier")
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Modifier)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => typeof(Modifier).GetTypeInfo().IsAssignableFrom(type) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs
new file mode 100644
index 0000000..489b3ee
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs
@@ -0,0 +1,34 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+public static class ParticleJsonSerializerOptionsProvider
+{
+ public static JsonSerializerOptions GetOptions(ITextureRegionService textureRegionService)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ options.Converters.Add(new Vector2JsonConverter());
+ options.Converters.Add(new Size2JsonConverter());
+ options.Converters.Add(new ColorJsonConverter());
+ options.Converters.Add(new TextureRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new ProfileJsonConverter());
+ options.Converters.Add(new ModifierJsonConverter());
+ options.Converters.Add(new InterpolatorJsonConverter());
+ options.Converters.Add(new TimeSpanJsonConverter());
+ options.Converters.Add(new RangeJsonConverter<int>());
+ options.Converters.Add(new RangeJsonConverter<float>());
+ options.Converters.Add(new RangeJsonConverter<HslColor>());
+ options.Converters.Add(new HslColorJsonConverter());
+ options.Converters.Add(new ModifierExecutionStrategyJsonConverter());
+
+ return options;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs
new file mode 100644
index 0000000..8d98079
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Profiles;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class ProfileJsonConverter : BaseTypeJsonConverter<Profile>
+ {
+ public ProfileJsonConverter()
+ : base(GetSupportedTypes(), nameof(Profile))
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Profile)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => type.IsSubclassOf(typeof(Profile)) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs
new file mode 100644
index 0000000..0155b08
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+public class TimeSpanJsonConverter : JsonConverter<TimeSpan>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TimeSpan);
+
+ /// <inheritdoc />
+ public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ double seconds = reader.GetDouble();
+ return TimeSpan.FromSeconds(seconds);
+ }
+
+ return TimeSpan.Zero;
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteNumberValue(value.TotalSeconds);
+ }
+}