@@ -22,6 +22,7 @@ mod walk;
2222mod world;
2323
2424use ai_driver:: AiDriver ;
25+ use bevy:: ecs:: resource:: IsResource ;
2526use bevy:: mesh:: { Indices , PrimitiveTopology } ;
2627use bevy:: pbr:: { DistanceFog , FogFalloff } ;
2728use bevy:: picking:: mesh_picking:: { MeshPickingCamera , MeshPickingPlugin , MeshPickingSettings } ;
@@ -264,7 +265,12 @@ fn main() {
264265 GameState :: Menu
265266 } )
266267 . add_systems ( Startup , log_mods)
267- . add_systems ( OnEnter ( GameState :: Menu ) , menu:: spawn_menu)
268+ // The world the last run built goes first — otherwise the next `setup` would put a
269+ // second one on top of it.
270+ . add_systems (
271+ OnEnter ( GameState :: Menu ) ,
272+ ( tear_down_run, menu:: spawn_menu) . chain ( ) ,
273+ )
268274 // The same menu, as an overlay over the standing world.
269275 . add_systems ( OnEnter ( GameState :: Paused ) , menu:: spawn_pause)
270276 . add_systems (
@@ -279,7 +285,13 @@ fn main() {
279285 // creates when its commands are applied — the chain inserts that sync point.
280286 . add_systems (
281287 OnEnter ( GameState :: Driving ) ,
282- ( setup, audio:: setup_audio, displays:: setup_displays) . chain ( ) ,
288+ (
289+ remember_before_run,
290+ setup,
291+ audio:: setup_audio,
292+ displays:: setup_displays,
293+ )
294+ . chain ( ) ,
283295 )
284296 . add_systems (
285297 Update ,
@@ -400,6 +412,73 @@ fn exit_after_frames(
400412 }
401413}
402414
415+ /// Everything that was alive before the run was built: the window, the picking pointers,
416+ /// the menu, the cloud dome. What is not in here is the run.
417+ #[ derive( Resource ) ]
418+ struct BeforeRun ( std:: collections:: HashSet < Entity > ) ;
419+
420+ /// What a run may have built, and nothing a despawn has any business touching:
421+ ///
422+ /// * a **resource** is an entity of its own in Bevy 0.19, and despawning one would
423+ /// *remove* the resource — the run's are replaced by the next `setup`, and the rest
424+ /// are not the run's to take;
425+ /// * an **observer** belongs to no world at all — Bevy drops the ones whose watched
426+ /// entity has gone and keeps the rest, which is exactly right here;
427+ /// * [`world_render::Persistent`] is what a plugin puts up once at startup, and a run it
428+ /// was never part of must not be able to take it down.
429+ type RunRoots < ' w , ' s > = Query <
430+ ' w ,
431+ ' s ,
432+ Entity ,
433+ (
434+ Without < ChildOf > ,
435+ Without < world_render:: Persistent > ,
436+ Without < IsResource > ,
437+ Without < Observer > ,
438+ ) ,
439+ > ;
440+
441+ /// Takes that snapshot — chained in front of `setup`, so it sees the world without one.
442+ fn remember_before_run ( mut commands : Commands , entities : Query < Entity > ) {
443+ commands. insert_resource ( BeforeRun ( entities. iter ( ) . collect ( ) ) ) ;
444+ }
445+
446+ /// Drops the built world when the player leaves a run for the title screen, so the next
447+ /// `setup` builds into an empty world rather than beside the old one.
448+ ///
449+ /// Roots only — `despawn` takes the children with it — and only the ones [`RunRoots`]
450+ /// lets through. An entity id carries a generation, so an id the run reused for something
451+ /// of its own is a *different* [`Entity`] than the one remembered and is dropped as it
452+ /// should be.
453+ fn tear_down_run (
454+ mut commands : Commands ,
455+ before : Option < Res < BeforeRun > > ,
456+ roots : RunRoots ,
457+ mixer : Option < ResMut < audio:: Audio > > ,
458+ mut walker : ResMut < walk:: Walker > ,
459+ mut camera : ResMut < ui:: CameraState > ,
460+ ) {
461+ // No run has been built yet — this is the title screen the program starts on.
462+ let Some ( before) = before else {
463+ return ;
464+ } ;
465+ for entity in & roots {
466+ if !before. 0 . contains ( & entity) {
467+ commands. entity ( entity) . despawn ( ) ;
468+ }
469+ }
470+ // A mixer track outlives the entity it followed: dropping the tracks is what stops
471+ // the loops of a run the player has just left.
472+ if let Some ( mut mixer) = mixer {
473+ mixer. silence ( ) ;
474+ }
475+ // Both of these point into the world that has just gone: the walker at a vehicle or
476+ // at a place on the earth, the camera at a wayside spot beside it.
477+ * walker = default ( ) ;
478+ * camera = default ( ) ;
479+ commands. remove_resource :: < BeforeRun > ( ) ;
480+ }
481+
403482/// Esc during a run raises the pause overlay, which also holds the settings. Leaving it
404483/// again is the overlay's own job — this system only runs while `Driving`, so the Esc that
405484/// resumes cannot bounce straight back into the pause.
@@ -655,7 +734,12 @@ fn setup(
655734 ) ;
656735 let streamer = streaming:: TerrainStreamer :: new (
657736 terrain_builder,
658- render:: terrain_material ( & mut images, & mut terrain_materials, season) ,
737+ render:: terrain_material (
738+ & mut images,
739+ & mut terrain_materials,
740+ season,
741+ settings:: ground_quality ( & graphics) ,
742+ ) ,
659743 tree_catalog,
660744 f64:: from ( graphics. view_distance ) ,
661745 ) ;
@@ -696,7 +780,10 @@ fn setup(
696780 models:: spawn ( & mut commands, & assets, entity, & view, & file) ;
697781 entity
698782 } else {
699- let mesh = meshes. add ( Cuboid :: new ( 3.0 , 3.8 , v. spec . length as f32 ) ) ;
783+ let mesh = meshes. add (
784+ Mesh :: from ( Cuboid :: new ( 3.0 , 3.8 , v. spec . length as f32 ) )
785+ . translated_by ( Vec3 :: Y * 2.2 ) ,
786+ ) ;
700787 commands
701788 . spawn ( (
702789 Mesh3d ( mesh) ,
@@ -733,17 +820,17 @@ fn setup(
733820 outer_angle : 0.32 ,
734821 ..default ( )
735822 } ,
736- // The vehicle origin sits 2.2 m above the rail; the lamp
737- // below it at the end, aimed a touch onto the track.
738- Transform :: from_xyz ( 0.0 , - 0 .6, end)
823+ // Buffer height above the rail, at the end of the vehicle,
824+ // aimed a touch onto the track.
825+ Transform :: from_xyz ( 0.0 , 1 .6, end)
739826 . looking_to ( Vec3 :: new ( 0.0 , -0.06 , dir) . normalize ( ) , Vec3 :: Y ) ,
740827 ) ) ;
741828 for x in [ -1.0 , 1.0 ] {
742829 parent. spawn ( (
743830 TailLamp { train, reverse } ,
744831 Mesh3d ( tail_mesh. clone ( ) ) ,
745832 MeshMaterial3d ( tail_material. clone ( ) ) ,
746- Transform :: from_xyz ( x, - 0 .6, end) ,
833+ Transform :: from_xyz ( x, 1 .6, end) ,
747834 Visibility :: Hidden ,
748835 ) ) ;
749836 }
@@ -760,7 +847,7 @@ fn setup(
760847 range : 4.0 ,
761848 ..default ( )
762849 } ,
763- Transform :: from_xyz ( 0.0 , 0.4 , -( v. spec . length as f32 ) / 2.0 + 1.8 ) ,
850+ Transform :: from_xyz ( 0.0 , 2.6 , -( v. spec . length as f32 ) / 2.0 + 1.8 ) ,
764851 ) ) ;
765852 } ) ;
766853 }
@@ -810,6 +897,9 @@ fn setup(
810897 if graphics. bloom {
811898 commands. entity ( camera) . insert ( Bloom :: NATURAL ) ;
812899 }
900+ // `apply_scene` only fires on a changed setting, and starting a run does not change
901+ // one — so the camera is dressed here as well as there.
902+ settings:: apply_anti_aliasing ( & mut commands. entity ( camera) , & graphics) ;
813903
814904 // Rain and snow: a particle column of crossed quads that follows the camera
815905 // and scrolls downwards (`update_precipitation`). Both fields exist from the
@@ -1054,6 +1144,11 @@ fn rebase_origin(
10541144}
10551145
10561146/// Mirror vehicle poses from the simulation into transforms.
1147+ ///
1148+ /// The view sits on the rail head, which is where a vehicle model's own origin is:
1149+ /// the BR 101's wheels touch y = 0 and its cab eye is 2.55 m above it. Everything
1150+ /// hung on a vehicle — lamps, the eye point (`ui::follow`), the walker's frame
1151+ /// (`walk::frame`) — measures from there.
10571152fn sync_vehicles (
10581153 sim : Res < SimResource > ,
10591154 origin : Res < Origin > ,
@@ -1067,8 +1162,7 @@ fn sync_vehicles(
10671162 continue ;
10681163 } ;
10691164 let pose = vehicle. pos . pose ( & sim. 0 . net ) ;
1070- let up = origin. 0 . dir_to_render ( pose. up ) ;
1071- transform. translation = origin. 0 . to_render ( pose. pos ) + up * 2.2 ;
1165+ transform. translation = origin. 0 . to_render ( pose. pos ) ;
10721166 transform. rotation = origin. 0 . look_rotation ( pose. tangent , pose. up ) ;
10731167 }
10741168}
@@ -1352,6 +1446,61 @@ fn update_precipitation(
13521446mod tests {
13531447 use super :: * ;
13541448
1449+ /// Leaving a run for the title screen has to take the world with it and nothing else
1450+ /// — the window, the picking pointers and the cloud dome are all older than the run
1451+ /// and all still needed by the next one.
1452+ #[ test]
1453+ fn going_back_to_the_menu_drops_the_run_and_only_the_run ( ) {
1454+ let mut world = World :: new ( ) ;
1455+ world. init_resource :: < walk:: Walker > ( ) ;
1456+ world. init_resource :: < ui:: CameraState > ( ) ;
1457+ let mut snapshot = Schedule :: default ( ) ;
1458+ snapshot. add_systems ( remember_before_run) ;
1459+ let mut leave = Schedule :: default ( ) ;
1460+ leave. add_systems ( tear_down_run) ;
1461+
1462+ // Before the run: something the program put up at startup, with a child of its
1463+ // own, and the cloud dome.
1464+ let older = world. spawn_empty ( ) . id ( ) ;
1465+ world. spawn ( ChildOf ( older) ) ;
1466+ let dome = world. spawn ( world_render:: Persistent ) . id ( ) ;
1467+ snapshot. run ( & mut world) ;
1468+
1469+ // The run: a root with a child, and one more persistent entity made after the
1470+ // snapshot — the marker is what saves it, not the moment it was made.
1471+ let train = world. spawn_empty ( ) . id ( ) ;
1472+ world. spawn ( ChildOf ( train) ) ;
1473+ let late_dome = world. spawn ( world_render:: Persistent ) . id ( ) ;
1474+ // A resource inserted by the run is an entity like any other in Bevy 0.19, and
1475+ // despawning it would take the resource with it.
1476+ world. insert_resource ( ViewDistance ( 4_000.0 ) ) ;
1477+
1478+ leave. run ( & mut world) ;
1479+ assert ! (
1480+ world. get_resource:: <ViewDistance >( ) . is_some( ) ,
1481+ "lost a resource"
1482+ ) ;
1483+ assert ! ( world. get_entity( older) . is_ok( ) , "dropped the older entity" ) ;
1484+ assert ! ( world. get_entity( dome) . is_ok( ) , "dropped the cloud dome" ) ;
1485+ assert ! (
1486+ world. get_entity( late_dome) . is_ok( ) ,
1487+ "dropped a persistent one"
1488+ ) ;
1489+ assert ! (
1490+ world. get_entity( train) . is_err( ) ,
1491+ "the run is still standing"
1492+ ) ;
1493+ let left = world. iter_entities ( ) . count ( ) ;
1494+
1495+ // A second visit to the title screen, with no run behind it, does nothing.
1496+ leave. run ( & mut world) ;
1497+ assert_eq ! (
1498+ world. iter_entities( ) . count( ) ,
1499+ left,
1500+ "a second visit took more"
1501+ ) ;
1502+ }
1503+
13551504 #[ test]
13561505 fn fall_offset_wraps_within_one_period ( ) {
13571506 for t in [ 0.0 , 1.7 , 100.0 , 86_400.0 ] {
0 commit comments