 206c7ce219
			
		
	
	
		206c7ce219
		
	
	
	
	
		
			
			Huge thanks to @maniwani, @devil-ira, @hymm, @cart, @superdump and @jakobhellermann for the help with this PR. # Objective - Followup #6587. - Minimal integration for the Stageless Scheduling RFC: https://github.com/bevyengine/rfcs/pull/45 ## Solution - [x] Remove old scheduling module - [x] Migrate new methods to no longer use extension methods - [x] Fix compiler errors - [x] Fix benchmarks - [x] Fix examples - [x] Fix docs - [x] Fix tests ## Changelog ### Added - a large number of methods on `App` to work with schedules ergonomically - the `CoreSchedule` enum - `App::add_extract_system` via the `RenderingAppExtension` trait extension method - the private `prepare_view_uniforms` system now has a public system set for scheduling purposes, called `ViewSet::PrepareUniforms` ### Removed - stages, and all code that mentions stages - states have been dramatically simplified, and no longer use a stack - `RunCriteriaLabel` - `AsSystemLabel` trait - `on_hierarchy_reports_enabled` run criteria (now just uses an ad hoc resource checking run condition) - systems in `RenderSet/Stage::Extract` no longer warn when they do not read data from the main world - `RunCriteriaLabel` - `transform_propagate_system_set`: this was a nonstandard pattern that didn't actually provide enough control. The systems are already `pub`: the docs have been updated to ensure that the third-party usage is clear. ### Changed - `System::default_labels` is now `System::default_system_sets`. - `App::add_default_labels` is now `App::add_default_sets` - `CoreStage` and `StartupStage` enums are now `CoreSet` and `StartupSet` - `App::add_system_set` was renamed to `App::add_systems` - The `StartupSchedule` label is now defined as part of the `CoreSchedules` enum - `.label(SystemLabel)` is now referred to as `.in_set(SystemSet)` - `SystemLabel` trait was replaced by `SystemSet` - `SystemTypeIdLabel<T>` was replaced by `SystemSetType<T>` - The `ReportHierarchyIssue` resource now has a public constructor (`new`), and implements `PartialEq` - Fixed time steps now use a schedule (`CoreSchedule::FixedTimeStep`) rather than a run criteria. - Adding rendering extraction systems now panics rather than silently failing if no subapp with the `RenderApp` label is found. - the `calculate_bounds` system, with the `CalculateBounds` label, is now in `CoreSet::Update`, rather than in `CoreSet::PostUpdate` before commands are applied. - `SceneSpawnerSystem` now runs under `CoreSet::Update`, rather than `CoreStage::PreUpdate.at_end()`. - `bevy_pbr::add_clusters` is no longer an exclusive system - the top level `bevy_ecs::schedule` module was replaced with `bevy_ecs::scheduling` - `tick_global_task_pools_on_main_thread` is no longer run as an exclusive system. Instead, it has been replaced by `tick_global_task_pools`, which uses a `NonSend` resource to force running on the main thread. ## Migration Guide - Calls to `.label(MyLabel)` should be replaced with `.in_set(MySet)` - Stages have been removed. Replace these with system sets, and then add command flushes using the `apply_system_buffers` exclusive system where needed. - The `CoreStage`, `StartupStage, `RenderStage` and `AssetStage` enums have been replaced with `CoreSet`, `StartupSet, `RenderSet` and `AssetSet`. The same scheduling guarantees have been preserved. - Systems are no longer added to `CoreSet::Update` by default. Add systems manually if this behavior is needed, although you should consider adding your game logic systems to `CoreSchedule::FixedTimestep` instead for more reliable framerate-independent behavior. - Similarly, startup systems are no longer part of `StartupSet::Startup` by default. In most cases, this won't matter to you. - For example, `add_system_to_stage(CoreStage::PostUpdate, my_system)` should be replaced with - `add_system(my_system.in_set(CoreSet::PostUpdate)` - When testing systems or otherwise running them in a headless fashion, simply construct and run a schedule using `Schedule::new()` and `World::run_schedule` rather than constructing stages - Run criteria have been renamed to run conditions. These can now be combined with each other and with states. - Looping run criteria and state stacks have been removed. Use an exclusive system that runs a schedule if you need this level of control over system control flow. - For app-level control flow over which schedules get run when (such as for rollback networking), create your own schedule and insert it under the `CoreSchedule::Outer` label. - Fixed timesteps are now evaluated in a schedule, rather than controlled via run criteria. The `run_fixed_timestep` system runs this schedule between `CoreSet::First` and `CoreSet::PreUpdate` by default. - Command flush points introduced by `AssetStage` have been removed. If you were relying on these, add them back manually. - Adding extract systems is now typically done directly on the main app. Make sure the `RenderingAppExtension` trait is in scope, then call `app.add_extract_system(my_system)`. - the `calculate_bounds` system, with the `CalculateBounds` label, is now in `CoreSet::Update`, rather than in `CoreSet::PostUpdate` before commands are applied. You may need to order your movement systems to occur before this system in order to avoid system order ambiguities in culling behavior. - the `RenderLabel` `AppLabel` was renamed to `RenderApp` for clarity - `App::add_state` now takes 0 arguments: the starting state is set based on the `Default` impl. - Instead of creating `SystemSet` containers for systems that run in stages, simply use `.on_enter::<State::Variant>()` or its `on_exit` or `on_update` siblings. - `SystemLabel` derives should be replaced with `SystemSet`. You will also need to add the `Debug`, `PartialEq`, `Eq`, and `Hash` traits to satisfy the new trait bounds. - `with_run_criteria` has been renamed to `run_if`. Run criteria have been renamed to run conditions for clarity, and should now simply return a bool. - States have been dramatically simplified: there is no longer a "state stack". To queue a transition to the next state, call `NextState::set` ## TODO - [x] remove dead methods on App and World - [x] add `App::add_system_to_schedule` and `App::add_systems_to_schedule` - [x] avoid adding the default system set at inappropriate times - [x] remove any accidental cycles in the default plugins schedule - [x] migrate benchmarks - [x] expose explicit labels for the built-in command flush points - [x] migrate engine code - [x] remove all mentions of stages from the docs - [x] verify docs for States - [x] fix uses of exclusive systems that use .end / .at_start / .before_commands - [x] migrate RenderStage and AssetStage - [x] migrate examples - [x] ensure that transform propagation is exported in a sufficiently public way (the systems are already pub) - [x] ensure that on_enter schedules are run at least once before the main app - [x] re-enable opt-in to execution order ambiguities - [x] revert change to `update_bounds` to ensure it runs in `PostUpdate` - [x] test all examples - [x] unbreak directional lights - [x] unbreak shadows (see 3d_scene, 3d_shape, lighting, transparaency_3d examples) - [x] game menu example shows loading screen and menu simultaneously - [x] display settings menu is a blank screen - [x] `without_winit` example panics - [x] ensure all tests pass - [x] SubApp doc test fails - [x] runs_spawn_local tasks fails - [x] [Fix panic_when_hierachy_cycle test hanging](https://github.com/alice-i-cecile/bevy/pull/120) ## Points of Difficulty and Controversy **Reviewers, please give feedback on these and look closely** 1. Default sets, from the RFC, have been removed. These added a tremendous amount of implicit complexity and result in hard to debug scheduling errors. They're going to be tackled in the form of "base sets" by @cart in a followup. 2. The outer schedule controls which schedule is run when `App::update` is called. 3. I implemented `Label for `Box<dyn Label>` for our label types. This enables us to store schedule labels in concrete form, and then later run them. I ran into the same set of problems when working with one-shot systems. We've previously investigated this pattern in depth, and it does not appear to lead to extra indirection with nested boxes. 4. `SubApp::update` simply runs the default schedule once. This sucks, but this whole API is incomplete and this was the minimal changeset. 5. `time_system` and `tick_global_task_pools_on_main_thread` no longer use exclusive systems to attempt to force scheduling order 6. Implemetnation strategy for fixed timesteps 7. `AssetStage` was migrated to `AssetSet` without reintroducing command flush points. These did not appear to be used, and it's nice to remove these bottlenecks. 8. Migration of `bevy_render/lib.rs` and pipelined rendering. The logic here is unusually tricky, as we have complex scheduling requirements. ## Future Work (ideally before 0.10) - Rename schedule_v3 module to schedule or scheduling - Add a derive macro to states, and likely a `EnumIter` trait of some form - Figure out what exactly to do with the "systems added should basically work by default" problem - Improve ergonomics for working with fixed timesteps and states - Polish FixedTime API to match Time - Rebase and merge #7415 - Resolve all internal ambiguities (blocked on better tools, especially #7442) - Add "base sets" to replace the removed default sets.
		
			
				
	
	
		
			423 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			423 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! A simplified implementation of the classic game "Breakout".
 | |
