// 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 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; /// /// 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 { private float _totalSeconds; private float _secondsSinceLastReclaim; private ParticleBuffer _buffer; /// /// Gets the buffer that stores and manages the particles for this emitter. /// public ParticleBuffer Buffer => _buffer; /// /// Gets or sets the name of this emitter, used for identification and debugging. /// public string Name { get; set; } /// /// Gets the maximum number of particles that this emitter can manage. /// /// The size of the underlying . public int Capacity { get { return Buffer.Size; } } /// /// Gets the current number of active particles in this emitter. /// /// The count of particles in the underlying . public int ActiveParticles { get { return Buffer.Count; } } /// /// 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 { get; set; } /// /// 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 { get; set; } /// /// 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 { get; set; } /// /// 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 { get; set; } /// /// 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 { get; set; } /// /// 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 { get; set; } /// /// 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 { get; set; } /// /// 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 { get; set; } /// /// The to use when rendering particles from this emitter. /// public Texture2DRegion TextureRegion { get; set; } /// /// 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 { get; set; } /// /// Get or sets a value that indicates whether the particles emitted by this emitter are visible. /// public bool Visible { get; set; } /// /// 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; Visible = true; } /// /// 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; } if (Buffer is ParticleBuffer oldBuffer) { oldBuffer.Dispose(); } _buffer = new ParticleBuffer(size); } /// /// 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 (Buffer.Count == 0) { return; } if (_secondsSinceLastReclaim > (1.0f / ReclaimFrequency)) { ReclaimExpiredParticles(); _secondsSinceLastReclaim -= (1.0f / ReclaimFrequency); } if (Buffer.Count > 0) { ParticleIterator iterator = Buffer.Iterator; while (iterator.HasNext) { 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); } } /// /// 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; Vector2 scale = Parameters.Scale.Value; particle->Scale[0] = scale.X; particle->Scale[1] = scale.Y; 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; } }