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

djeedai / bevy_hanabi / 11590789648

30 Oct 2024 09:55AM UTC coverage: 57.86% (+0.01%) from 57.849%
11590789648

push

github

web-flow
Rename `tick_{spawners:initializers}()` system for clarity (#394)

18 of 35 new or added lines in 3 files covered. (51.43%)

3 existing lines in 1 file now uncovered.

3537 of 6113 relevant lines covered (57.86%)

23.02 hits per line

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

48.67
/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]
162
    fn from(value: Cloner) -> Self {
×
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✔
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`]. The resulting
190
/// number of particles to spawn for the frame is then stored into
191
/// [`EffectSpawner::spawn_count`]. You can override that value to manually
192
/// control each frame how many particles are spawned.
193
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
194
#[reflect(Default)]
195
pub struct Spawner {
196
    /// Number of particles to spawn over [`spawn_duration`].
197
    ///
198
    /// [`spawn_duration`]: Spawner::spawn_duration
199
    count: CpuValue<f32>,
200

201
    /// Time over which to spawn [`count`], in seconds.
202
    ///
203
    /// [`count`]: Spawner::count
204
    spawn_duration: CpuValue<f32>,
205

206
    /// Time between bursts of the particle system, in seconds.
207
    ///
208
    /// If this is infinity, there's only one burst.
209
    /// If this is [`spawn_duration`] or less, the system spawns a steady stream
210
    /// of particles.
211
    ///
212
    /// [`spawn_duration`]: Spawner::spawn_duration
213
    period: CpuValue<f32>,
214

215
    /// Whether the spawner is active at startup.
216
    ///
217
    /// The value is used to initialize [`EffectSpawner::active`].
218
    ///
219
    /// [`EffectSpawner::active`]: crate::EffectSpawner::active
220
    starts_active: bool,
221

222
    /// Whether the burst of a once-style spawner triggers immediately when the
223
    /// spawner becomes active.
224
    ///
225
    /// If `false`, the spawner doesn't do anything until
226
    /// [`EffectSpawner::reset()`] is called.
227
    starts_immediately: bool,
228
}
229

230
impl Default for Spawner {
231
    fn default() -> Self {
×
232
        Self::once(1.0f32.into(), true)
×
233
    }
234
}
235

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

302
        Self {
303
            count,
304
            spawn_duration,
305
            period,
306
            starts_active: true,
307
            starts_immediately: true,
308
        }
309
    }
310

311
    /// Create a spawner that spawns a burst of particles once.
312
    ///
313
    /// The burst of particles is spawned all at once in the same frame. After
314
    /// that, the spawner idles, waiting to be manually reset via
315
    /// [`EffectSpawner::reset()`].
316
    ///
317
    /// If `spawn_immediately` is `false`, this waits until
318
    /// [`EffectSpawner::reset()`] before spawning a burst of particles.
319
    ///
320
    /// When `spawn_immediately == true`, this spawns a burst immediately on
321
    /// activation. In that case, this is a convenience for:
322
    ///
323
    /// ```
324
    /// # use bevy_hanabi::{Spawner, CpuValue};
325
    /// # let count = CpuValue::Single(1.);
326
    /// Spawner::new(count, 0.0.into(), f32::INFINITY.into());
327
    /// ```
328
    ///
329
    /// # Example
330
    ///
331
    /// ```
332
    /// # use bevy_hanabi::Spawner;
333
    /// // Spawn 32 particles in a burst once immediately on creation.
334
    /// let spawner = Spawner::once(32.0.into(), true);
335
    /// ```
336
    ///
337
    /// [`reset()`]: crate::Spawner::reset
338
    pub fn once(count: CpuValue<f32>, spawn_immediately: bool) -> Self {
6✔
339
        let mut spawner = Self::new(count, 0.0.into(), f32::INFINITY.into());
6✔
340
        spawner.starts_immediately = spawn_immediately;
6✔
341
        spawner
6✔
342
    }
343

344
    /// Get whether this spawner emits a single burst.
345
    pub fn is_once(&self) -> bool {
11✔
346
        if let CpuValue::Single(f) = self.period {
22✔
347
            f.is_infinite()
348
        } else {
349
            false
×
350
        }
351
    }
352

353
    /// Create a spawner that spawns particles at `rate`, accumulated each
354
    /// frame. `rate` is in particles per second.
355
    ///
356
    /// This is a convenience for:
357
    ///
358
    /// ```
359
    /// # use bevy_hanabi::{Spawner, CpuValue};
360
    /// # let rate = CpuValue::Single(1.);
361
    /// Spawner::new(rate, 1.0.into(), 1.0.into());
362
    /// ```
363
    ///
364
    /// # Example
365
    ///
366
    /// ```
367
    /// # use bevy_hanabi::Spawner;
368
    /// // Spawn 10 particles per second, indefinitely.
369
    /// let spawner = Spawner::rate(10.0.into());
370
    /// ```
371
    pub fn rate(rate: CpuValue<f32>) -> Self {
9✔
372
        Self::new(rate, 1.0.into(), 1.0.into())
9✔
373
    }
374

375
    /// Create a spawner that spawns `count` particles, waits `period` seconds,
376
    /// and repeats forever.
377
    ///
378
    /// This is a convenience for:
379
    ///
380
    /// ```
381
    /// # use bevy_hanabi::{Spawner, CpuValue};
382
    /// # let count = CpuValue::Single(1.);
383
    /// # let period = CpuValue::Single(1.);
384
    /// Spawner::new(count, 0.0.into(), period);
385
    /// ```
386
    ///
387
    /// # Example
388
    ///
389
    /// ```
390
    /// # use bevy_hanabi::Spawner;
391
    /// // Spawn a burst of 5 particles every 3 seconds, indefinitely.
392
    /// let spawner = Spawner::burst(5.0.into(), 3.0.into());
393
    /// ```
394
    pub fn burst(count: CpuValue<f32>, period: CpuValue<f32>) -> Self {
1✔
395
        Self::new(count, 0.0.into(), period)
1✔
396
    }
397

398
    /// Set the number of particles that are spawned each cycle.
399
    pub fn with_count(mut self, count: CpuValue<f32>) -> Self {
×
NEW
400
        self.count = count;
×
401
        self
×
402
    }
403

404
    /// Set the number of particles that are spawned each cycle.
405
    pub fn set_count(&mut self, count: CpuValue<f32>) {
×
NEW
406
        self.count = count;
×
407
    }
408

409
    /// Get the number of particles that are spawned each cycle.
410
    pub fn count(&self) -> CpuValue<f32> {
×
NEW
411
        self.count
×
412
    }
413

414
    /// Set the duration, in seconds, of the spawn time each cycle.
NEW
415
    pub fn with_spawn_time(mut self, spawn_duration: CpuValue<f32>) -> Self {
×
NEW
416
        self.spawn_duration = spawn_duration;
×
UNCOV
417
        self
×
418
    }
419

420
    /// Set the duration, in seconds, of the spawn time each cycle.
NEW
421
    pub fn set_spawn_time(&mut self, spawn_duration: CpuValue<f32>) {
×
NEW
422
        self.spawn_duration = spawn_duration;
×
423
    }
424

425
    /// Get the duration, in seconds, of spawn time each cycle.
NEW
426
    pub fn spawn_duration(&self) -> CpuValue<f32> {
×
NEW
427
        self.spawn_duration
×
428
    }
429

430
    /// Set the duration of a spawn cycle, in seconds.
431
    ///
432
    /// A spawn cycles includes the [`spawn_duration()`] value, and any extra
433
    /// wait time (if larger than spawn time).
434
    pub fn with_period(mut self, period: CpuValue<f32>) -> Self {
×
435
        self.period = period;
×
436
        self
×
437
    }
438

439
    /// Set the duration of the spawn cycle, in seconds.
440
    ///
441
    /// A spawn cycles includes the [`spawn_duration()`] value, and any extra
442
    /// wait time (if larger than spawn time).
443
    pub fn set_period(&mut self, period: CpuValue<f32>) {
×
444
        self.period = period;
×
445
    }
446

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

455
    /// Sets whether the spawner starts active when the effect is instantiated.
456
    ///
457
    /// This value will be transfered to the active state of the
458
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
459
    /// any particle.
460
    pub fn with_starts_active(mut self, starts_active: bool) -> Self {
1✔
461
        self.starts_active = starts_active;
1✔
462
        self
1✔
463
    }
464

465
    /// Set whether the spawner starts active when the effect is instantiated.
466
    ///
467
    /// This value will be transfered to the active state of the
468
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
469
    /// any particle.
470
    pub fn set_starts_active(&mut self, starts_active: bool) {
×
471
        self.starts_active = starts_active;
×
472
    }
473

474
    /// Get whether the spawner starts active when the effect is instantiated.
475
    ///
476
    /// This value will be transfered to the active state of the
477
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
478
    /// any particle.
479
    pub fn starts_active(&self) -> bool {
11✔
480
        self.starts_active
11✔
481
    }
482
}
483

