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}