• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

djeedai / bevy_hanabi / 13886176091

16 Mar 2025 06:23PM UTC coverage: 40.141% (-0.003%) from 40.144%
13886176091

Pull #437

github

web-flow
Merge 6d7350782 into 7e122b2be
Pull Request #437: Color changes to allow random firework colors

25 of 45 new or added lines in 4 files covered. (55.56%)

52 existing lines in 1 file now uncovered.

3245 of 8084 relevant lines covered (40.14%)

18.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.32
/src/spawn.rs
1
use std::hash::{Hash, Hasher};
2

3
use bevy::{ecs::system::Resource, math::FloatOrd, prelude::*, reflect::Reflect};
4
use rand::{
5
    distributions::{uniform::SampleUniform, Distribution, Uniform},
6
    SeedableRng,
7
};
8
use rand_pcg::Pcg32;
9
use serde::{Deserialize, Serialize};
10

11
use crate::{EffectAsset, EffectSimulation, ParticleEffect, SimulationCondition};
12

13
/// An RNG to be used in the CPU for the particle system engine
14
pub(crate) fn new_rng() -> Pcg32 {
17✔
15
    let mut rng = rand::thread_rng();
17✔
16
    let mut seed = [0u8; 16];
17✔
17
    seed.copy_from_slice(&Uniform::from(0..=u128::MAX).sample(&mut rng).to_le_bytes());
17✔
18
    Pcg32::from_seed(seed)
17✔
19
}
20

21
/// An RNG resource
22
#[derive(Resource)]
23
pub struct Random(pub Pcg32);
24

25
/// Utility trait to help implementing [`std::hash::Hash`] for [`CpuValue`] of
26
/// floating-point type.
27
pub trait FloatHash: PartialEq {
28
    fn hash_f32<H: Hasher>(&self, state: &mut H);
29
}
30

31
impl FloatHash for f32 {
32
    fn hash_f32<H: Hasher>(&self, state: &mut H) {
×
33
        FloatOrd(*self).hash(state);
×
34
    }
35
}
36

37
impl FloatHash for Vec2 {
38
    fn hash_f32<H: Hasher>(&self, state: &mut H) {
×
39
        FloatOrd(self.x).hash(state);
×
40
        FloatOrd(self.y).hash(state);
×
41
    }
42
}
43

44
impl FloatHash for Vec3 {
45
    fn hash_f32<H: Hasher>(&self, state: &mut H) {
×
46
        FloatOrd(self.x).hash(state);
×
47
        FloatOrd(self.y).hash(state);
×
48
        FloatOrd(self.z).hash(state);
×
49
    }
50
}
51

52
impl FloatHash for Vec4 {
53
    fn hash_f32<H: Hasher>(&self, state: &mut H) {
×
54
        FloatOrd(self.x).hash(state);
×
55
        FloatOrd(self.y).hash(state);
×
56
        FloatOrd(self.z).hash(state);
×
57
        FloatOrd(self.w).hash(state);
×
58
    }
59
}
60

61
/// A constant or random value evaluated on CPU.
62
///
63
/// This enum represents a value which is either constant, or randomly sampled
64
/// according to a given probability distribution.
65
///
66
/// Not to be confused with [`graph::Value`]. This [`CpuValue`] is a legacy type
67
/// that will be eventually replaced with a [`graph::Value`] once evaluation of
68
/// the latter can be emulated on CPU, which is required for use
69
/// with the [`SpawnerSettings`].
70
///
71
/// [`graph::Value`]: crate::graph::Value
72
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Reflect)]
73
#[non_exhaustive]
74
pub enum CpuValue<T: Copy + FromReflect> {
75
    /// Single constant value.
76
    Single(T),
77
    /// Random value distributed uniformly between two inclusive bounds.
78
    ///
79
    /// The minimum bound must be less than or equal to the maximum one,
80
    /// otherwise some methods like [`sample()`] will panic.
81
    ///
82
    /// [`sample()`]: crate::CpuValue::sample
83
    Uniform((T, T)),
84
}
85

86
impl<T: Copy + FromReflect + Default> Default for CpuValue<T> {
87
    fn default() -> Self {
2✔
88
        Self::Single(T::default())
2✔
89
    }
90
}
91

92
impl<T: Copy + FromReflect + SampleUniform> CpuValue<T> {
93
    /// Sample the value.
94
    /// - For [`CpuValue::Single`], always return the same single value.
95
    /// - For [`CpuValue::Uniform`], use the given pseudo-random number
96
    ///   generator to generate a random sample.
97
    pub fn sample(&self, rng: &mut Pcg32) -> T {
69✔
98
        match self {
69✔
99
            Self::Single(x) => *x,
69✔
100
            Self::Uniform((a, b)) => Uniform::new_inclusive(*a, *b).sample(rng),
×
101
        }
102
    }
103
}
104

105
impl<T: Copy + FromReflect + PartialOrd> CpuValue<T> {
106
    /// Returns the range of allowable values in the form `[minimum, maximum]`.
107
    /// For [`CpuValue::Single`], both values are the same.
108
    pub fn range(&self) -> [T; 2] {
94✔
109
        match self {
94✔
110
            Self::Single(x) => [*x; 2],
87✔
111
            Self::Uniform((a, b)) => {
7✔
112
                if a <= b {
7✔
113
                    [*a, *b]
6✔
114
                } else {
115
                    [*b, *a]
1✔
116
                }
117
            }
118
        }
119
    }
120
}
121

122
impl<T: Copy + FromReflect> From<T> for CpuValue<T> {
123
    fn from(t: T) -> Self {
102✔
124
        Self::Single(t)
102✔
125
    }
126
}
127

128
impl<T: Copy + FromReflect> From<[T; 2]> for CpuValue<T> {
NEW
129
    fn from(t: [T; 2]) -> Self {
×
NEW
130
        Self::Uniform((t[0], t[1]))
×
131
    }
132
}
133

134
impl<T: Copy + FromReflect> From<(T, T)> for CpuValue<T> {
NEW
135
    fn from(t: (T, T)) -> Self {
×
NEW
136
        Self::Uniform(t)
×
137
    }
138
}
139

140
impl<T: Copy + FromReflect + FloatHash> Eq for CpuValue<T> {}
141

142
impl<T: Copy + FromReflect + FloatHash> Hash for CpuValue<T> {
143
    fn hash<H: Hasher>(&self, state: &mut H) {
×
144
        match self {
×
145
            CpuValue::Single(f) => {
×
146
                1_u8.hash(state);
×
147
                f.hash_f32(state);
×
148
            }
149
            CpuValue::Uniform((a, b)) => {
×
150
                2_u8.hash(state);
×
151
                a.hash_f32(state);
×
152
                b.hash_f32(state);
×
153
            }
154
        }
155
    }
156
}
157

