Skip to content

Commit bcdca06

Browse files
authored
Various improvements to the contributors example (#12217)
# Objective / Solution - Use `Hsla` for the color constants (Fixes #12203) And a few other improvements: - Make contributor colors persistent between runs - Move text to top left where the birbs can't reach and add padding - Remove `Contributor:` text. It's obvious what is being shown from context, and then we can remove `has_triggered`. - Show the number of commits authored - Some system names were postfixed with `_system` and some weren't. Removed `_system` for consistency. - Clean up collision code slightly with a bounding volume - Someone accidentally typed "bird" instead of "birb" in one comment. - Other misc. cleanup ## Before <img width="1280" alt="image" src="https://github.com/bevyengine/bevy/assets/200550/9c6229d6-313a-464d-8a97-0220aa16901f"> ## After <img width="1280" alt="image" src="https://github.com/bevyengine/bevy/assets/200550/0c00e95b-2f50-4f50-b177-def4ca405313">
1 parent 4aca55d commit bcdca06

File tree

1 file changed

+101
-107
lines changed

1 file changed

+101
-107
lines changed

examples/games/contributors.rs

Lines changed: 101 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
//! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
22
33
use bevy::{
4+
math::bounding::Aabb2d,
45
prelude::*,
5-
utils::{thiserror, HashSet},
6+
utils::{thiserror, HashMap},
67
};
78
use rand::{prelude::SliceRandom, Rng};
89
use std::{
910
env::VarError,
11+
hash::{DefaultHasher, Hash, Hasher},
1012
io::{self, BufRead, BufReader},
1113
process::Stdio,
1214
};
1315

1416
fn main() {
1517
App::new()
1618
.add_plugins(DefaultPlugins)
17-
.init_resource::<SelectionState>()
19+
.init_resource::<SelectionTimer>()
1820
.add_systems(Startup, (setup_contributor_selection, setup))
19-
.add_systems(
20-
Update,
21-
(
22-
velocity_system,
23-
move_system,
24-
collision_system,
25-
select_system,
26-
),
27-
)
21+
.add_systems(Update, (gravity, movement, collisions, selection))
2822
.run();
2923
}
3024

31-
// Store contributors in a collection that preserves the uniqueness
32-
type Contributors = HashSet<String>;
25+
// Store contributors with their commit count in a collection that preserves the uniqueness
26+
type Contributors = HashMap<String, usize>;
3327

3428
#[derive(Resource)]
3529
struct ContributorSelection {
@@ -38,17 +32,14 @@ struct ContributorSelection {
3832
}
3933

4034
#[derive(Resource)]
41-
struct SelectionState {
42-
timer: Timer,
43-
has_triggered: bool,
44-
}
35+
struct SelectionTimer(Timer);
4536

46-
impl Default for SelectionState {
37+
impl Default for SelectionTimer {
4738
fn default() -> Self {
48-
Self {
49-
timer: Timer::from_seconds(SHOWCASE_TIMER_SECS, TimerMode::Repeating),
50-
has_triggered: false,
51-
}
39+
Self(Timer::from_seconds(
40+
SHOWCASE_TIMER_SECS,
41+
TimerMode::Repeating,
42+
))
5243
}
5344
}
5445

@@ -58,6 +49,7 @@ struct ContributorDisplay;
5849
#[derive(Component)]
5950
struct Contributor {
6051
name: String,
52+
num_commits: usize,
6153
hue: f32,
6254
}
6355

@@ -70,23 +62,21 @@ struct Velocity {
7062
const GRAVITY: f32 = 9.821 * 100.0;
7163
const SPRITE_SIZE: f32 = 75.0;
7264

73-
const SATURATION_DESELECTED: f32 = 0.3;
74-
const LIGHTNESS_DESELECTED: f32 = 0.2;
75-
const SATURATION_SELECTED: f32 = 0.9;
76-
const LIGHTNESS_SELECTED: f32 = 0.7;
77-
const ALPHA: f32 = 0.92;
65+
const SELECTED: Hsla = Hsla::hsl(0.0, 0.9, 0.7);
66+
const DESELECTED: Hsla = Hsla::new(0.0, 0.3, 0.2, 0.92);
7867

7968
const SHOWCASE_TIMER_SECS: f32 = 3.0;
8069

8170
const CONTRIBUTORS_LIST: &[&str] = &["Carter Anderson", "And Many More"];
8271

8372
fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetServer>) {
8473
// Load contributors from the git history log or use default values from
85-
// the constant array. Contributors must be unique, so they are stored in a HashSet
74+
// the constant array. Contributors are stored in a HashMap with their
75+
// commit count.
8676
let contribs = contributors().unwrap_or_else(|_| {
8777
CONTRIBUTORS_LIST
8878
.iter()
89-
.map(|name| name.to_string())
79+
.map(|name| (name.to_string(), 1))
9080
.collect()
9181
});
9282

@@ -99,28 +89,31 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
9989

10090
let mut rng = rand::thread_rng();
10191

102-
for name in contribs {
103-
let pos = (rng.gen_range(-400.0..400.0), rng.gen_range(0.0..400.0));
92+
for (name, num_commits) in contribs {
93+
let transform =
94+
Transform::from_xyz(rng.gen_range(-400.0..400.0), rng.gen_range(0.0..400.0), 0.0);
10495
let dir = rng.gen_range(-1.0..1.0);
10596
let velocity = Vec3::new(dir * 500.0, 0.0, 0.0);
106-
let hue = rng.gen_range(0.0..=360.0);
107-
108-
// some sprites should be flipped
109-
let flipped = rng.gen_bool(0.5);
97+
let hue = name_to_hue(&name);
11098

111-
let transform = Transform::from_xyz(pos.0, pos.1, 0.0);
99+
// Some sprites should be flipped for variety
100+
let flipped = rng.gen();
112101

113102
let entity = commands
114103
.spawn((
115-
Contributor { name, hue },
104+
Contributor {
105+
name,
106+
num_commits,
107+
hue,
108+
},
116109
Velocity {
117110
translation: velocity,
118111
rotation: -dir * 5.0,
119112
},
120113
SpriteBundle {
121114
sprite: Sprite {
122-
custom_size: Some(Vec2::new(1.0, 1.0) * SPRITE_SIZE),
123-
color: Color::hsla(hue, SATURATION_DESELECTED, LIGHTNESS_DESELECTED, ALPHA),
115+
custom_size: Some(Vec2::splat(SPRITE_SIZE)),
116+
color: DESELECTED.with_hue(hue).into(),
124117
flip_x: flipped,
125118
..default()
126119
},
@@ -142,53 +135,51 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
142135
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
143136
commands.spawn(Camera2dBundle::default());
144137

138+
let text_style = TextStyle {
139+
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
140+
font_size: 60.0,
141+
..default()
142+
};
143+
145144
commands.spawn((
146145
TextBundle::from_sections([
147-
TextSection::new(
148-
"Contributor showcase",
149-
TextStyle {
150-
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
151-
font_size: 60.0,
152-
..default()
153-
},
154-
),
146+
TextSection::new("Contributor showcase", text_style.clone()),
155147
TextSection::from_style(TextStyle {
156-
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
157-
font_size: 60.0,
158-
..default()
148+
font_size: 30.,
149+
..text_style
159150
}),
160151
])
161152
.with_style(Style {
162-
align_self: AlignSelf::FlexEnd,
153+
position_type: PositionType::Absolute,
154+
top: Val::Px(12.),
155+
left: Val::Px(12.),
163156
..default()
164157
}),
165158
ContributorDisplay,
166159
));
167160
}
168161

169162
/// Finds the next contributor to display and selects the entity
170-
fn select_system(
171-
mut timer: ResMut<SelectionState>,
163+
fn selection(
164+
mut timer: ResMut<SelectionTimer>,
172165
mut contributor_selection: ResMut<ContributorSelection>,
173166
mut text_query: Query<&mut Text, With<ContributorDisplay>>,
174167
mut query: Query<(&Contributor, &mut Sprite, &mut Transform)>,
175168
time: Res<Time>,
176169
) {
177-
if !timer.timer.tick(time.delta()).just_finished() {
170+
if !timer.0.tick(time.delta()).just_finished() {
178171
return;
179172
}
180-
if !timer.has_triggered {
181-
let mut text = text_query.single_mut();
182-
text.sections[0].value = "Contributor: ".to_string();
183173

184-
timer.has_triggered = true;
185-
}
174+
// Deselect the previous contributor
186175

187176
let entity = contributor_selection.order[contributor_selection.idx];
188177
if let Ok((contributor, mut sprite, mut transform)) = query.get_mut(entity) {
189178
deselect(&mut sprite, contributor, &mut transform);
190179
}
191180

181+
// Select the next contributor
182+
192183
if (contributor_selection.idx + 1) < contributor_selection.order.len() {
193184
contributor_selection.idx += 1;
194185
} else {
@@ -211,99 +202,91 @@ fn select(
211202
transform: &mut Transform,
212203
text: &mut Text,
213204
) {
214-
sprite.color = Color::hsla(
215-
contributor.hue,
216-
SATURATION_SELECTED,
217-
LIGHTNESS_SELECTED,
218-
ALPHA,
219-
);
205+
sprite.color = SELECTED.with_hue(contributor.hue).into();
220206

221207
transform.translation.z = 100.0;
222208

223-
text.sections[1].value.clone_from(&contributor.name);
224-
text.sections[1].style.color = sprite.color;
209+
text.sections[0].value.clone_from(&contributor.name);
210+
text.sections[1].value = format!(
211+
"\n{} commit{}",
212+
contributor.num_commits,
213+
if contributor.num_commits > 1 { "s" } else { "" }
214+
);
215+
text.sections[0].style.color = sprite.color;
225216
}
226217

227-
/// Change the modulate color to the "deselected" color and push
218+
/// Change the tint color to the "deselected" color and push
228219
/// the object to the back.
229220
fn deselect(sprite: &mut Sprite, contributor: &Contributor, transform: &mut Transform) {
230-
sprite.color = Color::hsla(
231-
contributor.hue,
232-
SATURATION_DESELECTED,
233-
LIGHTNESS_DESELECTED,
234-
ALPHA,
235-
);
221+
sprite.color = DESELECTED.with_hue(contributor.hue).into();
236222

237223
transform.translation.z = 0.0;
238224
}
239225

240-
/// Applies gravity to all entities with velocity
241-
fn velocity_system(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) {
226+
/// Applies gravity to all entities with a velocity.
227+
fn gravity(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) {
242228
let delta = time.delta_seconds();
243229

244230
for mut velocity in &mut velocity_query {
245231
velocity.translation.y -= GRAVITY * delta;
246232
}
247233
}
248234

249-
/// Checks for collisions of contributor-birds.
235+
/// Checks for collisions of contributor-birbs.
250236
///
251237
/// On collision with left-or-right wall it resets the horizontal
252238
/// velocity. On collision with the ground it applies an upwards
253239
/// force.
254-
fn collision_system(
240+
fn collisions(
255241
windows: Query<&Window>,
256242
mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>,
257243
) {
258244
let window = windows.single();
245+
let window_size = Vec2::new(window.width(), window.height());
259246

260-
let ceiling = window.height() / 2.;
261-
let ground = -window.height() / 2.;
262-
263-
let wall_left = -window.width() / 2.;
264-
let wall_right = window.width() / 2.;
247+
let collision_area = Aabb2d::new(Vec2::ZERO, (window_size - SPRITE_SIZE) / 2.);
265248

266249
// The maximum height the birbs should try to reach is one birb below the top of the window.
267-
let max_bounce_height = (window.height() - SPRITE_SIZE * 2.0).max(0.0);
250+
let max_bounce_height = (window_size.y - SPRITE_SIZE * 2.0).max(0.0);
251+
let min_bounce_height = max_bounce_height * 0.4;
268252

269253
let mut rng = rand::thread_rng();
270254

271255
for (mut velocity, mut transform) in &mut query {
272-
let left = transform.translation.x - SPRITE_SIZE / 2.0;
273-
let right = transform.translation.x + SPRITE_SIZE / 2.0;
274-
let top = transform.translation.y + SPRITE_SIZE / 2.0;
275-
let bottom = transform.translation.y - SPRITE_SIZE / 2.0;
276-
277-
// clamp the translation to not go out of the bounds
278-
if bottom < ground {
279-
transform.translation.y = ground + SPRITE_SIZE / 2.0;
256+
// Clamp the translation to not go out of the bounds
257+
if transform.translation.y < collision_area.min.y {
258+
transform.translation.y = collision_area.min.y;
280259

281260
// How high this birb will bounce.
282-
let bounce_height = rng.gen_range((max_bounce_height * 0.4)..=max_bounce_height);
261+
let bounce_height = rng.gen_range(min_bounce_height..=max_bounce_height);
283262

284263
// Apply the velocity that would bounce the birb up to bounce_height.
285264
velocity.translation.y = (bounce_height * GRAVITY * 2.).sqrt();
286265
}
287-
if top > ceiling {
288-
transform.translation.y = ceiling - SPRITE_SIZE / 2.0;
266+
267+
// Birbs might hit the ceiling if the window is resized.
268+
// If they do, bounce them.
269+
if transform.translation.y > collision_area.max.y {
270+
transform.translation.y = collision_area.max.y;
289271
velocity.translation.y *= -1.0;
290272
}
291-
// on side walls flip the horizontal velocity
292-
if left < wall_left {
293-
transform.translation.x = wall_left + SPRITE_SIZE / 2.0;
273+
274+
// On side walls flip the horizontal velocity
275+
if transform.translation.x < collision_area.min.x {
276+
transform.translation.x = collision_area.min.x;
294277
velocity.translation.x *= -1.0;
295278
velocity.rotation *= -1.0;
296279
}
297-
if right > wall_right {
298-
transform.translation.x = wall_right - SPRITE_SIZE / 2.0;
280+
if transform.translation.x > collision_area.max.x {
281+
transform.translation.x = collision_area.max.x;
299282
velocity.translation.x *= -1.0;
300283
velocity.rotation *= -1.0;
301284
}
302285
}
303286
}
304287

305288
/// Apply velocity to positions and rotations.
306-
fn move_system(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
289+
fn movement(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
307290
let delta = time.delta_seconds();
308291

309292
for (velocity, mut transform) in &mut query {
@@ -322,9 +305,8 @@ enum LoadContributorsError {
322305
Stdout,
323306
}
324307

325-
/// Get the names of all contributors from the git log.
308+
/// Get the names and commit counts of all contributors from the git log.
326309
///
327-
/// The names are deduplicated.
328310
/// This function only works if `git` is installed and
329311
/// the program is run through `cargo`.
330312
fn contributors() -> Result<Contributors, LoadContributorsError> {
@@ -338,10 +320,22 @@ fn contributors() -> Result<Contributors, LoadContributorsError> {
338320

339321
let stdout = cmd.stdout.take().ok_or(LoadContributorsError::Stdout)?;
340322

341-
let contributors = BufReader::new(stdout)
342-
.lines()
343-
.map_while(|x| x.ok())
344-
.collect();
323+
// Take the list of commit author names and collect them into a HashMap,
324+
// keeping a count of how many commits they authored.
325+
let contributors = BufReader::new(stdout).lines().map_while(Result::ok).fold(
326+
HashMap::new(),
327+
|mut acc, word| {
328+
*acc.entry(word).or_insert(0) += 1;
329+
acc
330+
},
331+
);
345332

346333
Ok(contributors)
347334
}
335+
336+
/// Give each unique contributor name a particular hue that is stable between runs.
337+
fn name_to_hue(s: &str) -> f32 {
338+
let mut hasher = DefaultHasher::new();
339+
s.hash(&mut hasher);
340+
hasher.finish() as f32 / u64::MAX as f32 * 360.
341+
}

0 commit comments

Comments
 (0)