 3d79dc4cdc
			
		
	
	
		3d79dc4cdc
		
			
		
	
	
	
	
		
			
			# Objective Current `FixedTime` and `Time` have several problems. This pull aims to fix many of them at once. - If there is a longer pause between app updates, time will jump forward a lot at once and fixed time will iterate on `FixedUpdate` for a large number of steps. If the pause is merely seconds, then this will just mean jerkiness and possible unexpected behaviour in gameplay. If the pause is hours/days as with OS suspend, the game will appear to freeze until it has caught up with real time. - If calculating a fixed step takes longer than specified fixed step period, the game will enter a death spiral where rendering each frame takes longer and longer due to more and more fixed step updates being run per frame and the game appears to freeze. - There is no way to see current fixed step elapsed time inside fixed steps. In order to track this, the game designer needs to add a custom system inside `FixedUpdate` that calculates elapsed or step count in a resource. - Access to delta time inside fixed step is `FixedStep::period` rather than `Time::delta`. This, coupled with the issue that `Time::elapsed` isn't available at all for fixed steps, makes it that time requiring systems are either implemented to be run in `FixedUpdate` or `Update`, but rarely work in both. - Fixes #8800 - Fixes #8543 - Fixes #7439 - Fixes #5692 ## Solution - Create a generic `Time<T>` clock that has no processing logic but which can be instantiated for multiple usages. This is also exposed for users to add custom clocks. - Create three standard clocks, `Time<Real>`, `Time<Virtual>` and `Time<Fixed>`, all of which contain their individual logic. - Create one "default" clock, which is just `Time` (or `Time<()>`), which will be overwritten from `Time<Virtual>` on each update, and `Time<Fixed>` inside `FixedUpdate` schedule. This way systems that do not care specifically which time they track can work both in `Update` and `FixedUpdate` without changes and the behaviour is intuitive. - Add `max_delta` to virtual time update, which limits how much can be added to virtual time by a single update. This fixes both the behaviour after a long freeze, and also the death spiral by limiting how many fixed timestep iterations there can be per update. Possible future work could be adding `max_accumulator` to add a sort of "leaky bucket" time processing to possibly smooth out jumps in time while keeping frame rate stable. - Many minor tweaks and clarifications to the time functions and their documentation. ## Changelog - `Time::raw_delta()`, `Time::raw_elapsed()` and related methods are moved to `Time<Real>::delta()` and `Time<Real>::elapsed()` and now match `Time` API - `FixedTime` is now `Time<Fixed>` and matches `Time` API. - `Time<Fixed>` default timestep is now 64 Hz, or 15625 microseconds. - `Time` inside `FixedUpdate` now reflects fixed timestep time, making systems portable between `Update ` and `FixedUpdate`. - `Time::pause()`, `Time::set_relative_speed()` and related methods must now be called as `Time<Virtual>::pause()` etc. - There is a new `max_delta` setting in `Time<Virtual>` that limits how much the clock can jump by a single update. The default value is 0.25 seconds. - Removed `on_fixed_timer()` condition as `on_timer()` does the right thing inside `FixedUpdate` now. ## Migration Guide - Change all `Res<Time>` instances that access `raw_delta()`, `raw_elapsed()` and related methods to `Res<Time<Real>>` and `delta()`, `elapsed()`, etc. - Change access to `period` from `Res<FixedTime>` to `Res<Time<Fixed>>` and use `delta()`. - The default timestep has been changed from 60 Hz to 64 Hz. If you wish to restore the old behaviour, use `app.insert_resource(Time::<Fixed>::from_hz(60.0))`. - Change `app.insert_resource(FixedTime::new(duration))` to `app.insert_resource(Time::<Fixed>::from_duration(duration))` - Change `app.insert_resource(FixedTime::new_from_secs(secs))` to `app.insert_resource(Time::<Fixed>::from_seconds(secs))` - Change `system.on_fixed_timer(duration)` to `system.on_timer(duration)`. Timers in systems placed in `FixedUpdate` schedule automatically use the fixed time clock. - Change `ResMut<Time>` calls to `pause()`, `is_paused()`, `set_relative_speed()` and related methods to `ResMut<Time<Virtual>>` calls. The API is the same, with the exception that `relative_speed()` will return the actual last ste relative speed, while `effective_relative_speed()` returns 0.0 if the time is paused and corresponds to the speed that was set when the update for the current frame started. ## Todo - [x] Update pull name and description - [x] Top level documentation on usage - [x] Fix examples - [x] Decide on default `max_delta` value - [x] Decide naming of the three clocks: is `Real`, `Virtual`, `Fixed` good? - [x] Decide if the three clock inner structures should be in prelude - [x] Decide on best way to configure values at startup: is manually inserting a new clock instance okay, or should there be config struct separately? - [x] Fix links in docs - [x] Decide what should be public and what not - [x] Decide how `wrap_period` should be handled when it is changed - [x] ~~Add toggles to disable setting the clock as default?~~ No, separate pull if needed. - [x] Add tests - [x] Reformat, ensure adheres to conventions etc. - [x] Build documentation and see that it looks correct ## Contributors Huge thanks to @alice-i-cecile and @maniwani while building this pull. It was a shared effort! --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Cameron <51241057+maniwani@users.noreply.github.com> Co-authored-by: Jerome Humbert <djeedai@gmail.com>
		
			
				
	
	
		
			581 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			581 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! This example provides a 2D benchmark.
 | |
