diff --git a/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj b/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj
index 9797b4dd..d2292011 100644
--- a/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj
+++ b/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj
@@ -13,9 +13,7 @@
-
+
diff --git a/source/MonoGame.Extended/Math/FastRandom.cs b/source/MonoGame.Extended/Math/FastRandom.cs
index dd0184c0..1d6f8628 100644
--- a/source/MonoGame.Extended/Math/FastRandom.cs
+++ b/source/MonoGame.Extended/Math/FastRandom.cs
@@ -11,7 +11,7 @@ namespace MonoGame.Extended
///
///
/// This implementation uses the same constants as Microsoft Visual C++ rand() function:
- ///
+ ///
/// a=214013
/// c=2531011
/// m=2^31
@@ -183,6 +183,15 @@ namespace MonoGame.Extended
_impl.NextUnitVector(out vector);
}
+ ///
+ /// Gets a random unit vector.
+ ///
+ /// A pointer to the Vector2 where the random unit vector will be stored.
+ public unsafe void NextUnitVector(Vector2* vector)
+ {
+ _impl.NextUnitVector(vector);
+ }
+
#region IFastRandomImplementation
private interface IFastRandomImpl
{
@@ -196,6 +205,7 @@ namespace MonoGame.Extended
float NextSingle(Range range);
float NextAngle();
void NextUnitVector(out Vector2 vector);
+ unsafe void NextUnitVector(Vector2* vector);
}
#endregion
@@ -270,6 +280,13 @@ namespace MonoGame.Extended
vector.X = MathF.Cos(angle);
vector.Y = MathF.Sin(angle);
}
+
+ public unsafe void NextUnitVector(Vector2* vector)
+ {
+ float angle = NextAngle();
+ vector->X = MathF.Cos(angle);
+ vector->Y = MathF.Sin(angle);
+ }
}
#endregion
@@ -336,6 +353,11 @@ namespace MonoGame.Extended
{
LocalRandom.NextUnitVector(out vector);
}
+
+ public unsafe void NextUnitVector(Vector2* vector)
+ {
+ LocalRandom.NextUnitVector(vector);
+ }
}
#endregion
}
diff --git a/source/MonoGame.Extended/Particles/Data/Particle.cs b/source/MonoGame.Extended/Particles/Data/Particle.cs
new file mode 100644
index 00000000..9f650c05
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Data/Particle.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+
+namespace MonoGame.Extended.Particles.Data;
+
+///
+/// Represents an individual particle within the particle system.
+///
+///
+/// The struct uses sequential layout with tight packing to optimize memory usage and performance.
+/// The fixed arrays are used to store positional, velocity, and color data efficiently in unmanaged memory.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public unsafe struct Particle
+{
+ ///
+ /// The time (in seconds) when this particle was created.
+ ///
+ public float Inception;
+
+ ///
+ /// The current age (in seconds) of this particle.
+ ///
+ public float Age;
+
+ ///
+ /// The current position of this particle in 2D space [X, Y].
+ ///
+ public fixed float Position[2];
+
+ ///
+ /// The current velocity vector of this particle [X, Y].
+ ///
+ public fixed float Velocity[2];
+
+ ///
+ /// The color of this particle in RGB format [R, G, B].
+ ///
+ public fixed float Color[3];
+
+ ///
+ /// The scale factor applied to this particle's visual representation.
+ ///
+ public float Scale;
+
+ ///
+ /// The position where this particle was triggered or emitted from [X, Y].
+ ///
+ public fixed float TriggeredPos[2];
+
+ ///
+ /// The opacity (alpha) value of this particle, ranging from 0.0 (transparent) to 1.0 (opaque).
+ ///
+ public float Opacity;
+
+ ///
+ /// The rotation of this particle in radians.
+ ///
+ public float Rotation;
+
+ ///
+ /// The mass of this particle used during physics calculations.
+ ///
+ public float Mass;
+
+ ///
+ /// The depth at which this particle is rendered, used for layering particles.
+ ///
+ ///
+ /// Values range from 0.0 (front) to 1.0 (back).
+ ///
+ public float LayerDepth;
+
+ ///
+ /// The size of the struct in bytes.
+ ///
+ ///
+ /// Used for memory allocations and buffer operations.
+ ///
+ public static readonly int SizeInBytes = Marshal.SizeOf();
+}
diff --git a/source/MonoGame.Extended/Particles/Data/ParticleColorParameter.cs b/source/MonoGame.Extended/Particles/Data/ParticleColorParameter.cs
new file mode 100644
index 00000000..37e5dc85
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Data/ParticleColorParameter.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Data;
+
+///
+/// Represents a color parameter for particle properties that can be either a constant color value
+/// or a randomly generated color within a specified range.
+///
+///
+/// This struct uses to represent color values, the HSL color space.
+///
+public struct ParticleColorParameter : IEquatable
+{
+ ///
+ /// The that determines whether this parameter uses a constant value or a randomly
+ /// generated value.
+ ///
+ public ParticleValueKind Kind;
+
+ ///
+ /// The constant color value when is set to .
+ ///
+ public Vector3 Constant;
+
+ ///
+ /// The minimum color values of the range when is set to .
+ ///
+ public Vector3 RandomMin;
+
+ ///
+ /// The maximum color values of the range when is set to .
+ ///
+ public Vector3 RandomMax;
+
+ ///
+ /// Gets the current color value of this parameter based on its .
+ ///
+ ///
+ /// If is , returns .
+ /// If is , returns a random color where each component
+ /// is a random value between the corresponding components of and .
+ /// The vector components represent HSL.
+ ///
+ public Vector3 Value
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant;
+ }
+ else
+ {
+ Vector3 hsl;
+ hsl.X = FastRandom.Shared.NextSingle(RandomMin.X, RandomMax.X);
+ hsl.Y = FastRandom.Shared.NextSingle(RandomMin.Y, RandomMax.Y);
+ hsl.Z = FastRandom.Shared.NextSingle(RandomMin.Z, RandomMax.Z);
+ return hsl;
+ }
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the struct with a constant color value.
+ ///
+ /// The constant color value for this parameter.
+ public ParticleColorParameter(Vector3 value)
+ {
+ Kind = ParticleValueKind.Constant;
+
+ Constant = value;
+
+ RandomMin = default;
+ RandomMax = default;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with a random color range.
+ ///
+ /// The minimum color values of the random range.
+ /// The maximum color values of the random range.
+ public ParticleColorParameter(Vector3 rangeStart, Vector3 rangeEnd)
+ {
+ Kind = ParticleValueKind.Random;
+ Constant = default;
+
+ RandomMin = rangeStart;
+ RandomMax = rangeEnd;
+ }
+
+ ///
+ /// Determines whether the specified object is equal to the current parameter.
+ ///
+ /// The object to compare with the current parameter.
+ ///
+ /// if the specified object is equal tot he current parameter;
+ /// otherwise, .
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object obj)
+ {
+ return obj is ParticleColorParameter other &&
+ Equals(other);
+ }
+
+ ///
+ /// Determines whether the specified parameter is equal to the current parameter.
+ ///
+ /// The parameter to compare with the current parameter.
+ ///
+ /// if the specified parameter is equal to the current parameter;
+ /// otherwise, .
+ ///
+ ///
+ /// When is only the values are
+ /// compared.
+ /// When is , both and
+ /// values are compared.
+ ///
+ public readonly bool Equals(ParticleColorParameter other)
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.Equals(other.Constant);
+ }
+
+ return RandomMin.Equals(other.RandomMin) && RandomMax.Equals(other.RandomMax);
+ }
+
+ ///
+ /// Returns the hash code for this parameter.
+ ///
+ /// A 32-bit signed integer that is the hash code for this instance.
+ ///
+ /// When is , returns the hash of .
+ /// When is returns the combined has of
+ /// and .
+ ///
+ public override readonly int GetHashCode()
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.GetHashCode();
+ }
+
+ return HashCode.Combine(RandomMin, RandomMax);
+ }
+
+ ///
+ /// Determines whether two parameters are equal.
+ ///
+ /// The first parameter to compare.
+ /// The second parameter to compare.
+ ///
+ /// if the parameters are equal; otherwise, .
+ ///
+ public static bool operator ==(ParticleColorParameter lhs, ParticleColorParameter rhs)
+ {
+ return lhs.Equals(rhs);
+ }
+
+ ///
+ /// Determines whether two parameters are not equal.
+ ///
+ /// The first parameter to compare.
+ /// The second parameter to compare.
+ ///
+ /// if the parameters are not equal; otherwise, .
+ ///
+ public static bool operator !=(ParticleColorParameter lhs, ParticleColorParameter rhs)
+ {
+ return !lhs.Equals(rhs);
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Data/ParticleFloatParameter.cs b/source/MonoGame.Extended/Particles/Data/ParticleFloatParameter.cs
new file mode 100644
index 00000000..71245dd9
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Data/ParticleFloatParameter.cs
@@ -0,0 +1,189 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+
+namespace MonoGame.Extended.Particles.Data;
+
+///
+/// Represents an floating-point number parameter for particle properties that can be either a constant value or a
+/// randomly generated value within a specified range.
+///
+public struct ParticleFloatParameter : IEquatable
+{
+ ///
+ /// The that determines whether this parameter uses a constant value or a randomly
+ /// generated value.
+ ///
+ public ParticleValueKind Kind;
+
+ ///
+ /// The constant value when is .
+ ///
+ public float Constant;
+
+ ///
+ /// The minimum value of the range when is .
+ ///
+ public float RandomMin;
+
+ ///
+ /// The maximum value of the range when is .
+ ///
+ public float RandomMax;
+
+ ///
+ /// Gets the current value of this parameter based on its
+ ///
+ ///
+ /// If is , returns .
+ /// If is , returns a random value between
+ /// and .
+ ///
+ public float Value
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant;
+ }
+
+ return FastRandom.Shared.NextSingle(RandomMin, RandomMax);
+ }
+ }
+
+ ///
+ /// Initializes a new value with a constant value.
+ ///
+ /// The constant value for this parameter.
+ public ParticleFloatParameter(float value)
+ {
+ Kind = ParticleValueKind.Constant;
+ Constant = value;
+ RandomMin = default;
+ RandomMax = default;
+ }
+
+ ///
+ /// Initializes a new value with a random range.
+ ///
+ /// The minimum value of the random range.
+ /// The maximum value of the random range.
+ public ParticleFloatParameter(float rangeStart, float rangeEnd)
+ {
+ Kind = ParticleValueKind.Random;
+ Constant = default;
+ RandomMin = rangeStart;
+ RandomMax = rangeEnd;
+ }
+
+ ///
+ /// Determines whether the specified object is equal to the current parameter.
+ ///
+ /// The object to compare with the current parameter.
+ ///
+ /// if the specified object is equal tot he current parameter;
+ /// otherwise, .
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object obj)
+ {
+ return obj is ParticleFloatParameter other &&
+ Equals(other);
+ }
+
+ ///
+ /// Determines whether the specified parameter is equal to the current parameter.
+ ///
+ /// The parameter to compare with the current parameter.
+ ///
+ /// if the specified parameter is equal to the current parameter;
+ /// otherwise, .
+ ///
+ ///
+ /// When is only the values are
+ /// compared.
+ /// When is , both and
+ /// values are compared.
+ ///
+ public readonly bool Equals(ParticleFloatParameter other)
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.Equals(other.Constant);
+ }
+
+ return RandomMin.Equals(other.RandomMin) &&
+ RandomMax.Equals(other.RandomMax);
+ }
+
+ ///
+ /// Returns the hash code for this parameter.
+ ///
+ /// A 32-bit signed integer that is the hash code for this instance.
+ ///
+ /// When is , returns the hash of .
+ /// When is returns the combined has of
+ /// and .
+ ///
+ public override readonly int GetHashCode()
+ {
+ base.GetHashCode();
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.GetHashCode();
+ }
+
+ return HashCode.Combine(RandomMin, RandomMax);
+ }
+
+ ///
+ /// Returns a string representation of this parameter.
+ ///
+ ///
+ /// When is , returns the string representation of
+ /// .
+ /// When is , returns a string in the format
+ /// "MinValue, MaxValue".
+ ///
+ public override readonly string ToString()
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.ToString(CultureInfo.InvariantCulture);
+ }
+
+ return string.Format(NumberFormatInfo.InvariantInfo, "{0}{1}{2}", RandomMin, NumberFormatInfo.InvariantInfo.NumberGroupSeparator, RandomMax);
+ }
+
+ ///
+ /// Determines whether two parameters are equal.
+ ///
+ /// The first parameter to compare.
+ /// The second parameter to compare.
+ ///
+ /// if the parameters are equal; otherwise, .
+ ///
+ public static bool operator ==(ParticleFloatParameter lhs, ParticleFloatParameter rhs)
+ {
+ return lhs.Equals(rhs);
+ }
+
+ ///
+ /// Determines whether two parameters are not equal.
+ ///
+ /// The first parameter to compare.
+ /// The second parameter to compare.
+ ///
+ /// if the parameters are not equal; otherwise, .
+ ///
+ public static bool operator !=(ParticleFloatParameter lhs, ParticleFloatParameter rhs)
+ {
+ return !lhs.Equals(rhs);
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Data/ParticleInt32Parameter.cs b/source/MonoGame.Extended/Particles/Data/ParticleInt32Parameter.cs
new file mode 100644
index 00000000..9e530c59
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Data/ParticleInt32Parameter.cs
@@ -0,0 +1,189 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+
+namespace MonoGame.Extended.Particles.Data;
+
+///
+/// Represents an integer parameter for particle properties that can be either a constant value or a randomly generated
+/// value within a specified range.
+///
+public struct ParticleInt32Parameter : IEquatable
+{
+ ///
+ /// The that determines whether this parameter uses a constant value or a randomly
+ /// generated value.
+ ///
+ public ParticleValueKind Kind;
+
+ ///
+ /// The constant value when is .
+ ///
+ public int Constant;
+
+ ///
+ /// The minimum value of the range when is .
+ ///
+ public int RandomMin;
+
+ ///
+ /// The maximum value of the range when is .
+ ///
+ public int RandomMax;
+
+ ///
+ /// Gets the current value of this parameter based on its
+ ///
+ ///
+ /// If is , returns .
+ /// If is , returns a random value between
+ /// and .
+ ///
+ public int Value
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant;
+ }
+
+ return FastRandom.Shared.Next(RandomMin, RandomMax);
+ }
+ }
+
+ ///
+ /// Initializes a new value with a constant value.
+ ///
+ /// The constant value for this parameter.
+ public ParticleInt32Parameter(int value)
+ {
+ Kind = ParticleValueKind.Constant;
+ Constant = value;
+ RandomMin = default;
+ RandomMax = default;
+ }
+
+ ///
+ /// Initializes a new value with a random range.
+ ///
+ /// The minimum value of the random range.
+ /// The maximum value of the random range.
+ public ParticleInt32Parameter(int rangeStart, int rangeEnd)
+ {
+ Kind = ParticleValueKind.Random;
+ Constant = default;
+ RandomMin = rangeStart;
+ RandomMax = rangeEnd;
+ }
+
+ ///
+ /// Determines whether the specified object is equal to the current parameter.
+ ///
+ /// The object to compare with the current parameter.
+ ///
+ /// if the specified object is equal tot he current parameter;
+ /// otherwise, .
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object obj)
+ {
+ return obj is ParticleInt32Parameter other &&
+ Equals(other);
+ }
+
+ ///
+ /// Determines whether the specified parameter is equal to the current parameter.
+ ///
+ /// The parameter to compare with the current parameter.
+ ///
+ /// if the specified parameter is equal to the current parameter;
+ /// otherwise, .
+ ///
+ ///
+ /// When is only the values are
+ /// compared.
+ /// When is , both and
+ /// values are compared.
+ ///
+ public readonly bool Equals(ParticleInt32Parameter other)
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.Equals(other.Constant);
+ }
+
+ return RandomMin.Equals(other.RandomMin) &&
+ RandomMax.Equals(other.RandomMax);
+ }
+
+ ///
+ /// Returns the hash code for this parameter.
+ ///
+ /// A 32-bit signed integer that is the hash code for this instance.
+ ///
+ /// When is , returns the hash of .
+ /// When is returns the combined has of
+ /// and .
+ ///
+ public override readonly int GetHashCode()
+ {
+ base.GetHashCode();
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.GetHashCode();
+ }
+
+ return HashCode.Combine(RandomMin, RandomMax);
+ }
+
+ ///
+ /// Returns a string representation of this parameter.
+ ///
+ ///
+ /// When is , returns the string representation of
+ /// .
+ /// When is , returns a string in the format
+ /// "MinValue, MaxValue".
+ ///
+ public override readonly string ToString()
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.ToString(CultureInfo.InvariantCulture);
+ }
+
+ return string.Format(NumberFormatInfo.InvariantInfo, "{0}{1}{2}", RandomMin, NumberFormatInfo.InvariantInfo.NumberGroupSeparator, RandomMax);
+ }
+
+ ///
+ /// Determines whether two parameters are equal.
+ ///
+ /// The first parameter to compare.
+ /// The second parameter to compare.
+ ///
+ /// if the parameters are equal; otherwise, .
+ ///
+ public static bool operator ==(ParticleInt32Parameter lhs, ParticleInt32Parameter rhs)
+ {
+ return lhs.Equals(rhs);
+ }
+
+ ///
+ /// Determines whether two parameters are not equal.
+ ///
+ /// The first parameter to compare.
+ /// The second parameter to compare.
+ ///
+ /// if the parameters are not equal; otherwise, .
+ ///
+ public static bool operator !=(ParticleInt32Parameter lhs, ParticleInt32Parameter rhs)
+ {
+ return !lhs.Equals(rhs);
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Data/ParticleReleaseParameters.cs b/source/MonoGame.Extended/Particles/Data/ParticleReleaseParameters.cs
new file mode 100644
index 00000000..0917bd80
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Data/ParticleReleaseParameters.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Data;
+
+///
+/// Defines the parameters used when releasing particles from an emitter.
+///
+///
+/// This class encapsulates all the configurable properties that control how particles are initialized when they are
+/// created by the particle system. Each property can be set as either a constant value or a random range.
+///
+public class ParticleReleaseParameters
+{
+ ///
+ /// Gets or sets the number of particles to release in a single emission.
+ ///
+ ///
+ /// Defaults to a random value between 5 and 100 particles per emission.
+ ///
+ public ParticleInt32Parameter Quantity = new ParticleInt32Parameter(5, 100);
+
+ ///
+ /// Gets or sets the initial speed of particles when released.
+ ///
+ ///
+ /// Defaults to a random value between 50.0 and 100.0 units per second.
+ ///
+ public ParticleFloatParameter Speed = new ParticleFloatParameter(50.0f, 100.0f);
+
+ ///
+ /// Gets or sets the initial color of particles when released.
+ ///
+ ///
+ /// Defaults to white (1.0f, 1.0f, 1.0f).
+ ///
+ public ParticleColorParameter Color = new ParticleColorParameter(new Vector3(1.0f, 1.0f, 1.0f));
+
+ ///
+ /// Gets or sets the initial opacity of particles when released.
+ ///
+ ///
+ /// Defaults to a random value between 0.0 (transparent) and 1.0 (opaque).
+ ///
+ public ParticleFloatParameter Opacity = new ParticleFloatParameter(0.0f, 1.0f);
+
+ ///
+ /// Gets or sets the initial scale of particles when released.
+ ///
+ ///
+ /// Defaults to a random value between 0.0 (half scale) and 1.0 (full scale)
+ ///
+ public ParticleFloatParameter Scale = new ParticleFloatParameter(0.5f, 1.0f);
+
+ ///
+ /// Gets or sets the initial rotation (in radians) of particles when released.
+ ///
+ ///
+ /// Defaults to a random value between -π and π radians (a full 360° range).
+ ///
+ public ParticleFloatParameter Rotation = new ParticleFloatParameter(-MathF.PI, MathF.PI);
+
+ ///
+ /// Gets or sets the mass of particles when released.
+ ///
+ ///
+ /// Defaults to a constant value of 1.0.
+ ///
+ public ParticleFloatParameter Mass = new ParticleFloatParameter(1.0f);
+
+
+ ///
+ /// Initializes a new instance of the class with default values.
+ ///
+ ///
+ /// Default values for properties:
+ ///
+ ///
+ ///
+ /// Property
+ /// Default Value
+ ///
+ /// -
+ /// Quantity
+ /// Random: 5-100 particles
+ ///
+ /// -
+ /// Speed
+ /// Random: 50.0-100.0 units/second
+ ///
+ /// -
+ /// Color
+ /// Constant: White (1.0, 1.0, 1.0)
+ ///
+ /// -
+ /// Opacity
+ /// Random: 0.0-1.0
+ ///
+ /// -
+ /// Scale
+ /// Random: 0.5-1.0
+ ///
+ /// -
+ /// Rotation
+ /// Random: -π to π radians
+ ///
+ /// -
+ /// Mass
+ /// Constant: 1.0
+ ///
+ ///
+ ///
+ ///
+ public ParticleReleaseParameters() { }
+}
diff --git a/source/MonoGame.Extended/Particles/Data/ParticleValueKind.cs b/source/MonoGame.Extended/Particles/Data/ParticleValueKind.cs
new file mode 100644
index 00000000..41f64c92
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Data/ParticleValueKind.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+namespace MonoGame.Extended.Particles.Data;
+
+///
+/// Defines how particle property values are determined during particle creation and simulation.
+///
+///
+/// This enum is used throughout the particle system to specify whether values should be constant or randomly generated
+/// within a specified range.
+///
+public enum ParticleValueKind
+{
+ ///
+ /// Indicates that a particle property should maintain a constant value.
+ ///
+ ///
+ /// WHen a particle property uses this kind, all particles will have the same value for that property, which remains
+ /// unchanged unless explicitly modified.
+ ///
+ Constant,
+
+ ///
+ /// Indicates that a particle property should be randomly generated within a specified range.
+ ///
+ ///
+ /// When a particle property uses this kind, each particle will receive a unique random value within the defined
+ /// minimum and maximum bounds when the particle is created.
+ ///
+ Random
+}
diff --git a/source/MonoGame.Extended/Particles/Data/ParticleVector2Parameter.cs b/source/MonoGame.Extended/Particles/Data/ParticleVector2Parameter.cs
new file mode 100644
index 00000000..6877fe3f
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Data/ParticleVector2Parameter.cs
@@ -0,0 +1,193 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Data;
+
+///
+/// Represents an vector parameter for particle properties that can be either a constant value or a
+/// randomly generated value within a specified range.
+///
+public struct ParticleVector2Parameter : IEquatable
+{
+ ///
+ /// The that determines whether this parameter uses a constant value or a randomly
+ /// generated value.
+ ///
+ public ParticleValueKind Kind;
+
+ ///
+ /// The constant value when is .
+ ///
+ public Vector2 Constant;
+
+ ///
+ /// The minimum value of the range when is .
+ ///
+ public Vector2 RandomMin;
+
+ ///
+ /// The maximum value of the range when is .
+ ///
+ public Vector2 RandomMax;
+
+ ///
+ /// Gets the current value of this parameter based on its
+ ///
+ ///
+ /// If is , returns .
+ /// If is , returns a random value between
+ /// and .
+ ///
+ public Vector2 Value
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant;
+ }
+
+ Vector2 v;
+ v.X = FastRandom.Shared.NextSingle(RandomMin.X, RandomMax.X);
+ v.Y = FastRandom.Shared.NextSingle(RandomMin.Y, RandomMax.Y);
+ return v;
+ }
+ }
+
+ ///
+ /// Initializes a new value with a constant value.
+ ///
+ /// The constant value for this parameter.
+ public ParticleVector2Parameter(Vector2 value)
+ {
+ Kind = ParticleValueKind.Constant;
+ Constant = value;
+ RandomMin = default;
+ RandomMax = default;
+ }
+
+ ///
+ /// Initializes a new value with a random range.
+ ///
+ /// The minimum value of the random range.
+ /// The maximum value of the random range.
+ public ParticleVector2Parameter(Vector2 rangeStart, Vector2 rangeEnd)
+ {
+ Kind = ParticleValueKind.Random;
+ Constant = default;
+ RandomMin = rangeStart;
+ RandomMax = rangeEnd;
+ }
+
+ ///
+ /// Determines whether the specified object is equal to the current parameter.
+ ///
+ /// The object to compare with the current parameter.
+ ///
+ /// if the specified object is equal tot he current parameter;
+ /// otherwise, .
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object obj)
+ {
+ return obj is ParticleVector2Parameter other &&
+ Equals(other);
+ }
+
+ ///
+ /// Determines whether the specified parameter is equal to the current parameter.
+ ///
+ /// The parameter to compare with the current parameter.
+ ///
+ /// if the specified parameter is equal to the current parameter;
+ /// otherwise, .
+ ///
+ ///
+ /// When is only the values are
+ /// compared.
+ /// When is , both and
+ /// values are compared.
+ ///
+ public readonly bool Equals(ParticleVector2Parameter other)
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.Equals(other.Constant);
+ }
+
+ return RandomMin.Equals(other.RandomMin) &&
+ RandomMax.Equals(other.RandomMax);
+ }
+
+ ///
+ /// Returns the hash code for this parameter.
+ ///
+ /// A 32-bit signed integer that is the hash code for this instance.
+ ///
+ /// When is , returns the hash of .
+ /// When is returns the combined has of
+ /// and .
+ ///
+ public override readonly int GetHashCode()
+ {
+ base.GetHashCode();
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.GetHashCode();
+ }
+
+ return HashCode.Combine(RandomMin, RandomMax);
+ }
+
+ ///
+ /// Returns a string representation of this parameter.
+ ///
+ ///
+ /// When is , returns the string representation of
+ /// .
+ /// When is , returns a string in the format
+ /// "MinValue, MaxValue".
+ ///
+ public override readonly string ToString()
+ {
+ if (Kind == ParticleValueKind.Constant)
+ {
+ return Constant.ToString();
+ }
+
+ return string.Format(NumberFormatInfo.InvariantInfo, "{0}{1}{2}", RandomMin, NumberFormatInfo.InvariantInfo.NumberGroupSeparator, RandomMax);
+ }
+
+ ///
+ /// Determines whether two parameters are equal.
+ ///
+ /// The first parameter to compare.
+ /// The second parameter to compare.
+ ///
+ /// if the parameters are equal; otherwise, .
+ ///
+ public static bool operator ==(ParticleVector2Parameter lhs, ParticleVector2Parameter rhs)
+ {
+ return lhs.Equals(rhs);
+ }
+
+ ///
+ /// Determines whether two parameters are not equal.
+ ///
+ /// The first parameter to compare.
+ /// The second parameter to compare.
+ ///
+ /// if the parameters are not equal; otherwise, .
+ ///
+ public static bool operator !=(ParticleVector2Parameter lhs, ParticleVector2Parameter rhs)
+ {
+ return !lhs.Equals(rhs);
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/FastRandomExtensions.cs b/source/MonoGame.Extended/Particles/FastRandomExtensions.cs
deleted file mode 100644
index 66c99e7e..00000000
--- a/source/MonoGame.Extended/Particles/FastRandomExtensions.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using MonoGame.Extended;
-
-namespace MonoGame.Extended.Particles
-{
- public static class FastRandomExtensions
- {
- public static void NextColor(this FastRandom random, out HslColor color, Range 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/source/MonoGame.Extended/Particles/LineSegment.cs b/source/MonoGame.Extended/Particles/LineSegment.cs
deleted file mode 100644
index a3c14226..00000000
--- a/source/MonoGame.Extended/Particles/LineSegment.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using Microsoft.Xna.Framework;
-
-namespace MonoGame.Extended.Particles
-{
- ///
- /// Defines a part of a line that is bounded by two distinct end points.
- ///
- [StructLayout(LayoutKind.Sequential)]
- public struct LineSegment : IEquatable
- {
- internal readonly Vector2 _point1;
- internal readonly Vector2 _point2;
-
- ///
- /// Initializes a new instance of the structure.
- ///
- ///
- ///
- 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/source/MonoGame.Extended/Particles/Modifiers/AgeModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/AgeModifier.cs
index fac85f06..6f0fabe1 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/AgeModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/AgeModifier.cs
@@ -1,26 +1,47 @@
-using System.Collections.Generic;
-using System.ComponentModel;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using MonoGame.Extended.Particles.Data;
using MonoGame.Extended.Particles.Modifiers.Interpolators;
-namespace MonoGame.Extended.Particles.Modifiers
-{
- public class AgeModifier : Modifier
- {
- [EditorBrowsable(EditorBrowsableState.Always)]
- public List Interpolators { get; set; } = new List();
+namespace MonoGame.Extended.Particles.Modifiers;
- public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+///
+/// A modifier that applies multiple interpolators to particles based on their age.
+///
+///
+/// The controls how particle properties change over their lifetime
+/// by applying a collection of objects to each particle. Each interpolator
+/// in the collection operates on a different property of the particle (such as color, scale, or opacity),
+/// creating complex, time-based transformations.
+///
+/// Unlike other modifiers that apply incremental changes each frame, interpolators directly compute
+/// the target property values based on the particle's current age as a fraction of its total lifespan.
+/// This provides more predictable and consistent results regardless of frame rate.
+///
+public class AgeModifier : Modifier
+{
+ ///
+ /// Gets or sets the collection of interpolators that will be applied to particles.
+ ///
+ public List Interpolators { get; set; } = new List();
+
+ ///
+ /// Updates all particles by applying each interpolator in the collection to each particle.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
{
- var n = Interpolators.Count;
- while (iterator.HasNext)
+ Particle* particle = iterator.Next();
+
+ for (int i = 0; i < Interpolators.Count; i++)
{
- var particle = iterator.Next();
- for (var i = 0; i < n; i++)
- {
- var interpolator = Interpolators[i];
- interpolator.Update(particle->Age, particle);
- }
+ Interpolators[i].Update(particle->Age, particle);
}
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Containers/CircleContainerModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/Containers/CircleContainerModifier.cs
index 30d28020..32327a5a 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Containers/CircleContainerModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Containers/CircleContainerModifier.cs
@@ -1,52 +1,110 @@
-using System;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Data;
-namespace MonoGame.Extended.Particles.Modifiers.Containers
+namespace MonoGame.Extended.Particles.Modifiers.Containers;
+
+///
+/// A modifier that constrains particles within or outside of a circular boundary.
+///
+///
+/// The keeps particles either inside or outside a circular area, reflecting them
+/// at the boundary based on a configurable restitution coefficient. The circle is centered at each particle's trigger
+/// position (where it was emitted), creating local containment areas.
+///
+public class CircleContainerModifier : Modifier
{
- public class CircleContainerModifier : Modifier
+ ///
+ /// The radius of the circular container.
+ ///
+ public float Radius;
+
+ ///
+ /// Indicates whether particles should be contained inside the circle.
+ ///
+ ///
+ ///
+ /// -
+ /// When , particles are kept inside the circle (bouncing inward at the boundary).
+ ///
+ /// -
+ /// When , particles are kept outside the circle (bouncing outward at the boundary).
+ ///
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool Inside = true;
+
+ ///
+ /// Gets or sets the coefficient of restitution (bounciness) for particle collisions with the boundary.
+ ///
+ ///
+ ///
+ /// -
+ /// A value of 1.0 creates a perfectly elastic collision where particles maintain their energy.
+ ///
+ /// -
+ /// Values less than 1.0 create inelastic collisions where particles lose energy with each bounce.
+ ///
+ /// -
+ /// Values greater than 1.0 create super-elastic collisions where particles gain energy.
+ ///
+ ///
+ ///
+ /// The default value is 1.0.
+ ///
+ public float RestitutionCoefficient = 1;
+
+ ///
+ /// Updates all particles by constraining them to the circular boundary.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- public float Radius { get; set; }
- public bool Inside { get; set; } = true;
- public float RestitutionCoefficient { get; set; } = 1;
+ float radiusSq = Radius * Radius;
- public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ while (iterator.HasNext)
{
- var radiusSq = Radius*Radius;
- while (iterator.HasNext)
+ Particle* particle = iterator.Next();
+
+ Vector2 localPos;
+ localPos.X = particle->Position[0] - particle->TriggeredPos[0];
+ localPos.Y = particle->Position[1] - particle->TriggeredPos[1];
+
+ float distSq = localPos.LengthSquared();
+ Vector2 normal = Vector2.Normalize(localPos);
+
+ if (Inside)
{
- 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);
- }
+ 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
+
+ private unsafe void SetReflected(float distSq, Particle* particle, Vector2 normal)
+ {
+ float dist = MathF.Sqrt(distSq);
+ float d = dist - Radius;
+
+ float twoRestDot = 2 * RestitutionCoefficient *
+ Vector2.Dot(new Vector2(particle->Velocity[0], particle->Velocity[1]), normal);
+
+ particle->Velocity[0] -= twoRestDot * normal.X;
+ particle->Velocity[1] -= twoRestDot * normal.Y;
+
+ // exact computation requires sqrt or goniometrics
+ particle->Position[0] -= normal.X * d;
+ particle->Position[1] -= normal.Y * d;
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Containers/RectangleContainerModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/Containers/RectangleContainerModifier.cs
index a56e0721..9a6de728 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Containers/RectangleContainerModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Containers/RectangleContainerModifier.cs
@@ -1,59 +1,104 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Modifiers.Containers
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers;
+
+///
+/// A modifier that constrains particles within a rectangular boundary.
+///
+///
+/// The keeps particles inside a rectangular area, reflecting them off the
+/// boundaries based on a configurable restitution coefficient. The rectangle is centered at each particle's trigger
+/// position (where it was emitted), creating local containment areas.
+///
+public sealed class RectangleContainerModifier : Modifier
{
- public sealed class RectangleContainerModifier : Modifier
+ ///
+ /// Gets or sets the width of the rectangular container.
+ ///
+ public int Width;
+
+ ///
+ /// Gets or sets the height of the rectangular container, in units.
+ ///
+ public int Height;
+
+ ///
+ /// Gets or sets the coefficient of restitution (bounciness) for particle
+ /// collisions with the boundary.
+ ///
+ ///
+ ///
+ /// -
+ /// A value of 1.0 creates a perfectly elastic collision where particles maintain their energy.
+ ///
+ /// -
+ /// Values less than 1.0 create inelastic collisions where particles lose energy with each bounce.
+ ///
+ /// -
+ /// Values greater than 1.0 create super-elastic collisions where particles gain energy.
+ ///
+ ///
+ ///
+ /// The default value is 1.0.
+ ///
+ public float RestitutionCoefficient = 1.0f;
+
+ ///
+ /// Updates all particles by constraining them to the rectangular boundary.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- 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)
{
- while (iterator.HasNext)
+ Particle* particle = iterator.Next();
+
+ float left = particle->TriggeredPos[0] + Width * -0.5f;
+ float right = particle->TriggeredPos[0] + Width * 0.5f;
+ float top = particle->TriggeredPos[1] + Height * -0.5f;
+ float bottom = particle->TriggeredPos[1] + Height * 0.5f;
+
+ float xPos = particle->Position[0];
+ float xVel = particle->Velocity[0];
+ float yPos = particle->Position[1];
+ float yVel = particle->Velocity[1];
+
+ if ((int)particle->Position[0] < left)
{
- 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);
+ xPos = left + (left - xPos);
+ xVel = -xVel * RestitutionCoefficient;
}
+ else
+ {
+ if (particle->Position[0] > right)
+ {
+ xPos = right - (xPos - right);
+ xVel = -xVel * RestitutionCoefficient;
+ }
+ }
+
+ if (particle->Position[1] < top)
+ {
+ yPos = top + (top - yPos);
+ yVel = -yVel * RestitutionCoefficient;
+ }
+ else
+ {
+ if ((int)particle->Position[1] > bottom)
+ {
+ yPos = bottom - (yPos - bottom);
+ yVel = -yVel * RestitutionCoefficient;
+ }
+ }
+
+ particle->Position[0] = xPos;
+ particle->Position[1] = yPos;
+ particle->Velocity[0] = xVel;
+ particle->Velocity[1] = yVel;
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
index 4da6efa9..9f7efb6c 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
@@ -1,42 +1,75 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Modifiers.Containers
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers;
+
+///
+/// A modifier that constrains particles within a rectangular boundary by wrapping them around to the opposite side.
+///
+///
+/// The creates a looping effect by teleporting particles that exit the
+/// boundary to the opposite side, similar to classic arcade games where objects wrap around the screen edges. The
+/// rectangle is centered at each particle's trigger position (where it was emitted), creating local containment areas.
+///
+public class RectangleLoopContainerModifier : Modifier
{
- public class RectangleLoopContainerModifier : Modifier
+ ///
+ /// Gets or sets the width of the rectangular container, in units.
+ ///
+ public int Width;
+
+ ///
+ /// Gets or sets the height of the rectangular container, in units.
+ ///
+ public int Height;
+
+ ///
+ /// Updates all particles by wrapping them around to the opposite side when they cross the rectangular boundary.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- public int Width { get; set; }
- public int Height { get; set; }
-
- public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ while (iterator.HasNext)
{
- while (iterator.HasNext)
+ Particle* particle = iterator.Next();
+
+ var left = particle->TriggeredPos[0] + Width * -0.5f;
+ var right = particle->TriggeredPos[0] + Width * 0.5f;
+ var top = particle->TriggeredPos[1] + Height * -0.5f;
+ var bottom = particle->TriggeredPos[1] + Height * 0.5f;
+
+ var xPos = particle->Position[0];
+ var yPos = particle->Position[1];
+
+ if ((int)particle->Position[0] < left)
{
- 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);
+ xPos = particle->Position[0] + Width;
}
+ else
+ {
+ if ((int)particle->Position[0] > right)
+ {
+ xPos = particle->Position[0] - Width;
+ }
+ }
+
+ if ((int)particle->Position[1] < top)
+ {
+ yPos = particle->Position[1] + Height;
+ }
+ else
+ {
+ if ((int)particle->Position[1] > bottom)
+ {
+ yPos = particle->Position[1] - Height;
+ }
+ }
+
+ particle->Position[0] = xPos;
+ particle->Position[1] = yPos;
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/DragModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/DragModifier.cs
index f2d6d091..923c5649 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/DragModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/DragModifier.cs
@@ -1,23 +1,64 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Modifiers
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// A modifier that applies fluid resistance (drag) to particles, gradually reducing their velocity.
+///
+///
+/// The simulates the effect of particles moving through a fluid medium
+/// such as air, water, or another substance. This creates a damping effect that slows particles over time,
+/// with the slowdown proportional to their velocity, mass, and the properties of the medium.
+///
+public class DragModifier : Modifier
{
- public class DragModifier : Modifier
+ ///
+ /// Gets or sets the drag coefficient, representing the aerodynamic or hydrodynamic properties of particles.
+ ///
+ ///
+ /// The drag coefficient is a dimensionless quantity used in fluid dynamics to model
+ /// the resistance of an object moving through a fluid. Higher values create stronger
+ /// drag effects, causing particles to slow down more quickly.
+ ///
+ /// For reference to approximate real-world drag cooeficients, see
+ /// .
+ ///
+ /// The default value is 0.47.
+ ///
+ public float DragCoefficient = 0.47f;
+
+ ///
+ /// Gets or sets the density of the fluid medium, affecting the strength of the drag force.
+ ///
+ ///
+ /// This value represents the density of the fluid through which particles are moving.
+ /// Higher values create stronger drag effects, simulating denser media like water or oil.
+ ///
+ /// For reference to approximate real-world density values for various fluids, see
+ /// .
+ ///
+ /// The default value is 0.5.
+ ///
+ public float Density = .5f;
+
+ ///
+ /// Updates all particles by applying drag forces based on their velocity.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- 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)
{
- while (iterator.HasNext)
- {
- var particle = iterator.Next();
- var drag = -DragCoefficient*Density*particle->Mass*elapsedSeconds;
+ Particle* particle = iterator.Next();
- particle->Velocity = new Vector2(
- particle->Velocity.X + particle->Velocity.X*drag,
- particle->Velocity.Y + particle->Velocity.Y*drag);
- }
+ var drag = -DragCoefficient * Density * particle->Mass * elapsedSeconds;
+
+ particle->Velocity[0] = particle->Velocity[0] + particle->Velocity[0] * drag;
+ particle->Velocity[1] = particle->Velocity[1] + particle->Velocity[1] * drag;
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/ColorInterpolator.cs b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/ColorInterpolator.cs
index f94c6897..172bf896 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/ColorInterpolator.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/ColorInterpolator.cs
@@ -1,13 +1,40 @@
-namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+///
+/// An interpolator that gradually changes particle color properties over their lifetime.
+///
+///
+///
+/// The transitions a particle's color from the inherited
+/// to based on the
+/// provided interpolation amount
+///
+///
+/// Color values are represented in the HSL (Hue, Saturation, Lightness) color space as a Vector3.
+///
+///
+public sealed class ColorInterpolator : Interpolator
{
///
- /// Defines a modifier which interpolates the color of a particle over the course of its lifetime.
+ /// Updates a particle's color by interpolating between the start and end values.
///
- public sealed class ColorInterpolator : Interpolator
+ /// The normalized interpolation amount (from 0.0 to 1.0).
+ /// A pointer to the particle to update.
+ public override unsafe void Update(float amount, Particle* particle)
{
- public override unsafe void Update(float amount, Particle* particle)
- {
- particle->Color = HslColor.Lerp(StartValue, EndValue, amount);
- }
+ float h = StartValue.X + (EndValue.X - StartValue.X) * amount;
+ float s = StartValue.Y + (EndValue.Y - StartValue.Y) * amount;
+ float l = StartValue.Z + (EndValue.Z - StartValue.Z) * amount;
+
+ particle->Color[0] = h;
+ particle->Color[1] = s;
+ particle->Color[2] = l;
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/HueInterpolator.cs b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/HueInterpolator.cs
index 8b58a8ea..ccc03b19 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/HueInterpolator.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/HueInterpolator.cs
@@ -1,12 +1,29 @@
-using MonoGame.Extended;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+///
+/// An interpolator that gradually changes only the hue component of particle colors over their lifetime.
+///
+///
+/// Unlike which changes all HSL components, this interpolator
+/// affects only the hue component, preserving the particle's existing saturation and lightness values.
+/// This allows for color cycling effects while maintaining consistent saturation and brightness.
+///
+public class HueInterpolator : Interpolator
{
- public class HueInterpolator : Interpolator
+ ///
+ /// Updates a particle's hue by interpolating between the start and end values.
+ ///
+ /// The normalized interpolation amount (from 0.0 to 1.0).
+ /// A pointer to the particle to update.
+ public override unsafe void Update(float amount, Particle* particle)
{
- public override unsafe void Update(float amount, Particle* particle)
- {
- particle->Color = new HslColor((EndValue - StartValue) * amount + StartValue, particle->Color.S, particle->Color.L);
- }
+ float h = StartValue + (EndValue - StartValue) * amount;
+ particle->Color[0] = h;
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/Interpolator.cs b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/Interpolator.cs
index 0ec98666..4e31d7aa 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/Interpolator.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/Interpolator.cs
@@ -1,26 +1,43 @@
-namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+///
+/// Represents a base class for all particle interpolators.
+///
+///
+/// Interpolators are specialized modifiers that gradually change particle properties
+/// based on a normalized time value (between 0.0 and 1.0) over the particle's lifetime.
+/// This enables smooth transitions between initial and final states, such as color fades,
+/// size changes, or opacity adjustments.
+///
+public abstract class Interpolator
{
- public abstract class Interpolator
- {
- protected Interpolator()
- {
- Name = GetType().Name;
- }
+ ///
+ /// Gets or sets the display name of this interpolator.
+ ///
+ public string Name;
- public string Name { get; set; }
- public abstract unsafe void Update(float amount, Particle* particle);
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected Interpolator()
+ {
+ Name = GetType().Name;
}
- public abstract class Interpolator : Interpolator
- {
- ///
- /// Gets or sets the intial value when the particles are created.
- ///
- public virtual T StartValue { get; set; }
-
- ///
- /// Gets or sets the final value when the particles are retired.
- ///
- public virtual T EndValue { get; set; }
- }
-}
\ No newline at end of file
+ ///
+ /// Updates a single particle property based on the interpolation amount.
+ ///
+ /// The normalized interpolation amount (from 0.0 to 1.0).
+ /// A pointer to the particle to update.
+ ///
+ /// This method is called for each particle during its lifetime. The
+ /// parameter represents the particle's age as a fraction of its total lifespan.
+ ///
+ public abstract unsafe void Update(float amount, Particle* particle);
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/InterpolatorOfT.cs b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/InterpolatorOfT.cs
new file mode 100644
index 00000000..4ab056a4
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/InterpolatorOfT.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+///
+/// Represents a generic base class for particle interpolators that work with specific value types.
+///
+/// The type of value being interpolated. Must be a value type.
+public abstract class Interpolator : Interpolator where T : struct
+{
+ ///
+ /// Gets or sets the starting value for the interpolation.
+ ///
+ public T StartValue;
+
+ ///
+ /// Gets or sets the ending value for the interpolation.
+ ///
+ public T EndValue;
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/OpacityInterpolator.cs b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/OpacityInterpolator.cs
index 0a26fcda..2694e450 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/OpacityInterpolator.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/OpacityInterpolator.cs
@@ -1,10 +1,33 @@
-namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+///
+/// An interpolator that gradually changes the opacity of particles over their lifetime.
+///
+///
+///
+/// The transitions a particle's opacity (alpha) value from the inherited
+/// to based on the
+/// provided interpolation amount.
+///
+///
+/// Valid opacity values range from 0.0 (completely transparent) to 1.0 (completely opaque).
+///
+///
+public class OpacityInterpolator : Interpolator
{
- public class OpacityInterpolator : Interpolator
+ ///
+ /// Updates a particle's opacity by interpolating between the start and end values.
+ ///
+ /// The normalized interpolation amount (from 0.0 to 1.0).
+ /// A pointer to the particle to update.
+ public override unsafe void Update(float amount, Particle* particle)
{
- public override unsafe void Update(float amount, Particle* particle)
- {
- particle->Opacity = (EndValue - StartValue) * amount + StartValue;
- }
+ particle->Opacity = StartValue + (EndValue - StartValue) * amount;
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/RotationInterpolator.cs b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/RotationInterpolator.cs
index 44a72de8..c37c49f0 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/RotationInterpolator.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/RotationInterpolator.cs
@@ -1,10 +1,28 @@
-namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+///
+/// An interpolator that gradually changes the rotation angle of particles over their lifetime.
+///
+///
+/// The transitions a particle's rotation value from the inherited
+/// to based on the
+/// provided interpolation amount.
+///
+public class RotationInterpolator : Interpolator
{
- public class RotationInterpolator : Interpolator
+ ///
+ /// Updates a particle's rotation by interpolating between the start and end values.
+ ///
+ /// The normalized interpolation amount (from 0.0 to 1.0).
+ /// A pointer to the particle to update.
+ public override unsafe void Update(float amount, Particle* particle)
{
- public override unsafe void Update(float amount, Particle* particle)
- {
- particle->Rotation = (EndValue - StartValue) * amount + StartValue;
- }
+ particle->Rotation = StartValue + (EndValue - StartValue) * amount;
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/ScaleInterpolator.cs b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/ScaleInterpolator.cs
index aff92449..dd198917 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/ScaleInterpolator.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/ScaleInterpolator.cs
@@ -1,12 +1,28 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+///
+/// An interpolator that gradually changes the size of particles over their lifetime.
+///
+///
+/// The transitions a particle's scale factor from the inherited
+/// to based on the
+/// provided interpolation amount (typically representing the particle's normalized age).
+///
+public class ScaleInterpolator : Interpolator
{
- public class ScaleInterpolator : Interpolator
+ ///
+ /// Updates a particle's scale by interpolating between the start and end values.
+ ///
+ /// The normalized interpolation amount (from 0.0 to 1.0).
+ /// A pointer to the particle to update.
+ public override unsafe void Update(float amount, Particle* particle)
{
- public override unsafe void Update(float amount, Particle* particle)
- {
- particle->Scale = (EndValue - StartValue) * amount + StartValue;
- }
+ particle->Scale = StartValue + (EndValue - StartValue) * amount;
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/VelocityInterpolator b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/VelocityInterpolator
deleted file mode 100644
index bad04b19..00000000
--- a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/VelocityInterpolator
+++ /dev/null
@@ -1,12 +0,0 @@
-using Microsoft.Xna.Framework;
-
-namespace MonoGame.Extended.Particles.Modifiers.Interpolators
-{
- public class VelocityInterpolator : Interpolator
- {
- public override unsafe void Update(float amount, Particle* particle)
- {
- particle->Velocity = (EndValue - StartValue) * amount + StartValue;
- }
- }
-}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Interpolators/VelocityInterpolator.cs b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/VelocityInterpolator.cs
new file mode 100644
index 00000000..4e0cd2c3
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Modifiers/Interpolators/VelocityInterpolator.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+///
+/// An interpolator that gradually changes the velocity vector of particles over their lifetime.
+///
+///
+/// The transitions a particle's velocity from the inherited
+/// to based on the
+/// provided interpolation amount (typically representing the particle's normalized age).
+///
+public class VelocityInterpolator : Interpolator
+{
+ ///
+ /// Updates a particle's velocity by interpolating between the start and end values.
+ ///
+ /// The normalized interpolation amount (from 0.0 to 1.0).
+ /// A pointer to the particle to update.
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Velocity[0] = StartValue.X + (EndValue.X - StartValue.X) * amount;
+ particle->Velocity[1] = StartValue.Y + (EndValue.Y - StartValue.Y) * amount;
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/LinearGravityModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/LinearGravityModifier.cs
index e9ed216e..23d7aa90 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/LinearGravityModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/LinearGravityModifier.cs
@@ -1,23 +1,56 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Modifiers
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// A modifier that applies a constant directional force to particles, simulating gravity or wind.
+///
+///
+/// The applies a uniform acceleration in a specified direction
+/// to all particles, creating effects such as gravity, wind, or other constant forces. The force
+/// is applied proportionally to each particle's mass, simulating realistic physical behavior.
+///
+/// Note that this modifier only changes particle velocities; the actual position changes
+/// occur during the standard particle update cycle.
+///
+public class LinearGravityModifier : Modifier
{
- public class LinearGravityModifier : Modifier
+ ///
+ /// Gets or sets the direction vector of the gravitational force.
+ ///
+ ///
+ /// This vector defines both the direction and the relative magnitude of the force.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// Gets or sets the strength of the gravitational force, in units per second squared.
+ ///
+ ///
+ /// This value scales the overall magnitude of the force. Higher values create
+ /// stronger acceleration effects, causing particles to change velocity more rapidly.
+ ///
+ public float Strength;
+
+ ///
+ /// Updates all particles by applying a linear gravitational force.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- public Vector2 Direction { get; set; }
- public float Strength { get; set; }
+ Vector2 vector = Direction * (Strength * elapsedSeconds);
- public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ while (iterator.HasNext)
{
- var vector = Direction*(Strength*elapsedSeconds);
+ Particle* particle = iterator.Next();
- 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);
- }
+ particle->Velocity[0] = particle->Velocity[0] + vector.X * particle->Mass;
+ particle->Velocity[1] = particle->Velocity[1] + vector.Y * particle->Mass;
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/Modifier.cs b/source/MonoGame.Extended/Particles/Modifiers/Modifier.cs
index bd62d255..b0e95ac5 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/Modifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/Modifier.cs
@@ -1,18 +1,66 @@
-namespace MonoGame.Extended.Particles.Modifiers
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// Represents a base class for all particle modifiers.
+///
+///
+/// Particle modifiers are used to alter the behavior or properties of particles during their lifetime.
+/// Each modifier applies changes to particles at a configurable frequency, optimizing performance by
+/// spreading updates across frames when appropriate.
+/// Custom modifiers should inherit from this class and implement the method.
+///
+public abstract class Modifier
{
- public abstract class Modifier
+ private const float DEFAULT_MODIFIER_FREQUENCY = 60.0f;
+
+ private int _particlesUpdatedThisCycle;
+
+ ///
+ /// Gets or sets the display name of this modifier.
+ ///
+ public string Name;
+
+ ///
+ /// Gets or sets the update frequency of this modifier.
+ ///
+ ///
+ /// This value defines how often, in times per second, the modifier attempts to update
+ /// the entire particle buffer. For example, a value of 60.0f means that all particles
+ /// will be updated collectively approximately 60 times per second.
+ ///
+ /// To improve performance, updates are distributed across frames. Rather than updating
+ /// every particle in every frame, the modifier mathematically distributes updates by
+ /// processing a portion of the particles each frame based on the elapsed time and the
+ /// desired frequency. Over time, this results in all particles being updated at the
+ /// specified frequency on average, regardless of the actual frame rate.
+ ///
+ /// Higher values result in more frequent updates and smoother particle behavior, at the
+ /// cost of performance. Lower values reduce CPU usage but may make particle changes appear
+ /// less fluid.
+ ///
+ public float Frequency;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The default constructor sets the property to the name of the derived class
+ /// and initializes to .
+ ///
+ protected 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}]";
- }
+ Name = GetType().Name;
+ Frequency = DEFAULT_MODIFIER_FREQUENCY;
}
-}
\ No newline at end of file
+
+ ///
+ /// Updates the properties of particles according to this modifier's specific behavior.
+ ///
+ /// The elapsed time, in seconds, since the last update.
+ /// The iterator used to iterate the particles ot update.
+ public abstract void Update(float elapsedSeconds, ParticleIterator iterator);
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/ModifierExecutionStrategy.cs b/source/MonoGame.Extended/Particles/Modifiers/ModifierExecutionStrategy.cs
new file mode 100644
index 00000000..35974e06
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Modifiers/ModifierExecutionStrategy.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using TPL = System.Threading.Tasks.Parallel;
+
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// Defines different strategies for executing particle modifiers within a particle system.
+///
+///
+/// The strategy pattern implemented by this class allows the particle system to switch
+/// between serial (single-threaded) and parallel (multi-threaded) execution methods
+/// to optimize performance based on the execution environment and particle workload.
+///
+public abstract class ModifierExecutionStrategy
+{
+ ///
+ /// Gets a singleton instance of the serial execution strategy.
+ ///
+ ///
+ /// The serial strategy processes each modifier one after another on a single thread.
+ /// This may be more efficient for small particle counts or when thread synchronization
+ /// overhead outweighs the benefits of parallelism.
+ ///
+ static public ModifierExecutionStrategy Serial = new SerialModifierExecutionStrategy();
+
+ ///
+ /// Gets a singleton instance of the parallel execution strategy.
+ ///
+ ///
+ /// The parallel strategy processes modifiers concurrently using multiple threads.
+ /// This can significantly improve performance for systems with many particles and
+ /// multiple modifiers, especially on multi-core processors.
+ ///
+ static public ModifierExecutionStrategy Parallel = new ParallelModifierExecutionStrategy();
+
+ ///
+ /// Executes all modifiers in the collection on the particle buffer using the implemented strategy.
+ ///
+ /// The collection of modifiers to execute.
+ /// The elapsed time, in seconds, since the last update.
+ /// The iterator used to iterate the particles.
+ internal abstract unsafe void ExecuteModifiers(List modifiers, float elapsedSeconds, ParticleIterator iterator);
+
+ ///
+ /// Implements a serial (single-threaded) execution strategy for particle modifiers.
+ ///
+ ///
+ /// This strategy processes each modifier sequentially on a single thread,
+ /// which can be more efficient for smaller particle counts or when the
+ /// overhead of thread synchronization would outweigh the benefits of parallelism.
+ ///
+ internal class SerialModifierExecutionStrategy : ModifierExecutionStrategy
+ {
+ internal override unsafe void ExecuteModifiers(List modifiers, float elapsedSeconds, ParticleIterator iterator)
+ {
+ for (int i = 0; i < modifiers.Count; i++)
+ {
+ modifiers[i].Update(elapsedSeconds, iterator.Reset());
+ }
+ }
+
+ public override string ToString()
+ {
+ return nameof(Serial);
+ }
+ }
+
+ ///
+ /// Implements a parallel (multi-threaded) execution strategy for particle modifiers.
+ ///
+ ///
+ /// This strategy processes modifiers concurrently. It can significantly improve
+ /// performance for systems with many particles and multiple modifiers.
+ ///
+ internal class ParallelModifierExecutionStrategy : ModifierExecutionStrategy
+ {
+ internal override unsafe void ExecuteModifiers(List modifiers, float elapsedSeconds, ParticleIterator iterator)
+ {
+ TPL.ForEach(modifiers, modifier => modifier.Update(elapsedSeconds, iterator.Reset()));
+ }
+
+ public override string ToString()
+ {
+ return nameof(Parallel);
+ }
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/OpacityFastFadeModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/OpacityFastFadeModifier.cs
index 9cc44d83..f6afeb91 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/OpacityFastFadeModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/OpacityFastFadeModifier.cs
@@ -1,14 +1,44 @@
-namespace MonoGame.Extended.Particles.Modifiers
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// A modifier that rapidly decreases particle opacity based on their age.
+///
+///
+/// The creates a linear fade-out effect where particles
+/// become more transparent as they age.
+///
+/// Important notes:
+///
+/// -
+/// This modifier assumes particles have a standard lifespan of 1.0 second. For particles
+/// with different lifespans, the fade effect may not complete before the particle is removed,
+/// or may become fully transparent before the particle's actual end of life.
+///
+/// -
+/// Unlike other modifiers that accumulate changes over time, this modifier directly sets
+/// the opacity value each frame based solely on the particle's age.
+///
+///
+///
+public sealed class OpacityFastFadeModifier : Modifier
{
- public sealed class OpacityFastFadeModifier : Modifier
+ ///
+ /// Updates all particles by setting their opacity based on their age.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ while (iterator.HasNext)
{
- while (iterator.HasNext)
- {
- var particle = iterator.Next();
- particle->Opacity = 1.0f - particle->Age;
- }
+ Particle* particle = iterator.Next();
+
+ particle->Opacity = 1.0f - particle->Age;
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/RotationModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/RotationModifier.cs
index 2b17058f..1519196c 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/RotationModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/RotationModifier.cs
@@ -1,18 +1,46 @@
-namespace MonoGame.Extended.Particles.Modifiers
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// A modifier that applies a constant rotational velocity to particles.
+///
+///
+/// The changes the orientation of particles over time
+/// by applying a continuous rotation at a specified rate.
+///
+/// The rotation is applied uniformly to all particles, but can be combined with other modifiers
+/// to create more complex behaviors. For non-uniform rotation, consider using multiple particle
+/// emitters with different rotation rates or implementing a custom modifier.
+///
+public class RotationModifier : Modifier
{
- public class RotationModifier : Modifier
+ ///
+ /// Gets or sets the rate at which particles rotate, in radians per second.
+ ///
+ ///
+ /// Positive values cause clockwise rotation, while negative values cause
+ /// counter-clockwise rotation.
+ ///
+ public float RotationRate;
+
+ ///
+ /// Updates all particles by applying rotation based on the elapsed time.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- public float RotationRate { get; set; }
+ float rotationRateDelta = RotationRate * elapsedSeconds;
- public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ while (iterator.HasNext)
{
- var rotationRateDelta = RotationRate*elapsedSeconds;
+ Particle* particle = iterator.Next();
- while (iterator.HasNext)
- {
- var particle = iterator.Next();
- particle->Rotation += rotationRateDelta;
- }
+ particle->Rotation += rotationRateDelta;
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/VelocityColorModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/VelocityColorModifier.cs
index ae9dc7b7..8ac790c3 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/VelocityColorModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/VelocityColorModifier.cs
@@ -1,36 +1,93 @@
-using System;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Modifiers
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// A modifier that changes particle colors based on their velocity.
+///
+///
+/// The adjusts particle colors dynamically according to their
+/// movement speed. Particles can transition smoothly between two defined colors:
+///
+/// - A color for stationary or slow-moving particles ()
+/// - A color for fast-moving particles ()
+///
+///
+/// The color values are represented in HSL (Hue, Saturation, Lightness) format as a Vector3.
+///
+public class VelocityColorModifier : Modifier
{
- public class VelocityColorModifier : Modifier
+ ///
+ /// Gets or sets the color for particles that are stationary or moving slowly.
+ ///
+ ///
+ /// This color is applied to particles with zero velocity and serves as the starting
+ /// point for color interpolation. The Vector3 components represent HSL values
+ /// (Hue, Saturation, Lightness).
+ ///
+ public Vector3 StationaryColor;
+
+ ///
+ /// Gets or sets the color for particles that have reached or exceeded the velocity threshold.
+ ///
+ ///
+ /// This color is applied to fast-moving particles and serves as the end point
+ /// for color interpolation. The Vector3 components represent HSL values
+ /// (Hue, Saturation, Lightness).
+ ///
+ public Vector3 VelocityColor;
+
+ ///
+ /// Gets or sets the velocity magnitude at which particles fully transition to the velocity color.
+ ///
+ ///
+ /// This value defines the speed threshold that determines when a particle should
+ /// display the full . Particles moving slower than this
+ /// threshold will display a color interpolated between and
+ /// based on their speed relative to this threshold.
+ ///
+ public float VelocityThreshold;
+
+ ///
+ /// Updates all particles by changing their colors based on their current velocity.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- public HslColor StationaryColor { get; set; }
- public HslColor VelocityColor { get; set; }
- public float VelocityThreshold { get; set; }
+ float velocityThreshold2 = VelocityThreshold * VelocityThreshold;
- public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ while (iterator.HasNext)
{
- var velocityThreshold2 = VelocityThreshold*VelocityThreshold;
+ Particle* particle = iterator.Next();
- while (iterator.HasNext)
+ float velocitySquared = particle->Velocity[0] * particle->Velocity[0] +
+ particle->Velocity[1] * particle->Velocity[1];
+
+ if (velocitySquared >= velocityThreshold2)
{
- var particle = iterator.Next();
- var velocity2 = particle->Velocity.X*particle->Velocity.X +
- particle->Velocity.Y*particle->Velocity.Y;
- var deltaColor = VelocityColor - StationaryColor;
+ particle->Color[0] = VelocityColor.X;
+ particle->Color[1] = VelocityColor.Y;
+ particle->Color[2] = VelocityColor.Z;
+ }
+ else
+ {
+ Vector3 deltaColor = VelocityColor - StationaryColor;
+ float t = MathF.Sqrt(velocitySquared) / VelocityThreshold;
- if (velocity2 >= velocityThreshold2)
- VelocityColor.CopyTo(out particle->Color);
- else
- {
- var t = (float) Math.Sqrt(velocity2)/VelocityThreshold;
+ float h = deltaColor.X * t + StationaryColor.X;
+ float s = deltaColor.Y * t + StationaryColor.Y;
+ float l = deltaColor.Z * t + StationaryColor.Z;
- particle->Color = new HslColor(
- deltaColor.H*t + StationaryColor.H,
- deltaColor.S*t + StationaryColor.S,
- deltaColor.L*t + StationaryColor.L);
- }
+ particle->Color[0] = h;
+ particle->Color[1] = s;
+ particle->Color[2] = l;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/VelocityModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/VelocityModifier.cs
index f089abcc..8a849841 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/VelocityModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/VelocityModifier.cs
@@ -1,43 +1,72 @@
-using System;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
using System.Collections.Generic;
+using MonoGame.Extended.Particles.Data;
using MonoGame.Extended.Particles.Modifiers.Interpolators;
-namespace MonoGame.Extended.Particles.Modifiers
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// A modifier that applies interpolators to particles based on their velocity magnitude.
+///
+///
+/// The controls how particle properties change based on their speed
+/// by applying a collection of objects to each particle. The intensity
+/// of the effect is determined by comparing the particle's velocity magnitude to a threshold value.
+///
+public class VelocityModifier : Modifier
{
- public class VelocityModifier : Modifier
+ ///
+ /// Gets or sets the collection of interpolators that will be applied to particles.
+ ///
+ public List Interpolators { get; set; } = new List();
+
+ ///
+ /// Gets or sets the velocity magnitude at which particles reach the maximum interpolation effect.
+ ///
+ ///
+ /// This value defines the speed threshold that determines when a particle should
+ /// receive the full interpolation effect (amount = 1.0). Particles moving slower than this
+ /// threshold will receive a proportionally reduced effect based on their velocity magnitude.
+ ///
+ public float VelocityThreshold;
+
+ ///
+ /// Updates all particles by applying interpolators with an amount based on each particle's velocity.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- public List Interpolators { get; set; } = new List();
+ float velocityThreshold2 = VelocityThreshold * VelocityThreshold;
- public float VelocityThreshold { get; set; }
-
- public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ while (iterator.HasNext)
{
- var velocityThreshold2 = VelocityThreshold*VelocityThreshold;
- var n = Interpolators.Count;
+ Particle* particle = iterator.Next();
- while (iterator.HasNext)
+ float velocitySquared = particle->Velocity[0] * particle->Velocity[0] +
+ particle->Velocity[1] * particle->Velocity[1];
+
+ if (velocitySquared >= velocityThreshold2)
{
- var particle = iterator.Next();
- var velocity2 = particle->Velocity.LengthSquared();
-
- if (velocity2 >= velocityThreshold2)
+ for (int i = 0; i < Interpolators.Count; i++)
{
- for (var i = 0; i < n; i++)
- {
- var interpolator = Interpolators[i];
- interpolator.Update(1, particle);
- }
+ Interpolator interpolator = Interpolators[i];
+ interpolator.Update(1, particle);
}
- else
+ }
+ else
+ {
+ float t = (float)Math.Sqrt(velocitySquared) / VelocityThreshold;
+
+ for (int i = 0; i < Interpolators.Count; i++)
{
- var t = (float) Math.Sqrt(velocity2)/VelocityThreshold;
- for (var i = 0; i < n; i++)
- {
- var interpolator = Interpolators[i];
- interpolator.Update(t, particle);
- }
+ Interpolator interpolator = Interpolators[i];
+ interpolator.Update(t, particle);
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Modifiers/VortexModifier.cs b/source/MonoGame.Extended/Particles/Modifiers/VortexModifier.cs
index 5011d198..7b573959 100644
--- a/source/MonoGame.Extended/Particles/Modifiers/VortexModifier.cs
+++ b/source/MonoGame.Extended/Particles/Modifiers/VortexModifier.cs
@@ -1,32 +1,77 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
using System;
using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Data;
-namespace MonoGame.Extended.Particles.Modifiers
+namespace MonoGame.Extended.Particles.Modifiers;
+
+///
+/// A modifier that creates a gravitational vortex effect, pulling particles toward a central point.
+///
+///
+/// The simulates a gravitational attraction between a central point and
+/// each particle. The strength of the attraction is based on gravitational principles, where the force
+/// is proportional to the masses and inversely proportional to the square of the distance.
+///
+/// The actual acceleration applied to each particle is clamped to prevent particles from moving
+/// too quickly when they get close to the center of the vortex.
+///
+public unsafe class VortexModifier : Modifier
{
- public unsafe class VortexModifier : Modifier
+ private const float GRAVITY = 100000f;
+
+ ///
+ /// Gets or sets the center position of the vortex in 2D space.
+ ///
+ ///
+ /// Particles will be attracted toward this point.
+ ///
+ public Vector2 Position;
+
+ ///
+ /// Gets or sets the mass of the vortex center.
+ ///
+ ///
+ /// Higher mass values create stronger attraction forces.
+ ///
+ public float Mass;
+
+ ///
+ /// Gets or sets the maximum speed that the vortex can impart on particles.
+ ///
+ ///
+ /// This value limits how quickly particles can be accelerated by the vortex,
+ /// preventing extreme velocities when particles get very close to the center.
+ ///
+ public float MaxSpeed;
+
+ ///
+ /// Updates all particles by applying a gravitational force towards the vortex center.
+ ///
+ ///
+ public override unsafe void Update(float elapsedSeconds, ParticleIterator iterator)
{
- // 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)
{
- while (iterator.HasNext)
- {
- var particle = iterator.Next();
- var diff = Position + particle->TriggerPos - particle->Position;
+ Particle* particle = iterator.Next();
- var distance2 = diff.LengthSquared();
+ float distx = Position.X - particle->Position[0];
+ float disty = Position.Y - particle->Position[1];
- var speedGain = _gravConst*Mass/distance2;
- speedGain = Math.Max(Math.Min(speedGain, MaxSpeed), -MaxSpeed) * elapsedSeconds;
- // normalize distances and multiply by speedGain
- diff.Normalize();
- particle->Velocity += diff*speedGain;
- }
+ float distance2 = (distx * distx) + (disty * disty);
+ float distance = (float)Math.Sqrt(distance2);
+
+ float m = (GRAVITY * Mass * particle->Mass) / distance2;
+ m = Math.Max(Math.Min(m, MaxSpeed), -MaxSpeed) * elapsedSeconds;
+
+ distx = (distx / distance) * m;
+ disty = (disty / distance) * m;
+
+ particle->Velocity[0] += distx;
+ particle->Velocity[1] += disty;
}
}
}
diff --git a/source/MonoGame.Extended/Particles/Particle.cs b/source/MonoGame.Extended/Particles/Particle.cs
deleted file mode 100644
index 042d8cef..00000000
--- a/source/MonoGame.Extended/Particles/Particle.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-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/source/MonoGame.Extended/Particles/ParticleBuffer.cs b/source/MonoGame.Extended/Particles/ParticleBuffer.cs
index e361b8b7..c3b4cbb9 100644
--- a/source/MonoGame.Extended/Particles/ParticleBuffer.cs
+++ b/source/MonoGame.Extended/Particles/ParticleBuffer.cs
@@ -1,157 +1,203 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
using System;
using System.Runtime.InteropServices;
+using MonoGame.Extended.Particles.Data;
-namespace MonoGame.Extended.Particles
+namespace MonoGame.Extended.Particles;
+
+///
+/// Represents a circular memory buffer that efficiently stores and manages particles in contiguous unmanaged memory.
+///
+///
+/// The class provides high-performance memory management for particle systems by
+/// allocating an unmanaged memory block to store particle data in a circular buffer arrangement. This implementation
+/// uses head and tail pointers to manage particle allocation and deallocation without requiring memory copying
+/// operations, making it more efficient than a linear buffer approach.
+///
+public unsafe class ParticleBuffer : IDisposable
{
- public class ParticleBuffer : IDisposable
+ private readonly ParticleIterator _iterator;
+
+ ///
+ /// Gets the native pointer to the beginning of the unmanaged memory buffer that stores particle data.
+ ///
+ ///
+ /// An pointing to the start of the allocated unmanaged memory block.
+ ///
+ ///
+ ///
+ /// This property provides direct access to the underlying unmanaged memory used by the particle buffer.
+ /// The memory pointed to by this value contains particle data stored in a contiguous block, with each
+ /// particle occupying bytes.
+ ///
+ ///
+ /// Warning: This pointer should be used with extreme caution and only within unsafe code blocks.
+ /// Improper use of this pointer can lead to memory corruption, access violations, or other undefined behavior.
+ /// The memory is automatically managed by this instance and should not be
+ /// manually freed or modified outside of the provided safe methods.
+ ///
+ ///
+ /// The memory layout is arranged as a circular buffer where particles are stored sequentially.
+ /// Use and the for safe access to active particles rather than
+ /// directly manipulating this pointer.
+ ///
+ ///
+ /// This pointer becomes invalid after the is disposed. Accessing it after
+ /// disposal will result in undefined behavior.
+ ///
+ ///
+ public IntPtr NativePointer { get; }
+
+ ///
+ /// Gets a pointer to the current tail position in the circular buffer where new particles are allocated.
+ ///
+ public unsafe Particle* Tail { get; private set; }
+
+ ///
+ /// Gets a pointer to the end fo the allocated buffer memory, used for circular buffer bounds checking.
+ ///
+ public Particle* BufferEnd { get; }
+
+ ///
+ /// Gets the maximum number of particles that can be stored in this buffer.
+ ///
+ public int Size { get; }
+
+ ///
+ /// Gets an iterator for traversing the active particles in the buffer.
+ ///
+ ///
+ /// The iterator is reset each time this property is accessed, starting from the current head position.
+ /// Use this to safely iterate through all active particles in the correct order.
+ ///
+ public ParticleIterator Iterator => _iterator.Reset();
+
+ ///
+ /// Gets a pointer to the current head position in the circular buffer where the oldest active particle is located.
+ ///
+ ///
+ /// This pointer should be used carefully and only within unsafe code blocks. The memory it points to is
+ /// automatically freed when the is disposed.
+ ///
+ public unsafe Particle* Head { get; private set; }
+
+ ///
+ /// Gets the number of additional particles that can be added to the buffer before it is full.
+ ///
+ public int Available => Size - Count;
+
+ ///
+ /// Gets the current number of active particles in the buffer.
+ ///
+ public int Count { get; private set; }
+
+ ///
+ /// Gets the total size of the buffer in bytes.
+ ///
+ ///
+ /// The size includes space for one additional particle beyond the specified capacity to facilitate
+ /// circular buffer operations.
+ ///
+ public int SizeInBytes => Particle.SizeInBytes * (Size + 1);
+
+ ///
+ /// Gets the size of the currently active portion of the buffer in bytes.
+ ///
+ public int ActiveSizeInBytes => Particle.SizeInBytes * Count;
+
+ ///
+ /// Gets a value indicating whether this has been disposed.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class with the specified capacity.
+ ///
+ /// The maximum number of particles that can be stored in the buffer.
+ public unsafe ParticleBuffer(int size)
{
- private readonly ParticleIterator _iterator;
- private readonly IntPtr _nativePointer;
+ Size = size;
+ NativePointer = Marshal.AllocHGlobal(SizeInBytes);
- // points to the first memory pos after the buffer
- protected readonly unsafe Particle* BufferEnd;
+ BufferEnd = (Particle*)(NativePointer + SizeInBytes);
+ Head = (Particle*)NativePointer;
+ Tail = (Particle*)NativePointer;
- // points to the particle after the last active particle.
- protected unsafe Particle* Tail;
+ _iterator = new ParticleIterator(this);
- public unsafe ParticleBuffer(int size)
+ GC.AddMemoryPressure(SizeInBytes);
+ }
+
+ ///
+ ~ParticleBuffer() => Dispose();
+
+ ///
+ /// Allocates space in the circular buffer for a specified number of particles to be released.
+ ///
+ /// The number of particles to allocate space for.
+ ///
+ /// A positioned at the start of the newly allocated particles,
+ /// allowing iteration over the allocated particle slots.
+ ///
+ ///
+ /// Thrown if this method is called after the buffer has been disposed.
+ ///
+ public unsafe ParticleIterator Release(int releaseQuantity)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, this);
+
+ int numToRelease = Math.Min(releaseQuantity, Available);
+
+ int prevCount = Count;
+ Count += numToRelease;
+
+ Tail += numToRelease;
+
+ if (Tail >= BufferEnd)
{
- Size = size;
- _nativePointer = Marshal.AllocHGlobal(SizeInBytes);
-
- BufferEnd = (Particle*) (_nativePointer + SizeInBytes);
- Head = (Particle*) _nativePointer;
- Tail = (Particle*) _nativePointer;
-
- _iterator = new ParticleIterator(this);
-
- GC.AddMemoryPressure(SizeInBytes);
+ Tail -= Size + 1;
}
- public int Size { get; }
+ return Iterator.Reset(prevCount);
+ }
- public ParticleIterator Iterator => _iterator.Reset();
- // pointer to the first particle
- public unsafe Particle* Head { get; private set; }
+ ///
+ /// Removes a specified number of particles from the beginning of the circular buffer.
+ ///
+ /// The number of particles to remove from the buffer.
+ ///
+ /// Thrown if this method is called after the buffer has been disposed.
+ ///
+ public unsafe void Reclaim(int number)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, this);
- // 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;
+ Count -= number;
- ///
- /// Gets a value that indicates whether this instance of the class has been
- /// disposed.
- ///
- public bool IsDisposed { get; set; }
+ Head += number;
- public void Dispose()
+ if (Head >= BufferEnd)
{
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if(IsDisposed)
- {
- return;
- }
-
- Marshal.FreeHGlobal(_nativePointer);
- GC.RemoveMemoryPressure(SizeInBytes);
- IsDisposed = true;
- }
-
- ///
- /// Release the given number of particles or the most available.
- /// Returns a started iterator to iterate over the new particles.
- ///
- public unsafe ParticleIterator Release(int releaseQuantity)
- {
- ThrowIfDisposed();
-
- 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)
- {
- ThrowIfDisposed();
-
- Count -= number;
-
- Head += number;
- if (Head >= BufferEnd)
- Head -= Size + 1;
- }
-
- private void ThrowIfDisposed()
- {
- if(IsDisposed)
- {
- throw new ObjectDisposedException(nameof(ParticleBuffer));
- }
- }
-
- ~ParticleBuffer()
- {
- Dispose(false);
- }
-
- 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;
- }
+ Head -= Size + 1;
}
}
+
+ ///
+ /// Releases all resources used by the .
+ ///
+ public void Dispose()
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ Marshal.FreeHGlobal(NativePointer);
+ GC.RemoveMemoryPressure(SizeInBytes);
+ IsDisposed = true;
+ GC.SuppressFinalize(this);
+ }
}
diff --git a/source/MonoGame.Extended/Particles/ParticleEffect.cs b/source/MonoGame.Extended/Particles/ParticleEffect.cs
index e4091107..ad1c76ee 100644
--- a/source/MonoGame.Extended/Particles/ParticleEffect.cs
+++ b/source/MonoGame.Extended/Particles/ParticleEffect.cs
@@ -1,123 +1,270 @@
-using System;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text.Json;
using Microsoft.Xna.Framework;
-using MonoGame.Extended.Particles.Serialization;
-using MonoGame.Extended.Serialization.Json;
+using MonoGame.Extended.Particles.Primitives;
-namespace MonoGame.Extended.Particles
+namespace MonoGame.Extended.Particles;
+
+///
+/// Represents a complete particle effect composed of multiple emitters.
+///
+///
+/// The class serves as a container for one or more instances,
+/// allowing for complex visual effects that combine different types of particle behaviors and appearances.
+///
+/// Effects can be positioned, rotated, and scaled as a single unit, with all contained emitters being affected
+/// by these transformations. Effects also provide methods for triggering all emitters simultaneously.
+///
+public class ParticleEffect : IDisposable
{
- public class ParticleEffect : Transform2, IDisposable
+ ///
+ /// Gets or sets the name of this effect, used for identification and debugging.
+ ///
+ public string Name;
+
+ ///
+ /// Gets or sets the position of this effect in 2D space.
+ ///
+ ///
+ /// This position is used as the reference point for all emitters in the effect.
+ /// When the effect is updated, this position is passed to each emitter's update method.
+ ///
+ public Vector2 Position;
+
+ ///
+ /// Gets or sets the rotation of this effect, in radians.
+ ///
+ ///
+ /// This property can be used to rotate the entire effect around its position.
+ /// Note that rotation is not automatically applied to emitters and must be handled by the rendering system.
+ ///
+ public float Rotation;
+
+ ///
+ /// Gets or sets the scale factor of this effect.
+ ///
+ ///
+ /// This property can be used to uniformly or non-uniformly scale the entire effect.
+ /// Note that scaling is not automatically applied to emitters and must be handled by the rendering system.
+ ///
+ public Vector2 Scale;
+
+ ///
+ /// Gets or sets the collection of emitters that compose this effect.
+ ///
+ ///
+ /// Each emitter in this collection contributes to the overall visual appearance of the effect,
+ /// potentially with different behaviors, textures, and particle properties.
+ ///
+ public List Emitters;
+
+ ///
+ /// Gets a value indicating whether this has been disposed.
+ ///
+ /// if the effect has been disposed; otherwise, .
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Gets the total number of active particles across all emitters in this effect.
+ ///
+ /// The sum of for all emitters in the effect.
+ public int ActiveParticles
{
- public ParticleEffect(string name = null)
+ get
{
- Name = name;
- Emitters = new List();
- }
-
- ~ParticleEffect() => Dispose(false);
-
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- private void Dispose(bool disposing)
- {
- if(IsDisposed)
- {
- return;
- }
-
- if(disposing)
- {
- foreach(var emitter in Emitters)
- {
- emitter.Dispose();
- }
- }
-
- IsDisposed = true;
- }
-
- public string Name { get; set; }
- public List Emitters { get; set; }
- public int ActiveParticles => Emitters.Sum(t => t.ActiveParticles);
-
- ///
- /// Gets a value that indicates whether this instance of the class has been
- /// disposed.
- ///
- public bool IsDisposed { get; private set; }
-
- 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(stream, options);
- }
-
- public void Update(float elapsedSeconds)
- {
- ThrowIfDisposed();
-
- 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)
- {
- ThrowIfDisposed();
- for (var i = 0; i < Emitters.Count; i++)
- Emitters[i].Trigger(position, layerDepth);
- }
-
- public void Trigger(LineSegment line, float layerDepth = 0)
- {
- ThrowIfDisposed();
- for (var i = 0; i < Emitters.Count; i++)
- Emitters[i].Trigger(line, layerDepth);
- }
-
- private void ThrowIfDisposed()
- {
- if(IsDisposed)
- {
- throw new ObjectDisposedException(nameof(ParticleBuffer));
- }
- }
-
- public override string ToString()
- {
- return Name;
+ return Emitters.Sum(t => t.ActiveParticles);
}
}
+
+ ///
+ /// Initializes a new instance of the class with the specified name.
+ ///
+ /// The name of the effect, used for identification and debugging.
+ ///
+ /// This constructor initializes the effect with default position, rotation, and scale,
+ /// and creates an empty collection of emitters.
+ ///
+ public ParticleEffect(string name)
+ {
+ Name = name;
+ Position = Vector2.Zero;
+ Rotation = 0.0f;
+ Scale = Vector2.One;
+ Emitters = new List();
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~ParticleEffect()
+ {
+ Dispose(false);
+ }
+
+ ///
+ /// Advances the effect's state rapidly to simulate it having been active for a period of time.
+ ///
+ /// The position at which to simulate the effect.
+ /// The total time, in seconds, to simulate.
+ /// The time interval, in seconds, between simulated triggers.
+ ///
+ /// This method is useful for pre-filling a scene with particles that appear to have been emitted
+ /// over time, rather than starting with an empty effect.
+ ///
+ public void FastForward(Vector2 position, float seconds, float triggerPeriod)
+ {
+ float time = 0.0f;
+ while (time < seconds)
+ {
+ Update(triggerPeriod);
+ Trigger(position);
+ time += triggerPeriod;
+ }
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ Update((float)gameTime.ElapsedGameTime.TotalSeconds);
+ }
+
+ ///
+ /// Updates the state of all emitters in this effect.
+ ///
+ /// The elapsed time, in seconds, since the last update.
+ ///
+ /// This method propagates the update call to each emitter in the effect, passing along
+ /// the elapsed time and the current position of the effect.
+ ///
+ ///
+ /// Thrown if this method is called after the effect has been disposed.
+ ///
+ public void Update(float elapsedSeconds)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, typeof(ParticleBuffer));
+
+ for (int i = 0; i < Emitters.Count; i++)
+ {
+ Emitters[i].Update(elapsedSeconds, Position);
+ }
+ }
+
+ ///
+ /// Triggers all emitters in this effect at the effect's current position.
+ ///
+ public void Trigger()
+ {
+ Trigger(Position);
+ }
+
+ ///
+ /// Triggers all emitters in this effect at the specified position.
+ ///
+ /// The position in 2D space at which to trigger the emitters.
+ /// The layer depth at which to render the emitted particles.
+ ///
+ /// Thrown if this method is called after the effect has been disposed.
+ ///
+ public void Trigger(Vector2 position, float layerDepth = 0)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, typeof(ParticleBuffer));
+
+ for (int i = 0; i < Emitters.Count; i++)
+ {
+ Emitters[i].Trigger(position, layerDepth);
+ }
+ }
+
+ ///
+ /// Triggers all emitters in this effect along a line segment.
+ ///
+ /// The line segment along which to distribute triggered particles.
+ /// The layer depth at which to render the emitted particles.
+ ///
+ /// Thrown if this method is called after the effect has been disposed.
+ ///
+ public void Trigger(LineSegment line, float layerDepth)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, typeof(ParticleBuffer));
+
+ for (int i = 0; i < Emitters.Count; i++)
+ {
+ Emitters[i].Trigger(line, layerDepth);
+ }
+ }
+
+ ///
+ /// Creates a new instance of the class from a file.
+ ///
+ /// The path to the file containing the serialized effect data.
+ /// A new instance with properties and emitters as defined in the file.
+ ///
+ /// This method is not yet implemented.
+ ///
+ ///
+ /// This method is intended to deserialize effect data from a file, but has not been implemented in this version.
+ ///
+ public static ParticleEffect FromFile(string path)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Creates a new instance of the class from a stream.
+ ///
+ /// The stream containing the serialized effect data.
+ /// A new instance with properties and emitters as defined in the stream.
+ ///
+ /// This method is not yet implemented.
+ ///
+ ///
+ /// This method is intended to deserialize effect data from a stream, but has not been implemented in this version.
+ ///
+ public static ParticleEffect FromStream(Stream stream)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Releases all resources used by the .
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases the resources used by the .
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ private void Dispose(bool disposing)
+ {
+ if (IsDisposed) { return; }
+
+ if (disposing)
+ {
+ for (int i = 0; i < Emitters.Count; i++)
+ {
+ Emitters[i].Dispose();
+ }
+ }
+
+ IsDisposed = true;
+ }
+
+ ///
+ /// Returns a string that represents the current effect.
+ ///
+ /// The of this effect.
+ public override string ToString()
+ {
+ return Name;
+ }
}
diff --git a/source/MonoGame.Extended/Particles/ParticleEffectReader.cs b/source/MonoGame.Extended/Particles/ParticleEffectReader.cs
new file mode 100644
index 00000000..23fd848f
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/ParticleEffectReader.cs
@@ -0,0 +1,635 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Graphics;
+using MonoGame.Extended.Particles.Data;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Particles.Modifiers.Containers;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+using MonoGame.Extended.Particles.Profiles;
+using MonoGame.Extended.Serialization.Xml;
+
+namespace MonoGame.Extended.Particles;
+
+///
+/// Represents a reader that deserializes a from an XML configuration.
+///
+public sealed class ParticleEffectReader : IDisposable
+{
+ private readonly XmlReader _reader;
+ private readonly ContentManager _content;
+
+ ///
+ /// Gets a value that indicates whether this has been disposed of.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class that reads from a file.
+ ///
+ /// The file path to read the XMl from.
+ /// The to use for loading textures.
+ /// is
+ /// is or empty.
+ public ParticleEffectReader(string fileName, ContentManager content)
+ {
+ ArgumentNullException.ThrowIfNull(content);
+ ArgumentException.ThrowIfNullOrEmpty(fileName);
+
+ XmlReaderSettings settings = new XmlReaderSettings();
+ settings.CloseInput = true;
+ settings.IgnoreComments = true;
+ settings.IgnoreWhitespace = true;
+
+ _content = content;
+ _reader = XmlReader.Create(fileName, settings);
+ }
+
+ ///
+ /// Initializes a new instance of the class that reads from a stream.
+ ///
+ /// The stream to read from.
+ /// The to use for loading textures.
+ /// or is
+ public ParticleEffectReader(Stream stream, ContentManager content)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+ ArgumentNullException.ThrowIfNull(content);
+
+ XmlReaderSettings settings = new XmlReaderSettings();
+ settings.IgnoreComments = true;
+ settings.IgnoreWhitespace = true;
+ settings.CloseInput = false;
+
+ _content = content;
+ _reader = XmlReader.Create(stream, settings);
+ }
+
+ ///
+ ~ParticleEffectReader()
+ {
+ Dispose();
+ }
+
+ ///
+ /// Reads a from the XML input.
+ ///
+ /// The deserialized .
+ /// The Xml format is invalid.
+ public ParticleEffect ReadParticleEffect()
+ {
+ _reader.MoveToContent();
+
+ if (_reader.NodeType != XmlNodeType.Element || _reader.LocalName != nameof(ParticleEffect))
+ {
+ throw new XmlException($"Expected {nameof(ParticleEffect)} root element");
+ }
+
+ string name = _reader.GetAttribute(nameof(ParticleEffect.Name)) ?? "Unnamed";
+ ParticleEffect effect = new ParticleEffect(name);
+
+ effect.Rotation = _reader.GetAttributeFloat(nameof(ParticleEffect.Rotation));
+ effect.Scale = _reader.GetAttributeVector2(nameof(ParticleEffect.Scale));
+
+ if (_reader.ReadToDescendant(nameof(ParticleEffect.Emitters)))
+ {
+ if (_reader.ReadToDescendant(nameof(ParticleEmitter)))
+ {
+ do
+ {
+ ParticleEmitter emitter = ReadParticleEmitter();
+ effect.Emitters.Add(emitter);
+ } while (_reader.ReadToNextSibling(nameof(ParticleEmitter)));
+ }
+ }
+
+ return effect;
+ }
+
+ private ParticleEmitter ReadParticleEmitter()
+ {
+ int capacity = _reader.GetAttributeInt(nameof(ParticleEmitter.Capacity));
+
+ ParticleEmitter emitter = new ParticleEmitter(capacity);
+ emitter.Name = _reader.GetAttribute(nameof(ParticleEmitter.Name)) ?? nameof(ParticleEmitter.Name);
+ emitter.LifeSpan = _reader.GetAttributeFloat(nameof(ParticleEmitter.LifeSpan));
+ emitter.Offset = _reader.GetAttributeVector2(nameof(ParticleEmitter.Offset));
+ emitter.LayerDepth = _reader.GetAttributeFloat(nameof(ParticleEmitter.LayerDepth));
+ emitter.AutoTrigger = _reader.GetAttributeBool(nameof(ParticleEmitter.AutoTrigger));
+ emitter.AutoTriggerFrequency = _reader.GetAttributeFloat(nameof(ParticleEmitter.AutoTriggerFrequency));
+ emitter.ReclaimFrequency = _reader.GetAttributeFloat(nameof(ParticleEmitter.ReclaimFrequency));
+
+ string strategy = _reader.GetAttribute(nameof(ParticleEmitter.ModifierExecutionStrategy));
+
+ if (strategy.Equals(nameof(ModifierExecutionStrategy.Serial)))
+ {
+ emitter.ModifierExecutionStrategy = ModifierExecutionStrategy.Serial;
+ }
+ else if (strategy.Equals(nameof(ModifierExecutionStrategy.Parallel)))
+ {
+ emitter.ModifierExecutionStrategy = ModifierExecutionStrategy.Parallel;
+ }
+ else
+ {
+ emitter.ModifierExecutionStrategy = ModifierExecutionStrategy.Serial;
+ }
+
+ emitter.RenderingOrder = _reader.GetAttributeEnum(nameof(ParticleEmitter.RenderingOrder));
+
+ using XmlReader subtree = _reader.ReadSubtree();
+ while (subtree.Read())
+ {
+ if (subtree.NodeType == XmlNodeType.Element)
+ {
+ switch (subtree.LocalName)
+ {
+ case nameof(ParticleEmitter.TextureRegion):
+ emitter.TextureRegion = ReadTexture2DRegion(subtree);
+ break;
+
+ case nameof(ParticleEmitter.Parameters):
+ emitter.Parameters = ReadParticleReleaseParameters(subtree);
+ break;
+
+ case nameof(ParticleEmitter.Profile):
+ emitter.Profile = ReadProfile(subtree);
+ break;
+
+ case nameof(ParticleEmitter.Modifiers):
+ ReadModifiers(subtree, emitter.Modifiers);
+ break;
+ }
+ }
+ }
+
+ return emitter;
+ }
+
+ private Texture2DRegion ReadTexture2DRegion(XmlReader reader)
+ {
+ string name = reader.GetAttribute(nameof(Texture2DRegion.Texture.Name));
+
+ if (string.IsNullOrEmpty(name))
+ {
+ return null;
+ }
+
+ Rectangle bounds = _reader.GetAttributeRectangle(nameof(Texture2DRegion.Bounds));
+
+ try
+ {
+ // Try loading directly though the content manager. This should work, but there is a bug
+ // for relative paths which has been documented at
+ // https://github.com/MonoGame/MonoGame/issues/8786
+ // And a propsed fix at
+ // https://github.com/MonoGame/MonoGame/pull/8787
+ // But until that is merged and released, we'll attempt to load here, then fall back to direct load in
+ // the catch
+ Texture2D texture = _content.Load(name);
+ return new Texture2DRegion(texture, bounds);
+ }
+ catch (ContentLoadException)
+ {
+ return TryLoadTextureDirectly(name, bounds);
+ }
+ }
+
+ private Texture2DRegion TryLoadTextureDirectly(string name, Rectangle bounds)
+ {
+ if(_content?.ServiceProvider == null)
+ {
+ return null;
+ }
+
+ IGraphicsDeviceService graphicsDeviceService = _content.ServiceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService;
+ if(graphicsDeviceService?.GraphicsDevice == null)
+ {
+ return null;
+ }
+
+ // Try common image extensions
+ string[] extensions = { ".png", ".jpg", ".jpeg", ".bmp" };
+ string baseDirectory = _content.RootDirectory;
+
+ foreach(string extension in extensions)
+ {
+ string filePath = Path.Combine(baseDirectory, name + extension);
+
+ if(File.Exists(filePath))
+ {
+ try
+ {
+ Texture2D texture = Texture2D.FromFile(graphicsDeviceService.GraphicsDevice, filePath);
+ texture.Name = name;
+ return new Texture2DRegion(texture, bounds);
+ }
+ catch
+ {
+ // Continue to next extension
+ continue;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private ParticleReleaseParameters ReadParticleReleaseParameters(XmlReader reader)
+ {
+ ParticleReleaseParameters parameters = new ParticleReleaseParameters();
+
+ using XmlReader subtree = reader.ReadSubtree();
+ while (subtree.Read())
+ {
+ if (subtree.NodeType == XmlNodeType.Element)
+ {
+ switch (subtree.LocalName)
+ {
+ case nameof(ParticleReleaseParameters.Quantity):
+ parameters.Quantity = ReadParticleInt32Parameter(subtree);
+ break;
+
+ case nameof(ParticleReleaseParameters.Speed):
+ parameters.Speed = ReadParticleFloatParameter(subtree);
+ break;
+
+ case nameof(ParticleReleaseParameters.Color):
+ parameters.Color = ReadParticleColorParameter(subtree);
+ break;
+
+ case nameof(ParticleReleaseParameters.Opacity):
+ parameters.Opacity = ReadParticleFloatParameter(subtree);
+ break;
+
+ case nameof(ParticleReleaseParameters.Scale):
+ parameters.Scale = ReadParticleFloatParameter(subtree);
+ break;
+
+ case nameof(ParticleReleaseParameters.Rotation):
+ parameters.Rotation = ReadParticleFloatParameter(subtree);
+ break;
+
+ case nameof(ParticleReleaseParameters.Mass):
+ parameters.Mass = ReadParticleFloatParameter(subtree);
+ break;
+ }
+ }
+ }
+
+ return parameters;
+ }
+
+ private ParticleInt32Parameter ReadParticleInt32Parameter(XmlReader reader)
+ {
+ ParticleValueKind kind = reader.GetAttributeEnum(nameof(ParticleInt32Parameter.Kind));
+
+ if (kind == ParticleValueKind.Constant)
+ {
+ int value = reader.GetAttributeInt(nameof(ParticleInt32Parameter.Constant));
+ return new ParticleInt32Parameter(value);
+ }
+ else if (kind == ParticleValueKind.Random)
+ {
+ int min = reader.GetAttributeInt(nameof(ParticleInt32Parameter.RandomMin));
+ int max = reader.GetAttributeInt(nameof(ParticleInt32Parameter.RandomMax));
+ return new ParticleInt32Parameter(min, max);
+ }
+
+ return new ParticleInt32Parameter(0);
+ }
+
+ private ParticleFloatParameter ReadParticleFloatParameter(XmlReader reader)
+ {
+ ParticleValueKind kind = reader.GetAttributeEnum(nameof(ParticleFloatParameter.Kind));
+
+ if (kind == ParticleValueKind.Constant)
+ {
+ float value = reader.GetAttributeFloat(nameof(ParticleFloatParameter.Constant));
+ return new ParticleFloatParameter(value);
+ }
+ else if (kind == ParticleValueKind.Random)
+ {
+ float min = reader.GetAttributeFloat(nameof(ParticleFloatParameter.RandomMin));
+ float max = reader.GetAttributeFloat(nameof(ParticleFloatParameter.RandomMax));
+ return new ParticleFloatParameter(min, max);
+ }
+
+ return new ParticleFloatParameter(0);
+ }
+
+ private ParticleColorParameter ReadParticleColorParameter(XmlReader reader)
+ {
+ ParticleValueKind kind = reader.GetAttributeEnum(nameof(ParticleColorParameter.Kind));
+
+ if (kind == ParticleValueKind.Constant)
+ {
+ Vector3 value = reader.GetAttributeVector3(nameof(ParticleColorParameter.Constant));
+ return new ParticleColorParameter(value);
+ }
+ else if (kind == ParticleValueKind.Random)
+ {
+ Vector3 min = reader.GetAttributeVector3(nameof(ParticleColorParameter.RandomMin));
+ Vector3 max = reader.GetAttributeVector3(nameof(ParticleColorParameter.RandomMax));
+ return new ParticleColorParameter(min, max);
+ }
+
+ return new ParticleColorParameter(Vector3.Zero);
+ }
+
+ private Profile ReadProfile(XmlReader reader)
+ {
+ string type = reader.GetAttribute(nameof(Type));
+
+ switch (type)
+ {
+ case nameof(BoxProfile):
+ BoxProfile boxProfile = new BoxProfile();
+ boxProfile.Width = reader.GetAttributeFloat(nameof(BoxProfile.Width));
+ boxProfile.Height = reader.GetAttributeFloat(nameof(BoxProfile.Height));
+ return boxProfile;
+
+ case nameof(BoxFillProfile):
+ BoxFillProfile boxFillProfile = new BoxFillProfile();
+ boxFillProfile.Width = reader.GetAttributeFloat(nameof(BoxFillProfile.Width));
+ boxFillProfile.Height = reader.GetAttributeFloat(nameof(BoxFillProfile.Height));
+ return boxFillProfile;
+
+ case nameof(BoxUniformProfile):
+ BoxUniformProfile boxUniformProfile = new BoxUniformProfile();
+ boxUniformProfile.Width = reader.GetAttributeFloat(nameof(BoxUniformProfile.Width));
+ boxUniformProfile.Height = reader.GetAttributeFloat(nameof(BoxUniformProfile.Height));
+ return boxUniformProfile;
+
+ case nameof(CircleProfile):
+ CircleProfile circleProfile = new CircleProfile();
+ circleProfile.Radius = reader.GetAttributeFloat(nameof(CircleProfile.Radius));
+ circleProfile.Radiate = reader.GetAttributeEnum(nameof(CircleProfile.Radiate));
+ return circleProfile;
+
+ case nameof(LineProfile):
+ LineProfile lineProfile = new LineProfile();
+ lineProfile.Axis = reader.GetAttributeVector2(nameof(LineProfile.Axis));
+ lineProfile.Length = reader.GetAttributeFloat(nameof(LineProfile.Length));
+ return lineProfile;
+
+ case nameof(LineUniformProfile):
+ LineUniformProfile lineUniformProfile = new LineUniformProfile();
+ lineUniformProfile.Axis = reader.GetAttributeVector2(nameof(LineUniformProfile.Axis));
+ lineUniformProfile.Length = reader.GetAttributeFloat(nameof(LineUniformProfile.Length));
+ lineUniformProfile.PerpendicularDirection = reader.GetAttributeVector2(nameof(LineUniformProfile.PerpendicularDirection));
+ return lineUniformProfile;
+
+ case nameof(PointProfile):
+ return Profile.Point();
+
+ case nameof(RingProfile):
+ RingProfile ringProfile = new RingProfile();
+ ringProfile.Radius = reader.GetAttributeFloat(nameof(RingProfile.Radius));
+ ringProfile.Radiate = reader.GetAttributeEnum(nameof(RingProfile.Radiate));
+ return ringProfile;
+
+ case nameof(SprayProfile):
+ SprayProfile sprayProfile = new SprayProfile();
+ sprayProfile.Direction = reader.GetAttributeVector2(nameof(SprayProfile.Direction));
+ sprayProfile.Spread = reader.GetAttributeFloat(nameof(SprayProfile.Spread));
+ return sprayProfile;
+
+ default:
+ return new PointProfile();
+ }
+ }
+
+ private void ReadModifiers(XmlReader reader, List modifiers)
+ {
+ using XmlReader subtree = reader.ReadSubtree();
+ while (subtree.Read())
+ {
+ if (subtree.NodeType == XmlNodeType.Element && subtree.LocalName == nameof(Modifier))
+ {
+ Modifier modifier = ReadModifier(subtree);
+
+ if (modifier != null)
+ {
+ modifiers.Add(modifier);
+ }
+ }
+ }
+ }
+
+ private Modifier ReadModifier(XmlReader reader)
+ {
+ string type = reader.GetAttribute(nameof(Type));
+ string name = reader.GetAttribute(nameof(Modifier.Name));
+ float frequency = reader.GetAttributeFloat(nameof(Modifier.Frequency));
+
+ Modifier modifier = type switch
+ {
+ nameof(AgeModifier) => ReadAgeModifier(reader),
+ nameof(DragModifier) => ReadDragModifier(reader),
+ nameof(LinearGravityModifier) => ReadLinearGravityModifier(reader),
+ nameof(OpacityFastFadeModifier) => new OpacityFastFadeModifier(),
+ nameof(RotationModifier) => ReadRotationModifier(reader),
+ nameof(VelocityColorModifier) => ReadVelocityColorModifier(reader),
+ nameof(VelocityModifier) => ReadVelocityModifier(reader),
+ nameof(VortexModifier) => ReadVortexModifier(reader),
+ nameof(CircleContainerModifier) => ReadCircleContainerModifier(reader),
+ nameof(RectangleContainerModifier) => ReadRectangleContainerModifier(reader),
+ nameof(RectangleLoopContainerModifier) => ReadRectangleLoopContainerModifier(reader),
+ _ => null
+ };
+
+ if (modifier != null)
+ {
+ modifier.Name = name;
+ modifier.Frequency = frequency;
+ }
+
+ return modifier;
+ }
+
+ private Modifier ReadAgeModifier(XmlReader reader)
+ {
+ AgeModifier modifier = new AgeModifier();
+
+ using XmlReader subtree = reader.ReadSubtree();
+ while (subtree.Read())
+ {
+ if (subtree.NodeType == XmlNodeType.Element && subtree.LocalName == nameof(AgeModifier.Interpolators))
+ {
+ ReadInterpolators(subtree, modifier.Interpolators);
+ }
+ }
+
+ return modifier;
+ }
+
+ private Modifier ReadDragModifier(XmlReader reader)
+ {
+ DragModifier modifier = new DragModifier();
+ modifier.DragCoefficient = reader.GetAttributeFloat(nameof(DragModifier.DragCoefficient));
+ modifier.Density = reader.GetAttributeFloat(nameof(DragModifier.Density));
+ return modifier;
+ }
+
+ private LinearGravityModifier ReadLinearGravityModifier(XmlReader reader)
+ {
+ LinearGravityModifier modifier = new LinearGravityModifier();
+ modifier.Direction = reader.GetAttributeVector2(nameof(LinearGravityModifier.Direction));
+ modifier.Strength = reader.GetAttributeFloat(nameof(LinearGravityModifier.Strength));
+ return modifier;
+ }
+
+ private Modifier ReadRotationModifier(XmlReader reader)
+ {
+ RotationModifier modifier = new RotationModifier();
+ modifier.RotationRate = reader.GetAttributeFloat(nameof(RotationModifier.RotationRate));
+ return modifier;
+ }
+
+ private Modifier ReadVelocityColorModifier(XmlReader reader)
+ {
+ VelocityColorModifier modifier = new VelocityColorModifier();
+ modifier.StationaryColor = reader.GetAttributeVector3(nameof(VelocityColorModifier.StationaryColor));
+ modifier.VelocityColor = reader.GetAttributeVector3(nameof(VelocityColorModifier.VelocityColor));
+ modifier.VelocityThreshold = reader.GetAttributeFloat(nameof(VelocityColorModifier.VelocityThreshold));
+ return modifier;
+ }
+
+ private Modifier ReadVelocityModifier(XmlReader reader)
+ {
+ VelocityModifier modifier = new VelocityModifier();
+ modifier.VelocityThreshold = reader.GetAttributeFloat(nameof(VelocityModifier.VelocityThreshold));
+
+ using XmlReader subtree = reader.ReadSubtree();
+ while (subtree.Read())
+ {
+ if (subtree.NodeType == XmlNodeType.Element && subtree.LocalName == nameof(VelocityModifier.Interpolators))
+ {
+ ReadInterpolators(subtree, modifier.Interpolators);
+ }
+ }
+
+ return modifier;
+ }
+
+ private Modifier ReadVortexModifier(XmlReader reader)
+ {
+ VortexModifier modifier = new VortexModifier();
+ modifier.Mass = reader.GetAttributeFloat(nameof(VortexModifier.Mass));
+ modifier.MaxSpeed = reader.GetAttributeFloat(nameof(VortexModifier.MaxSpeed));
+ return modifier;
+ }
+
+ private CircleContainerModifier ReadCircleContainerModifier(XmlReader reader)
+ {
+ CircleContainerModifier modifier = new CircleContainerModifier();
+ modifier.Radius = reader.GetAttributeFloat(nameof(CircleContainerModifier.Radius));
+ modifier.Inside = reader.GetAttributeBool(nameof(CircleContainerModifier.Inside));
+ modifier.RestitutionCoefficient = reader.GetAttributeFloat(nameof(CircleContainerModifier.RestitutionCoefficient));
+ return modifier;
+ }
+
+ private Modifier ReadRectangleContainerModifier(XmlReader reader)
+ {
+ RectangleContainerModifier modifier = new RectangleContainerModifier();
+ modifier.Width = reader.GetAttributeInt(nameof(RectangleContainerModifier.Width));
+ modifier.Height = reader.GetAttributeInt(nameof(RectangleContainerModifier.Height));
+ modifier.RestitutionCoefficient = reader.GetAttributeFloat(nameof(RectangleContainerModifier.RestitutionCoefficient));
+ return modifier;
+ }
+
+ private Modifier ReadRectangleLoopContainerModifier(XmlReader reader)
+ {
+ RectangleLoopContainerModifier modifier = new RectangleLoopContainerModifier();
+ modifier.Width = reader.GetAttributeInt(nameof(RectangleLoopContainerModifier.Width));
+ modifier.Height = reader.GetAttributeInt(nameof(RectangleLoopContainerModifier.Height));
+ return modifier;
+ }
+
+ private void ReadInterpolators(XmlReader reader, List interpolators)
+ {
+ using XmlReader subtree = reader.ReadSubtree();
+ while (subtree.Read())
+ {
+ if (subtree.NodeType == XmlNodeType.Element && subtree.LocalName == nameof(Interpolator))
+ {
+ Interpolator interpolator = ReadInterpolator(subtree);
+
+ if (interpolator != null)
+ {
+ interpolators.Add(interpolator);
+ }
+ }
+ }
+ }
+
+ private Interpolator ReadInterpolator(XmlReader reader)
+ {
+ string type = reader.GetAttribute(nameof(Type));
+ string name = reader.GetAttribute(nameof(Interpolator.Name));
+
+ switch (type)
+ {
+ case nameof(ColorInterpolator):
+ ColorInterpolator colorInterpolator = new ColorInterpolator();
+ colorInterpolator.StartValue = reader.GetAttributeVector3(nameof(ColorInterpolator.StartValue));
+ colorInterpolator.EndValue = reader.GetAttributeVector3(nameof(ColorInterpolator.EndValue));
+ return colorInterpolator;
+
+ case nameof(HueInterpolator):
+ HueInterpolator hueInterpolator = new HueInterpolator();
+ hueInterpolator.StartValue = reader.GetAttributeFloat(nameof(HueInterpolator.StartValue));
+ hueInterpolator.EndValue = reader.GetAttributeFloat(nameof(HueInterpolator.EndValue));
+ return hueInterpolator;
+
+ case nameof(OpacityInterpolator):
+ OpacityInterpolator opacityInterpolator = new OpacityInterpolator();
+ opacityInterpolator.StartValue = reader.GetAttributeFloat(nameof(OpacityInterpolator.StartValue));
+ opacityInterpolator.EndValue = reader.GetAttributeFloat(nameof(OpacityInterpolator.EndValue));
+ return opacityInterpolator;
+
+ case nameof(RotationInterpolator):
+ RotationInterpolator rotationInterpolator = new RotationInterpolator();
+ rotationInterpolator.StartValue = reader.GetAttributeFloat(nameof(RotationInterpolator.StartValue));
+ rotationInterpolator.EndValue = reader.GetAttributeFloat(nameof(RotationInterpolator.EndValue));
+ return rotationInterpolator;
+
+ case nameof(ScaleInterpolator):
+ ScaleInterpolator scaleInterpolator = new ScaleInterpolator();
+ scaleInterpolator.StartValue = reader.GetAttributeFloat(nameof(ScaleInterpolator.StartValue));
+ scaleInterpolator.EndValue = reader.GetAttributeFloat(nameof(ScaleInterpolator.EndValue));
+ return scaleInterpolator;
+
+ case nameof(VelocityInterpolator):
+ VelocityInterpolator velocityInterpolator = new VelocityInterpolator();
+ velocityInterpolator.StartValue = reader.GetAttributeVector2(nameof(VelocityInterpolator.StartValue));
+ velocityInterpolator.EndValue = reader.GetAttributeVector2(nameof(VelocityInterpolator.EndValue));
+ return velocityInterpolator;
+
+ default:
+ return null;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ _reader?.Dispose();
+
+ IsDisposed = true;
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/ParticleEffectWriter.cs b/source/MonoGame.Extended/Particles/ParticleEffectWriter.cs
new file mode 100644
index 00000000..f950b125
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/ParticleEffectWriter.cs
@@ -0,0 +1,435 @@
+using System;
+using System.IO;
+using System.Xml;
+using MonoGame.Extended.Graphics;
+using MonoGame.Extended.Particles.Data;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Particles.Modifiers.Containers;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+using MonoGame.Extended.Particles.Profiles;
+using MonoGame.Extended.Serialization.Xml;
+
+namespace MonoGame.Extended.Particles;
+
+///
+/// Represents a writer that serializes a to an XML configuration.
+///
+public class ParticleEffectWriter : IDisposable
+{
+ private readonly XmlWriter _writer;
+
+ ///
+ /// Gets a value that indicates if this has been disposed of.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class that writers to the writes to a file.
+ ///
+ /// The file path to write the XML to.
+ public ParticleEffectWriter(string fileName)
+ {
+ XmlWriterSettings settings = new XmlWriterSettings();
+ settings.Indent = true;
+ settings.IndentChars = " ";
+ settings.CloseOutput = true;
+ settings.NewLineChars = "\n";
+
+ _writer = XmlWriter.Create(fileName, settings);
+ }
+
+ ///
+ /// Initializes a new instance of the class that writes to a stream.
+ ///
+ /// The stream to write to.
+ public ParticleEffectWriter(Stream stream)
+ {
+ XmlWriterSettings settings = new XmlWriterSettings();
+ settings.Indent = true;
+ settings.IndentChars = " ";
+ settings.CloseOutput = false;
+ settings.NewLineChars = "\n";
+
+ _writer = XmlWriter.Create(stream, settings);
+ }
+
+ ///
+ ~ParticleEffectWriter()
+ {
+ Dispose();
+ }
+
+ ///
+ /// Write a to the XML output.
+ ///
+ /// The to serialize.
+ /// is .
+ public void WriteParticleEffect(ParticleEffect effect)
+ {
+ ArgumentNullException.ThrowIfNull(effect);
+
+ _writer.WriteStartDocument();
+ _writer.WriteStartElement("ParticleEffect");
+
+ _writer.WriteAttributeString(nameof(ParticleEffect.Name), effect.Name);
+ _writer.WriteAttributeVector2(nameof(ParticleEffect.Position), effect.Position);
+ _writer.WriteAttributeFloat(nameof(ParticleEffect.Rotation), effect.Rotation);
+ _writer.WriteAttributeVector2(nameof(ParticleEffect.Scale), effect.Scale);
+
+ if (effect.Emitters.Count > 0)
+ {
+ _writer.WriteStartElement(nameof(ParticleEffect.Emitters));
+ foreach (ParticleEmitter emitter in effect.Emitters)
+ {
+ _writer.WriteStartElement(nameof(ParticleEmitter));
+ WriteParticleEmitter(emitter);
+ _writer.WriteEndElement();
+ }
+ _writer.WriteEndElement();
+ }
+
+ _writer.WriteEndElement();
+ _writer.WriteEndDocument();
+
+ _writer.Flush();
+ }
+
+ private void WriteParticleEmitter(ParticleEmitter emitter)
+ {
+ _writer.WriteAttributeString(nameof(ParticleEmitter.Name), emitter.Name);
+ _writer.WriteAttributeFloat(nameof(ParticleEmitter.LifeSpan), emitter.LifeSpan);
+ _writer.WriteAttributeVector2(nameof(ParticleEmitter.Offset), emitter.Offset);
+ _writer.WriteAttributeFloat(nameof(ParticleEmitter.LayerDepth), emitter.LayerDepth);
+ _writer.WriteAttributeBool(nameof(ParticleEmitter.AutoTrigger), emitter.AutoTrigger);
+ _writer.WriteAttributeFloat(nameof(ParticleEmitter.AutoTriggerFrequency), emitter.AutoTriggerFrequency);
+ _writer.WriteAttributeFloat(nameof(ParticleEmitter.ReclaimFrequency), emitter.ReclaimFrequency);
+ _writer.WriteAttributeInt(nameof(ParticleEmitter.Capacity), emitter.Capacity);
+ _writer.WriteAttributeString(nameof(ParticleEmitter.ModifierExecutionStrategy), emitter.ModifierExecutionStrategy.ToString());
+ _writer.WriteAttributeString(nameof(ParticleEmitter.RenderingOrder), emitter.RenderingOrder.ToString());
+
+ if (emitter.TextureRegion is Texture2DRegion region)
+ {
+ _writer.WriteStartElement(nameof(ParticleEmitter.TextureRegion));
+ WriteTexture2DRegion(region);
+ _writer.WriteEndElement();
+ }
+
+ _writer.WriteStartElement(nameof(ParticleEmitter.Parameters));
+ WriteParticleReleaseParameters(emitter.Parameters);
+ _writer.WriteEndElement();
+
+ _writer.WriteStartElement(nameof(ParticleEmitter.Profile));
+ WriteProfile(emitter.Profile);
+ _writer.WriteEndElement();
+
+
+ if (emitter.Modifiers.Count > 0)
+ {
+ _writer.WriteStartElement(nameof(ParticleEmitter.Modifiers));
+ foreach (Modifier modifier in emitter.Modifiers)
+ {
+ _writer.WriteStartElement(nameof(Modifier));
+ WriteModifier(modifier);
+ _writer.WriteEndElement();
+ }
+ _writer.WriteEndElement();
+ }
+ }
+
+ private void WriteTexture2DRegion(Texture2DRegion region)
+ {
+ _writer.WriteAttributeString(nameof(Texture2DRegion.Texture.Name), region.Texture.Name);
+ _writer.WriteAttributeRectangle(nameof(Texture2DRegion.Texture.Bounds), region.Texture.Bounds);
+
+ }
+
+ private void WriteParticleReleaseParameters(ParticleReleaseParameters parameters)
+ {
+ WriteParticleInt32Parameter(nameof(ParticleReleaseParameters.Quantity), parameters.Quantity);
+ WriteParticleFloatParameter(nameof(ParticleReleaseParameters.Speed), parameters.Speed);
+ WriteParticleColorParameter(nameof(ParticleReleaseParameters.Color), parameters.Color);
+ WriteParticleFloatParameter(nameof(ParticleReleaseParameters.Opacity), parameters.Opacity);
+ WriteParticleFloatParameter(nameof(ParticleReleaseParameters.Scale), parameters.Scale);
+ WriteParticleFloatParameter(nameof(ParticleReleaseParameters.Rotation), parameters.Rotation);
+ WriteParticleFloatParameter(nameof(ParticleReleaseParameters.Mass), parameters.Mass);
+
+ }
+
+ private void WriteParticleInt32Parameter(string name, ParticleInt32Parameter parameter)
+ {
+ _writer.WriteStartElement(name);
+ _writer.WriteAttributeString(nameof(ParticleInt32Parameter.Kind), parameter.Kind.ToString());
+
+ if (parameter.Kind == ParticleValueKind.Constant)
+ {
+ _writer.WriteAttributeInt(nameof(ParticleInt32Parameter.Constant), parameter.Constant);
+ }
+ else
+ {
+ _writer.WriteAttributeInt(nameof(ParticleInt32Parameter.RandomMin), parameter.RandomMin);
+ _writer.WriteAttributeInt(nameof(ParticleInt32Parameter.RandomMax), parameter.RandomMax);
+ }
+
+ _writer.WriteEndElement();
+ }
+
+ private void WriteParticleFloatParameter(string name, ParticleFloatParameter parameter)
+ {
+ _writer.WriteStartElement(name);
+ _writer.WriteAttributeString(nameof(ParticleFloatParameter.Kind), parameter.Kind.ToString());
+
+ if (parameter.Kind == ParticleValueKind.Constant)
+ {
+ _writer.WriteAttributeFloat(nameof(ParticleFloatParameter.Constant), parameter.Constant);
+ }
+ else
+ {
+ _writer.WriteAttributeFloat(nameof(ParticleFloatParameter.RandomMin), parameter.RandomMin);
+ _writer.WriteAttributeFloat(nameof(ParticleFloatParameter.RandomMax), parameter.RandomMax);
+ }
+
+ _writer.WriteEndElement();
+ }
+
+ private void WriteParticleColorParameter(string name, ParticleColorParameter parameter)
+ {
+ _writer.WriteStartElement(name);
+ _writer.WriteAttributeString(nameof(ParticleColorParameter.Kind), parameter.Kind.ToString());
+
+ if (parameter.Kind == ParticleValueKind.Constant)
+ {
+ _writer.WriteAttributeVector3(nameof(ParticleColorParameter.Constant), parameter.Constant);
+ }
+ else
+ {
+ _writer.WriteAttributeVector3(nameof(ParticleColorParameter.RandomMin), parameter.RandomMin);
+ _writer.WriteAttributeVector3(nameof(ParticleColorParameter.RandomMax), parameter.RandomMax);
+ }
+
+ _writer.WriteEndElement();
+ }
+
+ private void WriteProfile(Profile profile)
+ {
+ switch (profile)
+ {
+ case BoxFillProfile boxFillProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(BoxFillProfile));
+ _writer.WriteAttributeFloat(nameof(BoxFillProfile.Width), boxFillProfile.Width);
+ _writer.WriteAttributeFloat(nameof(BoxFillProfile.Height), boxFillProfile.Height);
+ break;
+
+ case BoxProfile boxProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(BoxProfile));
+ _writer.WriteAttributeFloat(nameof(BoxProfile.Width), boxProfile.Width);
+ _writer.WriteAttributeFloat(nameof(BoxProfile.Height), boxProfile.Height);
+ break;
+
+ case BoxUniformProfile boxUniformProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(BoxUniformProfile));
+ _writer.WriteAttributeFloat(nameof(BoxUniformProfile.Width), boxUniformProfile.Width);
+ _writer.WriteAttributeFloat(nameof(BoxUniformProfile.Height), boxUniformProfile.Height);
+ break;
+
+ case CircleProfile circleProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(CircleProfile));
+ _writer.WriteAttributeFloat(nameof(CircleProfile.Radius), circleProfile.Radius);
+ _writer.WriteAttributeString(nameof(CircleProfile.Radiate), circleProfile.Radiate.ToString());
+ break;
+
+ case LineProfile lineProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(LineProfile));
+ _writer.WriteAttributeVector2(nameof(LineProfile.Axis), lineProfile.Axis);
+ _writer.WriteAttributeFloat(nameof(LineProfile.Length), lineProfile.Length);
+ break;
+
+ case LineUniformProfile lineUniformProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(LineUniformProfile));
+ _writer.WriteAttributeVector2(nameof(LineUniformProfile.Axis), lineUniformProfile.Axis);
+ _writer.WriteAttributeFloat(nameof(LineUniformProfile.Length), lineUniformProfile.Length);
+ _writer.WriteAttributeVector2(nameof(LineUniformProfile.PerpendicularDirection), lineUniformProfile.PerpendicularDirection);
+ break;
+
+ case PointProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(PointProfile));
+ break;
+
+ case RingProfile ringProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(RingProfile));
+ _writer.WriteAttributeFloat(nameof(RingProfile.Radius), ringProfile.Radius);
+ _writer.WriteAttributeString(nameof(RingProfile.Radiate), ringProfile.Radiate.ToString());
+ break;
+
+ case SprayProfile sprayProfile:
+ _writer.WriteAttributeString(nameof(Type), nameof(SprayProfile));
+ _writer.WriteAttributeVector2(nameof(SprayProfile.Direction), sprayProfile.Direction);
+ _writer.WriteAttributeFloat(nameof(SprayProfile.Spread), sprayProfile.Spread);
+ break;
+
+ default:
+ _writer.WriteAttributeString(nameof(Type), nameof(PointProfile));
+ break;
+ }
+ }
+
+ private void WriteModifier(Modifier modifier)
+ {
+ _writer.WriteAttributeString(nameof(Modifier.Name), modifier.Name);
+ _writer.WriteAttributeFloat(nameof(Modifier.Frequency), modifier.Frequency);
+
+ switch (modifier)
+ {
+ case AgeModifier ageModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(AgeModifier));
+ if (ageModifier.Interpolators.Count > 0)
+ {
+ _writer.WriteStartElement(nameof(AgeModifier.Interpolators));
+ foreach (Interpolator interpolator in ageModifier.Interpolators)
+ {
+ _writer.WriteStartElement(nameof(Interpolator));
+ WriteInterpolator(interpolator);
+ _writer.WriteEndElement();
+ }
+ _writer.WriteEndElement();
+ }
+ break;
+
+ case CircleContainerModifier circleContainerModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(CircleContainerModifier));
+ _writer.WriteAttributeFloat(nameof(CircleContainerModifier.Radius), circleContainerModifier.Radius);
+ _writer.WriteAttributeBool(nameof(CircleContainerModifier.Inside), circleContainerModifier.Inside);
+ _writer.WriteAttributeFloat(nameof(CircleContainerModifier.RestitutionCoefficient), circleContainerModifier.RestitutionCoefficient);
+ break;
+
+ case DragModifier dragModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(DragModifier));
+ _writer.WriteAttributeFloat(nameof(DragModifier.DragCoefficient), dragModifier.DragCoefficient);
+ _writer.WriteAttributeFloat(nameof(DragModifier.Density), dragModifier.Density);
+ break;
+
+ case LinearGravityModifier linearGravityModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(LinearGravityModifier));
+ _writer.WriteAttributeVector2(nameof(LinearGravityModifier.Direction), linearGravityModifier.Direction);
+ _writer.WriteAttributeFloat(nameof(LinearGravityModifier.Strength), linearGravityModifier.Strength);
+ break;
+
+ case OpacityFastFadeModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(OpacityFastFadeModifier));
+ break;
+
+ case RectangleContainerModifier rectangleContainerModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(RectangleContainerModifier));
+ _writer.WriteAttributeInt(nameof(RectangleContainerModifier.Width), rectangleContainerModifier.Width);
+ _writer.WriteAttributeInt(nameof(RectangleContainerModifier.Height), rectangleContainerModifier.Height);
+ _writer.WriteAttributeFloat(nameof(RectangleContainerModifier.RestitutionCoefficient), rectangleContainerModifier.RestitutionCoefficient);
+ break;
+
+ case RectangleLoopContainerModifier rectangleLoopContainerModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(RectangleLoopContainerModifier));
+ _writer.WriteAttributeInt(nameof(RectangleLoopContainerModifier.Width), rectangleLoopContainerModifier.Width);
+ _writer.WriteAttributeInt(nameof(RectangleLoopContainerModifier.Height), rectangleLoopContainerModifier.Height);
+ break;
+
+ case RotationModifier rotationModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(RotationModifier));
+ _writer.WriteAttributeFloat(nameof(RotationModifier.RotationRate), rotationModifier.RotationRate);
+ break;
+
+ case VelocityColorModifier velocityColorModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(VelocityColorModifier));
+ _writer.WriteAttributeVector3(nameof(VelocityColorModifier.StationaryColor), velocityColorModifier.StationaryColor);
+ _writer.WriteAttributeVector3(nameof(VelocityColorModifier.VelocityColor), velocityColorModifier.VelocityColor);
+ _writer.WriteAttributeFloat(nameof(VelocityColorModifier.VelocityThreshold), velocityColorModifier.VelocityThreshold);
+ break;
+
+ case VelocityModifier velocityModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(VelocityModifier));
+ _writer.WriteAttributeFloat(nameof(VelocityModifier.VelocityThreshold), velocityModifier.VelocityThreshold);
+ if (velocityModifier.Interpolators.Count > 0)
+ {
+ _writer.WriteStartElement(nameof(VelocityModifier.Interpolators));
+ foreach (Interpolator interpolator in velocityModifier.Interpolators)
+ {
+ _writer.WriteStartElement(nameof(Interpolator));
+ WriteInterpolator(interpolator);
+ _writer.WriteEndElement();
+ }
+ _writer.WriteEndElement();
+ }
+ break;
+
+ case VortexModifier vortexModifier:
+ _writer.WriteAttributeString(nameof(Type), nameof(VortexModifier));
+ _writer.WriteAttributeVector2(nameof(VortexModifier.Position), vortexModifier.Position);
+ _writer.WriteAttributeFloat(nameof(VortexModifier.Mass), vortexModifier.Mass);
+ _writer.WriteAttributeFloat(nameof(VortexModifier.MaxSpeed), vortexModifier.MaxSpeed);
+ break;
+
+ default:
+ _writer.WriteAttributeString(nameof(Type), "Unknown");
+ break;
+ }
+ }
+
+ private void WriteInterpolator(Interpolator interpolator)
+ {
+ _writer.WriteAttributeString(nameof(interpolator.Name), interpolator.Name);
+
+ switch (interpolator)
+ {
+ case ColorInterpolator colorInterpolator:
+ _writer.WriteAttributeString(nameof(Type), nameof(ColorInterpolator));
+ _writer.WriteAttributeVector3(nameof(ColorInterpolator.StartValue), colorInterpolator.StartValue);
+ _writer.WriteAttributeVector3(nameof(ColorInterpolator.EndValue), colorInterpolator.EndValue);
+ break;
+
+ case HueInterpolator hueInterpolator:
+ _writer.WriteAttributeString(nameof(Type), nameof(HueInterpolator));
+ _writer.WriteAttributeFloat(nameof(HueInterpolator.StartValue), hueInterpolator.StartValue);
+ _writer.WriteAttributeFloat(nameof(HueInterpolator.EndValue), hueInterpolator.EndValue);
+ break;
+
+ case OpacityInterpolator opacityInterpolator:
+ _writer.WriteAttributeString(nameof(Type), nameof(OpacityInterpolator));
+ _writer.WriteAttributeFloat(nameof(OpacityInterpolator.StartValue), opacityInterpolator.StartValue);
+ _writer.WriteAttributeFloat(nameof(OpacityInterpolator.EndValue), opacityInterpolator.EndValue);
+ break;
+
+ case RotationInterpolator rotationInterpolator:
+ _writer.WriteAttributeString(nameof(Type), nameof(RotationInterpolator));
+ _writer.WriteAttributeFloat(nameof(RotationInterpolator.StartValue), rotationInterpolator.StartValue);
+ _writer.WriteAttributeFloat(nameof(RotationInterpolator.EndValue), rotationInterpolator.EndValue);
+ break;
+
+ case ScaleInterpolator scaleInterpolator:
+ _writer.WriteAttributeString(nameof(Type), nameof(ScaleInterpolator));
+ _writer.WriteAttributeFloat(nameof(ScaleInterpolator.StartValue), scaleInterpolator.StartValue);
+ _writer.WriteAttributeFloat(nameof(ScaleInterpolator.EndValue), scaleInterpolator.EndValue);
+ break;
+
+ case VelocityInterpolator velocityInterpolator:
+ _writer.WriteAttributeString(nameof(Type), nameof(VelocityInterpolator));
+ _writer.WriteAttributeVector2(nameof(VelocityInterpolator.StartValue), velocityInterpolator.StartValue);
+ _writer.WriteAttributeVector2(nameof(VelocityInterpolator.EndValue), velocityInterpolator.EndValue);
+ break;
+
+ default:
+ _writer.WriteAttributeString(nameof(Type), "Unknown");
+ break;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ _writer.Dispose();
+ GC.SuppressFinalize(this);
+
+ IsDisposed = true;
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/ParticleEmitter.cs b/source/MonoGame.Extended/Particles/ParticleEmitter.cs
index 211fad77..a9f200fc 100644
--- a/source/MonoGame.Extended/Particles/ParticleEmitter.cs
+++ b/source/MonoGame.Extended/Particles/ParticleEmitter.cs
@@ -1,258 +1,442 @@
-using System;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.Text.Json.Serialization;
using Microsoft.Xna.Framework;
using MonoGame.Extended.Graphics;
+using MonoGame.Extended.Particles.Data;
using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Particles.Primitives;
using MonoGame.Extended.Particles.Profiles;
-namespace MonoGame.Extended.Particles
+namespace MonoGame.Extended.Particles;
+
+///
+/// Represents a particle emitter that creates, manages, and updates particles within a particle system.
+///
+///
+/// The class is the core component of the particle system. It handles particle
+/// creation (triggering), lifecycle management, and application of modifiers according to defined profiles
+/// and parameters. Each emitter operates independently and can be configured with different behaviors, appearances,
+/// and physical properties.
+///
+public sealed unsafe class ParticleEmitter : IDisposable
{
- public unsafe class ParticleEmitter : IDisposable
+ private float _totalSeconds;
+ private float _secondsSinceLastReclaim;
+ private float _nextAutoTrigger;
+
+ ///
+ /// Gets or sets the buffer that stores and manages the particles for this emitter.
+ ///
+ public ParticleBuffer Buffer;
+
+ ///
+ /// Gets or sets the name of this emitter, used for identification and debugging.
+ ///
+ public string Name;
+
+ ///
+ /// Gets the maximum number of particles that this emitter can manage.
+ ///
+ /// The size of the underlying .
+ public int Capacity
{
- private readonly FastRandom _random = new FastRandom(Math.Abs(Guid.NewGuid().GetHashCode()));
- private float _totalSeconds;
-
- [JsonConstructor]
- public ParticleEmitter(string name, Texture2DRegion textureRegion, int capacity, TimeSpan lifeSpan, Profile profile)
+ get
{
- if (profile == null)
- throw new ArgumentNullException(nameof(profile));
+ return Buffer.Size;
+ }
+ }
- _lifeSpanSeconds = (float)lifeSpan.TotalSeconds;
+ ///
+ /// Gets the current number of active particles in this emitter.
+ ///
+ /// The count of particles in the underlying .
+ public int ActiveParticles
+ {
+ get
+ {
+ return Buffer.Count;
+ }
+ }
- Name = name;
- TextureRegion = textureRegion;
- Buffer = new ParticleBuffer(capacity);
- Offset = Vector2.Zero;
- Profile = profile;
- Modifiers = new List();
- ModifierExecutionStrategy = ParticleModifierExecutionStrategy.Serial;
- Parameters = new ParticleReleaseParameters();
+ ///
+ /// Gets or sets the lifespan of particles emitted by this emitter, in seconds.
+ ///
+ ///
+ /// After a particle's age exceeds this value, it will be automatically reclaimed during the next cleanup cycle.
+ ///
+ public float LifeSpan;
+
+ ///
+ /// Gets or sets the position offset applied to this emitter.
+ ///
+ ///
+ /// This offset is applied to the emitter's position when triggering particles, allowing for fine adjustment
+ /// of the emission point without changing the overall position passed to the method.
+ ///
+ public Vector2 Offset;
+
+ ///
+ /// Gets or sets the default layer depth for particles emitted by this emitter.
+ ///
+ ///
+ /// This value determines the rendering order of particles relative to other sprites and particles.
+ /// Values range from 0.0 (front) to 1.0 (back).
+ ///
+ public float LayerDepth;
+
+ ///
+ /// Gets or sets a value indicating whether this emitter should automatically trigger particle emissions.
+ ///
+ ///
+ /// When set to , the emitter will periodically emit particles based on the
+ /// property, without requiring explicit calls to .
+ ///
+ public bool AutoTrigger;
+
+ ///
+ /// Gets or sets the frequency, in seconds, at which this emitter automatically triggers particle emissions.
+ ///
+ ///
+ /// This property only has an effect when is set to .
+ ///
+ public float AutoTriggerFrequency;
+
+ ///
+ /// Gets or sets the frequency, in times per second, at which expired particles are reclaimed.
+ ///
+ ///
+ /// Higher values result in more frequent cleanup of expired particles, potentially improving memory
+ /// utilization at the cost of slightly increased CPU usage.
+ ///
+ public float ReclaimFrequency;
+
+ ///
+ /// Gets or sets the parameters that control the physical and visual properties of emitted particles.
+ ///
+ ///
+ /// These parameters include properties such as initial speed, color, opacity, scale, rotation, and mass.
+ ///
+ public ParticleReleaseParameters Parameters;
+
+ ///
+ /// Gets or sets the strategy used to execute modifiers on particles.
+ ///
+ ///
+ /// This determines whether modifiers are executed serially (single-threaded) or in parallel (multi-threaded),
+ /// affecting performance characteristics based on the system's capabilities and the number of particles.
+ ///
+ public ModifierExecutionStrategy ModifierExecutionStrategy;
+
+ ///
+ /// Gets or sets the list of modifiers that affect particles emitted by this emitter.
+ ///
+ ///
+ /// Modifiers alter particle properties over time, creating effects such as gravity, color changes,
+ /// rotation, and containment within boundaries.
+ ///
+ public List Modifiers;
+
+ ///
+ /// Gets or sets the profile that determines the initial position and heading of emitted particles.
+ ///
+ ///
+ /// Profiles define the emission pattern, such as points, lines, rings, or areas from which particles originate.
+ ///
+ public Profile Profile;
+
+ ///
+ /// The to use when rendering particles from this emitter.
+ ///
+ public Texture2DRegion TextureRegion;
+
+ ///
+ /// Gets or sets the order in which particles are rendered within this emitter.
+ ///
+ ///
+ /// This property determines whether particles are drawn front-to-back or back-to-front,
+ /// affecting how they visually overlap when using alpha blending.
+ ///
+ public ParticleRenderingOrder RenderingOrder;
+
+ ///
+ /// Gets a value indicating whether this has been disposed.
+ ///
+ /// if the emitter has been disposed; otherwise, .
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class with default capacity.
+ ///
+ ///
+ /// Creates an emitter with a capacity of 1000 particles and default settings.
+ ///
+ public ParticleEmitter() : this(1000) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified capacity.
+ ///
+ /// The maximum number of particles this emitter can manage.
+ ///
+ /// This constructor initializes the emitter with default settings but allows for specifying
+ /// the maximum number of particles it can handle.
+ ///
+ public ParticleEmitter(int initialCapacity)
+ {
+ LifeSpan = 1.0f;
+ Name = nameof(ParticleEmitter);
+ TextureRegion = null;
+ Buffer = new ParticleBuffer(initialCapacity);
+ Profile = Profile.Point();
+ Modifiers = new List();
+ ModifierExecutionStrategy = ModifierExecutionStrategy.Serial;
+ Parameters = new ParticleReleaseParameters();
+ ReclaimFrequency = 60.0f;
+ Offset = Vector2.Zero;
+ LayerDepth = 0.0f;
+ AutoTrigger = true;
+ AutoTriggerFrequency = 1.0f;
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~ParticleEmitter()
+ {
+ Dispose(false);
+ }
+
+ ///
+ /// Changes the maximum capacity of this emitter.
+ ///
+ /// The new maximum number of particles this emitter can manage.
+ ///
+ /// This method disposes the old buffer and creates a new one with the specified capacity.
+ /// Any existing particles are lost during this operation.
+ ///
+ ///
+ /// Thrown if this method is called after the emitter has been disposed.
+ ///
+ public void ChangeCapacity(int size)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, typeof(ParticleBuffer));
+
+ if (Capacity == size)
+ {
+ return;
}
- public ParticleEmitter(Texture2DRegion textureRegion, int capacity, TimeSpan lifeSpan, Profile profile)
- : this(null, textureRegion, capacity, lifeSpan, profile)
+ if (Buffer is ParticleBuffer oldBuffer)
{
+ oldBuffer.Dispose();
}
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
+ Buffer = new ParticleBuffer(size);
+ }
- protected virtual void Dispose(bool disposing)
+ ///
+ /// Updates the state of all particles managed by this emitter.
+ ///
+ /// The elapsed time, in seconds, since the last update.
+ /// The current position of the emitter in 2D space.
+ ///
+ /// This method handles automatic triggering of particle emissions, updates the positions of all active
+ /// particles based on their velocities, applies all registered modifiers, and reclaims expired particles.
+ ///
+ ///
+ /// Thrown if this method is called after the emitter has been disposed.
+ ///
+ public void Update(float elapsedSeconds, Vector2 position = default)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, typeof(ParticleBuffer));
+
+ _totalSeconds += elapsedSeconds;
+ _secondsSinceLastReclaim += elapsedSeconds;
+
+ if (AutoTrigger)
{
- if(IsDisposed)
+ _nextAutoTrigger -= elapsedSeconds;
+
+ if (_nextAutoTrigger <= 0)
{
- return;
- }
-
- Buffer.Dispose();
- Buffer = null;
- IsDisposed = true;
- }
-
- ~ParticleEmitter()
- {
- Dispose(false);
- }
-
- public string Name { get; set; }
- public int ActiveParticles => Buffer.Count;
- public Vector2 Offset { get; set; }
- public List Modifiers { get; }
- public Profile Profile { get; set; }
- public float LayerDepth { get; set; }
- public ParticleReleaseParameters Parameters { get; set; }
- public Texture2DRegion TextureRegion { get; set; }
-
- ///
- /// Gets a value that indicates whether this instance of the class has been
- /// disposed.
- ///
- public bool IsDisposed { get; private set;}
-
- [EditorBrowsable(EditorBrowsableState.Never)]
- public ParticleModifierExecutionStrategy ModifierExecutionStrategy { get; set; }
-
- internal ParticleBuffer Buffer;
-
- public int Capacity
- {
- get
- {
- ThrowIfDisposed();
- return Buffer.Size;
- }
- set
- {
- ThrowIfDisposed();
-
- var oldBuffer = Buffer;
- oldBuffer.Dispose();
- Buffer = new ParticleBuffer(value);
+ Trigger(position, LayerDepth);
+ _nextAutoTrigger = AutoTriggerFrequency;
}
}
- private float _lifeSpanSeconds;
- public TimeSpan LifeSpan
+ if (Buffer.Count == 0)
{
- get { return TimeSpan.FromSeconds(_lifeSpanSeconds); }
- set { _lifeSpanSeconds = (float) value.TotalSeconds; }
+ return;
}
- private float _nextAutoTrigger;
-
- private bool _autoTrigger = true;
- public bool AutoTrigger
+ if (_secondsSinceLastReclaim > (1.0f / ReclaimFrequency))
{
- 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))
- {
- ThrowIfDisposed();
-
- _totalSeconds += elapsedSeconds;
-
- if (_autoTrigger)
- {
- _nextAutoTrigger -= elapsedSeconds;
-
- if (_nextAutoTrigger <= 0)
- {
- Trigger(position, this.LayerDepth);
- _nextAutoTrigger = _autoTriggerFrequency;
- }
- }
-
- if (Buffer.Count == 0)
- return false;
-
ReclaimExpiredParticles();
+ _secondsSinceLastReclaim -= (1.0f / ReclaimFrequency);
+ }
- var iterator = Buffer.Iterator;
-
+ if (Buffer.Count > 0)
+ {
+ ParticleIterator iterator = Buffer.Iterator;
while (iterator.HasNext)
{
- var particle = iterator.Next();
- particle->Age = (_totalSeconds - particle->Inception) / _lifeSpanSeconds;
- particle->Position = particle->Position + particle->Velocity * elapsedSeconds;
+ Particle* particle = iterator.Next();
+ particle->Age = (_totalSeconds - particle->Inception) / LifeSpan;
+ particle->Position[0] += particle->Velocity[0] * elapsedSeconds;
+ particle->Position[1] += particle->Velocity[1] * 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)
- {
- ThrowIfDisposed();
-
- 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;
- }
- }
-
- private void ThrowIfDisposed()
- {
- if(IsDisposed)
- {
- throw new ObjectDisposedException(nameof(ParticleBuffer));
- }
- }
-
- public override string ToString()
- {
- return Name;
}
}
+
+ ///
+ /// Triggers the emission of particles at the specified position.
+ ///
+ /// The position in 2D space from which to emit particles.
+ /// The layer depth at which to render the emitted particles.
+ ///
+ /// This method creates a burst of particles according to the configured .
+ /// The number of particles released is determined by the property.
+ ///
+ public void Trigger(Vector2 position, float layerDepth = 0)
+ {
+ int numToRelease = Parameters.Quantity.Value;
+ Release(position, numToRelease, layerDepth);
+ }
+
+ ///
+ /// Triggers the emission of particles along a line segment.
+ ///
+ /// The line segment along which to distribute emitted particles.
+ /// The layer depth at which to render the emitted particles.
+ ///
+ /// This method creates particles at random positions along the specified line segment.
+ /// The number of particles released is determined by the property.
+ ///
+ public void Trigger(LineSegment line, float layerDepth = 0)
+ {
+ int numToRelease = Parameters.Quantity.Value;
+ Vector2 lineVector = line.ToVector2();
+
+ for (int i = 0; i < numToRelease; i++)
+ {
+ Vector2 offset = lineVector * FastRandom.Shared.NextSingle();
+ Release(line.Origin + offset, 1, layerDepth);
+ }
+ }
+
+ ///
+ /// Releases a specified number of particles at the given position.
+ ///
+ /// The position in 2D space from which to emit particles.
+ /// The number of particles to release.
+ /// The layer depth at which to render the emitted particles.
+ ///
+ /// This method initializes newly created particles with properties based on the emitter's
+ /// and .
+ ///
+ private void Release(Vector2 position, int numToRelease, float layerDepth)
+ {
+ ParticleIterator iterator = Buffer.Release(numToRelease);
+
+ while (iterator.HasNext)
+ {
+ Particle* particle = iterator.Next();
+
+ Profile.GetOffsetAndHeading((Vector2*)particle->Position, (Vector2*)particle->Velocity);
+
+ particle->Age = 0.0f;
+ particle->Inception = _totalSeconds;
+
+ particle->Position[0] += position.X;
+ particle->Position[1] += position.Y;
+
+ particle->TriggeredPos[0] = position.X;
+ particle->TriggeredPos[1] = position.Y;
+
+ float speed = Parameters.Speed.Value;
+
+ particle->Velocity[0] *= speed;
+ particle->Velocity[1] *= speed;
+
+ Vector3 color = Parameters.Color.Value;
+ particle->Color[0] = color.X;
+ particle->Color[1] = color.Y;
+ particle->Color[2] = color.Z;
+
+ particle->Opacity = Parameters.Opacity.Value;
+ particle->Scale = Parameters.Scale.Value;
+ particle->Rotation = Parameters.Rotation.Value;
+ particle->Mass = Parameters.Mass.Value;
+ particle->LayerDepth = layerDepth;
+ }
+ }
+
+ ///
+ /// Reclaims particles that have exceeded their lifespan.
+ ///
+ ///
+ /// This method removes expired particles from the beginning of the buffer and compacts
+ /// the remaining particles to maintain efficient memory usage.
+ ///
+ private void ReclaimExpiredParticles()
+ {
+ int expired = 0;
+ ParticleIterator iterator = Buffer.Iterator;
+ while (iterator.HasNext)
+ {
+ Particle* particle = iterator.Next();
+
+ if ((_totalSeconds - particle->Inception) < LifeSpan)
+ {
+ break;
+ }
+ expired++;
+ }
+
+ if (expired != 0)
+ {
+ Buffer.Reclaim(expired);
+ }
+ }
+
+ ///
+ /// Returns a string that represents the current emitter.
+ ///
+ /// The of this emitter.
+ public override string ToString()
+ {
+ return Name;
+ }
+
+ ///
+ /// Releases all resources used by the .
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases the unmanaged resources used by the .
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ private void Dispose(bool disposing)
+ {
+ if (IsDisposed) { return; }
+
+ if (disposing)
+ {
+ // No managed objects
+ }
+
+ Buffer.Dispose();
+ IsDisposed = true;
+ }
}
diff --git a/source/MonoGame.Extended/Particles/ParticleExtensions.cs b/source/MonoGame.Extended/Particles/ParticleExtensions.cs
deleted file mode 100644
index b5c17e0c..00000000
--- a/source/MonoGame.Extended/Particles/ParticleExtensions.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-using MonoGame.Extended.Graphics;
-
-namespace MonoGame.Extended.Particles
-{
- public static class ParticleExtensions
- {
- public static void Draw(this SpriteBatch spriteBatch, ParticleEffect effect)
- {
- if(effect.IsDisposed)
- {
- return;
- }
-
- for (var i = 0; i < effect.Emitters.Count; i++)
- UnsafeDraw(spriteBatch, effect.Emitters[i]);
- }
-
- public static void Draw(this SpriteBatch spriteBatch, ParticleEmitter emitter)
- {
- if(emitter.IsDisposed)
- {
- return;
- }
-
- 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;
- color.A = (byte)MathHelper.Clamp(particle->Opacity*255, 0, 255);
- var rotation = particle->Rotation;
- var layerDepth = particle->LayerDepth;
-
- spriteBatch.Draw(textureRegion, position, color, rotation, origin, scale, SpriteEffects.None, layerDepth);
- }
- }
- }
-}
diff --git a/source/MonoGame.Extended/Particles/ParticleIterator.cs b/source/MonoGame.Extended/Particles/ParticleIterator.cs
new file mode 100644
index 00000000..ac88b051
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/ParticleIterator.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles;
+
+///
+/// Provides functionality for iterating through particles in a circular buffer.
+///
+///
+/// The class enables safe traversal of active particles in a
+/// , automatically handling the circular nature of the buffer and wrapping around
+/// boundaries as needed.
+///
+public sealed class ParticleIterator
+{
+ private readonly ParticleBuffer _buffer;
+ private unsafe Particle* _current;
+
+ ///
+ /// Gets the total number of particles that can be iterated over.
+ ///
+ public int Total { get; private set; }
+
+ ///
+ /// Gets a value indicating whether there are more particles to iterate over.
+ ///
+ ///
+ /// if there are more particles available; otherwise, .
+ ///
+ public unsafe bool HasNext => _current != _buffer.Tail;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to iterate over.
+ /// is .
+ /// has previously been disposed.
+ public ParticleIterator(ParticleBuffer buffer)
+ {
+ ArgumentNullException.ThrowIfNull(buffer);
+ ObjectDisposedException.ThrowIf(buffer.IsDisposed, buffer);
+ _buffer = buffer;
+ }
+
+ ///
+ /// Resets the iterator to the beginning of the active particles in the buffer.
+ ///
+ /// This instance.
+ public unsafe ParticleIterator Reset()
+ {
+ _current = _buffer.Head;
+ Total = _buffer.Count;
+ return this;
+ }
+
+ ///
+ /// Resets the iterator to a specific offset position within the active particles.
+ ///
+ /// The number of particles to offset from the head position.
+ /// This instance.
+ internal unsafe ParticleIterator Reset(int offset)
+ {
+ Total = _buffer.Count;
+
+ _current = _buffer.Head + offset;
+
+ if (_current >= _buffer.BufferEnd)
+ {
+ _current -= _buffer.Size + 1;
+ }
+
+ return this;
+ }
+
+ ///
+ /// Advances the iterator to the next particle and returns a pointer to the current particle.
+ ///
+ /// A pointer to the current particle before advancing the iterator.
+ public unsafe Particle* Next()
+ {
+ Particle* particle = _current;
+
+ _current++;
+
+ if (_current == _buffer.BufferEnd)
+ {
+ _current = (Particle*)_buffer.NativePointer;
+ }
+
+ return particle;
+
+ }
+
+
+
+
+}
diff --git a/source/MonoGame.Extended/Particles/ParticleModifierExecutionStrategy.cs b/source/MonoGame.Extended/Particles/ParticleModifierExecutionStrategy.cs
deleted file mode 100644
index 20a4d51c..00000000
--- a/source/MonoGame.Extended/Particles/ParticleModifierExecutionStrategy.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-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 modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator);
-
- internal class SerialModifierExecutionStrategy : ParticleModifierExecutionStrategy
- {
- internal override void ExecuteModifiers(List 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 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/source/MonoGame.Extended/Particles/ParticleReleaseParameters.cs b/source/MonoGame.Extended/Particles/ParticleReleaseParameters.cs
deleted file mode 100644
index 195cbdd3..00000000
--- a/source/MonoGame.Extended/Particles/ParticleReleaseParameters.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Microsoft.Xna.Framework;
-using MonoGame.Extended;
-
-namespace MonoGame.Extended.Particles
-{
- public class ParticleReleaseParameters
- {
- public ParticleReleaseParameters()
- {
- Quantity = 1;
- Speed = new Range(-1f, 1f);
- Color = new Range(HslColor.FromRgb(Microsoft.Xna.Framework.Color.Wheat), HslColor.FromRgb(Microsoft.Xna.Framework.Color.White));
- Opacity = new Range(0f, 1f);
- Scale = new Range(1f, 1f);
- Rotation = new Range(-MathHelper.Pi, MathHelper.Pi);
- Mass = 1f;
- MaintainAspectRatioOnScale = true;
- ScaleX = new Range(1f, 1f);
- ScaleY = new Range(1f, 1f);
- }
-
- public Range Quantity { get; set; }
- public Range Speed { get; set; }
- public Range Color { get; set; }
- public Range Opacity { get; set; }
- public Range Scale { get; set; }
- public Range Rotation { get; set; }
- public Range Mass { get; set; }
- public bool MaintainAspectRatioOnScale { get; set; }
- public Range ScaleX { get; set; }
- public Range ScaleY { get; set; }
-
- }
-}
diff --git a/source/MonoGame.Extended/Particles/ParticleRenderingOrder.cs b/source/MonoGame.Extended/Particles/ParticleRenderingOrder.cs
new file mode 100644
index 00000000..7a564fe4
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/ParticleRenderingOrder.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+namespace MonoGame.Extended.Particles;
+
+///
+/// Specifies the order in which particles are rendered within the particle system.
+///
+///
+/// This enumeration defines the rendering order strategies that can be applied to particles to control how they are
+/// layered visually. The chosen rendering order affects which particles appear in front of others when they overlap.
+///
+public enum ParticleRenderingOrder
+{
+ ///
+ /// Particles are rendered from front to back.
+ ///
+ FrontToBack,
+
+ ///
+ /// Particles are rendered from back to front.
+ ///
+ BackToFront
+}
diff --git a/source/MonoGame.Extended/Particles/Primatives/LineSegment.cs b/source/MonoGame.Extended/Particles/Primatives/LineSegment.cs
new file mode 100644
index 00000000..8fa2a323
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Primatives/LineSegment.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Xna.Framework;
+using System.Runtime.InteropServices;
+
+namespace MonoGame.Extended.Particles.Primitives;
+
+///
+/// Represents a line segment defined by two points in 2D space.
+///
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct LineSegment : IEquatable
+{
+ ///
+ /// The first point of the line segment.
+ ///
+ internal readonly Vector2 _point1;
+
+ ///
+ /// The second point of the line segment.
+ ///
+ internal readonly Vector2 _point2;
+
+ ///
+ /// Gets the origin point of the line segment.
+ ///
+ /// The first point of the line segment.
+ public readonly Vector2 Origin
+ {
+ get
+ {
+ return _point1;
+ }
+ }
+
+ ///
+ /// Gets the direction vector of the line segment.
+ ///
+ /// A vector from the first point to the second point.
+ public readonly Vector2 Direction
+ {
+ get
+ {
+ return _point2 - _point1;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the struct with the specified points.
+ ///
+ /// The first point of the line segment.
+ /// The second point of the line segment.
+ public LineSegment(Vector2 point1, Vector2 point2)
+ {
+ _point1 = point1;
+ _point2 = point2;
+ }
+
+ ///
+ /// Returns a new line segment that is a translated version of this line segment.
+ ///
+ /// The vector by which to translate the line segment.
+ /// A new that is offset by the specified vector.
+ public LineSegment Translate(Vector2 vector)
+ {
+ return new LineSegment(_point1 + vector, _point2 + vector);
+ }
+
+ ///
+ /// Converts the line segment to a vector representing its direction and magnitude.
+ ///
+ ///
+ /// A representing the direction and length of the line segment, calculated as the second
+ /// point minus the first point.
+ ///
+ public Vector2 ToVector2()
+ {
+ return _point2 - _point1;
+ }
+
+ ///
+ /// Creates a new line segment from two points.
+ ///
+ /// The first point of the line segment.
+ /// The second point of the line segment.
+ /// A new defined by the two points.
+ public static LineSegment FromPoints(Vector2 point1, Vector2 point2)
+ {
+ return new LineSegment(point1, point2);
+ }
+
+ ///
+ /// Creates a new line segment from an origin point and a direction vector.
+ ///
+ /// The starting point of the line segment.
+ /// The direction and length vector of the line segment.
+ ///
+ /// A new starting at the origin and extending by the specified vector.
+ ///
+ public static LineSegment FromOrigin(Vector2 origin, Vector2 vector)
+ {
+ return new LineSegment(origin, origin + vector);
+ }
+
+ ///
+ /// Determines whether the specified object is equal to the current line segment.
+ ///
+ /// The object to compare with the current line segment.
+ ///
+ /// if the specified object is a and is equal to the
+ /// current line segment; otherwise, .
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object obj)
+ {
+ return obj is LineSegment other && Equals(other);
+ }
+
+ ///
+ /// Determines whether the specified line segment is equal to the current line segment.
+ ///
+ /// The line segment to compare with the current line segment.
+ ///
+ /// if the specified line segment is equal to the current line segment;
+ /// otherwise, .
+ ///
+ public readonly bool Equals(LineSegment other)
+ {
+ return _point1.Equals(other._point1) && _point2.Equals(other._point2);
+ }
+
+ ///
+ /// Returns the hash code for this line segment.
+ ///
+ /// A 32-bit signed integer that is the hash code for this instance.
+ public override readonly int GetHashCode()
+ {
+ return HashCode.Combine(_point1, _point2);
+ }
+
+ ///
+ /// Returns a string representation of this line segment.
+ ///
+ ///
+ /// A string containing the coordinates of both points in the format:
+ /// "(x1:y1,x2:y2)".
+ ///
+ public override readonly string ToString()
+ {
+ return $"({_point1:x}:{_point1:y},{_point2:x}:{_point2:y})";
+ }
+
+ ///
+ /// Determines whether two line segments are equal.
+ ///
+ /// The first line segment to compare.
+ /// The second line segment to compare.
+ ///
+ /// if the line segments are equal; otherwise, .
+ ///
+ public static bool operator ==(LineSegment lhs, LineSegment rhs)
+ {
+ return lhs.Equals(rhs);
+ }
+
+ ///
+ /// Determines whether two line segments are not equal.
+ ///
+ /// The first line segment to compare.
+ /// The second line segment to compare.
+ ///
+ /// if the line segments are not equal; otherwise, .
+ ///
+ public static bool operator !=(LineSegment lhs, LineSegment rhs)
+ {
+ return !lhs.Equals(rhs);
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/BoxFillProfile.cs b/source/MonoGame.Extended/Particles/Profiles/BoxFillProfile.cs
index e3637135..ef81830a 100644
--- a/source/MonoGame.Extended/Particles/Profiles/BoxFillProfile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/BoxFillProfile.cs
@@ -1,18 +1,40 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Profiles
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that randomly distributes particles throughout a rectangular area.
+///
+///
+/// The generates random positions within a rectangle centered the emitter's position, with
+/// random unit vector headings. This creates a uniform distribution of particles across the defined area, with
+/// particles moving in all directions.
+///
+public sealed class BoxFillProfile : Profile
{
- public class BoxFillProfile : Profile
+ ///
+ /// The width of the rectangular area.
+ ///
+ public float Width;
+
+ ///
+ /// The height of the rectangular area.
+ ///
+ public float Height;
+
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
{
- 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);
- }
+ offset->X = FastRandom.Shared.NextSingle(Width * -0.5f, Width * 0.5f);
+ offset->Y = FastRandom.Shared.NextSingle(Height * -0.5f, Height * 0.5f);
+ FastRandom.Shared.NextUnitVector(heading);
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/BoxProfile.cs b/source/MonoGame.Extended/Particles/Profiles/BoxProfile.cs
index 7d31984c..92cabc6e 100644
--- a/source/MonoGame.Extended/Particles/Profiles/BoxProfile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/BoxProfile.cs
@@ -1,31 +1,61 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Profiles
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that distributes particles along the edges of a rectangular boundary.
+///
+///
+/// The randomly positions new particles on one of the four sides of a rectangular area
+/// centered at the emitter's position. Each side has an equal probability of being selected. Particles are given random
+/// unit vector headings, allowing them to move in any direction regardless of their starting edge.
+///
+public sealed class BoxProfile : Profile
{
- public class BoxProfile : Profile
+ ///
+ /// The width of the rectangular perimeter.
+ ///
+ public float Width;
+
+ ///
+ /// The height of the rectangular perimeter.
+ ///
+ public float Height;
+
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
{
- public float Width { get; set; }
- public float Height { get; set; }
-
- public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ switch (FastRandom.Shared.Next(4))
{
- 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;
- }
+ case 0: // Left
+ offset->X = Width * -0.5f;
+ offset->Y = FastRandom.Shared.NextSingle(Height * -0.5f, Height * 0.5f);
+ break;
- Random.NextUnitVector(out heading);
+ case 1: // Top
+ offset->X = FastRandom.Shared.NextSingle(Width * -0.5f, Width * 0.5f);
+ offset->Y = Height * -0.5f;
+ break;
+
+ case 2: // Right
+ offset->X = Width * 0.5f;
+ offset->Y = FastRandom.Shared.NextSingle(Height * -0.5f, Height * 0.5f);
+ break;
+
+ default: // Bottom
+ offset->X = FastRandom.Shared.NextSingle(Width * -0.5f, Width * 0.5f);
+ offset->Y = Height * 0.5f;
+ break;
}
+
+ FastRandom.Shared.NextUnitVector(heading);
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/BoxUniformProfile.cs b/source/MonoGame.Extended/Particles/Profiles/BoxUniformProfile.cs
index 9f766dcf..3cef853f 100644
--- a/source/MonoGame.Extended/Particles/Profiles/BoxUniformProfile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/BoxUniformProfile.cs
@@ -1,32 +1,79 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Profiles
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that distributes particles along the edges of a rectangular boundary with uniform density.
+///
+///
+///
+/// The positions new particles on the perimeter of a rectangle centered at the
+/// emitter's position. Unlike which gives equal probability to each side, this profile
+/// allocates probability proportional to the length of each side, ensuring a uniform distribution of particles
+/// around the entire perimeter.
+///
+///
+/// This means longer sides will receive more particles than shorter sides, creating a visually balanced
+/// distribution regardless of the rectangle's dimensions.
+///
+///
+/// Particles are given random unit vector headings, allowing them to move in any direction regardless of their
+/// starting edge.
+///
+///
+public class BoxUniformProfile : Profile
{
- public class BoxUniformProfile : Profile
+ ///
+ /// The width of the rectangular perimeter.
+ ///
+ public float Width;
+
+ ///
+ /// The height of the rectangular perimeter.
+ ///
+ public float Height;
+
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
{
- public float Width { get; set; }
- public float Height { get; set; }
+ int perimeter = (int)(2 * Width + 2 * Height);
+ int value = FastRandom.Shared.Next(perimeter);
- public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ switch (value)
{
- var value = Random.Next((int) (2*Width + 2*Height));
+ // Top
+ case var _ when value < Width:
+ offset->X = FastRandom.Shared.NextSingle(Width * -0.5f, Width * 0.5f);
+ offset->Y = Height * -0.5f;
+ break;
- 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));
- }
- }
+ // Bottom
+ case var _ when value < 2 * Width:
+ offset->X = FastRandom.Shared.NextSingle(Width * -0.5f, Width * 0.5f);
+ offset->Y = Height * 0.5f;
+ break;
- Random.NextUnitVector(out heading);
+ // Left
+ case var _ when value < 2 * Width + Height:
+ offset->X = Width * -0.5f;
+ offset->Y = FastRandom.Shared.NextSingle(Height * -0.5f, Height * 0.5f);
+ break;
+
+ // Right
+ default:
+ offset->X = Width * 0.5f;
+ offset->Y = FastRandom.Shared.NextSingle(Height * -0.5f, Height * 0.5f);
+ break;
}
+
+ FastRandom.Shared.NextUnitVector(heading);
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/CircleProfile.cs b/source/MonoGame.Extended/Particles/Profiles/CircleProfile.cs
index ef7f11e7..141ac99f 100644
--- a/source/MonoGame.Extended/Particles/Profiles/CircleProfile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/CircleProfile.cs
@@ -1,24 +1,66 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Profiles
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that distributes particles throughout a circular area with controllable radiation patterns.
+///
+///
+/// The randomly positions new particles within a circle centered at the emitter's position.
+/// The movement direction (heading) of each particle can be configured to radiate inward toward the center, outward
+/// from the center, or in random directions.
+///
+public sealed class CircleProfile : Profile
{
- public class CircleProfile : Profile
+ ///
+ /// The radius of the circular area.
+ ///
+ public float Radius;
+
+ ///
+ /// The radiation mode that determines how particle headings are calculated.
+ ///
+ public CircleRadiation Radiate;
+
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ ///
+ /// Thrown when contains an unsupported value.
+ ///
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
{
- public float Radius { get; set; }
- public CircleRadiation Radiate { get; set; }
+ float distance = FastRandom.Shared.NextSingle(0f, Radius);
- public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ FastRandom.Shared.NextUnitVector(heading);
+
+ switch (Radiate)
{
- var dist = Random.NextSingle(0f, Radius);
+ case CircleRadiation.In:
+ offset->X = -heading->X * distance;
+ offset->Y = -heading->Y * distance;
+ break;
- Random.NextUnitVector(out heading);
+ case CircleRadiation.Out:
+ offset->X = heading->X * distance;
+ offset->Y = heading->Y * distance;
+ break;
- offset = Radiate == CircleRadiation.In
- ? new Vector2(-heading.X*dist, -heading.Y*dist)
- : new Vector2(heading.X*dist, heading.Y*dist);
+ case CircleRadiation.None:
+ offset->X = heading->X * distance;
+ offset->Y = heading->Y * distance;
+ FastRandom.Shared.NextUnitVector(heading);
+ break;
- if (Radiate == CircleRadiation.None)
- Random.NextUnitVector(out heading);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(Radiate), Radiate, "Unsupported radiation mode");
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/CircleRadiation.cs b/source/MonoGame.Extended/Particles/Profiles/CircleRadiation.cs
new file mode 100644
index 00000000..f42b7a26
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Profiles/CircleRadiation.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// Defines the radiation pattern for particles when using a .
+///
+///
+/// This enumeration determines how a particle's initial position within a circle affects its movement direction
+/// (heading). Different radiation patterns can create varied visual effects such as explosions, implosions, or
+/// randomized dispersion.
+///
+public enum CircleRadiation
+{
+ ///
+ /// Particles move in random directions unrelated to their position.
+ ///
+ ///
+ /// In this mode, the initial heading of particles is completely random and has no relationship to their position
+ /// within the circle. This creates a chaotic dispersion effect with no discernible pattern of movement.
+ ///
+ None,
+
+ ///
+ /// Particles move toward the center of the circle.
+ ///
+ ///
+ /// In this mode, particles are given initial headings that point directly toward the center of the circle from
+ /// their starting position. This creates an implosion or suction effect, as if particles are being drawn inward.
+ ///
+ In,
+
+ ///
+ /// Particles move away from the center of the circle.
+ ///
+ ///
+ /// In this mode, particles are given initial headings that point directly away from the center of the circle,
+ /// extending their starting position outward. This creates an explosion or burst effect, as if particles are
+ /// emanating from a central point.
+ ///
+ Out
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/LineProfile.cs b/source/MonoGame.Extended/Particles/Profiles/LineProfile.cs
index bea584e9..cf308224 100644
--- a/source/MonoGame.Extended/Particles/Profiles/LineProfile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/LineProfile.cs
@@ -1,17 +1,41 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Profiles
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that distributes particles uniformly along a line segment with random headings.
+///
+///
+/// The positions particles randomly along a line segment centered at the emitter position and
+/// defined by an axis direction and length. Unlike , this profile gives each particle a
+/// random heading in any direction.
+///
+public sealed class LineProfile : Profile
{
- public class LineProfile : Profile
- {
- public Vector2 Axis { get; set; }
- public float Length { get; set; }
+ ///
+ /// The direction vector of the line axis.
+ ///
+ public Vector2 Axis;
- 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);
- }
+ ///
+ /// The length of the line segment.
+ ///
+ public float Length;
+
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
+ {
+ float value = FastRandom.Shared.NextSingle(Length * -0.5f, Length * 0.5f);
+ offset->X = Axis.X * value;
+ offset->Y = Axis.Y * value;
+ FastRandom.Shared.NextUnitVector(heading);
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/LineUniformProfile.cs b/source/MonoGame.Extended/Particles/Profiles/LineUniformProfile.cs
new file mode 100644
index 00000000..1a585e6f
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/Profiles/LineUniformProfile.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that distributes particles uniformly along a line segment with a fixed heading direction.
+///
+///
+/// The positions particles randomly along a line segment centered at the emitter
+/// position and defined by an axis direction and length. Unlike other profiles, this profile uses a fixed heading
+/// direction for all particles, perpendicular to the line.
+///
+public sealed class LineUniformProfile : Profile
+{
+ ///
+ /// The direction vector of the line axis.
+ ///
+ public Vector2 Axis;
+
+ ///
+ /// The length of the line segment.
+ ///
+ public float Length;
+
+ ///
+ /// The fixed heading direction for all particles spawned from this profile.
+ ///
+ public Vector2 PerpendicularDirection;
+
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
+ {
+ // 1. Spawn the particle at a random point on the line axis
+ float value = FastRandom.Shared.NextSingle(Length * -0.5f, Length * 0.5f);
+ offset->X = Axis.X * value;
+ offset->Y = Axis.Y * value;
+
+ // 2. Set the heading to the perpendicular direction
+ *heading = PerpendicularDirection;
+ }
+
+ ///
+ /// Sets the property to a normalized version of the specified vector.
+ ///
+ /// The direction vector to normalize and use as the perpendicular direction.
+ public void SetPerpendicularDirection(Vector2 direction)
+ {
+ PerpendicularDirection = Vector2.Normalize(direction);
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/PointProfile.cs b/source/MonoGame.Extended/Particles/Profiles/PointProfile.cs
index 04456d3f..3dbd8ea7 100644
--- a/source/MonoGame.Extended/Particles/Profiles/PointProfile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/PointProfile.cs
@@ -1,14 +1,33 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Profiles
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that emits all particles from a single point with random headings.
+///
+///
+/// The is the simplest emission profile, where all particles originate exactly at the
+/// emitter position with no offset. Each particle is given a random heading in any direction, creating a radial
+/// dispersion pattern.
+///
+public sealed class PointProfile : Profile
{
- public class PointProfile : Profile
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ ///
+ /// The offset is always set to (0,0), meaning particles will spawn exactly at the emitter position.
+ ///
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
{
- public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
- {
- offset = Vector2.Zero;
-
- Random.NextUnitVector(out heading);
- }
+ offset->X = 0.0f;
+ offset->Y = 0.0f;
+ FastRandom.Shared.NextUnitVector(heading);
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/Profile.cs b/source/MonoGame.Extended/Particles/Profiles/Profile.cs
index bfde371b..2b0aedc7 100644
--- a/source/MonoGame.Extended/Particles/Profiles/Profile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/Profile.cs
@@ -1,68 +1,129 @@
-using Microsoft.Xna.Framework;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
-namespace MonoGame.Extended.Particles.Profiles
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// Provides an abstract base class for particle emission profiles.
+///
+///
+/// A profile determines how particles are initially positioned and directed when they are emitted. Different profiles
+/// create different distribution patterns, such as points, lines, circles, or boxes.
+///
+public abstract class Profile
{
- public abstract class Profile
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ public abstract unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading);
+
+ ///
+ /// Creates a that emits particles from a single point.
+ ///
+ /// A new instance.
+ public static Profile Point()
{
- 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();
- }
+ return new PointProfile();
}
-}
\ No newline at end of file
+
+ ///
+ /// Creates a that emits particles along a line segment.
+ ///
+ /// The direction vector of the line.
+ /// The length of the line segment.
+ /// A new instance.
+ public static Profile Line(Vector2 axis, float length)
+ {
+ return new LineProfile { Axis = axis, Length = length };
+ }
+
+ ///
+ /// Creates a that emits particles uniformly along a line segment with a fixed heading.
+ ///
+ /// The direction vector of the line axis.
+ /// The length fo the line segment.
+ /// The fixed heading direction for all particles spawned from the profile.
+ ///
+ public static Profile LineUniform(Vector2 axis, float length, Vector2 perpendicularDirection)
+ {
+ return new LineUniformProfile { Axis = axis, Length = length, PerpendicularDirection = perpendicularDirection };
+ }
+
+ ///
+ /// Creates a that emits particles from the perimeter of a circle.
+ ///
+ /// The radius of the ring.
+ /// The radiation pattern for particle headings.
+ /// A new instance.
+ public static Profile Ring(float radius, CircleRadiation radiate)
+ {
+ return new RingProfile { Radius = radius, Radiate = radiate };
+ }
+
+ ///
+ /// Creates a that emits particles from the perimeter of a rectangle.
+ ///
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ /// A new instance.
+ public static Profile Box(float width, float height)
+ {
+ return new BoxProfile { Width = width, Height = height };
+ }
+
+ ///
+ /// Creates a that emits particles from within a rectangular area.
+ ///
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ /// A new instance.
+ public static Profile BoxFill(float width, float height)
+ {
+ return new BoxFillProfile { Width = width, Height = height };
+ }
+
+ ///
+ /// Creates a that emits particles from the perimeter of a rectangle with uniform density.
+ ///
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ /// A new instance.
+ public static Profile BoxUniform(float width, float height)
+ {
+ return new BoxUniformProfile { Width = width, Height = height };
+ }
+
+ ///
+ /// Creates a that emits particles from within a circular area.
+ ///
+ /// The radius of the circle.
+ /// The radiation pattern for particle headings.
+ /// A new instance.
+ public static Profile Circle(float radius, CircleRadiation radiate)
+ {
+ return new CircleProfile { Radius = radius, Radiate = radiate };
+ }
+
+ ///
+ /// Creates a that emits particles in a directional cone.
+ ///
+ /// The central direction of the spray.
+ /// The angular spread of the spray, in radians.
+ /// A new instance.
+ public static Profile Spray(Vector2 direction, float spread)
+ {
+ return new SprayProfile { Direction = direction, Spread = spread };
+ }
+
+ ///
+ public override string ToString()
+ {
+ return GetType().Name;
+ }
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/RingProfile.cs b/source/MonoGame.Extended/Particles/Profiles/RingProfile.cs
index d5ac2f82..2975a507 100644
--- a/source/MonoGame.Extended/Particles/Profiles/RingProfile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/RingProfile.cs
@@ -1,32 +1,70 @@
-using System;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
using Microsoft.Xna.Framework;
-namespace MonoGame.Extended.Particles.Profiles
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that distributes particles along the perimeter of a circle with controllable radiation patterns.
+///
+///
+///
+/// The positions new particles exclusively on the circumference of a circle centered
+/// at the emitter's position. Unlike which distributes particles throughout the
+/// circular area, this profile places particles only on the edge.
+///
+///
+/// The movement direction (heading) of each particle can be configured to radiate inward toward the center,
+/// outward from the center, or in random directions unrelated to their position.
+///
+///
+public sealed class RingProfile : Profile
{
- public class RingProfile : Profile
+ ///
+ /// The radius if the ring.
+ ///
+ public float Radius;
+
+ ///
+ /// The radiation mode that determines how particle headings are calculated.
+ ///
+ public CircleRadiation Radiate;
+
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ ///
+ /// Thrown when contains an unsupported value.
+ ///
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
{
- public float Radius { get; set; }
- public CircleRadiation Radiate { get; set; }
+ FastRandom.Shared.NextUnitVector(heading);
- public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ switch (Radiate)
{
- Random.NextUnitVector(out heading);
+ case CircleRadiation.In:
+ offset->X = -heading->X * Radius;
+ offset->Y = -heading->Y * Radius;
+ break;
- 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");
- }
+ case CircleRadiation.Out:
+ offset->X = heading->X * Radius;
+ offset->Y = heading->Y * Radius;
+ break;
+
+ case CircleRadiation.None:
+ offset->X = heading->X * Radius;
+ offset->Y = heading->Y * Radius;
+ FastRandom.Shared.NextUnitVector(heading);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(Radiate), Radiate, "Unsupported radiation mode");
}
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Profiles/SprayProfile.cs b/source/MonoGame.Extended/Particles/Profiles/SprayProfile.cs
index 6259210f..0f37cb67 100644
--- a/source/MonoGame.Extended/Particles/Profiles/SprayProfile.cs
+++ b/source/MonoGame.Extended/Particles/Profiles/SprayProfile.cs
@@ -1,20 +1,57 @@
-using System;
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
using Microsoft.Xna.Framework;
-namespace MonoGame.Extended.Particles.Profiles
+namespace MonoGame.Extended.Particles.Profiles;
+
+///
+/// A profile that emits particles from a single point in a directional cone pattern.
+///
+///
+/// The positions all particles exactly at the emitter's position,
+/// like , but instead of random directions in all directions, it constrains the particle
+/// headings to a cone-shaped area defined by a central direction and spread angle.
+///
+public sealed class SprayProfile : Profile
{
- public class SprayProfile : Profile
+ ///
+ /// The central direction vector of the spray.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// The angular spread of the spray cone (in radians).
+ ///
+ ///
+ /// This value determines how wide the spray cone is. For example:
+ ///
+ ///
+ /// - A value of 0 will emit all particles in exactly the same direction.
+ /// - A value of π (Pi) will create a 180-degree fan.
+ /// - A value of 2π will emit in all directions (similar to ).
+ ///
+ ///
+ public float Spread;
+
+ ///
+ /// Computes the offset and heading for a new particle.
+ ///
+ /// A pointer to the Vector2 where the offset from the emitter position will be stored.
+ /// A pointer to the Vector2 where the unit direction vector will be stored.
+ ///
+ /// The offset is always set to (0,0), meaning particles will spawn exactly at the emitter position.
+ ///
+ public override unsafe void GetOffsetAndHeading(Vector2* offset, Vector2* heading)
{
- public Vector2 Direction { get; set; }
- public float Spread { get; set; }
+ offset->X = offset->Y = 0.0f;
- public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
- {
- var angle = (float) Math.Atan2(Direction.Y, Direction.X);
+ float angle = MathF.Atan2(Direction.Y, Direction.X);
+ angle = FastRandom.Shared.NextSingle(angle - Spread * 0.5f, angle + Spread * 0.5f);
- angle = Random.NextSingle(angle - Spread/2f, angle + Spread/2f);
- offset = Vector2.Zero;
- heading = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
- }
+ heading->X = MathF.Cos(angle);
+ heading->Y = MathF.Sin(angle);
}
-}
\ No newline at end of file
+}
diff --git a/source/MonoGame.Extended/Particles/Serialization/InterpolatorJsonConverter.cs b/source/MonoGame.Extended/Particles/Serialization/InterpolatorJsonConverter.cs
deleted file mode 100644
index 4a884fdc..00000000
--- a/source/MonoGame.Extended/Particles/Serialization/InterpolatorJsonConverter.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using MonoGame.Extended.Particles.Modifiers.Interpolators;
-using MonoGame.Extended.Serialization.Json;
-
-namespace MonoGame.Extended.Particles.Serialization
-{
- public class InterpolatorJsonConverter : BaseTypeJsonConverter
- {
- public InterpolatorJsonConverter()
- : base(GetSupportedTypes(), "Interpolator")
- {
- }
-
- private static IEnumerable 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/source/MonoGame.Extended/Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs b/source/MonoGame.Extended/Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs
deleted file mode 100644
index 8fc894bb..00000000
--- a/source/MonoGame.Extended/Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Reflection;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MonoGame.Extended.Particles.Serialization;
-
-///
-/// Converts a value to or from JSON.
-///
-public class ModifierExecutionStrategyJsonConverter : JsonConverter
-{
- ///
- public override bool CanConvert(Type typeToConvert) =>
- typeToConvert == typeof(ParticleModifierExecutionStrategy) ||
- typeToConvert.GetTypeInfo().BaseType == typeof(ParticleModifierExecutionStrategy);
-
- ///
- public override ParticleModifierExecutionStrategy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var value = JsonSerializer.Deserialize(ref reader, options);
- return ParticleModifierExecutionStrategy.Parse(value);
- }
-
- ///
- ///
- /// Throw if is .
- ///
- public override void Write(Utf8JsonWriter writer, ParticleModifierExecutionStrategy value, JsonSerializerOptions options)
- {
- ArgumentNullException.ThrowIfNull(writer);
- writer.WriteStringValue(value.ToString());
- }
-}
diff --git a/source/MonoGame.Extended/Particles/Serialization/ModifierJsonConverter.cs b/source/MonoGame.Extended/Particles/Serialization/ModifierJsonConverter.cs
deleted file mode 100644
index 20d09984..00000000
--- a/source/MonoGame.Extended/Particles/Serialization/ModifierJsonConverter.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using MonoGame.Extended.Particles.Modifiers;
-using MonoGame.Extended.Serialization.Json;
-
-namespace MonoGame.Extended.Particles.Serialization
-{
- public class ModifierJsonConverter : BaseTypeJsonConverter
- {
- public ModifierJsonConverter()
- : base(GetSupportedTypes(), "Modifier")
- {
- }
-
- private static IEnumerable 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/source/MonoGame.Extended/Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs b/source/MonoGame.Extended/Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs
deleted file mode 100644
index be4764b5..00000000
--- a/source/MonoGame.Extended/Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using MonoGame.Extended.Serialization;
-using MonoGame.Extended.Serialization.Json;
-
-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());
- options.Converters.Add(new RangeJsonConverter());
- options.Converters.Add(new RangeJsonConverter());
- options.Converters.Add(new HslColorJsonConverter());
- options.Converters.Add(new ModifierExecutionStrategyJsonConverter());
-
- return options;
- }
-}
diff --git a/source/MonoGame.Extended/Particles/Serialization/ProfileJsonConverter.cs b/source/MonoGame.Extended/Particles/Serialization/ProfileJsonConverter.cs
deleted file mode 100644
index 8e59f75c..00000000
--- a/source/MonoGame.Extended/Particles/Serialization/ProfileJsonConverter.cs
+++ /dev/null
@@ -1,306 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Reflection;
-using System.Security.Cryptography;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Text.Unicode;
-using Microsoft.Xna.Framework;
-using MonoGame.Extended.Particles.Profiles;
-using MonoGame.Extended.Serialization.Json;
-
-namespace MonoGame.Extended.Particles.Serialization
-{
- public class ProfileJsonConverter : JsonConverter
- {
- ///
- public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Profile);
-
- ///
- public override Profile Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- if (reader.TokenType != JsonTokenType.StartObject)
- {
- throw new JsonException($"Expected {nameof(JsonTokenType.StartObject)} token");
- }
-
- Profile profile = null;
-
- while (reader.Read())
- {
- if (reader.TokenType == JsonTokenType.EndObject)
- {
- break;
- }
-
- if (reader.TokenType == JsonTokenType.PropertyName)
- {
- var propertyName = reader.GetString();
- reader.Read();
-
- if (propertyName.Equals("type", StringComparison.InvariantCultureIgnoreCase))
- {
- var type = reader.GetString();
- profile = type switch
- {
- nameof(Profile.Point) => Profile.Point(),
- nameof(Profile.Line) => ReadLineProfile(ref reader),
- nameof(Profile.Ring) => ReadRingProfile(ref reader),
- nameof(Profile.Box) => ReadBoxProfile(ref reader),
- nameof(Profile.BoxFill) => ReadBoxFillProfile(ref reader),
- nameof(Profile.BoxUniform) => ReadBoxUniformProfile(ref reader),
- nameof(Profile.Circle) => ReadCircleProfile(ref reader),
- nameof(Profile.Spray) => ReadSprayProfile(ref reader),
-
- _ => throw new NotSupportedException($"The profile type {type} is not supported at this time")
- };
- }
- }
- }
-
- return profile;
- }
-
- private static Profile ReadLineProfile(ref Utf8JsonReader reader)
- {
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("axis", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- string[] split = reader.GetString().Split(" ", StringSplitOptions.RemoveEmptyEntries);
- Debug.Assert(split.Length == 2);
- Vector2 axis = new Vector2(float.Parse(split[0]), float.Parse(split[1]));
-
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("length", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float length = reader.GetSingle();
-
- return Profile.Line(axis, length);
- }
-
- private static Profile ReadRingProfile(ref Utf8JsonReader reader)
- {
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("radius", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float radius = reader.GetSingle();
-
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("radiate", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- int radiate = reader.GetInt32();
-
- return Profile.Ring(radius, (Profile.CircleRadiation)radiate);
- }
-
- private static Profile ReadBoxProfile(ref Utf8JsonReader reader)
- {
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("width", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float width = reader.GetSingle();
-
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("height", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float height = reader.GetSingle();
-
- return Profile.Box(width, height);
- }
-
- private static Profile ReadBoxFillProfile(ref Utf8JsonReader reader)
- {
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("width", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float width = reader.GetSingle();
-
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("height", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float height = reader.GetSingle();
-
- return Profile.BoxFill(width, height);
- }
-
- private static Profile ReadBoxUniformProfile(ref Utf8JsonReader reader)
- {
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("width", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float width = reader.GetSingle();
-
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("height", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float height = reader.GetSingle();
-
- return Profile.BoxUniform(width, height);
- }
-
- private static Profile ReadCircleProfile(ref Utf8JsonReader reader)
- {
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("radius", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float radius = reader.GetSingle();
-
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("radiate", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- int radiate = reader.GetInt32();
-
- return Profile.Circle(radius, (Profile.CircleRadiation)radiate);
-
- }
-
- private static Profile ReadSprayProfile(ref Utf8JsonReader reader)
- {
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("direction", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- string[] split = reader.GetString().Split(" ", StringSplitOptions.RemoveEmptyEntries);
- Debug.Assert(split.Length == 2);
- Vector2 direction = new Vector2(float.Parse(split[0]), float.Parse(split[1]));
-
- reader.Read();
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- Debug.Assert(reader.GetString().Equals("spread", StringComparison.InvariantCultureIgnoreCase));
- reader.Read();
- float spread = reader.GetSingle();
-
- return Profile.Spray(direction, spread);
- }
-
- ///
- public override void Write(Utf8JsonWriter writer, Profile value, JsonSerializerOptions options)
- {
- ArgumentNullException.ThrowIfNull(writer);
- var type = value.GetType().ToString();
- switch (type)
- {
- case nameof(PointProfile):
- WritePointProfile(ref writer, (PointProfile)value);
- break;
-
- case nameof(LineProfile):
- WriteLineProfile(ref writer, (LineProfile)value);
- break;
-
- case nameof(RingProfile):
- WriteRingProfile(ref writer, (RingProfile)value);
- break;
-
- case nameof(BoxProfile):
- WriteBoxProfile(ref writer, (BoxProfile)value);
- break;
-
- case nameof(BoxFillProfile):
- WriteBoxFillProfile(ref writer, (BoxFillProfile)value);
- break;
-
- case nameof(BoxUniformProfile):
- WriteBoxUniformProfile(ref writer, (BoxUniformProfile)value);
- break;
-
- case nameof(CircleProfile):
- WriteCircleProfile(ref writer, (CircleProfile)value);
- break;
-
- case nameof(SprayProfile):
- WriteSprayProfile(ref writer, (SprayProfile)value);
- break;
-
- default:
- throw new InvalidOperationException("Unknown profile type");
- }
- }
-
- private static void WritePointProfile(ref Utf8JsonWriter writer, PointProfile value)
- {
- writer.WriteStartObject();
- writer.WritePropertyName("type");
- writer.WriteStringValue(value.GetType().ToString());
- writer.WriteEndObject();
- }
-
- private static void WriteLineProfile(ref Utf8JsonWriter writer, LineProfile value)
- {
- writer.WriteStartObject();
- writer.WriteString("type", value.GetType().ToString());
- writer.WriteString("axis", $"{value.Axis.X} {value.Axis.Y}");
- writer.WriteNumber("length", value.Length);
- writer.WriteEndObject();
- }
-
- private static void WriteRingProfile(ref Utf8JsonWriter writer, RingProfile value)
- {
- writer.WriteStartObject();
- writer.WriteString("type", value.GetType().ToString());
- writer.WriteNumber("radius", value.Radius);
- writer.WriteNumber("radiate", (int)value.Radiate);
- writer.WriteEndObject();
- }
-
- private static void WriteBoxProfile(ref Utf8JsonWriter writer, BoxProfile value)
- {
- writer.WriteStartObject();
- writer.WriteString("type", value.GetType().ToString());
- writer.WriteNumber("width", value.Width);
- writer.WriteNumber("height", value.Height);
- writer.WriteEndObject();
- }
-
- private static void WriteBoxFillProfile(ref Utf8JsonWriter writer, BoxFillProfile value)
- {
- writer.WriteStartObject();
- writer.WriteString("type", value.GetType().ToString());
- writer.WriteNumber("width", value.Width);
- writer.WriteNumber("height", value.Height);
- writer.WriteEndObject();
- }
-
- private static void WriteBoxUniformProfile(ref Utf8JsonWriter writer, BoxUniformProfile value)
- {
- writer.WriteStartObject();
- writer.WriteString("type", value.GetType().ToString());
- writer.WriteNumber("width", value.Width);
- writer.WriteNumber("height", value.Height);
- writer.WriteEndObject();
- }
-
- private static void WriteCircleProfile(ref Utf8JsonWriter writer, CircleProfile value)
- {
- writer.WriteStartObject();
- writer.WriteString("type", value.GetType().ToString());
- writer.WriteNumber("radius", value.Radius);
- writer.WriteNumber("radiate", (int)value.Radiate);
- writer.WriteEndObject();
-
- }
-
- private static void WriteSprayProfile(ref Utf8JsonWriter writer, SprayProfile value)
- {
- writer.WriteStartObject();
- writer.WriteString("type", value.GetType().ToString());
- writer.WriteString("direction", $"{value.Direction.X} {value.Direction.Y}");
- writer.WriteNumber("spread", value.Spread);
- writer.WriteEndObject();
- }
- }
-}
diff --git a/source/MonoGame.Extended/Particles/Serialization/TimeSpanJsonConverter.cs b/source/MonoGame.Extended/Particles/Serialization/TimeSpanJsonConverter.cs
deleted file mode 100644
index 0155b083..00000000
--- a/source/MonoGame.Extended/Particles/Serialization/TimeSpanJsonConverter.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MonoGame.Extended.Particles.Serialization;
-
-public class TimeSpanJsonConverter : JsonConverter
-{
- ///
- public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TimeSpan);
-
- ///
- 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;
- }
-
- ///
- ///
- /// Throw if is .
- ///
- public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
- {
- ArgumentNullException.ThrowIfNull(writer);
- writer.WriteNumberValue(value.TotalSeconds);
- }
-}
diff --git a/source/MonoGame.Extended/Particles/SpriteBatchExtensions.ParticleEfffect.cs b/source/MonoGame.Extended/Particles/SpriteBatchExtensions.ParticleEfffect.cs
new file mode 100644
index 00000000..eaf21ac7
--- /dev/null
+++ b/source/MonoGame.Extended/Particles/SpriteBatchExtensions.ParticleEfffect.cs
@@ -0,0 +1,115 @@
+using System;
+
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Graphics;
+using MonoGame.Extended.Particles.Data;
+
+namespace MonoGame.Extended.Particles;
+
+public static class SpriteBatchExtensions
+{
+ public static void Draw(this SpriteBatch spriteBatch, ParticleEffect effect)
+ {
+ ArgumentNullException.ThrowIfNull(effect);
+ ObjectDisposedException.ThrowIf(effect.IsDisposed, effect);
+
+ for (int i = 0; i < effect.Emitters.Count; i++)
+ {
+ UnsafeDraw(spriteBatch, effect.Emitters[i]);
+ }
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, ParticleEmitter emitter)
+ {
+ ArgumentNullException.ThrowIfNull(emitter);
+ ObjectDisposedException.ThrowIf(emitter.IsDisposed, emitter);
+ UnsafeDraw(spriteBatch, emitter);
+ }
+
+ private static unsafe void UnsafeDraw(SpriteBatch spriteBatch, ParticleEmitter emitter)
+ {
+ ArgumentNullException.ThrowIfNull(spriteBatch);
+
+ // Early exit if no texture region assigned
+ if (emitter.TextureRegion == null)
+ {
+ return;
+ }
+
+ // Early exit if there are no active particles
+ if (emitter.ActiveParticles == 0)
+ {
+ return;
+ }
+
+ Texture2DRegion region = emitter.TextureRegion;
+ Texture2D texture = region.Texture;
+ Rectangle sourceRect = region.Bounds;
+ Vector2 origin = new Vector2(region.Width, region.Height) * 0.5f;
+
+ if (emitter.RenderingOrder == ParticleRenderingOrder.FrontToBack)
+ {
+ int count = emitter.ActiveParticles;
+
+ Span particlePtrs = count <= 1024 ?
+ stackalloc IntPtr[count] :
+ new IntPtr[count];
+
+ ParticleIterator iterator = emitter.Buffer.Iterator;
+ int index = 0;
+
+ while (iterator.HasNext)
+ {
+ particlePtrs[index++] = (IntPtr)iterator.Next();
+ }
+
+ for (int i = count - 1; i >= 0; i--)
+ {
+ RenderParticle(spriteBatch, (Particle*)particlePtrs[i], texture, sourceRect, origin);
+ }
+ }
+ else
+ {
+ ParticleIterator iterator = emitter.Buffer.Iterator;
+
+ while (iterator.HasNext)
+ {
+ Particle* particle = iterator.Next();
+ RenderParticle(spriteBatch, particle, texture, sourceRect, origin);
+ }
+ }
+ }
+
+ private static unsafe void RenderParticle(SpriteBatch spriteBatch, Particle* particle, Texture2D texture, Rectangle sourceRect, Vector2 origin)
+ {
+ HslColor hsl = new HslColor(particle->Color[0], particle->Color[1], particle->Color[2]);
+ Color color = HslColor.ToRgb(hsl);
+
+ if (spriteBatch.GraphicsDevice.BlendState == BlendState.AlphaBlend)
+ {
+ color *= particle->Opacity;
+ }
+ else
+ {
+ color.A = (byte)MathHelper.Clamp(particle->Opacity * 255, 0, 255);
+ }
+
+ Vector2 position = new Vector2(particle->Position[0], particle->Position[1]);
+ float scale = particle->Scale;
+ float rotation = particle->Rotation;
+ float layerDepth = particle->LayerDepth;
+
+ spriteBatch.Draw(
+ texture,
+ position,
+ sourceRect,
+ color,
+ rotation,
+ origin,
+ scale,
+ SpriteEffects.None,
+ layerDepth
+ );
+ }
+}
diff --git a/source/MonoGame.Extended/Serialization/Xml/XmlReaderExtensions.cs b/source/MonoGame.Extended/Serialization/Xml/XmlReaderExtensions.cs
new file mode 100644
index 00000000..922cfdd0
--- /dev/null
+++ b/source/MonoGame.Extended/Serialization/Xml/XmlReaderExtensions.cs
@@ -0,0 +1,267 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.Xml;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Serialization.Xml;
+
+///
+/// Provides extension methods for to simplify reading and parsing XML Attributes into
+/// strong-typed values.
+///
+public static class XmlReaderExtensions
+{
+ ///
+ /// Reads an XML attribute as an value.
+ ///
+ /// The XML reader instance.
+ /// The name of the attribute to read.
+ /// The value parsed from the value of the specified attribute.
+ ///
+ /// Thrown when the attribute is missing, or when the attribute value cannot be parsed as an .
+ ///
+ public static int GetAttributeInt(this XmlReader reader, string attributeName)
+ {
+ string value = reader.GetAttribute(attributeName);
+
+ if (value == null)
+ {
+ throw new XmlException($"Required attribute '{attributeName}' is missing.");
+ }
+
+ try
+ {
+ return int.Parse(value);
+ }
+ catch (Exception ex) when (ex is FormatException || ex is OverflowException)
+ {
+ throw new XmlException(
+ $"Invalid integer format for attribute '{attributeName}'. Expected integer, but got '{value}'",
+ ex
+ );
+ }
+ }
+
+ ///
+ /// Reads an XML attribute as a value.
+ ///
+ /// The XML reader instance.
+ /// The name of the attribute to read.
+ /// The value parsed from the specified attribute.
+ ///
+ /// Thrown when the attribute is missing, or when the attribute value cannot be parsed as a .
+ ///
+ public static float GetAttributeFloat(this XmlReader reader, string attributeName)
+ {
+ string value = reader.GetAttribute(attributeName);
+
+ if (value == null)
+ {
+ throw new XmlException($"Required attribute '{attributeName}' is missing.");
+ }
+
+ try
+ {
+ return float.Parse(value);
+ }
+ catch (Exception ex) when (ex is FormatException || ex is OverflowException)
+ {
+ throw new XmlException(
+ $"Invalid float format for attribute '{attributeName}'. Expected float, but got '{value}'",
+ ex
+ );
+ }
+ }
+
+ ///
+ /// Reads an XML attribute as a value.
+ ///
+ /// The XML reader instance.
+ /// The name of the attribute to read.
+ /// The value parsed from the value of the specified attribute.
+ ///
+ /// Thrown when the attribute is missing, or when the attribute value cannot be parsed as a .
+ ///
+ public static bool GetAttributeBool(this XmlReader reader, string attributeName)
+ {
+ string value = reader.GetAttribute(attributeName);
+
+ if (value == null)
+ {
+ throw new XmlException($"Required attribute '{attributeName}' is missing.");
+ }
+
+ try
+ {
+ return bool.Parse(value);
+ }
+ catch (Exception ex) when (ex is FormatException)
+ {
+ throw new XmlException(
+ $"Invalid bool format for attribute '{attributeName}'. Expected 'true' or 'false' but got '{value}'",
+ ex
+ );
+ }
+ }
+
+ ///
+ /// Reads an XML attribute as an enumeration value.
+ ///
+ /// The enumeration type to parse.
+ /// The XML reader instance.
+ /// The name of the attribute to read.
+ /// The enumeration value parsed from the value of the specified attribute.
+ ///
+ /// Thrown when the attribute is missing, or when the attribute value cannot be parsed as the specified enumeration type.
+ ///
+ public static T GetAttributeEnum(this XmlReader reader, string attributeName) where T : struct, Enum
+ {
+ string value = reader.GetAttribute(attributeName);
+
+ if (value == null)
+ {
+ throw new XmlException($"Required attribute '{attributeName}' is missing.");
+ }
+
+ try
+ {
+ return Enum.Parse(value);
+ }
+ catch (Exception ex) when (ex is ArgumentException)
+ {
+ throw new XmlException(
+ $"Invalid {typeof(T).Name} format for attribute '{attributeName}'. Expected a {typeof(T).Name} value but got '{value}'",
+ ex
+ );
+ }
+ }
+
+ ///
+ /// Reads an XML attribute as a value.
+ ///
+ /// The XML reader instance.
+ /// The name of the attribute to read.
+ /// The value parsed from the value of the specified attribute.
+ ///
+ /// Thrown when the attribute is missing, or when the attribute value cannot be parsed as a .
+ ///
+ public static Rectangle GetAttributeRectangle(this XmlReader reader, string attributeName)
+ {
+ string value = reader.GetAttribute(attributeName);
+
+ if (value == null)
+ {
+ throw new XmlException($"Required attribute '{attributeName}' is missing.");
+ }
+
+ string[] split = value.Split(',', StringSplitOptions.RemoveEmptyEntries);
+
+ try
+ {
+ if (split.Length != 4)
+ {
+ throw new InvalidOperationException($"{nameof(Rectangle)} attribute must contain four integer values separated by a comma");
+ }
+
+ int x = int.Parse(split[0]);
+ int y = int.Parse(split[1]);
+ int width = int.Parse(split[2]);
+ int height = int.Parse(split[3]);
+
+ return new Rectangle(x, y, width, height);
+ }
+ catch (Exception ex) when (ex is FormatException || ex is OverflowException || ex is InvalidOperationException)
+ {
+ throw new XmlException(
+ $"Invalid {nameof(Rectangle)} format for attribute '{attributeName}'. Expected 'x,y,width,height' but got '{value}'",
+ ex
+ );
+ }
+ }
+
+ ///
+ /// Reads an XML attribute as a value.
+ ///
+ /// The XML reader instance.
+ /// The name of the attribute to read.
+ /// The value parsed from the value of the specified attribute.
+ ///
+ /// Thrown when the attribute is missing, or when the attribute value cannot be parsed as a .
+ ///
+ public static Vector2 GetAttributeVector2(this XmlReader reader, string attributeName)
+ {
+ string value = reader.GetAttribute(attributeName);
+
+ if (value == null)
+ {
+ throw new XmlException($"Required attribute '{attributeName}' is missing.");
+ }
+
+ string[] split = value.Split(',', StringSplitOptions.RemoveEmptyEntries);
+
+ try
+ {
+ if (split.Length != 2)
+ {
+ throw new InvalidOperationException($"{nameof(Vector2)} attribute must contain two float values separated by a comma");
+ }
+
+ float x = float.Parse(split[0]);
+ float y = float.Parse(split[1]);
+
+ return new Vector2(x, y);
+ }
+ catch (Exception ex) when (ex is FormatException || ex is OverflowException || ex is InvalidOperationException)
+ {
+ throw new XmlException(
+ $"Invalid {nameof(Vector2)} format for attribute '{attributeName}'. Expected 'x,y', but got '{value}'",
+ ex
+ );
+ }
+ }
+
+ ///
+ /// Reads an XML attribute as a value.
+ ///
+ /// The XML reader instance.
+ /// The name of the attribute to read.
+ /// The value parsed from the value of the specified attribute.
+ ///
+ /// Thrown when the attribute is missing, or when the attribute value cannot be parsed as a .
+ ///
+ public static Vector3 GetAttributeVector3(this XmlReader reader, string attributeName)
+ {
+ string value = reader.GetAttribute(attributeName);
+
+ if (value == null)
+ {
+ throw new XmlException($"Required attribute '{attributeName}' is missing.");
+ }
+
+ string[] split = value.Split(',', StringSplitOptions.RemoveEmptyEntries);
+
+ try
+ {
+ if (split.Length != 3)
+ {
+ throw new InvalidOperationException($"{nameof(Vector3)} attribute must contain three float values separated by a comma");
+ }
+
+ float x = float.Parse(split[0]);
+ float y = float.Parse(split[1]);
+ float z = float.Parse(split[2]);
+
+ return new Vector3(x, y, z);
+ }
+ catch (Exception ex) when (ex is FormatException || ex is OverflowException || ex is InvalidOperationException)
+ {
+ throw new XmlException(
+ $"Invalid {nameof(Vector3)} format for attribute '{attributeName}'. Expected 'x,y,z', but got '{value}'",
+ ex
+ );
+ }
+ }
+}
diff --git a/source/MonoGame.Extended/Serialization/Xml/XmlWriterExtensions.cs b/source/MonoGame.Extended/Serialization/Xml/XmlWriterExtensions.cs
new file mode 100644
index 00000000..e4b13e29
--- /dev/null
+++ b/source/MonoGame.Extended/Serialization/Xml/XmlWriterExtensions.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.Xml;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Serialization.Xml;
+
+///
+/// Provides extension methods for to simplify writing XML attributes from strongly-typed values
+///
+public static class XmlWriterExtensions
+{
+ ///
+ /// Writes an XML attribute from an value.
+ ///
+ /// The XML writer instance.
+ /// The name of the attribute to write.
+ /// The value to write as the attribute value.
+ /// The writer state is not valid for this operation.
+ /// The attribute name is not valid.
+ public static void WriteAttributeInt(this XmlWriter writer, string attributeName, int value)
+ {
+ writer.WriteAttributeString(attributeName, $"{value}");
+ }
+
+ ///
+ /// Writes an XML attribute from a value.
+ ///
+ /// The XML writer instance.
+ /// The name of the attribute to write.
+ /// The value to write as the attribute value.
+ /// The writer state is not valid for this operation.
+ /// The attribute name is not valid.
+ public static void WriteAttributeFloat(this XmlWriter writer, string attributeName, float value)
+ {
+ writer.WriteAttributeString(attributeName, $"{value}");
+ }
+
+ ///
+ /// Writes an XML attribute from a value.
+ ///
+ /// The XML writer instance.
+ /// The name of the attribute to write.
+ /// The value to write as the attribute value.
+ /// The writer state is not valid for this operation.
+ /// The attribute name is not valid.
+ public static void WriteAttributeBool(this XmlWriter writer, string attributeName, bool value)
+ {
+ writer.WriteAttributeString(attributeName, $"{value}");
+ }
+
+ ///
+ /// Writes an XML attribute from a value.
+ ///
+ /// The XML writer instance.
+ /// The name of the attribute to write.
+ /// The value to write as the attribute value.
+ /// The writer state is not valid for this operation.
+ /// The attribute name is not valid.
+ public static void WriteAttributeRectangle(this XmlWriter writer, string attributeName, Rectangle value)
+ {
+ writer.WriteAttributeString(attributeName, $"{value.X},{value.Y},{value.Width},{value.Height}");
+ }
+
+ ///
+ /// Writes an XML attribute from a value.
+ ///
+ /// The XML writer instance.
+ /// The name of the attribute to write.
+ /// The value to write as the attribute value.
+ /// The writer state is not valid for this operation.
+ /// The attribute name is not valid.
+ public static void WriteAttributeVector2(this XmlWriter writer, string attributeName, Vector2 value)
+ {
+ writer.WriteAttributeString(attributeName, $"{value.X},{value.Y}");
+ }
+
+ ///
+ /// Writes an XML attribute from a value.
+ ///
+ /// The XML writer instance.
+ /// The name of the attribute to write.
+ /// The value to write as the attribute value.
+ /// The writer state is not valid for this operation.
+ /// The attribute name is not valid.
+ public static void WriteAttributeVector3(this XmlWriter writer, string attributeName, Vector3 value)
+ {
+ writer.WriteAttributeString(attributeName, $"{value.X},{value.Y},{value.Z}");
+ }
+}
diff --git a/tests/MonoGame.Extended.Tests/MockContentManager.cs b/tests/MonoGame.Extended.Tests/MockContentManager.cs
new file mode 100644
index 00000000..c099804a
--- /dev/null
+++ b/tests/MonoGame.Extended.Tests/MockContentManager.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGame.Extended.Tests;
+
+public class MockContentManager : ContentManager
+{
+ public MockContentManager() : base(new GameServiceContainer())
+ {
+ }
+
+ public override T Load(string assetName)
+ {
+ return default(T);
+ }
+}
diff --git a/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj b/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj
index 00ccb5bf..236a62b1 100644
--- a/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj
+++ b/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj
@@ -1,5 +1,9 @@
+
+ true
+
+
diff --git a/tests/MonoGame.Extended.Tests/Particles/ParticleEffectReaderTests.cs b/tests/MonoGame.Extended.Tests/Particles/ParticleEffectReaderTests.cs
new file mode 100644
index 00000000..66090066
--- /dev/null
+++ b/tests/MonoGame.Extended.Tests/Particles/ParticleEffectReaderTests.cs
@@ -0,0 +1,1172 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Xml;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Particles.Modifiers.Containers;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+using MonoGame.Extended.Particles.Profiles;
+
+namespace MonoGame.Extended.Tests.Particles;
+
+public class ParticleEffectReaderTests
+{
+ private readonly MockContentManager _mockContentManager;
+
+ public ParticleEffectReaderTests()
+ {
+ _mockContentManager = new MockContentManager();
+ }
+
+ private ParticleEffect ReadParticleEffectFromXml(string xml)
+ {
+ using MemoryStream stream = new MemoryStream();
+ using StreamWriter writer = new StreamWriter(stream);
+ writer.Write(xml);
+ writer.Flush();
+ stream.Position = 0;
+
+ using ParticleEffectReader reader = new ParticleEffectReader(stream, _mockContentManager);
+ return reader.ReadParticleEffect();
+ }
+
+ [Fact]
+ public void ReadParticleEffect_NulLStream_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => new ParticleEffectReader((Stream)null, _mockContentManager));
+ }
+
+ [Fact]
+ public void ReadParticleEffect_NullContentManager_ThrowsArgumentNullException()
+ {
+ using MemoryStream stream = new MemoryStream();
+ Assert.Throws(() => new ParticleEffectReader(stream, null));
+ }
+
+ [Fact]
+ public void ReadParticleEffect_InvalidXmlRoot_ThrowsXmlException()
+ {
+ string xml = "";
+ Assert.Throws(() => ReadParticleEffectFromXml(xml));
+ }
+
+ [Fact]
+ public void ReadParticleEffect_EmptyEffect_ReturnsExpected()
+ {
+ string xml =
+ """
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Equal("EmptyEffect", effect.Name);
+ Assert.Equal(Vector2.Zero, effect.Position);
+ Assert.Equal(0.0f, effect.Rotation);
+ Assert.Equal(Vector2.One, effect.Scale);
+ Assert.Empty(effect.Emitters);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_EmptyModifiers_ReturnsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Empty(emitter.Modifiers);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_EmptyInterpolators_ReturnsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ AgeModifier modifier = Assert.IsType(emitter.Modifiers[0]);
+ Assert.Empty(modifier.Interpolators);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_BoxFillProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ BoxFillProfile profile = Assert.IsType(emitter.Profile);
+ Assert.Equal(1.0f, profile.Height);
+ Assert.Equal(1.0f, profile.Height);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_BoxProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ BoxProfile profile = Assert.IsType(emitter.Profile);
+ Assert.Equal(1.0f, profile.Height);
+ Assert.Equal(1.0f, profile.Height);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_BoxUniformProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ BoxUniformProfile profile = Assert.IsType(emitter.Profile);
+ Assert.Equal(1.0f, profile.Height);
+ Assert.Equal(1.0f, profile.Height);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_CircleProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ CircleProfile profile = Assert.IsType(emitter.Profile);
+ Assert.Equal(1.0f, profile.Radius);
+ Assert.Equal(CircleRadiation.Out, profile.Radiate);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_LineProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ LineProfile profile = Assert.IsType(emitter.Profile);
+ Assert.Equal(Vector2.One, profile.Axis);
+ Assert.Equal(1.0f, profile.Length);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_LineUniformProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ LineUniformProfile profile = Assert.IsType(emitter.Profile);
+ Assert.Equal(Vector2.One, profile.Axis);
+ Assert.Equal(1.0f, profile.Length);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_PointProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ PointProfile profile = Assert.IsType(emitter.Profile);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_RingProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ RingProfile profile = Assert.IsType(emitter.Profile);
+ Assert.Equal(1.0f, profile.Radius);
+ Assert.Equal(CircleRadiation.In, profile.Radiate);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_SprayProfile_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ SprayProfile profile = Assert.IsType(emitter.Profile);
+ Assert.Equal(Vector2.One, profile.Direction);
+ Assert.Equal(1.0f, profile.Spread);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_AgeModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ AgeModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("AgeModifier", modifier.Name);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_CircleContainerModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ CircleContainerModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("CircleContainerModifier", modifier.Name);
+ Assert.Equal(0.0f, modifier.Radius);
+ Assert.True(modifier.Inside);
+ Assert.Equal(1.0f, modifier.RestitutionCoefficient);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_DragModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ DragModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("DragModifier", modifier.Name);
+ Assert.Equal(0.47f, modifier.DragCoefficient);
+ Assert.Equal(0.5f, modifier.Density);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_LinearGravityModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ LinearGravityModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("LinearGravityModifier", modifier.Name);
+ Assert.Equal(Vector2.Zero, modifier.Direction);
+ Assert.Equal(0.0f, modifier.Strength);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_OpacityFastFadeModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ OpacityFastFadeModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("OpacityFastFadeModifier", modifier.Name);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_RectangleContainerModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ RectangleContainerModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("RectangleContainerModifier", modifier.Name);
+ Assert.Equal(0.0f, modifier.Width);
+ Assert.Equal(0.0f, modifier.Height);
+ Assert.Equal(1.0f, modifier.RestitutionCoefficient);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_RectangleLoopContainerModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ RectangleLoopContainerModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("RectangleLoopContainerModifier", modifier.Name);
+ Assert.Equal(0.0f, modifier.Width);
+ Assert.Equal(0.0f, modifier.Height);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_RotationModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ RotationModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("RotationModifier", modifier.Name);
+ Assert.Equal(0.0f, modifier.RotationRate);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_VelocityColorModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ VelocityColorModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("VelocityColorModifier", modifier.Name);
+ Assert.Equal(Vector3.Zero, modifier.StationaryColor);
+ Assert.Equal(Vector3.Zero, modifier.VelocityColor);
+ Assert.Equal(0.0f, modifier.VelocityThreshold);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_VelocityModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ VelocityModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("VelocityModifier", modifier.Name);
+ Assert.Equal(0.0f, modifier.VelocityThreshold);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_VortexModifier_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+
+ VortexModifier modifier = Assert.IsType(effect.Emitters[0].Modifiers[0]);
+ Assert.Equal(60.0f, modifier.Frequency);
+ Assert.Equal("VortexModifier", modifier.Name);
+ Assert.Equal(Vector2.Zero, modifier.Position);
+ Assert.Equal(0.0f, modifier.Mass);
+ Assert.Equal(0.0f, modifier.MaxSpeed);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_ColorInterpolator_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+ AgeModifier modifier = Assert.IsType(emitter.Modifiers[0]);
+
+ Assert.Single(modifier.Interpolators);
+
+ ColorInterpolator interpolator = Assert.IsType(modifier.Interpolators[0]);
+ Assert.Equal(Vector3.Zero, interpolator.StartValue);
+ Assert.Equal(Vector3.Zero, interpolator.EndValue);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_HueInterpolator_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+ AgeModifier modifier = Assert.IsType(emitter.Modifiers[0]);
+
+ Assert.Single(modifier.Interpolators);
+
+ HueInterpolator interpolator = Assert.IsType(modifier.Interpolators[0]);
+ Assert.Equal(0.0f, interpolator.StartValue);
+ Assert.Equal(0.0f, interpolator.EndValue);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_OpacityInterpolator_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+ AgeModifier modifier = Assert.IsType(emitter.Modifiers[0]);
+
+ Assert.Single(modifier.Interpolators);
+
+ OpacityInterpolator interpolator = Assert.IsType(modifier.Interpolators[0]);
+ Assert.Equal(0.0f, interpolator.StartValue);
+ Assert.Equal(0.0f, interpolator.EndValue);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_RotationInterpolator_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+ AgeModifier modifier = Assert.IsType(emitter.Modifiers[0]);
+
+ Assert.Single(modifier.Interpolators);
+
+ RotationInterpolator interpolator = Assert.IsType(modifier.Interpolators[0]);
+ Assert.Equal(0.0f, interpolator.StartValue);
+ Assert.Equal(0.0f, interpolator.EndValue);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_ScaleInterpolator_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+ AgeModifier modifier = Assert.IsType(emitter.Modifiers[0]);
+
+ Assert.Single(modifier.Interpolators);
+
+ ScaleInterpolator interpolator = Assert.IsType(modifier.Interpolators[0]);
+ Assert.Equal(0.0f, interpolator.StartValue);
+ Assert.Equal(0.0f, interpolator.EndValue);
+ }
+
+ [Fact]
+ public void ReadParticleEffect_VelocityInterpolator_ReadsExpected()
+ {
+ string xml =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ ParticleEffect effect = ReadParticleEffectFromXml(xml);
+
+ Assert.Single(effect.Emitters);
+ ParticleEmitter emitter = effect.Emitters[0];
+
+ Assert.Single(emitter.Modifiers);
+ AgeModifier modifier = Assert.IsType(emitter.Modifiers[0]);
+
+ Assert.Single(modifier.Interpolators);
+
+ VelocityInterpolator interpolator = Assert.IsType(modifier.Interpolators[0]);
+ Assert.Equal(Vector2.Zero, interpolator.StartValue);
+ Assert.Equal(Vector2.Zero, interpolator.EndValue);
+ }
+}
diff --git a/tests/MonoGame.Extended.Tests/Particles/ParticleEffectWriterTests.cs b/tests/MonoGame.Extended.Tests/Particles/ParticleEffectWriterTests.cs
new file mode 100644
index 00000000..901b6e59
--- /dev/null
+++ b/tests/MonoGame.Extended.Tests/Particles/ParticleEffectWriterTests.cs
@@ -0,0 +1,1131 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Particles.Modifiers.Containers;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+using MonoGame.Extended.Particles.Profiles;
+
+namespace MonoGame.Extended.Tests.Particles;
+
+public class ParticleEffectWriterTests
+{
+ [Fact]
+ public void WriteParticleEffect_NulLEffect_ThrowsArgumentNullException()
+ {
+ using MemoryStream stream = new MemoryStream();
+ using ParticleEffectWriter writer = new ParticleEffectWriter(stream);
+ Assert.Throws(() => writer.WriteParticleEffect(null));
+ }
+
+ [Fact]
+ public void WriteParticleEffect_EmptyEffect_WritesMinimalXml()
+ {
+ ParticleEffect effect = new ParticleEffect("EmptyEffect");
+
+ string expected =
+ $"""
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_EmptyModifiers_WritesMinimalXml()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "EmptyModifiers";
+ effect.Emitters.Add(emitter);
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_EmptyInterpolators_WritesMinimalXml()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ effect.Emitters.Add(emitter);
+ AgeModifier ageModifier = new AgeModifier();
+ emitter.Modifiers.Add(ageModifier);
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_BoxFillProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.BoxFill(1.0f, 2.0f);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_BoxProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Box(1.0f, 2.0f);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_BoxUniformProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.BoxUniform(1.0f, 2.0f);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_CircleProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Circle(1.0f, CircleRadiation.Out);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_LineProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Line(Vector2.One, 1.0f);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_LineUniformProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.LineUniform(Vector2.One, 1.0f, Vector2.One);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_PointProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_RingProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Ring(1.0f, CircleRadiation.In);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_SprayProfile_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Spray(Vector2.One, 1.0f);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_AgeModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ AgeModifier modifier = new AgeModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_CircleContainerModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ CircleContainerModifier modifier = new CircleContainerModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_DragModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ DragModifier modifier = new DragModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_LinearGravityModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ LinearGravityModifier modifier = new LinearGravityModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_OpacityFastFadeModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ OpacityFastFadeModifier modifier = new OpacityFastFadeModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_RectangleContainerModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ RectangleContainerModifier modifier = new RectangleContainerModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_RectangleLoopContainerModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ RectangleLoopContainerModifier modifier = new RectangleLoopContainerModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_RotationModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ RotationModifier modifier = new RotationModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_VelocityColorModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ VelocityColorModifier modifier = new VelocityColorModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_VelocityModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ VelocityModifier modifier = new VelocityModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_VortexModifier_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ VortexModifier modifier = new VortexModifier();
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_ColorInterpolator_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ AgeModifier modifier = new AgeModifier();
+ ColorInterpolator interpolator = new ColorInterpolator();
+ modifier.Interpolators.Add(interpolator);
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_HueInterpolator_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ AgeModifier modifier = new AgeModifier();
+ HueInterpolator interpolator = new HueInterpolator();
+ modifier.Interpolators.Add(interpolator);
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_OpacityInterpolator_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ AgeModifier modifier = new AgeModifier();
+ OpacityInterpolator interpolator = new OpacityInterpolator();
+ modifier.Interpolators.Add(interpolator);
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_RotationInterpolator_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ AgeModifier modifier = new AgeModifier();
+ RotationInterpolator interpolator = new RotationInterpolator();
+ modifier.Interpolators.Add(interpolator);
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_ScaleInterpolator_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ AgeModifier modifier = new AgeModifier();
+ ScaleInterpolator interpolator = new ScaleInterpolator();
+ modifier.Interpolators.Add(interpolator);
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ [Fact]
+ public void WriteParticleEffect_VelocityInterpolator_WritesExpected()
+ {
+ ParticleEffect effect = new ParticleEffect("TestEffect");
+ ParticleEmitter emitter = new ParticleEmitter(1);
+ emitter.Name = "TestEmitter";
+ emitter.Profile = Profile.Point();
+ AgeModifier modifier = new AgeModifier();
+ VelocityInterpolator interpolator = new VelocityInterpolator();
+ modifier.Interpolators.Add(interpolator);
+ emitter.Modifiers.Add(modifier);
+ effect.Emitters.Add(emitter);
+
+
+ string expected =
+ $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ AssertParticleEffect(effect, expected);
+ }
+
+ private void AssertParticleEffect(ParticleEffect effect, string expected)
+ {
+ string actual = string.Empty;
+ using (MemoryStream stream = new MemoryStream())
+ {
+ using (ParticleEffectWriter writer = new ParticleEffectWriter(stream))
+ {
+ writer.WriteParticleEffect(effect);
+ stream.Position = 0;
+ }
+
+ using StreamReader reader = new StreamReader(stream);
+ actual = reader.ReadToEnd();
+ }
+
+ Assert.Equal(expected, actual);
+ }
+}
diff --git a/tests/MonoGame.Extended.Tests/Particles/Profiles/PointProfileTests.cs b/tests/MonoGame.Extended.Tests/Particles/Profiles/PointProfileTests.cs
index cb22b3bd..56743b47 100644
--- a/tests/MonoGame.Extended.Tests/Particles/Profiles/PointProfileTests.cs
+++ b/tests/MonoGame.Extended.Tests/Particles/Profiles/PointProfileTests.cs
@@ -1,33 +1,35 @@
using System;
+using Microsoft.Xna.Framework;
using MonoGame.Extended.Particles.Profiles;
-using Xunit;
namespace MonoGame.Extended.Tests.Particles.Profiles
{
-
public class PointProfileTests
{
[Fact]
- public void ReturnsZeroOffset()
+ public unsafe void ReturnsZeroOffset()
{
- var subject = new PointProfile();
+ PointProfile subject = new PointProfile();
- subject.GetOffsetAndHeading(out var offset, out _);
+ Vector2 offset;
+ Vector2 heading;
+ subject.GetOffsetAndHeading(&offset, &heading);
Assert.Equal(0f, offset.X);
Assert.Equal(0f, offset.Y);
}
[Fact]
- public void ReturnsHeadingAsUnitVector()
+ public unsafe void ReturnsHeadingAsUnitVector()
{
- var subject = new PointProfile();
+ PointProfile subject = new PointProfile();
- subject.GetOffsetAndHeading(out _, out var heading);
+ Vector2 offset;
+ Vector2 heading;
+ subject.GetOffsetAndHeading(&offset, &heading);
- var length = Math.Sqrt(heading.X * heading.X + heading.Y * heading.Y);
+ double length = Math.Sqrt(heading.X * heading.X + heading.Y * heading.Y);
Assert.Equal(1f, length, 6);
}
-
}
}
diff --git a/tests/MonoGame.Extended.Tests/Particles/Profiles/RingProfileTests.cs b/tests/MonoGame.Extended.Tests/Particles/Profiles/RingProfileTests.cs
index a18f5b7b..4f716bdf 100644
--- a/tests/MonoGame.Extended.Tests/Particles/Profiles/RingProfileTests.cs
+++ b/tests/MonoGame.Extended.Tests/Particles/Profiles/RingProfileTests.cs
@@ -1,38 +1,42 @@
using System;
+using Microsoft.Xna.Framework;
using MonoGame.Extended.Particles.Profiles;
-using Xunit;
namespace MonoGame.Extended.Tests.Particles.Profiles
{
public class RingProfileTests
{
[Fact]
- public void ReturnsOffsetEqualToRadius()
+ public unsafe void ReturnsOffsetEqualToRadius()
{
- var subject = new RingProfile
+ RingProfile subject = new RingProfile
{
Radius = 10f
};
- subject.GetOffsetAndHeading(out var offset, out _);
- var length = Math.Sqrt(offset.X * offset.X + offset.Y * offset.Y);
- Assert.Equal(10f, length, 6);
+ Vector2 offset;
+ Vector2 heading;
+ subject.GetOffsetAndHeading(&offset, &heading);
+
+ double length = Math.Sqrt(offset.X * offset.X + offset.Y * offset.Y);
+ Assert.Equal(10.0f, length, precision: 5);
}
[Fact]
- public void WhenRadiateIsTrue_HeadingIsEqualToNormalizedOffset()
+ public unsafe void WhenRadiateIsTrue_HeadingIsEqualToNormalizedOffset()
{
- var subject = new RingProfile
+ RingProfile subject = new RingProfile
{
Radius = 10f,
- Radiate = Profile.CircleRadiation.Out
+ Radiate = CircleRadiation.Out
};
- subject.GetOffsetAndHeading(out var offset, out var heading);
- Assert.Equal(heading.X, offset.X / 10, 6);
- Assert.Equal(heading.Y, offset.Y / 10, 6);
+ Vector2 offset;
+ Vector2 heading;
+ subject.GetOffsetAndHeading(&offset, &heading);
+ Assert.Equal(heading.X, offset.X / 10, precision: 5);
+ Assert.Equal(heading.Y, offset.Y / 10, precision: 5);
}
-
}
}
diff --git a/tests/MonoGame.Extended.Tests/Serialization/Xml/XmlReaderExtensionsTests.cs b/tests/MonoGame.Extended.Tests/Serialization/Xml/XmlReaderExtensionsTests.cs
new file mode 100644
index 00000000..357f7604
--- /dev/null
+++ b/tests/MonoGame.Extended.Tests/Serialization/Xml/XmlReaderExtensionsTests.cs
@@ -0,0 +1,221 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System.IO;
+using System.Xml;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Serialization.Xml;
+
+namespace MonoGame.Extended.Tests.Serialization.Xml;
+
+public class XmlReaderExtensionsTests
+{
+ private static XmlReader CreateXmlReader(string xml)
+ {
+ StringReader stringReader = new StringReader(xml);
+ XmlReader xmlReader = XmlReader.Create(stringReader);
+ xmlReader.MoveToContent();
+ return xmlReader;
+ }
+
+ [Fact]
+ public void GetAttributeInt_ValidValue_ReturnsCorrectInt()
+ {
+ int expected = 42;
+
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ int actual = reader.GetAttributeInt("testAttr");
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void GetAttributeInt_MissingAttribute_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Fact]
+ public void GetAttributeInt_InvalidFormat_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Fact]
+ public void GetAttributeFloat_ValidValue_ReturnsCorrectFloat()
+ {
+ float expected = 3.14f;
+
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ float actual = reader.GetAttributeFloat("testAttr");
+
+ Assert.Equal(expected, actual, precision: 5);
+ }
+
+ [Fact]
+ public void GetAttributeFloat_MissingAttribute_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Fact]
+ public void GetAttributeFloat_InvalidFormat_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Theory]
+ [InlineData("true", true)]
+ [InlineData("false", false)]
+ [InlineData("True", true)]
+ [InlineData("False", false)]
+ public void GetAttributeBool_ValidValues_ReturnsCorrectBool(string value, bool expected)
+ {
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ bool actual = reader.GetAttributeBool("testAttr");
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void GetAttributeBool_InvalidFormat_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Theory]
+ [InlineData(nameof(PlayerIndex.One), PlayerIndex.One)]
+ [InlineData(nameof(PlayerIndex.Two), PlayerIndex.Two)]
+ [InlineData(nameof(PlayerIndex.Three), PlayerIndex.Three)]
+ [InlineData(nameof(PlayerIndex.Four), PlayerIndex.Four)]
+ public void GetAttributeEnum_ValidValues_ReturnsCorrectEnum(string value, PlayerIndex expected)
+ {
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ PlayerIndex actual = reader.GetAttributeEnum("testAttr");
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void GetAttributeEnum_MissingAttribute_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Fact]
+ public void GetAttributeEnum_InvalidValue_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Fact]
+ public void GetAttributeRectangle_ValidValue_ReturnsCorrectRectangle()
+ {
+ Rectangle expected = new Rectangle(1, 2, 3, 4);
+
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ Rectangle actual = reader.GetAttributeRectangle("testAttr");
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void GetAttributeRectangle_MissingAttribute_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Theory]
+ [InlineData("1,2,3")]
+ [InlineData("1,2,3,4,5")]
+ [InlineData("a,b,c,d")]
+ [InlineData("1.5,2,3,4")]
+ public void GetAttributeRectangle_InvalidFormat_ThrowsXmlException(string value)
+ {
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Fact]
+ public void GetAttributeVector2_ValidValue_ReturnsCorrectVector2()
+ {
+ Vector2 expected = new Vector2(1.0f, 2.0f);
+
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ Vector2 actual = reader.GetAttributeVector2("testAttr");
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void GetAttributeVector2_MissingAttribute_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Theory]
+ [InlineData("3.14")]
+ [InlineData("1,2,3")]
+ [InlineData("a,b")]
+ public void GetAttributeVector2_InvalidFormat_ThrowsXmlException(string value)
+ {
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Fact]
+ public void GetAttributeVector3_ValidValue_ReturnsCorrectVector3()
+ {
+ Vector3 expected = new Vector3(1.0f, 2.0f, 3.0f);
+
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ Vector3 actual = reader.GetAttributeVector3("testAttr");
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void GetAttributeVector3_MissingAttribute_ThrowsXmlException()
+ {
+ string xml = "";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+
+ [Theory]
+ [InlineData("1.0,2.0")]
+ [InlineData("1,2,3,4")]
+ [InlineData("a,b,c")]
+ public void GetAttributeVector3_InvalidFormat_ThrowsXmlException(string value)
+ {
+ string xml = $"";
+ using XmlReader reader = CreateXmlReader(xml);
+ Assert.Throws(() => reader.GetAttributeInt("testAttr"));
+ }
+}
diff --git a/tests/MonoGame.Extended.Tests/Serialization/Xml/XmlWriterExtensionsTests.cs b/tests/MonoGame.Extended.Tests/Serialization/Xml/XmlWriterExtensionsTests.cs
new file mode 100644
index 00000000..c20c571d
--- /dev/null
+++ b/tests/MonoGame.Extended.Tests/Serialization/Xml/XmlWriterExtensionsTests.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Craftwork Games. All rights reserved.
+// Licensed under the MIT license.
+// See LICENSE file in the project root for full license information.
+
+using System.Text;
+using System.Xml;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Serialization.Xml;
+
+namespace MonoGame.Extended.Tests.Serialization.Xml;
+
+public class XmlWriterExtensionsTests
+{
+ private static (XmlWriter writer, StringBuilder output) CreateXmlWriter()
+ {
+ StringBuilder output = new StringBuilder();
+
+ XmlWriterSettings settings = new XmlWriterSettings();
+ settings.OmitXmlDeclaration = true;
+ settings.Indent = true;
+
+ XmlWriter writer = XmlWriter.Create(output, settings);
+ return (writer, output);
+ }
+
+ [Fact]
+ public void WriteAttributeInt_WritesCorrectAttribute()
+ {
+ (XmlWriter writer, StringBuilder output) = CreateXmlWriter();
+
+ writer.WriteStartElement("test");
+ writer.WriteAttributeInt("value", 1);
+ writer.WriteEndElement();
+ writer.Flush();
+
+ Assert.Equal("", output.ToString());
+ }
+
+ [Fact]
+ public void WriteAttributeFloat_WritesCorrectAttribute()
+ {
+ (XmlWriter writer, StringBuilder output) = CreateXmlWriter();
+
+ writer.WriteStartElement("test");
+ writer.WriteAttributeFloat("value", 1.2f);
+ writer.WriteEndElement();
+ writer.Flush();
+
+ Assert.Equal("", output.ToString());
+ }
+
+ [Theory]
+ [InlineData(true, "True")]
+ [InlineData(false, "False")]
+ public void WriteAttributeBool_WritesCorrectAttribute(bool value, string expected)
+ {
+ (XmlWriter writer, StringBuilder output) = CreateXmlWriter();
+
+ writer.WriteStartElement("test");
+ writer.WriteAttributeBool("value", value);
+ writer.WriteEndElement();
+ writer.Flush();
+
+ Assert.Equal($"", output.ToString());
+ }
+
+ [Fact]
+ public void WriteAttributeRectangle_WritesCorrectAttribute()
+ {
+ (XmlWriter writer, StringBuilder output) = CreateXmlWriter();
+
+ Rectangle rectangle = new Rectangle(1, 2, 3, 4);
+
+ writer.WriteStartElement("test");
+ writer.WriteAttributeRectangle("value", rectangle);
+ writer.WriteEndElement();
+ writer.Flush();
+
+ Assert.Equal($"", output.ToString());
+ }
+
+ [Fact]
+ public void WriteAttributeVector2_WriteCorrectValue()
+ {
+ (XmlWriter writer, StringBuilder output) = CreateXmlWriter();
+
+ Vector2 vector = new Vector2(1.1f, 2.2f);
+
+ writer.WriteStartElement("test");
+ writer.WriteAttributeVector2("value", vector);
+ writer.WriteEndElement();
+ writer.Flush();
+
+ Assert.Equal($"", output.ToString());
+ }
+
+ [Fact]
+ public void WriteAttributeVector3_WriteCorrectValue()
+ {
+ (XmlWriter writer, StringBuilder output) = CreateXmlWriter();
+
+ Vector3 vector = new Vector3(1.1f, 2.2f, 3.3f);
+
+ writer.WriteStartElement("test");
+ writer.WriteAttributeVector3("value", vector);
+ writer.WriteEndElement();
+ writer.Flush();
+
+ Assert.Equal($"", output.ToString());
+ }
+}