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

djeedai / bevy_hanabi / 13873674325

15 Mar 2025 02:18PM UTC coverage: 40.166% (+0.2%) from 40.012%
13873674325

Pull #434

github

web-flow
Merge ec3e8bcc6 into a97ab1e92
Pull Request #434: Add spawner cycle count

66 of 80 new or added lines in 1 file covered. (82.5%)

1 existing line in 1 file now uncovered.

3233 of 8049 relevant lines covered (40.17%)

18.68 hits per line

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

64.68
/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 [`Spawner`].
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 + FloatHash> Eq for CpuValue<T> {}
129

130
impl<T: Copy + FromReflect + FloatHash> Hash for CpuValue<T> {
131
    fn hash<H: Hasher>(&self, state: &mut H) {
×
132
        match self {
×
133
            CpuValue::Single(f) => {
×
134
                1_u8.hash(state);
×
135
                f.hash_f32(state);
×
136
            }
137
            CpuValue::Uniform((a, b)) => {
×
138
                2_u8.hash(state);
×
139
                a.hash_f32(state);
×
140
                b.hash_f32(state);
×
141
            }
142
        }
143
    }
144
}
145

146
/// Spawner defining how new particles are emitted.
147
///
148
/// The spawner defines how new particles are emitted and when. Each time the
149
/// spawner ticks, it calculates a number of particles to emit for this frame.
150
/// This spawn count is passed to the GPU for the init compute pass to actually
151
/// allocate the new particles and initialize them. The number of particles to
152
/// spawn is stored as a floating-point number, and any remainder accumulates
153
/// for the next emitting.
154
///
155
/// Once per frame the [`tick_spawners()`] system will add the component if
156
/// it's missing, cloning the [`Spawner`] from the source [`EffectAsset`], then
157
/// tick that [`Spawner`] stored in the component. The resulting number of
158
/// particles to spawn for the frame is then stored into
159
/// [`EffectSpawner::spawn_count`]. You can override that value to manually
160
/// control each frame how many particles are spawned.
161
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
162
#[reflect(Default)]
163
pub struct Spawner {
164
    /// Number of particles to spawn over [`spawn_duration`].
165
    ///
166
    /// [`spawn_duration`]: Spawner::spawn_duration
167
    count: CpuValue<f32>,
168

169
    /// Time over which to spawn [`count`], in seconds.
170
    ///
171
    /// [`count`]: Spawner::count
172
    spawn_duration: CpuValue<f32>,
173

174
    /// Time between bursts of the particle system, in seconds.
175
    ///
176
    /// If this is [`spawn_duration`] or less, the system spawns a steady stream
177
    /// of particles.
178
    ///
179
    /// [`spawn_duration`]: Spawner::spawn_duration
180
    period: CpuValue<f32>,
181

182
    /// Number of cycles the spawner is active before completing.
183
    ///
184
    /// Each cycle lasts for `period`. A value of `0` means "infinite", that is
185
    /// the spanwe emits particle forever as long as it's active.
186
    cycle_count: u32,
187

188
    /// Whether the spawner is active at startup.
189
    ///
190
    /// The value is used to initialize [`EffectSpawner::active`].
191
    ///
192
    /// [`EffectSpawner::active`]: crate::EffectSpawner::active
193
    starts_active: bool,
194
}
195

196
impl Default for Spawner {
197
    fn default() -> Self {
15✔
198
        Self::once(1.0f32.into())
15✔
199
    }
200
}
201