158
/// Settings for an [`EffectSpawner`].
159
///
160
/// A [`SpawnerSettings`] represents the settings of an [`EffectSpawner`].
161
///
162
/// The spawning logic is based around the concept of _cycles_. A spawner
163
/// defines a pattern of particle spawning as the repetition of a number of unit
164
/// cycles. Each cycle is composed of a period of emission, followed by a period
165
/// of rest (idling). Both periods can be of zero duration.
166
///
167
/// The settings are:
168
///
169
/// - `count` is the number of particles to spawn over a single cycle, during
170
///   the emission period. It can evaluate to negative or zero random values, in
171
///   which case no particle is spawned during the current cycle.
172
/// - `spawn_duration` is the duration of the spawn part of a single cycle. If
173
///   this is <= 0, then the particles spawn all at once exactly at the same
174
///   instant at the beginning of the cycle.
175
/// - `period` is the period of a cycle. If this is <= `spawn_duration`, then
176
///   the spawner spawns a steady stream of particles. This is ignored if
177
///   `cycle_count == 1`.
178
/// - `cycle_count` is the number of cycles, that is the number of times this
179
///   spawn-rest pattern occurs. Set this to `0` to repeat forever.
180
///
181
/// ```txt
182
///  <----------- period ----------->
183
///  <- spawn_duration ->
184
/// |********************|-----------|
185
///      spawn 'count'        wait
186
///        particles
187
/// ```
188
///
189
/// Most settings are stored as a [`CpuValue`] to allow some randomizing. If
190
/// using a random distribution, the value is resampled each cycle.
191
///
192
/// Note that the "burst" semantic here doesn't strictly mean a one-off
193
/// emission, since that emission is spread over a number of simulation
194
/// frames that total a duration of `spawn_duration`. If you want a strict
195
/// single-frame burst, simply set the `spawn_duration` to zero; this is
196
/// what [`once()`] does.
197
///
198
/// [`once()`]: Self::once
199
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
200
#[reflect(Default)]
201
pub struct SpawnerSettings {
202
    /// Number of particles to spawn over [`spawn_duration`].
203
    ///
204
    /// [`spawn_duration`]: Self::spawn_duration
205
    count: CpuValue<f32>,
206

207
    /// Time over which to spawn [`count`], in seconds.
208
    ///
209
    /// [`count`]: Self::count
210
    spawn_duration: CpuValue<f32>,
211

212
    /// Time between bursts of the particle system, in seconds.
213
    ///
214
    /// If this is [`spawn_duration`] or less, the system spawns a steady stream
215
    /// of particles.
216
    ///
217
    /// [`spawn_duration`]: Self::spawn_duration
218
    period: CpuValue<f32>,
219

220
    /// Number of cycles the spawner is active before completing.
221
    ///
222
    /// Each cycle lasts for `period`. A value of `0` means "infinite", that is
223
    /// the spanwe emits particle forever as long as it's active.
224
    cycle_count: u32,
225

226
    /// Whether the [`EffectSpawner`] is active at startup.
227
    ///
228
    /// The value is used to initialize [`EffectSpawner::active`].
229
    ///
230
    /// [`EffectSpawner::active`]: crate::EffectSpawner::active
231
    starts_active: bool,
232

233
    /// Whether the [`EffectSpawner`] immediately starts emitting particles.
234
    emit_on_start: bool,
235
}
236

237
impl Default for SpawnerSettings {
238
    fn default() -> Self {
15✔
239
        Self::once(1.0f32.into())
15✔
240
    }
241
}
242