| 
 | |
| use bevy::{
 | |
|     prelude::*,
 | |
|     sprite::collide_aabb::{collide, Collision},
 | |
|     sprite::MaterialMesh2dBundle,
 | |
| };
 | |
| 
 | |
| // Defines the amount of time that should elapse between each physics step.
 | |
| const TIME_STEP: f32 = 1.0 / 60.0;
 | |
| 
 | |
| // These constants are defined in `Transform` units.
 | |
| // Using the default 2D camera they correspond 1:1 with screen pixels.
 | |
| const PADDLE_SIZE: Vec3 = Vec3::new(120.0, 20.0, 0.0);
 | |
| const GAP_BETWEEN_PADDLE_AND_FLOOR: f32 = 60.0;
 | |
| const PADDLE_SPEED: f32 = 500.0;
 | |
| // How close can the paddle get to the wall
 | |
| const PADDLE_PADDING: f32 = 10.0;
 | |
| 
 | |
| // We set the z-value of the ball to 1 so it renders on top in the case of overlapping sprites.
 | |
| const BALL_STARTING_POSITION: Vec3 = Vec3::new(0.0, -50.0, 1.0);
 | |
| const BALL_SIZE: Vec3 = Vec3::new(30.0, 30.0, 0.0);
 | |
| const BALL_SPEED: f32 = 400.0;
 | |
