1
1
//! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
2
2
3
3
use bevy:: {
4
+ math:: bounding:: Aabb2d ,
4
5
prelude:: * ,
5
- utils:: { thiserror, HashSet } ,
6
+ utils:: { thiserror, HashMap } ,
6
7
} ;
7
8
use rand:: { prelude:: SliceRandom , Rng } ;
8
9
use std:: {
9
10
env:: VarError ,
11
+ hash:: { DefaultHasher , Hash , Hasher } ,
10
12
io:: { self , BufRead , BufReader } ,
11
13
process:: Stdio ,
12
14
} ;
13
15
14
16
fn main ( ) {
15
17
App :: new ( )
16
18
. add_plugins ( DefaultPlugins )
17
- . init_resource :: < SelectionState > ( )
19
+ . init_resource :: < SelectionTimer > ( )
18
20
. 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) )
28
22
. run ( ) ;
29
23
}
30
24
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 > ;
33
27
34
28
#[ derive( Resource ) ]
35
29
struct ContributorSelection {
@@ -38,17 +32,14 @@ struct ContributorSelection {
38
32
}
39
33
40
34
#[ derive( Resource ) ]
41
- struct SelectionState {
42
- timer : Timer ,
43
- has_triggered : bool ,
44
- }
35
+ struct SelectionTimer ( Timer ) ;
45
36
46
- impl Default for SelectionState {
37
+ impl Default for SelectionTimer {
47
38
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
+ ) )
52
43
}
53
44
}
54
45
@@ -58,6 +49,7 @@ struct ContributorDisplay;
58
49
#[ derive( Component ) ]
59
50
struct Contributor {
60
51
name : String ,
52
+ num_commits : usize ,
61
53
hue : f32 ,
62
54
}
63
55
@@ -70,23 +62,21 @@ struct Velocity {
70
62
const GRAVITY : f32 = 9.821 * 100.0 ;
71
63
const SPRITE_SIZE : f32 = 75.0 ;
72
64
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 ) ;
78
67
79
68
const SHOWCASE_TIMER_SECS : f32 = 3.0 ;
80
69
81
70
const CONTRIBUTORS_LIST : & [ & str ] = & [ "Carter Anderson" , "And Many More" ] ;
82
71
83
72
fn setup_contributor_selection ( mut commands : Commands , asset_server : Res < AssetServer > ) {
84
73
// 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.
86
76
let contribs = contributors ( ) . unwrap_or_else ( |_| {
87
77
CONTRIBUTORS_LIST
88
78
. iter ( )
89
- . map ( |name| name. to_string ( ) )
79
+ . map ( |name| ( name. to_string ( ) , 1 ) )
90
80
. collect ( )
91
81
} ) ;
92
82
@@ -99,28 +89,31 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
99
89
100
90
let mut rng = rand:: thread_rng ( ) ;
101
91
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 ) ;
104
95
let dir = rng. gen_range ( -1.0 ..1.0 ) ;
105
96
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) ;
110
98
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 ( ) ;
112
101
113
102
let entity = commands
114
103
. spawn ( (
115
- Contributor { name, hue } ,
104
+ Contributor {
105
+ name,
106
+ num_commits,
107
+ hue,
108
+ } ,
116
109
Velocity {
117
110
translation : velocity,
118
111
rotation : -dir * 5.0 ,
119
112
} ,
120
113
SpriteBundle {
121
114
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 ( ) ,
124
117
flip_x : flipped,
125
118
..default ( )
126
119
} ,
@@ -142,53 +135,51 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
142
135
fn setup ( mut commands : Commands , asset_server : Res < AssetServer > ) {
143
136
commands. spawn ( Camera2dBundle :: default ( ) ) ;
144
137
138
+ let text_style = TextStyle {
139
+ font : asset_server. load ( "fonts/FiraSans-Bold.ttf" ) ,
140
+ font_size : 60.0 ,
141
+ ..default ( )
142
+ } ;
143
+
145
144
commands. spawn ( (
146
145
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 ( ) ) ,
155
147
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
159
150
} ) ,
160
151
] )
161
152
. with_style ( Style {
162
- align_self : AlignSelf :: FlexEnd ,
153
+ position_type : PositionType :: Absolute ,
154
+ top : Val :: Px ( 12. ) ,
155
+ left : Val :: Px ( 12. ) ,
163
156
..default ( )
164
157
} ) ,
165
158
ContributorDisplay ,
166
159
) ) ;
167
160
}
168
161
169
162
/// 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 > ,
172
165
mut contributor_selection : ResMut < ContributorSelection > ,
173
166
mut text_query : Query < & mut Text , With < ContributorDisplay > > ,
174
167
mut query : Query < ( & Contributor , & mut Sprite , & mut Transform ) > ,
175
168
time : Res < Time > ,
176
169
) {
177
- if !timer. timer . tick ( time. delta ( ) ) . just_finished ( ) {
170
+ if !timer. 0 . tick ( time. delta ( ) ) . just_finished ( ) {
178
171
return ;
179
172
}
180
- if !timer. has_triggered {
181
- let mut text = text_query. single_mut ( ) ;
182
- text. sections [ 0 ] . value = "Contributor: " . to_string ( ) ;
183
173
184
- timer. has_triggered = true ;
185
- }
174
+ // Deselect the previous contributor
186
175
187
176
let entity = contributor_selection. order [ contributor_selection. idx ] ;
188
177
if let Ok ( ( contributor, mut sprite, mut transform) ) = query. get_mut ( entity) {
189
178
deselect ( & mut sprite, contributor, & mut transform) ;
190
179
}
191
180
181
+ // Select the next contributor
182
+
192
183
if ( contributor_selection. idx + 1 ) < contributor_selection. order . len ( ) {
193
184
contributor_selection. idx += 1 ;
194
185
} else {
@@ -211,99 +202,91 @@ fn select(
211
202
transform : & mut Transform ,
212
203
text : & mut Text ,
213
204
) {
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 ( ) ;
220
206
221
207
transform. translation . z = 100.0 ;
222
208
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 ;
225
216
}
226
217
227
- /// Change the modulate color to the "deselected" color and push
218
+ /// Change the tint color to the "deselected" color and push
228
219
/// the object to the back.
229
220
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 ( ) ;
236
222
237
223
transform. translation . z = 0.0 ;
238
224
}
239
225
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 > ) {
242
228
let delta = time. delta_seconds ( ) ;
243
229
244
230
for mut velocity in & mut velocity_query {
245
231
velocity. translation . y -= GRAVITY * delta;
246
232
}
247
233
}
248
234
249
- /// Checks for collisions of contributor-birds .
235
+ /// Checks for collisions of contributor-birbs .
250
236
///
251
237
/// On collision with left-or-right wall it resets the horizontal
252
238
/// velocity. On collision with the ground it applies an upwards
253
239
/// force.
254
- fn collision_system (
240
+ fn collisions (
255
241
windows : Query < & Window > ,
256
242
mut query : Query < ( & mut Velocity , & mut Transform ) , With < Contributor > > ,
257
243
) {
258
244
let window = windows. single ( ) ;
245
+ let window_size = Vec2 :: new ( window. width ( ) , window. height ( ) ) ;
259
246
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. ) ;
265
248
266
249
// 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 ;
268
252
269
253
let mut rng = rand:: thread_rng ( ) ;
270
254
271
255
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 ;
280
259
281
260
// 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) ;
283
262
284
263
// Apply the velocity that would bounce the birb up to bounce_height.
285
264
velocity. translation . y = ( bounce_height * GRAVITY * 2. ) . sqrt ( ) ;
286
265
}
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 ;
289
271
velocity. translation . y *= -1.0 ;
290
272
}
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 ;
294
277
velocity. translation . x *= -1.0 ;
295
278
velocity. rotation *= -1.0 ;
296
279
}
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 ;
299
282
velocity. translation . x *= -1.0 ;
300
283
velocity. rotation *= -1.0 ;
301
284
}
302
285
}
303
286
}
304
287
305
288
/// 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 ) > ) {
307
290
let delta = time. delta_seconds ( ) ;
308
291
309
292
for ( velocity, mut transform) in & mut query {
@@ -322,9 +305,8 @@ enum LoadContributorsError {
322
305
Stdout ,
323
306
}
324
307
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.
326
309
///
327
- /// The names are deduplicated.
328
310
/// This function only works if `git` is installed and
329
311
/// the program is run through `cargo`.
330
312
fn contributors ( ) -> Result < Contributors , LoadContributorsError > {
@@ -338,10 +320,22 @@ fn contributors() -> Result<Contributors, LoadContributorsError> {
338
320
339
321
let stdout = cmd. stdout . take ( ) . ok_or ( LoadContributorsError :: Stdout ) ?;
340
322
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
+ ) ;
345
332
346
333
Ok ( contributors)
347
334
}
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