202
impl Spawner {
203
    /// Create a spawner.
204
    ///
205
    /// This is the _raw_ constructor. In general you should prefer using one of
206
    /// the utility constructors [`once()`], [`burst()`], or [`rate()`],
207
    /// which will ensure the control parameters are set consistently relative
208
    /// to each other.
209
    ///
210
    /// The control parameters are:
211
    ///
212
    /// - `count` is the number of particles to spawn over `spawn_duration` in a
213
    ///   burst. It can generate negative or zero random values, in which case
214
    ///   no particle is spawned during the current frame.
215
    /// - `spawn_duration` is how long to spawn particles for. If this is <= 0,
216
    ///   then the particles spawn all at once exactly at the same instant.
217
    /// - `period` is the amount of time between bursts of particles. If this is
218
    ///   <= `spawn_duration`, then the spawner spawns a steady stream of
219
    ///   particles. This is ignored if `cycle_count == 1`.
220
    /// - `cycle_count` is the number of times this pattern occurs. Set this to
221
    ///   `0` to repeat forever while the spawner is active.
222
    ///
223
    /// ```txt
224
    ///  <----------- period ----------->
225
    ///  <- spawn_duration ->
226
    /// |********************|-----------|
227
    ///      spawn 'count'        wait
228
    ///        particles
229
    /// ```
230
    ///
231
    /// Note that the "burst" semantic here doesn't strictly mean a one-off
232
    /// emission, since that emission is spread over a number of simulation
233
    /// frames that total a duration of `spawn_duration`. If you want a strict
234
    /// single-frame burst, simply set the `spawn_duration` to zero; this is
235
    /// what [`once()`] does.
236
    ///
237
    /// # Panics
238
    ///
239
    /// Panics if `period` can produce a negative number (the sample range lower
240
    /// bound is negative), unless the cycle count is exactly 1, in which case
241
    /// the period is irrelevant.
242
    ///
243
    /// Panics if `period` can only produce 0 (the sample range upper bound
244
    /// is not strictly positive), unless the cycle count is exactly 1, in which
245
    /// case the period is irrelevant.
246
    ///
247
    /// Panics any value is infinite.
248
    ///
249
    /// # Example
250
    ///
251
    /// ```
252
    /// # use bevy_hanabi::Spawner;
253
    /// // Spawn 32 particles over 3 seconds, then pause for 7 seconds (10 - 3),
254
    /// // doing that 5 times in total.
255
    /// let spawner = Spawner::new(32.0.into(), 3.0.into(), 10.0.into(), 5);
256
    /// ```
257
    ///
258
    /// [`once()`]: crate::Spawner::once
259
    /// [`burst()`]: crate::Spawner::burst
260
    /// [`rate()`]: crate::Spawner::rate
261
    pub fn new(
34✔
262
        count: CpuValue<f32>,
263
        spawn_duration: CpuValue<f32>,
264
        period: CpuValue<f32>,
265
        cycle_count: u32,
266
    ) -> Self {
267
        assert!(
34✔
268
            cycle_count == 1 || period.range()[0] >= 0.,
47✔
269
            "`period` must not generate negative numbers (period.min was {}, expected >= 0).",
1✔
270
            period.range()[0]
1✔
271
        );
272
        assert!(
33✔
273
            cycle_count == 1 || period.range()[1] > 0.,
45✔
274
            "`period` must be able to generate a positive number (period.max was {}, expected > 0).",
1✔
275
            period.range()[1]
1✔
276
        );
277
        assert!(
32✔
278
            period.range()[0].is_finite() && period.range()[1].is_finite(),
64✔
NEW
279
            "`period` {:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",
×
280
            period
281
        );
282

283
        Self {
284
            count,
285
            spawn_duration,
286
            period,
287
            cycle_count,
288
            starts_active: true,
289
        }
290
    }
291

292
    /// Create a spawner that spawns a burst of particles once.
293
    ///
294
    /// The burst of particles is spawned all at once in the same frame. After
295
    /// that, the spawner deactivates, waiting to be manually reset via
296
    /// [`EffectSpawner::reset()`] and re-activated with
297
    /// [`EffectSpawner::set_active(true)`].
298
    ///
299
    /// This is a convenience for:
300
    ///
301
    /// ```
302
    /// # use bevy_hanabi::{Spawner, CpuValue};
303
    /// # let count = CpuValue::Single(1.);
304
    /// Spawner::new(count, 0.0.into(), 0.0.into(), 1);
305
    /// ```
306
    ///
307
    /// # Example
308
    ///
309
    /// ```
310
    /// # use bevy_hanabi::Spawner;
311
    /// // Spawn 32 particles in a burst once immediately on creation.
312
    /// let spawner = Spawner::once(32.0.into());
313
    /// ```
314
    ///
315
    /// [`EffectSpawner::set_active(true)`]: crate::EffectSpawner::set_active
316
    pub fn once(count: CpuValue<f32>) -> Self {
21✔
317
        Self::new(count, 0.0.into(), 0.0.into(), 1)
21✔
318
    }
319

320
    /// Get whether this spawner emits a single burst.
321
    ///
322
    /// This is true if the cycle count is exactly equal to 1.
323
    pub fn is_once(&self) -> bool {
32✔
324
        self.cycle_count == 1
32✔
325
    }
326

327
    /// Get whether this spawner emits particles forever while active.
328
    ///
329
    /// This is true if the cycle count is exactly equal to 0.
330
    pub fn is_forever(&self) -> bool {
56✔
331
        self.cycle_count == 0
56✔
332
    }
333

334
    /// Create a spawner that spawns particles at `rate`, accumulated each
335
    /// frame. `rate` is in particles per second.
336
    ///
337
    /// This is a convenience for:
338
    ///
339
    /// ```
340
    /// # use bevy_hanabi::{Spawner, CpuValue};
341
    /// # let rate = CpuValue::Single(1.);
342
    /// Spawner::new(rate, 1.0.into(), 1.0.into(), 0);
343
    /// ```
344
    ///
345
    /// # Example
346
    ///
347
    /// ```
348
    /// # use bevy_hanabi::Spawner;
349
    /// // Spawn 10 particles per second, indefinitely.
350
    /// let spawner = Spawner::rate(10.0.into());
351
    /// ```
352
    pub fn rate(rate: CpuValue<f32>) -> Self {
9✔
353
        Self::new(rate, 1.0.into(), 1.0.into(), 0)
9✔
354
    }
355

356
    /// Create a spawner that spawns `count` particles, waits `period` seconds,
357
    /// and repeats forever.
358
    ///
359
    /// This is a convenience for:
360
    ///
361
    /// ```
362
    /// # use bevy_hanabi::{Spawner, CpuValue};
363
    /// # let count = CpuValue::Single(1.);
364
    /// # let period = CpuValue::Single(1.);
365
    /// Spawner::new(count, 0.0.into(), period, 0);
366
    /// ```
367
    ///
368
    /// # Example
369
    ///
370
    /// ```
371
    /// # use bevy_hanabi::Spawner;
372
    /// // Spawn a burst of 5 particles every 3 seconds, indefinitely.
373
    /// let spawner = Spawner::burst(5.0.into(), 3.0.into());
374
    /// ```
375
    pub fn burst(count: CpuValue<f32>, period: CpuValue<f32>) -> Self {
1✔
376
        Self::new(count, 0.0.into(), period, 0)
1✔
377
    }
378

379
    /// Set the number of particles that are spawned each cycle.
380
    pub fn with_count(mut self, count: CpuValue<f32>) -> Self {
×
381
        self.count = count;
×
382
        self
×
383
    }
384

385
    /// Set the number of particles that are spawned each cycle.
386
    pub fn set_count(&mut self, count: CpuValue<f32>) {
×
387
        self.count = count;
×
388
    }
389

390
    /// Get the number of particles that are spawned each cycle.
391
    pub fn count(&self) -> CpuValue<f32> {
×
392
        self.count
×
393
    }
394

395
    /// Set the duration, in seconds, of the spawn time each cycle.
396
    pub fn with_spawn_time(mut self, spawn_duration: CpuValue<f32>) -> Self {
×
397
        self.spawn_duration = spawn_duration;
×
398
        self
×
399
    }
400

401
    /// Set the duration, in seconds, of the spawn time each cycle.
402
    pub fn set_spawn_time(&mut self, spawn_duration: CpuValue<f32>) {
×
403
        self.spawn_duration = spawn_duration;
×
404
    }
405

406
    /// Get the duration, in seconds, of spawn time each cycle.
407
    pub fn spawn_duration(&self) -> CpuValue<f32> {
×
408
        self.spawn_duration
×
409
    }
410

411
    /// Set the duration of a spawn cycle, in seconds.
412
    ///
413
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
414
    /// wait time (if larger than spawn time).
415
    ///
416
    /// # Panics
417
    ///
418
    /// Panics if the period is infinite.
419
    ///
420
    /// [`spawn_duration()`]: Self::spawn_duration
421
    pub fn with_period(mut self, period: CpuValue<f32>) -> Self {
×
NEW
422
        assert!(
×
NEW
423
            period.range()[0].is_finite() && period.range()[1].is_finite(),
×
NEW
424
            "`period` {:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",
×
425
            period
426
        );
427
        self.period = period;
×
428
        self
×
429
    }
430

431
    /// Set the duration of the spawn cycle, in seconds.
432
    ///
433
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
434
    /// wait time (if larger than spawn time).
435
    ///
436
    /// # Panics
437
    ///
438
    /// Panics if the period is infinite.
439
    ///
440
    /// [`spawn_duration()`]: Self::spawn_duration
441
    pub fn set_period(&mut self, period: CpuValue<f32>) {
×
NEW
442
        assert!(
×
NEW
443
            period.range()[0].is_finite() && period.range()[1].is_finite(),
×
NEW
444
            "`period` {:?} has an infinite bound. If upgrading from a previous version, use `cycle_count = 1` instead for a single-cycle burst.",
×
445
            period
446
        );
UNCOV
447
        self.period = period;
×
448
    }
449

450
    /// Get the duration of the spawn cycle, in seconds.
451
    ///
452
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
453
    /// wait time (if larger than spawn time).
454
    ///
455
    /// [`spawn_duration()`]: Self::spawn_duration
456
    pub fn period(&self) -> CpuValue<f32> {
×
457
        self.period
×
458
    }
459

460
    /// Set the number of cycles to spawn for.
461
    ///
462
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
463
    /// wait time (if larger than spawn time). It lasts for [`period()`].
464
    ///
465
    /// [`spawn_duration()`]: Self::spawn_duration
466
    /// [`period()`]: Self::period
NEW
467
    pub fn with_cycle_count(mut self, cycle_count: u32) -> Self {
×
NEW
468
        self.cycle_count = cycle_count;
×
NEW
469
        self
×
470
    }
471

472
    /// Set the number of cycles to spawn for.
473
    ///
474
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
475
    /// wait time (if larger than spawn time). It lasts for [`period()`].
476
    ///
477
    /// [`spawn_duration()`]: Self::spawn_duration
478
    /// [`period()`]: Self::period
NEW
479
    pub fn set_cycle_count(&mut self, cycle_count: u32) {
×
NEW
480
        self.cycle_count = cycle_count;
×
481
    }
482

483
    /// Get the number of cycles to spawn for.
484
    ///
485
    /// A spawn cycle includes the [`spawn_duration()`] value, and any extra
486
    /// wait time (if larger than spawn time). It lasts for [`period()`].
487
    ///
488
    /// [`spawn_duration()`]: Self::spawn_duration
489
    /// [`period()`]: Self::period
490
    pub fn cycle_count(&self) -> u32 {
25✔
491
        self.cycle_count
25✔
492
    }
493

494
    /// Sets whether the spawner starts active when the effect is instantiated.
495
    ///
496
    /// This value will be transfered to the active state of the
497
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
498
    /// any particle.
499
    pub fn with_starts_active(mut self, starts_active: bool) -> Self {
2✔
500
        self.starts_active = starts_active;
2✔
501
        self
2✔
502
    }
503

504
    /// Set whether the spawner starts active when the effect is instantiated.
505
    ///
506
    /// This value will be transfered to the active state of the
507
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
508
    /// any particle.
509
    pub fn set_starts_active(&mut self, starts_active: bool) {
×
510
        self.starts_active = starts_active;
×
511
    }
512

513
    /// Get whether the spawner starts active when the effect is instantiated.
514
    ///
515
    /// This value will be transfered to the active state of the
516
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
517
    /// any particle.
518
    pub fn starts_active(&self) -> bool {
13✔
519
        self.starts_active
13✔
520
    }
521
}
522