243
impl SpawnerSettings {
244
    /// Create settings from individual values.
245
    ///
246
    /// This is the _raw_ constructor. In general you should prefer using one of
247
    /// the utility constructors [`once()`], [`burst()`], or [`rate()`],
248
    /// which will ensure the control parameters are set consistently relative
249
    /// to each other.
250
    ///
251
    /// # Panics
252
    ///
253
    /// Panics if `period` can produce a negative number (the sample range lower
254
    /// bound is negative), unless the cycle count is exactly 1, in which case
255
    /// `period` is ignored.
256
    ///
257
    /// Panics if `period` can only produce 0 (the sample range upper bound
258
    /// is not strictly positive), unless the cycle count is exactly 1, in which
259
    /// case `period` is ignored.
260
    ///
261
    /// Panics if any value is infinite.
262
    ///
263
    /// # Example
264
    ///
265
    /// ```
266
    /// # use bevy_hanabi::SpawnerSettings;
267
    /// // Spawn 32 particles over 3 seconds, then pause for 7 seconds (10 - 3),
268
    /// // doing that 5 times in total.
269
    /// let spawner = SpawnerSettings::new(32.0.into(), 3.0.into(), 10.0.into(), 5);
270
    /// ```
271
    ///
272
    /// [`once()`]: Self::once
273
    /// [`burst()`]: Self::burst
274
    /// [`rate()`]: Self::rate
275
    pub fn new(
34✔
276
        count: CpuValue<f32>,
277
        spawn_duration: CpuValue<f32>,
278
        period: CpuValue<f32>,
279
        cycle_count: u32,
280
    ) -> Self {
281
        assert!(
34✔
282
            cycle_count == 1 || period.range()[0] >= 0.,
47✔
283
            "`period` must not generate negative numbers (period.min was {}, expected >= 0).",
1✔
284
            period.range()[0]
1✔
285
        );
286
        assert!(
33✔
287
            cycle_count == 1 || period.range()[1] > 0.,
45✔
288
            "`period` must be able to generate a positive number (period.max was {}, expected > 0).",
1✔
289
            period.range()[1]
1✔
290
        );
291
        assert!(
32✔
292
            period.range()[0].is_finite() && period.range()[1].is_finite(),
64✔
293
            "`period` {:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",
×
294
            period
295
        );
296

297
        Self {
298
            count,
299
            spawn_duration,
300
            period,
301
            cycle_count,
302
            starts_active: true,
303
            emit_on_start: true,
304
        }
305
    }
306

307
    /// Set whether the [`EffectSpawner`] immediately starts to emit particle
308
    /// when the [`ParticleEffect`] is spawned into the ECS world.
309
    ///
310
    /// If set to `false`, then [`EffectSpawner::has_completed()`] will return
311
    /// `true` after spawning the component, and the spawner needs to be
312
    /// [`EffectSpawner::reset()`] before it can spawn particles. This is
313
    /// useful to spawn a particle effect instance immediately, but only start
314
    /// emitting particles when an event occurs (collision, user input, any
315
    /// other game logic...).
316
    ///
317
    /// Because a spawner repeating forever never completes, this has no effect
318
    /// if [`is_forever()`] is `true`. To start/stop spawning with those
319
    /// effects, use [`EffectSpawner::active`] instead.
320
    ///
321
    /// [`is_forever()`]: Self::is_forever
322
    pub fn with_emit_on_start(mut self, emit_on_start: bool) -> Self {
×
323
        self.emit_on_start = emit_on_start;
×
324
        self
×
325
    }
326

327
    /// Create settings to spawn a burst of particles once.
328
    ///
329
    /// The burst of particles is spawned all at once in the same frame. After
330
    /// that, the spawner idles, waiting to be manually reset via
331
    /// [`EffectSpawner::reset()`].
332
    ///
333
    /// This is a convenience for:
334
    ///
335
    /// ```
336
    /// # use bevy_hanabi::{SpawnerSettings, CpuValue};
337
    /// # let count = CpuValue::Single(1.);
338
    /// SpawnerSettings::new(count, 0.0.into(), 0.0.into(), 1);
339
    /// ```
340
    ///
341
    /// # Example
342
    ///
343
    /// ```
344
    /// # use bevy_hanabi::SpawnerSettings;
345
    /// // Spawn 32 particles in a burst once immediately on creation.
346
    /// let spawner = SpawnerSettings::once(32.0.into());
347
    /// ```
348
    pub fn once(count: CpuValue<f32>) -> Self {
21✔
349
        Self::new(count, 0.0.into(), 0.0.into(), 1)
21✔
350
    }
351

352
    /// Get whether the spawner has a single cycle.
353
    ///
354
    /// This is true if the cycle count is exactly equal to 1.
355
    pub fn is_once(&self) -> bool {
32✔
356
        self.cycle_count == 1
32✔
357
    }
358

359
    /// Get whether the spawner has an infinite number of cycles.
360
    ///
361
    /// This is true if the cycle count is exactly equal to 0.
362
    pub fn is_forever(&self) -> bool {
57✔
363
        self.cycle_count == 0
57✔
364
    }
365

366
    /// Create settings to spawn a continuous stream of particles.
367
    ///
368
    /// The particle spawn `rate` is expressed in particles per second.
369
    /// Fractional values are accumulated each frame.
370
    ///
371
    /// This is a convenience for:
372
    ///
373
    /// ```
374
    /// # use bevy_hanabi::{SpawnerSettings, CpuValue};
375
    /// # let rate = CpuValue::Single(1.);
376
    /// SpawnerSettings::new(rate, 1.0.into(), 1.0.into(), 0);
377
    /// ```
378
    ///
379
    /// # Example
380
    ///
381
    /// ```
382
    /// # use bevy_hanabi::SpawnerSettings;
383
    /// // Spawn 10 particles per second, indefinitely.
384
    /// let spawner = SpawnerSettings::rate(10.0.into());
385
    /// ```
386
    pub fn rate(rate: CpuValue<f32>) -> Self {
9✔
387
        Self::new(rate, 1.0.into(), 1.0.into(), 0)
9✔
388
    }
389

390
    /// Create settings to spawn particles in bursts.
391
    ///
392
    /// The settings define an infinite number of cycles where `count` particles
393
    /// are spawned at the beginning of the cycle, then the spawner waits
394
    /// `period` seconds, and repeats forever.
395
    ///
396
    /// This is a convenience for:
397
    ///
398
    /// ```
399
    /// # use bevy_hanabi::{SpawnerSettings, CpuValue};
400
    /// # let count = CpuValue::Single(1.);
401
    /// # let period = CpuValue::Single(1.);
402
    /// SpawnerSettings::new(count, 0.0.into(), period, 0);
403
    /// ```
404
    ///
405
    /// # Example
406
    ///
407
    /// ```
408
    /// # use bevy_hanabi::SpawnerSettings;
409
    /// // Spawn a burst of 5 particles every 3 seconds, indefinitely.
410
    /// let spawner = SpawnerSettings::burst(5.0.into(), 3.0.into());
411
    /// ```
412
    pub fn burst(count: CpuValue<f32>, period: CpuValue<f32>) -> Self {
1✔
413
        Self::new(count, 0.0.into(), period, 0)
1✔
414
    }
415

416
    /// Set the number of particles that are spawned each cycle.
417
    pub fn with_count(mut self, count: CpuValue<f32>) -> Self {
×
418
        self.count = count;
×
419
        self
×
420
    }
421

422
    /// Set the number of particles that are spawned each cycle.
423
    pub fn set_count(&mut self, count: CpuValue<f32>) {
×
424
        self.count = count;
×
425
    }
426

427
    /// Get the number of particles that are spawned each cycle.
428
    pub fn count(&self) -> CpuValue<f32> {
×
429
        self.count
×
430
    }
431

432
    /// Set the duration, in seconds, of the spawn part each cycle.
433
    pub fn with_spawn_duration(mut self, spawn_duration: CpuValue<f32>) -> Self {
×
434
        self.spawn_duration = spawn_duration;
×
435
        self
×
436
    }
437

438
    /// Set the duration, in seconds, of the spawn part each cycle.
439
    pub fn set_spawn_duration(&mut self, spawn_duration: CpuValue<f32>) {
×
440
        self.spawn_duration = spawn_duration;
×
441
    }
442

443
    /// Get the duration, in seconds, of the spawn part each cycle.
444
    pub fn spawn_duration(&self) -> CpuValue<f32> {
×
445
        self.spawn_duration
×
446
    }
447

448
    /// Set the duration of a single spawn cycle, in seconds.
449
    ///
450
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
451
    /// wait time (if larger than spawn time).
452
    ///
453
    /// # Panics
454
    ///
455
    /// Panics if the period is infinite.
456
    ///
457
    /// [`spawn_duration()`]: Self::spawn_duration
458
    pub fn with_period(mut self, period: CpuValue<f32>) -> Self {
×
459
        assert!(
×
460
            period.range()[0].is_finite() && period.range()[1].is_finite(),
×
461
            "`period` {:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",
×
462
            period
463
        );
464
        self.period = period;
×
465
        self
×
466
    }
467

468
    /// Set the duration of a single spawn cycle, in seconds.
469
    ///
470
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
471
    /// wait time (if larger than spawn time).
472
    ///
473
    /// # Panics
474
    ///
475
    /// Panics if the period is infinite.
476
    ///
477
    /// [`spawn_duration()`]: Self::spawn_duration
478
    pub fn set_period(&mut self, period: CpuValue<f32>) {
×
479
        assert!(
×
480
            period.range()[0].is_finite() && period.range()[1].is_finite(),
×
481
            "`period` {:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",
×
482
            period
483
        );
484
        self.period = period;
×
485
    }
486

487
    /// Get the duration of a single spawn cycle, in seconds.
488
    ///
489
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
490
    /// wait time (if larger than spawn time).
491
    ///
492
    /// [`spawn_duration()`]: Self::spawn_duration
493
    pub fn period(&self) -> CpuValue<f32> {
×
494
        self.period
×
495
    }
496

497
    /// Set the number of cycles to spawn for.
498
    ///
499
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
500
    /// wait time (if larger than spawn time). It lasts for [`period()`].
501
    ///
502
    /// [`spawn_duration()`]: Self::spawn_duration
503
    /// [`period()`]: Self::period
504
    pub fn with_cycle_count(mut self, cycle_count: u32) -> Self {
×
505
        self.cycle_count = cycle_count;
×
506
        self
×
507
    }
508

509
    /// Set the number of cycles to spawn for.
510
    ///
511
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
512
    /// wait time (if larger than spawn time). It lasts for [`period()`].
513
    ///
514
    /// [`spawn_duration()`]: Self::spawn_duration
515
    /// [`period()`]: Self::period
516
    pub fn set_cycle_count(&mut self, cycle_count: u32) {
×
517
        self.cycle_count = cycle_count;
×
518
    }
519

520
    /// Get the number of cycles to spawn for.
521
    ///
522
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
523
    /// wait time (if larger than spawn time). It lasts for [`period()`].
524
    ///
525
    /// [`spawn_duration()`]: Self::spawn_duration
526
    /// [`period()`]: Self::period
527
    pub fn cycle_count(&self) -> u32 {
29✔
528
        self.cycle_count
29✔
529
    }
530

531
    /// Sets whether the spawner starts active when the effect is instantiated.
532
    ///
533
    /// This value will be transfered to the active state of the
534
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
535
    /// any particle.
536
    pub fn with_starts_active(mut self, starts_active: bool) -> Self {
2✔
537
        self.starts_active = starts_active;
2✔
538
        self
2✔
539
    }
540

541
    /// Set whether the spawner starts active when the effect is instantiated.
542
    ///
543
    /// This value will be transfered to the active state of the
544
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
545
    /// any particle.
546
    pub fn set_starts_active(&mut self, starts_active: bool) {
×
547
        self.starts_active = starts_active;
×
548
    }
549

550
    /// Get whether the spawner starts active when the effect is instantiated.
551
    ///
552
    /// This value will be transfered to the active state of the
553
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
554
    /// any particle.
555
    pub fn starts_active(&self) -> bool {
13✔
556
        self.starts_active
13✔
557
    }
558
}
559

