diff --git a/Cargo.toml b/Cargo.toml index a3d3a2ab63e51..2fdf729522eeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4361,3 +4361,14 @@ name = "Extended Bindless Material" description = "Demonstrates bindless `ExtendedMaterial`" category = "Shaders" wasm = false + +[[example]] +name = "flappy_bird" +path = "examples/games/flappy_bird.rs" +doc-scrape-examples = true + +[package.metadata.example.flappy_bird] +name = "Flappy Bird" +description = "An implementation of the game \"Flappy Bird\"." +category = "Games" +wasm = true diff --git a/examples/README.md b/examples/README.md index 060683f96d891..6a42d7010d3b5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -349,6 +349,7 @@ Example | Description [Breakout](../examples/games/breakout.rs) | An implementation of the classic game "Breakout". [Contributors](../examples/games/contributors.rs) | Displays each contributor as a bouncy bevy-ball! [Desk Toy](../examples/games/desk_toy.rs) | Bevy logo as a desk toy using transparent windows! Now with Googly Eyes! +[Flappy Bird](../examples/games/flappy_bird.rs) | An implementation of the game "Flappy Bird". [Game Menu](../examples/games/game_menu.rs) | A simple game menu [Loading Screen](../examples/games/loading_screen.rs) | Demonstrates how to create a loading screen that waits for all assets to be loaded and render pipelines to be compiled. diff --git a/examples/games/flappy_bird.rs b/examples/games/flappy_bird.rs new file mode 100644 index 0000000000000..5c82384da6f49 --- /dev/null +++ b/examples/games/flappy_bird.rs @@ -0,0 +1,516 @@ +//! An implementation of the game "Flappy Bird". + +use std::time::Duration; + +use bevy::math::{ + bounding::{Aabb2d, BoundingCircle, IntersectsVolume}, + ops::exp, +}; +use bevy::prelude::*; +use rand::Rng; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +enum State { + #[default] + MainMenu, + InGame, + GameOver, +} + +#[derive(Resource, Reflect)] +struct Settings { + background_color: Color, + + /// Timer spawning a pipe each time it finishes + pipe_timer_duration: Duration, + + /// Movement speed of the pipes + pipe_speed: f32, + + /// The size of each pipe rectangle + pipe_size: Vec2, + + /// How large the gap is between the pipes + gap_height: f32, + + /// Gravity applied to the bird + gravity: f32, + + /// Size of the bird sprite + bird_size: f32, + + /// Acceleration the bird is set to on a flap + flap_power: f32, + + /// Horizontal position of the bird + bird_position: f32, +} + +impl Default for Settings { + fn default() -> Self { + Self { + background_color: Color::srgb(0.9, 0.9, 0.9), + pipe_timer_duration: Duration::from_millis(2000), + pipe_speed: 200., + pipe_size: Vec2::new(100., 500.), + gap_height: 300., + gravity: 700., + bird_size: 100., + flap_power: 400., + bird_position: -500., + } + } +} + +#[derive(Component)] +struct Bird; + +#[derive(Component)] +struct Pipe; + +#[derive(Component)] +struct PipeMarker; + +/// Marker component for the text displaying the score +#[derive(Component)] +struct ScoreText; + +#[derive(Component)] +struct StartButton; + +#[derive(Component)] +struct OnMainMenu; + +#[derive(Component)] +struct OnGameScreen; + +#[derive(Component)] +struct OnGameOverScreen; + +/// This resource tracks the game's score +#[derive(Resource, Deref, DerefMut)] +struct Score(usize); + +/// 2-dimensional velocity +#[derive(Component, Deref, DerefMut)] +struct Velocity(Vec2); + +/// Timer that determines when new pipes are spawned +#[derive(Resource, Deref, DerefMut)] +struct PipeTimer(Timer); + +/// Event emitted when a new pipe should be spawned +#[derive(Event, Default)] +struct SpawnPipeEvent; + +/// Sound that should be played when a pipe is passed +#[derive(Resource, Deref)] +struct ScoreSound(Handle); + +fn main() { + let settings = Settings::default(); + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(OnEnter(State::MainMenu), setup_main_menu) + .add_systems(OnExit(State::MainMenu), despawn_screen::) + .add_systems(OnEnter(State::GameOver), setup_game_over) + .add_systems(OnExit(State::GameOver), despawn_screen::) + .add_systems( + Update, + handle_start_button.run_if(in_state(State::MainMenu)), + ) + .add_systems( + Update, + handle_start_button.run_if(in_state(State::GameOver)), + ) + .add_systems(OnEnter(State::InGame), setup_game) + .add_systems(OnExit(State::InGame), teardown_game) + .add_systems( + FixedUpdate, + ( + add_pipes, + spawn_pipe, + flap, + apply_gravity, + apply_velocity, + check_collisions, + increase_score, + remove_pipes, + ) + .run_if(in_state(State::InGame)), + ) + .insert_resource(Score(0)) + .insert_resource(ClearColor(settings.background_color)) + .insert_resource(PipeTimer(Timer::new( + settings.pipe_timer_duration, + TimerMode::Repeating, + ))) + .insert_resource(settings) + .insert_state(State::MainMenu) + .add_event::() + .run(); +} + +fn despawn_screen(menu: Single>, mut commands: Commands) { + commands.entity(*menu).despawn(); +} + +/// Set up the camera and score UI +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2d); + + // TODO: Replace with a custom sound, or rename file + let score_sound = asset_server.load("sounds/breakout_collision.ogg"); + commands.insert_resource(ScoreSound(score_sound)); +} + +fn setup_main_menu(mut commands: Commands) { + commands.spawn(( + OnMainMenu, + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + flex_direction: FlexDirection::Column, + ..default() + }, + children![ + ( + Text::new("Flipper Birb"), + TextFont { + font_size: 66.0, + ..default() + }, + TextColor(Color::BLACK), + ), + ( + Button, + StartButton, + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + margin: UiRect::top(Val::Px(20.0)), + ..default() + }, + BorderColor(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(Color::srgb(0.15, 0.15, 0.15)), + children![( + Text::new("Start"), + TextFont { + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + ) + ], + )); +} + +fn handle_start_button( + query: Query<&Interaction, (Changed, With, With