| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- // 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;
- /// <summary>
- /// Represents a particle emitter that creates, manages, and updates particles within a particle system.
- /// </summary>
- /// <remarks>
- /// The <see cref="ParticleEmitter"/> 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.
- /// </remarks>
- public sealed unsafe class ParticleEmitter : IDisposable
- {
- private float _totalSeconds;
- private float _secondsSinceLastReclaim;
- private ParticleBuffer _buffer;
- /// <summary>
- /// Gets the buffer that stores and manages the particles for this emitter.
- /// </summary>
- public ParticleBuffer Buffer => _buffer;
- /// <summary>
- /// Gets or sets the name of this emitter, used for identification and debugging.
- /// </summary>
- public string Name { get; set; }
- /// <summary>
- /// Gets the maximum number of particles that this emitter can manage.
- /// </summary>
- /// <value>The size of the underlying <see cref="ParticleBuffer"/>.</value>
- public int Capacity
- {
- get
- {
- return Buffer.Size;
- }
- }
- /// <summary>
- /// Gets the current number of active particles in this emitter.
- /// </summary>
- /// <value>The count of particles in the underlying <see cref="ParticleBuffer"/>.</value>
- public int ActiveParticles
- {
- get
- {
- return Buffer.Count;
- }
- }
- /// <summary>
- /// Gets or sets the lifespan of particles emitted by this emitter, in seconds.
- /// </summary>
- /// <remarks>
- /// After a particle's age exceeds this value, it will be automatically reclaimed during the next cleanup cycle.
- /// </remarks>
- public float LifeSpan { get; set; }
- /// <summary>
- /// Gets or sets the position offset applied to this emitter.
- /// </summary>
- /// <remarks>
- /// 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 <see cref="Update"/> method.
- /// </remarks>
- public Vector2 Offset { get; set; }
- /// <summary>
- /// Gets or sets the default layer depth for particles emitted by this emitter.
- /// </summary>
- /// <remarks>
- /// This value determines the rendering order of particles relative to other sprites and particles.
- /// Values range from 0.0 (front) to 1.0 (back).
- /// </remarks>
- public float LayerDepth { get; set; }
- /// <summary>
- /// Gets or sets the frequency, in times per second, at which expired particles are reclaimed.
- /// </summary>
- /// <remarks>
- /// Higher values result in more frequent cleanup of expired particles, potentially improving memory
- /// utilization at the cost of slightly increased CPU usage.
- /// </remarks>
- public float ReclaimFrequency { get; set; }
- /// <summary>
- /// Gets or sets the parameters that control the physical and visual properties of emitted particles.
- /// </summary>
- /// <remarks>
- /// These parameters include properties such as initial speed, color, opacity, scale, rotation, and mass.
- /// </remarks>
- public ParticleReleaseParameters Parameters { get; set; }
- /// <summary>
- /// Gets or sets the strategy used to execute modifiers on particles.
- /// </summary>
- /// <remarks>
- /// 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.
- /// </remarks>
- public ModifierExecutionStrategy ModifierExecutionStrategy { get; set; }
- /// <summary>
- /// Gets or sets the list of modifiers that affect particles emitted by this emitter.
- /// </summary>
- /// <remarks>
- /// Modifiers alter particle properties over time, creating effects such as gravity, color changes,
- /// rotation, and containment within boundaries.
- /// </remarks>
- public List<Modifier> Modifiers { get; set; }
- /// <summary>
- /// Gets or sets the profile that determines the initial position and heading of emitted particles.
- /// </summary>
- /// <remarks>
- /// Profiles define the emission pattern, such as points, lines, rings, or areas from which particles originate.
- /// </remarks>
- public Profile Profile { get; set; }
- /// <summary>
- /// The <see cref="Texture2DRegion"/> to use when rendering particles from this emitter.
- /// </summary>
- public Texture2DRegion TextureRegion { get; set; }
- /// <summary>
- /// Gets or sets the order in which particles are rendered within this emitter.
- /// </summary>
- /// <remarks>
- /// This property determines whether particles are drawn front-to-back or back-to-front,
- /// affecting how they visually overlap when using alpha blending.
- /// </remarks>
- public ParticleRenderingOrder RenderingOrder { get; set; }
- /// <summary>
- /// Get or sets a value that indicates whether the particles emitted by this emitter are visible.
- /// </summary>
- public bool Visible { get; set; }
- /// <summary>
- /// Gets a value indicating whether this <see cref="ParticleEmitter"/> has been disposed.
- /// </summary>
- /// <value><see langword="true"/> if the emitter has been disposed; otherwise, <see langword="false"/>.</value>
- public bool IsDisposed { get; private set; }
- /// <summary>
- /// Initializes a new instance of the <see cref="ParticleEmitter"/> class with default capacity.
- /// </summary>
- /// <remarks>
- /// Creates an emitter with a capacity of 1000 particles and default settings.
- /// </remarks>
- public ParticleEmitter() : this(1000) { }
- /// <summary>
- /// Initializes a new instance of the <see cref="ParticleEmitter"/> class with the specified capacity.
- /// </summary>
- /// <param name="initialCapacity">The maximum number of particles this emitter can manage.</param>
- /// <remarks>
- /// This constructor initializes the emitter with default settings but allows for specifying
- /// the maximum number of particles it can handle.
- /// </remarks>
- public ParticleEmitter(int initialCapacity)
- {
- LifeSpan = 1.0f;
- Name = nameof(ParticleEmitter);
- TextureRegion = null;
- _buffer = new ParticleBuffer(initialCapacity);
- Profile = Profile.Point();
- Modifiers = new List<Modifier>();
- ModifierExecutionStrategy = ModifierExecutionStrategy.Serial;
- Parameters = new ParticleReleaseParameters();
- ReclaimFrequency = 60.0f;
- Offset = Vector2.Zero;
- LayerDepth = 0.0f;
- Visible = true;
- }
- /// <summary>
- /// Finalizes an instance of the <see cref="ParticleEmitter"/> class.
- /// </summary>
- ~ParticleEmitter()
- {
- Dispose(false);
- }
- /// <summary>
- /// Changes the maximum capacity of this emitter.
- /// </summary>
- /// <param name="size">The new maximum number of particles this emitter can manage.</param>
- /// <remarks>
- /// This method disposes the old buffer and creates a new one with the specified capacity.
- /// Any existing particles are lost during this operation.
- /// </remarks>
- /// <exception cref="ObjectDisposedException">
- /// Thrown if this method is called after the emitter has been disposed.
- /// </exception>
- 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);
- }
- /// <summary>
- /// Updates the state of all particles managed by this emitter.
- /// </summary>
- /// <param name="elapsedSeconds">The elapsed time, in seconds, since the last update.</param>
- /// <param name="position">The current position of the emitter in 2D space.</param>
- /// <remarks>
- /// 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.
- /// </remarks>
- /// <exception cref="ObjectDisposedException">
- /// Thrown if this method is called after the emitter has been disposed.
- /// </exception>
- 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);
- }
- }
- /// <summary>
- /// Triggers the emission of particles at the specified position.
- /// </summary>
- /// <param name="position">The position in 2D space from which to emit particles.</param>
- /// <param name="layerDepth">The layer depth at which to render the emitted particles.</param>
- /// <remarks>
- /// This method creates a burst of particles according to the configured <see cref="Parameters"/>.
- /// The number of particles released is determined by the <see cref="ParticleReleaseParameters.Quantity"/> property.
- /// </remarks>
- public void Trigger(Vector2 position, float layerDepth = 0)
- {
- int numToRelease = Parameters.Quantity.Value;
- Release(position, numToRelease, layerDepth);
- }
- /// <summary>
- /// Triggers the emission of particles along a line segment.
- /// </summary>
- /// <param name="line">The line segment along which to distribute emitted particles.</param>
- /// <param name="layerDepth">The layer depth at which to render the emitted particles.</param>
- /// <remarks>
- /// This method creates particles at random positions along the specified line segment.
- /// The number of particles released is determined by the <see cref="ParticleReleaseParameters.Quantity"/> property.
- /// </remarks>
- 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);
- }
- }
- /// <summary>
- /// Releases a specified number of particles at the given position.
- /// </summary>
- /// <param name="position">The position in 2D space from which to emit particles.</param>
- /// <param name="numToRelease">The number of particles to release.</param>
- /// <param name="layerDepth">The layer depth at which to render the emitted particles.</param>
- /// <remarks>
- /// This method initializes newly created particles with properties based on the emitter's
- /// <see cref="Profile"/> and <see cref="Parameters"/>.
- /// </remarks>
- 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;
- }
- }
- /// <summary>
- /// Reclaims particles that have exceeded their lifespan.
- /// </summary>
- /// <remarks>
- /// This method removes expired particles from the beginning of the buffer and compacts
- /// the remaining particles to maintain efficient memory usage.
- /// </remarks>
- 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);
- }
- }
- /// <summary>
- /// Returns a string that represents the current emitter.
- /// </summary>
- /// <returns>The <see cref="Name"/> of this emitter.</returns>
- public override string ToString()
- {
- return Name;
- }
- /// <summary>
- /// Releases all resources used by the <see cref="ParticleEmitter"/>.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
- /// <summary>
- /// Releases the unmanaged resources used by the <see cref="ParticleEmitter"/>.
- /// </summary>
- /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
- /// <see langword="false"/> to release only unmanaged resources.</param>
- private void Dispose(bool disposing)
- {
- if (IsDisposed) { return; }
- if (disposing)
- {
- // No managed objects
- }
- Buffer.Dispose();
- IsDisposed = true;
- }
- }
|