560
/// Runtime state machine for CPU particle spawning.
561
///
562
/// The spawner defines how new particles are emitted and when. Each time the
563
/// spawner ticks, it calculates a number of particles to emit for this frame,
564
/// based on its [`SpawnerSettings`]. This spawn count is passed to the GPU for
565
/// the init compute pass to actually allocate the new particles and initialize
566
/// them. The number of particles to spawn is stored as a floating-point number,
567
/// and any remainder accumulates for the next tick.
568
///
569
/// Spawners are used to control from CPU when particles are spawned. To use GPU
570
/// spawn events instead, and spawn particles based on events occurring on
571
/// existing particles in other effects, see [`EffectParent`]. Those two
572
/// mechanisms (CPU and GPU spawner) are mutually exclusive.
573
///
574
/// Once per frame the [`tick_spawners()`] system will add the [`EffectSpawner`]
575
/// component if it's missing, cloning the [`SpawnerSettings`] from the source
576
/// [`EffectAsset`] to initialize it. After that, it ticks the
577
/// [`SpawnerSettings`] stored in the component. The resulting number of
578
/// particles to spawn for the frame is then stored into
579
/// [`EffectSpawner::spawn_count`]. You can override that value to manually
580
/// control each frame how many particles are spawned, instead of using the
581
/// logic of [`SpawnerSettings`].
582
///
583
/// [`EffectParent`]: crate::EffectParent
584
#[derive(Debug, Default, Clone, Copy, PartialEq, Component, Reflect)]
585
#[reflect(Component)]
586
pub struct EffectSpawner {
587
    /// The spawner settings extracted from the [`EffectAsset`], or directly
588
    /// overriden by the user.
589
    pub settings: SpawnerSettings,
590

591
    /// Accumulated time for the current (partial) cycle, in seconds.
592
    cycle_time: f32,
593

594
    /// Number of cycles already completed.
595
    completed_cycle_count: u32,
596

597
    /// Sampled value of `spawn_duration` until `period` is reached. This is the
598
    /// duration of the "active" period during which we spawn particles, as
599
    /// opposed to the "wait" period during which we do nothing until the next
600
    /// spawn cycle.
601
    sampled_spawn_duration: f32,
602

603
    /// Sampled value of the time period, in seconds, until the next spawn
604
    /// cycle.
605
    sampled_period: f32,
606

607
    /// Sampled value of the number of particles to spawn per `spawn_duration`.
608
    sampled_count: f32,
609

610
    /// Number of particles to spawn this frame.
611
    ///
612
    /// This value is normally updated by calling [`tick()`], which
613
    /// automatically happens once per frame when the [`tick_spawners()`]
614
    /// system runs in the [`PostUpdate`] schedule.
615
    ///
616
    /// You can manually assign this value to override the one calculated by
617
    /// [`tick()`]. Note in this case that you need to override the value after
618
    /// the automated one was calculated, by ordering your system
619
    /// after [`tick_spawners()`] or [`EffectSystems::TickSpawners`].
620
    ///
621
    /// [`tick()`]: crate::EffectSpawner::tick
622
    /// [`EffectSystems::TickSpawners`]: crate::EffectSystems::TickSpawners
623
    pub spawn_count: u32,
624

625
    /// Fractional remainder of particle count to spawn.
626
    ///
627
    /// This is accumulated each tick, and the integral part is added to
628
    /// `spawn_count`. The reminder gets saved for next frame.
629
    spawn_remainder: f32,
630

631
    /// Whether the spawner is active. Defaults to
632
    /// [`SpawnerSettings::starts_active()`]. An inactive spawner
633
    /// doesn't tick (no particle spawned, no internal state updated).
634
    pub active: bool,
635
}
636

