unlit3dx_examples/scene/balls.rs
1//! The balls: dropping them, giving each one a render instance and a physics
2//! body, and publishing where they are.
3//!
4//! This is the scene's busiest object. Every drop spawns a `Ball` entity of its
5//! own and then asks the two other worlds for the halves that make it visible:
6//! the render world creates an instance, the physics world a body, and both
7//! answer with the ball entity they were asked for, so an answer that no longer
8//! finds its ball is cleaned up instead of leaking. A ball the camera loses
9//! sight of is put away whole: the render world despawns the instance and
10//! reports it, and the body and the ball entity go with it.
11//!
12//! Every answer names the ball, never a place in a parallel list, so two
13//! answers arriving in either order land on the same ball. A ball counts as
14//! shown once the render world confirms the instance wears its assets, not
15//! when the job is queued: a job a full queue turned away is simply sent
16//! again on the next tick.
17
18use std::sync::Arc;
19
20use glam::Vec3;
21use unlit_ecs::{Bundle, DynamicBundle, Entity, Scope, World, bundle};
22use unlit3d::components::Transform;
23use unlit3dx::prelude::*;
24
25use super::Scene;
26use super::shapes::{BALL_COLORS, SHAPE_KINDS, shape_body};
27use super::shared::{SceneState, create_instance, look};
28use super::tick::advancing_in;
29use super::{GamePhysics, ScenePoses, pose_job, world_ended};
30use crate::render::{BallInstance, RenderEntity, despawn_instances_job, show, sweep};
31
32/// How long between automatic ball drops, in seconds.
33const SPAWN_PERIOD: f32 = 0.7;
34
35/// How long a ball may wait for its two halves before its instance and body
36/// are reclaimed, in seconds.
37const PENDING_TIMEOUT: f32 = 5.0;
38
39/// How often live instances are tested against the camera's view, in seconds.
40const SWEEP_PERIOD: f32 = 0.5;
41
42/// How often the ball flow reports its own status, in seconds.
43const STATUS_PERIOD: f32 = 1.0;
44
45/// Where balls spawn, in metres above the ground.
46const SPAWN_HEIGHT: f32 = 7.0;
47
48/// How far from the centre a ball may spawn, in metres.
49const SPAWN_SPREAD: f32 = 3.0;
50
51/// How far a ball has come through its flow.
52///
53/// One field rather than one flag per step, because the steps are exclusive: a
54/// ball waiting for its show is not drawn, and a ball that is drawn is not
55/// waiting for one, while two flags could claim both at once.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum BallPhase {
58 /// A half is still on its way.
59 Waiting,
60 /// Both halves are here and a show is on its way or unanswered.
61 Requested,
62 /// The render world confirmed the instance wears its assets.
63 Shown,
64}
65
66/// One ball, on an entity of its own in the gameplay world.
67///
68/// The entity is the ball's identity: every answer names it, so a ball needs
69/// no id of its own and no table to be looked up in. A ball still waiting for a
70/// half has `None` where that half will be, and it counts as drawn only
71/// once the render world has confirmed it: see [`BallPhase`].
72#[derive(Debug)]
73pub struct Ball {
74 /// Which shape it is, an index into `SHAPE_KINDS`.
75 pub kind: usize,
76 /// Which color it is, an index into `BALL_COLORS`.
77 pub color: usize,
78 /// The render instance the render world created for it.
79 pub instance: Option<RenderEntity>,
80 /// The body the physics world spawned for it.
81 pub body: Option<RigidBodyHandle>,
82 /// How far the ball has come: see [`BallPhase`].
83 pub phase: BallPhase,
84 /// Where the ball starts, which is where its instance is shown.
85 pub transform: Transform,
86}
87
88/// A ball whose two halves have both landed, waiting to be shown.
89struct ShowCandidate {
90 /// The ball entity.
91 ball: Entity,
92 /// The shape kind, an index into `SceneAssets::meshes`.
93 kind: usize,
94 /// The color, an index into `BALL_COLORS`.
95 color: usize,
96 /// The instance to point at the assets and show.
97 instance: RenderEntity,
98 /// The transform to show it at.
99 transform: Transform,
100}
101
102/// What a tick does with a ball that has not been shown yet.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104enum Waiting {
105 /// A half is still on its way, or the show is already queued.
106 Wait,
107 /// Both halves are here and no show is queued: show this instance.
108 ///
109 /// The instance travels with the decision, so a ball that reaches this
110 /// branch is known to have both halves and needs no second look.
111 Show(RenderEntity),
112 /// It waited too long, or a world that owed a half ended: reclaim it.
113 Reclaim,
114}
115
116/// The balls, living on their own entity in the gameplay world.
117///
118/// A body's pose is authoritative in the physics world and drawn by the render
119/// world, so this object is the one place the two meet: it reads the handles
120/// the snapshot names off the ball entities, and publishes the resulting poses
121/// as a job.
122///
123/// It reaches the other worlds through the `Scene` its tick is handed, so it
124/// keeps no channel of its own: what it does keep is the answers waiting to be
125/// folded into the balls they name, and the buffers those answers land in, so
126/// a tick that folds nothing allocates nothing but the poses it sends.
127pub struct Balls {
128 /// The bodies the physics world owes: one request per ball, asked under
129 /// the ball it is for.
130 bodies: Pending<Entity, RigidBodyHandle>,
131 /// The instances the render world owes: one request per ball, asked under
132 /// the ball it is for.
133 instances: Pending<Entity, RenderEntity>,
134 /// The shows the render world owes: one request per ball, asked under the
135 /// ball whose instance is being shown. What it answers is whether the
136 /// instance really wears its assets.
137 shows: Pending<Entity, bool>,
138 /// What a sweep swept away: one request per sweep, asked under no key at
139 /// all, because what it names is the whole scene rather than one ball.
140 retired: Pending<(), Vec<BallInstance>>,
141 /// Where the answers land, reused across ticks.
142 settled_bodies: Vec<(Entity, Settled<RigidBodyHandle>)>,
143 settled_instances: Vec<(Entity, Settled<RenderEntity>)>,
144 settled_shows: Vec<(Entity, Settled<bool>)>,
145 settled_retired: Vec<((), Settled<Vec<BallInstance>>)>,
146 /// The balls a request was given up on this tick, and has to reclaim.
147 expired: Vec<Entity>,
148 /// Whether a sweep is still owed an answer: a second sweep asked before
149 /// the first answered would have its answer dropped, and with it the
150 /// instances the first one retired.
151 sweep_asked: bool,
152 /// How many drops have been made, picking each ball's look. The counter
153 /// only orders the looks: a ball is found by its entity, not by it.
154 drops: u32,
155 /// A clear requested but not yet consumed by the tick.
156 clear: bool,
157 /// The snapshot the previous tick published, to skip repeated work.
158 last_snapshot: Option<Arc<GamePhysics>>,
159 /// Ball drops requested but not yet consumed by the tick.
160 requests: u32,
161 /// Seconds since the previous automatic drop.
162 spawn_elapsed: f32,
163 /// Seconds since the previous view sweep.
164 sweep_elapsed: f32,
165 /// Seconds since the previous status line.
166 status_elapsed: f32,
167 /// Whether the spawn cadence drops balls without a panel request.
168 pub automatic_drops: bool,
169 /// Pose entries the last job carried, for the status line.
170 poses_sent: usize,
171 /// The balls the tick has decided to show, reused across ticks.
172 showing: Vec<ShowCandidate>,
173 /// The instances a sweep tests against the camera's view, reused across
174 /// sweeps.
175 sweeping: Vec<BallInstance>,
176 /// The bodies a tick has to send away, kept until a send goes through so
177 /// a full queue only delays the cleanup.
178 despawns: Vec<RigidBodyHandle>,
179 /// The instances that have nobody left to stand for, kept the same way.
180 orphans: Vec<RenderEntity>,
181 /// The xorshift state for spawn placement.
182 rng: u64,
183}
184
185impl Balls {
186 /// The balls, driving the worlds their tick is handed.
187 #[must_use]
188 pub fn new(scene: &Scene) -> Self {
189 Self {
190 // Each of the three is bounded by the same wait: a half that has
191 // not come within it is given up on, and so is a show the render
192 // world never answered.
193 bodies: Pending::new(scene.physics_state.clone(), Some(PENDING_TIMEOUT)),
194 instances: Pending::new(scene.render_state.clone(), Some(PENDING_TIMEOUT)),
195 shows: Pending::new(scene.render_state.clone(), Some(PENDING_TIMEOUT)),
196 retired: Pending::new(scene.render_state.clone(), Some(PENDING_TIMEOUT)),
197 settled_bodies: Vec::new(),
198 settled_instances: Vec::new(),
199 settled_shows: Vec::new(),
200 settled_retired: Vec::new(),
201 expired: Vec::new(),
202 sweep_asked: false,
203 drops: 0,
204 clear: false,
205 last_snapshot: None,
206 requests: 0,
207 spawn_elapsed: 0.0,
208 sweep_elapsed: 0.0,
209 status_elapsed: 0.0,
210 automatic_drops: true,
211 poses_sent: 0,
212 showing: Vec::new(),
213 sweeping: Vec::new(),
214 despawns: Vec::new(),
215 orphans: Vec::new(),
216 rng: 0x853c49e6748fea9b,
217 }
218 }
219
220 /// Ask for one more ball on the next tick.
221 pub fn queue_drop(&mut self) {
222 self.requests += 1;
223 }
224
225 /// Ask the next tick to put every ball away.
226 pub fn queue_clear(&mut self) {
227 self.clear = true;
228 }
229
230 /// The next symmetric random float in -1..1.
231 fn random_signed(&mut self) -> f32 {
232 self.rng ^= self.rng << 13;
233 self.rng ^= self.rng >> 7;
234 self.rng ^= self.rng << 17;
235 (self.rng >> 40) as f32 / (1u64 << 24) as f32 - 1.0
236 }
237
238 /// Spawn one ball and ask the render and physics worlds for its halves.
239 ///
240 /// The look picks the collider shape as well as the mesh and material, so
241 /// both worlds agree on what the drop is. The ball entity is queued first:
242 /// it is what both answers name, so neither half can arrive for a ball that
243 /// does not exist.
244 ///
245 /// A world that refuses the request outright — its queue is full, or it has
246 /// stopped — costs this drop and no more: the ball is despawned before any
247 /// answer can name it.
248 fn drop_ball(&mut self, world: &World, state: Entity, scene: &Scene) {
249 let (kind, color) = look(self.drops as usize);
250 self.drops = self.drops.wrapping_add(1);
251
252 let selected = world
253 .root_scope()
254 .query_one::<&SceneState>(state)
255 .ok()
256 .and_then(|mut state| {
257 state.get().loaded.as_ref().map(|assets| {
258 (
259 assets.meshes[kind],
260 assets.materials[kind * BALL_COLORS.len() + color],
261 )
262 })
263 });
264 let Some((mesh, material)) = selected else {
265 return;
266 };
267
268 let position = Vec3::new(
269 self.random_signed() * SPAWN_SPREAD,
270 SPAWN_HEIGHT,
271 self.random_signed() * SPAWN_SPREAD,
272 );
273 let velocity = Vec3::new(self.random_signed() * 2.0, 0.0, self.random_signed() * 2.0);
274
275 // The handle comes back before the entity exists: commands land at the
276 // end of this world's dispatch, and no tick reads a ball before the
277 // next one, by which time it does.
278 let commands = world.root_scope().queue();
279 let ball = commands.spawn(bundle![Ball {
280 kind,
281 color,
282 instance: None,
283 body: None,
284 phase: BallPhase::Waiting,
285 transform: Transform {
286 translation: position,
287 ..Default::default()
288 },
289 }]);
290
291 // Both halves are asked for under the ball: the answer names it, and
292 // the wait is bounded, so a ball that hears nothing is reclaimed
293 // rather left waiting forever.
294 if let Err(error) =
295 self.instances
296 .request(&scene.render, ball, create_instance(mesh, material))
297 {
298 log::warn!("the render world refused an instance request ({error:?})");
299 commands.despawn(ball);
300 return;
301 }
302
303 let description = BodyDesc {
304 position,
305 velocity,
306 restitution: 0.65,
307 shape: shape_body(SHAPE_KINDS[kind]),
308 };
309 if let Err(error) = self
310 .bodies
311 .request(&scene.physics, ball, move |world: &mut World| {
312 spawn_body(world, description)
313 })
314 {
315 log::warn!("the physics world refused a body request ({error:?})");
316 // The instance already asked for has no ball to be shown on; its
317 // answer despawns it when it arrives.
318 commands.despawn(ball);
319 return;
320 }
321 log::debug!("a ball was dropped at {position:?}");
322 }
323
324 /// Put every ball away, bodies and instances included.
325 ///
326 /// The instances are despawned rather than kept hidden: one the camera
327 /// cannot see has no reason to stay. The music is left alone — the panel
328 /// drives it, not the balls. A ball whose halves are still in flight is
329 /// gone by the time they arrive, so they are cleaned up as orphans.
330 fn clear_balls(&mut self, world: &World, scene: &Scene) {
331 let scope = world.root_scope();
332 let mut cleared = 0;
333 for (ball, entity) in scope.query::<(&mut Ball, Entity)>().iter() {
334 // Taken rather than read: the halves are sent away, and a ball
335 // that is gone must not leave them to be sent a second time.
336 if let Some(handle) = ball.body.take() {
337 self.despawns.push(handle);
338 }
339 if let Some(instance) = ball.instance.take() {
340 self.orphans.push(instance);
341 }
342 scope.queue().despawn(entity);
343 cleared += 1;
344 }
345 // A clear also cancels the drops the panel asked for but the tick has
346 // not spent yet: they would repopulate the scene immediately.
347 self.requests = 0;
348 self.last_snapshot = None;
349 self.poses_sent = 0;
350 self.despawn_bodies(scene);
351 self.despawn_instances(scene);
352 log::info!("the balls were cleared: {cleared} despawned");
353 }
354
355 /// Fold the answers the other worlds sent since the last tick into the
356 /// balls they name, show the ones that now have both halves, and put away
357 /// the ones that waited too long.
358 ///
359 /// Every wait is bounded by the same timeout, so this is the one place a
360 /// request is given up on: an answer that never came names its ball, and
361 /// the ball is reclaimed with whatever halves it did receive.
362 fn fold_answers(&mut self, world: &World, state: Entity, scene: &Scene, delta_seconds: f32) {
363 let scope = world.root_scope();
364
365 // A ball whose request was given up on loses the halves it has, and
366 // the ones still in flight are dropped when they arrive: nothing is
367 // left to fill them into.
368 self.expired.clear();
369
370 // A retired instance is already gone from the render world: only its
371 // body and its ball entity are left to despawn.
372 self.retired
373 .advance(delta_seconds, &mut self.settled_retired);
374 for (_, answer) in self.settled_retired.drain(..) {
375 let Settled::Answered(answers) = answer else {
376 // A sweep that never answered leaves the instances it was
377 // asked about in the render world rather than retiring them
378 // twice: the next sweep names them again.
379 continue;
380 };
381 for answer in answers {
382 if let Some(handle) = scope
383 .query_one::<&Ball>(answer.ball)
384 .ok()
385 .and_then(|mut found| found.get().body)
386 {
387 self.despawns.push(handle);
388 }
389 scope.queue().despawn(answer.ball);
390 }
391 }
392
393 // An instance whose ball is already gone has nobody to be shown on,
394 // so it is despawned rather than leaked.
395 self.instances
396 .advance(delta_seconds, &mut self.settled_instances);
397 for (ball, answer) in self.settled_instances.drain(..) {
398 match (scope.query_one::<&mut Ball>(ball), answer) {
399 (Ok(mut found), Settled::Answered(instance)) => {
400 found.get().instance = Some(instance);
401 }
402 (Err(_), Settled::Answered(instance)) => self.orphans.push(instance),
403 (_, Settled::Expired) => self.expired.push(ball),
404 }
405 }
406
407 self.bodies.advance(delta_seconds, &mut self.settled_bodies);
408 for (ball, answer) in self.settled_bodies.drain(..) {
409 match (scope.query_one::<&mut Ball>(ball), answer) {
410 (Ok(mut found), Settled::Answered(handle)) => {
411 found.get().body = Some(handle);
412 }
413 (Err(_), Settled::Answered(handle)) => self.despawns.push(handle),
414 (_, Settled::Expired) => self.expired.push(ball),
415 }
416 }
417
418 // The show answers once the instance really wears its assets, so a
419 // show the render world never answered leaves the ball waiting instead
420 // of marking it shown.
421 self.shows.advance(delta_seconds, &mut self.settled_shows);
422 for (ball, answer) in self.settled_shows.drain(..) {
423 match (scope.query_one::<&mut Ball>(ball), answer) {
424 (Ok(mut found), Settled::Answered(true)) => {
425 found.get().phase = BallPhase::Shown;
426 }
427 // A show answered no, or a ball that went away while its show
428 // was in flight: the instance it wore has nobody left to
429 // stand for.
430 (Ok(mut found), Settled::Answered(false)) | (Ok(mut found), Settled::Expired) => {
431 if let Some(instance) = found.get().instance.take() {
432 self.orphans.push(instance);
433 }
434 self.expired.push(ball);
435 }
436 (Err(_), _) => self.expired.push(ball),
437 }
438 }
439
440 // The balls that are ready to show, or that have waited past the point
441 // of being worth waiting for.
442 let stale = world_ended(&scene.physics_state) || world_ended(&scene.render_state);
443 let mut showing = std::mem::take(&mut self.showing);
444 showing.clear();
445 for (ball, entity) in scope.query::<(&mut Ball, Entity)>().iter() {
446 match waiting(ball, entity, &self.expired, stale) {
447 Waiting::Wait => {}
448 Waiting::Reclaim => {
449 // A reclaim is the one way a ball leaves without anyone
450 // asking, so it says so: a silent one looks like a ball
451 // that vanished.
452 log::debug!(
453 "reclaiming a ball that was {} ({:?})",
454 match ball.phase {
455 BallPhase::Waiting => "waiting for a half",
456 BallPhase::Requested => "waiting for its show",
457 BallPhase::Shown => "shown",
458 },
459 ball.kind,
460 );
461 // The halves are taken as they are sent away, so a clear
462 // later in this tick cannot send them a second time.
463 if let Some(handle) = ball.body.take() {
464 self.despawns.push(handle);
465 }
466 if let Some(instance) = ball.instance.take() {
467 self.orphans.push(instance);
468 }
469 scope.queue().despawn(entity);
470 }
471 Waiting::Show(instance) => {
472 showing.push(ShowCandidate {
473 ball: entity,
474 kind: ball.kind,
475 color: ball.color,
476 instance,
477 transform: ball.transform.clone(),
478 });
479 }
480 }
481 }
482
483 // The assets are read once for every ball that is ready to show.
484 let assets = scope
485 .query_one::<&SceneState>(state)
486 .ok()
487 .and_then(|mut state| state.get().loaded.clone());
488 if let Some(assets) = assets {
489 for candidate in showing.drain(..) {
490 let mesh = assets.meshes[candidate.kind];
491 let material =
492 assets.materials[candidate.kind * BALL_COLORS.len() + candidate.color];
493 let instance = candidate.instance;
494 let transform = candidate.transform;
495 let sent =
496 self.shows
497 .request(&scene.render, candidate.ball, move |world: &mut World| {
498 show(world, instance, mesh, material, &transform)
499 });
500 match sent {
501 Ok(()) => {
502 if let Ok(mut ball) = scope.query_one::<&mut Ball>(candidate.ball) {
503 ball.get().phase = BallPhase::Requested;
504 }
505 }
506 // A closed queue is the scene going away, not a fault.
507 Err(SendError::Closed) => {}
508 // Backpressure: the candidate is collected again next
509 // tick, so the ball is not given up on.
510 Err(SendError::Full) => {
511 log::debug!("the render world's queue is full; a show waits a tick");
512 }
513 }
514 }
515 }
516 self.showing = showing;
517
518 self.despawn_bodies(scene);
519 self.despawn_instances(scene);
520 }
521
522 /// Despawn the bodies the tick has collected, in one job.
523 ///
524 /// A despawn is idempotent, so the list is kept until a send goes through:
525 /// a full queue delays the cleanup by a tick instead of losing it.
526 fn despawn_bodies(&mut self, scene: &Scene) {
527 if self.despawns.is_empty() {
528 return;
529 }
530 // The list is cloned rather than handed away: a job the queue cannot
531 // take would go with its buffer, and the cleanup would be lost.
532 let handles = self.despawns.clone();
533 let sent = scene.physics.send(Box::new(move |world: &mut World| {
534 for handle in handles {
535 let _ = despawn_body(world, handle);
536 }
537 }));
538 match sent {
539 Ok(()) | Err(SendError::Closed) => self.despawns.clear(),
540 Err(SendError::Full) => {
541 log::debug!("the physics world's queue is full; a despawn waits a tick");
542 }
543 }
544 }
545
546 /// Despawn the instances the tick has collected, in one job.
547 fn despawn_instances(&mut self, scene: &Scene) {
548 if self.orphans.is_empty() {
549 return;
550 }
551 let instances = self.orphans.clone();
552 match scene.render.send(despawn_instances_job(instances)) {
553 Ok(()) | Err(SendError::Closed) => self.orphans.clear(),
554 Err(SendError::Full) => {
555 log::debug!("the render world's queue is full; a despawn waits a tick");
556 }
557 }
558 }
559
560 /// Ask the render world to despawn the shown instances the camera can no
561 /// longer see. Each answer names the ball it belonged to, so the body and
562 /// the ball entity go with the instance.
563 ///
564 /// The sweep repeats every `SWEEP_PERIOD` seconds, so a ball the camera
565 /// loses is put away within one period, and one whose sweep met a full
566 /// queue within two.
567 fn sweep_outside_view(&mut self, world: &World, scene: &Scene) {
568 let mut sweeping = std::mem::take(&mut self.sweeping);
569 sweeping.clear();
570 for (ball, entity) in world.root_scope().query::<(&Ball, Entity)>().iter() {
571 // Only a ball the render world confirmed is out there to be seen:
572 // one still waiting for its show has no instance to lose.
573 if ball.phase != BallPhase::Shown {
574 continue;
575 }
576 if let Some(instance) = ball.instance {
577 sweeping.push(BallInstance {
578 ball: entity,
579 instance,
580 });
581 }
582 }
583 if sweeping.is_empty() {
584 self.sweeping = sweeping;
585 return;
586 }
587 // One sweep at a time: a second one asked before the first answered
588 // would be dropped on the floor, and with it the instances the first
589 // retired.
590 if self.sweep_asked {
591 self.sweeping = sweeping;
592 return;
593 }
594 match self
595 .retired
596 .request(&scene.render, (), sweep(sweeping.clone()))
597 {
598 Ok(()) => {
599 self.sweep_asked = true;
600 sweeping.clear();
601 }
602 // A closed queue is the scene going away: nothing is left to
603 // sweep, and the instances stay for the next one.
604 Err(SendError::Closed) => {}
605 Err(SendError::Full) => {
606 log::debug!("the render world's queue is full; a sweep waits for the next one");
607 }
608 }
609 self.sweeping = sweeping;
610 }
611
612 /// Publish the snapshot's poses onto the balls they name.
613 fn publish(&mut self, world: &World, scene: &Scene) {
614 let Some(snapshot) = scene.poses.latest() else {
615 return;
616 };
617 if self
618 .last_snapshot
619 .as_ref()
620 .is_some_and(|last| Arc::ptr_eq(last, &snapshot))
621 {
622 return;
623 }
624 self.last_snapshot = Some(snapshot.clone());
625
626 let mut poses = Vec::with_capacity(snapshot.transforms.len());
627 live_poses(&world.root_scope(), &snapshot, &mut poses);
628
629 // One status line a second: the ball flow passes through the answers,
630 // the ball entities and the physics snapshot, and a stall in any of
631 // the three hides every ball. It keeps its own clock, so the line
632 // stays once a second whatever the drop cadence is.
633 if self.status_elapsed >= STATUS_PERIOD {
634 self.status_elapsed %= STATUS_PERIOD;
635 log::debug!(
636 "shown balls: {}; snapshot bodies: {}; posed last tick: {}",
637 world
638 .root_scope()
639 .query::<&Ball>()
640 .iter()
641 .filter(|ball| ball.phase == BallPhase::Shown)
642 .count(),
643 snapshot.transforms.len(),
644 self.poses_sent,
645 );
646 }
647
648 self.poses_sent = poses.len();
649 match scene.render.send(pose_job(ScenePoses(poses))) {
650 Ok(()) => {}
651 // A closed queue is the scene going away, not a fault: the render
652 // world stops before the gameplay world's last tick, and a pose
653 // sent after it has nothing left to draw.
654 Err(SendError::Closed) => {}
655 // The poses are remembered as published only once they are on
656 // their way: a full queue forgets the snapshot, so the same one
657 // is posed again next tick instead of being skipped as unchanged.
658 Err(SendError::Full) => {
659 self.last_snapshot = None;
660 log::debug!("the render world's queue is full; the poses wait for the next one");
661 }
662 }
663 }
664}
665
666/// The balls and the tick that drops, poses and sweeps them, spawned together.
667///
668/// The balls hold no world of their own: every instance, body and pose is
669/// asked of the worlds the tick is handed, so a state spawned without its
670/// tick would hold streams nobody reads.
671///
672/// The tick reads the scene's assets, which the build in the default band
673/// writes, so it states [`BALLS_ORDER`](super::BALLS_ORDER): two behaviours in
674/// one band have no order between them, and the read would find no assets.
675pub(super) struct BallDrops {
676 /// The state the tick advances.
677 balls: Balls,
678 /// The worlds the balls ask their instances, bodies and poses from.
679 scene: Scene,
680}
681
682impl Bundle for BallDrops {
683 fn into_bundle(self) -> DynamicBundle {
684 let Self { balls, scene } = self;
685 bundle![
686 balls,
687 advancing_in(super::BALLS_ORDER, move |balls, world, frame| Self::tick(
688 balls,
689 world,
690 &scene,
691 frame.delta_time
692 )),
693 ]
694 }
695}
696
697impl BallDrops {
698 /// The balls for the worlds `scene` reaches.
699 #[must_use]
700 pub(super) fn new(scene: &Scene, automatic_drops: bool) -> Self {
701 let mut balls = Balls::new(scene);
702 balls.automatic_drops = automatic_drops;
703 Self {
704 balls,
705 scene: scene.clone(),
706 }
707 }
708
709 /// Advance the balls by one tick: fold the answers, drop, pose and sweep.
710 fn tick(balls: &mut Balls, world: &World, scene: &Scene, delta_seconds: f32) {
711 balls.spawn_elapsed += delta_seconds;
712 balls.sweep_elapsed += delta_seconds;
713 balls.status_elapsed += delta_seconds;
714 let Some((state, ready)) = world
715 .root_scope()
716 .query::<(&SceneState, Entity)>()
717 .iter()
718 .next()
719 .map(|(state, entity)| (entity, state.ready))
720 else {
721 return;
722 };
723 if !ready {
724 return;
725 }
726
727 // The answers land before a clear, never after: a clear despawns the
728 // balls it finds, so an answer naming one of them has to fill it in
729 // first. Folded the other way round, the clear would queue the
730 // despawn and the fold would still find the ball, show it, and leave
731 // its instance behind when the entity went away.
732 balls.fold_answers(world, state, scene, delta_seconds);
733 if core::mem::take(&mut balls.clear) {
734 balls.clear_balls(world, scene);
735 }
736
737 // Manual requests first, then the automatic cadence. This runs before
738 // the pose publish below: a drop is a request to the physics world,
739 // not a reaction to its output.
740 while balls.requests > 0 {
741 balls.requests -= 1;
742 balls.drop_ball(world, state, scene);
743 }
744 if balls.automatic_drops && balls.spawn_elapsed >= SPAWN_PERIOD {
745 balls.spawn_elapsed %= SPAWN_PERIOD;
746 balls.drop_ball(world, state, scene);
747 }
748
749 balls.publish(world, scene);
750
751 if balls.sweep_elapsed >= SWEEP_PERIOD {
752 balls.sweep_elapsed %= SWEEP_PERIOD;
753 balls.sweep_outside_view(world, scene);
754 }
755 }
756}
757
758/// What a tick does with a ball.
759///
760/// A ball is reclaimed when one of the requests it is waiting on was given up
761/// on — it waited out `PENDING_TIMEOUT`, or a world that owed it an
762/// answer ended — and when the world it wears an instance in ended, because
763/// the instance went with that world.
764///
765/// The wait is measured by the requests themselves, not by the tick the ball
766/// was dropped on: a ball the render world confirmed owes nothing, and no
767/// number of ticks takes it away.
768fn waiting(ball: &Ball, entity: Entity, expired: &[Entity], stale: bool) -> Waiting {
769 if stale {
770 return Waiting::Reclaim;
771 }
772 if expired.contains(&entity) {
773 return Waiting::Reclaim;
774 }
775 if ball.phase == BallPhase::Shown {
776 return Waiting::Wait;
777 }
778 // A ball whose show is on its way is left alone: showing it twice would
779 // send a second job for the same instance.
780 if ball.phase == BallPhase::Requested {
781 return Waiting::Wait;
782 }
783 match (ball.instance, ball.body) {
784 (Some(instance), Some(_)) => Waiting::Show(instance),
785 // A half is still on its way, so there is nothing to show yet.
786 _ => Waiting::Wait,
787 }
788}
789
790/// The poses a tick publishes: one per shown ball, at the pose the physics
791/// snapshot names for its body. A ball the snapshot has not named yet is left
792/// out, keeping the transform the show job gave it.
793///
794/// A pose names the ball's render instance, not the ball entity: the job that
795/// carries it runs in the render world, whose entities are its own. A ball
796/// entity from this world happens to share a number with some render entity,
797/// and writing a pose onto that one moves whatever it is.
798///
799/// A ball whose show job is queued counts as posed: both jobs travel the same
800/// queue in the order they were sent, so the show has put the instance in
801/// place before the pose reads it. Waiting for the show's answer instead would
802/// cost the ball a tick of movement, and pose it one frame behind the drop.
803///
804/// The poses are the one buffer a tick does not keep: the job that carries
805/// them runs once in the render world and answers nothing, so the `Vec`
806/// moved into it never comes back. Everything else a tick reuses is a field.
807fn live_poses(
808 scope: &Scope<'_>,
809 snapshot: &GamePhysics,
810 poses: &mut Vec<(RenderEntity, Transform)>,
811) {
812 for ball in scope.query::<&Ball>().iter() {
813 if ball.phase == BallPhase::Waiting {
814 continue;
815 }
816 let (Some(handle), Some(instance)) = (ball.body, ball.instance) else {
817 continue;
818 };
819 let Some((translation, rotation)) = snapshot.transforms.get(&handle) else {
820 continue;
821 };
822 poses.push((
823 instance,
824 Transform {
825 translation: *translation,
826 rotation: *rotation,
827 ..Default::default()
828 },
829 ));
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::super::shared::SceneAssets;
836 use super::*;
837 use glam::Quat;
838
839 fn ball(instance: Option<RenderEntity>, body: Option<RigidBodyHandle>, shown: bool) -> Ball {
840 Ball {
841 kind: 0,
842 color: 0,
843 instance,
844 body,
845 phase: if shown {
846 BallPhase::Shown
847 } else {
848 BallPhase::Waiting
849 },
850 transform: Transform::default(),
851 }
852 }
853
854 /// Only a shown ball with a body the snapshot names is posed: a ball still
855 /// waiting for a half, or one the physics world has not reported yet, keeps
856 /// the transform the show job gave it.
857 #[test]
858 fn only_shown_balls_are_posed() {
859 let mut world = World::new();
860 let handle = RigidBodyHandle::from_raw_parts(7, 0);
861 let waiting_body = RigidBodyHandle::from_raw_parts(8, 0);
862 let instance = RenderEntity(Entity::from_raw(10, 0));
863 {
864 let mut scope = world.root_scope_mut();
865 scope.spawn(bundle![ball(Some(instance), Some(handle), true)]);
866 }
867 {
868 let mut scope = world.root_scope_mut();
869 scope.spawn(bundle![ball(
870 Some(RenderEntity(Entity::from_raw(11, 0))),
871 Some(waiting_body),
872 false
873 )]);
874 scope.spawn(bundle![ball(
875 Some(RenderEntity(Entity::from_raw(12, 0))),
876 None,
877 true
878 )]);
879 }
880
881 let mut snapshot = GamePhysics::default();
882 snapshot
883 .transforms
884 .insert(handle, (Vec3::new(1.0, 2.0, 3.0), Quat::IDENTITY));
885 snapshot
886 .transforms
887 .insert(waiting_body, (Vec3::ZERO, Quat::IDENTITY));
888
889 let mut poses = Vec::new();
890 live_poses(&world.root_scope(), &snapshot, &mut poses);
891
892 assert_eq!(poses.len(), 1, "only a shown ball with a body is posed");
893 assert_eq!(poses[0].1.translation, Vec3::new(1.0, 2.0, 3.0));
894 }
895
896 /// A pose names the ball's render instance: the job that carries it runs in
897 /// the render world, where a ball entity from this world means something
898 /// else entirely.
899 #[test]
900 fn a_pose_names_the_instance_not_the_ball() {
901 let mut world = World::new();
902 let handle = RigidBodyHandle::from_raw_parts(7, 0);
903 let instance = RenderEntity(Entity::from_raw(10, 0));
904 let ball = {
905 let mut scope = world.root_scope_mut();
906 scope.spawn(bundle![ball(Some(instance), Some(handle), true)])
907 };
908 assert_ne!(
909 RenderEntity(ball),
910 instance,
911 "the fixture must not let them coincide"
912 );
913
914 let mut snapshot = GamePhysics::default();
915 snapshot
916 .transforms
917 .insert(handle, (Vec3::new(1.0, 2.0, 3.0), Quat::IDENTITY));
918
919 let mut poses = Vec::new();
920 live_poses(&world.root_scope(), &snapshot, &mut poses);
921
922 assert_eq!(poses.len(), 1);
923 assert_eq!(poses[0].0, instance, "the pose names the render instance");
924 assert_ne!(
925 poses[0].0,
926 RenderEntity(ball),
927 "the pose must not name the ball entity"
928 );
929 }
930
931 /// A ball waits while a half is missing, is shown once both are in, and is
932 /// reclaimed when a request it is waiting on was given up on.
933 #[test]
934 fn a_ball_waits_shows_and_is_reclaimed() {
935 let entity = Entity::from_raw(1, 0);
936 let instance = RenderEntity(Entity::from_raw(10, 0));
937 let body = RigidBodyHandle::from_raw_parts(7, 0);
938
939 let missing_instance = ball(None, Some(body), false);
940 assert_eq!(
941 waiting(&missing_instance, entity, &[], false),
942 Waiting::Wait
943 );
944 assert_eq!(
945 waiting(&missing_instance, entity, &[entity], false),
946 Waiting::Reclaim,
947 "an instance that was given up on takes the ball with it"
948 );
949
950 let ready = ball(Some(instance), Some(body), false);
951 assert_eq!(waiting(&ready, entity, &[], false), Waiting::Show(instance));
952 assert_eq!(waiting(&ready, entity, &[], true), Waiting::Reclaim);
953
954 let requested = Ball {
955 phase: BallPhase::Requested,
956 ..ball(Some(instance), Some(body), false)
957 };
958 assert_eq!(waiting(&requested, entity, &[], false), Waiting::Wait);
959 assert_eq!(
960 waiting(&requested, entity, &[entity], false),
961 Waiting::Reclaim,
962 "a show nobody answered must not keep the ball alive forever"
963 );
964 }
965
966 /// A shown ball owes no request, so only the world it wears an instance in
967 /// ending takes it away: a wait measured against the tick the ball was
968 /// dropped on would reclaim it a fixed time after it landed.
969 #[test]
970 fn a_shown_ball_is_only_taken_by_its_world_ending() {
971 let entity = Entity::from_raw(1, 0);
972 let instance = RenderEntity(Entity::from_raw(10, 0));
973 let body = RigidBodyHandle::from_raw_parts(7, 0);
974 let shown = ball(Some(instance), Some(body), true);
975 assert_eq!(waiting(&shown, entity, &[], false), Waiting::Wait);
976 assert_eq!(
977 waiting(&shown, entity, &[], true),
978 Waiting::Reclaim,
979 "a world that ended takes the instance the ball wears with it"
980 );
981 }
982
983 /// A scene whose queues a test runs itself to answer the requests on them,
984 /// and fills again to keep whatever a tick sends away in the lists, where
985 /// the test can count it.
986 fn stalled_scene() -> (
987 Scene,
988 async_channel::Receiver<Job>,
989 async_channel::Receiver<Job>,
990 ) {
991 let (render_tx, render_rx) = async_channel::bounded(1);
992 let (physics_tx, physics_rx) = async_channel::bounded(1);
993 let (audio_tx, _audio_rx) = async_channel::bounded(1);
994 let scene = Scene {
995 render: JobSender::new(render_tx),
996 physics: JobSender::new(physics_tx),
997 audio: JobSender::new(audio_tx),
998 poses: Snapshot::new().reader(),
999 render_state: Snapshot::new().reader(),
1000 physics_state: Snapshot::new().reader(),
1001 audio_state: Snapshot::new().reader(),
1002 };
1003 (scene, render_rx, physics_rx)
1004 }
1005
1006 /// Run every job waiting on a queue, so the answer it publishes is there
1007 /// for the next tick to fold. The world those jobs are for does not exist
1008 /// in a test, so each runs against an empty world of its own.
1009 fn answer(jobs: &async_channel::Receiver<Job>) {
1010 let mut other = World::new();
1011 while let Ok(job) = jobs.try_recv() {
1012 job(&mut other);
1013 }
1014 }
1015
1016 /// Fill every queue, so a send a tick makes on the way out reports `Full`
1017 /// and the cleanup stays in the lists, where a test can count it.
1018 fn stall(scene: &Scene) {
1019 for queue in [&scene.render, &scene.physics, &scene.audio] {
1020 let _ = queue.send(Box::new(|_: &mut World| {}));
1021 }
1022 }
1023
1024 /// A scene state whose assets are loaded, so a tick does not turn back at
1025 /// the door: no render world builds them in a test.
1026 fn ready_scene_state(world: &mut World, scene: &Scene, jobs: &async_channel::Receiver<Job>) {
1027 let entity = world
1028 .root_scope_mut()
1029 .spawn(bundle![SceneState::new(scene)]);
1030 // The asset build has no render world to run in, so the job it sent is
1031 // dropped rather than run: the assets the tick needs are put in by
1032 // hand.
1033 while jobs.try_recv().is_ok() {}
1034 let Ok(mut state) = world.root_scope().query_one::<&mut SceneState>(entity) else {
1035 panic!("the state was just spawned")
1036 };
1037 let state = state.get();
1038 state.ready = true;
1039 state.loaded = Some(SceneAssets {
1040 meshes: vec![RenderEntity(Entity::from_raw(1, 0))],
1041 materials: vec![RenderEntity(Entity::from_raw(2, 0)); BALL_COLORS.len()],
1042 });
1043 }
1044
1045 /// The answers land before the clear, so a clear takes a ball away only
1046 /// once: the halves it just received are sent away exactly once, not once
1047 /// by the reclaim and once again by the clear.
1048 #[test]
1049 fn a_clear_takes_the_halves_a_fold_just_landed() {
1050 let mut world = World::new();
1051 let (scene, render, physics) = stalled_scene();
1052 ready_scene_state(&mut world, &scene, &render);
1053 let instance = RenderEntity(Entity::from_raw(10, 0));
1054 let body = RigidBodyHandle::from_raw_parts(7, 0);
1055 let entity = world
1056 .root_scope_mut()
1057 .spawn(bundle![ball(None, None, false)]);
1058 let mut balls = Balls::new(&scene);
1059 // Both halves are asked of the worlds the tick is handed and answered
1060 // by running what they were sent, so the fold finds them in flight
1061 // rather than published by hand.
1062 balls
1063 .instances
1064 .request(&scene.render, entity, move |_: &mut World| instance)
1065 .expect("the queue was drained");
1066 balls
1067 .bodies
1068 .request(&scene.physics, entity, move |_: &mut World| body)
1069 .expect("the queue was drained");
1070 answer(&render);
1071 answer(&physics);
1072 stall(&scene);
1073 balls.clear = true;
1074
1075 BallDrops::tick(&mut balls, &world, &scene, FRAME_SECONDS);
1076 let _ = world.apply();
1077
1078 assert_eq!(
1079 world.root_scope().query::<&Ball>().iter().count(),
1080 0,
1081 "the clear takes the ball the answers landed on"
1082 );
1083 assert_eq!(
1084 balls.despawns,
1085 vec![body],
1086 "the body the fold landed is sent away once"
1087 );
1088 assert_eq!(
1089 balls.orphans,
1090 vec![instance],
1091 "and so is the instance it was shown on"
1092 );
1093 }
1094
1095 /// An answer whose ball is already gone is cleaned up: an instance with
1096 /// nobody to be shown on is despawned rather than left in the scene.
1097 #[test]
1098 fn an_answer_without_its_ball_is_cleaned_up() {
1099 let mut world = World::new();
1100 let (scene, render, physics) = stalled_scene();
1101 ready_scene_state(&mut world, &scene, &render);
1102 let instance = RenderEntity(Entity::from_raw(10, 0));
1103 let body = RigidBodyHandle::from_raw_parts(7, 0);
1104 let gone = world
1105 .root_scope_mut()
1106 .spawn(bundle![ball(None, None, false)]);
1107 world
1108 .root_scope_mut()
1109 .despawn(gone)
1110 .expect("the ball the answers are about is gone");
1111 let _ = world.apply();
1112
1113 let mut balls = Balls::new(&scene);
1114 balls
1115 .instances
1116 .request(&scene.render, gone, move |_: &mut World| instance)
1117 .expect("the queue was drained");
1118 balls
1119 .bodies
1120 .request(&scene.physics, gone, move |_: &mut World| body)
1121 .expect("the queue was drained");
1122 answer(&render);
1123 answer(&physics);
1124 stall(&scene);
1125 BallDrops::tick(&mut balls, &world, &scene, FRAME_SECONDS);
1126
1127 assert_eq!(
1128 balls.orphans,
1129 vec![instance],
1130 "an instance whose ball is gone is despawned"
1131 );
1132 assert_eq!(
1133 balls.despawns,
1134 vec![body],
1135 "and so is the body that answered for it"
1136 );
1137 }
1138}