484
/// Defines how particle trails are to be constructed.
485
///
486
/// Particle trails are constructed by cloning the particles from a group into
487
/// a different group on a fixed interval. Each time the cloner ticks, it
488
/// clones all the particles from the source group into the destination group.
489
/// Hanabi then runs the initialization modifiers on the newly-cloned
490
/// particles. Particle clones that would overflow the destination group
491
/// (exceed its capacity) are dropped.
492
///
493
/// The cloner itself is embedded into the [`EffectInitializers`] component.
494
/// Once per frame the [`tick_spawners()`] system will add the component if
495
/// it's missing, copying fields from the [`Cloner`] to the [`EffectCloner`].
496
#[derive(Default, Clone, Copy, Debug, PartialEq, Reflect, Serialize, Deserialize)]
497
#[reflect(Serialize, Deserialize)]
498
pub struct Cloner {
499
    /// The group from which the cloner copies.
500
    pub src_group_index: u32,
501

502
    /// Time between clone operations, in seconds.
503
    pub period: CpuValue<f32>,
504

505
    /// Time that the particles persist, in seconds.
506
    ///
507
    /// Unlike spawned particles, cloned particles don't use the
508
    /// [`crate::attributes::Attribute::LIFETIME`] attribute and instead track
509
    /// lifetime themselves, using this value. This is because, internally,
510
    /// their lifetimes must follow last-in-first-out (LIFO) order.
511
    pub lifetime: f32,
512

513
    /// Whether the system is active at startup. The value is used to initialize
514
    /// [`EffectCloner::active`].
515
    ///
516
    /// [`EffectCloner::active`]: crate::EffectCloner::active
517
    pub starts_active: bool,
518
}
519

