mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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">
This commit is contained in:
parent
4aca55d76a
commit
bcdca068ad
1 changed files with 101 additions and 107 deletions
|
@ -1,12 +1,14 @@
|
|||
//! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
|
||||
|
||||
use bevy::{
|
||||
math::bounding::Aabb2d,
|
||||
prelude::*,
|
||||
utils::{thiserror, HashSet},
|
||||
utils::{thiserror, HashMap},
|
||||
};
|
||||
use rand::{prelude::SliceRandom, Rng};
|
||||
use std::{
|
||||
env::VarError,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
io::{self, BufRead, BufReader},
|
||||
process::Stdio,
|
||||
};
|
||||
|
@ -14,22 +16,14 @@ use std::{
|
|||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.init_resource::<SelectionState>()
|
||||
.init_resource::<SelectionTimer>()
|
||||
.add_systems(Startup, (setup_contributor_selection, setup))
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
velocity_system,
|
||||
move_system,
|
||||
collision_system,
|
||||
select_system,
|
||||
),
|
||||
)
|
||||
.add_systems(Update, (gravity, movement, collisions, selection))
|
||||
.run();
|
||||
}
|
||||
|
||||
// Store contributors in a collection that preserves the uniqueness
|
||||
type Contributors = HashSet<String>;
|
||||
// Store contributors with their commit count in a collection that preserves the uniqueness
|
||||
type Contributors = HashMap<String, usize>;
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ContributorSelection {
|
||||
|
@ -38,17 +32,14 @@ struct ContributorSelection {
|
|||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct SelectionState {
|
||||
timer: Timer,
|
||||
has_triggered: bool,
|
||||
}
|
||||
struct SelectionTimer(Timer);
|
||||
|
||||
impl Default for SelectionState {
|
||||
impl Default for SelectionTimer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timer: Timer::from_seconds(SHOWCASE_TIMER_SECS, TimerMode::Repeating),
|
||||
has_triggered: false,
|
||||
}
|
||||
Self(Timer::from_seconds(
|
||||
SHOWCASE_TIMER_SECS,
|
||||
TimerMode::Repeating,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,6 +49,7 @@ struct ContributorDisplay;
|
|||
#[derive(Component)]
|
||||
struct Contributor {
|
||||
name: String,
|
||||
num_commits: usize,
|
||||
hue: f32,
|
||||
}
|
||||
|
||||
|
@ -70,11 +62,8 @@ struct Velocity {
|
|||
const GRAVITY: f32 = 9.821 * 100.0;
|
||||
const SPRITE_SIZE: f32 = 75.0;
|
||||
|
||||
const SATURATION_DESELECTED: f32 = 0.3;
|
||||
const LIGHTNESS_DESELECTED: f32 = 0.2;
|
||||
const SATURATION_SELECTED: f32 = 0.9;
|
||||
const LIGHTNESS_SELECTED: f32 = 0.7;
|
||||
const ALPHA: f32 = 0.92;
|
||||
const SELECTED: Hsla = Hsla::hsl(0.0, 0.9, 0.7);
|
||||
const DESELECTED: Hsla = Hsla::new(0.0, 0.3, 0.2, 0.92);
|
||||
|
||||
const SHOWCASE_TIMER_SECS: f32 = 3.0;
|
||||
|
||||
|
@ -82,11 +71,12 @@ const CONTRIBUTORS_LIST: &[&str] = &["Carter Anderson", "And Many More"];
|
|||
|
||||
fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
// Load contributors from the git history log or use default values from
|
||||
// the constant array. Contributors must be unique, so they are stored in a HashSet
|
||||
// the constant array. Contributors are stored in a HashMap with their
|
||||
// commit count.
|
||||
let contribs = contributors().unwrap_or_else(|_| {
|
||||
CONTRIBUTORS_LIST
|
||||
.iter()
|
||||
.map(|name| name.to_string())
|
||||
.map(|name| (name.to_string(), 1))
|
||||
.collect()
|
||||
});
|
||||
|
||||
|
@ -99,28 +89,31 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
|
|||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for name in contribs {
|
||||
let pos = (rng.gen_range(-400.0..400.0), rng.gen_range(0.0..400.0));
|
||||
for (name, num_commits) in contribs {
|
||||
let transform =
|
||||
Transform::from_xyz(rng.gen_range(-400.0..400.0), rng.gen_range(0.0..400.0), 0.0);
|
||||
let dir = rng.gen_range(-1.0..1.0);
|
||||
let velocity = Vec3::new(dir * 500.0, 0.0, 0.0);
|
||||
let hue = rng.gen_range(0.0..=360.0);
|
||||
let hue = name_to_hue(&name);
|
||||
|
||||
// some sprites should be flipped
|
||||
let flipped = rng.gen_bool(0.5);
|
||||
|
||||
let transform = Transform::from_xyz(pos.0, pos.1, 0.0);
|
||||
// Some sprites should be flipped for variety
|
||||
let flipped = rng.gen();
|
||||
|
||||
let entity = commands
|
||||
.spawn((
|
||||
Contributor { name, hue },
|
||||
Contributor {
|
||||
name,
|
||||
num_commits,
|
||||
hue,
|
||||
},
|
||||
Velocity {
|
||||
translation: velocity,
|
||||
rotation: -dir * 5.0,
|
||||
},
|
||||
SpriteBundle {
|
||||
sprite: Sprite {
|
||||
custom_size: Some(Vec2::new(1.0, 1.0) * SPRITE_SIZE),
|
||||
color: Color::hsla(hue, SATURATION_DESELECTED, LIGHTNESS_DESELECTED, ALPHA),
|
||||
custom_size: Some(Vec2::splat(SPRITE_SIZE)),
|
||||
color: DESELECTED.with_hue(hue).into(),
|
||||
flip_x: flipped,
|
||||
..default()
|
||||
},
|
||||
|
@ -142,24 +135,24 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
|
|||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
|
||||
let text_style = TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 60.0,
|
||||
..default()
|
||||
};
|
||||
|
||||
commands.spawn((
|
||||
TextBundle::from_sections([
|
||||
TextSection::new(
|
||||
"Contributor showcase",
|
||||
TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 60.0,
|
||||
..default()
|
||||
},
|
||||
),
|
||||
TextSection::new("Contributor showcase", text_style.clone()),
|
||||
TextSection::from_style(TextStyle {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 60.0,
|
||||
..default()
|
||||
font_size: 30.,
|
||||
..text_style
|
||||
}),
|
||||
])
|
||||
.with_style(Style {
|
||||
align_self: AlignSelf::FlexEnd,
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(12.),
|
||||
left: Val::Px(12.),
|
||||
..default()
|
||||
}),
|
||||
ContributorDisplay,
|
||||
|
@ -167,28 +160,26 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
}
|
||||
|
||||
/// Finds the next contributor to display and selects the entity
|
||||
fn select_system(
|
||||
mut timer: ResMut<SelectionState>,
|
||||
fn selection(
|
||||
mut timer: ResMut<SelectionTimer>,
|
||||
mut contributor_selection: ResMut<ContributorSelection>,
|
||||
mut text_query: Query<&mut Text, With<ContributorDisplay>>,
|
||||
mut query: Query<(&Contributor, &mut Sprite, &mut Transform)>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
if !timer.timer.tick(time.delta()).just_finished() {
|
||||
if !timer.0.tick(time.delta()).just_finished() {
|
||||
return;
|
||||
}
|
||||
if !timer.has_triggered {
|
||||
let mut text = text_query.single_mut();
|
||||
text.sections[0].value = "Contributor: ".to_string();
|
||||
|
||||
timer.has_triggered = true;
|
||||
}
|
||||
// Deselect the previous contributor
|
||||
|
||||
let entity = contributor_selection.order[contributor_selection.idx];
|
||||
if let Ok((contributor, mut sprite, mut transform)) = query.get_mut(entity) {
|
||||
deselect(&mut sprite, contributor, &mut transform);
|
||||
}
|
||||
|
||||
// Select the next contributor
|
||||
|
||||
if (contributor_selection.idx + 1) < contributor_selection.order.len() {
|
||||
contributor_selection.idx += 1;
|
||||
} else {
|
||||
|
@ -211,34 +202,29 @@ fn select(
|
|||
transform: &mut Transform,
|
||||
text: &mut Text,
|
||||
) {
|
||||
sprite.color = Color::hsla(
|
||||
contributor.hue,
|
||||
SATURATION_SELECTED,
|
||||
LIGHTNESS_SELECTED,
|
||||
ALPHA,
|
||||
);
|
||||
sprite.color = SELECTED.with_hue(contributor.hue).into();
|
||||
|
||||
transform.translation.z = 100.0;
|
||||
|
||||
text.sections[1].value.clone_from(&contributor.name);
|
||||
text.sections[1].style.color = sprite.color;
|
||||
text.sections[0].value.clone_from(&contributor.name);
|
||||
text.sections[1].value = format!(
|
||||
"\n{} commit{}",
|
||||
contributor.num_commits,
|
||||
if contributor.num_commits > 1 { "s" } else { "" }
|
||||
);
|
||||
text.sections[0].style.color = sprite.color;
|
||||
}
|
||||
|
||||
/// Change the modulate color to the "deselected" color and push
|
||||
/// Change the tint color to the "deselected" color and push
|
||||
/// the object to the back.
|
||||
fn deselect(sprite: &mut Sprite, contributor: &Contributor, transform: &mut Transform) {
|
||||
sprite.color = Color::hsla(
|
||||
contributor.hue,
|
||||
SATURATION_DESELECTED,
|
||||
LIGHTNESS_DESELECTED,
|
||||
ALPHA,
|
||||
);
|
||||
sprite.color = DESELECTED.with_hue(contributor.hue).into();
|
||||
|
||||
transform.translation.z = 0.0;
|
||||
}
|
||||
|
||||
/// Applies gravity to all entities with velocity
|
||||
fn velocity_system(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) {
|
||||
/// Applies gravity to all entities with a velocity.
|
||||
fn gravity(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) {
|
||||
let delta = time.delta_seconds();
|
||||
|
||||
for mut velocity in &mut velocity_query {
|
||||
|
@ -246,56 +232,53 @@ fn velocity_system(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Checks for collisions of contributor-birds.
|
||||
/// Checks for collisions of contributor-birbs.
|
||||
///
|
||||
/// On collision with left-or-right wall it resets the horizontal
|
||||
/// velocity. On collision with the ground it applies an upwards
|
||||
/// force.
|
||||
fn collision_system(
|
||||
fn collisions(
|
||||
windows: Query<&Window>,
|
||||
mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>,
|
||||
) {
|
||||
let window = windows.single();
|
||||
let window_size = Vec2::new(window.width(), window.height());
|
||||
|
||||
let ceiling = window.height() / 2.;
|
||||
let ground = -window.height() / 2.;
|
||||
|
||||
let wall_left = -window.width() / 2.;
|
||||
let wall_right = window.width() / 2.;
|
||||
let collision_area = Aabb2d::new(Vec2::ZERO, (window_size - SPRITE_SIZE) / 2.);
|
||||
|
||||
// The maximum height the birbs should try to reach is one birb below the top of the window.
|
||||
let max_bounce_height = (window.height() - SPRITE_SIZE * 2.0).max(0.0);
|
||||
let max_bounce_height = (window_size.y - SPRITE_SIZE * 2.0).max(0.0);
|
||||
let min_bounce_height = max_bounce_height * 0.4;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for (mut velocity, mut transform) in &mut query {
|
||||
let left = transform.translation.x - SPRITE_SIZE / 2.0;
|
||||
let right = transform.translation.x + SPRITE_SIZE / 2.0;
|
||||
let top = transform.translation.y + SPRITE_SIZE / 2.0;
|
||||
let bottom = transform.translation.y - SPRITE_SIZE / 2.0;
|
||||
|
||||
// clamp the translation to not go out of the bounds
|
||||
if bottom < ground {
|
||||
transform.translation.y = ground + SPRITE_SIZE / 2.0;
|
||||
// Clamp the translation to not go out of the bounds
|
||||
if transform.translation.y < collision_area.min.y {
|
||||
transform.translation.y = collision_area.min.y;
|
||||
|
||||
// How high this birb will bounce.
|
||||
let bounce_height = rng.gen_range((max_bounce_height * 0.4)..=max_bounce_height);
|
||||
let bounce_height = rng.gen_range(min_bounce_height..=max_bounce_height);
|
||||
|
||||
// Apply the velocity that would bounce the birb up to bounce_height.
|
||||
velocity.translation.y = (bounce_height * GRAVITY * 2.).sqrt();
|
||||
}
|
||||
if top > ceiling {
|
||||
transform.translation.y = ceiling - SPRITE_SIZE / 2.0;
|
||||
|
||||
// Birbs might hit the ceiling if the window is resized.
|
||||
// If they do, bounce them.
|
||||
if transform.translation.y > collision_area.max.y {
|
||||
transform.translation.y = collision_area.max.y;
|
||||
velocity.translation.y *= -1.0;
|
||||
}
|
||||
// on side walls flip the horizontal velocity
|
||||
if left < wall_left {
|
||||
transform.translation.x = wall_left + SPRITE_SIZE / 2.0;
|
||||
|
||||
// On side walls flip the horizontal velocity
|
||||
if transform.translation.x < collision_area.min.x {
|
||||
transform.translation.x = collision_area.min.x;
|
||||
velocity.translation.x *= -1.0;
|
||||
velocity.rotation *= -1.0;
|
||||
}
|
||||
if right > wall_right {
|
||||
transform.translation.x = wall_right - SPRITE_SIZE / 2.0;
|
||||
if transform.translation.x > collision_area.max.x {
|
||||
transform.translation.x = collision_area.max.x;
|
||||
velocity.translation.x *= -1.0;
|
||||
velocity.rotation *= -1.0;
|
||||
}
|
||||
|
@ -303,7 +286,7 @@ fn collision_system(
|
|||
}
|
||||
|
||||
/// Apply velocity to positions and rotations.
|
||||
fn move_system(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
|
||||
fn movement(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
|
||||
let delta = time.delta_seconds();
|
||||
|
||||
for (velocity, mut transform) in &mut query {
|
||||
|
@ -322,9 +305,8 @@ enum LoadContributorsError {
|
|||
Stdout,
|
||||
}
|
||||
|
||||
/// Get the names of all contributors from the git log.
|
||||
/// Get the names and commit counts of all contributors from the git log.
|
||||
///
|
||||
/// The names are deduplicated.
|
||||
/// This function only works if `git` is installed and
|
||||
/// the program is run through `cargo`.
|
||||
fn contributors() -> Result<Contributors, LoadContributorsError> {
|
||||
|
@ -338,10 +320,22 @@ fn contributors() -> Result<Contributors, LoadContributorsError> {
|
|||
|
||||
let stdout = cmd.stdout.take().ok_or(LoadContributorsError::Stdout)?;
|
||||
|
||||
let contributors = BufReader::new(stdout)
|
||||
.lines()
|
||||
.map_while(|x| x.ok())
|
||||
.collect();
|
||||
// Take the list of commit author names and collect them into a HashMap,
|
||||
// keeping a count of how many commits they authored.
|
||||
let contributors = BufReader::new(stdout).lines().map_while(Result::ok).fold(
|
||||
HashMap::new(),
|
||||
|mut acc, word| {
|
||||
*acc.entry(word).or_insert(0) += 1;
|
||||
acc
|
||||
},
|
||||
);
|
||||
|
||||
Ok(contributors)
|
||||
}
|
||||
|
||||
/// Give each unique contributor name a particular hue that is stable between runs.
|
||||
fn name_to_hue(s: &str) -> f32 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
s.hash(&mut hasher);
|
||||
hasher.finish() as f32 / u64::MAX as f32 * 360.
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue