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

djeedai / bevy_hanabi / 12128238298

02 Dec 2024 09:24PM UTC coverage: 48.661% (-7.6%) from 56.217%
12128238298

Pull #401

github

web-flow
Merge 30c486d1a into 19aee8dbc
Pull Request #401: Upgrade to Bevy v0.15.0

39 of 284 new or added lines in 11 files covered. (13.73%)

435 existing lines in 8 files now uncovered.

3106 of 6383 relevant lines covered (48.66%)

21.61 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

92
impl<T: Copy + FromReflect + SampleUniform> CpuValue<T> {
93
    /// Sample the value.
94
    /// - For [`CpuValue::Single`], always return the same single value.
95
    /// - For [`CpuValue::Uniform`], use the given pseudo-random number
96
    ///   generator to generate a random sample.
97
    pub fn sample(&self, rng: &mut Pcg32) -> T {
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
/// Initializer to emit new particles.
147
///
148
/// An initializer defines when a particle is emitted (spawned or cloned).
149
/// - For CPU spawning, a [`Spawner`] defines how often new particles are
150
///   spawned. This is the typical way to emit particles.
151
/// - For GPU cloning, a [`Cloner`] defines how often an existing particle is
152
///   cloned into a new one. This is used by trails and ribbons only.
153
#[derive(Clone, Copy, PartialEq, Debug, Reflect, Serialize, Deserialize)]
154
#[reflect(Serialize, Deserialize)]
155
pub enum Initializer {
156
    /// CPU spawner initializer.
157
    Spawner(Spawner),
158
    /// GPU cloner initializer, for trails and ribbons.
159
    Cloner(Cloner),
160
}
161

162
impl From<Spawner> for Initializer {
163
    #[inline]
164
    fn from(value: Spawner) -> Self {
21✔
165
        Self::Spawner(value)
21✔
166
    }
167
}
168

169
impl From<Cloner> for Initializer {
170
    #[inline]
171
    fn from(value: Cloner) -> Self {
×
172
        Self::Cloner(value)
×
173
    }
174
}
175

176
impl Initializer {
177
    #[cfg(test)]
178
    fn get_spawner(&self) -> Option<&Spawner> {
9✔
179
        match *self {
9✔
180
            Initializer::Spawner(ref spawner) => Some(spawner),
9✔
181
            Initializer::Cloner(_) => None,
×
182
        }
183
    }
184
}
185

186
/// Spawner defining how new particles are emitted.
187
///
188
/// The spawner defines how new particles are emitted and when. Each time the
189
/// spawner ticks, it calculates a number of particles to emit for this frame.
190
/// This spawn count is passed to the GPU for the init compute pass to actually
191
/// allocate the new particles and initialize them. The number of particles to
192
/// spawn is stored as a floating-point number, and any remainder accumulates
193
/// for the next emitting.
194
///
195
/// The spawner itself is embedded into the [`EffectInitializers`] component.
196
/// Once per frame the [`tick_initializers()`] system will add the component if
197
/// it's missing, cloning the [`Spawner`] from the source [`EffectAsset`], then
198
/// tick the [`Spawner`] stored in the [`EffectInitializers`]. The resulting
199
/// number of particles to spawn for the frame is then stored into
200
/// [`EffectSpawner::spawn_count`]. You can override that value to manually
201
/// control each frame how many particles are spawned.
202
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
203
#[reflect(Default)]
204
pub struct Spawner {
205
    /// Number of particles to spawn over [`spawn_duration`].
206
    ///
207
    /// [`spawn_duration`]: Spawner::spawn_duration
208
    count: CpuValue<f32>,
209

210
    /// Time over which to spawn [`count`], in seconds.
211
    ///
212
    /// [`count`]: Spawner::count
213
    spawn_duration: CpuValue<f32>,
214

215
    /// Time between bursts of the particle system, in seconds.
216
    ///
217
    /// If this is infinity, there's only one burst.
218
    /// If this is [`spawn_duration`] or less, the system spawns a steady stream
219
    /// of particles.
220
    ///
221
    /// [`spawn_duration`]: Spawner::spawn_duration
222
    period: CpuValue<f32>,
223

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

231
    /// Whether the burst of a once-style spawner triggers immediately when the
232
    /// spawner becomes active.
233
    ///
234
    /// If `false`, the spawner doesn't do anything until
235
    /// [`EffectSpawner::reset()`] is called.
236
    starts_immediately: bool,
237
}
238

239
impl Default for Spawner {
240
    fn default() -> Self {
×
241
        Self::once(1.0f32.into(), true)
×
242
    }
243
}
244

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

311
        Self {
312
            count,
313
            spawn_duration,
314
            period,
315
            starts_active: true,
316
            starts_immediately: true,
317
        }
318
    }
319

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

353
    /// Get whether this spawner emits a single burst.
354
    pub fn is_once(&self) -> bool {
18✔
355
        if let CpuValue::Single(f) = self.period {
36✔
356
            f.is_infinite()
357
        } else {
358
            false
×
359
        }
360
    }
361

362
    /// Create a spawner that spawns particles at `rate`, accumulated each
363
    /// frame. `rate` is in particles per second.
364
    ///
365
    /// This is a convenience for:
366
    ///
367
    /// ```
368
    /// # use bevy_hanabi::{Spawner, CpuValue};
369
    /// # let rate = CpuValue::Single(1.);
370
    /// Spawner::new(rate, 1.0.into(), 1.0.into());
371
    /// ```
372
    ///
373
    /// # Example
374
    ///
375
    /// ```
376
    /// # use bevy_hanabi::Spawner;
377
    /// // Spawn 10 particles per second, indefinitely.
378
    /// let spawner = Spawner::rate(10.0.into());
379
    /// ```
380
    pub fn rate(rate: CpuValue<f32>) -> Self {
9✔
381
        Self::new(rate, 1.0.into(), 1.0.into())
9✔
382
    }
383

384
    /// Create a spawner that spawns `count` particles, waits `period` seconds,
385
    /// and repeats forever.
386
    ///
387
    /// This is a convenience for:
388
    ///
389
    /// ```
390
    /// # use bevy_hanabi::{Spawner, CpuValue};
391
    /// # let count = CpuValue::Single(1.);
392
    /// # let period = CpuValue::Single(1.);
393
    /// Spawner::new(count, 0.0.into(), period);
394
    /// ```
395
    ///
396
    /// # Example
397
    ///
398
    /// ```
399
    /// # use bevy_hanabi::Spawner;
400
    /// // Spawn a burst of 5 particles every 3 seconds, indefinitely.
401
    /// let spawner = Spawner::burst(5.0.into(), 3.0.into());
402
    /// ```
403
    pub fn burst(count: CpuValue<f32>, period: CpuValue<f32>) -> Self {
1✔
404
        Self::new(count, 0.0.into(), period)
1✔
405
    }
406

407
    /// Set the number of particles that are spawned each cycle.
408
    pub fn with_count(mut self, count: CpuValue<f32>) -> Self {
×
409
        self.count = count;
×
410
        self
×
411
    }
412

413
    /// Set the number of particles that are spawned each cycle.
414
    pub fn set_count(&mut self, count: CpuValue<f32>) {
×
415
        self.count = count;
×
416
    }
417

418
    /// Get the number of particles that are spawned each cycle.
419
    pub fn count(&self) -> CpuValue<f32> {
×
420
        self.count
×
421
    }
422

423
    /// Set the duration, in seconds, of the spawn time each cycle.
424
    pub fn with_spawn_time(mut self, spawn_duration: CpuValue<f32>) -> Self {
×
425
        self.spawn_duration = spawn_duration;
×
426
        self
×
427
    }
428

429
    /// Set the duration, in seconds, of the spawn time each cycle.
430
    pub fn set_spawn_time(&mut self, spawn_duration: CpuValue<f32>) {
×
431
        self.spawn_duration = spawn_duration;
×
432
    }
433

434
    /// Get the duration, in seconds, of spawn time each cycle.
435
    pub fn spawn_duration(&self) -> CpuValue<f32> {
×
436
        self.spawn_duration
×
437
    }
438

439
    /// Set the duration of a 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
    ///
444
    /// [`spawn_duration()`]: Self::spawn_duration
445
    pub fn with_period(mut self, period: CpuValue<f32>) -> Self {
×
446
        self.period = period;
×
447
        self
×
448
    }
449

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

460
    /// Get the duration of the spawn cycle, in seconds.
461
    ///
462
    /// A spawn cycles includes the [`spawn_duration()`] value, and any extra
463
    /// wait time (if larger than spawn time).
464
    ///
465
    /// [`spawn_duration()`]: Self::spawn_duration
466
    pub fn period(&self) -> CpuValue<f32> {
×
467
        self.period
×
468
    }
469

470
    /// Sets whether the spawner starts active when the effect is instantiated.
471
    ///
472
    /// This value will be transfered to the active state of the
473
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
474
    /// any particle.
475
    pub fn with_starts_active(mut self, starts_active: bool) -> Self {
1✔
476
        self.starts_active = starts_active;
1✔
477
        self
1✔
478
    }
479

480
    /// Set whether the spawner starts active when the effect is instantiated.
481
    ///
482
    /// This value will be transfered to the active state of the
483
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
484
    /// any particle.
485
    pub fn set_starts_active(&mut self, starts_active: bool) {
×
486
        self.starts_active = starts_active;
×
487
    }
488

489
    /// Get whether the spawner starts active when the effect is instantiated.
490
    ///
491
    /// This value will be transfered to the active state of the
492
    /// [`EffectSpawner`] once it's instantiated. Inactive spawners do not spawn
493
    /// any particle.
494
    pub fn starts_active(&self) -> bool {
11✔
495
        self.starts_active
11✔
496
    }
497
}
498

