Skip to content

Commit 6e4a60f

Browse files
committed
add initial implementation of the Flappy Bird example
1 parent 6397a28 commit 6e4a60f

File tree

3 files changed

+372
-0
lines changed

3 files changed

+372
-0
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4361,3 +4361,14 @@ name = "Extended Bindless Material"
43614361
description = "Demonstrates bindless `ExtendedMaterial`"
43624362
category = "Shaders"
43634363
wasm = false
4364+
4365+
[[example]]
4366+
name = "flappy_bird"
4367+
path = "examples/games/flappy_bird.rs"
4368+
doc-scrape-examples = true
4369+
4370+
[package.metadata.example.flappy_bird]
4371+
name = "Flappy Bird"
4372+
description = "An implementation of the game \"Flappy Bird\"."
4373+
category = "Games"
4374+
wasm = true

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ Example | Description
349349
[Breakout](../examples/games/breakout.rs) | An implementation of the classic game "Breakout".
350350
[Contributors](../examples/games/contributors.rs) | Displays each contributor as a bouncy bevy-ball!
351351
[Desk Toy](../examples/games/desk_toy.rs) | Bevy logo as a desk toy using transparent windows! Now with Googly Eyes!
352+
[Flappy Bird](../examples/games/flappy_bird.rs) | An implementation of the game "Flappy Bird".
352353
[Game Menu](../examples/games/game_menu.rs) | A simple game menu
353354
[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.
354355

examples/games/flappy_bird.rs

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
//! An implementation of the game "Flappy Bird".
2+
3+
use std::time::Duration;
4+
5+
use bevy::math::{
6+
bounding::{Aabb2d, BoundingCircle, IntersectsVolume},
7+
ops::exp,
8+
};
9+
use bevy::prelude::*;
10+
use rand::Rng;
11+
12+
const BACKGROUND_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);
13+
14+
/// Timer spawning a pipe each time it finishes
15+
const PIPE_TIMER_DURATION: Duration = Duration::from_millis(2000);
16+
17+
/// Movement speed of the pipes
18+
const PIPE_SPEED: f32 = 200.;
19+
20+
/// The size of each pipe rectangle
21+
const PIPE_SIZE: Vec2 = Vec2::new(100., 500.);
22+
23+
/// How large the gap is between the pipes
24+
const GAP_HEIGHT: f32 = 300.;
25+
26+
/// Gravity applied to the bird
27+
const GRAVITY: f32 = 700.;
28+
29+
/// Size of the bird sprite
30+
const BIRD_SIZE: f32 = 100.;
31+
32+
/// Acceleration the bird is set to on a flap
33+
const FLAP_POWER: f32 = 400.;
34+
35+
/// Horizontal position of the bird
36+
const BIRD_POSITION: f32 = -500.;
37+
38+
#[derive(Component)]
39+
struct Bird;
40+
41+
#[derive(Component)]
42+
struct Pipe;
43+
44+
#[derive(Component)]
45+
struct PipeMarker;
46+
47+
/// Marker component for the text displaying the score
48+
#[derive(Component)]
49+
struct ScoreText;
50+
51+
/// This resource tracks the game's score
52+
#[derive(Resource, Deref, DerefMut)]
53+
struct Score(usize);
54+
55+
/// 2-dimensional velocity
56+
#[derive(Component, Deref, DerefMut)]
57+
struct Velocity(Vec2);
58+
59+
/// Timer that determines when new pipes are spawned
60+
#[derive(Resource, Deref, DerefMut)]
61+
struct PipeTimer(Timer);
62+
63+
/// The size of the window at the start of the game
64+
///
65+
/// Handling resizing while the game is playing is quite hard, so we ignore that
66+
#[derive(Resource, Deref, DerefMut)]
67+
struct WindowSize(Vec2);
68+
69+
/// Event emitted when the bird touches the edges or a pipe
70+
#[derive(Event, Default)]
71+
struct CollisionEvent;
72+
73+
/// Event emitted when a new pipe should be spawned
74+
#[derive(Event, Default)]
75+
struct SpawnPipeEvent;
76+
77+
/// Sound that should be played when a pipe is passed
78+
#[derive(Resource, Deref)]
79+
struct ScoreSound(Handle<AudioSource>);
80+
81+
fn main() {
82+
App::new()
83+
.add_plugins(DefaultPlugins)
84+
.add_systems(Startup, (set_window_size, setup))
85+
.add_systems(
86+
FixedUpdate,
87+
(
88+
reset,
89+
add_pipes,
90+
spawn_pipe,
91+
flap,
92+
apply_gravity,
93+
apply_velocity,
94+
check_collisions,
95+
increase_score,
96+
remove_pipes,
97+
),
98+
)
99+
.insert_resource(Score(0))
100+
.insert_resource(ClearColor(BACKGROUND_COLOR))
101+
.insert_resource(PipeTimer(Timer::new(
102+
PIPE_TIMER_DURATION,
103+
TimerMode::Repeating,
104+
)))
105+
.insert_resource(WindowSize(Vec2::ZERO))
106+
.add_event::<CollisionEvent>()
107+
.add_event::<SpawnPipeEvent>()
108+
.run();
109+
}
110+
111+
/// Set up the camera and score UI
112+
fn setup(
113+
mut commands: Commands,
114+
mut collision_events: EventWriter<CollisionEvent>,
115+
asset_server: Res<AssetServer>,
116+
) {
117+
commands.spawn(Camera2d);
118+
119+
let score_sound = asset_server.load("sounds/breakout_collision.ogg");
120+
commands.insert_resource(ScoreSound(score_sound));
121+
122+
// Spawn the score UI.
123+
commands.spawn((
124+
Node {
125+
width: Val::Percent(100.0),
126+
height: Val::Percent(100.0),
127+
align_items: AlignItems::Start,
128+
justify_content: JustifyContent::Center,
129+
padding: UiRect::all(Val::Px(10.0)),
130+
..default()
131+
},
132+
children![(
133+
ScoreText,
134+
Text::new("0"),
135+
TextFont {
136+
font_size: 66.0,
137+
..default()
138+
},
139+
TextColor(Color::srgb(0.3, 0.3, 0.9)),
140+
)],
141+
));
142+
143+
// Create a collision event to trigger a reset
144+
collision_events.write_default();
145+
}
146+
147+
/// Clear everything and put everything to its start state
148+
fn reset(
149+
mut commands: Commands,
150+
mut timer: ResMut<PipeTimer>,
151+
mut score: ResMut<Score>,
152+
mut collision_events: EventReader<CollisionEvent>,
153+
mut spawn_pipe_events: EventWriter<SpawnPipeEvent>,
154+
score_text: Query<&mut Text, With<ScoreText>>,
155+
pipes: Query<Entity, With<Pipe>>,
156+
pipe_markers: Query<Entity, With<PipeMarker>>,
157+
bird: Query<Entity, With<Bird>>,
158+
asset_server: Res<AssetServer>,
159+
) {
160+
if collision_events.is_empty() {
161+
return;
162+
}
163+
164+
collision_events.clear();
165+
166+
// Remove any entities left over from the previous game (if any)
167+
for ent in bird {
168+
commands.entity(ent).despawn();
169+
}
170+
171+
for ent in pipes {
172+
commands.entity(ent).despawn();
173+
}
174+
175+
for ent in pipe_markers {
176+
commands.entity(ent).despawn();
177+
}
178+
179+
// Set the score to 0
180+
score.0 = 0;
181+
for mut text in score_text {
182+
text.0 = 0.to_string();
183+
}
184+
185+
// Spawn a new bird
186+
commands.spawn((
187+
Bird,
188+
Sprite {
189+
image: asset_server.load("branding/icon.png"),
190+
custom_size: Some(Vec2::splat(BIRD_SIZE)),
191+
..default()
192+
},
193+
Transform::from_xyz(BIRD_POSITION, 0., 0.),
194+
Velocity(Vec2::new(0., FLAP_POWER)),
195+
));
196+
197+
timer.reset();
198+
spawn_pipe_events.write_default();
199+
}
200+
201+
fn set_window_size(window: Single<&mut Window>, mut window_size: ResMut<WindowSize>) {
202+
window_size.0 = Vec2::new(window.resolution.width(), window.resolution.height());
203+
}
204+
205+
/// Flap on a spacebar or left mouse button press
206+
fn flap(
207+
keyboard_input: Res<ButtonInput<KeyCode>>,
208+
mouse_input: Res<ButtonInput<MouseButton>>,
209+
mut bird_velocity: Single<&mut Velocity, With<Bird>>,
210+
) {
211+
if keyboard_input.pressed(KeyCode::Space) || mouse_input.pressed(MouseButton::Left) {
212+
bird_velocity.y = FLAP_POWER;
213+
}
214+
}
215+
216+
/// Apply gravity to the bird and set its rotation
217+
fn apply_gravity(mut bird: Single<(&mut Transform, &mut Velocity), With<Bird>>, time: Res<Time>) {
218+
/// The logistic function, which is an example of a sigmoid function
219+
fn logistic(x: f32) -> f32 {
220+
1. / (1. + exp(-x))
221+
}
222+
223+
bird.1.y -= GRAVITY * time.delta_secs();
224+
225+
// We determine the rotation based on the y-component of the velocity.
226+
// This is tweaked such that a velocity of 100 is pretty much a 90 degree
227+
// rotation. We take the output of the sigmoid function, which goes from
228+
// 0 to 1 and stretch it to -1 to 1. Then we multiply with PI/2 to get
229+
// a rotation in radians.
230+
let rotation = std::f32::consts::PI / 2. * 2. * (logistic(bird.1.y / 600.) - 0.5);
231+
bird.0.rotation = Quat::from_rotation_z(rotation);
232+
}
233+
234+
/// Apply velocity to everything with a `Velocity` component
235+
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>) {
236+
for (mut transform, velocity) in &mut query {
237+
transform.translation.x += velocity.x * time.delta_secs();
238+
transform.translation.y += velocity.y * time.delta_secs();
239+
}
240+
}
241+
242+
/// Check for collision with the borders of the window and the pipes
243+
fn check_collisions(
244+
bird: Single<&Transform, With<Bird>>,
245+
pipes: Query<&Transform, With<Pipe>>,
246+
window_size: Res<WindowSize>,
247+
mut collision_events: EventWriter<CollisionEvent>,
248+
) {
249+
if bird.translation.y.abs() > window_size.y / 2. {
250+
collision_events.write_default();
251+
return;
252+
}
253+
254+
let bird_collider = BoundingCircle::new(bird.translation.truncate(), BIRD_SIZE / 2.);
255+
for pipe in pipes {
256+
let pipe_collider = Aabb2d::new(pipe.translation.truncate(), PIPE_SIZE / 2.);
257+
if bird_collider.intersects(&pipe_collider) {
258+
collision_events.write_default();
259+
return;
260+
}
261+
}
262+
}
263+
264+
/// Add a pipe each time the timer finishes
265+
fn add_pipes(
266+
mut timer: ResMut<PipeTimer>,
267+
time: Res<Time>,
268+
mut events: EventWriter<SpawnPipeEvent>,
269+
) {
270+
timer.tick(time.delta());
271+
272+
if timer.finished() {
273+
events.write_default();
274+
}
275+
}
276+
277+
fn spawn_pipe(
278+
mut events: EventReader<SpawnPipeEvent>,
279+
window_size: Res<WindowSize>,
280+
mut commands: Commands,
281+
mut meshes: ResMut<Assets<Mesh>>,
282+
mut materials: ResMut<Assets<ColorMaterial>>,
283+
) {
284+
if events.is_empty() {
285+
return;
286+
}
287+
events.clear();
288+
289+
let color = Color::BLACK;
290+
let shape = meshes.add(Rectangle::new(PIPE_SIZE.x, PIPE_SIZE.y));
291+
292+
let mut rng = rand::thread_rng();
293+
let gap_offset: i64 = rng.gen_range(-200..=200);
294+
let gap_offset: f32 = gap_offset as f32;
295+
296+
let pipe_offset = PIPE_SIZE.y / 2. + GAP_HEIGHT / 2.;
297+
298+
let pipe_location = window_size.x / 2. + PIPE_SIZE.x / 2.;
299+
300+
// We first spawn in invisible marker that will increase the score once
301+
// it passes the bird position and then despawns. This assures that each
302+
// pipe is counted once.
303+
commands.spawn((
304+
PipeMarker,
305+
Transform::from_xyz(pipe_location, 0.0, 0.0),
306+
Velocity(Vec2::new(-PIPE_SPEED, 0.)),
307+
));
308+
309+
// bottom pipe
310+
commands.spawn((
311+
Pipe,
312+
Mesh2d(shape.clone()),
313+
MeshMaterial2d(materials.add(color)),
314+
Transform::from_xyz(pipe_location, pipe_offset + gap_offset, 0.0),
315+
Velocity(Vec2::new(-PIPE_SPEED, 0.)),
316+
));
317+
318+
// top pipe
319+
commands.spawn((
320+
Pipe,
321+
Mesh2d(shape),
322+
MeshMaterial2d(materials.add(color)),
323+
Transform::from_xyz(pipe_location, -pipe_offset + gap_offset, 0.0),
324+
Velocity(Vec2::new(-PIPE_SPEED, 0.)),
325+
));
326+
}
327+
328+
/// Increase the score every time a pipe marker passes the bird
329+
fn increase_score(
330+
mut commands: Commands,
331+
mut marker_query: Query<(Entity, &mut Transform), With<PipeMarker>>,
332+
mut text_query: Query<&mut Text, With<ScoreText>>,
333+
mut score: ResMut<Score>,
334+
sound: Res<ScoreSound>,
335+
) {
336+
for (entity, transform) in &mut marker_query {
337+
if transform.translation.x < BIRD_POSITION {
338+
commands.entity(entity).despawn();
339+
score.0 += 1;
340+
text_query.single_mut().unwrap().0 = score.0.to_string();
341+
commands.spawn((AudioPlayer(sound.clone()), PlaybackSettings::DESPAWN));
342+
}
343+
}
344+
}
345+
346+
/// Remove pipes that have left the screen
347+
fn remove_pipes(
348+
mut commands: Commands,
349+
mut query: Query<(Entity, &mut Transform), With<Pipe>>,
350+
window_size: Res<WindowSize>,
351+
) {
352+
for (entity, transform) in &mut query {
353+
// The entire pipe needs to have left the screen, not just its origin,
354+
// so we check that the right side of the pipe is off screen.
355+
let right_side_of_pipe = transform.translation.x + PIPE_SIZE.x / 2.;
356+
if right_side_of_pipe < -window_size.x / 2. {
357+
commands.entity(entity).despawn();
358+
}
359+
}
360+
}

0 commit comments

Comments
 (0)