523
/// Runtime structure maintaining the state of the spawner for a particle group.
524
#[derive(Debug, Default, Clone, Copy, PartialEq, Component, Reflect)]
525
#[reflect(Component)]
526
pub struct EffectSpawner {
527
    /// The spawner configuration extracted from the [`EffectAsset`], or
528
    /// directly overriden by the user.
529
    pub spawner: Spawner,
530

531
    /// Accumulated time for the current (partial) cycle, in seconds.
532
    cycle_time: f32,
533

534
    /// Number of cycles already completed.
535
    completed_cycle_count: u32,
536

537
    /// Sampled value of `spawn_duration` until `period` is reached. This is the
538
    /// duration of the "active" period during which we spawn particles, as
539
    /// opposed to the "wait" period during which we do nothing until the next
540
    /// spawn cycle.
541
    sampled_spawn_duration: f32,
542

543
    /// Sampled value of the time period, in seconds, until the next spawn
544
    /// cycle.
545
    sampled_period: f32,
546

547
    /// Sampled value of the number of particles to spawn per `spawn_duration`.
548
    sampled_count: f32,
549

550
    /// Number of particles to spawn this frame.
551
    ///
552
    /// This value is normally updated by calling [`tick()`], which
553
    /// automatically happens once per frame when the [`tick_initializers()`]
554
    /// system runs in the [`PostUpdate`] schedule.
555
    ///
556
    /// You can manually assign this value to override the one calculated by
557
    /// [`tick()`]. Note in this case that you need to override the value after
558
    /// the automated one was calculated, by ordering your system
559
    /// after [`tick_spawners()`] or [`EffectSystems::TickSpawners`].
560
    ///
561
    /// [`tick()`]: crate::EffectSpawner::tick
562
    /// [`EffectSystems::TickSpawners`]: crate::EffectSystems::TickSpawners
563
    pub spawn_count: u32,
564

565
    /// Fractional remainder of particle count to spawn.
566
    ///
567
    /// This is accumulated each tick, and the integral part is added to
568
    /// `spawn_count`. The reminder gets saved for next frame.
569
    spawn_remainder: f32,
570

571
    /// Whether the spawner is active. Defaults to `true`. An inactive spawner
572
    /// doesn't tick (no particle spawned, no internal time updated).
573
    active: bool,
574
}
575