499
/// Defines how particle trails are to be constructed.
500
///
501
/// Particle trails are constructed by cloning the particles from a group into
502
/// a different group on a fixed interval. Each time the cloner ticks, it
503
/// clones all the particles from the source group into the destination group.
504
/// Hanabi then runs the initialization modifiers on the newly-cloned
505
/// particles. Particle clones that would overflow the destination group
506
/// (exceed its capacity) are dropped.
507
///
508
/// The cloner itself is embedded into the [`EffectInitializers`] component.
509
/// Once per frame the [`tick_initializers()`] system will add the component if
510
/// it's missing, copying fields from the [`Cloner`] to the [`EffectCloner`].
511
#[derive(Default, Clone, Copy, Debug, PartialEq, Reflect, Serialize, Deserialize)]
512
#[reflect(Serialize, Deserialize)]
513
pub struct Cloner {
514
    /// The group from which the cloner copies.
515
    pub src_group_index: u32,
516

517
    /// Time between clone operations, in seconds.
518
    pub period: CpuValue<f32>,
519

520
    /// Time that the particles persist, in seconds.
521
    ///
522
    /// Unlike spawned particles, cloned particles don't use the
523
    /// [`crate::attributes::Attribute::LIFETIME`] attribute and instead track
524
    /// lifetime themselves, using this value. This is because, internally,
525
    /// their lifetimes must follow last-in-first-out (LIFO) order.
526
    pub lifetime: f32,
527

528
    /// Whether the system is active at startup. The value is used to initialize
529
    /// [`EffectCloner::active`].
530
    ///
531
    /// [`EffectCloner::active`]: crate::EffectCloner::active
532
    pub starts_active: bool,
533
}
534

