diff --git a/Cargo.toml b/Cargo.toml index 120fd6d67981c..cab6b379cdf8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2058,6 +2058,17 @@ description = "Demonstrates how to create a loading screen that waits for all as category = "Games" wasm = true +[[example]] +name = "flappy_bevy" +path = "examples/games/flappy_bevy.rs" +doc-scrape-examples = true + +[package.metadata.example.flappy_bevy] +name = "Flappy Bevy" +description = "It's Flappy Bird, but an entire flock." +category = "Games" +wasm = true + # Input [[example]] name = "char_input_events" diff --git a/examples/README.md b/examples/README.md index 616e37648aed0..ec7475c50b262 100644 --- a/examples/README.md +++ b/examples/README.md @@ -314,6 +314,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 Bevy](../examples/games/flappy_bevy.rs) | It's Flappy Bird, but an entire flock. [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_bevy.rs b/examples/games/flappy_bevy.rs new file mode 100644 index 0000000000000..041db0c7bfc53 --- /dev/null +++ b/examples/games/flappy_bevy.rs @@ -0,0 +1,535 @@ +//! A simplified Flappy Bird but with many birds. Press space to flap. + +use bevy::ecs::system::RunSystemOnce; +use bevy::input::common_conditions::input_just_pressed; +use bevy::math::bounding::{Aabb2d, BoundingCircle, IntersectsVolume}; +use bevy::window::PrimaryWindow; +use bevy::{ecs::world::Command, prelude::*}; + +use rand::random; + +const CAMERA_SPEED: f32 = 120.0; + +#[derive(States, Debug, Hash, PartialEq, Eq, Clone, Default)] +enum GameState { + #[default] + Loading, + Over, + Playing, +} + +fn main() { + let mut app = App::new(); + + app.add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + // TODO: remove + resizable: false, + ..Default::default() + }), + ..Default::default() + })); + app.init_state::(); + app.enable_state_scoped_entities::(); + app.add_plugins(( + asset_plugin, + bird_plugin, + physics_plugin, + terrain_plugin, + game_over_plugin, + )); + app.configure_sets( + FixedUpdate, + AppSet::Physics.run_if(in_state(GameState::Playing)), + ); + app.configure_sets(Update, AppSet::Loading.run_if(in_state(GameState::Loading))); + app.configure_sets( + Update, + (AppSet::RecordInput, AppSet::Playing) + .chain() + .run_if(in_state(GameState::Playing)), + ); + app.run(); +} + +// High-level groupings of systems for the game. +#[derive(SystemSet, Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] +enum AppSet { + // Record player input. + RecordInput, + // Anything to do with gravity or velocity. + Physics, + // Asset loading updates. + Loading, + // In-game updates. + Playing, +} + +// Asset Plugin +// +// Normally, plugins would be separated into different modules. For the purposes of this example, +// everything is in one file but it's a valuable pattern to keep in mind. With the aid of `State`s +// and `SystemSet`s, we can often separate a Bevy app into areas of concern. It's not uncommon to +// nest plugins within plugins, so long as it makes sense for your overall design. +fn asset_plugin(app: &mut App) { + app.add_systems(Startup, load_assets); + app.add_systems(Update, wait_for_asset_load.in_set(AppSet::Loading)); +} + +#[derive(Resource)] +struct TextureAssets { + bird: Handle, +} + +fn load_assets(mut commands: Commands, asset_server: Res) { + commands.insert_resource(TextureAssets { + bird: asset_server.load("branding/icon.png"), + }); +} + +fn wait_for_asset_load( + asset_server: Res, + mut next_state: ResMut>, + texture_assets: Res, +) { + if asset_server.is_loaded_with_dependencies(&texture_assets.bird) { + next_state.set(GameState::Playing); + } +} + +// Bird Plugin +fn bird_plugin(app: &mut App) { + app.init_resource::(); + // This will run once we've finished loading assets and we know we're ready to go. + app.add_systems(OnEnter(GameState::Playing), setup); + app.add_systems(Update, input.in_set(AppSet::RecordInput)); + app.add_systems(Update, reproduction.in_set(AppSet::Playing)); +} + +#[derive(Component)] +struct Bird; + +#[derive(Resource)] +struct FlockSettings { + pub bird_size: f32, + pub drift: Vec2, + pub max_birds: usize, + pub reproduction_chance: f32, +} + +impl Default for FlockSettings { + fn default() -> Self { + Self { + bird_size: 24.0, + drift: Vec2::new(2.0, 2.0), + max_birds: 500, + reproduction_chance: 1.0, + } + } +} + +struct SpawnBird { + translation: Vec3, + velocity: Vec2, +} + +// This allows us to `queue` up a `Command` to spawn a `Bird`, with whatever configuration we might +// need to display it correctly. +impl Command for SpawnBird { + fn apply(self, world: &mut World) { + world.run_system_once_with(self, spawn_bird); + } +} + +fn spawn_bird( + In(config): In, + mut commands: Commands, + texture_assets: Res, +) { + commands.spawn(( + Name::new("Bird"), + Bird, + Gravity, + MovementController::default(), + SpriteBundle { + sprite: Sprite { + color: Color::srgb(random::(), random::(), random::()), + ..default() + }, + texture: texture_assets.bird.clone(), + transform: Transform::from_translation(config.translation).with_scale(Vec3::splat(0.1)), + ..default() + }, + StateScoped(GameState::Playing), + Velocity(config.velocity), + )); +} + +fn setup(mut commands: Commands, cameras: Query<&Camera>) { + dbg!(cameras.iter().count()); + commands.queue(SpawnBird { + translation: Vec3::ZERO, + velocity: Vec2::ZERO, + }); + + commands.spawn(( + Name::new("Camera"), + Camera2dBundle::default(), + MovementController { + intent: Vec2::new(1.0, 0.0), + horizontal_speed: CAMERA_SPEED, + // We never need to move the camera vertically. + vertical_speed: 0.0, + }, + StateScoped(GameState::Playing), + Velocity(Vec2::new(CAMERA_SPEED, 0.0)), + )); +} + +fn input(input: Res>, mut moveable: Query<&mut MovementController>) { + // Per the genre, flappy games characters continually move at a constant rate along the x axis. + // So our "intent" is always positive for `x`. + let mut intent = Vec2::new(1.0, 0.0); + + if input.just_pressed(KeyCode::Space) { + // We'd like all the birds to "flap". + intent.y = 1.0; + } + for mut controller in &mut moveable { + controller.intent = intent; + } +} + +fn reproduction( + mut commands: Commands, + birds: Query<(&Transform, &Velocity), With>, + flock_settings: Res, + mut next_state: ResMut>, + time: Res