576
impl EffectSpawner {
577
    /// Create a new spawner state from a [`Spawner`].
578
    pub fn new(spawner: &Spawner) -> Self {
11✔
579
        Self {
580
            spawner: *spawner,
11✔
581
            cycle_time: 0.,
582
            completed_cycle_count: 0,
583
            sampled_spawn_duration: 0.,
584
            sampled_period: 0.,
585
            sampled_count: 0.,
586
            spawn_count: 0,
587
            spawn_remainder: 0.,
588
            active: spawner.starts_active(),
11✔
589
        }
590
    }
591

592
    /// Set whether the spawner is active.
593
    ///
594
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
595
    pub fn with_active(mut self, active: bool) -> Self {
×
596
        self.active = active;
×
597
        self
×
598
    }
599

600
    /// Set whether the spawner is active.
601
    ///
602
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
603
    #[inline]
604
    pub fn set_active(&mut self, active: bool) {
6✔
605
        self.active = active;
6✔
606
    }
607

608
    /// Get whether the spawner is active.
609
    ///
610
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
611
    #[inline]
612
    pub fn is_active(&self) -> bool {
14✔
613
        self.active
14✔
614
    }
615

616
    /// Get the time relative to the beginning of the current cycle.
617
    #[inline]
618
    pub fn cycle_time(&self) -> f32 {
3✔
619
        self.cycle_time
3✔
620
    }
621

622
    /// Get the spawn duration for the current cycle.
623
    ///
624
    /// This value can change every cycle if [`Spawner::spawn_duration`] is a
625
    /// randomly distributed value.
626
    #[inline]
627
    pub fn cycle_spawn_duration(&self) -> f32 {
3✔
628
        self.sampled_spawn_duration
3✔
629
    }
630

631
    /// Get the period of the current cycle.
632
    ///
633
    /// This value can change every cycle if [`Spawner::period`] is a randomly
634
    /// distributed value. If the effect spawns only once, and therefore its
635
    /// cycle period is ignored, this returns `0`.
636
    #[inline]
637
    pub fn cycle_period(&self) -> f32 {
3✔
638
        if self.spawner.is_once() {
3✔
NEW
639
            0.
×
640
        } else {
641
            self.sampled_period
3✔
642
        }
643
    }
644

645
    /// Get the progress ratio in 0..1 of the current cycle.
646
    ///
647
    /// This is the ratio of the [`cycle_time()`] over [`cycle_period()`]. If
648
    /// the effect spawns only once, and therefore its cycle period is
649
    /// ignored, this returns `0`.
650
    ///
651
    /// [`cycle_time()`]: Self::cycle_time
652
    /// [`cycle_period()`]: Self::cycle_period
653
    #[inline]
654
    pub fn cycle_ratio(&self) -> f32 {
3✔
655
        if self.spawner.is_once() {
3✔
NEW
656
            0.
×
657
        } else {
658
            self.cycle_time / self.sampled_period
3✔
659
        }
660
    }
661

662
    /// Get the number of particles to spawn during the current cycle
663
    ///
664
    /// This value can change every cycle if [`Spawner::count`] is a randomly
665
    /// distributed value.
666
    #[inline]
667
    pub fn cycle_spawn_count(&self) -> f32 {
3✔
668
        self.sampled_count
3✔
669
    }
670

671
    /// Get the number of completed cycles since last [`reset()`].
672
    ///
673
    /// The value loops back if the pattern repeats forever
674
    /// ([`Spawner::is_forever()`] is `true`).
675
    ///
676
    /// [`reset()`]: Self::reset
677
    #[inline]
678
    pub fn completed_cycle_count(&self) -> u32 {
5✔
679
        self.completed_cycle_count
5✔
680
    }
681

682
    /// Reset the spawner state.
683
    ///
684
    /// This resets the internal spawner time to zero, and restarts any internal
685
    /// particle counter.
686
    ///
687
    /// Use this, for example, to immediately spawn some particles in a spawner
688
    /// constructed with [`Spawner::once`].
689
    ///
690
    /// [`Spawner::once`]: crate::Spawner::once
691
    pub fn reset(&mut self) {
2✔
692
        self.cycle_time = 0.;
2✔
693
        self.completed_cycle_count = 0;
2✔
694
        self.sampled_spawn_duration = 0.;
2✔
695
        self.sampled_period = 0.;
2✔
696
        self.sampled_count = 0.;
2✔
697
        self.spawn_count = 0;
2✔
698
        self.spawn_remainder = 0.;
2✔
699
    }
700

701
    /// Tick the spawner to calculate the number of particles to spawn this
702
    /// frame.
703
    ///
704
    /// The frame delta time `dt` is added to the current spawner time, before
705
    /// the spawner calculates the number of particles to spawn.
706
    ///
707
    /// This method is called automatically by [`tick_initializers()`] during
708
    /// the [`PostUpdate`], so you normally don't have to call it yourself
709
    /// manually.
710
    ///
711
    /// # Returns
712
    ///
713
    /// The integral number of particles to spawn this frame. Any fractional
714
    /// remainder is saved for the next call.
715
    pub fn tick(&mut self, mut dt: f32, rng: &mut Pcg32) -> u32 {
40✔
716
        // Handle max cycle count here, which might have been reached last frame, but we
717
        // couldn't deactivate before the end of the frame.
718
        if !self.spawner.is_forever() && (self.completed_cycle_count >= self.spawner.cycle_count())
56✔
719
        {
720
            self.active = false;
3✔
721
        }
722

723
        if !self.active {
40✔
724
            self.spawn_count = 0;
8✔
725
            return 0;
8✔
726
        }
727

728
        // Use a loop in case the timestep dt spans multiple cycles
729
        loop {
730
            // Check if this is a new cycle which needs resampling
731
            if self.sampled_period == 0.0 {
38✔
732
                if self.spawner.is_once() {
26✔
733
                    self.sampled_spawn_duration = self.spawner.spawn_duration.sample(rng);
7✔
734
                    // Period is unchecked, should be ignored (could sample to <= 0). Use the spawn
735
                    // duration, but ensure we have something > 0 as a marker that we've resampled.
736
                    self.sampled_period = self.sampled_spawn_duration.max(1e-12);
7✔
737
                } else {
738
                    self.sampled_period = self.spawner.period.sample(rng);
12✔
739
                    assert!(self.sampled_period > 0.);
12✔
740
                    self.sampled_spawn_duration = self
12✔
741
                        .spawner
12✔
742
                        .spawn_duration
12✔
743
                        .sample(rng)
12✔
744
                        .clamp(0., self.sampled_period);
12✔
745
                }
746
                self.sampled_spawn_duration = self.spawner.spawn_duration.sample(rng);
19✔
747
                self.sampled_count = self.spawner.count.sample(rng).max(0.);
19✔
748
            }
749

750
            let new_time = self.cycle_time + dt;
38✔
751

752
            // If inside the spawn period, accumulate some particle spawn count
753
            if self.cycle_time <= self.sampled_spawn_duration {
38✔
754
                // If the spawn time is very small, close to zero, spawn all particles
755
                // immediately in one burst over a single frame.
756
                self.spawn_remainder += if self.sampled_spawn_duration < 1e-5f32.max(dt / 100.0) {
34✔
757
                    self.sampled_count
10✔
758
                } else {
759
                    // Spawn an amount of particles equal to the fraction of time the current frame
760
                    // spans compared to the total burst duration.
761
                    let ratio = ((new_time.min(self.sampled_spawn_duration) - self.cycle_time)
24✔
762
                        / self.sampled_spawn_duration)
24✔
763
                        .clamp(0., 1.);
24✔
764
                    self.sampled_count * ratio
24✔
765
                };
766
            }
767

768
            // Increment current time
769
            self.cycle_time = new_time;
770

771
            // Check for cycle completion
772
            if self.cycle_time >= self.sampled_period {
773
                dt = self.cycle_time - self.sampled_period;
14✔
774
                self.cycle_time = 0.0;
14✔
775
                self.completed_cycle_count += 1;
14✔
776

777
                // Mark as "need resampling"
778
                self.sampled_period = 0.0;
14✔
779

780
                // If this was the last cycle, we're done
781
                if !self.spawner.is_forever()
14✔
782
                    && (self.completed_cycle_count >= self.spawner.cycle_count())
9✔
783
                {
784
                    // Don't deactivate quite yet, otherwise we'll miss the spawns for this frame
785
                    break;
8✔
786
                }
787
            } else {
788
                // We're done for this frame
789
                break;
24✔
790
            }
791
        }
792

793
        // Extract integral number of particles to spawn this frame, keep remainder for
794
        // next one
795
        let count = self.spawn_remainder.floor();
32✔
796
        self.spawn_remainder -= count;
32✔
797
        self.spawn_count = count as u32;
32✔
798

799
        self.spawn_count
32✔
800
    }
801
}
802