520
impl Cloner {
521
    /// Creates a cloner with the given source group index, period, and
522
    /// lifetime.
523
    ///
524
    /// This is the raw constructor. A more convenient way to create cloners is
525
    /// to use [`EffectAsset::with_trails`] or [`EffectAsset::with_ribbons`].
526
    pub fn new(src_group_index: u32, period: impl Into<CpuValue<f32>>, lifetime: f32) -> Self {
×
527
        Self {
528
            src_group_index,
529
            period: period.into(),
×
530
            lifetime,
531
            starts_active: true,
532
        }
533
    }
534

535
    /// Sets whether the cloner starts active when the effect is instantiated.
536
    ///
537
    /// This value will be transfered to the active state of the
538
    /// [`EffectCloner`] once it's instantiated. Inactive cloners do not clone
539
    /// any particle.
540
    pub fn with_starts_active(mut self, starts_active: bool) -> Self {
×
541
        self.starts_active = starts_active;
×
542
        self
×
543
    }
544

545
    /// Set whether the cloner starts active when the effect is instantiated.
546
    ///
547
    /// This value will be transfered to the active state of the
548
    /// [`EffectCloner`] once it's instantiated. Inactive cloners do not clone
549
    /// any particle.
550
    pub fn set_starts_active(&mut self, starts_active: bool) {
×
551
        self.starts_active = starts_active;
×
552
    }
553

554
    /// Get whether the cloner starts active when the effect is instantiated.
555
    ///
556
    /// This value will be transfered to the active state of the
557
    /// [`EffectCloner`] once it's instantiated. Inactive cloners do not clone
558
    /// any particle.
559
    pub fn starts_active(&self) -> bool {
×
560
        self.starts_active
×
561
    }
562
}
563

564
/// A runtime component maintaining the state of all initializers for an effect.
565
///
566
/// This component is automatically added to the same [`Entity`] as the
567
/// [`ParticleEffect`] it's associated with, during [`tick_spawners()`], if not
568
/// already present on the entity. In that case, the initializer configurations
569
/// are cloned from the underlying [`EffectAsset`] associated with the particle
570
/// effect instance.
571
///
572
/// You can manually add this component in advance to override its [`Spawner`]s
573
/// and/or [`Cloner`]s. In that case [`tick_spawners()`] will use the existing
574
/// component you added.
575
///
576
/// Each frame, for spawners, the component will automatically calculate the
577
/// number of particles to spawn, via its internal [`Spawner`], and store it
578
/// into [`spawn_count`]. You can manually override that value if you want, to
579
/// create more complex spawning sequences. For cloners, the component sets the
580
/// [`spawn_this_frame`] flag as appropriate. You can likewise manually override
581
/// that value if you want in order to clone on different schedules.
582
///
583
/// [`spawn_count`]: crate::EffectSpawner::spawn_count
584
/// [`spawn_count`]: crate::EffectCloner::spawn_this_frame
585
#[derive(Default, Clone, Component, PartialEq, Reflect, Debug, Deref, DerefMut)]
586
#[reflect(Component)]
587
pub struct EffectInitializers(pub Vec<EffectInitializer>);
588

589
impl EffectInitializers {
590
    /// Resets the initializer state.
591
    ///
592
    /// This resets the internal time for all initializers to zero, and restarts
593
    /// any internal particle counters that they might possess.
594
    ///
595
    /// Use this, for example, to immediately spawn some particles in a spawner
596
    /// constructed with [`Spawner::once`].
597
    ///
598
    /// [`Spawner::once`]: crate::Spawner::once
599
    pub fn reset(&mut self) {
×
600
        for initializer in &mut self.0 {
×
601
            initializer.reset();
×
602
        }
603
    }
604

605
    /// Marks all initializers as either active or inactive.
606
    ///
607
    /// Inactive initializers don't spawn any particles.
608
    pub fn set_active(&mut self, active: bool) {
×
609
        for initializer in &mut self.0 {
×
610
            initializer.set_active(active);
×
611
        }
612
    }
613
}
614

615
/// Holds the runtime state for the initializer of a single particle group on a
616
/// particle effect.
617
#[derive(Clone, Copy, PartialEq, Reflect, Debug)]
618
pub enum EffectInitializer {
619
    /// The group uses a spawner.
620
    Spawner(EffectSpawner),
621
    /// The group uses a cloner (i.e. is a trail or ribbon).
622
    Cloner(EffectCloner),
623
}
624

625
impl EffectInitializer {
626
    /// If this initializer is a spawner, returns an immutable reference to it.
627
    pub fn get_spawner(&self) -> Option<&EffectSpawner> {
2✔
628
        match *self {
2✔
629
            EffectInitializer::Spawner(ref spawner) => Some(spawner),
2✔
630
            _ => None,
×
631
        }
632
    }
633

634
    /// Resets the initializer state.
635
    ///
636
    /// This resets the internal time for this initializer to zero, and
637
    /// restarts any internal particle counters that it might possess.
638
    ///
639
    /// Use this, for example, to immediately spawn some particles in a spawner
640
    /// constructed with [`Spawner::once`].
641
    ///
642
    /// [`Spawner::once`]: crate::Spawner::once
643
    pub fn reset(&mut self) {
×
644
        match self {
×
645
            EffectInitializer::Spawner(effect_spawner) => effect_spawner.reset(),
×
646
            EffectInitializer::Cloner(effect_cloner) => effect_cloner.reset(),
×
647
        }
648
    }
649

650
    /// Marks this initializer as either active or inactive.
651
    ///
652
    /// Inactive initializers don't spawn any particles.
653
    pub fn set_active(&mut self, active: bool) {
×
654
        match self {
×
655
            EffectInitializer::Spawner(effect_spawner) => effect_spawner.set_active(active),
×
656
            EffectInitializer::Cloner(effect_cloner) => effect_cloner.set_active(active),
×
657
        }
658
    }
659
}
660

