// Bevy basically forces "complex types" with Querys #![allow(clippy::type_complexity)] use games::physics3d::*; use games::*; fn main() { App::new() .add_plugins(( BaseGamePlugin { name: "flappy bird (with rewind)".into(), ..default() }, Physics3dPlugin, )) .init_state::() .add_systems( Startup, ( init_bird, init_obstacles, init_ui, tweak_camera.after(create_camera_3d), ), ) .add_systems(OnEnter(PlayerState::Alive), alive_bird) .add_systems(OnEnter(PlayerState::Pause), pause_bird) .add_systems(OnEnter(PlayerState::Stasis), pause_bird) .add_systems(OnEnter(PlayerState::Rewind), pause_bird) .add_systems( Update, ( // Systems to run when player state changes ( // Print out when this state changes for debugging purposes debug_state_changes::, // Toggle (UI) elements when the player dies/alives toggle_state_visibility::, ) .run_if(state_changed::), // Detect if the bird is "dead" by checking if it is visible // from the point of view of the camera detect_dead.run_if(in_state(PlayerState::Alive)), // Toggle rewinding state when "R" is pressed/released toggle_rewind.run_if( input_just_pressed(KeyCode::KeyR).or(input_just_released(KeyCode::KeyR)), ), // Systems to run in the "play" state ( // Only flap when we press the space key flap.run_if(input_just_pressed(KeyCode::Space)), // Rewinding systems record.run_if(any_component_changed::), ) .run_if(in_state(PlayerState::Alive)), // Rewinding systems rewind.run_if(in_state(PlayerState::Rewind)), // Camera follows when bird moves regardless of player state camera_follow_bird.run_if(any_component_changed::), // Pause when the player presses Escape pause_game.run_if(input_just_pressed(KeyCode::Escape)), // Transition out of the pause screen if the player presses space un_pause_game .run_if(input_just_pressed(KeyCode::Space)) .run_if(in_state(PlayerState::Pause)), ), ) .run(); } fn tweak_camera(mut camera: Query<(&mut Camera, &mut AmbientLight, &mut Transform), With>) { camera.iter_mut().for_each(|(mut c, mut al, mut t)| { c.clear_color = ClearColorConfig::Custom(WHITE.into()); al.brightness = 100.0; // move the camera "back" so everything else is at 0 on the Z axis t.translation.z = 10.0; }); } #[derive(Component)] struct Bird; #[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)] enum PlayerState { Alive, Rewind, Stasis, #[default] Pause, } // A tape tracking the bird's state every frame #[derive(Component, Default)] struct Tape { translations: Vec, rotations: Vec, linear_velocities: Vec, angular_velocities: Vec, } fn init_bird( mut commands: Commands, server: Res, mut meshes: ResMut>, mut materials: ResMut>, ) { let material = MeshMaterial3d(materials.add(StandardMaterial { base_color_texture: Some(server.load("flappy/bevy.png")), base_color: WHITE.into(), alpha_mode: AlphaMode::Blend, ..default() })); let mesh = Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)))); let name = Name::new("bird"); let t = Transform::from_xyz(0.0, 0.0, 0.0).with_rotation(Quat::from_rotation_x(PI / 2.0)); let physics = ( RigidBody::Static, Collider::capsule(1.0, 1.0), Mass(1.0), ExternalImpulse::default().with_persistence(false), LockedAxes::ROTATION_LOCKED.lock_translation_z(), ); let tape = Tape::default(); commands.spawn((name, mesh, material, physics, t, Bird, tape)); } #[derive(Component, Clone)] struct Ground; #[derive(Component, Clone)] struct Pipe; fn init_obstacles( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { let ground = { let material = MeshMaterial3d(materials.add(StandardMaterial { base_color: LIGHT_GREEN.into(), ..default() })); let mesh = Mesh3d(meshes.add(Cuboid::new(10.0, 1.0, 1.0))); let name = Name::new("ground"); let physics = (RigidBody::Static, Collider::cuboid(1.0, 1.0, 1.0)); (name, mesh, material, physics, Ground) }; let pipe = { let material = MeshMaterial3d(materials.add(StandardMaterial { base_color: GREEN.into(), ..default() })); let mesh = Mesh3d(meshes.add(Cuboid::new(1.0, 3.0, 1.0))); let physics = (RigidBody::Static, Collider::cuboid(1.0, 3.0, 1.0)); let name = Name::new("pipe"); ( name.clone(), mesh.clone(), material.clone(), physics.clone(), Pipe, ) }; // TODO: Instead of spawning infinite floor/pipes, we should instead spawn enough for 1-3 a few // screens and then "move" them around. // This is considerably more complexity so can be implemented later, but would keep memory // overhead fairly static. (1..99).for_each(|i| { // TODO: Jitter up/down/close/far of pipes for challenge let above = Transform::from_xyz(5.0 * i as f32, 4.0, 0.0); let below = Transform::from_xyz(5.0 * i as f32, -4.0, 0.0); let floor = Transform::from_xyz(1.0 * i as f32, -4.0, 0.0); commands.spawn((pipe.clone(), above)); commands.spawn((pipe.clone(), below)); commands.spawn((ground.clone(), floor)); }); } fn init_ui(mut commands: Commands) { commands.spawn(( Node { align_self: AlignSelf::Center, justify_self: JustifySelf::Center, flex_direction: FlexDirection::Column, ..default() }, PlayerState::Stasis, children![Text::new("You Died"), Text::new("Press R to Rewind"),], )); fn start_game(_trigger: Trigger>, mut next: ResMut>) { next.set(PlayerState::Alive); } commands .spawn(( Node { align_self: AlignSelf::Center, justify_self: JustifySelf::Center, flex_direction: FlexDirection::Column, ..default() }, Button, // TODO: Add Pause (basically Stasis) state PlayerState::Pause, children![Text::new("Go!"),], )) .observe(start_game); fn start_rewind(trigger: Trigger>, mut next: ResMut>) { next.set(PlayerState::Rewind); } fn end_rewind(trigger: Trigger>, mut next: ResMut>) { next.set(PlayerState::Alive); } commands .spawn(( Node { align_self: AlignSelf::End, justify_self: JustifySelf::Center, flex_direction: FlexDirection::Column, ..default() }, Button, children![Text::new("Rewind!"),], )) .observe(start_rewind) .observe(end_rewind); } /// Pause the game when the player presses "Escape" fn pause_game(mut next: ResMut>) { next.set(PlayerState::Pause); } fn un_pause_game(mut next: ResMut>) { next.set(PlayerState::Alive); } // TODO: Create floor (and ceiling?) // Q: Move bird + camera or move world around bird & camera? // TODO: Obstacles fn flap( #[cfg(debug_assertions)] state: Res>, #[cfg(debug_assertions)] keycode: Res>, mut bird: Query<(&Transform, &mut ExternalImpulse), With>, ) { debug_assert!( matches!(state.get(), PlayerState::Alive), "Only flap when playing" ); debug_assert!( keycode.just_pressed(KeyCode::Space), "Only flap when space is just pressed" ); bird.iter_mut().for_each(|(t, mut f)| { f.apply_impulse(t.rotation * Vec3::NEG_Z * 5.0 + t.rotation * Vec3::X); }); } fn toggle_rewind(keycode: Res>, mut next: ResMut>) { debug_assert!( keycode.just_pressed(KeyCode::KeyR) || keycode.just_released(KeyCode::KeyR), "Only toggle rewind when R is pressed" ); if keycode.just_pressed(KeyCode::KeyR) { debug!("Toggling rewind ON"); next.set(PlayerState::Rewind) } else { debug!("Toggling rewind OFF"); next.set(PlayerState::Alive) } } fn record( #[cfg(debug_assertions)] state: Res>, mut birds: Query<(&Transform, &LinearVelocity, &AngularVelocity, &mut Tape), With>, ) { debug_assert!( matches!(state.get(), PlayerState::Alive), "Only record in the alive state" ); birds.iter_mut().for_each(|(transform, linear_velocity, angular_velocity, mut tape)| { tape.translations.push(transform.translation); tape.rotations.push(transform.rotation); tape.linear_velocities.push(*linear_velocity); tape.angular_velocities.push(*angular_velocity); }); } fn rewind( #[cfg(debug_assertions)] state: Res>, mut birds: Query<(&mut Transform, &mut AngularVelocity, &mut LinearVelocity, &mut Tape), With>, ) { debug_assert!( matches!(state.get(), PlayerState::Rewind), "Only rewind in the rewinding state" ); birds.iter_mut().for_each(|(mut transform, mut angular_velocity, mut linear_velocity, mut tape)| { if let Some(t) = tape.translations.pop() { transform.translation = t; } if let Some(r) = tape.rotations.pop() { transform.rotation = r; } if let Some(av) = tape.angular_velocities.pop() { *angular_velocity = av; } if let Some(lv) = tape.linear_velocities.pop() { *linear_velocity = lv; } }); } // PERF: May run more than necessary, should be event-driven on aabb intersection fn detect_dead( #[cfg(debug_assertions)] state: Res>, bird: Single<&ColliderAabb, With>, obstacles: Query<&ColliderAabb, Or<(With, With)>>, mut next: ResMut>, ) { debug_assert!( matches!(state.get(), PlayerState::Alive), "Only check if dead while alive" ); if obstacles.iter().any(|obstacle| bird.intersects(obstacle)) { next.set(PlayerState::Stasis); } } fn alive_bird( #[cfg(debug_assertions)] state: Res>, mut bird: Single<&mut RigidBody, With>, ) { debug_assert!(matches!(state.get(), PlayerState::Alive)); debug!("Setting bird to Dynamic"); **bird = RigidBody::Dynamic; } fn pause_bird( #[cfg(debug_assertions)] state: Res>, mut bird: Single<&mut RigidBody, With>, ) { debug_assert!(!matches!(state.get(), PlayerState::Alive)); debug!("Setting bird to Static"); **bird = RigidBody::Static; } fn camera_follow_bird( bird: Single<&Transform, (With, Changed)>, mut camera: Single<&mut Transform, (With, Without)>, ) { camera.translation.x = bird.translation.x; }