637
impl EffectSpawner {
638
    /// Create a new spawner.
639
    pub fn new(settings: &SpawnerSettings) -> Self {
11✔
640
        Self {
641
            settings: *settings,
11✔
642
            cycle_time: 0.,
643
            completed_cycle_count: if settings.emit_on_start || settings.is_forever() {
11✔
644
                // Infinitely repeating effects always start at cycle #0.
645
                0
646
            } else {
647
                // Start at last cycle. This means has_completed() is true.
648
                settings.cycle_count()
649
            },
650
            sampled_spawn_duration: 0.,
651
            sampled_period: 0.,
652
            sampled_count: 0.,
653
            spawn_count: 0,
654
            spawn_remainder: 0.,
655
            active: settings.starts_active(),
11✔
656
        }
657
    }
658

659
    /// Set whether the spawner is active.
660
    ///
661
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
662
    /// Their internal state do not update.
663
    pub fn with_active(mut self, active: bool) -> Self {
×
664
        self.active = active;
×
665
        self
×
666
    }
667

668
    /// Get the time relative to the beginning of the current cycle.
669
    #[inline]
670
    pub fn cycle_time(&self) -> f32 {
3✔
671
        self.cycle_time
3✔
672
    }
673

674
    /// Get the spawn duration for the current cycle.
675
    ///
676
    /// This value can change every cycle if [`SpawnerSettings::spawn_duration`]
677
    /// is a randomly distributed value.
678
    #[inline]
679
    pub fn cycle_spawn_duration(&self) -> f32 {
3✔
680
        self.sampled_spawn_duration
3✔
681
    }
682

683
    /// Get the period of the current cycle.
684
    ///
685
    /// This value can change every cycle if [`SpawnerSettings::period`] is a
686
    /// randomly distributed value. If the effect spawns only once, and
687
    /// therefore its cycle period is ignored, this returns `0`.
688
    #[inline]
689
    pub fn cycle_period(&self) -> f32 {
3✔
690
        if self.settings.is_once() {
3✔
691
            0.
×
692
        } else {
693
            self.sampled_period
3✔
694
        }
695
    }
696

697
    /// Get the progress ratio in 0..1 of the current cycle.
698
    ///
699
    /// This is the ratio of the [`cycle_time()`] over [`cycle_period()`]. If
700
    /// the effect spawns only once, and therefore its cycle period is
701
    /// ignored, this returns `0`.
702
    ///
703
    /// [`cycle_time()`]: Self::cycle_time
704
    /// [`cycle_period()`]: Self::cycle_period
705
    #[inline]
706
    pub fn cycle_ratio(&self) -> f32 {
3✔
707
        if self.settings.is_once() {
3✔
708
            0.
×
709
        } else {
710
            self.cycle_time / self.sampled_period
3✔
711
        }
712
    }
713

714
    /// Get the number of particles to spawn during the current cycle
715
    ///
716
    /// This value can change every cycle if [`SpawnerSettings::count`] is a
717
    /// randomly distributed value.
718
    #[inline]
719
    pub fn cycle_spawn_count(&self) -> f32 {
3✔
720
        self.sampled_count
3✔
721
    }
722

723
    /// Get the number of completed cycles since last [`reset()`].
724
    ///
725
    /// The value loops back if the pattern repeats forever
726
    /// ([`SpawnerSettings::is_forever()`] is `true`).
727
    ///
728
    /// [`reset()`]: Self::reset
729
    #[inline]
730
    pub fn completed_cycle_count(&self) -> u32 {
5✔
731
        self.completed_cycle_count
5✔
732
    }
733

734
    /// Get whether the spawner has completed.
735
    ///
736
    /// A spawner has completed if it already ticked through its maximum number
737
    /// of cycles. It can be reset back to its original state with [`reset()`].
738
    /// A spawner repeating forever never completes.
739
    ///
740
    /// [`reset()`]: Self::reset
741
    #[inline]
742
    pub fn has_completed(&self) -> bool {
6✔
743
        !self.settings.is_forever() && (self.completed_cycle_count >= self.settings.cycle_count())
12✔
744
    }
745

746
    /// Reset the spawner state.
747
    ///
748
    /// This resets the internal spawner time and cycle count to zero.
749
    ///
750
    /// Use this, for example, to immediately spawn some particles in a spawner
751
    /// constructed with [`SpawnerSettings::once`].
752
    ///
753
    /// [`SpawnerSettings::once`]: crate::SpawnerSettings::once
754
    pub fn reset(&mut self) {
2✔
755
        self.cycle_time = 0.;
2✔
756
        self.completed_cycle_count = 0;
2✔
757
        self.sampled_spawn_duration = 0.;
2✔
758
        self.sampled_period = 0.;
2✔
759
        self.sampled_count = 0.;
2✔
760
        self.spawn_count = 0;
2✔
761
        self.spawn_remainder = 0.;
2✔
762
    }
763

764
    /// Tick the spawner to calculate the number of particles to spawn this
765
    /// frame.
766
    ///
767
    /// The frame delta time `dt` is added to the current spawner time, before
768
    /// the spawner calculates the number of particles to spawn.
769
    ///
770
    /// This method is called automatically by [`tick_spawners()`] during the
771
    /// [`PostUpdate`], so you normally don't have to call it yourself
772
    /// manually.
773
    ///
774
    /// # Returns
775
    ///
776
    /// The integral number of particles to spawn this frame. Any fractional
777
    /// remainder is saved for the next call.
778
    pub fn tick(&mut self, mut dt: f32, rng: &mut Pcg32) -> u32 {
39✔
779
        // If inactive, or if the finite number of cycles has been completed, then we're
780
        // done.
781
        if !self.active
39✔
782
            || (!self.settings.is_forever()
35✔
783
                && (self.completed_cycle_count >= self.settings.cycle_count()))
14✔
784
        {
785
            self.spawn_count = 0;
7✔
786
            return 0;
7✔
787
        }
788

789
        // Use a loop in case the timestep dt spans multiple cycles
790
        loop {
791
            // Check if this is a new cycle which needs resampling
792
            if self.sampled_period == 0.0 {
38✔
793
                if self.settings.is_once() {
26✔
794
                    self.sampled_spawn_duration = self.settings.spawn_duration.sample(rng);
7✔
795
                    // Period is unchecked, should be ignored (could sample to <= 0). Use the spawn
796
                    // duration, but ensure we have something > 0 as a marker that we've resampled.
797
                    self.sampled_period = self.sampled_spawn_duration.max(1e-12);
7✔
798
                } else {
799
                    self.sampled_period = self.settings.period.sample(rng);
12✔
800
                    assert!(self.sampled_period > 0.);
12✔
801
                    self.sampled_spawn_duration = self
12✔
802
                        .settings
12✔
803
                        .spawn_duration
12✔
804
                        .sample(rng)
12✔
805
                        .clamp(0., self.sampled_period);
12✔
806
                }
807
                self.sampled_spawn_duration = self.settings.spawn_duration.sample(rng);
19✔
808
                self.sampled_count = self.settings.count.sample(rng).max(0.);
19✔
809
            }
810

811
            let new_time = self.cycle_time + dt;
38✔
812

813
            // If inside the spawn period, accumulate some particle spawn count
814
            if self.cycle_time <= self.sampled_spawn_duration {
38✔
815
                // If the spawn time is very small, close to zero, spawn all particles
816
                // immediately in one burst over a single frame.
817
                self.spawn_remainder += if self.sampled_spawn_duration < 1e-5f32.max(dt / 100.0) {
34✔
818
                    self.sampled_count
10✔
819
                } else {
820
                    // Spawn an amount of particles equal to the fraction of time the current frame
821
                    // spans compared to the total burst duration.
822
                    let ratio = ((new_time.min(self.sampled_spawn_duration) - self.cycle_time)
24✔
823
                        / self.sampled_spawn_duration)
24✔
824
                        .clamp(0., 1.);
24✔
825
                    self.sampled_count * ratio
24✔
826
                };
827
            }
828

829
            // Increment current time
830
            self.cycle_time = new_time;
831

832
            // Check for cycle completion
833
            if self.cycle_time >= self.sampled_period {
834
                dt = self.cycle_time - self.sampled_period;
14✔
835
                self.cycle_time = 0.0;
14✔
836
                self.completed_cycle_count += 1;
14✔
837

838
                // Mark as "need resampling"
839
                self.sampled_period = 0.0;
14✔
840

841
                // If this was the last cycle, we're done
842
                if !self.settings.is_forever()
14✔
843
                    && (self.completed_cycle_count >= self.settings.cycle_count())
9✔
844
                {
845
                    // Don't deactivate quite yet, otherwise we'll miss the spawns for this frame
846
                    break;
8✔
847
                }
848
            } else {
849
                // We're done for this frame
850
                break;
24✔
851
            }
852
        }
853

854
        // Extract integral number of particles to spawn this frame, keep remainder for
855
        // next one
856
        let count = self.spawn_remainder.floor();
32✔
857
        self.spawn_remainder -= count;
32✔
858
        self.spawn_count = count as u32;
32✔
859

860
        self.spawn_count
32✔
861
    }
862
}
863

