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

djeedai / bevy_hanabi / 11543837292

27 Oct 2024 09:10PM UTC coverage: 57.849% (-1.2%) from 59.035%
11543837292

Pull #387

github

web-flow
Merge a72c10537 into 75f07d778
Pull Request #387: Unify the clone modifier and spawners, and fix races.

114 of 621 new or added lines in 7 files covered. (18.36%)

23 existing lines in 5 files now uncovered.

3534 of 6109 relevant lines covered (57.85%)

23.02 hits per line

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

47.98
/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 {
18✔
15
    let mut rng = rand::thread_rng();
18✔
16
    let mut seed = [0u8; 16];
18✔
17
    seed.copy_from_slice(&Uniform::from(0..=u128::MAX).sample(&mut rng).to_le_bytes());
18✔
18
    Pcg32::from_seed(seed)
18✔
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 {
71✔
98
        match self {
71✔
99
            Self::Single(x) => *x,
71✔
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] {
42✔
109
        match self {
42✔
110
            Self::Single(x) => [*x; 2],
35✔
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 {
57✔
124
        Self::Single(t)
57✔
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
#[derive(Clone, Copy, PartialEq, Debug, Reflect, Serialize, Deserialize)]
147
#[reflect(Serialize, Deserialize)]
148
pub enum Initializer {
149
    Spawner(Spawner),
150
    Cloner(Cloner),
151
}
152

153
impl From<Spawner> for Initializer {
154
    #[inline]
155
    fn from(value: Spawner) -> Self {
21✔
156
        Self::Spawner(value)
21✔
157
    }
158
}
159

160
impl From<Cloner> for Initializer {
161
    #[inline]
NEW
162
    fn from(value: Cloner) -> Self {
×
NEW
163
        Self::Cloner(value)
×
164
    }
165
}
166

167
impl Initializer {
168
    #[cfg(test)]
169
    fn get_spawner(&self) -> Option<&Spawner> {
9✔
170
        match *self {
9✔
171
            Initializer::Spawner(ref spawner) => Some(spawner),
9✔
NEW
172
            Initializer::Cloner(_) => None,
×
173
        }
174
    }
175
}
176

177
/// Spawner defining how new particles are emitted.
178
///
179
/// The spawner defines how new particles are emitted and when. Each time the
180
/// spawner ticks, it calculates a number of particles to emit for this frame.
181
/// This spawn count is passed to the GPU for the init compute pass to actually
182
/// allocate the new particles and initialize them. The number of particles to
183
/// spawn is stored as a floating-point number, and any remainder accumulates
184
/// for the next emitting.
185
///
186
/// The spawner itself is embedded into the [`EffectInitializers`] component.
187
/// Once per frame the [`tick_spawners()`] system will add the component if
188
/// it's missing, cloning the [`Spawner`] from the source [`EffectAsset`], then
189
/// tick the [`Spawner`] stored in the [`EffectInitializers`].
190
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
191
#[reflect(Default)]
192
pub struct Spawner {
193
    /// Number of particles to spawn over [`spawn_time`].
194
    ///
195
    /// [`spawn_time`]: Spawner::spawn_time
196
    num_particles: CpuValue<f32>,
197

198
    /// Time over which to spawn `num_particles`, in seconds.
199
    spawn_time: CpuValue<f32>,
200

201
    /// Time between bursts of the particle system, in seconds.
202
    /// If this is infinity, there's only one burst.
203
    /// If this is `spawn_time`, the system spawns a steady stream of particles.
204
    period: CpuValue<f32>,
205

206
    /// Whether the spawner is active at startup. The value is used to initialize
207
    /// [`EffectSpawner::active`].
208
    ///
209
    /// [`EffectSpawner::active`]: crate::EffectSpawner::active
210
    starts_active: bool,
211

212
    /// Whether the burst of a once-style spawner triggers immediately when the
213
    /// spawner becomes active. If `false`, the spawner doesn't do anything
214
    /// until [`EffectSpawner::reset()`] is called.
215
    starts_immediately: bool,
216
}
217

218
impl Default for Spawner {
UNCOV
219
    fn default() -> Self {
×
UNCOV
220
        Self::once(1.0f32.into(), true)
×
221
    }
222
}
223

224
impl Spawner {
225
    /// Create a spawner with a given count, time, and period.
226
    ///
227
    /// This is the _raw_ constructor. In general you should prefer using one of
228
    /// the utility constructors [`once()`], [`burst()`], or [`rate()`],
229
    /// which will ensure the control parameters are set consistently relative
230
    /// to each other.
231
    ///
232
    /// The control parameters are:
233
    ///
234
    /// - `count` is the number of particles to spawn over `time` in a burst. It
235
    ///   can generate negative or zero random values, in which case no particle
236
    ///   is spawned during the current frame.
237
    /// - `time` is how long to spawn particles for. If this is <= 0, then the
238
    ///   particles spawn all at once exactly at the same instant.
239
    /// - `period` is the amount of time between bursts of particles. If this is
240
    ///   <= `time`, then the spawner spawns a steady stream of particles. If
241
    ///   this is infinity, then there is a single burst.
242
    ///
243
    /// Note that the "burst" semantic here doesn't strictly mean a one-off
244
    /// emission, since that emission is spread over a number of simulation
245
    /// frames that total a duration of `time`. If you want a strict
246
    /// single-frame burst, simply set the `time` to zero; this is what
247
    /// [`once()`] does.
248
    ///
249
    /// # Panics
250
    ///
251
    /// Panics if `period` can be a negative number (the sample range lower
252
    /// bound is negative), or can only be 0 (the sample range upper bound is
253
    /// not strictly positive).
254
    ///
255
    /// # Example
256
    ///
257
    /// ```
258
    /// # use bevy_hanabi::Spawner;
259
    /// // Spawn 32 particles over 3 seconds, then pause for 7 seconds (10 - 3).
260
    /// let spawner = Spawner::new(32.0.into(), 3.0.into(), 10.0.into());
261
    /// ```
262
    ///
263
    /// [`once()`]: crate::Spawner::once
264
    /// [`burst()`]: crate::Spawner::burst
265
    /// [`rate()`]: crate::Spawner::rate
266
    pub fn new(count: CpuValue<f32>, time: CpuValue<f32>, period: CpuValue<f32>) -> Self {
19✔
267
        assert!(
19✔
268
            period.range()[0] >= 0.,
19✔
269
            "`period` must not generate negative numbers (period.min was {}, expected >= 0).",
1✔
270
            period.range()[0]
1✔
271
        );
272
        assert!(
18✔
273
            period.range()[1] > 0.,
18✔
274
            "`period` must be able to generate a positive number (period.max was {}, expected > 0).",
1✔
275
            period.range()[1]
1✔
276
        );
277

278
        Self {
279
            num_particles: count,
280
            spawn_time: time,
281
            period,
282
            starts_active: true,
283
            starts_immediately: true,
284
        }
285
    }
286

287
    /// Create a spawner that spawns `count` particles, then waits until reset.
288
    ///
289
    /// If `spawn_immediately` is `false`, this waits until
290
    /// [`EffectSpawner::reset()`] before spawning a burst of particles.
291
    ///
292
    /// When `spawn_immediately == true`, this is a convenience for:
293
    ///
294
    /// ```
295
    /// # use bevy_hanabi::{Spawner, CpuValue};
296
    /// # let count = CpuValue::Single(1.);
297
    /// Spawner::new(count, 0.0.into(), f32::INFINITY.into());
298
    /// ```
299
    ///
300
    /// # Example
301
    ///
302
    /// ```
303
    /// # use bevy_hanabi::Spawner;
304
    /// // Spawn 32 particles in a burst once immediately on creation.
305
    /// let spawner = Spawner::once(32.0.into(), true);
306
    /// ```
307
    ///
308
    /// [`reset()`]: crate::Spawner::reset
309
    pub fn once(count: CpuValue<f32>, spawn_immediately: bool) -> Self {
6✔
310
        let mut spawner = Self::new(count, 0.0.into(), f32::INFINITY.into());
6✔
311
        spawner.starts_immediately = spawn_immediately;
6✔
312
        spawner
6✔
313
    }
314

315
    /// Get whether this spawner emits a single burst.
316
    pub fn is_once(&self) -> bool {
11✔
317
        if let CpuValue::Single(f) = self.period {
22✔
318
            f.is_infinite()
319
        } else {
320
            false
×
321
        }
322
    }
323

324
    /// Create a spawner that spawns particles at `rate`, accumulated each
325
    /// frame. `rate` is in particles per second.
326
    ///
327
    /// This is a convenience for:
328
    ///
329
    /// ```
330
    /// # use bevy_hanabi::{Spawner, CpuValue};
331
    /// # let rate = CpuValue::Single(1.);
332
    /// Spawner::new(rate, 1.0.into(), 1.0.into());
333
    /// ```
334
    ///
335
    /// # Example
336
    ///
337
    /// ```
338
    /// # use bevy_hanabi::Spawner;
339
    /// // Spawn 10 particles per second, indefinitely.
340
    /// let spawner = Spawner::rate(10.0.into());
341
    /// ```
342
    pub fn rate(rate: CpuValue<f32>) -> Self {
9✔
343
        Self::new(rate, 1.0.into(), 1.0.into())
9✔
344
    }
345

346
    /// Create a spawner that spawns `count` particles, waits `period` seconds,
347
    /// and repeats forever.
348
    ///
349
    /// This is a convenience for:
350
    ///
351
    /// ```
352
    /// # use bevy_hanabi::{Spawner, CpuValue};
353
    /// # let count = CpuValue::Single(1.);
354
    /// # let period = CpuValue::Single(1.);
355
    /// Spawner::new(count, 0.0.into(), period);
356
    /// ```
357
    ///
358
    /// # Example
359
    ///
360
    /// ```
361
    /// # use bevy_hanabi::Spawner;
362
    /// // Spawn a burst of 5 particles every 3 seconds, indefinitely.
363
    /// let spawner = Spawner::burst(5.0.into(), 3.0.into());
364
    /// ```
365
    pub fn burst(count: CpuValue<f32>, period: CpuValue<f32>) -> Self {
1✔
366
        Self::new(count, 0.0.into(), period)
1✔
367
    }
368

369
    /// Set the number of particles that are spawned each cycle.
370
    pub fn with_count(mut self, count: CpuValue<f32>) -> Self {
×
371
        self.num_particles = count;
×
372
        self
×
373
    }
374

375
    /// Set the number of particles that are spawned each cycle.
376
    pub fn set_count(&mut self, count: CpuValue<f32>) {
×
377
        self.num_particles = count;
×
378
    }
379

380
    /// Get the number of particles that are spawned each cycle.
381
    pub fn count(&self) -> CpuValue<f32> {
×
382
        self.num_particles
×
383
    }
384

385
    /// Set the length of the spawn time each cycle.
386
    pub fn with_spawn_time(mut self, spawn_time: CpuValue<f32>) -> Self {
×
387
        self.spawn_time = spawn_time;
×
388
        self
×
389
    }
390

391
    /// Set the length of the spawn time each cycle.
392
    pub fn set_spawn_time(&mut self, spawn_time: CpuValue<f32>) {
×
393
        self.spawn_time = spawn_time;
×
394
    }
395

396
    /// Get the length of spawn time each cycle.
397
    pub fn spawn_time(&self) -> CpuValue<f32> {
×
398
        self.spawn_time
×
399
    }
400

401
    /// Set the wait time between spawn cycles.
402
    pub fn with_period(mut self, period: CpuValue<f32>) -> Self {
×
403
        self.period = period;
×
404
        self
×
405
    }
406

407
    /// Set the wait time between spawn cycles.
408
    pub fn set_period(&mut self, period: CpuValue<f32>) {
×
409
        self.period = period;
×
410
    }
411

412
    /// Get the wait time between spawn cycles
413
    pub fn period(&self) -> CpuValue<f32> {
×
414
        self.period
×
415
    }
416

417
    /// Sets whether the spawner starts active when the effect is instantiated.
418
    ///
419
    /// This value will be transfered to the active state of the
420
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
421
    /// any particle.
422
    pub fn with_starts_active(mut self, starts_active: bool) -> Self {
1✔
423
        self.starts_active = starts_active;
1✔
424
        self
1✔
425
    }
426

427
    /// Set whether the spawner starts active when the effect is instantiated.
428
    ///
429
    /// This value will be transfered to the active state of the
430
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
431
    /// any particle.
432
    pub fn set_starts_active(&mut self, starts_active: bool) {
×
433
        self.starts_active = starts_active;
×
434
    }
435

436
    /// Get whether the spawner starts active when the effect is instantiated.
437
    ///
438
    /// This value will be transfered to the active state of the
439
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
440
    /// any particle.
441
    pub fn starts_active(&self) -> bool {
11✔
442
        self.starts_active
11✔
443
    }
444
}
445

446
/// Defines how particle trails are to be constructed.
447
///
448
/// Particle trails are constructed by cloning the particles from a group into
449
/// a different group on a fixed interval. Each time the cloner ticks, it
450
/// clones all the particles from the source group into the destination group.
451
/// Hanabi then runs the initialization modifiers on the newly-cloned
452
/// particles. Particle clones that would overflow the destination group
453
/// (exceed its capacity) are dropped.
454
///
455
/// The cloner itself is embedded into the [`EffectInitializers`] component.
456
/// Once per frame the [`tick_spawners()`] system will add the component if
457
/// it's missing, copying fields from the [`Cloner`] to the [`EffectCloner`].
458
#[derive(Default, Clone, Copy, Debug, PartialEq, Reflect, Serialize, Deserialize)]
459
#[reflect(Serialize, Deserialize)]
460
pub struct Cloner {
461
    /// The group from which the cloner copies.
462
    pub src_group_index: u32,
463

464
    /// Time between clone operations, in seconds.
465
    pub period: CpuValue<f32>,
466

467
    /// Time that the particles persist, in seconds.
468
    ///
469
    /// Unlike spawned particles, cloned particles don't use the
470
    /// [`crate::attributes::Attribute::LIFETIME`] attribute and instead track
471
    /// lifetime themselves, using this value. This is because, internally,
472
    /// their lifetimes must follow last-in-first-out (LIFO) order.
473
    pub lifetime: f32,
474

475
    /// Whether the system is active at startup. The value is used to initialize
476
    /// [`EffectCloner::active`].
477
    ///
478
    /// [`EffectCloner::active`]: crate::EffectCloner::active
479
    pub starts_active: bool,
480
}
481

482
impl Cloner {
483
    /// Creates a cloner with the given source group index, period, and lifetime.
484
    ///
485
    /// This is the raw constructor. A more convenient way to create cloners is
486
    /// to use [`EffectAsset::with_trails`] or [`EffectAsset::with_ribbons`].
NEW
487
    pub fn new(src_group_index: u32, period: impl Into<CpuValue<f32>>, lifetime: f32) -> Self {
×
488
        Self {
489
            src_group_index,
NEW
490
            period: period.into(),
×
491
            lifetime,
492
            starts_active: true,
493
        }
494
    }
495

496
    /// Sets whether the cloner starts active when the effect is instantiated.
497
    ///
498
    /// This value will be transfered to the active state of the
499
    /// [`EffectCloner`] once it's instantiated. Inactive cloners do not clone
500
    /// any particle.
NEW
501
    pub fn with_starts_active(mut self, starts_active: bool) -> Self {
×
NEW
502
        self.starts_active = starts_active;
×
NEW
503
        self
×
504
    }
505

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

515
    /// Get whether the cloner starts active when the effect is instantiated.
516
    ///
517
    /// This value will be transfered to the active state of the
518
    /// [`EffectCloner`] once it's instantiated. Inactive cloners do not clone
519
    /// any particle.
NEW
520
    pub fn starts_active(&self) -> bool {
×
NEW
521
        self.starts_active
×
522
    }
523
}
524

525
/// A runtime component maintaining the state of all initializers for an effect.
526
///
527
/// This component is automatically added to the same [`Entity`] as the
528
/// [`ParticleEffect`] it's associated with, during [`tick_spawners()`], if not
529
/// already present on the entity. In that case, the initializer configurations
530
/// are cloned from the underlying [`EffectAsset`] associated with the particle
531
/// effect instance.
532
///
533
/// You can manually add this component in advance to override its [`Spawner`]s
534
/// and/or [`Cloner`]s. In that case [`tick_spawners()`] will use the existing
535
/// component you added.
536
///
537
/// Each frame, for spawners, the component will automatically calculate the
538
/// number of particles to spawn, via its internal [`Spawner`], and store it
539
/// into [`spawn_count`]. You can manually override that value if you want, to
540
/// create more complex spawning sequences. For cloners, the component sets the
541
/// [`spawn_this_frame`] flag as appropriate. You can likewise manually override
542
/// that value if you want in order to clone on different schedules.
543
///
544
/// [`spawn_count`]: crate::EffectSpawner::spawn_count
545
/// [`spawn_count`]: crate::EffectCloner::spawn_this_frame
546
#[derive(Default, Clone, Component, PartialEq, Reflect, Debug, Deref, DerefMut)]
547
#[reflect(Component)]
548
pub struct EffectInitializers(pub Vec<EffectInitializer>);
549

550
impl EffectInitializers {
551
    /// Resets the initializer state.
552
    ///
553
    /// This resets the internal time for all initializers to zero, and restarts
554
    /// any internal particle counters that they might possess.
555
    ///
556
    /// Use this, for example, to immediately spawn some particles in a spawner
557
    /// constructed with [`Spawner::once`].
558
    ///
559
    /// [`Spawner::once`]: crate::Spawner::once
NEW
560
    pub fn reset(&mut self) {
×
NEW
561
        for initializer in &mut self.0 {
×
NEW
562
            initializer.reset();
×
563
        }
564
    }
565

566
    /// Marks all initializers as either active or inactive.
567
    ///
568
    /// Inactive initializers don't spawn any particles.
NEW
569
    pub fn set_active(&mut self, active: bool) {
×
NEW
570
        for initializer in &mut self.0 {
×
NEW
571
            initializer.set_active(active);
×
572
        }
573
    }
574
}
575

576
/// Holds the runtime state for the initializer of a single particle group on a
577
/// particle effect.
578
#[derive(Clone, Copy, PartialEq, Reflect, Debug)]
579
pub enum EffectInitializer {
580
    /// The group uses a spawner.
581
    Spawner(EffectSpawner),
582
    /// The group uses a cloner (i.e. is a trail or ribbon).
583
    Cloner(EffectCloner),
584
}
585

586
impl EffectInitializer {
587
    /// If this initializer is a spawner, returns an immutable reference to it.
588
    pub fn get_spawner(&self) -> Option<&EffectSpawner> {
2✔
589
        match *self {
2✔
590
            EffectInitializer::Spawner(ref spawner) => Some(spawner),
2✔
NEW
591
            _ => None,
×
592
        }
593
    }
594

595
    /// Resets the initializer state.
596
    ///
597
    /// This resets the internal time for this initializer to zero, and
598
    /// restarts any internal particle counters that it might possess.
599
    ///
600
    /// Use this, for example, to immediately spawn some particles in a spawner
601
    /// constructed with [`Spawner::once`].
602
    ///
603
    /// [`Spawner::once`]: crate::Spawner::once
NEW
604
    pub fn reset(&mut self) {
×
NEW
605
        match self {
×
NEW
606
            EffectInitializer::Spawner(effect_spawner) => effect_spawner.reset(),
×
NEW
607
            EffectInitializer::Cloner(effect_cloner) => effect_cloner.reset(),
×
608
        }
609
    }
610

611
    /// Marks this initializer as either active or inactive.
612
    ///
613
    /// Inactive initializers don't spawn any particles.
NEW
614
    pub fn set_active(&mut self, active: bool) {
×
NEW
615
        match self {
×
NEW
616
            EffectInitializer::Spawner(effect_spawner) => effect_spawner.set_active(active),
×
NEW
617
            EffectInitializer::Cloner(effect_cloner) => effect_cloner.set_active(active),
×
618
        }
619
    }
620
}
621

622
/// Runtime structure maintaining the state of the spawner for a particle group.
623
#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
624
pub struct EffectSpawner {
625
    /// The spawner configuration extracted either from the [`EffectAsset`], or
626
    /// from any overriden value provided by the user on the [`ParticleEffect`].
627
    spawner: Spawner,
628

629
    /// Accumulated time since last spawn.
630
    time: f32,
631

632
    /// Sampled value of `spawn_time` until `limit` is reached.
633
    curr_spawn_time: f32,
634

635
    /// Time limit until next spawn.
636
    limit: f32,
637

638
    /// Number of particles to spawn this frame.
639
    ///
640
    /// This value is normally updated by calling [`tick()`], which
641
    /// automatically happens once per frame when the [`tick_spawners()`] system
642
    /// runs in the [`PostUpdate`] schedule.
643
    ///
644
    /// You can manually assign this value to override the one calculated by
645
    /// [`tick()`]. Note in this case that you need to override the value after
646
    /// the automated one was calculated, by ordering your system
647
    /// after [`tick_spawners()`] or [`EffectSystems::TickSpawners`].
648
    ///
649
    /// [`tick()`]: crate::EffectSpawner::tick
650
    /// [`EffectSystems::TickSpawners`]: crate::EffectSystems::TickSpawners
651
    pub spawn_count: u32,
652

653
    /// Fractional remainder of particle count to spawn.
654
    ///
655
    /// This will be accumulated with the new count next tick, and the integral
656
    /// part will be stored in `spawn_count`.
657
    spawn_remainder: f32,
658

659
    /// Whether the system is active. Defaults to `true`.
660
    active: bool,
661
}
662

663
impl EffectSpawner {
664
    /// Create a new spawner state from an asset definition.
665
    pub fn new(spawner: &Spawner) -> Self {
11✔
666
        Self {
667
            spawner: *spawner,
11✔
668
            time: if spawner.is_once() && !spawner.starts_immediately {
16✔
669
                1. // anything > 0
670
            } else {
671
                0.
672
            },
673
            curr_spawn_time: 0.,
674
            limit: 0.,
675
            spawn_count: 0,
676
            spawn_remainder: 0.,
677
            active: spawner.starts_active(),
11✔
678
        }
679
    }
680

681
    /// Set whether the spawner is active.
682
    ///
683
    /// Inactive spawners do not spawn any particle.
684
    pub fn with_active(mut self, active: bool) -> Self {
×
685
        self.active = active;
×
686
        self
×
687
    }
688

689
    /// Set whether the spawner is active.
690
    ///
691
    /// Inactive spawners do not spawn any particle.
692
    pub fn set_active(&mut self, active: bool) {
4✔
693
        self.active = active;
4✔
694
    }
695

696
    /// Get whether the spawner is active.
697
    ///
698
    /// Inactive spawners do not spawn any particle.
699
    pub fn is_active(&self) -> bool {
4✔
700
        self.active
4✔
701
    }
702

703
    /// Get the spawner configuration in use.
704
    ///
705
    /// The effective [`Spawner`] used is either the override specified in the
706
    /// associated [`ParticleEffect`] instance, or the fallback one specified in
707
    /// underlying [`EffectAsset`].
UNCOV
708
    pub fn spawner(&self) -> &Spawner {
×
UNCOV
709
        &self.spawner
×
710
    }
711

712
    /// Reset the spawner state.
713
    ///
714
    /// This resets the internal spawner time to zero, and restarts any internal
715
    /// particle counter.
716
    ///
717
    /// Use this, for example, to immediately spawn some particles in a spawner
718
    /// constructed with [`Spawner::once`].
719
    ///
720
    /// [`Spawner::once`]: crate::Spawner::once
721
    pub fn reset(&mut self) {
2✔
722
        self.time = 0.;
2✔
723
        self.limit = 0.;
2✔
724
        self.spawn_count = 0;
2✔
725
        self.spawn_remainder = 0.;
2✔
726
    }
727

728
    /// Tick the spawner to calculate the number of particles to spawn this
729
    /// frame.
730
    ///
731
    /// The frame delta time `dt` is added to the current spawner time, before
732
    /// the spawner calculates the number of particles to spawn.
733
    ///
734
    /// This method is called automatically by [`tick_spawners()`] during the
735
    /// [`PostUpdate`], so you normally don't have to call it yourself
736
    /// manually.
737
    ///
738
    /// # Returns
739
    ///
740
    /// The integral number of particles to spawn this frame. Any fractional
741
    /// remainder is saved for the next call.
742
    pub fn tick(&mut self, mut dt: f32, rng: &mut Pcg32) -> u32 {
35✔
743
        if !self.active {
35✔
744
            self.spawn_count = 0;
3✔
745
            return 0;
3✔
746
        }
747

748
        // The limit can be reached multiple times, so use a loop
749
        loop {
750
            if self.limit == 0.0 {
51✔
751
                self.resample(rng);
13✔
752
                continue;
13✔
753
            }
754

755
            let new_time = self.time + dt;
38✔
756
            if self.time <= self.curr_spawn_time {
38✔
757
                // If the spawn time is very small, close to zero, spawn all particles
758
                // immediately in one burst over a single frame.
759
                self.spawn_remainder += if self.curr_spawn_time < 1e-5f32.max(dt / 100.0) {
33✔
760
                    self.spawner.num_particles.sample(rng)
9✔
761
                } else {
762
                    // Spawn an amount of particles equal to the fraction of time the current frame
763
                    // spans compared to the total burst duration.
764
                    self.spawner.num_particles.sample(rng)
24✔
765
                        * (new_time.min(self.curr_spawn_time) - self.time)
24✔
766
                        / self.curr_spawn_time
24✔
767
                };
768
            }
769

770
            let old_time = self.time;
771
            self.time = new_time;
772

773
            if self.time >= self.limit {
6✔
774
                dt -= self.limit - old_time;
6✔
775
                self.time = 0.0; // dt will be added on in the next iteration
6✔
776
                self.resample(rng);
6✔
777
            } else {
778
                break;
32✔
779
            }
780
        }
781

782
        let count = self.spawn_remainder.floor();
32✔
783
        self.spawn_remainder -= count;
32✔
784
        self.spawn_count = count as u32;
32✔
785

786
        self.spawn_count
32✔
787
    }
788

789
    /// Resamples the spawn time and period.
790
    fn resample(&mut self, rng: &mut Pcg32) {
19✔
791
        self.limit = self.spawner.period.sample(rng);
19✔
792
        self.curr_spawn_time = self.spawner.spawn_time.sample(rng).clamp(0.0, self.limit);
19✔
793
    }
794
}
795

796
/// A runtime structure maintaining the state of the cloner for a particle
797
/// group.
798
#[derive(Default, Clone, Copy, PartialEq, Reflect, Debug)]
799
pub struct EffectCloner {
800
    /// The cloner configuration extracted either from the [`EffectAsset`] or
801
    /// overridden manually.
802
    pub cloner: Cloner,
803
    /// Accumulated time since last spawn.
804
    time: f32,
805
    /// Time limit until next spawn.
806
    limit: f32,
807
    /// The capacity of the group.
808
    capacity: u32,
809
    /// Whether the cloner is to spawn this frame.
810
    pub spawn_this_frame: bool,
811
    /// Whether the cloner is active. Defaults to true.
812
    pub active: bool,
813
}
814

815
impl EffectCloner {
NEW
816
    pub(crate) fn new(cloner: Cloner, capacity: u32) -> EffectCloner {
×
817
        EffectCloner {
818
            cloner,
819
            time: 0.0,
820
            limit: 0.0,
821
            capacity,
822
            spawn_this_frame: false,
NEW
823
            active: cloner.starts_active(),
×
824
        }
825
    }
826

827
    /// Reset the spawner state.
828
    ///
829
    /// This resets the internal spawner time to zero, and restarts any internal
830
    /// particle counter.
831
    ///
832
    /// [`Spawner::once`]: crate::Spawner::once
NEW
833
    pub fn reset(&mut self) {
×
NEW
834
        self.time = 0.0;
×
NEW
835
        self.limit = 0.0;
×
836
    }
837

838
    /// Returns true if we should spawn this frame.
NEW
839
    pub fn tick(&mut self, dt: f32, rng: &mut Pcg32) {
×
NEW
840
        if !self.active {
×
NEW
841
            self.spawn_this_frame = false;
×
NEW
842
            return;
×
843
        }
844

NEW
845
        if self.limit == 0.0 {
×
NEW
846
            self.resample(rng);
×
847
        }
848

NEW
849
        let new_time = self.time + dt;
×
NEW
850
        self.time = new_time;
×
851

NEW
852
        self.spawn_this_frame = self.time >= self.limit;
×
853

NEW
854
        if self.spawn_this_frame {
×
NEW
855
            self.time = 0.0;
×
NEW
856
            self.resample(rng);
×
857
        }
858
    }
859

NEW
860
    fn resample(&mut self, rng: &mut Pcg32) {
×
NEW
861
        self.limit = self.cloner.period.sample(rng);
×
862
    }
863

864
    /// Marks this cloner as either active or inactive.
865
    ///
866
    /// Inactive cloners don't clone any particles.
NEW
867
    pub fn set_active(&mut self, active: bool) {
×
NEW
868
        self.active = active;
×
869
    }
870
}
871

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

908
    let dt = time.delta_seconds();
13✔
909

910
    for (entity, effect, maybe_inherited_visibility, maybe_initializers) in query.iter_mut() {
13✔
911
        // TODO - maybe cache simulation_condition so we don't need to unconditionally
912
        // query the asset?
913
        let Some(asset) = effects.get(&effect.handle) else {
16✔
914
            continue;
10✔
915
        };
916

917
        if asset.simulation_condition == SimulationCondition::WhenVisible
918
            && !maybe_inherited_visibility
2✔
919
                .map(|iv| iv.get())
6✔
920
                .unwrap_or(true)
2✔
921
        {
922
            continue;
1✔
923
        }
924

NEW
925
        if let Some(mut initializers) = maybe_initializers {
×
NEW
926
            for initializer in &mut **initializers {
×
NEW
927
                match initializer {
×
NEW
928
                    EffectInitializer::Spawner(effect_spawner) => {
×
NEW
929
                        effect_spawner.tick(dt, &mut rng.0);
×
930
                    }
NEW
931
                    EffectInitializer::Cloner(effect_cloner) => {
×
NEW
932
                        effect_cloner.tick(dt, &mut rng.0);
×
933
                    }
934
                }
935
            }
NEW
936
            continue;
×
937
        }
938

939
        let initializers = asset
2✔
940
            .init
2✔
941
            .iter()
942
            .enumerate()
943
            .map(|(group_index, init)| match *init {
4✔
944
                Initializer::Spawner(spawner) => {
2✔
945
                    let mut effect_spawner = EffectSpawner::new(&spawner);
2✔
946
                    effect_spawner.tick(dt, &mut rng.0);
2✔
947
                    EffectInitializer::Spawner(effect_spawner)
2✔
948
                }
NEW
949
                Initializer::Cloner(cloner) => {
×
NEW
950
                    let mut effect_cloner =
×
NEW
951
                        EffectCloner::new(cloner, asset.capacities()[group_index]);
×
NEW
952
                    effect_cloner.tick(dt, &mut rng.0);
×
NEW
953
                    EffectInitializer::Cloner(effect_cloner)
×
954
                }
955
            })
956
            .collect();
957
        commands
958
            .entity(entity)
959
            .insert(EffectInitializers(initializers));
960
    }
961
}
962

963
#[cfg(test)]
964
mod test {
965
    use std::time::Duration;
966

967
    use bevy::{
968
        asset::{
969
            io::{
970
                memory::{Dir, MemoryAssetReader},
971
                AssetSourceBuilder, AssetSourceBuilders, AssetSourceId,
972
            },
973
            AssetServerMode,
974
        },
975
        render::view::{VisibilityPlugin, VisibilitySystems},
976
        tasks::{IoTaskPool, TaskPoolBuilder},
977
    };
978

979
    use super::*;
980
    use crate::Module;
981

982
    /// Make an `EffectSpawner` wrapping a `Spawner`.
983
    fn make_effect_spawner(spawner: Spawner) -> EffectSpawner {
984
        EffectSpawner::new(
985
            EffectAsset::new(256, spawner, Module::default()).init[0]
986
                .get_spawner()
987
                .expect("Expected the first group to have a spawner"),
988
        )
989
    }
990

991
    #[test]
992
    fn test_range_single() {
993
        let value = CpuValue::Single(1.0);
994
        assert_eq!(value.range(), [1.0, 1.0]);
995
    }
996

997
    #[test]
998
    fn test_range_uniform() {
999
        let value = CpuValue::Uniform((1.0, 3.0));
1000
        assert_eq!(value.range(), [1.0, 3.0]);
1001
    }
1002

1003
    #[test]
1004
    fn test_range_uniform_reverse() {
1005
        let value = CpuValue::Uniform((3.0, 1.0));
1006
        assert_eq!(value.range(), [1.0, 3.0]);
1007
    }
1008

1009
    #[test]
1010
    fn test_new() {
1011
        let rng = &mut new_rng();
1012
        // 3 particles over 3 seconds, pause 7 seconds (total 10 seconds period).
1013
        let spawner = Spawner::new(3.0.into(), 3.0.into(), 10.0.into());
1014
        let mut spawner = make_effect_spawner(spawner);
1015
        let count = spawner.tick(2.0, rng); // t = 2s
1016
        assert_eq!(count, 2);
1017
        let count = spawner.tick(5.0, rng); // t = 7s
1018
        assert_eq!(count, 1);
1019
        let count = spawner.tick(8.0, rng); // t = 15s
1020
        assert_eq!(count, 3);
1021
    }
1022

1023
    #[test]
1024
    #[should_panic]
1025
    fn test_new_panic_negative_period() {
1026
        let _ = Spawner::new(3.0.into(), 1.0.into(), CpuValue::Uniform((-1., 1.)));
1027
    }
1028

1029
    #[test]
1030
    #[should_panic]
1031
    fn test_new_panic_zero_period() {
1032
        let _ = Spawner::new(3.0.into(), 1.0.into(), CpuValue::Uniform((0., 0.)));
1033
    }
1034

1035
    #[test]
1036
    fn test_once() {
1037
        let rng = &mut new_rng();
1038
        let spawner = Spawner::once(5.0.into(), true);
1039
        let mut spawner = make_effect_spawner(spawner);
1040
        let count = spawner.tick(0.001, rng);
1041
        assert_eq!(count, 5);
1042
        let count = spawner.tick(100.0, rng);
1043
        assert_eq!(count, 0);
1044
    }
1045

1046
    #[test]
1047
    fn test_once_reset() {
1048
        let rng = &mut new_rng();
1049
        let spawner = Spawner::once(5.0.into(), true);
1050
        let mut spawner = make_effect_spawner(spawner);
1051
        spawner.tick(1.0, rng);
1052
        spawner.reset();
1053
        let count = spawner.tick(1.0, rng);
1054
        assert_eq!(count, 5);
1055
    }
1056

1057
    #[test]
1058
    fn test_once_not_immediate() {
1059
        let rng = &mut new_rng();
1060
        let spawner = Spawner::once(5.0.into(), false);
1061
        let mut spawner = make_effect_spawner(spawner);
1062
        let count = spawner.tick(1.0, rng);
1063
        assert_eq!(count, 0);
1064
        spawner.reset();
1065
        let count = spawner.tick(1.0, rng);
1066
        assert_eq!(count, 5);
1067
    }
1068

1069
    #[test]
1070
    fn test_rate() {
1071
        let rng = &mut new_rng();
1072
        let spawner = Spawner::rate(5.0.into());
1073
        let mut spawner = make_effect_spawner(spawner);
1074
        // Slightly over 1.0 to avoid edge case
1075
        let count = spawner.tick(1.01, rng);
1076
        assert_eq!(count, 5);
1077
        let count = spawner.tick(0.4, rng);
1078
        assert_eq!(count, 2);
1079
    }
1080

1081
    #[test]
1082
    fn test_rate_active() {
1083
        let rng = &mut new_rng();
1084
        let spawner = Spawner::rate(5.0.into());
1085
        let mut spawner = make_effect_spawner(spawner);
1086
        spawner.tick(1.01, rng);
1087
        spawner.set_active(false);
1088
        assert!(!spawner.is_active());
1089
        let count = spawner.tick(0.4, rng);
1090
        assert_eq!(count, 0);
1091
        spawner.set_active(true);
1092
        assert!(spawner.is_active());
1093
        let count = spawner.tick(0.4, rng);
1094
        assert_eq!(count, 2);
1095
    }
1096

1097
    #[test]
1098
    fn test_rate_accumulate() {
1099
        let rng = &mut new_rng();
1100
        let spawner = Spawner::rate(5.0.into());
1101
        let mut spawner = make_effect_spawner(spawner);
1102
        // 13 ticks instead of 12 to avoid edge case
1103
        let count = (0..13).map(|_| spawner.tick(1.0 / 60.0, rng)).sum::<u32>();
1104
        assert_eq!(count, 1);
1105
    }
1106

1107
    #[test]
1108
    fn test_burst() {
1109
        let rng = &mut new_rng();
1110
        let spawner = Spawner::burst(5.0.into(), 2.0.into());
1111
        let mut spawner = make_effect_spawner(spawner);
1112
        let count = spawner.tick(1.0, rng);
1113
        assert_eq!(count, 5);
1114
        let count = spawner.tick(4.0, rng);
1115
        assert_eq!(count, 10);
1116
        let count = spawner.tick(0.1, rng);
1117
        assert_eq!(count, 0);
1118
    }
1119

1120
    #[test]
1121
    fn test_with_active() {
1122
        let rng = &mut new_rng();
1123
        let spawner = Spawner::rate(5.0.into()).with_starts_active(false);
1124
        let mut spawner = make_effect_spawner(spawner);
1125
        assert!(!spawner.is_active());
1126
        let count = spawner.tick(1., rng);
1127
        assert_eq!(count, 0);
1128
        spawner.set_active(false); // no-op
1129
        let count = spawner.tick(1., rng);
1130
        assert_eq!(count, 0);
1131
        spawner.set_active(true);
1132
        assert!(spawner.is_active());
1133
        let count = spawner.tick(1., rng);
1134
        assert_eq!(count, 5);
1135
    }
1136

1137
    fn make_test_app() -> App {
1138
        IoTaskPool::get_or_init(|| {
1139
            TaskPoolBuilder::default()
1140
                .num_threads(1)
1141
                .thread_name("Hanabi test IO Task Pool".to_string())
1142
                .build()
1143
        });
1144

1145
        let mut app = App::new();
1146

1147
        let watch_for_changes = false;
1148
        let mut builders = app
1149
            .world_mut()
1150
            .get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
1151
        let dir = Dir::default();
1152
        let dummy_builder = AssetSourceBuilder::default()
1153
            .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }));
1154
        builders.insert(AssetSourceId::Default, dummy_builder);
1155
        let sources = builders.build_sources(watch_for_changes, false);
1156
        let asset_server =
1157
            AssetServer::new(sources, AssetServerMode::Unprocessed, watch_for_changes);
1158

1159
        app.insert_resource(asset_server);
1160
        // app.add_plugins(DefaultPlugins);
1161
        app.init_asset::<Mesh>();
1162
        app.add_plugins(VisibilityPlugin);
1163
        app.init_resource::<Time<EffectSimulation>>();
1164
        app.insert_resource(Random(new_rng()));
1165
        app.init_asset::<EffectAsset>();
1166
        app.add_systems(
1167
            PostUpdate,
1168
            tick_spawners.after(VisibilitySystems::CheckVisibility),
1169
        );
1170

1171
        app
1172
    }
1173

1174
    /// Test case for `tick_spawners()`.
1175
    struct TestCase {
1176
        /// Initial entity visibility on spawn. If `None`, do not add a
1177
        /// [`Visibility`] component.
1178
        visibility: Option<Visibility>,
1179

1180
        /// Spawner assigned to the `EffectAsset`.
1181
        asset_spawner: Spawner,
1182
    }
1183

1184
    impl TestCase {
1185
        fn new(visibility: Option<Visibility>, asset_spawner: Spawner) -> Self {
1186
            Self {
1187
                visibility,
1188
                asset_spawner,
1189
            }
1190
        }
1191
    }
1192

1193
    #[test]
1194
    fn test_tick_spawners() {
1195
        let asset_spawner = Spawner::once(32.0.into(), true);
1196

1197
        for test_case in &[
1198
            TestCase::new(None, asset_spawner),
1199
            TestCase::new(Some(Visibility::Hidden), asset_spawner),
1200
            TestCase::new(Some(Visibility::Visible), asset_spawner),
1201
        ] {
1202
            let mut app = make_test_app();
1203

1204
            let (effect_entity, handle) = {
1205
                let world = app.world_mut();
1206

1207
                // Add effect asset
1208
                let mut assets = world.resource_mut::<Assets<EffectAsset>>();
1209
                let mut asset = EffectAsset::new(64, test_case.asset_spawner, Module::default());
1210
                asset.simulation_condition = if test_case.visibility.is_some() {
1211
                    SimulationCondition::WhenVisible
1212
                } else {
1213
                    SimulationCondition::Always
1214
                };
1215
                let handle = assets.add(asset);
1216

1217
                // Spawn particle effect
1218
                let entity = if let Some(visibility) = test_case.visibility {
1219
                    world
1220
                        .spawn((
1221
                            visibility,
1222
                            InheritedVisibility::default(),
1223
                            ParticleEffect {
1224
                                handle: handle.clone(),
1225
                                #[cfg(feature = "2d")]
1226
                                z_layer_2d: None,
1227
                            },
1228
                        ))
1229
                        .id()
1230
                } else {
1231
                    world
1232
                        .spawn((ParticleEffect {
1233
                            handle: handle.clone(),
1234
                            #[cfg(feature = "2d")]
1235
                            z_layer_2d: None,
1236
                        },))
1237
                        .id()
1238
                };
1239

1240
                // Spawn a camera, otherwise ComputedVisibility stays at HIDDEN
1241
                world.spawn(Camera3dBundle::default());
1242

1243
                (entity, handle)
1244
            };
1245

1246
            // Tick once
1247
            let cur_time = {
1248
                // Make sure to increment the current time so that the spawners spawn something.
1249
                // Note that `Time` has this weird behavior where the common quantities like
1250
                // `Time::delta_seconds()` only update after the *second* update. So we tick the
1251
                // `Time` twice here to enforce this.
1252
                let mut time = app.world_mut().resource_mut::<Time<EffectSimulation>>();
1253
                time.advance_by(Duration::from_millis(16));
1254
                time.elapsed()
1255
            };
1256
            app.update();
1257

1258
            let world = app.world_mut();
1259

1260
            // Check the state of the components after `tick_spawners()` ran
1261
            if let Some(test_visibility) = test_case.visibility {
1262
                // Simulated-when-visible effect (SimulationCondition::WhenVisible)
1263

1264
                let (entity, visibility, inherited_visibility, particle_effect, effect_spawners) =
1265
                    world
1266
                        .query::<(
1267
                            Entity,
1268
                            &Visibility,
1269
                            &InheritedVisibility,
1270
                            &ParticleEffect,
1271
                            Option<&EffectInitializers>,
1272
                        )>()
1273
                        .iter(world)
1274
                        .next()
1275
                        .unwrap();
1276
                assert_eq!(entity, effect_entity);
1277
                assert_eq!(visibility, test_visibility);
1278
                assert_eq!(
1279
                    inherited_visibility.get(),
1280
                    test_visibility == Visibility::Visible
1281
                );
1282
                assert_eq!(particle_effect.handle, handle);
1283
                if inherited_visibility.get() {
1284
                    // If visible, `tick_spawners()` spawns the EffectSpawner and ticks it
1285
                    assert!(effect_spawners.is_some());
1286
                    let effect_spawner = effect_spawners.unwrap()[0].get_spawner().unwrap();
1287
                    let actual_spawner = effect_spawner.spawner;
1288

1289
                    // Check the spawner ticked
1290
                    assert!(effect_spawner.active);
1291
                    assert_eq!(effect_spawner.spawn_remainder, 0.);
1292
                    assert_eq!(effect_spawner.time, cur_time.as_secs_f32());
1293

1294
                    assert_eq!(actual_spawner, test_case.asset_spawner);
1295
                    assert_eq!(effect_spawner.spawn_count, 32);
1296
                } else {
1297
                    // If not visible, `tick_spawners()` skips the effect entirely so won't spawn an
1298
                    // `EffectSpawner` for it
1299
                    assert!(effect_spawners.is_none());
1300
                }
1301
            } else {
1302
                // Always-simulated effect (SimulationCondition::Always)
1303

1304
                let (entity, particle_effect, effect_spawners) = world
1305
                    .query::<(Entity, &ParticleEffect, Option<&EffectInitializers>)>()
1306
                    .iter(world)
1307
                    .next()
1308
                    .unwrap();
1309
                assert_eq!(entity, effect_entity);
1310
                assert_eq!(particle_effect.handle, handle);
1311

1312
                assert!(effect_spawners.is_some());
1313
                let effect_spawner = effect_spawners.unwrap()[0].get_spawner().unwrap();
1314
                let actual_spawner = effect_spawner.spawner;
1315

1316
                // Check the spawner ticked
1317
                assert!(effect_spawner.active);
1318
                assert_eq!(effect_spawner.spawn_remainder, 0.);
1319
                assert_eq!(effect_spawner.time, cur_time.as_secs_f32());
1320

1321
                assert_eq!(actual_spawner, test_case.asset_spawner);
1322
                assert_eq!(effect_spawner.spawn_count, 32);
1323
            }
1324
        }
1325
    }
1326
}
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