661
/// Runtime structure maintaining the state of the spawner for a particle group.
662
#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
663
pub struct EffectSpawner {
664
    /// The spawner configuration extracted either from the [`EffectAsset`], or
665
    /// from any overriden value provided by the user on the [`ParticleEffect`].
666
    spawner: Spawner,
667

668
    /// Accumulated time since last spawn, in seconds.
669
    time: f32,
670

671
    /// Sampled value of `spawn_duration` until `period` is reached. This is the
672
    /// duration of the "active" period during which we spawn particles, as
673
    /// opposed to the "wait" period during which we do nothing until the next
674
    /// spawn cycle.
675
    spawn_duration: f32,
676

677
    /// Sampled value of the time period, in seconds, until the next spawn
678
    /// cycle.
679
    period: f32,
680

681
    /// Number of particles to spawn this frame.
682
    ///
683
    /// This value is normally updated by calling [`tick()`], which
684
    /// automatically happens once per frame when the [`tick_spawners()`] system
685
    /// runs in the [`PostUpdate`] schedule.
686
    ///
687
    /// You can manually assign this value to override the one calculated by
688
    /// [`tick()`]. Note in this case that you need to override the value after
689
    /// the automated one was calculated, by ordering your system
690
    /// after [`tick_spawners()`] or [`EffectSystems::TickSpawners`].
691
    ///
692
    /// [`tick()`]: crate::EffectSpawner::tick
693
    /// [`EffectSystems::TickSpawners`]: crate::EffectSystems::TickSpawners
694
    pub spawn_count: u32,
695

696
    /// Fractional remainder of particle count to spawn.
697
    ///
698
    /// This is accumulated each tick, and the integral part is added to
699
    /// `spawn_count`. The reminder gets saved for next frame.
700
    spawn_remainder: f32,
701

702
    /// Whether the spawner is active. Defaults to `true`. An inactive spawner
703
    /// doesn't tick (no particle spawned, no internal time updated).
704
    active: bool,
705
}
706

707
impl EffectSpawner {
708
    /// Create a new spawner state from a [`Spawner`].
709
    pub fn new(spawner: &Spawner) -> Self {
11✔
710
        Self {
711
            spawner: *spawner,
11✔
712
            time: if spawner.is_once() && !spawner.starts_immediately {
16✔
713
                1. // anything > 0
714
            } else {
715
                0.
716
            },
717
            spawn_duration: 0.,
718
            period: 0.,
719
            spawn_count: 0,
720
            spawn_remainder: 0.,
721
            active: spawner.starts_active(),
11✔
722
        }
723
    }
724

725
    /// Set whether the spawner is active.
726
    ///
727
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
728
    pub fn with_active(mut self, active: bool) -> Self {
×
729
        self.active = active;
×
730
        self
×
731
    }
732

733
    /// Set whether the spawner is active.
734
    ///
735
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
736
    pub fn set_active(&mut self, active: bool) {
4✔
737
        self.active = active;
4✔
738
    }
739

740
    /// Get whether the spawner is active.
741
    ///
742
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
743
    pub fn is_active(&self) -> bool {
4✔
744
        self.active
4✔
745
    }
746

747
    /// Get the spawner configuration in use.
748
    ///
749
    /// The effective [`Spawner`] used is either the override specified in the
750
    /// associated [`ParticleEffect`] instance, or the fallback one specified in
751
    /// underlying [`EffectAsset`].
752
    pub fn spawner(&self) -> &Spawner {
×
753
        &self.spawner
×
754
    }
755

756
    /// Reset the spawner state.
757
    ///
758
    /// This resets the internal spawner time to zero, and restarts any internal
759
    /// particle counter.
760
    ///
761
    /// Use this, for example, to immediately spawn some particles in a spawner
762
    /// constructed with [`Spawner::once`].
763
    ///
764
    /// [`Spawner::once`]: crate::Spawner::once
765
    pub fn reset(&mut self) {
2✔
766
        self.time = 0.;
2✔
767
        self.period = 0.;
2✔
768
        self.spawn_count = 0;
2✔
769
        self.spawn_remainder = 0.;
2✔
770
    }
771

772
    /// Tick the spawner to calculate the number of particles to spawn this
773
    /// frame.
774
    ///
775
    /// The frame delta time `dt` is added to the current spawner time, before
776
    /// the spawner calculates the number of particles to spawn.
777
    ///
778
    /// This method is called automatically by [`tick_spawners()`] during the
779
    /// [`PostUpdate`], so you normally don't have to call it yourself
780
    /// manually.
781
    ///
782
    /// # Returns
783
    ///
784
    /// The integral number of particles to spawn this frame. Any fractional
785
    /// remainder is saved for the next call.
786
    pub fn tick(&mut self, mut dt: f32, rng: &mut Pcg32) -> u32 {
35✔
787
        if !self.active {
35✔
788
            self.spawn_count = 0;
3✔
789
            return 0;
3✔
790
        }
791

792
        // The limit can be reached multiple times, so use a loop
793
        loop {
794
            if self.period == 0.0 {
51✔
795
                self.resample(rng);
13✔
796
                continue;
13✔
797
            }
798

799
            let new_time = self.time + dt;
38✔
800
            if self.time <= self.spawn_duration {
38✔
801
                // If the spawn time is very small, close to zero, spawn all particles
802
                // immediately in one burst over a single frame.
803
                self.spawn_remainder += if self.spawn_duration < 1e-5f32.max(dt / 100.0) {
33✔
804
                    self.spawner.count.sample(rng)
9✔
805
                } else {
806
                    // Spawn an amount of particles equal to the fraction of time the current frame
807
                    // spans compared to the total burst duration.
808
                    self.spawner.count.sample(rng) * (new_time.min(self.spawn_duration) - self.time)
24✔
809
                        / self.spawn_duration
24✔
810
                };
811
            }
812

813
            let old_time = self.time;
814
            self.time = new_time;
815

816
            if self.time >= self.period {
6✔
817
                dt -= self.period - old_time;
6✔
818
                self.time = 0.0; // dt will be added on in the next iteration
6✔
819
                self.resample(rng);
6✔
820
            } else {
821
                break;
32✔
822
            }
823
        }
824

825
        let count = self.spawn_remainder.floor();
32✔
826
        self.spawn_remainder -= count;
32✔
827
        self.spawn_count = count as u32;
32✔
828

829
        self.spawn_count
32✔
830
    }
831

832
    /// Resamples the spawn time and period.
833
    fn resample(&mut self, rng: &mut Pcg32) {
19✔
834
        self.period = self.spawner.period.sample(rng);
19✔
835
        self.spawn_duration = self
19✔
836
            .spawner
19✔
837
            .spawn_duration
19✔
838
            .sample(rng)
19✔
839
            .clamp(0.0, self.period);
19✔
840
    }
841
}
842