| //!
 | |
| //! Usage: spawn more entities by clicking on the screen.
 | |
| 
 | |
| use std::str::FromStr;
 | |
| 
 | |
| use argh::FromArgs;
 | |
| use bevy::{
 | |
|     diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
 | |
|     prelude::*,
 | |
|     render::render_resource::{Extent3d, TextureDimension, TextureFormat},
 | |
|     sprite::{MaterialMesh2dBundle, Mesh2dHandle},
 | |
|     utils::Duration,
 | |
|     window::{PresentMode, WindowResolution},
 | |
| };
 | |
| use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng};
 | |
| 
 | |
| const BIRDS_PER_SECOND: u32 = 10000;
 | |
| const GRAVITY: f32 = -9.8 * 100.0;
 | |
| const MAX_VELOCITY: f32 = 750.;
 | |
| const BIRD_SCALE: f32 = 0.15;
 | |
| const BIRD_TEXTURE_SIZE: usize = 256;
 | |
| const HALF_BIRD_SIZE: f32 = BIRD_TEXTURE_SIZE as f32 * BIRD_SCALE * 0.5;
 | |
| 
 | |
| #[derive(Resource)]
 | |
| struct BevyCounter {
 | |
|     pub count: usize,
 | |
|     pub color: Color,
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| struct Bird {
 | |
|     velocity: Vec3,
 | |
| }
 | |
| 
 | |
| #[derive(FromArgs, Resource)]
 | |
| /// `bevymark` sprite / 2D mesh stress test
 | |
| struct Args {
 | |
|     /// whether to use sprite or mesh2d
 | |
|     #[argh(option, default = "Mode::Sprite")]
 | |
|     mode: Mode,
 | |
| 
 | |
|     /// whether to step animations by a fixed amount such that each frame is the same across runs.
 | |
|     /// If spawning waves, all are spawned up-front to immediately start rendering at the heaviest
 | |
|     /// load.
 | |
|     #[argh(switch)]
 | |
|     benchmark: bool,
 | |
| 
 | |
|     /// how many birds to spawn per wave.
 | |
|     #[argh(option, default = "0")]
 | |
|     per_wave: usize,
 | |
| 
 | |
|     /// the number of waves to spawn.
 | |
|     #[argh(option, default = "0")]
 | |
|     waves: usize,
 | |
| 
 | |
|     /// whether to vary the material data in each instance.
 | |
|     #[argh(switch)]
 | |
|     vary_per_instance: bool,
 | |
| 
 | |
|     /// the number of different textures from which to randomly select the material color. 0 means no textures.
 | |
|     #[argh(option, default = "1")]
 | |
|     material_texture_count: usize,
 | |
| 
 | |
|     /// generate z values in increasing order rather than randomly
 | |
|     #[argh(switch)]
 | |
|     ordered_z: bool,
 | |
| }
 | |
| 
 | |
| #[derive(Default, Clone)]
 | |
| enum Mode {
 | |
|     #[default]
 | |
|     Sprite,
 | |
|     Mesh2d,
 | |
| }
 | |
| 
 | |
| impl FromStr for Mode {
 | |
|     type Err = String;
 | |
| 
 | |
|     fn from_str(s: &str) -> Result<Self, Self::Err> {
 | |
|         match s {
 | |
|             "sprite" => Ok(Self::Sprite),
 | |
|             "mesh2d" => Ok(Self::Mesh2d),
 | |
|             _ => Err(format!(
 | |
|                 "Unknown mode: '{s}', valid modes: 'sprite', 'mesh2d'"
 | |
|             )),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| const FIXED_TIMESTEP: f32 = 0.2;
 | |
| 
 | |
| fn main() {
 | |
|     let args: Args = argh::from_env();
 | |
| 
 | |
|     App::new()
 | |
|         .add_plugins((
 | |
|             DefaultPlugins.set(WindowPlugin {
 | |
|                 primary_window: Some(Window {
 | |
|                     title: "BevyMark".into(),
 | |
|                     resolution: WindowResolution::new(1920.0, 1080.0)
 | |
|                         .with_scale_factor_override(1.0),
 | |
|                     present_mode: PresentMode::AutoNoVsync,
 | |
|                     ..default()
 | |
|                 }),
 | |
|                 ..default()
 | |
|             }),
 | |
|             FrameTimeDiagnosticsPlugin,
 | |
|             LogDiagnosticsPlugin::default(),
 | |
|         ))
 | |
|         .insert_resource(args)
 | |
|         .insert_resource(BevyCounter {
 | |
|             count: 0,
 | |
|             color: Color::WHITE,
 | |
|         })
 | |
|         .add_systems(Startup, setup)
 | |
|         .add_systems(FixedUpdate, scheduled_spawner)
 | |
|         .add_systems(
 | |
|             Update,
 | |
|             (
 | |
|                 mouse_handler,
 | |
|                 movement_system,
 | |
|                 collision_system,
 | |
|                 counter_system,
 | |
|             ),
 | |
|         )
 | |
|         .insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
 | |
|             FIXED_TIMESTEP,
 | |
|         )))
 | |
|         .run();
 | |
| }
 | |
| 
 | |
| #[derive(Resource)]
 | |
| struct BirdScheduled {
 | |
|     waves: usize,
 | |
|     per_wave: usize,
 | |
| }
 | |
| 
 | |
| fn scheduled_spawner(
 | |
|     mut commands: Commands,
 | |
|     args: Res<Args>,
 | |
|     windows: Query<&Window>,
 | |
|     mut scheduled: ResMut<BirdScheduled>,
 | |
|     mut counter: ResMut<BevyCounter>,
 | |
|     bird_resources: ResMut<BirdResources>,
 | |
| ) {
 | |
|     let window = windows.single();
 | |
| 
 | |
|     if scheduled.waves > 0 {
 | |
|         let bird_resources = bird_resources.into_inner();
 | |
|         spawn_birds(
 | |
|             &mut commands,
 | |
|             args.into_inner(),
 | |
|             &window.resolution,
 | |
|             &mut counter,
 | |
|             scheduled.per_wave,
 | |
|             bird_resources,
 | |
|             None,
 | |
|             scheduled.waves - 1,
 | |
|         );
 | |
| 
 | |
|         scheduled.waves -= 1;
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(Resource)]
 | |
| struct BirdResources {
 | |
|     textures: Vec<Handle<Image>>,
 | |
|     materials: Vec<Handle<ColorMaterial>>,
 | |
|     quad: Mesh2dHandle,
 | |
|     color_rng: StdRng,
 | |
|     material_rng: StdRng,
 | |
|     velocity_rng: StdRng,
 | |
|     transform_rng: StdRng,
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| struct StatsText;
 | |
| 
 | |
| #[allow(clippy::too_many_arguments)]
 | |
| fn setup(
 | |
|     mut commands: Commands,
 | |
|     args: Res<Args>,
 | |
|     asset_server: Res<AssetServer>,
 | |
|     mut meshes: ResMut<Assets<Mesh>>,
 | |
|     material_assets: ResMut<Assets<ColorMaterial>>,
 | |
|     images: ResMut<Assets<Image>>,
 | |
|     windows: Query<&Window>,
 | |
|     counter: ResMut<BevyCounter>,
 | |
| ) {
 | |
|     warn!(include_str!("warning_string.txt"));
 | |
| 
 | |
|     let args = args.into_inner();
 | |
|     let images = images.into_inner();
 | |
| 
 | |
|     let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
 | |
|     if matches!(args.mode, Mode::Sprite) || args.material_texture_count > 0 {
 | |
|         textures.push(asset_server.load("branding/icon.png"));
 | |
|     }
 | |
|     init_textures(&mut textures, args, images);
 | |
| 
 | |
|     let material_assets = material_assets.into_inner();
 | |
|     let materials = init_materials(args, &textures, material_assets);
 | |
| 
 | |
|     let mut bird_resources = BirdResources {
 | |
|         textures,
 | |
|         materials,
 | |
|         quad: meshes
 | |
|             .add(Mesh::from(shape::Quad::new(Vec2::splat(
 | |
|                 BIRD_TEXTURE_SIZE as f32,
 | |
|             ))))
 | |
|             .into(),
 | |
|         color_rng: StdRng::seed_from_u64(42),
 | |
|         material_rng: StdRng::seed_from_u64(42),
 | |
|         velocity_rng: StdRng::seed_from_u64(42),
 | |
|         transform_rng: StdRng::seed_from_u64(42),
 | |
|     };
 | |
| 
 | |
|     let text_section = move |color, value: &str| {
 | |
|         TextSection::new(
 | |
|             value,
 | |
|             TextStyle {
 | |
|                 font_size: 40.0,
 | |
|                 color,
 | |
|                 ..default()
 | |
|             },
 | |
|         )
 | |
|     };
 | |
| 
 | |
|     commands.spawn(Camera2dBundle::default());
 | |
|     commands
 | |
|         .spawn(NodeBundle {
 | |
|             style: Style {
 | |
|                 position_type: PositionType::Absolute,
 | |
|                 padding: UiRect::all(Val::Px(5.0)),
 | |
|                 ..default()
 | |
|             },
 | |
|             z_index: ZIndex::Global(i32::MAX),
 | |
|             background_color: Color::BLACK.with_a(0.75).into(),
 | |
|             ..default()
 | |
|         })
 | |
|         .with_children(|c| {
 | |
|             c.spawn((
 | |
|                 TextBundle::from_sections([
 | |
|                     text_section(Color::GREEN, "Bird Count: "),
 | |
|                     text_section(Color::CYAN, ""),
 | |
|                     text_section(Color::GREEN, "\nFPS (raw): "),
 | |
|                     text_section(Color::CYAN, ""),
 | |
|                     text_section(Color::GREEN, "\nFPS (SMA): "),
 | |
|                     text_section(Color::CYAN, ""),
 | |
|                     text_section(Color::GREEN, "\nFPS (EMA): "),
 | |
|                     text_section(Color::CYAN, ""),
 | |
|                 ]),
 | |
|                 StatsText,
 | |
|             ));
 | |
|         });
 | |
| 
 | |
|     let mut scheduled = BirdScheduled {
 | |
|         per_wave: args.per_wave,
 | |
|         waves: args.waves,
 | |
|     };
 | |
| 
 | |
|     if args.benchmark {
 | |
|         let counter = counter.into_inner();
 | |
|         for wave in (0..scheduled.waves).rev() {
 | |
|             spawn_birds(
 | |
|                 &mut commands,
 | |
|                 args,
 | |
|                 &windows.single().resolution,
 | |
|                 counter,
 | |
|                 scheduled.per_wave,
 | |
|                 &mut bird_resources,
 | |
|                 Some(wave),
 | |
|                 wave,
 | |
|             );
 | |
|         }
 | |
|         scheduled.waves = 0;
 | |
|     }
 | |
|     commands.insert_resource(bird_resources);
 | |
|     commands.insert_resource(scheduled);
 | |
| }
 | |
| 
 | |
| #[allow(clippy::too_many_arguments)]
 | |
| fn mouse_handler(
 | |
|     mut commands: Commands,
 | |
|     args: Res<Args>,
 | |
|     time: Res<Time>,
 | |
|     mouse_button_input: Res<Input<MouseButton>>,
 | |
|     windows: Query<&Window>,
 | |
|     bird_resources: ResMut<BirdResources>,
 | |
|     mut counter: ResMut<BevyCounter>,
 | |
|     mut rng: Local<Option<StdRng>>,
 | |
|     mut wave: Local<usize>,
 | |
| ) {
 | |
|     if rng.is_none() {
 | |
|         *rng = Some(StdRng::seed_from_u64(42));
 | |
|     }
 | |
|     let rng = rng.as_mut().unwrap();
 | |
|     let window = windows.single();
 | |
| 
 | |
|     if mouse_button_input.just_released(MouseButton::Left) {
 | |
|         counter.color = Color::rgb_linear(rng.gen(), rng.gen(), rng.gen());
 | |
|     }
 | |
| 
 | |
|     if mouse_button_input.pressed(MouseButton::Left) {
 | |
|         let spawn_count = (BIRDS_PER_SECOND as f64 * time.delta_seconds_f64()) as usize;
 | |
|         spawn_birds(
 | |
|             &mut commands,
 | |
|             args.into_inner(),
 | |
|             &window.resolution,
 | |
|             &mut counter,
 | |
|             spawn_count,
 | |
|             bird_resources.into_inner(),
 | |
|             None,
 | |
|             *wave,
 | |
|         );
 | |
|         *wave += 1;
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn bird_velocity_transform(
 | |
|     half_extents: Vec2,
 | |
|     mut translation: Vec3,
 | |
|     velocity_rng: &mut StdRng,
 | |
|     waves: Option<usize>,
 | |
|     dt: f32,
 | |
| ) -> (Transform, Vec3) {
 | |
|     let mut velocity = Vec3::new(MAX_VELOCITY * (velocity_rng.gen::<f32>() - 0.5), 0., 0.);
 | |
| 
 | |
|     if let Some(waves) = waves {
 | |
|         // Step the movement and handle collisions as if the wave had been spawned at fixed time intervals
 | |
|         // and with dt-spaced frames of simulation
 | |
|         for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
 | |
|             step_movement(&mut translation, &mut velocity, dt);
 | |
|             handle_collision(half_extents, &translation, &mut velocity);
 | |
|         }
 | |
|     }
 | |
|     (
 | |
|         Transform::from_translation(translation).with_scale(Vec3::splat(BIRD_SCALE)),
 | |
|         velocity,
 | |
|     )
 | |
| }
 | |
| 
 | |
| const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
 | |
| 
 | |
| #[allow(clippy::too_many_arguments)]
 | |
| fn spawn_birds(
 | |
|     commands: &mut Commands,
 | |
|     args: &Args,
 | |
|     primary_window_resolution: &WindowResolution,
 | |
|     counter: &mut BevyCounter,
 | |
|     spawn_count: usize,
 | |
|     bird_resources: &mut BirdResources,
 | |
|     waves_to_simulate: Option<usize>,
 | |
|     wave: usize,
 | |
| ) {
 | |
|     let bird_x = (primary_window_resolution.width() / -2.) + HALF_BIRD_SIZE;
 | |
|     let bird_y = (primary_window_resolution.height() / 2.) - HALF_BIRD_SIZE;
 | |
| 
 | |
|     let half_extents = 0.5
 | |
|         * Vec2::new(
 | |
|             primary_window_resolution.width(),
 | |
|             primary_window_resolution.height(),
 | |
|         );
 | |
| 
 | |
|     let color = counter.color;
 | |
|     let current_count = counter.count;
 | |
| 
 | |
|     match args.mode {
 | |
|         Mode::Sprite => {
 | |
|             let batch = (0..spawn_count)
 | |
|                 .map(|count| {
 | |
|                     let bird_z = if args.ordered_z {
 | |
|                         (current_count + count) as f32 * 0.00001
 | |
|                     } else {
 | |
|                         bird_resources.transform_rng.gen::<f32>()
 | |
|                     };
 | |
| 
 | |
|                     let (transform, velocity) = bird_velocity_transform(
 | |
|                         half_extents,
 | |
|                         Vec3::new(bird_x, bird_y, bird_z),
 | |
|                         &mut bird_resources.velocity_rng,
 | |
|                         waves_to_simulate,
 | |
|                         FIXED_DELTA_TIME,
 | |
|                     );
 | |
| 
 | |
|                     let color = if args.vary_per_instance {
 | |
|                         Color::rgb_linear(
 | |
|                             bird_resources.color_rng.gen(),
 | |
|                             bird_resources.color_rng.gen(),
 | |
|                             bird_resources.color_rng.gen(),
 | |
|                         )
 | |
|                     } else {
 | |
|                         color
 | |
|                     };
 | |
|                     (
 | |
|                         SpriteBundle {
 | |
|                             texture: bird_resources
 | |
|                                 .textures
 | |
|                                 .choose(&mut bird_resources.material_rng)
 | |
|                                 .unwrap()
 | |
|                                 .clone(),
 | |
|                             transform,
 | |
|                             sprite: Sprite { color, ..default() },
 | |
|                             ..default()
 | |
|                         },
 | |
|                         Bird { velocity },
 | |
|                     )
 | |
|                 })
 | |
|                 .collect::<Vec<_>>();
 | |
|             commands.spawn_batch(batch);
 | |
|         }
 | |
|         Mode::Mesh2d => {
 | |
|             let batch = (0..spawn_count)
 | |
|                 .map(|count| {
 | |
|                     let bird_z = if args.ordered_z {
 | |
|                         (current_count + count) as f32 * 0.00001
 | |
|                     } else {
 | |
|                         bird_resources.transform_rng.gen::<f32>()
 | |
|                     };
 | |
| 
 | |
|                     let (transform, velocity) = bird_velocity_transform(
 | |
|                         half_extents,
 | |
|                         Vec3::new(bird_x, bird_y, bird_z),
 | |
|                         &mut bird_resources.velocity_rng,
 | |
|                         waves_to_simulate,
 | |
|                         FIXED_DELTA_TIME,
 | |
|                     );
 | |
| 
 | |
|                     let material =
 | |
|                         if args.vary_per_instance || args.material_texture_count > args.waves {
 | |
|                             bird_resources
 | |
|                                 .materials
 | |
|                                 .choose(&mut bird_resources.material_rng)
 | |
|                                 .unwrap()
 | |
|                                 .clone()
 | |
|                         } else {
 | |
|                             bird_resources.materials[wave % bird_resources.materials.len()].clone()
 | |
|                         };
 | |
|                     (
 | |
|                         MaterialMesh2dBundle {
 | |
|                             mesh: bird_resources.quad.clone(),
 | |
|                             material,
 | |
|                             transform,
 | |
|                             ..default()
 | |
|                         },
 | |
|                         Bird { velocity },
 | |
|                     )
 | |
|                 })
 | |
|                 .collect::<Vec<_>>();
 | |
|             commands.spawn_batch(batch);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     counter.count += spawn_count;
 | |
|     counter.color = Color::rgb_linear(
 | |
|         bird_resources.color_rng.gen(),
 | |
|         bird_resources.color_rng.gen(),
 | |
|         bird_resources.color_rng.gen(),
 | |
|     );
 | |
| }
 | |
| 
 | |
| fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
 | |
|     translation.x += velocity.x * dt;
 | |
|     translation.y += velocity.y * dt;
 | |
|     velocity.y += GRAVITY * dt;
 | |
| }
 | |
| 
 | |
| fn movement_system(
 | |
|     args: Res<Args>,
 | |
|     time: Res<Time>,
 | |
|     mut bird_query: Query<(&mut Bird, &mut Transform)>,
 | |
| ) {
 | |
|     let dt = if args.benchmark {
 | |
|         FIXED_DELTA_TIME
 | |
|     } else {
 | |
|         time.delta_seconds()
 | |
|     };
 | |
|     for (mut bird, mut transform) in &mut bird_query {
 | |
|         step_movement(&mut transform.translation, &mut bird.velocity, dt);
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn handle_collision(half_extents: Vec2, translation: &Vec3, velocity: &mut Vec3) {
 | |
|     if (velocity.x > 0. && translation.x + HALF_BIRD_SIZE > half_extents.x)
 | |
|         || (velocity.x <= 0. && translation.x - HALF_BIRD_SIZE < -(half_extents.x))
 | |
|     {
 | |
|         velocity.x = -velocity.x;
 | |
|     }
 | |
|     let velocity_y = velocity.y;
 | |
|     if velocity_y < 0. && translation.y - HALF_BIRD_SIZE < -half_extents.y {
 | |
|         velocity.y = -velocity_y;
 | |
|     }
 | |
|     if translation.y + HALF_BIRD_SIZE > half_extents.y && velocity_y > 0.0 {
 | |
|         velocity.y = 0.0;
 | |
|     }
 | |
| }
 | |
| fn collision_system(windows: Query<&Window>, mut bird_query: Query<(&mut Bird, &Transform)>) {
 | |
|     let window = windows.single();
 | |
| 
 | |
|     let half_extents = 0.5 * Vec2::new(window.width(), window.height());
 | |
| 
 | |
|     for (mut bird, transform) in &mut bird_query {
 | |
|         handle_collision(half_extents, &transform.translation, &mut bird.velocity);
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn counter_system(
 | |
|     diagnostics: Res<DiagnosticsStore>,
 | |
|     counter: Res<BevyCounter>,
 | |
|     mut query: Query<&mut Text, With<StatsText>>,
 | |
| ) {
 | |
|     let mut text = query.single_mut();
 | |
| 
 | |
|     if counter.is_changed() {
 | |
|         text.sections[1].value = counter.count.to_string();
 | |
|     }
 | |
| 
 | |
|     if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
 | |
|         if let Some(raw) = fps.value() {
 | |
|             text.sections[3].value = format!("{raw:.2}");
 | |
|         }
 | |
|         if let Some(sma) = fps.average() {
 | |
|             text.sections[5].value = format!("{sma:.2}");
 | |
|         }
 | |
|         if let Some(ema) = fps.smoothed() {
 | |
|             text.sections[7].value = format!("{ema:.2}");
 | |
|         }
 | |
|     };
 | |
| }
 | |
| 
 | |
| fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
 | |
|     let mut color_rng = StdRng::seed_from_u64(42);
 | |
|     while textures.len() < args.material_texture_count {
 | |
|         let pixel = [color_rng.gen(), color_rng.gen(), color_rng.gen(), 255];
 | |
|         textures.push(images.add(Image::new_fill(
 | |
|             Extent3d {
 | |
|                 width: BIRD_TEXTURE_SIZE as u32,
 | |
|                 height: BIRD_TEXTURE_SIZE as u32,
 | |
|                 depth_or_array_layers: 1,
 | |
|             },
 | |
|             TextureDimension::D2,
 | |
|             &pixel,
 | |
|             TextureFormat::Rgba8UnormSrgb,
 | |
|         )));
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn init_materials(
 | |
|     args: &Args,
 | |
|     textures: &[Handle<Image>],
 | |
|     assets: &mut Assets<ColorMaterial>,
 | |
| ) -> Vec<Handle<ColorMaterial>> {
 | |
|     let capacity = if args.vary_per_instance {
 | |
|         args.per_wave * args.waves
 | |
|     } else {
 | |
|         args.material_texture_count.max(args.waves)
 | |
|     }
 | |
|     .max(1);
 | |
| 
 | |
|     let mut materials = Vec::with_capacity(capacity);
 | |
|     materials.push(assets.add(ColorMaterial {
 | |
|         color: Color::WHITE,
 | |
|         texture: textures.get(0).cloned(),
 | |
|     }));
 | |
| 
 | |
|     let mut color_rng = StdRng::seed_from_u64(42);
 | |
|     let mut texture_rng = StdRng::seed_from_u64(42);
 | |
|     materials.extend(
 | |
|         std::iter::repeat_with(|| {
 | |
|             assets.add(ColorMaterial {
 | |
|                 color: Color::rgb_u8(color_rng.gen(), color_rng.gen(), color_rng.gen()),
 | |
|                 texture: textures.choose(&mut texture_rng).cloned(),
 | |
|             })
 | |
|         })
 | |
|         .take(capacity - materials.len()),
 | |
|     );
 | |
| 
 | |
|     materials
 | |
| }
 |