803
/// Tick all the [`EffectSpawner`] components.
804
///
805
/// This system runs in the [`PostUpdate`] stage, after the visibility system
806
/// has updated the [`InheritedVisibility`] of each effect instance (see
807
/// [`VisibilitySystems::VisibilityPropagate`]). Hidden instances are not
808
/// updated, unless the [`EffectAsset::simulation_condition`]
809
/// is set to [`SimulationCondition::Always`]. If no [`InheritedVisibility`] is
810
/// present, the effect is assumed to be visible.
811
///
812
/// Note that by that point the [`ViewVisibility`] is not yet calculated, and it
813
/// may happen that spawners are ticked but no effect is visible in any view
814
/// even though some are "visible" (active) in the [`World`]. The actual
815
/// per-view culling of invisible (not in view) effects is performed later on
816
/// the render world.
817
///
818
/// Once the system determined that the effect instance needs to be simulated
819
/// this frame, it ticks the effect's spawner by calling
820
/// [`EffectSpawner::tick()`], adding a new [`EffectSpawner`] component if it
821
/// doesn't already exist on the same entity as the [`ParticleEffect`].
822
///
823
/// [`VisibilitySystems::VisibilityPropagate`]: bevy::render::view::VisibilitySystems::VisibilityPropagate
824
/// [`EffectAsset::simulation_condition`]: crate::EffectAsset::simulation_condition
825
pub fn tick_spawners(
3✔
826
    mut commands: Commands,
827
    time: Res<Time<EffectSimulation>>,
828
    effects: Res<Assets<EffectAsset>>,
829
    mut rng: ResMut<Random>,
830
    mut query: Query<(
831
        Entity,
832
        &ParticleEffect,
833
        &InheritedVisibility,
834
        Option<&mut EffectSpawner>,
835
    )>,
836
) {
837
    trace!("tick_spawners()");
3✔
838

839
    let dt = time.delta_secs();
3✔
840

841
    for (entity, effect, inherited_visibility, maybe_spawner) in query.iter_mut() {
3✔
842
        let Some(asset) = effects.get(&effect.handle) else {
6✔
843
            trace!(
×
844
                "Effect asset with handle {:?} is not available; skipped initializers tick.",
×
845
                effect.handle
846
            );
847
            continue;
×
848
        };
849

850
        if asset.simulation_condition == SimulationCondition::WhenVisible
851
            && !inherited_visibility.get()
2✔
852
        {
853
            trace!(
1✔
854
                "Effect asset with handle {:?} is not visible, and simulates only WhenVisible; skipped initializers tick.",
×
855
                effect.handle
856
            );
857
            continue;
1✔
858
        }
859

860
        if let Some(mut effect_spawner) = maybe_spawner {
×
861
            effect_spawner.tick(dt, &mut rng.0);
×
862
            continue;
×
863
        }
864

865
        let effect_spawner = {
2✔
866
            let mut effect_spawner = EffectSpawner::new(&asset.spawner);
2✔
867
            effect_spawner.tick(dt, &mut rng.0);
2✔
868
            effect_spawner
2✔
869
        };
870
        commands.entity(entity).insert(effect_spawner);
2✔
871
    }
872
}
873