843
/// A runtime structure maintaining the state of the cloner for a particle
844
/// group.
845
#[derive(Default, Clone, Copy, PartialEq, Reflect, Debug)]
846
pub struct EffectCloner {
847
    /// The cloner configuration extracted either from the [`EffectAsset`] or
848
    /// overridden manually.
849
    pub cloner: Cloner,
850
    /// Accumulated time since last clone, in seconds.
851
    time: f32,
852
    /// Sampled value of the time period, in seconds, until the next clone
853
    /// cycle.
854
    period: f32,
855
    /// The capacity of the group.
856
    capacity: u32,
857
    /// Whether the cloner is to clone any particle this frame.
858
    pub clone_this_frame: bool,
859
    /// Whether the cloner is active. Defaults to `true`.
860
    pub active: bool,
861
}
862

863
impl EffectCloner {
864
    pub(crate) fn new(cloner: Cloner, capacity: u32) -> EffectCloner {
×
865
        EffectCloner {
866
            cloner,
867
            time: 0.0,
868
            period: 0.0,
869
            capacity,
870
            clone_this_frame: false,
871
            active: cloner.starts_active(),
×
872
        }
873
    }
874

875
    /// Reset the cloner state.
876
    ///
877
    /// This resets the internal cloner time to zero, and restarts any internal
878
    /// particle counter.
UNCOV
879
    pub fn reset(&mut self) {
×
UNCOV
880
        self.time = 0.0;
×
NEW
881
        self.period = 0.0;
×
882
    }
883

884
    /// Tick the cloner and update [`clone_this_frame`] to trigger cloning.
885
    ///
886
    /// [`clone_this_frame`]: EffectCloner::clone_this_frame
887
    pub fn tick(&mut self, dt: f32, rng: &mut Pcg32) {
×
888
        if !self.active {
×
NEW
889
            self.clone_this_frame = false;
×
890
            return;
×
891
        }
892

NEW
893
        if self.period <= 0.0 {
×
894
            self.resample(rng);
×
895
        }
896

897
        let new_time = self.time + dt;
×
898
        self.time = new_time;
×
899

NEW
900
        self.clone_this_frame = self.time >= self.period;
×
901

NEW
902
        if self.clone_this_frame {
×
903
            self.time = 0.0;
×
904
            self.resample(rng);
×
905
        }
906
    }
907

908
    fn resample(&mut self, rng: &mut Pcg32) {
×
NEW
909
        self.period = self.cloner.period.sample(rng);
×
910
    }
911

912
    /// Marks this cloner as either active or inactive.
913
    ///
914
    /// Inactive cloners don't clone any particles.
915
    pub fn set_active(&mut self, active: bool) {
×
916
        self.active = active;
×
917
    }
918
}
919