535
impl Cloner {
536
    /// Creates a cloner with the given source group index, period, and
537
    /// lifetime.
538
    ///
539
    /// This is the raw constructor. A more convenient way to create cloners is
540
    /// to use [`EffectAsset::with_trails`] or [`EffectAsset::with_ribbons`].
541
    pub fn new(src_group_index: u32, period: impl Into<CpuValue<f32>>, lifetime: f32) -> Self {
×
542
        Self {
543
            src_group_index,
544
            period: period.into(),
×
545
            lifetime,
546
            starts_active: true,
547
        }
548
    }
549

550
    /// Sets whether the cloner starts active when the effect is instantiated.
551
    ///
552
    /// This value will be transfered to the active state of the
553
    /// [`EffectCloner`] once it's instantiated. Inactive cloners do not clone
554
    /// any particle.
555
    pub fn with_starts_active(mut self, starts_active: bool) -> Self {
×
556
        self.starts_active = starts_active;
×
557
        self
×
558
    }
559

560
    /// Set whether the cloner starts active when the effect is instantiated.
561
    ///
562
    /// This value will be transfered to the active state of the
563
    /// [`EffectCloner`] once it's instantiated. Inactive cloners do not clone
564
    /// any particle.
565
    pub fn set_starts_active(&mut self, starts_active: bool) {
×
566
        self.starts_active = starts_active;
×
567
    }
568

569
    /// Get whether the cloner starts active when the effect is instantiated.
570
    ///
571
    /// This value will be transfered to the active state of the
572
    /// [`EffectCloner`] once it's instantiated. Inactive cloners do not clone
573
    /// any particle.
574
    pub fn starts_active(&self) -> bool {
×
575
        self.starts_active
×
576
    }
577
}
578

579
/// A runtime component maintaining the state of all initializers for an effect.
580
///
581
/// This component is automatically added to the same [`Entity`] as the
582
/// [`ParticleEffect`] it's associated with, during [`tick_initializers()`], if
583
/// not already present on the entity. In that case, the initializer
584
/// configurations are cloned from the underlying [`EffectAsset`] associated
585
/// with the particle effect instance.
586
///
587
/// You can manually add this component in advance to override its [`Spawner`]s
588
/// and/or [`Cloner`]s. In that case [`tick_initializers()`] will use the
589
/// existing component you added.
590
///
591
/// Each frame, for spawners, the component will automatically calculate the
592
/// number of particles to spawn, via its internal [`Spawner`], and store it
593
/// into [`spawn_count`]. You can manually override that value if you want, to
594
/// create more complex spawning sequences. For cloners, the component sets the
595
/// [`clone_this_frame`] flag as appropriate. You can likewise manually override
596
/// that value if you want in order to clone on different schedules.
597
///
598
/// [`spawn_count`]: crate::EffectSpawner::spawn_count
599
/// [`clone_this_frame`]: crate::EffectCloner::clone_this_frame
600
#[derive(Default, Clone, Component, PartialEq, Reflect, Debug, Deref, DerefMut)]
601
#[reflect(Component)]
602
pub struct EffectInitializers(pub Vec<EffectInitializer>);
603

604
impl EffectInitializers {
605
    /// Resets the initializer state.
606
    ///
607
    /// This resets the internal time for all initializers to zero, and restarts
608
    /// any internal particle counters that they might possess.
609
    ///
610
    /// Use this, for example, to immediately spawn some particles in a spawner
611
    /// constructed with [`Spawner::once`].
612
    ///
613
    /// [`Spawner::once`]: crate::Spawner::once
614
    pub fn reset(&mut self) {
×
615
        for initializer in &mut self.0 {
×
616
            initializer.reset();
×
617
        }
618
    }
619

620
    /// Marks all initializers as either active or inactive.
621
    ///
622
    /// Inactive initializers don't spawn any particles.
623
    pub fn set_active(&mut self, active: bool) {
×
624
        for initializer in &mut self.0 {
×
625
            initializer.set_active(active);
×
626
        }
627
    }
628
}
629

630
/// Holds the runtime state for the initializer of a single particle group on a
631
/// particle effect.
632
#[derive(Clone, Copy, PartialEq, Reflect, Debug)]
633
pub enum EffectInitializer {
634
    /// The group uses a spawner.
635
    Spawner(EffectSpawner),
636
    /// The group uses a cloner (i.e. is a trail or ribbon).
637
    Cloner(EffectCloner),
638
}
639

