11//! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
22
33use bevy:: {
4+ math:: bounding:: Aabb2d ,
45 prelude:: * ,
5- utils:: { thiserror, HashSet } ,
6+ utils:: { thiserror, HashMap } ,
67} ;
78use rand:: { prelude:: SliceRandom , Rng } ;
89use std:: {
910 env:: VarError ,
11+ hash:: { DefaultHasher , Hash , Hasher } ,
1012 io:: { self , BufRead , BufReader } ,
1113 process:: Stdio ,
1214} ;
1315
1416fn 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 ) ]
3529struct 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 ) ]
5950struct Contributor {
6051 name : String ,
52+ num_commits : usize ,
6153 hue : f32 ,
6254}
6355
@@ -70,23 +62,21 @@ struct Velocity {
7062const GRAVITY : f32 = 9.821 * 100.0 ;
7163const 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
7968const SHOWCASE_TIMER_SECS : f32 = 3.0 ;
8069
8170const CONTRIBUTORS_LIST : & [ & str ] = & [ "Carter Anderson" , "And Many More" ] ;
8271
8372fn 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
142135fn 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.
229220fn 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`.
330312fn 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