920
/// Tick all the [`EffectSpawner`] and [`EffectCloner`] initializers.
921
///
922
/// This system runs in the [`PostUpdate`] stage, after the visibility system
923
/// has updated the [`InheritedVisibility`] of each effect instance (see
924
/// [`VisibilitySystems::VisibilityPropagate`]). Hidden instances are not
925
/// updated, unless the [`EffectAsset::simulation_condition`]
926
/// is set to [`SimulationCondition::Always`]. If no [`InheritedVisibility`] is
927
/// present, the effect is assumed to be visible.
928
///
929
/// Note that by that point the [`ViewVisibility`] is not yet calculated, and it
930
/// may happen that spawners are ticked but no effect is visible in any view
931
/// even though some are "visible" (active) in the [`World`]. The actual
932
/// per-view culling of invisible (not in view) effects is performed later on
933
/// the render world.
934
///
935
/// Once the system determined that the effect instance needs to be simulated
936
/// this frame, it ticks the effect's initializer by calling
937
/// [`EffectSpawner::tick()`] or [`EffectCloner::tick()`], adding a new
938
/// [`EffectInitializers`] component if it doesn't already exist on the
939
/// same entity as the [`ParticleEffect`].
940
///
941
/// [`VisibilitySystems::VisibilityPropagate`]: bevy::render::view::VisibilitySystems::VisibilityPropagate
942
/// [`EffectAsset::simulation_condition`]: crate::EffectAsset::simulation_condition
943
pub fn tick_initializers(
13✔
944
    mut commands: Commands,
945
    time: Res<Time<EffectSimulation>>,
946
    effects: Res<Assets<EffectAsset>>,
947
    mut rng: ResMut<Random>,
948
    mut query: Query<(
949
        Entity,
950
        &ParticleEffect,
951
        Option<&InheritedVisibility>,
952
        Option<&mut EffectInitializers>,
953
    )>,
954
) {
955
    trace!("tick_spawners");
13✔
956

957
    let dt = time.delta_seconds();
13✔
958

959
    for (entity, effect, maybe_inherited_visibility, maybe_initializers) in query.iter_mut() {
13✔
960
        // TODO - maybe cache simulation_condition so we don't need to unconditionally
961
        // query the asset?
962
        let Some(asset) = effects.get(&effect.handle) else {
16✔
963
            continue;
10✔
964
        };
965

966
        if asset.simulation_condition == SimulationCondition::WhenVisible
967
            && !maybe_inherited_visibility
2✔
968
                .map(|iv| iv.get())
6✔
969
                .unwrap_or(true)
2✔
970
        {
971
            continue;
1✔
972
        }
973

974
        if let Some(mut initializers) = maybe_initializers {
×
975
            for initializer in &mut **initializers {
×
976
                match initializer {
×
977
                    EffectInitializer::Spawner(effect_spawner) => {
×
978
                        effect_spawner.tick(dt, &mut rng.0);
×
979
                    }
980
                    EffectInitializer::Cloner(effect_cloner) => {
×
981
                        effect_cloner.tick(dt, &mut rng.0);
×
982
                    }
983
                }
984
            }
985
            continue;
×
986
        }
987

988
        let initializers = asset
2✔
989
            .init
2✔
990
            .iter()
991
            .enumerate()
992
            .map(|(group_index, init)| match *init {
4✔
993
                Initializer::Spawner(spawner) => {
2✔
994
                    let mut effect_spawner = EffectSpawner::new(&spawner);
2✔
995
                    effect_spawner.tick(dt, &mut rng.0);
2✔
996
                    EffectInitializer::Spawner(effect_spawner)
2✔
997
                }
998
                Initializer::Cloner(cloner) => {
×
999
                    let mut effect_cloner =
×
1000
                        EffectCloner::new(cloner, asset.capacities()[group_index]);
×
1001
                    effect_cloner.tick(dt, &mut rng.0);
×
1002
                    EffectInitializer::Cloner(effect_cloner)
×
1003
                }
1004
            })
1005
            .collect();
1006
        commands
1007
            .entity(entity)
1008
            .insert(EffectInitializers(initializers));
1009
    }
1010
}
1011