640
impl EffectInitializer {
641
    /// If this initializer is a spawner, returns an immutable reference to it.
642
    pub fn get_spawner(&self) -> Option<&EffectSpawner> {
2✔
643
        match *self {
2✔
644
            EffectInitializer::Spawner(ref spawner) => Some(spawner),
2✔
645
            _ => None,
×
646
        }
647
    }
648

649
    /// Resets the initializer state.
650
    ///
651
    /// This resets the internal time for this initializer to zero, and
652
    /// restarts any internal particle counters that it might possess.
653
    ///
654
    /// Use this, for example, to immediately spawn some particles in a spawner
655
    /// constructed with [`Spawner::once`].
656
    ///
657
    /// [`Spawner::once`]: crate::Spawner::once
658
    pub fn reset(&mut self) {
×
659
        match self {
×
660
            EffectInitializer::Spawner(effect_spawner) => effect_spawner.reset(),
×
661
            EffectInitializer::Cloner(effect_cloner) => effect_cloner.reset(),
×
662
        }
663
    }
664

665
    /// Marks this initializer as either active or inactive.
666
    ///
667
    /// Inactive initializers don't spawn any particles.
668
    pub fn set_active(&mut self, active: bool) {
×
669
        match self {
×
670
            EffectInitializer::Spawner(effect_spawner) => effect_spawner.set_active(active),
×
671
            EffectInitializer::Cloner(effect_cloner) => effect_cloner.set_active(active),
×
672
        }
673
    }
674
}
675

676
/// Runtime structure maintaining the state of the spawner for a particle group.
677
#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
678
pub struct EffectSpawner {
679
    /// The spawner configuration extracted either from the [`EffectAsset`], or
680
    /// from any overriden value provided by the user on the [`ParticleEffect`].
681
    spawner: Spawner,
682

683
    /// Accumulated time since last spawn, in seconds.
684
    time: f32,
685

686
    /// Sampled value of `spawn_duration` until `period` is reached. This is the
687
    /// duration of the "active" period during which we spawn particles, as
688
    /// opposed to the "wait" period during which we do nothing until the next
689
    /// spawn cycle.
690
    spawn_duration: f32,
691

692
    /// Sampled value of the time period, in seconds, until the next spawn
693
    /// cycle.
694
    period: f32,
695

696
    /// Number of particles to spawn this frame.
697
    ///
698
    /// This value is normally updated by calling [`tick()`], which
699
    /// automatically happens once per frame when the [`tick_initializers()`]
700
    /// system runs in the [`PostUpdate`] schedule.
701
    ///
702
    /// You can manually assign this value to override the one calculated by
703
    /// [`tick()`]. Note in this case that you need to override the value after
704
    /// the automated one was calculated, by ordering your system
705
    /// after [`tick_initializers()`] or [`EffectSystems::TickSpawners`].
706
    ///
707
    /// [`tick()`]: crate::EffectSpawner::tick
708
    /// [`EffectSystems::TickSpawners`]: crate::EffectSystems::TickSpawners
709
    pub spawn_count: u32,
710

711
    /// Fractional remainder of particle count to spawn.
712
    ///
713
    /// This is accumulated each tick, and the integral part is added to
714
    /// `spawn_count`. The reminder gets saved for next frame.
715
    spawn_remainder: f32,
716

717
    /// Whether the spawner is active. Defaults to `true`. An inactive spawner
718
    /// doesn't tick (no particle spawned, no internal time updated).
719
    active: bool,
720
}
721

722
impl EffectSpawner {
723
    /// Create a new spawner state from a [`Spawner`].
724
    pub fn new(spawner: &Spawner) -> Self {
11✔
725
        Self {
726
            spawner: *spawner,
11✔
727
            time: if spawner.is_once() && !spawner.starts_immediately {
16✔
728
                1. // anything > 0
729
            } else {
730
                0.
731
            },
732
            spawn_duration: 0.,
733
            period: 0.,
734
            spawn_count: 0,
735
            spawn_remainder: 0.,
736
            active: spawner.starts_active(),
11✔
737
        }
738
    }
739

740
    /// Set whether the spawner is active.
741
    ///
742
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
743
    pub fn with_active(mut self, active: bool) -> Self {
×
744
        self.active = active;
×
745
        self
×
746
    }
747

748
    /// Set whether the spawner is active.
749
    ///
750
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
751
    pub fn set_active(&mut self, active: bool) {
4✔
752
        self.active = active;
4✔
753
    }
754

755
    /// Get whether the spawner is active.
756
    ///
757
    /// Inactive spawners do not tick, and therefore do not spawn any particle.
758
    pub fn is_active(&self) -> bool {
4✔
759
        self.active
4✔
760
    }
761

762
    /// Get the spawner configuration in use.
763
    ///
764
    /// The effective [`Spawner`] used is either the override specified in the
765
    /// associated [`ParticleEffect`] instance, or the fallback one specified in
766
    /// underlying [`EffectAsset`].
767
    pub fn spawner(&self) -> &Spawner {
×
768
        &self.spawner
×
769
    }
770

771
    /// Reset the spawner state.
772
    ///
773
    /// This resets the internal spawner time to zero, and restarts any internal
774
    /// particle counter.
775
    ///
776
    /// Use this, for example, to immediately spawn some particles in a spawner
777
    /// constructed with [`Spawner::once`].
778
    ///
779
    /// [`Spawner::once`]: crate::Spawner::once
780
    pub fn reset(&mut self) {
2✔
781
        self.time = 0.;
2✔
782
        self.period = 0.;
2✔
783
        self.spawn_count = 0;
2✔
784
        self.spawn_remainder = 0.;
2✔
785
    }
786

787
    /// Tick the spawner to calculate the number of particles to spawn this
788
    /// frame.
789
    ///
790
    /// The frame delta time `dt` is added to the current spawner time, before
791
    /// the spawner calculates the number of particles to spawn.
792
    ///
793
    /// This method is called automatically by [`tick_initializers()`] during
794
    /// the [`PostUpdate`], so you normally don't have to call it yourself
795
    /// manually.
796
    ///
797
    /// # Returns
798
    ///
799
    /// The integral number of particles to spawn this frame. Any fractional
800
    /// remainder is saved for the next call.
801
    pub fn tick(&mut self, mut dt: f32, rng: &mut Pcg32) -> u32 {
35✔
802
        if !self.active {
35✔
803
            self.spawn_count = 0;
3✔
804
            return 0;
3✔
805
        }
806

807
        // The limit can be reached multiple times, so use a loop
808
        loop {
809
            if self.period == 0.0 {
51✔
810
                self.resample(rng);
13✔
811
                continue;
13✔
812
            }
813

814
            let new_time = self.time + dt;
38✔
815
            if self.time <= self.spawn_duration {
38✔
816
                // If the spawn time is very small, close to zero, spawn all particles
817
                // immediately in one burst over a single frame.
818
                self.spawn_remainder += if self.spawn_duration < 1e-5f32.max(dt / 100.0) {
33✔
819
                    self.spawner.count.sample(rng)
9✔
820
                } else {
821
                    // Spawn an amount of particles equal to the fraction of time the current frame
822
                    // spans compared to the total burst duration.
823
                    self.spawner.count.sample(rng) * (new_time.min(self.spawn_duration) - self.time)
24✔
824
                        / self.spawn_duration
24✔
825
                };
826
            }
827

828
            let old_time = self.time;
829
            self.time = new_time;
830

831
            if self.time >= self.period {
6✔
832
                dt -= self.period - old_time;
6✔
833
                self.time = 0.0; // dt will be added on in the next iteration
6✔
834
                self.resample(rng);
6✔
835
            } else {
836
                break;
32✔
837
            }
838
        }
839

840
        let count = self.spawn_remainder.floor();
32✔
841
        self.spawn_remainder -= count;
32✔
842
        self.spawn_count = count as u32;
32✔
843

844
        self.spawn_count
32✔
845
    }
846

847
    /// Resamples the spawn time and period.
848
    fn resample(&mut self, rng: &mut Pcg32) {
19✔
849
        self.period = self.spawner.period.sample(rng);
19✔
850
        self.spawn_duration = self
19✔
851
            .spawner
19✔
852
            .spawn_duration
19✔
853
            .sample(rng)
19✔
854
            .clamp(0.0, self.period);
19✔
855
    }
856
}
857