| const INITIAL_BALL_DIRECTION: Vec2 = Vec2::new(0.5, -0.5);
 | |
| 
 | |
| const WALL_THICKNESS: f32 = 10.0;
 | |
| // x coordinates
 | |
| const LEFT_WALL: f32 = -450.;
 | |
| const RIGHT_WALL: f32 = 450.;
 | |
| // y coordinates
 | |
| const BOTTOM_WALL: f32 = -300.;
 | |
| const TOP_WALL: f32 = 300.;
 | |
| 
 | |
| const BRICK_SIZE: Vec2 = Vec2::new(100., 30.);
 | |
| // These values are exact
 | |
| const GAP_BETWEEN_PADDLE_AND_BRICKS: f32 = 270.0;
 | |
| const GAP_BETWEEN_BRICKS: f32 = 5.0;
 | |
| // These values are lower bounds, as the number of bricks is computed
 | |
| const GAP_BETWEEN_BRICKS_AND_CEILING: f32 = 20.0;
 | |
| const GAP_BETWEEN_BRICKS_AND_SIDES: f32 = 20.0;
 | |
| 
 | |
| const SCOREBOARD_FONT_SIZE: f32 = 40.0;
 | |
| const SCOREBOARD_TEXT_PADDING: Val = Val::Px(5.0);
 | |
| 
 | |
| const BACKGROUND_COLOR: Color = Color::rgb(0.9, 0.9, 0.9);
 | |
| const PADDLE_COLOR: Color = Color::rgb(0.3, 0.3, 0.7);
 | |
| const BALL_COLOR: Color = Color::rgb(1.0, 0.5, 0.5);
 | |
| const BRICK_COLOR: Color = Color::rgb(0.5, 0.5, 1.0);
 | |
| const WALL_COLOR: Color = Color::rgb(0.8, 0.8, 0.8);
 | |
| const TEXT_COLOR: Color = Color::rgb(0.5, 0.5, 1.0);
 | |
| const SCORE_COLOR: Color = Color::rgb(1.0, 0.5, 0.5);
 | |
| 
 | |