864
/// Tick all the [`EffectSpawner`] components.
865
///
866
/// This system runs in the [`PostUpdate`] stage, after the visibility system
867
/// has updated the [`InheritedVisibility`] of each effect instance (see
868
/// [`VisibilitySystems::VisibilityPropagate`]). Hidden instances are not
869
/// updated, unless the [`EffectAsset::simulation_condition`]
870
/// is set to [`SimulationCondition::Always`]. If no [`InheritedVisibility`] is
871
/// present, the effect is assumed to be visible.
872
///
873
/// Note that by that point the [`ViewVisibility`] is not yet calculated, and it
874
/// may happen that spawners are ticked but no effect is visible in any view
875
/// even though some are "visible" (active) in the [`World`]. The actual
876
/// per-view culling of invisible (not in view) effects is performed later on
877
/// the render world.
878
///
879
/// Once the system determined that the effect instance needs to be simulated
880
/// this frame, it ticks the effect's spawner by calling
881
/// [`EffectSpawner::tick()`], adding a new [`EffectSpawner`] component if it
882
/// doesn't already exist on the same entity as the [`ParticleEffect`].
883
///
884
/// [`VisibilitySystems::VisibilityPropagate`]: bevy::render::view::VisibilitySystems::VisibilityPropagate
885
/// [`EffectAsset::simulation_condition`]: crate::EffectAsset::simulation_condition
886
pub fn tick_spawners(
3✔
887
    mut commands: Commands,
888
    time: Res<Time<EffectSimulation>>,
889
    effects: Res<Assets<EffectAsset>>,
890
    mut rng: ResMut<Random>,
891
    mut query: Query<(
892
        Entity,
893
        &ParticleEffect,
894
        &InheritedVisibility,
895
        Option<&mut EffectSpawner>,
896
    )>,
897
) {
898
    trace!("tick_spawners()");
3✔
899

900
    let dt = time.delta_secs();
3✔
901

902
    for (entity, effect, inherited_visibility, maybe_spawner) in query.iter_mut() {
3✔
903
        let Some(asset) = effects.get(&effect.handle) else {
6✔
904
            trace!(
×
905
                "Effect asset with handle {:?} is not available; skipped initializers tick.",
×
906
                effect.handle
907
            );
908
            continue;
×
909
        };
910

911
        if asset.simulation_condition == SimulationCondition::WhenVisible
912
            && !inherited_visibility.get()
2✔
913
        {
914
            trace!(
1✔
915
                "Effect asset with handle {:?} is not visible, and simulates only WhenVisible; skipped initializers tick.",
×
916
                effect.handle
917
            );
918
            continue;
1✔
919
        }
920

921
        if let Some(mut effect_spawner) = maybe_spawner {
×
922
            effect_spawner.tick(dt, &mut rng.0);
×
923
            continue;
×
924
        }
925

926
        let effect_spawner = {
2✔
927
            let mut effect_spawner = EffectSpawner::new(&asset.spawner);
2✔
928
            effect_spawner.tick(dt, &mut rng.0);
2✔
929
            effect_spawner
2✔
930
        };
931
        commands.entity(entity).insert(effect_spawner);
2✔
932
    }
933
}
934