858
/// A runtime structure maintaining the state of the cloner for a particle
859
/// group.
860
#[derive(Default, Clone, Copy, PartialEq, Reflect, Debug)]
861
pub struct EffectCloner {
862
    /// The cloner configuration extracted either from the [`EffectAsset`] or
863
    /// overridden manually.
864
    pub cloner: Cloner,
865
    /// Accumulated time since last clone, in seconds.
866
    time: f32,
867
    /// Sampled value of the time period, in seconds, until the next clone
868
    /// cycle.
869
    period: f32,
870
    /// The capacity of the group.
871
    capacity: u32,
872
    /// Whether the cloner is to clone any particle this frame.
873
    pub clone_this_frame: bool,
874
    /// Whether the cloner is active. Defaults to `true`.
875
    pub active: bool,
876
}
877

878
impl EffectCloner {
879
    pub(crate) fn new(cloner: Cloner, capacity: u32) -> EffectCloner {
×
880
        EffectCloner {
881
            cloner,
882
            time: 0.0,
883
            period: 0.0,
884
            capacity,
885
            clone_this_frame: false,
886
            active: cloner.starts_active(),
×
887
        }
888
    }
889

890
    /// Reset the cloner state.
891
    ///
892
    /// This resets the internal cloner time to zero, and restarts any internal
893
    /// particle counter.
894
    pub fn reset(&mut self) {
×
895
        self.time = 0.0;
×
896
        self.period = 0.0;
×
897
    }
898

899
    /// Tick the cloner and update [`clone_this_frame`] to trigger cloning.
900
    ///
901
    /// [`clone_this_frame`]: EffectCloner::clone_this_frame
902
    pub fn tick(&mut self, dt: f32, rng: &mut Pcg32) {
×
903
        if !self.active {
×
904
            self.clone_this_frame = false;
×
905
            return;
×
906
        }
907

908
        if self.period <= 0.0 {
×
909
            self.resample(rng);
×
910
        }
911

912
        let new_time = self.time + dt;
×
913
        self.time = new_time;
×
914

915
        self.clone_this_frame = self.time >= self.period;
×
916

917
        if self.clone_this_frame {
×
918
            self.time = 0.0;
×
919
            self.resample(rng);
×
920
        }
921
    }
922

923
    fn resample(&mut self, rng: &mut Pcg32) {
×
924
        self.period = self.cloner.period.sample(rng);
×
925
    }
926

927
    /// Marks this cloner as either active or inactive.
928
    ///
929
    /// Inactive cloners don't clone any particles.
930
    pub fn set_active(&mut self, active: bool) {
×
931
        self.active = active;
×
932
    }
933
}
934

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

972
    let dt = time.delta_secs();
3✔
973