1012
#[cfg(test)]
1013
mod test {
1014
    use std::time::Duration;
1015

1016
    use bevy::{
1017
        asset::{
1018
            io::{
1019
                memory::{Dir, MemoryAssetReader},
1020
                AssetSourceBuilder, AssetSourceBuilders, AssetSourceId,
1021
            },
1022
            AssetServerMode,
1023
        },
1024
        render::view::{VisibilityPlugin, VisibilitySystems},
1025
        tasks::{IoTaskPool, TaskPoolBuilder},
1026
    };
1027

1028
    use super::*;
1029
    use crate::Module;
1030

1031
    /// Make an `EffectSpawner` wrapping a `Spawner`.
1032
    fn make_effect_spawner(spawner: Spawner) -> EffectSpawner {
1033
        EffectSpawner::new(
1034
            EffectAsset::new(256, spawner, Module::default()).init[0]
1035
                .get_spawner()
1036
                .expect("Expected the first group to have a spawner"),
1037
        )
1038
    }
1039

1040
    #[test]
1041
    fn test_range_single() {
1042
        let value = CpuValue::Single(1.0);
1043
        assert_eq!(value.range(), [1.0, 1.0]);
1044
    }
1045

1046
    #[test]
1047
    fn test_range_uniform() {
1048
        let value = CpuValue::Uniform((1.0, 3.0));
1049
        assert_eq!(value.range(), [1.0, 3.0]);
1050
    }
1051

1052
    #[test]
1053
    fn test_range_uniform_reverse() {
1054
        let value = CpuValue::Uniform((3.0, 1.0));
1055
        assert_eq!(value.range(), [1.0, 3.0]);
1056
    }
1057

1058
    #[test]
1059
    fn test_new() {
1060
        let rng = &mut new_rng();
1061
        // 3 particles over 3 seconds, pause 7 seconds (total 10 seconds period).
1062
        let spawner = Spawner::new(3.0.into(), 3.0.into(), 10.0.into());
1063
        let mut spawner = make_effect_spawner(spawner);
1064
        let count = spawner.tick(2.0, rng); // t = 2s
1065
        assert_eq!(count, 2);
1066
        let count = spawner.tick(5.0, rng); // t = 7s
1067
        assert_eq!(count, 1);
1068
        let count = spawner.tick(8.0, rng); // t = 15s
1069
        assert_eq!(count, 3);
1070
    }
1071

1072
    #[test]
1073
    #[should_panic]
1074
    fn test_new_panic_negative_period() {
1075
        let _ = Spawner::new(3.0.into(), 1.0.into(), CpuValue::Uniform((-1., 1.)));
1076
    }
1077

1078
    #[test]
1079
    #[should_panic]
1080
    fn test_new_panic_zero_period() {
1081
        let _ = Spawner::new(3.0.into(), 1.0.into(), CpuValue::Uniform((0., 0.)));
1082
    }
1083

1084
    #[test]
1085
    fn test_once() {
1086
        let rng = &mut new_rng();
1087
        let spawner = Spawner::once(5.0.into(), true);
1088
        let mut spawner = make_effect_spawner(spawner);
1089
        let count = spawner.tick(0.001, rng);
1090
        assert_eq!(count, 5);
1091
        let count = spawner.tick(100.0, rng);
1092
        assert_eq!(count, 0);
1093
    }
1094

1095
    #[test]
1096
    fn test_once_reset() {
1097
        let rng = &mut new_rng();
1098
        let spawner = Spawner::once(5.0.into(), true);
1099
        let mut spawner = make_effect_spawner(spawner);
1100
        spawner.tick(1.0, rng);
1101
        spawner.reset();
1102
        let count = spawner.tick(1.0, rng);
1103
        assert_eq!(count, 5);
1104
    }
1105

1106
    #[test]
1107
    fn test_once_not_immediate() {
1108
        let rng = &mut new_rng();
1109
        let spawner = Spawner::once(5.0.into(), false);
1110
        let mut spawner = make_effect_spawner(spawner);
1111
        let count = spawner.tick(1.0, rng);
1112
        assert_eq!(count, 0);
1113
        spawner.reset();
1114
        let count = spawner.tick(1.0, rng);
1115
        assert_eq!(count, 5);
1116
    }
1117

1118
    #[test]
1119
    fn test_rate() {
1120
        let rng = &mut new_rng();
1121
        let spawner = Spawner::rate(5.0.into());
1122
        let mut spawner = make_effect_spawner(spawner);
1123
        // Slightly over 1.0 to avoid edge case
1124
        let count = spawner.tick(1.01, rng);
1125
        assert_eq!(count, 5);
1126
        let count = spawner.tick(0.4, rng);
1127
        assert_eq!(count, 2);
1128
    }
1129

1130
    #[test]
1131
    fn test_rate_active() {
1132
        let rng = &mut new_rng();
1133
        let spawner = Spawner::rate(5.0.into());
1134
        let mut spawner = make_effect_spawner(spawner);
1135
        spawner.tick(1.01, rng);
1136
        spawner.set_active(false);
1137
        assert!(!spawner.is_active());
1138
        let count = spawner.tick(0.4, rng);
1139
        assert_eq!(count, 0);
1140
        spawner.set_active(true);
1141
        assert!(spawner.is_active());
1142
        let count = spawner.tick(0.4, rng);
1143
        assert_eq!(count, 2);
1144
    }
1145

1146
    #[test]
1147
    fn test_rate_accumulate() {
1148
        let rng = &mut new_rng();
1149
        let spawner = Spawner::rate(5.0.into());
1150
        let mut spawner = make_effect_spawner(spawner);
1151
        // 13 ticks instead of 12 to avoid edge case
1152
        let count = (0..13).map(|_| spawner.tick(1.0 / 60.0, rng)).sum::<u32>();
1153
        assert_eq!(count, 1);
1154
    }
1155

1156
    #[test]
1157
    fn test_burst() {
1158
        let rng = &mut new_rng();
1159
        let spawner = Spawner::burst(5.0.into(), 2.0.into());
1160
        let mut spawner = make_effect_spawner(spawner);
1161
        let count = spawner.tick(1.0, rng);
1162
        assert_eq!(count, 5);
1163
        let count = spawner.tick(4.0, rng);
1164
        assert_eq!(count, 10);
1165
        let count = spawner.tick(0.1, rng);
1166
        assert_eq!(count, 0);
1167
    }
1168

1169
    #[test]
1170
    fn test_with_active() {
1171
        let rng = &mut new_rng();
1172
        let spawner = Spawner::rate(5.0.into()).with_starts_active(false);
1173
        let mut spawner = make_effect_spawner(spawner);
1174
        assert!(!spawner.is_active());
1175
        let count = spawner.tick(1., rng);
1176
        assert_eq!(count, 0);
1177
        spawner.set_active(false); // no-op
1178
        let count = spawner.tick(1., rng);
1179
        assert_eq!(count, 0);
1180
        spawner.set_active(true);
1181
        assert!(spawner.is_active());
1182
        let count = spawner.tick(1., rng);
1183
        assert_eq!(count, 5);
1184
    }
1185

1186
    fn make_test_app() -> App {
1187
        IoTaskPool::get_or_init(|| {
1188
            TaskPoolBuilder::default()
1189
                .num_threads(1)
1190
                .thread_name("Hanabi test IO Task Pool".to_string())
1191
                .build()
1192
        });
1193

1194
        let mut app = App::new();
1195

1196
        let watch_for_changes = false;
1197
        let mut builders = app
1198
            .world_mut()
1199
            .get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
1200
        let dir = Dir::default();
1201
        let dummy_builder = AssetSourceBuilder::default()
1202
            .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }));
1203
        builders.insert(AssetSourceId::Default, dummy_builder);
1204
        let sources = builders.build_sources(watch_for_changes, false);
1205
        let asset_server =
1206
            AssetServer::new(sources, AssetServerMode::Unprocessed, watch_for_changes);
1207

1208
        app.insert_resource(asset_server);
1209
        // app.add_plugins(DefaultPlugins);
1210
        app.init_asset::<Mesh>();
1211
        app.add_plugins(VisibilityPlugin);
1212
        app.init_resource::<Time<EffectSimulation>>();
1213
        app.insert_resource(Random(new_rng()));
1214
        app.init_asset::<EffectAsset>();
1215
        app.add_systems(
1216
            PostUpdate,
1217
            tick_initializers.after(VisibilitySystems::CheckVisibility),
1218
        );
1219

1220
        app
1221
    }
1222

1223
    /// Test case for `tick_spawners()`.
1224
    struct TestCase {
1225
        /// Initial entity visibility on spawn. If `None`, do not add a
1226
        /// [`Visibility`] component.
1227
        visibility: Option<Visibility>,
1228

1229
        /// Spawner assigned to the `EffectAsset`.
1230
        asset_spawner: Spawner,
1231
    }
1232

1233
    impl TestCase {
1234
        fn new(visibility: Option<Visibility>, asset_spawner: Spawner) -> Self {
1235
            Self {
1236
                visibility,
1237
                asset_spawner,
1238
            }
1239
        }
1240
    }