874
#[cfg(test)]
875
mod test {
876
    use std::time::Duration;
877

878
    use bevy::{
879
        asset::{
880
            io::{
881
                memory::{Dir, MemoryAssetReader},
882
                AssetSourceBuilder, AssetSourceBuilders, AssetSourceId,
883
            },
884
            AssetServerMode,
885
        },
886
        render::view::{VisibilityPlugin, VisibilitySystems},
887
        tasks::{IoTaskPool, TaskPoolBuilder},
888
    };
889

890
    use super::*;
891
    use crate::Module;
892

893
    #[test]
894
    fn test_range_single() {
895
        let value = CpuValue::Single(1.0);
896
        assert_eq!(value.range(), [1.0, 1.0]);
897
    }
898

899
    #[test]
900
    fn test_range_uniform() {
901
        let value = CpuValue::Uniform((1.0, 3.0));
902
        assert_eq!(value.range(), [1.0, 3.0]);
903
    }
904

905
    #[test]
906
    fn test_range_uniform_reverse() {
907
        let value = CpuValue::Uniform((3.0, 1.0));
908
        assert_eq!(value.range(), [1.0, 3.0]);
909
    }
910

911
    #[test]
912
    fn test_new() {
913
        let rng = &mut new_rng();
914
        // 3 particles over 3 seconds, pause 7 seconds (total 10 seconds period). 2
915
        // cycles.
916
        let spawner = Spawner::new(3.0.into(), 3.0.into(), 10.0.into(), 2);
917
        let mut spawner = EffectSpawner::new(&spawner);
918
        let count = spawner.tick(2., rng); // t = 2s
919
        assert_eq!(count, 2);
920
        assert!(spawner.is_active());
921
        assert_eq!(spawner.cycle_time(), 2.);
922
        assert_eq!(spawner.cycle_spawn_duration(), 3.);
923
        assert_eq!(spawner.cycle_period(), 10.);
924
        assert_eq!(spawner.cycle_ratio(), 0.2); // 2s / 10s
925
        assert_eq!(spawner.cycle_spawn_count(), 3.);
926
        assert_eq!(spawner.completed_cycle_count(), 0);
927
        let count = spawner.tick(5., rng); // t = 7s
928
        assert_eq!(count, 1);
929
        assert!(spawner.is_active());
930
        assert_eq!(spawner.cycle_time(), 7.);
931
        assert_eq!(spawner.cycle_spawn_duration(), 3.);
932
        assert_eq!(spawner.cycle_period(), 10.);
933
        assert_eq!(spawner.cycle_ratio(), 0.7); // 7s / 10s
934
        assert_eq!(spawner.cycle_spawn_count(), 3.);
935
        assert_eq!(spawner.completed_cycle_count(), 0);
936
        let count = spawner.tick(8., rng); // t = 15s
937
        assert_eq!(count, 3);
938
        assert!(spawner.is_active());
939
        assert_eq!(spawner.cycle_time(), 5.); // 15. mod 10.
940
        assert_eq!(spawner.cycle_spawn_duration(), 3.);
941
        assert_eq!(spawner.cycle_period(), 10.);
942
        assert_eq!(spawner.cycle_ratio(), 0.5); // 5s / 10s
943
        assert_eq!(spawner.cycle_spawn_count(), 3.);
944
        assert_eq!(spawner.completed_cycle_count(), 1);
945
        let count = spawner.tick(10., rng); // t = 25s
946
        assert_eq!(count, 0);
947
        assert!(spawner.is_active()); // still active for one frame
948
        assert_eq!(spawner.completed_cycle_count(), 2);
949
        let count = spawner.tick(0.1, rng); // t = 25.1s
950
        assert_eq!(count, 0);
951
        assert!(!spawner.is_active());
952
        assert_eq!(spawner.completed_cycle_count(), 2);
953
    }
954

955
    #[test]
956
    #[should_panic]
957
    fn test_new_panic_negative_period() {
958
        let _ = Spawner::new(3.0.into(), 1.0.into(), CpuValue::Uniform((-1., 1.)), 0);
959
    }
960

961
    #[test]
962
    #[should_panic]
963
    fn test_new_panic_zero_period() {
964
        let _ = Spawner::new(3.0.into(), 1.0.into(), CpuValue::Uniform((0., 0.)), 0);
965
    }
966

967
    #[test]
968
    fn test_once() {
969
        let rng = &mut new_rng();
970
        let spawner = Spawner::once(5.0.into());
971
        assert!(spawner.is_once());
972
        let mut spawner = EffectSpawner::new(&spawner);
973
        assert!(spawner.is_active());
974
        let count = spawner.tick(0.001, rng);
975
        assert_eq!(count, 5);
976
        let count = spawner.tick(100.0, rng);
977
        assert_eq!(count, 0);
978
    }
979

980
    #[test]
981
    fn test_once_reset() {
982
        let rng = &mut new_rng();
983
        let spawner = Spawner::once(5.0.into());
984
        assert!(spawner.is_once());
985
        assert!(spawner.starts_active());
986
        let mut spawner = EffectSpawner::new(&spawner);
987
        spawner.tick(1.0, rng);
988
        spawner.reset();
989
        let count = spawner.tick(1.0, rng);
990
        assert_eq!(count, 5);
991
    }
992

993
    #[test]
994
    fn test_once_start_inactive() {
995
        let rng = &mut new_rng();
996

997
        let spawner = Spawner::once(5.0.into()).with_starts_active(false);
998
        assert!(spawner.is_once());
999
        assert!(!spawner.starts_active());
1000
        let mut spawner = EffectSpawner::new(&spawner);
1001

1002
        // Inactive; no-op
1003
        let count = spawner.tick(1.0, rng);
1004
        assert_eq!(count, 0);
1005

1006
        spawner.set_active(true);
1007

1008
        // Active; spawns
1009
        let count = spawner.tick(1.0, rng);
1010
        assert_eq!(count, 5);
1011
        assert!(spawner.is_active());
1012

1013
        // Completed; deactivated itself
1014
        let count = spawner.tick(1.0, rng);
1015
        assert_eq!(count, 0);
1016
        assert!(!spawner.is_active());
1017

1018
        // Reset internal state, but doesn't activate
1019
        spawner.reset();
1020
        let count = spawner.tick(1.0, rng);
1021
        assert_eq!(count, 0);
1022
        assert!(!spawner.is_active());
1023

1024
        // Now activate again
1025
        spawner.set_active(true);
1026
        let count = spawner.tick(1.0, rng);
1027
        assert_eq!(count, 5);
1028
        assert!(spawner.is_active());
1029
    }
1030

1031
    #[test]
1032
    fn test_rate() {
1033
        let rng = &mut new_rng();
1034
        let spawner = Spawner::rate(5.0.into());
1035
        assert!(!spawner.is_once());
1036
        assert!(spawner.is_forever());
1037
        let mut spawner = EffectSpawner::new(&spawner);
1038
        // Slightly over 1.0 to avoid edge case
1039
        let count = spawner.tick(1.01, rng);
1040
        assert_eq!(count, 5);
1041
        let count = spawner.tick(0.4, rng);
1042
        assert_eq!(count, 2);
1043
    }
1044

1045
    #[test]
1046
    fn test_rate_active() {
1047
        let rng = &mut new_rng();
1048
        let spawner = Spawner::rate(5.0.into());
1049
        assert!(!spawner.is_once());
1050
        let mut spawner = EffectSpawner::new(&spawner);
1051
        spawner.tick(1.01, rng);
1052
        spawner.set_active(false);
1053
        assert!(!spawner.is_active());
1054
        let count = spawner.tick(0.4, rng);
1055
        assert_eq!(count, 0);
1056
        spawner.set_active(true);
1057
        assert!(spawner.is_active());
1058
        let count = spawner.tick(0.4, rng);
1059
        assert_eq!(count, 2);
1060
    }
1061

1062
    #[test]
1063
    fn test_rate_accumulate() {
1064
        let rng = &mut new_rng();
1065
        let spawner = Spawner::rate(5.0.into());
1066
        assert!(!spawner.is_once());
1067
        let mut spawner = EffectSpawner::new(&spawner);
1068
        // 13 ticks instead of 12 to avoid edge case
1069
        let count = (0..13).map(|_| spawner.tick(1.0 / 60.0, rng)).sum::<u32>();
1070
        assert_eq!(count, 1);
1071
    }
1072

1073
    #[test]
1074
    fn test_burst() {
1075
        let rng = &mut new_rng();
1076
        let spawner = Spawner::burst(5.0.into(), 2.0.into());
1077
        assert!(!spawner.is_once());
1078
        assert!(spawner.is_forever());
1079
        let mut spawner = EffectSpawner::new(&spawner);
1080
        let count = spawner.tick(1.0, rng);
1081
        assert_eq!(count, 5);
1082
        let count = spawner.tick(4.0, rng);
1083
        assert_eq!(count, 10);
1084
        let count = spawner.tick(0.1, rng);
1085
        assert_eq!(count, 0);
1086
    }
1087

1088
    #[test]
1089
    fn test_with_active() {
1090
        let rng = &mut new_rng();
1091
        let spawner = Spawner::rate(5.0.into()).with_starts_active(false);
1092
        let mut spawner = EffectSpawner::new(&spawner);
1093
        assert!(!spawner.is_active());
1094
        let count = spawner.tick(1., rng);
1095
        assert_eq!(count, 0);
1096
        spawner.set_active(false); // no-op
1097
        let count = spawner.tick(1., rng);
1098
        assert_eq!(count, 0);
1099
        spawner.set_active(true);
1100
        assert!(spawner.is_active());
1101
        let count = spawner.tick(1., rng);
1102
        assert_eq!(count, 5);
1103
    }
1104

1105
    fn make_test_app() -> App {
1106
        IoTaskPool::get_or_init(|| {
1107
            TaskPoolBuilder::default()
1108
                .num_threads(1)
1109
                .thread_name("Hanabi test IO Task Pool".to_string())
1110
                .build()
1111
        });
1112

1113
        let mut app = App::new();
1114

1115
        let watch_for_changes = false;
1116
        let mut builders = app
1117
            .world_mut()
1118
            .get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
1119
        let dir = Dir::default();
1120
        let dummy_builder = AssetSourceBuilder::default()
1121
            .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }));
