use bevy::{input::common_conditions::input_just_released, render::view::VisibleEntities}; use games::*; fn main() { App::new() .add_plugins(BaseGamePlugin { name: "flappy bird (with rewind)".into(), }) .init_state::() .add_systems(Startup, (init_bird, init_ground, init_ui, tweak_camera.after(setup_camera))) .add_systems(OnEnter(PlayerState::Alive), alive_bird) .add_systems(OnExit(PlayerState::Alive), kill_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)), ), ) .run(); } fn tweak_camera( mut camera: Query<(&mut Camera, &mut AmbientLight), With>, ) { camera.iter_mut().for_each(|(mut c, mut al)| { c.clear_color = ClearColorConfig::Custom(WHITE.into()); al.brightness = 100.0; }); } #[derive(Component)] struct Bird; #[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)] enum PlayerState { #[default] Alive, Rewind, Dead, } // A tape tracking the bird's state every frame #[derive(Component, Default)] struct Tape { translations: Vec, rotations: 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, -10.0).with_rotation(Quat::from_rotation_x(PI / 2.0)); let physics = ( RigidBody::Dynamic, Collider::capsule(1.0, 1.0), Mass(1.0), ExternalForce::new(Vec3::X * 1.0).with_persistence(true), 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)] struct Ground; fn init_ground( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { let material = MeshMaterial3d(materials.add(StandardMaterial { base_color: GREEN.into(), ..default() })); let mesh = Mesh3d(meshes.add(Cuboid::new(10.0, 1.0, 1.0))); let name = Name::new("ground"); let t = Transform::from_xyz(0.0, -4.0, -10.0); let physics = (RigidBody::Static, Collider::cuboid(1.0, 1.0, 1.0)); commands.spawn((name, mesh, material, physics, t, Ground)); } fn init_ui( mut commands: Commands, ) { commands.spawn(( Node { align_self: AlignSelf::Center, justify_self: JustifySelf::Center, flex_direction: FlexDirection::Column, ..default() }, PlayerState::Dead, children![ Text::new("You Died"), Text::new("Press R to Rewind"), ], )); } // 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); }); } 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, &mut Tape), With>, ) { debug_assert!( matches!(state.get(), PlayerState::Alive), "Only record in the alive state" ); birds.iter_mut().for_each(|(transform, mut tape)| { tape.translations.push(transform.translation); tape.rotations.push(transform.rotation); }); } fn rewind( #[cfg(debug_assertions)] state: Res>, mut birds: Query<(&mut Transform, &mut Tape), With>, ) { debug_assert!( matches!(state.get(), PlayerState::Rewind), "Only rewind in the rewinding state" ); birds.iter_mut().for_each(|(mut transform, mut tape)| { if let Some(t) = tape.translations.pop() { transform.translation = t; } if let Some(r) = tape.rotations.pop() { transform.rotation = r; } }); } // PERF: Runs more than it needs, should only execute when bird enters/exit frame fn detect_dead( #[cfg(debug_assertions)] state: Res>, bird: Single<&ColliderAabb, With>, ground: Single<&ColliderAabb, With>, mut next: ResMut>, ) { debug_assert!( matches!(state.get(), PlayerState::Alive), "Only check if dead while alive" ); if bird.intersects(*ground) { next.set(PlayerState::Dead); } } fn alive_bird( #[cfg(debug_assertions)] state: Res>, mut bird: Single<&mut RigidBody, With>, ) { debug_assert!(!matches!(state.get(), PlayerState::Dead)); debug!("Aliving bird"); **bird = RigidBody::Dynamic; } fn kill_bird( #[cfg(debug_assertions)] state: Res>, mut bird: Single<&mut RigidBody, With>, ) { debug_assert!(!matches!(state.get(), PlayerState::Alive)); debug!("Killing bird"); **bird = RigidBody::Static; }