Skip to content

Commit 839f771

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

File tree

2 files changed

+368
-0
lines changed

2 files changed

+368
-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 = "Flap a bird"
4373+
category = "Games"
4374+
wasm = true

examples/games/flappy_bird.rs

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

0 commit comments

Comments
 (0)