935
#[cfg(test)]
936
mod test {
937
    use std::time::Duration;
938

939
    use bevy::{
940
        asset::{
941
            io::{
942
                memory::{Dir, MemoryAssetReader},
943
                AssetSourceBuilder, AssetSourceBuilders, AssetSourceId,
944
            },
945
            AssetServerMode,
946
        },
947
        render::view::{VisibilityPlugin, VisibilitySystems},
948
        tasks::{IoTaskPool, TaskPoolBuilder},
949
    };
950

951
    use super::*;
952
    use crate::Module;
953

954
    #[test]
955
    fn test_range_single() {
956
        let value = CpuValue::Single(1.0);
957
        assert_eq!(value.range(), [1.0, 1.0]);
958
    }
959

960
    #[test]
961
    fn test_range_uniform() {
962
        let value = CpuValue::Uniform((1.0, 3.0));
963
        assert_eq!(value.range(), [1.0, 3.0]);
964
    }
965

966
    #[test]
967
    fn test_range_uniform_reverse() {
968
        let value = CpuValue::Uniform((3.0, 1.0));
969
        assert_eq!(value.range(), [1.0, 3.0]);
970
    }
971

972
    #[test]
973
    fn test_new() {
974
        let rng = &mut new_rng();
975
        // 3 particles over 3 seconds, pause 7 seconds (total 10 seconds period). 2
976
        // cycles.
977
        let spawner = SpawnerSettings::new(3.0.into(), 3.0.into(), 10.0.into(), 2);
978
        let mut spawner = EffectSpawner::new(&spawner);
979
        let count = spawner.tick(2., rng); // t = 2s
980
        assert_eq!(count, 2);
981
        assert!(spawner.active);
982
        assert_eq!(spawner.cycle_time(), 2.);
983
        assert_eq!(spawner.cycle_spawn_duration(), 3.);
984
        assert_eq!(spawner.cycle_period(), 10.);
985
        assert_eq!(spawner.cycle_ratio(), 0.2); // 2s / 10s
986
        assert_eq!(spawner.cycle_spawn_count(), 3.);
987
        assert_eq!(spawner.completed_cycle_count(), 0);
988
        let count = spawner.tick(5., rng); // t = 7s
989
        assert_eq!(count, 1);
990
        assert!(spawner.active);
991
        assert_eq!(spawner.cycle_time(), 7.);
992
        assert_eq!(spawner.cycle_spawn_duration(), 3.);
993
        assert_eq!(spawner.cycle_period(), 10.);
994
        assert_eq!(spawner.cycle_ratio(), 0.7); // 7s / 10s
995
        assert_eq!(spawner.cycle_spawn_count(), 3.);
996
        assert_eq!(spawner.completed_cycle_count(), 0);
997
        let count = spawner.tick(8., rng); // t = 15s
998
        assert_eq!(count, 3);
999
        assert!(spawner.active);
1000
        assert_eq!(spawner.cycle_time(), 5.); // 15. mod 10.
1001
        assert_eq!(spawner.cycle_spawn_duration(), 3.);
1002
        assert_eq!(spawner.cycle_period(), 10.);
1003
        assert_eq!(spawner.cycle_ratio(), 0.5); // 5s / 10s
1004
        assert_eq!(spawner.cycle_spawn_count(), 3.);
1005
        assert_eq!(spawner.completed_cycle_count(), 1);
1006
        let count = spawner.tick(10., rng); // t = 25s
1007
        assert_eq!(count, 0);
1008
        assert!(spawner.active);
1009
        assert_eq!(spawner.completed_cycle_count(), 2);
1010
        let count = spawner.tick(0.1, rng); // t = 25.1s
1011
        assert_eq!(count, 0);
1012
        assert!(spawner.active);
1013
        assert_eq!(spawner.completed_cycle_count(), 2);
1014
    }
1015

1016
    #[test]
1017
    #[should_panic]
1018
    fn test_new_panic_negative_period() {
1019
        let _ = SpawnerSettings::new(3.0.into(), 1.0.into(), CpuValue::Uniform((-1., 1.)), 0);
1020
    }
1021

1022
    #[test]
1023
    #[should_panic]
1024
    fn test_new_panic_zero_period() {
1025
        let _ = SpawnerSettings::new(3.0.into(), 1.0.into(), CpuValue::Uniform((0., 0.)), 0);
1026
    }
1027

1028
    #[test]
1029
    fn test_once() {
1030
        let rng = &mut new_rng();
1031
        let spawner = SpawnerSettings::once(5.0.into());
1032
        assert!(spawner.is_once());
1033
        let mut spawner = EffectSpawner::new(&spawner);
1034
        assert!(spawner.active);
1035
        let count = spawner.tick(0.001, rng);
1036
        assert_eq!(count, 5);
1037
        let count = spawner.tick(100.0, rng);
1038
        assert_eq!(count, 0);
1039
    }
1040

1041
    #[test]
1042
    fn test_once_reset() {
1043
        let rng = &mut new_rng();
1044
        let spawner = SpawnerSettings::once(5.0.into());
1045
        assert!(spawner.is_once());
1046
        assert!(spawner.starts_active());
1047
        let mut spawner = EffectSpawner::new(&spawner);
1048
        spawner.tick(1.0, rng);
1049
        spawner.reset();
1050
        let count = spawner.tick(1.0, rng);
1051
        assert_eq!(count, 5);
1052
    }
1053

1054
    #[test]
1055
    fn test_once_start_inactive() {
1056
        let rng = &mut new_rng();
1057

1058
        let spawner = SpawnerSettings::once(5.0.into()).with_starts_active(false);
1059
        assert!(spawner.is_once());
1060
        assert!(!spawner.starts_active());
1061
        let mut spawner = EffectSpawner::new(&spawner);
1062
        assert!(!spawner.has_completed());
1063

1064
        // Inactive; no-op
1065
        let count = spawner.tick(1.0, rng);
1066
        assert_eq!(count, 0);
1067
        assert!(!spawner.has_completed());
1068

1069
        spawner.active = true;
1070

1071
        // Active; spawns
1072
        let count = spawner.tick(1.0, rng);
1073
        assert_eq!(count, 5);
1074
        assert!(spawner.active);
1075
        assert!(spawner.has_completed()); // once(), so completes on first tick()
1076

1077
        // Completed; no-op
1078
        let count = spawner.tick(1.0, rng);
1079
        assert_eq!(count, 0);
1080
        assert!(spawner.active);
1081
        assert!(spawner.has_completed());
1082

1083
        // Reset internal state, still active
1084
        spawner.reset();
1085
        assert!(spawner.active);
1086
        assert!(!spawner.has_completed());
1087

1088
        let count = spawner.tick(1.0, rng);
1089
        assert_eq!(count, 5);
1090
        assert!(spawner.active);
1091
        assert!(spawner.has_completed());
1092
    }
1093

1094
    #[test]
1095
    fn test_rate() {
1096
        let rng = &mut new_rng();
1097
        let spawner = SpawnerSettings::rate(5.0.into());
1098
        assert!(!spawner.is_once());
1099
        assert!(spawner.is_forever());
1100
        let mut spawner = EffectSpawner::new(&spawner);
1101
        // Slightly over 1.0 to avoid edge case
1102
        let count = spawner.tick(1.01, rng);
1103
        assert_eq!(count, 5);
1104
        let count = spawner.tick(0.4, rng);
1105
        assert_eq!(count, 2);
1106
    }
1107

1108
    #[test]
1109
    fn test_rate_active() {
1110
        let rng = &mut new_rng();
1111
        let spawner = SpawnerSettings::rate(5.0.into());
1112
        assert!(!spawner.is_once());
1113
        let mut spawner = EffectSpawner::new(&spawner);
1114
        spawner.tick(1.01, rng);
1115
        spawner.active = false;
1116
        assert!(!spawner.active);
1117
        let count = spawner.tick(0.4, rng);
1118
        assert_eq!(count, 0);
1119
        spawner.active = true;
1120
        assert!(spawner.active);
1121
        let count = spawner.tick(0.4, rng);
1122
        assert_eq!(count, 2);
1123
    }
1124

1125
    #[test]
1126
    fn test_rate_accumulate() {
1127
        let rng = &mut new_rng();
1128
        let spawner = SpawnerSettings::rate(5.0.into());
1129
        assert!(!spawner.is_once());
1130
        let mut spawner = EffectSpawner::new(&spawner);
1131
        // 13 ticks instead of 12 to avoid edge case
1132
        let count = (0..13).map(|_| spawner.tick(1.0 / 60.0, rng)).sum::<u32>();
1133
        assert_eq!(count, 1);
1134
    }
1135

1136
    #[test]
1137
    fn test_burst() {
1138
        let rng = &mut new_rng();
1139
        let spawner = SpawnerSettings::burst(5.0.into(), 2.0.into());
1140
        assert!(!spawner.is_once());
1141
        assert!(spawner.is_forever());
1142
        let mut spawner = EffectSpawner::new(&spawner);
1143
        let count = spawner.tick(1.0, rng);
1144
        assert_eq!(count, 5);
1145
        let count = spawner.tick(4.0, rng);
1146
        assert_eq!(count, 10);
1147
        let count = spawner.tick(0.1, rng);
1148
        assert_eq!(count, 0);
1149
    }
1150

1151
    #[test]
1152
    fn test_with_active() {
1153
        let rng = &mut new_rng();
1154
        let spawner = SpawnerSettings::rate(5.0.into()).with_starts_active(false);
1155
        let mut spawner = EffectSpawner::new(&spawner);
1156
        assert!(!spawner.active);
1157
        let count = spawner.tick(1., rng);
1158
        assert_eq!(count, 0);
1159
        spawner.active = false; // no-op
1160
        let count = spawner.tick(1., rng);
1161
        assert_eq!(count, 0);
1162
        spawner.active = true;
1163
        assert!(spawner.active);
1164
        let count = spawner.tick(1., rng);
1165
        assert_eq!(count, 5);
1166
    }
1167

1168
    fn make_test_app() -> App {
1169
        IoTaskPool::get_or_init(|| {
1170
            TaskPoolBuilder::default()
1171
                .num_threads(1)
1172
                .thread_name("Hanabi test IO Task Pool".to_string())
1173
                .build()
1174
        });
1175

1176
        let mut app = App::new();
1177

1178
        let watch_for_changes = false;
1179
        let mut builders = app
1180
            .world_mut()
1181
            .get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
1182
        let dir = Dir::default();
1183
        let dummy_builder = AssetSourceBuilder::default()
1184
            .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }));