1241

1242
    #[test]
1243
    fn test_tick_spawners() {
1244
        let asset_spawner = Spawner::once(32.0.into(), true);
1245

1246
        for test_case in &[
1247
            TestCase::new(None, asset_spawner),
1248
            TestCase::new(Some(Visibility::Hidden), asset_spawner),
1249
            TestCase::new(Some(Visibility::Visible), asset_spawner),
1250
        ] {
1251
            let mut app = make_test_app();
1252

1253
            let (effect_entity, handle) = {
1254
                let world = app.world_mut();
1255

1256
                // Add effect asset
1257
                let mut assets = world.resource_mut::<Assets<EffectAsset>>();
1258
                let mut asset = EffectAsset::new(64, test_case.asset_spawner, Module::default());
1259
                asset.simulation_condition = if test_case.visibility.is_some() {
1260
                    SimulationCondition::WhenVisible
1261
                } else {
1262
                    SimulationCondition::Always
1263
                };
1264
                let handle = assets.add(asset);
1265

1266
                // Spawn particle effect
1267
                let entity = if let Some(visibility) = test_case.visibility {
1268
                    world
1269
                        .spawn((
1270
                            visibility,
1271
                            InheritedVisibility::default(),
1272
                            ParticleEffect {
1273
                                handle: handle.clone(),
1274
                                #[cfg(feature = "2d")]
1275
                                z_layer_2d: None,
1276
                            },
1277
                        ))
1278
                        .id()
1279
                } else {
1280
                    world
1281
                        .spawn((ParticleEffect {
1282
                            handle: handle.clone(),
1283
                            #[cfg(feature = "2d")]
1284
                            z_layer_2d: None,
1285
                        },))
1286
                        .id()
1287
                };
1288

1289
                // Spawn a camera, otherwise ComputedVisibility stays at HIDDEN
1290
                world.spawn(Camera3dBundle::default());
1291

1292
                (entity, handle)
1293
            };
1294

1295
            // Tick once
1296
            let cur_time = {
1297
                // Make sure to increment the current time so that the spawners spawn something.
1298
                // Note that `Time` has this weird behavior where the common quantities like
1299
                // `Time::delta_seconds()` only update after the *second* update. So we tick the
1300
                // `Time` twice here to enforce this.
1301
                let mut time = app.world_mut().resource_mut::<Time<EffectSimulation>>();
1302
                time.advance_by(Duration::from_millis(16));
1303
                time.elapsed()
1304
            };
1305
            app.update();
1306

1307
            let world = app.world_mut();
1308

1309
            // Check the state of the components after `tick_spawners()` ran
1310
            if let Some(test_visibility) = test_case.visibility {
1311
                // Simulated-when-visible effect (SimulationCondition::WhenVisible)
1312

1313
                let (entity, visibility, inherited_visibility, particle_effect, effect_spawners) =
1314
                    world
1315
                        .query::<(
1316
                            Entity,
1317
                            &Visibility,
1318
                            &InheritedVisibility,
1319
                            &ParticleEffect,
1320
                            Option<&EffectInitializers>,
1321
                        )>()
1322
                        .iter(world)
1323
                        .next()
1324
                        .unwrap();
1325
                assert_eq!(entity, effect_entity);
1326
                assert_eq!(visibility, test_visibility);
1327
                assert_eq!(
1328
                    inherited_visibility.get(),
1329
                    test_visibility == Visibility::Visible
1330
                );
1331
                assert_eq!(particle_effect.handle, handle);
1332
                if inherited_visibility.get() {
1333
                    // If visible, `tick_spawners()` spawns the EffectSpawner and ticks it
1334
                    assert!(effect_spawners.is_some());
1335
                    let effect_spawner = effect_spawners.unwrap()[0].get_spawner().unwrap();
1336
                    let actual_spawner = effect_spawner.spawner;
1337

1338
                    // Check the spawner ticked
1339
                    assert!(effect_spawner.active);
1340
                    assert_eq!(effect_spawner.spawn_remainder, 0.);
1341
                    assert_eq!(effect_spawner.time, cur_time.as_secs_f32());
1342

1343
                    assert_eq!(actual_spawner, test_case.asset_spawner);
1344
                    assert_eq!(effect_spawner.spawn_count, 32);
1345
                } else {
1346
                    // If not visible, `tick_spawners()` skips the effect entirely so won't spawn an
1347
                    // `EffectSpawner` for it
1348
                    assert!(effect_spawners.is_none());
1349
                }
1350
            } else {
1351
                // Always-simulated effect (SimulationCondition::Always)
1352

1353
                let (entity, particle_effect, effect_spawners) = world
1354
                    .query::<(Entity, &ParticleEffect, Option<&EffectInitializers>)>()
1355
                    .iter(world)
1356
                    .next()
1357
                    .unwrap();
1358
                assert_eq!(entity, effect_entity);
1359
                assert_eq!(particle_effect.handle, handle);
1360

1361
                assert!(effect_spawners.is_some());
1362
                let effect_spawner = effect_spawners.unwrap()[0].get_spawner().unwrap();
1363
                let actual_spawner = effect_spawner.spawner;
1364

1365
                // Check the spawner ticked
1366
                assert!(effect_spawner.active);
1367
                assert_eq!(effect_spawner.spawn_remainder, 0.);
1368
                assert_eq!(effect_spawner.time, cur_time.as_secs_f32());
1369

1370
                assert_eq!(actual_spawner, test_case.asset_spawner);
1371
                assert_eq!(effect_spawner.spawn_count, 32);
1372
            }
1373
        }
1374
    }
1375
}
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