974
    for (entity, effect, maybe_inherited_visibility, maybe_initializers) in query.iter_mut() {
3✔
975
        // TODO - maybe cache simulation_condition so we don't need to unconditionally
976
        // query the asset?
977
        let Some(asset) = effects.get(&effect.handle) else {
6✔
UNCOV
978
            continue;
×
979
        };
980

981
        if asset.simulation_condition == SimulationCondition::WhenVisible
982
            && !maybe_inherited_visibility
2✔
983
                .map(|iv| iv.get())
6✔
984
                .unwrap_or(true)
2✔
985
        {
986
            continue;
1✔
987
        }
988

989
        if let Some(mut initializers) = maybe_initializers {
×
990
            for initializer in &mut **initializers {
×
991
                match initializer {
×
992
                    EffectInitializer::Spawner(effect_spawner) => {
×
993
                        effect_spawner.tick(dt, &mut rng.0);
×
994
                    }
995
                    EffectInitializer::Cloner(effect_cloner) => {
×
996
                        effect_cloner.tick(dt, &mut rng.0);
×
997
                    }
998
                }
999
            }
1000
            continue;
×
1001
        }
1002

1003
        let initializers = asset
2✔
1004
            .init
2✔
1005
            .iter()
1006
            .enumerate()
1007
            .map(|(group_index, init)| match *init {
4✔
1008
                Initializer::Spawner(spawner) => {
2✔
1009
                    let mut effect_spawner = EffectSpawner::new(&spawner);
2✔
1010
                    effect_spawner.tick(dt, &mut rng.0);
2✔
1011
                    EffectInitializer::Spawner(effect_spawner)
2✔
1012
                }
1013
                Initializer::Cloner(cloner) => {
×
1014
                    let mut effect_cloner =
×
1015
                        EffectCloner::new(cloner, asset.capacities()[group_index]);
×
1016
                    effect_cloner.tick(dt, &mut rng.0);
×
1017
                    EffectInitializer::Cloner(effect_cloner)
×
1018
                }
1019
            })
1020
            .collect();
1021
        commands
1022
            .entity(entity)
1023
            .insert(EffectInitializers(initializers));
1024
    }
1025
}
1026

