// Bevy basically forces "complex types" with Querys #![allow(clippy::type_complexity)] use games::physics2d::*; use games::*; fn main() { App::new() .add_plugins(( BaseGamePlugin { name: "flappy bird (with rewind)".into(), game_type: GameType::Two, }, Physics2dPlugin, )) .insert_resource(Gravity(Vec2::NEG_Y * 9.8 * 100.0)) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_state::() .add_systems( Startup, ( init_bird, init_obstacles, init_ui, tweak_camera.after(create_camera_2d), ), ) .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_kb.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)), // Stats that get synced to the UI ( sync_resource_to_ui::.run_if(resource_changed::), sync_resource_to_ui::.run_if(resource_changed::), sync_resource_to_ui::.run_if(resource_changed::), sync_resource_to_ui::.run_if(resource_changed::), ), scoring.run_if(on_event::), ), ) .add_observer(flap) .run(); } fn tweak_camera(camera: Single<&mut Camera>) { info!("Tweaking camera"); let mut c = camera.into_inner(); c.clear_color = ClearColorConfig::Custom(WHITE.into()); } #[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 = MeshMaterial2d(materials.add(ColorMaterial { texture: Some(server.load("kenny.nl/1-bit-platformer-pack/tile_0380.png")), color: ORANGE.into(), alpha_mode: AlphaMode2d::Blend, ..default() })); let mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0))); let name = Name::new("bird"); let t = Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::splat(100.0)); let physics = ( RigidBody::Static, Collider::circle(0.5), Mass(10.0), ExternalImpulse::default().with_persistence(false), ); let tape = Tape::default(); commands.spawn(( name, mesh, material, physics, t, Bird, tape, CollisionEventsEnabled, )); } #[derive(Component, Clone)] struct Ground; #[derive(Component, Clone)] struct Pipe; #[derive(Component, Clone)] struct Hitbox; fn init_obstacles( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, server: Res, ) { let ground = { let material = MeshMaterial2d(materials.add(ColorMaterial { texture: Some(server.load("kenny.nl/1-bit-platformer-pack/tile_0088.png")), color: BLACK.into(), ..default() })); let mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0))); let name = Name::new("ground"); let physics = (RigidBody::Static, Collider::rectangle(1.0, 1.0)); (name, mesh, material, physics, Ground) }; let pipe = { let material = MeshMaterial2d(materials.add(ColorMaterial { texture: Some(server.load("kenny.nl/1-bit-platformer-pack/tile_0247.png")), color: GREEN.into(), ..default() })); let mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0))); let physics = (RigidBody::Static, Collider::rectangle(1.0, 1.0)); let name = Name::new("pipe"); ( name.clone(), mesh.clone(), material.clone(), physics.clone(), Pipe, ) }; let hitbox = { let physics = ( RigidBody::Static, Collider::rectangle(1.0, 10.0), Sensor, CollisionEventsEnabled, ); let name = Name::new("hitbox"); (name.clone(), physics.clone(), Hitbox) }; // 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..10).for_each(|i| { // TODO: Jitter up/down/close/far of pipes for challenge let above = Transform::from_xyz(300.0 * i as f32, 300.0, -1.0).with_scale(Vec3::splat(100.0)); commands.spawn((pipe.clone(), above)); let below = Transform::from_xyz(300.0 * i as f32, -200.0, -1.0).with_scale(Vec3::splat(100.0)); commands.spawn((pipe.clone(), below)); let hitbox_pos = Transform::from_xyz(300.0 * i as f32, 0.0, 10.0).with_scale(Vec3::splat(100.0)); commands.spawn((hitbox_pos, hitbox.clone())); let floor1 = Transform::from_xyz(300.0 * i as f32 - 100.0, -300.0, 1.0).with_scale(Vec3::splat(100.0)); commands.spawn((ground.clone(), floor1)); let floor2 = Transform::from_xyz(300.0 * i as f32, -300.0, 1.0).with_scale(Vec3::splat(100.0)); commands.spawn((ground.clone(), floor2)); let floor3 = Transform::from_xyz(300.0 * i as f32 + 100.0, -300.0, 1.0).with_scale(Vec3::splat(100.0)); commands.spawn((ground.clone(), floor3)); }); } 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"), TextLayout::new_with_justify(JustifyText::Center) ), ( SyncResource::::default(), Text::default(), TextLayout::new_with_justify(JustifyText::Center) ), ( SyncResource::::default(), Text::default(), TextLayout::new_with_justify(JustifyText::Center) ), ( SyncResource::::default(), Text::default(), TextLayout::new_with_justify(JustifyText::Center) ), ( SyncResource::::default(), Text::default(), TextLayout::new_with_justify(JustifyText::Center) ), ( Text::new("Press R to Rewind"), TextLayout::new_with_justify(JustifyText::Center) ), ], )); 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); commands .spawn(Node { align_self: AlignSelf::End, justify_self: JustifySelf::Center, flex_direction: FlexDirection::Row, ..default() }) .with_children(|parent| { parent .spawn(( Node { ..default() }, Button, children![Text::new("Rewind!"),], )) .observe(start_rewind) .observe(end_rewind); parent .spawn((Node { ..default() }, Button, children![Text::new("Flap!"),])) .observe(flap_button); }); commands.spawn(( Node { align_self: AlignSelf::Start, justify_self: JustifySelf::Center, ..default() }, BackgroundColor(WHITE.into()), SyncResource::::default(), Text::default(), TextLayout::new_with_justify(JustifyText::Center), )); } fn start_rewind(_trigger: Trigger>, mut next: ResMut>) { next.set(PlayerState::Rewind); } fn end_rewind(_trigger: Trigger>, mut next: ResMut>) { next.set(PlayerState::Alive); } fn flap_button( trigger: Trigger>, mut commands: Commands, bird: Single>, ) { let e = *bird; info!("Flapping {:?}", e); commands.trigger_targets(Flap, e); } /// 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 #[derive(Component, Clone, Event)] struct Flap; // Observer for flapping fn flap( trigger: Trigger, mut bird: Query<&mut ExternalImpulse, With>, mut flaps: ResMut, ) { info!("real flap for {:?}", trigger.target()); // Increment flap stat flaps.0 += 1; // Flap birds wings if let Ok(mut f) = bird.get_mut(trigger.target()) { f.apply_impulse(Vec2::Y * 5000.0 + Vec2::X * 1000.0); } } fn flap_kb( #[cfg(debug_assertions)] state: Res>, #[cfg(debug_assertions)] keycode: Res>, birds: Query>, mut commands: Commands, ) { 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" ); birds.iter().for_each(|e| { info!("Flapping {:?}", e); commands.trigger_targets(Flap, e); }); } 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, >, mut frames: ResMut, ) { 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)| { match (tape.translations.pop(), tape.rotations.pop()) { (Some(t), Some(r)) => { transform.translation = t; transform.rotation = r; frames.0 += 1; } (None, None) => (), _ => panic!("Translations and rotations are out of sync!"), } // TODO: Only need to set {angular|linear}_velocity at end of Rewind 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( state: Res>, mut bird: Single<&mut RigidBody, With>, mut deaths: ResMut, ) { debug_assert!(!matches!(state.get(), PlayerState::Alive)); // Increment death count if state.get() == &PlayerState::Stasis { deaths.0 += 1 } 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; } /// How many pipes the player has passed #[derive(Resource, Default)] struct Score(usize); impl Display for Score { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { writeln!(f, "Score: {}", self.0) } } /// Number of frames that were rewound #[derive(Resource, Default)] struct RewindFrames(usize); impl Display for RewindFrames { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { writeln!(f, "Frames Rewound: {}", self.0) } } // Track number of times player flapped their wings #[derive(Resource, Default)] struct Flaps(usize); impl Display for Flaps { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { writeln!(f, "Flaps: {}", self.0) } } // Track number of times player died #[derive(Resource, Default)] struct Deaths(usize); impl Display for Deaths { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { writeln!(f, "Deaths: {}", self.0) } } // TODO: Scoring is bugged, need to make it correct. fn scoring( mut events: EventReader, state: Res>, bird: Query>, hitboxes: Query>, mut score: ResMut, ) { events .read() .filter(|CollisionEnded(a, b)| bird.contains(*a) && hitboxes.contains(*b)) .for_each(|_| { info!("Hit event while {:?}", state.get()); match state.get() { PlayerState::Alive => score.0 = score.0.saturating_add(1), PlayerState::Rewind => score.0 = score.0.saturating_sub(1), PlayerState::Pause | PlayerState::Stasis => (), } }) }