|
| 1 | +//! Shows how `Time<Virtual>` can be used to pause, resume, slow down |
| 2 | +//! and speed up a game. |
| 3 | +
|
| 4 | +use std::time::Duration; |
| 5 | + |
| 6 | +use bevy::{ |
| 7 | + input::common_conditions::input_just_pressed, prelude::*, |
| 8 | + time::common_conditions::on_real_timer, |
| 9 | +}; |
| 10 | + |
| 11 | +fn main() { |
| 12 | + App::new() |
| 13 | + .add_plugins(DefaultPlugins) |
| 14 | + .add_systems(Startup, setup) |
| 15 | + .add_systems( |
| 16 | + Update, |
| 17 | + ( |
| 18 | + move_virtual_time_sprites, |
| 19 | + move_real_time_sprites, |
| 20 | + toggle_pause.run_if(input_just_pressed(KeyCode::Space)), |
| 21 | + change_time_speed::<1>.run_if(input_just_pressed(KeyCode::Up)), |
| 22 | + change_time_speed::<-1>.run_if(input_just_pressed(KeyCode::Down)), |
| 23 | + (update_virtual_time_info_text, update_real_time_info_text) |
| 24 | + // update the texts on a timer to make them more readable |
| 25 | + // `on_timer` run condition uses `Virtual` time meaning it's scaled |
| 26 | + // and would result in the UI updating at different intervals based |
| 27 | + // on `Time<Virtual>::relative_speed` and `Time<Virtual>::is_paused()` |
| 28 | + .run_if(on_real_timer(Duration::from_millis(250))), |
| 29 | + ), |
| 30 | + ) |
| 31 | + .run(); |
| 32 | +} |
| 33 | + |
| 34 | +/// `Real` time related marker |
| 35 | +#[derive(Component)] |
| 36 | +struct RealTime; |
| 37 | + |
| 38 | +/// `Virtual` time related marker |
| 39 | +#[derive(Component)] |
| 40 | +struct VirtualTime; |
| 41 | + |
| 42 | +/// Setup the example |
| 43 | +fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut time: ResMut<Time<Virtual>>) { |
| 44 | + // start with double `Virtual` time resulting in one of the sprites moving at twice the speed |
| 45 | + // of the other sprite which moves based on `Real` (unscaled) time |
| 46 | + time.set_relative_speed(2.); |
| 47 | + |
| 48 | + commands.spawn(Camera2dBundle::default()); |
| 49 | + |
| 50 | + let virtual_color = Color::GOLD; |
| 51 | + let sprite_scale = Vec2::splat(0.5).extend(1.); |
| 52 | + let texture_handle = asset_server.load("branding/icon.png"); |
| 53 | + |
| 54 | + // the sprite moving based on real time |
| 55 | + commands.spawn(( |
| 56 | + SpriteBundle { |
| 57 | + texture: texture_handle.clone(), |
| 58 | + transform: Transform::from_scale(sprite_scale), |
| 59 | + ..default() |
| 60 | + }, |
| 61 | + RealTime, |
| 62 | + )); |
| 63 | + |
| 64 | + // the sprite moving based on virtual time |
| 65 | + commands.spawn(( |
| 66 | + SpriteBundle { |
| 67 | + texture: texture_handle, |
| 68 | + sprite: Sprite { |
| 69 | + color: virtual_color, |
| 70 | + ..default() |
| 71 | + }, |
| 72 | + transform: Transform { |
| 73 | + scale: sprite_scale, |
| 74 | + translation: Vec3::new(0., -160., 0.), |
| 75 | + ..default() |
| 76 | + }, |
| 77 | + ..default() |
| 78 | + }, |
| 79 | + VirtualTime, |
| 80 | + )); |
| 81 | + |
| 82 | + // info UI |
| 83 | + let font_size = 40.; |
| 84 | + |
| 85 | + commands |
| 86 | + .spawn(NodeBundle { |
| 87 | + style: Style { |
| 88 | + display: Display::Flex, |
| 89 | + justify_content: JustifyContent::SpaceBetween, |
| 90 | + width: Val::Percent(100.), |
| 91 | + position_type: PositionType::Absolute, |
| 92 | + top: Val::Px(0.), |
| 93 | + padding: UiRect::all(Val::Px(20.0)), |
| 94 | + ..default() |
| 95 | + }, |
| 96 | + ..default() |
| 97 | + }) |
| 98 | + .with_children(|builder| { |
| 99 | + // real time info |
| 100 | + builder.spawn(( |
| 101 | + TextBundle::from_section( |
| 102 | + "", |
| 103 | + TextStyle { |
| 104 | + font_size, |
| 105 | + ..default() |
| 106 | + }, |
| 107 | + ), |
| 108 | + RealTime, |
| 109 | + )); |
| 110 | + |
| 111 | + // keybindings |
| 112 | + builder.spawn( |
| 113 | + TextBundle::from_section( |
| 114 | + "CONTROLS\nUn/Pause: Space\nSpeed+: Up\nSpeed-: Down", |
| 115 | + TextStyle { |
| 116 | + font_size, |
| 117 | + color: Color::rgb(0.85, 0.85, 0.85), |
| 118 | + ..default() |
| 119 | + }, |
| 120 | + ) |
| 121 | + .with_text_alignment(TextAlignment::Center), |
| 122 | + ); |
| 123 | + |
| 124 | + // virtual time info |
| 125 | + builder.spawn(( |
| 126 | + TextBundle::from_section( |
| 127 | + "", |
| 128 | + TextStyle { |
| 129 | + font_size, |
| 130 | + color: virtual_color, |
| 131 | + ..default() |
| 132 | + }, |
| 133 | + ) |
| 134 | + .with_text_alignment(TextAlignment::Right), |
| 135 | + VirtualTime, |
| 136 | + )); |
| 137 | + }); |
| 138 | +} |
| 139 | + |
| 140 | +/// Move sprites using `Real` (unscaled) time |
| 141 | +fn move_real_time_sprites( |
| 142 | + mut sprite_query: Query<&mut Transform, (With<Sprite>, With<RealTime>)>, |
| 143 | + // `Real` time which is not scaled or paused |
| 144 | + time: Res<Time<Real>>, |
| 145 | +) { |
| 146 | + for mut transform in sprite_query.iter_mut() { |
| 147 | + // move roughly half the screen in a `Real` second |
| 148 | + // when the time is scaled the speed is going to change |
| 149 | + // and the sprite will stay still the the time is paused |
| 150 | + transform.translation.x = get_sprite_translation_x(time.elapsed_seconds()); |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +/// Move sprites using `Virtual` (scaled) time |
| 155 | +fn move_virtual_time_sprites( |
| 156 | + mut sprite_query: Query<&mut Transform, (With<Sprite>, With<VirtualTime>)>, |
| 157 | + // the default `Time` is either `Time<Virtual>` in regular systems |
| 158 | + // or `Time<Fixed>` in fixed timestep systems so `Time::delta()`, |
| 159 | + // `Time::elapsed()` will return the appropriate values either way |
| 160 | + time: Res<Time>, |
| 161 | +) { |
| 162 | + for mut transform in sprite_query.iter_mut() { |
| 163 | + // move roughly half the screen in a `Virtual` second |
| 164 | + // when time is scaled using `Time<Virtual>::set_relative_speed` it's going |
| 165 | + // to move at a different pace and the sprite will stay still when time is |
| 166 | + // `Time<Virtual>::is_paused()` |
| 167 | + transform.translation.x = get_sprite_translation_x(time.elapsed_seconds()); |
| 168 | + } |
| 169 | +} |
| 170 | + |
| 171 | +fn get_sprite_translation_x(elapsed: f32) -> f32 { |
| 172 | + elapsed.sin() * 500. |
| 173 | +} |
| 174 | + |
| 175 | +/// Update the speed of `Time<Virtual>.` by `DELTA` |
| 176 | +fn change_time_speed<const DELTA: i8>(mut time: ResMut<Time<Virtual>>) { |
| 177 | + let time_speed = (time.relative_speed() + DELTA as f32) |
| 178 | + .round() |
| 179 | + .clamp(0.25, 5.); |
| 180 | + |
| 181 | + // set the speed of the virtual time to speed it up or slow it down |
| 182 | + time.set_relative_speed(time_speed); |
| 183 | +} |
| 184 | + |
| 185 | +/// pause or resume `Relative` time |
| 186 | +fn toggle_pause(mut time: ResMut<Time<Virtual>>) { |
| 187 | + if time.is_paused() { |
| 188 | + time.unpause(); |
| 189 | + } else { |
| 190 | + time.pause(); |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +/// Update the `Real` time info text |
| 195 | +fn update_real_time_info_text(time: Res<Time<Real>>, mut query: Query<&mut Text, With<RealTime>>) { |
| 196 | + for mut text in &mut query { |
| 197 | + text.sections[0].value = format!( |
| 198 | + "REAL TIME\nElapsed: {:.1}\nDelta: {:.5}\n", |
| 199 | + time.elapsed_seconds(), |
| 200 | + time.delta_seconds(), |
| 201 | + ); |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +/// Update the `Virtual` time info text |
| 206 | +fn update_virtual_time_info_text( |
| 207 | + time: Res<Time<Virtual>>, |
| 208 | + mut query: Query<&mut Text, With<VirtualTime>>, |
| 209 | +) { |
| 210 | + for mut text in &mut query { |
| 211 | + text.sections[0].value = format!( |
| 212 | + "VIRTUAL TIME\nElapsed: {:.1}\nDelta: {:.5}\nSpeed: {:.2}", |
| 213 | + time.elapsed_seconds(), |
| 214 | + time.delta_seconds(), |
| 215 | + time.relative_speed() |
| 216 | + ); |
| 217 | + } |
| 218 | +} |
0 commit comments