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