From 6e4a60fa3eadf5480ad409313cb3a2c58c0a0365 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sun, 18 May 2025 22:04:11 +0200 Subject: [PATCH 1/4] add initial implementation of the Flappy Bird example --- Cargo.toml | 11 ++ examples/README.md | 1 + examples/games/flappy_bird.rs | 360 ++++++++++++++++++++++++++++++++++ 3 files changed, 372 insertions(+) create mode 100644 examples/games/flappy_bird.rs 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..4b498c8323613 --- /dev/null +++ b/examples/games/flappy_bird.rs @@ -0,0 +1,360 @@ +//! 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; + +const BACKGROUND_COLOR: Color = Color::srgb(0.9, 0.9, 0.9); + +/// Timer spawning a pipe each time it finishes +const PIPE_TIMER_DURATION: Duration = Duration::from_millis(2000); + +/// Movement speed of the pipes +const PIPE_SPEED: f32 = 200.; + +/// The size of each pipe rectangle +const PIPE_SIZE: Vec2 = Vec2::new(100., 500.); + +/// How large the gap is between the pipes +const GAP_HEIGHT: f32 = 300.; + +/// Gravity applied to the bird +const GRAVITY: f32 = 700.; + +/// Size of the bird sprite +const BIRD_SIZE: f32 = 100.; + +/// Acceleration the bird is set to on a flap +const FLAP_POWER: f32 = 400.; + +/// Horizontal position of the bird +const BIRD_POSITION: f32 = -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; + +/// 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); + +/// The size of the window at the start of the game +/// +/// Handling resizing while the game is playing is quite hard, so we ignore that +#[derive(Resource, Deref, DerefMut)] +struct WindowSize(Vec2); + +/// Event emitted when the bird touches the edges or a pipe +#[derive(Event, Default)] +struct CollisionEvent; + +/// 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() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, (set_window_size, setup)) + .add_systems( + FixedUpdate, + ( + reset, + add_pipes, + spawn_pipe, + flap, + apply_gravity, + apply_velocity, + check_collisions, + increase_score, + remove_pipes, + ), + ) + .insert_resource(Score(0)) + .insert_resource(ClearColor(BACKGROUND_COLOR)) + .insert_resource(PipeTimer(Timer::new( + PIPE_TIMER_DURATION, + TimerMode::Repeating, + ))) + .insert_resource(WindowSize(Vec2::ZERO)) + .add_event::() + .add_event::() + .run(); +} + +/// Set up the camera and score UI +fn setup( + mut commands: Commands, + mut collision_events: EventWriter, + asset_server: Res, +) { + commands.spawn(Camera2d); + + let score_sound = asset_server.load("sounds/breakout_collision.ogg"); + commands.insert_resource(ScoreSound(score_sound)); + + // Spawn the score UI. + commands.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Start, + justify_content: JustifyContent::Center, + padding: UiRect::all(Val::Px(10.0)), + ..default() + }, + children![( + ScoreText, + Text::new("0"), + TextFont { + font_size: 66.0, + ..default() + }, + TextColor(Color::srgb(0.3, 0.3, 0.9)), + )], + )); + + // Create a collision event to trigger a reset + collision_events.write_default(); +} + +/// Clear everything and put everything to its start state +fn reset( + mut commands: Commands, + mut timer: ResMut, + mut score: ResMut, + mut collision_events: EventReader, + mut spawn_pipe_events: EventWriter, + score_text: Query<&mut Text, With>, + pipes: Query>, + pipe_markers: Query>, + bird: Query>, + asset_server: Res, +) { + if collision_events.is_empty() { + return; + } + + collision_events.clear(); + + // Remove any entities left over from the previous game (if any) + for ent in bird { + commands.entity(ent).despawn(); + } + + for ent in pipes { + commands.entity(ent).despawn(); + } + + for ent in pipe_markers { + commands.entity(ent).despawn(); + } + + // Set the score to 0 + score.0 = 0; + for mut text in score_text { + text.0 = 0.to_string(); + } + + // Spawn a new bird + commands.spawn(( + Bird, + Sprite { + image: asset_server.load("branding/icon.png"), + custom_size: Some(Vec2::splat(BIRD_SIZE)), + ..default() + }, + Transform::from_xyz(BIRD_POSITION, 0., 0.), + Velocity(Vec2::new(0., FLAP_POWER)), + )); + + timer.reset(); + spawn_pipe_events.write_default(); +} + +fn set_window_size(window: Single<&mut Window>, mut window_size: ResMut) { + window_size.0 = Vec2::new(window.resolution.width(), window.resolution.height()); +} + +/// Flap on a spacebar or left mouse button press +fn flap( + keyboard_input: Res>, + mouse_input: Res>, + mut bird_velocity: Single<&mut Velocity, With>, +) { + if keyboard_input.pressed(KeyCode::Space) || mouse_input.pressed(MouseButton::Left) { + bird_velocity.y = FLAP_POWER; + } +} + +/// Apply gravity to the bird and set its rotation +fn apply_gravity(mut bird: Single<(&mut Transform, &mut Velocity), With>, time: Res