Skip to main content

concinnity_core/components/
spawner.rs

1// src/components/spawner.rs
2//
3// The `Spawner` asset: the authored args a world declares, and the runtime
4// component (with its spawn accumulator) they bake into.
5
6use crate::ecs::Component;
7use crate::ecs::asset_id::AssetId;
8
9/// Authored fields of a `Spawner`; the runtime accumulator is not declared.
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
11#[serde(default)]
12pub struct SpawnerArgs {
13    /// Name of the placement to copy on each spawn.
14    pub template: AssetId,
15    /// Seconds between spawns.
16    pub interval: f32,
17    /// Seconds each spawned copy lives before auto-removal; 0 keeps it forever.
18    pub lifetime: f32,
19}
20
21impl Default for SpawnerArgs {
22    fn default() -> Self {
23        Self {
24            template: AssetId::default(),
25            interval: 1.0,
26            lifetime: 0.0,
27        }
28    }
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn a_blank_spawner_ticks_once_a_second_and_never_expires_its_copies() {
37        let s = SpawnerArgs::default();
38        assert_eq!(s.interval, 1.0);
39        // Zero lifetime means the copy lives until something despawns it.
40        assert_eq!(s.lifetime, 0.0);
41        assert_eq!(s.template, AssetId::default());
42    }
43
44    #[test]
45    fn an_authored_spawner_parses_and_round_trips_through_postcard() {
46        crate::test_support::install_resolvers();
47        let s: SpawnerArgs =
48            serde_json::from_str(r#"{"template":"spark","interval":0.25,"lifetime":3}"#).unwrap();
49        assert_eq!(s.template, AssetId(5));
50        assert_eq!(s.interval, 0.25);
51
52        let bytes = postcard::to_allocvec(&s).unwrap();
53        let back: SpawnerArgs = postcard::from_bytes(&bytes).unwrap();
54        assert_eq!(back.template, AssetId(5));
55        assert_eq!(back.lifetime, 3.0);
56    }
57}
58
59/// Periodically instantiates copies of an existing placement at this entity's
60/// position.
61///
62/// A spawner clones `template` (the name of another placement in the world)
63/// every `interval` seconds, giving each copy a `lifetime` after which it is
64/// automatically removed. Pairing a short lifetime with a short interval keeps a
65/// bounded population churning (an enemy wave, a particle of debris, a fountain
66/// of props) and is what exercises GPU draw-slot recycling: each expiry frees a
67/// slot the next spawn reuses.
68///
69/// The spawner's own `Transform` (its position) is where copies appear, so place
70/// the spawner where you want the stream to originate.
71#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
72pub struct Spawner {
73    /// Name of the placement to copy on each spawn.
74    pub template: AssetId,
75    /// Seconds between spawns.
76    pub interval: f32,
77    /// Seconds each spawned copy lives before auto-removal; 0 keeps it forever.
78    pub lifetime: f32,
79    /// Runtime: seconds accumulated toward the next spawn.
80    pub elapsed: f32,
81    /// Runtime: number of copies spawned so far.
82    pub count: u32,
83}
84
85impl Spawner {
86    /// Translate the authored args into the runtime spawner: clamp the timing
87    /// knobs and zero the runtime counters. Run by cook at build time (the
88    /// baked blob record carries the result).
89    pub fn bake(args: SpawnerArgs) -> Self {
90        Self {
91            template: args.template,
92            interval: args.interval.max(0.0),
93            lifetime: args.lifetime.max(0.0),
94            elapsed: 0.0,
95            count: 0,
96        }
97    }
98}
99
100impl Component for Spawner {
101    const NAME: &'static str = "Spawner";
102
103    fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
104        Ok(crate::blob::decode_exact(bytes)?)
105    }
106}