1027
#[cfg(test)]
1028
mod test {
1029
    use std::time::Duration;
1030

1031
    use bevy::{
1032
        asset::{
1033
            io::{
1034
                memory::{Dir, MemoryAssetReader},
1035
                AssetSourceBuilder, AssetSourceBuilders, AssetSourceId,
1036
            },
1037
            AssetServerMode,
1038
        },
1039
        render::view::{VisibilityPlugin, VisibilitySystems},
1040
        tasks::{IoTaskPool, TaskPoolBuilder},
1041
    };
1042

1043
    use super::*;
1044
    use crate::Module;
1045

1046
    /// Make an `EffectSpawner` wrapping a `Spawner`.
1047
    fn make_effect_spawner(spawner: Spawner) -> EffectSpawner {
1048
        EffectSpawner::new(
1049
            EffectAsset::new(256, spawner, Module::default()).init[0]
1050
                .get_spawner()
1051
                .expect("Expected the first group to have a spawner"),
1052
        )
1053
    }
1054

1055
    #[test]
1056
    fn test_range_single() {
1057
        let value = CpuValue::Single(1.0);
1058
        assert_eq!(value.range(), [1.0, 1.0]);
1059
    }
1060

1061
    #[test]
1062
    fn test_range_uniform() {
1063
        let value = CpuValue::Uniform((1.0, 3.0));
1064
        assert_eq!(value.range(), [1.0, 3.0]);
1065
    }
1066

1067
    #[test]
1068
    fn test_range_uniform_reverse() {
1069
        let value = CpuValue::Uniform((3.0, 1.0));
1070
        assert_eq!(value.range(), [1.0, 3.0]);
1071
    }
1072

1073
    #[test]
1074
    fn test_new() {
1075
        let rng = &mut new_rng();
1076
        // 3 particles over 3 seconds, pause 7 seconds (total 10 seconds period).
1077
        let spawner = Spawner::new(3.0.into(), 3.0.into(), 10.0.into());
1078
        let mut spawner = make_effect_spawner(spawner);
1079
        let count = spawner.tick(2.0, rng); // t = 2s
1080
        assert_eq!(count, 2);
1081
        let count = spawner.tick(5.0, rng); // t = 7s
1082
        assert_eq!(count, 1);
1083
        let count = spawner.tick(8.0, rng); // t = 15s
1084
        assert_eq!(count, 3);
1085
    }
1086

1087
    #[test]
1088
    #[should_panic]
1089
    fn test_new_panic_negative_period() {
1090
        let _ = Spawner::new(3.0.into(), 1.0.into(), CpuValue::Uniform((-1., 1.)));
1091
    }
1092

1093
    #[test]
1094
    #[should_panic]
1095
    fn test_new_panic_zero_period() {
1096
        let _ = Spawner::new(3.0.into(), 1.0.into(), CpuValue::Uniform((0., 0.)));
1097
    }
1098

1099
    #[test]
1100
    fn test_once() {
1101
        let rng = &mut new_rng();
1102
        let spawner = Spawner::once(5.0.into(), true);
1103
        assert!(spawner.is_once());
1104
        let mut spawner = make_effect_spawner(spawner);
1105
        let count = spawner.tick(0.001, rng);
1106
        assert_eq!(count, 5);
1107
        let count = spawner.tick(100.0, rng);
1108
        assert_eq!(count, 0);
1109
    }
1110

1111
    #[test]
1112
    fn test_once_reset() {
1113
        let rng = &mut new_rng();
1114
        let spawner = Spawner::once(5.0.into(), true);
1115
        assert!(spawner.is_once());
1116
        let mut spawner = make_effect_spawner(spawner);
1117
        spawner.tick(1.0, rng);
1118
        spawner.reset();
1119
        let count = spawner.tick(1.0, rng);
1120
        assert_eq!(count, 5);
1121
    }
1122

1123
    #[test]
1124
    fn test_once_not_immediate() {
1125
        let rng = &mut new_rng();
1126
        let spawner = Spawner::once(5.0.into(), false);
1127
        assert!(spawner.is_once());
1128
        let mut spawner = make_effect_spawner(spawner);
1129
        let count = spawner.tick(1.0, rng);
1130
        assert_eq!(count, 0);
1131
        spawner.reset();
1132
        let count = spawner.tick(1.0, rng);
1133
        assert_eq!(count, 5);
1134
    }
1135

1136
    #[test]
1137
    fn test_rate() {
1138
        let rng = &mut new_rng();
1139
        let spawner = Spawner::rate(5.0.into());
1140
        assert!(!spawner.is_once());
1141
        let mut spawner = make_effect_spawner(spawner);
1142
        // Slightly over 1.0 to avoid edge case
1143
        let count = spawner.tick(1.01, rng);
1144
        assert_eq!(count, 5);
1145
        let count = spawner.tick(0.4, rng);
1146
        assert_eq!(count, 2);
1147
    }
1148

1149
    #[test]
1150
    fn test_rate_active() {
1151
        let rng = &mut new_rng();
1152
        let spawner = Spawner::rate(5.0.into());
1153
        assert!(!spawner.is_once());
1154
        let mut spawner = make_effect_spawner(spawner);
1155
        spawner.tick(1.01, rng);
1156
        spawner.set_active(false);
1157
        assert!(!spawner.is_active());
1158
        let count = spawner.tick(0.4, rng);
1159
        assert_eq!(count, 0);
1160
        spawner.set_active(true);
1161
        assert!(spawner.is_active());
1162
        let count = spawner.tick(0.4, rng);
1163
        assert_eq!(count, 2);
1164
    }
1165

1166
    #[test]
1167
    fn test_rate_accumulate() {
1168
        let rng = &mut new_rng();
1169
        let spawner = Spawner::rate(5.0.into());
1170
        assert!(!spawner.is_once());
1171
        let mut spawner = make_effect_spawner(spawner);
1172
        // 13 ticks instead of 12 to avoid edge case
1173
        let count = (0..13).map(|_| spawner.tick(1.0 / 60.0, rng)).sum::<u32>();
1174
        assert_eq!(count, 1);
1175
    }
1176

1177
    #[test]
1178
    fn test_burst() {
1179
        let rng = &mut new_rng();
1180
        let spawner = Spawner::burst(5.0.into(), 2.0.into());
1181
        assert!(!spawner.is_once());
1182
        let mut spawner = make_effect_spawner(spawner);
1183
        let count = spawner.tick(1.0, rng);
1184
        assert_eq!(count, 5);
1185
        let count = spawner.tick(4.0, rng);
1186
        assert_eq!(count, 10);
1187
        let count = spawner.tick(0.1, rng);
1188
        assert_eq!(count, 0);
1189
    }
1190

1191
    #[test]
1192
    fn test_with_active() {
1193
        let rng = &mut new_rng();
1194
        let spawner = Spawner::rate(5.0.into()).with_starts_active(false);
1195
        let mut spawner = make_effect_spawner(spawner);
1196
        assert!(!spawner.is_active());
1197
        let count = spawner.tick(1., rng);
1198
        assert_eq!(count, 0);
1199
        spawner.set_active(false); // no-op
1200
        let count = spawner.tick(1., rng);
1201
        assert_eq!(count, 0);
1202
        spawner.set_active(true);
1203
        assert!(spawner.is_active());
1204
        let count = spawner.tick(1., rng);
1205
        assert_eq!(count, 5);
1206
    }
1207

1208
    fn make_test_app() -> App {
1209
        IoTaskPool::get_or_init(|| {
1210
            TaskPoolBuilder::default()
1211
                .num_threads(1)
1212
                .thread_name("Hanabi test IO Task Pool".to_string())
1213
                .build()
1214
        });
1215

1216
        let mut app = App::new();
1217

1218
        let watch_for_changes = false;
1219
        let mut builders = app
1220
            .world_mut()
1221
            .get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
1222
        let dir = Dir::default();
1223
        let dummy_builder = AssetSourceBuilder::default()
1224
            .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }));
1225
        builders.insert(AssetSourceId::Default, dummy_builder);
1226
        let sources = builders.build_sources(watch_for_changes, false);
1227
        let asset_server =
1228
            AssetServer::new(sources, AssetServerMode::Unprocessed, watch_for_changes);
1229

1230
        app.insert_resource(asset_server);
1231
        // app.add_plugins(DefaultPlugins);
1232
        app.init_asset::<Mesh>();
1233
        app.add_plugins(VisibilityPlugin);
1234
        app.init_resource::<Time<EffectSimulation>>();
1235
        app.insert_resource(Random(new_rng()));
1236
        app.init_asset::<EffectAsset>();
1237
        app.add_systems(
1238
            PostUpdate,
1239
            tick_initializers.after(VisibilitySystems::CheckVisibility),
1240
        );
1241

1242
        app
1243
    }
1244

1245
    /// Test case for `tick_initializers()`.
1246
    struct TestCase {
1247
        /// Initial entity visibility on spawn. If `None`, do not add a
1248
        /// [`Visibility`] component.
1249
        visibility: Option<Visibility>,
1250

1251
        /// Spawner assigned to the `EffectAsset`.
1252
        asset_spawner: Spawner,
1253
    }
1254

1255
    impl TestCase {
1256
        fn new(visibility: Option<Visibility>, asset_spawner: Spawner) -> Self {
1257
            Self {
1258
                visibility,
1259
                asset_spawner,
1260
            }
1261
        }
1262
    }
1263

1264
    #[test]
1265
    fn test_tick_spawners() {
1266
        let asset_spawner = Spawner::once(32.0.into(), true);
1267

1268
        for test_case in &[
1269
            TestCase::new(None, asset_spawner),
1270
            TestCase::new(Some(Visibility::Hidden), asset_spawner),
1271
            TestCase::new(Some(Visibility::Visible), asset_spawner),
1272
        ] {
1273
            let mut app = make_test_app();
1274

1275
            let (effect_entity, handle) = {
1276
                let world = app.world_mut();
1277

1278
                // Add effect asset
1279
                let mut assets = world.resource_mut::<Assets<EffectAsset>>();
1280
                let mut asset = EffectAsset::new(64, test_case.asset_spawner, Module::default());
1281
                asset.simulation_condition = if test_case.visibility.is_some() {
1282
                    SimulationCondition::WhenVisible
1283
                } else {
1284
                    SimulationCondition::Always
1285
                };
1286
                let handle = assets.add(asset);
1287

1288
                // Spawn particle effect
1289
                let entity = if let Some(visibility) = test_case.visibility {
1290
                    world
1291
                        .spawn((
1292
                            visibility,
1293
                            InheritedVisibility::default(),
1294
                            ParticleEffect {
1295
                                handle: handle.clone(),
1296
                                #[cfg(feature = "2d")]
1297
                                z_layer_2d: None,
1298
                            },
1299
                        ))
1300
                        .id()
1301
                } else {
1302
                    world
1303
                        .spawn((ParticleEffect {
1304
                            handle: handle.clone(),
1305
                            #[cfg(feature = "2d")]
1306
                            z_layer_2d: None,
1307
                        },))
1308
                        .id()
1309
                };
1310

1311
                // Spawn a camera, otherwise ComputedVisibility stays at HIDDEN
1312
                world.spawn(Camera3d::default());
1313

1314
                (entity, handle)
1315
            };
1316

1317
            // Tick once
1318
            let cur_time = {
1319
                // Make sure to increment the current time so that the spawners spawn something.
1320
                // Note that `Time` has this weird behavior where the common quantities like
1321
                // `Time::delta_secs()` only update after the *second* update. So we tick the
1322
                // `Time` twice here to enforce this.
1323
                let mut time = app.world_mut().resource_mut::<Time<EffectSimulation>>();
1324
                time.advance_by(Duration::from_millis(16));
1325
                time.elapsed()
1326
            };
1327
            app.update();
1328

1329
            let world = app.world_mut();
1330

1331
            // Check the state of the components after `tick_initializers()` ran
1332
            if let Some(test_visibility) = test_case.visibility {
1333
                // Simulated-when-visible effect (SimulationCondition::WhenVisible)
1334

1335
                let (entity, visibility, inherited_visibility, particle_effect, effect_spawners) =
1336
                    world
1337
                        .query::<(
1338
                            Entity,
1339
                            &Visibility,
1340
                            &InheritedVisibility,
1341
                            &ParticleEffect,
1342
                            Option<&EffectInitializers>,
1343
                        )>()
1344
                        .iter(world)
1345
                        .next()
1346
                        .unwrap();
1347
                assert_eq!(entity, effect_entity);
1348
                assert_eq!(visibility, test_visibility);
1349
                assert_eq!(
1350
                    inherited_visibility.get(),
1351
                    test_visibility == Visibility::Visible
1352
                );
1353
                assert_eq!(particle_effect.handle, handle);
1354
                if inherited_visibility.get() {
1355
                    // If visible, `tick_initializers()` spawns the EffectSpawner and ticks it
1356
                    assert!(effect_spawners.is_some());
1357
                    let effect_spawner = effect_spawners.unwrap()[0].get_spawner().unwrap();
1358
                    let actual_spawner = effect_spawner.spawner;
1359

1360
                    // Check the spawner ticked
1361
                    assert!(effect_spawner.active);
1362
                    assert_eq!(effect_spawner.spawn_remainder, 0.);
1363
                    assert_eq!(effect_spawner.time, cur_time.as_secs_f32());
1364

1365
                    assert_eq!(actual_spawner, test_case.asset_spawner);
1366
                    assert_eq!(effect_spawner.spawn_count, 32);
1367
                } else {
1368
                    // If not visible, `tick_initializers()` skips the effect entirely so won't
1369
                    // spawn an `EffectSpawner` for it
1370
                    assert!(effect_spawners.is_none());
1371
                }
1372
            } else {
1373
                // Always-simulated effect (SimulationCondition::Always)
1374

1375
                let (entity, particle_effect, effect_spawners) = world
1376
                    .query::<(Entity, &ParticleEffect, Option<&EffectInitializers>)>()
1377
                    .iter(world)
1378
                    .next()
1379
                    .unwrap();
1380
                assert_eq!(entity, effect_entity);
1381
                assert_eq!(particle_effect.handle, handle);
1382

1383
                assert!(effect_spawners.is_some());
1384
                let effect_spawner = effect_spawners.unwrap()[0].get_spawner().unwrap();
1385
                let actual_spawner = effect_spawner.spawner;
1386

1387
                // Check the spawner ticked
1388
                assert!(effect_spawner.active);
1389
                assert_eq!(effect_spawner.spawn_remainder, 0.);
1390
                assert_eq!(effect_spawner.time, cur_time.as_secs_f32());
1391

1392
                assert_eq!(actual_spawner, test_case.asset_spawner);
1393
                assert_eq!(effect_spawner.spawn_count, 32);
1394
            }
1395
        }
1396
    }
1397
}
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