| fn main() {
 | |
|     App::new()
 | |
|         .add_plugins(DefaultPlugins)
 | |
|         .insert_resource(Scoreboard { score: 0 })
 | |
|         .insert_resource(ClearColor(BACKGROUND_COLOR))
 | |
|         .add_startup_system(setup)
 | |
|         .add_event::<CollisionEvent>()
 | |
|         // Add our gameplay simulation systems to the fixed timestep schedule
 | |
|         .add_systems_to_schedule(
 | |
|             CoreSchedule::FixedUpdate,
 | |
|             (
 | |
|                 check_for_collisions,
 | |
|                 apply_velocity.before(check_for_collisions),
 | |
|                 move_paddle
 | |
|                     .before(check_for_collisions)
 | |
|                     .after(apply_velocity),
 | |
|                 play_collision_sound.after(check_for_collisions),
 | |
|             ),
 | |
|         )
 | |
|         // Configure how frequently our gameplay systems are run
 | |
|         .insert_resource(FixedTime::new_from_secs(TIME_STEP))
 | |
|         .add_system(update_scoreboard)
 | |
|         .add_system(bevy::window::close_on_esc)
 | |
|         .run();
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| struct Paddle;
 | |
| 
 | |
| #[derive(Component)]
 | |
| struct Ball;
 | |
| 
 | |
| #[derive(Component, Deref, DerefMut)]
 | |
| struct Velocity(Vec2);
 | |
| 
 | |
| #[derive(Component)]
 | |
| struct Collider;
 | |
| 
 | |
| #[derive(Default)]
 | |
| struct CollisionEvent;
 | |
| 
 | |
| #[derive(Component)]
 | |
| struct Brick;
 | |
| 
 | |
| #[derive(Resource)]
 | |
| struct CollisionSound(Handle<AudioSource>);
 | |
| 
 | |
| // This bundle is a collection of the components that define a "wall" in our game
 | |
| #[derive(Bundle)]
 | |
| struct WallBundle {
 | |
|     // You can nest bundles inside of other bundles like this
 | |
|     // Allowing you to compose their functionality
 | |
|     sprite_bundle: SpriteBundle,
 | |
|     collider: Collider,
 | |
| }
 | |
| 
 | |
| /// Which side of the arena is this wall located on?
 | |
| enum WallLocation {
 | |
|     Left,
 | |
|     Right,
 | |
|     Bottom,
 | |
|     Top,
 | |
| }
 | |
| 
 | |
| impl WallLocation {
 | |
|     fn position(&self) -> Vec2 {
 | |
|         match self {
 | |
|             WallLocation::Left => Vec2::new(LEFT_WALL, 0.),
 | |
|             WallLocation::Right => Vec2::new(RIGHT_WALL, 0.),
 | |
|             WallLocation::Bottom => Vec2::new(0., BOTTOM_WALL),
 | |
|             WallLocation::Top => Vec2::new(0., TOP_WALL),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fn size(&self) -> Vec2 {
 | |
|         let arena_height = TOP_WALL - BOTTOM_WALL;
 | |
|         let arena_width = RIGHT_WALL - LEFT_WALL;
 | |
|         // Make sure we haven't messed up our constants
 | |
|         assert!(arena_height > 0.0);
 | |
|         assert!(arena_width > 0.0);
 | |
| 
 | |
|         match self {
 | |
|             WallLocation::Left | WallLocation::Right => {
 | |
|                 Vec2::new(WALL_THICKNESS, arena_height + WALL_THICKNESS)
 | |
|             }
 | |
|             WallLocation::Bottom | WallLocation::Top => {
 | |
|                 Vec2::new(arena_width + WALL_THICKNESS, WALL_THICKNESS)
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl WallBundle {
 | |
|     // This "builder method" allows us to reuse logic across our wall entities,
 | |
|     // making our code easier to read and less prone to bugs when we change the logic
 | |
|     fn new(location: WallLocation) -> WallBundle {
 | |
|         WallBundle {
 | |
|             sprite_bundle: SpriteBundle {
 | |
|                 transform: Transform {
 | |
|                     // We need to convert our Vec2 into a Vec3, by giving it a z-coordinate
 | |
|                     // This is used to determine the order of our sprites
 | |
|                     translation: location.position().extend(0.0),
 | |
|                     // The z-scale of 2D objects must always be 1.0,
 | |
|                     // or their ordering will be affected in surprising ways.
 | |
|                     // See https://github.com/bevyengine/bevy/issues/4149
 | |
|                     scale: location.size().extend(1.0),
 | |
|                     ..default()
 | |
|                 },
 | |
|                 sprite: Sprite {
 | |
|                     color: WALL_COLOR,
 | |
|                     ..default()
 | |
|                 },
 | |
|                 ..default()
 | |
|             },
 | |
|             collider: Collider,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| // This resource tracks the game's score
 | |
| #[derive(Resource)]
 | |
| struct Scoreboard {
 | |
|     score: usize,
 | |
| }
 | |
| 
 | |
| // Add the game's entities to our world
 | |
| fn setup(
 | |
|     mut commands: Commands,
 | |
|     mut meshes: ResMut<Assets<Mesh>>,
 | |
|     mut materials: ResMut<Assets<ColorMaterial>>,
 | |
|     asset_server: Res<AssetServer>,
 | |
| ) {
 | |
|     // Camera
 | |
|     commands.spawn(Camera2dBundle::default());
 | |
| 
 | |
|     // Sound
 | |
|     let ball_collision_sound = asset_server.load("sounds/breakout_collision.ogg");
 | |
|     commands.insert_resource(CollisionSound(ball_collision_sound));
 | |
| 
 | |
|     // Paddle
 | |
|     let paddle_y = BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR;
 | |
| 
 | |
|     commands.spawn((
 | |
|         SpriteBundle {
 | |
|             transform: Transform {
 | |
|                 translation: Vec3::new(0.0, paddle_y, 0.0),
 | |
|                 scale: PADDLE_SIZE,
 | |
|                 ..default()
 | |
|             },
 | |
|             sprite: Sprite {
 | |
|                 color: PADDLE_COLOR,
 | |
|                 ..default()
 | |
|             },
 | |
|             ..default()
 | |
|         },
 | |
|         Paddle,
 | |
|         Collider,
 | |
|     ));
 | |
| 
 | |
|     // Ball
 | |
|     commands.spawn((
 | |
|         MaterialMesh2dBundle {
 | |
|             mesh: meshes.add(shape::Circle::default().into()).into(),
 | |
|             material: materials.add(ColorMaterial::from(BALL_COLOR)),
 | |
|             transform: Transform::from_translation(BALL_STARTING_POSITION).with_scale(BALL_SIZE),
 | |
|             ..default()
 | |
|         },
 | |
|         Ball,
 | |
|         Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED),
 | |
|     ));
 | |
| 
 | |
|     // Scoreboard
 | |
|     commands.spawn(
 | |
|         TextBundle::from_sections([
 | |
|             TextSection::new(
 | |
|                 "Score: ",
 | |
|                 TextStyle {
 | |
|                     font: asset_server.load("fonts/FiraSans-Bold.ttf"),
 | |
|                     font_size: SCOREBOARD_FONT_SIZE,
 | |
|                     color: TEXT_COLOR,
 | |
|                 },
 | |
|             ),
 | |
|             TextSection::from_style(TextStyle {
 | |
|                 font: asset_server.load("fonts/FiraMono-Medium.ttf"),
 | |
|                 font_size: SCOREBOARD_FONT_SIZE,
 | |
|                 color: SCORE_COLOR,
 | |
|             }),
 | |
|         ])
 | |
|         .with_style(Style {
 | |
|             position_type: PositionType::Absolute,
 | |
|             position: UiRect {
 | |
|                 top: SCOREBOARD_TEXT_PADDING,
 | |
|                 left: SCOREBOARD_TEXT_PADDING,
 | |
|                 ..default()
 | |
|             },
 | |
|             ..default()
 | |
|         }),
 | |
|     );
 | |
| 
 | |
|     // Walls
 | |
|     commands.spawn(WallBundle::new(WallLocation::Left));
 | |
|     commands.spawn(WallBundle::new(WallLocation::Right));
 | |
|     commands.spawn(WallBundle::new(WallLocation::Bottom));
 | |
|     commands.spawn(WallBundle::new(WallLocation::Top));
 | |
| 
 | |
|     // Bricks
 | |
|     // Negative scales result in flipped sprites / meshes,
 | |
|     // which is definitely not what we want here
 | |
|     assert!(BRICK_SIZE.x > 0.0);
 | |
|     assert!(BRICK_SIZE.y > 0.0);
 | |
| 
 | |
|     let total_width_of_bricks = (RIGHT_WALL - LEFT_WALL) - 2. * GAP_BETWEEN_BRICKS_AND_SIDES;
 | |
|     let bottom_edge_of_bricks = paddle_y + GAP_BETWEEN_PADDLE_AND_BRICKS;
 | |
|     let total_height_of_bricks = TOP_WALL - bottom_edge_of_bricks - GAP_BETWEEN_BRICKS_AND_CEILING;
 | |
| 
 | |
|     assert!(total_width_of_bricks > 0.0);
 | |
|     assert!(total_height_of_bricks > 0.0);
 | |
| 
 | |
|     // Given the space available, compute how many rows and columns of bricks we can fit
 | |
|     let n_columns = (total_width_of_bricks / (BRICK_SIZE.x + GAP_BETWEEN_BRICKS)).floor() as usize;
 | |
|     let n_rows = (total_height_of_bricks / (BRICK_SIZE.y + GAP_BETWEEN_BRICKS)).floor() as usize;
 | |
|     let n_vertical_gaps = n_columns - 1;
 | |
| 
 | |
|     // Because we need to round the number of columns,
 | |
|     // the space on the top and sides of the bricks only captures a lower bound, not an exact value
 | |
|     let center_of_bricks = (LEFT_WALL + RIGHT_WALL) / 2.0;
 | |
|     let left_edge_of_bricks = center_of_bricks
 | |
|         // Space taken up by the bricks
 | |
|         - (n_columns as f32 / 2.0 * BRICK_SIZE.x)
 | |
|         // Space taken up by the gaps
 | |
|         - n_vertical_gaps as f32 / 2.0 * GAP_BETWEEN_BRICKS;
 | |
| 
 | |
|     // In Bevy, the `translation` of an entity describes the center point,
 | |
|     // not its bottom-left corner
 | |
|     let offset_x = left_edge_of_bricks + BRICK_SIZE.x / 2.;
 | |
|     let offset_y = bottom_edge_of_bricks + BRICK_SIZE.y / 2.;
 | |
| 
 | |
|     for row in 0..n_rows {
 | |
|         for column in 0..n_columns {
 | |
|             let brick_position = Vec2::new(
 | |
|                 offset_x + column as f32 * (BRICK_SIZE.x + GAP_BETWEEN_BRICKS),
 | |
|                 offset_y + row as f32 * (BRICK_SIZE.y + GAP_BETWEEN_BRICKS),
 | |
|             );
 | |
| 
 | |
|             // brick
 | |
|             commands.spawn((
 | |
|                 SpriteBundle {
 | |
|                     sprite: Sprite {
 | |
|                         color: BRICK_COLOR,
 | |
|                         ..default()
 | |
|                     },
 | |
|                     transform: Transform {
 | |
|                         translation: brick_position.extend(0.0),
 | |
|                         scale: Vec3::new(BRICK_SIZE.x, BRICK_SIZE.y, 1.0),
 | |
|                         ..default()
 | |
|                     },
 | |
|                     ..default()
 | |
|                 },
 | |
|                 Brick,
 | |
|                 Collider,
 | |
|             ));
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn move_paddle(
 | |
|     keyboard_input: Res<Input<KeyCode>>,
 | |
|     mut query: Query<&mut Transform, With<Paddle>>,
 | |
| ) {
 | |
|     let mut paddle_transform = query.single_mut();
 | |
|     let mut direction = 0.0;
 | |
| 
 | |
|     if keyboard_input.pressed(KeyCode::Left) {
 | |
|         direction -= 1.0;
 | |
|     }
 | |
| 
 | |
|     if keyboard_input.pressed(KeyCode::Right) {
 | |
|         direction += 1.0;
 | |
|     }
 | |
| 
 | |
|     // Calculate the new horizontal paddle position based on player input
 | |
|     let new_paddle_position = paddle_transform.translation.x + direction * PADDLE_SPEED * TIME_STEP;
 | |
| 
 | |
|     // Update the paddle position,
 | |
|     // making sure it doesn't cause the paddle to leave the arena
 | |
|     let left_bound = LEFT_WALL + WALL_THICKNESS / 2.0 + PADDLE_SIZE.x / 2.0 + PADDLE_PADDING;
 | |
|     let right_bound = RIGHT_WALL - WALL_THICKNESS / 2.0 - PADDLE_SIZE.x / 2.0 - PADDLE_PADDING;
 | |
| 
 | |
|     paddle_transform.translation.x = new_paddle_position.clamp(left_bound, right_bound);
 | |
| }
 | |
| 
 | |
| fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) {
 | |
|     for (mut transform, velocity) in &mut query {
 | |
|         transform.translation.x += velocity.x * TIME_STEP;
 | |
|         transform.translation.y += velocity.y * TIME_STEP;
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn update_scoreboard(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) {
 | |
|     let mut text = query.single_mut();
 | |
|     text.sections[1].value = scoreboard.score.to_string();
 | |
| }
 | |
| 
 | |
| fn check_for_collisions(
 | |
|     mut commands: Commands,
 | |
|     mut scoreboard: ResMut<Scoreboard>,
 | |
|     mut ball_query: Query<(&mut Velocity, &Transform), With<Ball>>,
 | |
|     collider_query: Query<(Entity, &Transform, Option<&Brick>), With<Collider>>,
 | |
|     mut collision_events: EventWriter<CollisionEvent>,
 | |
| ) {
 | |
|     let (mut ball_velocity, ball_transform) = ball_query.single_mut();
 | |
|     let ball_size = ball_transform.scale.truncate();
 | |
| 
 | |
|     // check collision with walls
 | |
|     for (collider_entity, transform, maybe_brick) in &collider_query {
 | |
|         let collision = collide(
 | |
|             ball_transform.translation,
 | |
|             ball_size,
 | |
|             transform.translation,
 | |
|             transform.scale.truncate(),
 | |
|         );
 | |
|         if let Some(collision) = collision {
 | |
|             // Sends a collision event so that other systems can react to the collision
 | |
|             collision_events.send_default();
 | |
| 
 | |
|             // Bricks should be despawned and increment the scoreboard on collision
 | |
|             if maybe_brick.is_some() {
 | |
|                 scoreboard.score += 1;
 | |
|                 commands.entity(collider_entity).despawn();
 | |
|             }
 | |
| 
 | |
|             // reflect the ball when it collides
 | |
|             let mut reflect_x = false;
 | |
|             let mut reflect_y = false;
 | |
| 
 | |
|             // only reflect if the ball's velocity is going in the opposite direction of the
 | |
|             // collision
 | |
|             match collision {
 | |
|                 Collision::Left => reflect_x = ball_velocity.x > 0.0,
 | |
|                 Collision::Right => reflect_x = ball_velocity.x < 0.0,
 | |
|                 Collision::Top => reflect_y = ball_velocity.y < 0.0,
 | |
|                 Collision::Bottom => reflect_y = ball_velocity.y > 0.0,
 | |
|                 Collision::Inside => { /* do nothing */ }
 | |
|             }
 | |
| 
 | |
|             // reflect velocity on the x-axis if we hit something on the x-axis
 | |
|             if reflect_x {
 | |
|                 ball_velocity.x = -ball_velocity.x;
 | |
|             }
 | |
| 
 | |
|             // reflect velocity on the y-axis if we hit something on the y-axis
 | |
|             if reflect_y {
 | |
|                 ball_velocity.y = -ball_velocity.y;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn play_collision_sound(
 | |
|     mut collision_events: EventReader<CollisionEvent>,
 | |
|     audio: Res<Audio>,
 | |
|     sound: Res<CollisionSound>,
 | |
| ) {
 | |
|     // Play a sound once per frame if a collision occurred.
 | |
|     if !collision_events.is_empty() {
 | |
|         // This prevents events staying active on the next frame.
 | |
|         collision_events.clear();
 | |
|         audio.play(sound.0.clone());
 | |
|     }
 | |
| }
 |