1185
        builders.insert(AssetSourceId::Default, dummy_builder);
1186
        let sources = builders.build_sources(watch_for_changes, false);
1187
        let asset_server =
1188
            AssetServer::new(sources, AssetServerMode::Unprocessed, watch_for_changes);
1189

1190
        app.insert_resource(asset_server);
1191
        // app.add_plugins(DefaultPlugins);
1192
        app.init_asset::<Mesh>();
1193
        app.add_plugins(VisibilityPlugin);
1194
        app.init_resource::<Time<EffectSimulation>>();
1195
        app.insert_resource(Random(new_rng()));
1196
        app.init_asset::<EffectAsset>();
1197
        app.add_systems(
1198
            PostUpdate,
1199
            tick_spawners.after(VisibilitySystems::CheckVisibility),
1200
        );
1201

1202
        app
1203
    }
1204

1205
    /// Test case for `tick_spawners()`.
1206
    struct TestCase {
1207
        /// Initial entity visibility on spawn. If `None`, do not add a
1208
        /// [`Visibility`] component.
1209
        visibility: Option<Visibility>,
1210

1211
        /// Spawner settings assigned to the `EffectAsset`.
1212
        asset_spawner: SpawnerSettings,
1213
    }
1214

1215
    impl TestCase {
1216
        fn new(visibility: Option<Visibility>, asset_spawner: SpawnerSettings) -> Self {
1217
            Self {
1218
                visibility,
1219
                asset_spawner,
1220
            }
1221
        }
1222
    }
1223

1224
    #[test]
1225
    fn test_tick_spawners() {
1226
        let asset_spawner = SpawnerSettings::once(32.0.into());
1227

1228
        for test_case in &[
1229
            TestCase::new(None, asset_spawner),
1230
            TestCase::new(Some(Visibility::Hidden), asset_spawner),
1231
            TestCase::new(Some(Visibility::Visible), asset_spawner),
1232
        ] {
1233
            let mut app = make_test_app();
1234

1235
            let (effect_entity, handle) = {
1236
                let world = app.world_mut();
1237

1238
                // Add effect asset
1239
                let mut assets = world.resource_mut::<Assets<EffectAsset>>();
1240
                let mut asset = EffectAsset::new(64, test_case.asset_spawner, Module::default());
1241
                asset.simulation_condition = if test_case.visibility.is_some() {
1242
                    SimulationCondition::WhenVisible
1243
                } else {
1244
                    SimulationCondition::Always
1245
                };
1246
                let handle = assets.add(asset);
1247

1248
                // Spawn particle effect
1249
                let entity = if let Some(visibility) = test_case.visibility {
1250
                    world
1251
                        .spawn((
1252
                            visibility,
1253
                            InheritedVisibility::default(),
1254
                            ParticleEffect {
1255
                                handle: handle.clone(),
1256
                            },
1257
                        ))
1258
                        .id()
1259
                } else {
1260
                    world
1261
                        .spawn((ParticleEffect {
1262
                            handle: handle.clone(),
1263
                        },))
1264
                        .id()
1265
                };
1266

1267
                // Spawn a camera, otherwise ComputedVisibility stays at HIDDEN
1268
                world.spawn(Camera3d::default());
1269

1270
                (entity, handle)
1271
            };
1272

1273
            // Tick once
1274
            let _cur_time = {
1275
                // Make sure to increment the current time so that the spawners spawn something.
1276
                // Note that `Time` has this weird behavior where the common quantities like
1277
                // `Time::delta_secs()` only update after the *second* update. So we tick the
1278
                // `Time` twice here to enforce this.
1279
                let mut time = app.world_mut().resource_mut::<Time<EffectSimulation>>();
1280
                time.advance_by(Duration::from_millis(16));
1281
                time.elapsed()
1282
            };
1283
            app.update();
1284

1285
            let world = app.world_mut();
1286

1287
            // Check the state of the components after `tick_spawners()` ran
1288
            if let Some(test_visibility) = test_case.visibility {
1289
                // Simulated-when-visible effect (SimulationCondition::WhenVisible)
1290

1291
                let (entity, visibility, inherited_visibility, particle_effect, effect_spawner) =
1292
                    world
1293
                        .query::<(
1294
                            Entity,
1295
                            &Visibility,
1296
                            &InheritedVisibility,
1297
                            &ParticleEffect,
1298
                            Option<&EffectSpawner>,
1299
                        )>()
1300
                        .iter(world)
1301
                        .next()
1302
                        .unwrap();
1303
                assert_eq!(entity, effect_entity);
1304
                assert_eq!(visibility, test_visibility);
1305
                assert_eq!(
1306
                    inherited_visibility.get(),
1307
                    test_visibility == Visibility::Visible
1308
                );
1309
                assert_eq!(particle_effect.handle, handle);
1310
                if inherited_visibility.get() {
1311
                    // If visible, `tick_spawners()` spawns the EffectSpawner and ticks it
1312
                    assert!(effect_spawner.is_some());
1313
                    let effect_spawner = effect_spawner.unwrap();
1314
                    let actual_spawner = effect_spawner.settings;
1315

1316
                    // Check the spawner ticked
1317
                    assert!(effect_spawner.active); // will get deactivated next tick()
1318
                    assert_eq!(effect_spawner.spawn_remainder, 0.);
1319
                    assert_eq!(effect_spawner.cycle_time, 0.);
1320
                    assert_eq!(effect_spawner.completed_cycle_count, 1);
1321
                    assert_eq!(effect_spawner.spawn_count, 32);
1322

1323
                    assert_eq!(actual_spawner, test_case.asset_spawner);
1324
                } else {
1325
                    // If not visible, `tick_spawners()` skips the effect entirely so won't
1326
                    // spawn an `EffectSpawner` for it
1327
                    assert!(effect_spawner.is_none());
1328
                }
1329
            } else {
1330
                // Always-simulated effect (SimulationCondition::Always)
1331

1332
                let (entity, particle_effect, effect_spawners) = world
1333
                    .query::<(Entity, &ParticleEffect, Option<&EffectSpawner>)>()
1334
                    .iter(world)
1335
                    .next()
1336
                    .unwrap();
1337
                assert_eq!(entity, effect_entity);
1338
                assert_eq!(particle_effect.handle, handle);
1339

1340
                assert!(effect_spawners.is_some());
1341
                let effect_spawner = effect_spawners.unwrap();
1342
                let actual_spawner = effect_spawner.settings;
1343

1344
                // Check the spawner ticked
1345
                assert!(effect_spawner.active); // will get deactivated next tick()
1346
                assert_eq!(effect_spawner.spawn_remainder, 0.);
1347
                assert_eq!(effect_spawner.cycle_time, 0.);
1348
                assert_eq!(effect_spawner.completed_cycle_count, 1);
1349
                assert_eq!(effect_spawner.spawn_count, 32);
1350

1351
                assert_eq!(actual_spawner, test_case.asset_spawner);
1352
            }
1353
        }
1354
    }
1355
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc