ParticleEmitter.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. // Copyright (c) Craftwork Games. All rights reserved.
  2. // Licensed under the MIT license.
  3. // See LICENSE file in the project root for full license information.
  4. using System;
  5. using System.Collections.Generic;
  6. using Microsoft.Xna.Framework;
  7. using MonoGame.Extended.Graphics;
  8. using MonoGame.Extended.Particles.Data;
  9. using MonoGame.Extended.Particles.Modifiers;
  10. using MonoGame.Extended.Particles.Primitives;
  11. using MonoGame.Extended.Particles.Profiles;
  12. namespace MonoGame.Extended.Particles;
  13. /// <summary>
  14. /// Represents a particle emitter that creates, manages, and updates particles within a particle system.
  15. /// </summary>
  16. /// <remarks>
  17. /// The <see cref="ParticleEmitter"/> class is the core component of the particle system. It handles particle
  18. /// creation (triggering), lifecycle management, and application of modifiers according to defined profiles
  19. /// and parameters. Each emitter operates independently and can be configured with different behaviors, appearances,
  20. /// and physical properties.
  21. /// </remarks>
  22. public sealed unsafe class ParticleEmitter : IDisposable
  23. {
  24. private float _totalSeconds;
  25. private float _secondsSinceLastReclaim;
  26. private ParticleBuffer _buffer;
  27. /// <summary>
  28. /// Gets the buffer that stores and manages the particles for this emitter.
  29. /// </summary>
  30. public ParticleBuffer Buffer => _buffer;
  31. /// <summary>
  32. /// Gets or sets the name of this emitter, used for identification and debugging.
  33. /// </summary>
  34. public string Name { get; set; }
  35. /// <summary>
  36. /// Gets the maximum number of particles that this emitter can manage.
  37. /// </summary>
  38. /// <value>The size of the underlying <see cref="ParticleBuffer"/>.</value>
  39. public int Capacity
  40. {
  41. get
  42. {
  43. return Buffer.Size;
  44. }
  45. }
  46. /// <summary>
  47. /// Gets the current number of active particles in this emitter.
  48. /// </summary>
  49. /// <value>The count of particles in the underlying <see cref="ParticleBuffer"/>.</value>
  50. public int ActiveParticles
  51. {
  52. get
  53. {
  54. return Buffer.Count;
  55. }
  56. }
  57. /// <summary>
  58. /// Gets or sets the lifespan of particles emitted by this emitter, in seconds.
  59. /// </summary>
  60. /// <remarks>
  61. /// After a particle's age exceeds this value, it will be automatically reclaimed during the next cleanup cycle.
  62. /// </remarks>
  63. public float LifeSpan { get; set; }
  64. /// <summary>
  65. /// Gets or sets the position offset applied to this emitter.
  66. /// </summary>
  67. /// <remarks>
  68. /// This offset is applied to the emitter's position when triggering particles, allowing for fine adjustment
  69. /// of the emission point without changing the overall position passed to the <see cref="Update"/> method.
  70. /// </remarks>
  71. public Vector2 Offset { get; set; }
  72. /// <summary>
  73. /// Gets or sets the default layer depth for particles emitted by this emitter.
  74. /// </summary>
  75. /// <remarks>
  76. /// This value determines the rendering order of particles relative to other sprites and particles.
  77. /// Values range from 0.0 (front) to 1.0 (back).
  78. /// </remarks>
  79. public float LayerDepth { get; set; }
  80. /// <summary>
  81. /// Gets or sets the frequency, in times per second, at which expired particles are reclaimed.
  82. /// </summary>
  83. /// <remarks>
  84. /// Higher values result in more frequent cleanup of expired particles, potentially improving memory
  85. /// utilization at the cost of slightly increased CPU usage.
  86. /// </remarks>
  87. public float ReclaimFrequency { get; set; }
  88. /// <summary>
  89. /// Gets or sets the parameters that control the physical and visual properties of emitted particles.
  90. /// </summary>
  91. /// <remarks>
  92. /// These parameters include properties such as initial speed, color, opacity, scale, rotation, and mass.
  93. /// </remarks>
  94. public ParticleReleaseParameters Parameters { get; set; }
  95. /// <summary>
  96. /// Gets or sets the strategy used to execute modifiers on particles.
  97. /// </summary>
  98. /// <remarks>
  99. /// This determines whether modifiers are executed serially (single-threaded) or in parallel (multi-threaded),
  100. /// affecting performance characteristics based on the system's capabilities and the number of particles.
  101. /// </remarks>
  102. public ModifierExecutionStrategy ModifierExecutionStrategy { get; set; }
  103. /// <summary>
  104. /// Gets or sets the list of modifiers that affect particles emitted by this emitter.
  105. /// </summary>
  106. /// <remarks>
  107. /// Modifiers alter particle properties over time, creating effects such as gravity, color changes,
  108. /// rotation, and containment within boundaries.
  109. /// </remarks>
  110. public List<Modifier> Modifiers { get; set; }
  111. /// <summary>
  112. /// Gets or sets the profile that determines the initial position and heading of emitted particles.
  113. /// </summary>
  114. /// <remarks>
  115. /// Profiles define the emission pattern, such as points, lines, rings, or areas from which particles originate.
  116. /// </remarks>
  117. public Profile Profile { get; set; }
  118. /// <summary>
  119. /// The <see cref="Texture2DRegion"/> to use when rendering particles from this emitter.
  120. /// </summary>
  121. public Texture2DRegion TextureRegion { get; set; }
  122. /// <summary>
  123. /// Gets or sets the order in which particles are rendered within this emitter.
  124. /// </summary>
  125. /// <remarks>
  126. /// This property determines whether particles are drawn front-to-back or back-to-front,
  127. /// affecting how they visually overlap when using alpha blending.
  128. /// </remarks>
  129. public ParticleRenderingOrder RenderingOrder { get; set; }
  130. /// <summary>
  131. /// Get or sets a value that indicates whether the particles emitted by this emitter are visible.
  132. /// </summary>
  133. public bool Visible { get; set; }
  134. /// <summary>
  135. /// Gets a value indicating whether this <see cref="ParticleEmitter"/> has been disposed.
  136. /// </summary>
  137. /// <value><see langword="true"/> if the emitter has been disposed; otherwise, <see langword="false"/>.</value>
  138. public bool IsDisposed { get; private set; }
  139. /// <summary>
  140. /// Initializes a new instance of the <see cref="ParticleEmitter"/> class with default capacity.
  141. /// </summary>
  142. /// <remarks>
  143. /// Creates an emitter with a capacity of 1000 particles and default settings.
  144. /// </remarks>
  145. public ParticleEmitter() : this(1000) { }
  146. /// <summary>
  147. /// Initializes a new instance of the <see cref="ParticleEmitter"/> class with the specified capacity.
  148. /// </summary>
  149. /// <param name="initialCapacity">The maximum number of particles this emitter can manage.</param>
  150. /// <remarks>
  151. /// This constructor initializes the emitter with default settings but allows for specifying
  152. /// the maximum number of particles it can handle.
  153. /// </remarks>
  154. public ParticleEmitter(int initialCapacity)
  155. {
  156. LifeSpan = 1.0f;
  157. Name = nameof(ParticleEmitter);
  158. TextureRegion = null;
  159. _buffer = new ParticleBuffer(initialCapacity);
  160. Profile = Profile.Point();
  161. Modifiers = new List<Modifier>();
  162. ModifierExecutionStrategy = ModifierExecutionStrategy.Serial;
  163. Parameters = new ParticleReleaseParameters();
  164. ReclaimFrequency = 60.0f;
  165. Offset = Vector2.Zero;
  166. LayerDepth = 0.0f;
  167. Visible = true;
  168. }
  169. /// <summary>
  170. /// Finalizes an instance of the <see cref="ParticleEmitter"/> class.
  171. /// </summary>
  172. ~ParticleEmitter()
  173. {
  174. Dispose(false);
  175. }
  176. /// <summary>
  177. /// Changes the maximum capacity of this emitter.
  178. /// </summary>
  179. /// <param name="size">The new maximum number of particles this emitter can manage.</param>
  180. /// <remarks>
  181. /// This method disposes the old buffer and creates a new one with the specified capacity.
  182. /// Any existing particles are lost during this operation.
  183. /// </remarks>
  184. /// <exception cref="ObjectDisposedException">
  185. /// Thrown if this method is called after the emitter has been disposed.
  186. /// </exception>
  187. public void ChangeCapacity(int size)
  188. {
  189. ObjectDisposedException.ThrowIf(IsDisposed, typeof(ParticleBuffer));
  190. if (Capacity == size)
  191. {
  192. return;
  193. }
  194. if (Buffer is ParticleBuffer oldBuffer)
  195. {
  196. oldBuffer.Dispose();
  197. }
  198. _buffer = new ParticleBuffer(size);
  199. }
  200. /// <summary>
  201. /// Updates the state of all particles managed by this emitter.
  202. /// </summary>
  203. /// <param name="elapsedSeconds">The elapsed time, in seconds, since the last update.</param>
  204. /// <param name="position">The current position of the emitter in 2D space.</param>
  205. /// <remarks>
  206. /// This method handles automatic triggering of particle emissions, updates the positions of all active
  207. /// particles based on their velocities, applies all registered modifiers, and reclaims expired particles.
  208. /// </remarks>
  209. /// <exception cref="ObjectDisposedException">
  210. /// Thrown if this method is called after the emitter has been disposed.
  211. /// </exception>
  212. public void Update(float elapsedSeconds, Vector2 position = default)
  213. {
  214. ObjectDisposedException.ThrowIf(IsDisposed, typeof(ParticleBuffer));
  215. _totalSeconds += elapsedSeconds;
  216. _secondsSinceLastReclaim += elapsedSeconds;
  217. if (Buffer.Count == 0)
  218. {
  219. return;
  220. }
  221. if (_secondsSinceLastReclaim > (1.0f / ReclaimFrequency))
  222. {
  223. ReclaimExpiredParticles();
  224. _secondsSinceLastReclaim -= (1.0f / ReclaimFrequency);
  225. }
  226. if (Buffer.Count > 0)
  227. {
  228. ParticleIterator iterator = Buffer.Iterator;
  229. while (iterator.HasNext)
  230. {
  231. Particle* particle = iterator.Next();
  232. particle->Age = (_totalSeconds - particle->Inception) / LifeSpan;
  233. particle->Position[0] += particle->Velocity[0] * elapsedSeconds;
  234. particle->Position[1] += particle->Velocity[1] * elapsedSeconds;
  235. }
  236. ModifierExecutionStrategy.ExecuteModifiers(Modifiers, elapsedSeconds, iterator);
  237. }
  238. }
  239. /// <summary>
  240. /// Triggers the emission of particles at the specified position.
  241. /// </summary>
  242. /// <param name="position">The position in 2D space from which to emit particles.</param>
  243. /// <param name="layerDepth">The layer depth at which to render the emitted particles.</param>
  244. /// <remarks>
  245. /// This method creates a burst of particles according to the configured <see cref="Parameters"/>.
  246. /// The number of particles released is determined by the <see cref="ParticleReleaseParameters.Quantity"/> property.
  247. /// </remarks>
  248. public void Trigger(Vector2 position, float layerDepth = 0)
  249. {
  250. int numToRelease = Parameters.Quantity.Value;
  251. Release(position, numToRelease, layerDepth);
  252. }
  253. /// <summary>
  254. /// Triggers the emission of particles along a line segment.
  255. /// </summary>
  256. /// <param name="line">The line segment along which to distribute emitted particles.</param>
  257. /// <param name="layerDepth">The layer depth at which to render the emitted particles.</param>
  258. /// <remarks>
  259. /// This method creates particles at random positions along the specified line segment.
  260. /// The number of particles released is determined by the <see cref="ParticleReleaseParameters.Quantity"/> property.
  261. /// </remarks>
  262. public void Trigger(LineSegment line, float layerDepth = 0)
  263. {
  264. int numToRelease = Parameters.Quantity.Value;
  265. Vector2 lineVector = line.ToVector2();
  266. for (int i = 0; i < numToRelease; i++)
  267. {
  268. Vector2 offset = lineVector * FastRandom.Shared.NextSingle();
  269. Release(line.Origin + offset, 1, layerDepth);
  270. }
  271. }
  272. /// <summary>
  273. /// Releases a specified number of particles at the given position.
  274. /// </summary>
  275. /// <param name="position">The position in 2D space from which to emit particles.</param>
  276. /// <param name="numToRelease">The number of particles to release.</param>
  277. /// <param name="layerDepth">The layer depth at which to render the emitted particles.</param>
  278. /// <remarks>
  279. /// This method initializes newly created particles with properties based on the emitter's
  280. /// <see cref="Profile"/> and <see cref="Parameters"/>.
  281. /// </remarks>
  282. private void Release(Vector2 position, int numToRelease, float layerDepth)
  283. {
  284. ParticleIterator iterator = Buffer.Release(numToRelease);
  285. while (iterator.HasNext)
  286. {
  287. Particle* particle = iterator.Next();
  288. Profile.GetOffsetAndHeading((Vector2*)particle->Position, (Vector2*)particle->Velocity);
  289. particle->Age = 0.0f;
  290. particle->Inception = _totalSeconds;
  291. particle->Position[0] += position.X;
  292. particle->Position[1] += position.Y;
  293. particle->TriggeredPos[0] = position.X;
  294. particle->TriggeredPos[1] = position.Y;
  295. float speed = Parameters.Speed.Value;
  296. particle->Velocity[0] *= speed;
  297. particle->Velocity[1] *= speed;
  298. Vector3 color = Parameters.Color.Value;
  299. particle->Color[0] = color.X;
  300. particle->Color[1] = color.Y;
  301. particle->Color[2] = color.Z;
  302. particle->Opacity = Parameters.Opacity.Value;
  303. Vector2 scale = Parameters.Scale.Value;
  304. particle->Scale[0] = scale.X;
  305. particle->Scale[1] = scale.Y;
  306. particle->Rotation = Parameters.Rotation.Value;
  307. particle->Mass = Parameters.Mass.Value;
  308. particle->LayerDepth = layerDepth;
  309. }
  310. }
  311. /// <summary>
  312. /// Reclaims particles that have exceeded their lifespan.
  313. /// </summary>
  314. /// <remarks>
  315. /// This method removes expired particles from the beginning of the buffer and compacts
  316. /// the remaining particles to maintain efficient memory usage.
  317. /// </remarks>
  318. private void ReclaimExpiredParticles()
  319. {
  320. int expired = 0;
  321. ParticleIterator iterator = Buffer.Iterator;
  322. while (iterator.HasNext)
  323. {
  324. Particle* particle = iterator.Next();
  325. if ((_totalSeconds - particle->Inception) < LifeSpan)
  326. {
  327. break;
  328. }
  329. expired++;
  330. }
  331. if (expired != 0)
  332. {
  333. Buffer.Reclaim(expired);
  334. }
  335. }
  336. /// <summary>
  337. /// Returns a string that represents the current emitter.
  338. /// </summary>
  339. /// <returns>The <see cref="Name"/> of this emitter.</returns>
  340. public override string ToString()
  341. {
  342. return Name;
  343. }
  344. /// <summary>
  345. /// Releases all resources used by the <see cref="ParticleEmitter"/>.
  346. /// </summary>
  347. public void Dispose()
  348. {
  349. Dispose(true);
  350. GC.SuppressFinalize(this);
  351. }
  352. /// <summary>
  353. /// Releases the unmanaged resources used by the <see cref="ParticleEmitter"/>.
  354. /// </summary>
  355. /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
  356. /// <see langword="false"/> to release only unmanaged resources.</param>
  357. private void Dispose(bool disposing)
  358. {
  359. if (IsDisposed) { return; }
  360. if (disposing)
  361. {
  362. // No managed objects
  363. }
  364. Buffer.Dispose();
  365. IsDisposed = true;
  366. }
  367. }