Skip to main content

unlit3dx_examples/scene/
shared.rs

1//! The scene's assets, built once in the render world.
2//!
3//! The render world allocates the meshes and materials in one job and
4//! publishes them. Every shape a drop needs is its own render instance, asked
5//! for when the drop happens; an instance the camera can no longer see is
6//! despawned by the render world, so nothing is kept hidden for reuse.
7
8use unlit_ecs::bundle;
9use unlit_ecs::{Bundle, DynamicBundle, World};
10use unlit3d::components::Transform;
11use unlit3dx::prelude::*;
12
13use super::HIDDEN_POSITION;
14use super::Scene;
15use super::shapes::{BALL_COLORS, PATTERN_SIZE, SHAPE_KINDS, pattern_texture, shape_mesh};
16use super::tick::advancing;
17use crate::render::{
18    RenderEntity, allocate_mesh, allocate_rgba_material, hidden_instance_of, linear_sampler,
19};
20
21/// The render entities a shape is built from, in kind-major order.
22///
23/// The build job allocates them in the render world and publishes them here;
24/// the gameplay world names them when it asks for an instance. They are
25/// [`RenderEntity`]s: the world that built them and the world that names
26/// them are two, so a bare entity is not enough to say which.
27#[derive(Clone, Debug)]
28pub struct SceneAssets {
29    /// One mesh per [`SHAPE_KINDS`] entry, in that order.
30    pub meshes: Vec<RenderEntity>,
31    /// One material per shape kind and color, kind-major:
32    /// `kind * BALL_COLORS.len() + color`.
33    pub materials: Vec<RenderEntity>,
34}
35
36/// The scene's assets, on their own entity in the gameplay world.
37///
38/// It owns the render entities every shape is built from, and whether the
39/// build has landed. It does not own instances: a drop asks the render world
40/// for one, and the render world despawns it once the camera loses sight of
41/// it.
42pub struct SceneState {
43    /// The build the render world owes, and its answer once it comes.
44    ///
45    /// The whole build is one request, so it is asked under no key at all:
46    /// what it names is nothing but itself.
47    build: Pending<(), SceneAssets>,
48    /// Where the answer lands, reused across ticks.
49    settled: Vec<((), Settled<SceneAssets>)>,
50    /// The assets once the build landed, for the jobs that name them.
51    pub loaded: Option<SceneAssets>,
52    /// Whether the build landed and the scene is live.
53    pub ready: bool,
54    /// Whether the wait is over: the build landed, or the render world ended
55    /// before it answered.
56    given_up: bool,
57}
58
59impl SceneState {
60    /// Ask the render world for the scene's assets.
61    ///
62    /// The whole build is one question there: it allocates the meshes and the
63    /// materials, and the answer is their entities. Until it lands the scene
64    /// is empty, and every object that needs a shape stays unspawned.
65    #[must_use]
66    pub fn new(scene: &Scene) -> Self {
67        log::info!("the gameplay world started; asking the render world for the scene");
68        let mut build = Pending::new(scene.render_state.clone(), None);
69        // A build that never lands is never retried: the scene is the whole
70        // of it, so the wait runs until the render world answers or ends.
71        let given_up = match build.request(&scene.render, (), build_assets) {
72            Ok(()) => false,
73            Err(error) => {
74                log::error!(
75                    "the render world refused the scene build ({error:?}); the scene stays empty"
76                );
77                // The request was never accepted, so no answer is coming:
78                // the wait is over before it starts.
79                true
80            }
81        };
82        Self {
83            build,
84            settled: Vec::new(),
85            loaded: None,
86            ready: false,
87            given_up,
88        }
89    }
90}
91
92/// The scene's assets and the tick that collects them.
93///
94/// The build is asked once, when the state is built, but its answer is folded
95/// on a tick: a state spawned without its tick would wait for an answer
96/// nobody collects, and a tick without its state has nothing to advance.
97pub(super) struct SceneBuild {
98    /// The state the tick advances.
99    state: SceneState,
100}
101
102impl SceneBuild {
103    /// The build of `scene`, ready to spawn.
104    #[must_use]
105    pub(super) fn new(scene: &Scene) -> Self {
106        Self {
107            state: SceneState::new(scene),
108        }
109    }
110
111    /// Collect the assets once the build job has published them.
112    ///
113    /// The scene stays empty if the render world refused the job, and it stops
114    /// waiting if the world that owed the answer is gone: a job that panicked
115    /// takes its answer with it, and no reader will ever see one.
116    fn tick(state: &mut SceneState) {
117        if state.given_up {
118            return;
119        }
120        let mut settled = core::mem::take(&mut state.settled);
121        state.build.advance(0.0, &mut settled);
122        for (_, settled) in settled.drain(..) {
123            match settled {
124                Settled::Answered(assets) => {
125                    log::info!(
126                        "the scene is live: {} meshes, {} materials",
127                        assets.meshes.len(),
128                        assets.materials.len()
129                    );
130                    state.loaded = Some(assets);
131                    state.ready = true;
132                    state.given_up = true;
133                }
134                // The wait runs until the world answers or ends, so this is
135                // the world ending: a job a panic took never builds a scene.
136                Settled::Expired => {
137                    log::error!(
138                        "the render world ended before it built the scene; the scene stays empty"
139                    );
140                    state.given_up = true;
141                }
142            }
143        }
144        state.settled = settled;
145    }
146}
147
148impl Bundle for SceneBuild {
149    fn into_bundle(self) -> DynamicBundle {
150        let Self { state } = self;
151        bundle![
152            state,
153            advancing(move |state, _world, _frame| Self::tick(state)),
154        ]
155    }
156}
157
158/// Allocate the scene's meshes and materials, and answer their entities.
159///
160/// One step, so nothing waits for an answer in between.
161fn build_assets(world: &mut World) -> SceneAssets {
162    let mut meshes = Vec::with_capacity(SHAPE_KINDS.len());
163    for kind in SHAPE_KINDS {
164        meshes.push(allocate_mesh(world, &shape_mesh(kind)));
165    }
166    // One patterned material per shape kind and color, kind-major.
167    let mut materials = Vec::with_capacity(SHAPE_KINDS.len() * BALL_COLORS.len());
168    for kind in SHAPE_KINDS {
169        for color in BALL_COLORS {
170            materials.push(allocate_rgba_material(
171                world,
172                (PATTERN_SIZE, PATTERN_SIZE),
173                &pattern_texture(kind, color),
174                linear_sampler(),
175            ));
176        }
177    }
178    SceneAssets { meshes, materials }
179}
180
181/// Spawn one hidden instance wearing the assets it names, and answer it.
182///
183/// It stays hidden until its body is ready and the show puts it in place.
184pub fn create_instance(
185    mesh: RenderEntity,
186    material: RenderEntity,
187) -> impl FnOnce(&mut World) -> RenderEntity {
188    move |world: &mut World| {
189        hidden_instance_of(
190            world,
191            mesh,
192            material,
193            Transform {
194                translation: HIDDEN_POSITION,
195                ..Default::default()
196            },
197        )
198    }
199}
200
201/// The kind and color a drop wears, as indices into [`SceneAssets`].
202///
203/// The palette of every shape kind and color runs from its last entry down,
204/// so a drop picks the last look first and cycles through the rest.
205#[must_use]
206pub fn look(index: usize) -> (usize, usize) {
207    let looks = SHAPE_KINDS.len() * BALL_COLORS.len();
208    let palette = looks - 1 - index % looks;
209    (
210        palette % SHAPE_KINDS.len(),
211        (palette + palette / SHAPE_KINDS.len()) % BALL_COLORS.len(),
212    )
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    /// One cycle of the palette covers every shape and color exactly once, so
220    /// a scene that keeps dropping shapes never runs out of looks.
221    #[test]
222    fn looks_cover_the_palette() {
223        let looks = SHAPE_KINDS.len() * BALL_COLORS.len();
224        let mut seen = vec![false; looks];
225        for index in 0..looks {
226            let (kind, color) = look(index);
227            assert!(kind < SHAPE_KINDS.len());
228            assert!(color < BALL_COLORS.len());
229            let at = kind * BALL_COLORS.len() + color;
230            assert!(!seen[at], "look {at} repeats at index {index}");
231            seen[at] = true;
232        }
233        assert!(seen.iter().all(|seen| *seen));
234    }
235}