1122
        builders.insert(AssetSourceId::Default, dummy_builder);
1123
        let sources = builders.build_sources(watch_for_changes, false);
1124
        let asset_server =
1125
            AssetServer::new(sources, AssetServerMode::Unprocessed, watch_for_changes);
1126

1127
        app.insert_resource(asset_server);
1128
        // app.add_plugins(DefaultPlugins);
1129
        app.init_asset::<Mesh>();
1130
        app.add_plugins(VisibilityPlugin);
1131
        app.init_resource::<Time<EffectSimulation>>();
1132
        app.insert_resource(Random(new_rng()));
1133
        app.init_asset::<EffectAsset>();
1134
        app.add_systems(
1135
            PostUpdate,
1136
            tick_spawners.after(VisibilitySystems::CheckVisibility),
1137
        );
1138

1139
        app
1140
    }
1141

1142
    /// Test case for `tick_initializers()`.
1143
    struct TestCase {
1144
        /// Initial entity visibility on spawn. If `None`, do not add a
1145
        /// [`Visibility`] component.
1146
        visibility: Option<Visibility>,
1147

1148
        /// Spawner assigned to the `EffectAsset`.
1149
        asset_spawner: Spawner,
1150
    }
1151

1152
    impl TestCase {
1153
        fn new(visibility: Option<Visibility>, asset_spawner: Spawner) -> Self {
1154
            Self {
1155
                visibility,
1156
                asset_spawner,
1157
            }
1158
        }
1159
    }
