Skip to main content

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}