1160

1161
    #[test]
1162
    fn test_tick_spawners() {
1163
        let asset_spawner = Spawner::once(32.0.into());
1164

1165
        for test_case in &[
1166
            TestCase::new(None, asset_spawner),
1167
            TestCase::new(Some(Visibility::Hidden), asset_spawner),
1168
            TestCase::new(Some(Visibility::Visible), asset_spawner),
1169
        ] {
1170
            let mut app = make_test_app();
1171

1172
            let (effect_entity, handle) = {
1173
                let world = app.world_mut();
1174

1175
                // Add effect asset
1176
                let mut assets = world.resource_mut::<Assets<EffectAsset>>();
1177
                let mut asset = EffectAsset::new(64, test_case.asset_spawner, Module::default());
1178
                asset.simulation_condition = if test_case.visibility.is_some() {
1179
                    SimulationCondition::WhenVisible
1180
                } else {
1181
                    SimulationCondition::Always
1182
                };
1183
                let handle = assets.add(asset);
1184

1185
                // Spawn particle effect
1186
                let entity = if let Some(visibility) = test_case.visibility {
1187
                    world
1188
                        .spawn((
1189
                            visibility,
1190
                            InheritedVisibility::default(),
1191
                            ParticleEffect {
1192
                                handle: handle.clone(),
1193
                            },
1194
                        ))
1195
                        .id()
1196
                } else {
1197
                    world
1198
                        .spawn((ParticleEffect {
1199
                            handle: handle.clone(),
1200
                        },))
1201
                        .id()
1202
                };
1203

1204
                // Spawn a camera, otherwise ComputedVisibility stays at HIDDEN
1205
                world.spawn(Camera3d::default());
1206

1207
                (entity, handle)
1208
            };
1209

1210
            // Tick once
1211
            let _cur_time = {
1212
                // Make sure to increment the current time so that the spawners spawn something.
1213
                // Note that `Time` has this weird behavior where the common quantities like
1214
                // `Time::delta_secs()` only update after the *second* update. So we tick the
1215
                // `Time` twice here to enforce this.
1216
                let mut time = app.world_mut().resource_mut::<Time<EffectSimulation>>();
1217
                time.advance_by(Duration::from_millis(16));
1218
                time.elapsed()
1219
            };
1220
            app.update();
1221

1222
            let world = app.world_mut();
1223

1224
            // Check the state of the components after `tick_initializers()` ran
1225
            if let Some(test_visibility) = test_case.visibility {
1226
                // Simulated-when-visible effect (SimulationCondition::WhenVisible)
1227

1228
                let (entity, visibility, inherited_visibility, particle_effect, effect_spawner) =
1229
                    world
1230
                        .query::<(
1231
                            Entity,
1232
                            &Visibility,
1233
                            &InheritedVisibility,
1234
                            &ParticleEffect,
1235
                            Option<&EffectSpawner>,
1236
                        )>()
1237
                        .iter(world)
1238
                        .next()
1239
                        .unwrap();
1240
                assert_eq!(entity, effect_entity);
1241
                assert_eq!(visibility, test_visibility);
1242
                assert_eq!(
1243
                    inherited_visibility.get(),
1244
                    test_visibility == Visibility::Visible
1245
                );
1246
                assert_eq!(particle_effect.handle, handle);
1247
                if inherited_visibility.get() {
1248
                    // If visible, `tick_initializers()` spawns the EffectSpawner and ticks it
1249
                    assert!(effect_spawner.is_some());
1250
                    let effect_spawner = effect_spawner.unwrap();
1251
                    let actual_spawner = effect_spawner.spawner;
1252

1253
                    // Check the spawner ticked
1254
                    assert!(effect_spawner.active); // will get deactivated next tick()
1255
                    assert_eq!(effect_spawner.spawn_remainder, 0.);
1256
                    assert_eq!(effect_spawner.cycle_time, 0.);
1257
                    assert_eq!(effect_spawner.completed_cycle_count, 1);
1258
                    assert_eq!(effect_spawner.spawn_count, 32);
1259

1260
                    assert_eq!(actual_spawner, test_case.asset_spawner);
1261
                } else {
1262
                    // If not visible, `tick_initializers()` skips the effect entirely so won't
1263
                    // spawn an `EffectSpawner` for it
1264
                    assert!(effect_spawner.is_none());
1265
                }
1266
            } else {
1267
                // Always-simulated effect (SimulationCondition::Always)
1268

1269
                let (entity, particle_effect, effect_spawners) = world
1270
                    .query::<(Entity, &ParticleEffect, Option<&EffectSpawner>)>()
1271
                    .iter(world)
1272
                    .next()
1273
                    .unwrap();
1274
                assert_eq!(entity, effect_entity);
1275
                assert_eq!(particle_effect.handle, handle);
1276

1277
                assert!(effect_spawners.is_some());
1278
                let effect_spawner = effect_spawners.unwrap();
1279
                let actual_spawner = effect_spawner.spawner;
1280

1281
                // Check the spawner ticked
1282
                assert!(effect_spawner.active); // will get deactivated next tick()
1283
                assert_eq!(effect_spawner.spawn_remainder, 0.);
1284
                assert_eq!(effect_spawner.cycle_time, 0.);
1285
                assert_eq!(effect_spawner.completed_cycle_count, 1);
1286
                assert_eq!(effect_spawner.spawn_count, 32);
1287

1288
                assert_eq!(actual_spawner, test_case.asset_spawner);
1289
            }
1290
        }
1291
    }
1292
}
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