Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/games/contributors.rs
6592 views
1
//! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
2
3
use bevy::{math::bounding::Aabb2d, prelude::*};
4
use rand::{Rng, SeedableRng};
5
use rand_chacha::ChaCha8Rng;
6
use std::{
7
collections::HashMap,
8
env::VarError,
9
hash::{DefaultHasher, Hash, Hasher},
10
io::{self, BufRead, BufReader},
11
process::Stdio,
12
};
13
14
fn main() {
15
App::new()
16
.add_plugins(DefaultPlugins)
17
.init_resource::<SelectionTimer>()
18
.init_resource::<SharedRng>()
19
.add_systems(Startup, (setup_contributor_selection, setup))
20
// Systems are chained for determinism only
21
.add_systems(Update, (gravity, movement, collisions, selection).chain())
22
.run();
23
}
24
25
type Contributors = Vec<(String, usize)>;
26
27
#[derive(Resource)]
28
struct ContributorSelection {
29
order: Vec<Entity>,
30
idx: usize,
31
}
32
33
#[derive(Resource)]
34
struct SelectionTimer(Timer);
35
36
impl Default for SelectionTimer {
37
fn default() -> Self {
38
Self(Timer::from_seconds(
39
SHOWCASE_TIMER_SECS,
40
TimerMode::Repeating,
41
))
42
}
43
}
44
45
#[derive(Component)]
46
struct ContributorDisplay;
47
48
#[derive(Component)]
49
struct Contributor {
50
name: String,
51
num_commits: usize,
52
hue: f32,
53
}
54
55
#[derive(Component)]
56
struct Velocity {
57
translation: Vec3,
58
rotation: f32,
59
}
60
61
// We're using a shared seeded RNG here to make this example deterministic for testing purposes.
62
// This isn't strictly required in practical use unless you need your app to be deterministic.
63
#[derive(Resource, Deref, DerefMut)]
64
struct SharedRng(ChaCha8Rng);
65
impl Default for SharedRng {
66
fn default() -> Self {
67
Self(ChaCha8Rng::seed_from_u64(10223163112))
68
}
69
}
70
71
const GRAVITY: f32 = 9.821 * 100.0;
72
const SPRITE_SIZE: f32 = 75.0;
73
74
const SELECTED: Hsla = Hsla::hsl(0.0, 0.9, 0.7);
75
const DESELECTED: Hsla = Hsla::new(0.0, 0.3, 0.2, 0.92);
76
77
const SELECTED_Z_OFFSET: f32 = 100.0;
78
79
const SHOWCASE_TIMER_SECS: f32 = 3.0;
80
81
const CONTRIBUTORS_LIST: &[&str] = &["Carter Anderson", "And Many More"];
82
83
fn setup_contributor_selection(
84
mut commands: Commands,
85
asset_server: Res<AssetServer>,
86
mut rng: ResMut<SharedRng>,
87
) {
88
let contribs = contributors_or_fallback();
89
90
let texture_handle = asset_server.load("branding/icon.png");
91
92
let mut contributor_selection = ContributorSelection {
93
order: Vec::with_capacity(contribs.len()),
94
idx: 0,
95
};
96
97
for (name, num_commits) in contribs {
98
let transform = Transform::from_xyz(
99
rng.random_range(-400.0..400.0),
100
rng.random_range(0.0..400.0),
101
rng.random(),
102
);
103
let dir = rng.random_range(-1.0..1.0);
104
let velocity = Vec3::new(dir * 500.0, 0.0, 0.0);
105
let hue = name_to_hue(&name);
106
107
// Some sprites should be flipped for variety
108
let flipped = rng.random();
109
110
let entity = commands
111
.spawn((
112
Contributor {
113
name,
114
num_commits,
115
hue,
116
},
117
Velocity {
118
translation: velocity,
119
rotation: -dir * 5.0,
120
},
121
Sprite {
122
image: texture_handle.clone(),
123
custom_size: Some(Vec2::splat(SPRITE_SIZE)),
124
color: DESELECTED.with_hue(hue).into(),
125
flip_x: flipped,
126
..default()
127
},
128
transform,
129
))
130
.id();
131
132
contributor_selection.order.push(entity);
133
}
134
135
commands.insert_resource(contributor_selection);
136
}
137
138
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
139
commands.spawn(Camera2d);
140
141
let text_style = TextFont {
142
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
143
font_size: 60.0,
144
..default()
145
};
146
147
commands
148
.spawn((
149
Text::new("Contributor showcase"),
150
text_style.clone(),
151
ContributorDisplay,
152
Node {
153
position_type: PositionType::Absolute,
154
top: px(12),
155
left: px(12),
156
..default()
157
},
158
))
159
.with_child((
160
TextSpan::default(),
161
TextFont {
162
font_size: 30.,
163
..text_style
164
},
165
));
166
}
167
168
/// Finds the next contributor to display and selects the entity
169
fn selection(
170
mut timer: ResMut<SelectionTimer>,
171
mut contributor_selection: ResMut<ContributorSelection>,
172
contributor_root: Single<Entity, (With<ContributorDisplay>, With<Text>)>,
173
mut query: Query<(&Contributor, &mut Sprite, &mut Transform)>,
174
mut writer: TextUiWriter,
175
time: Res<Time>,
176
) {
177
if !timer.0.tick(time.delta()).just_finished() {
178
return;
179
}
180
181
// Deselect the previous contributor
182
183
let entity = contributor_selection.order[contributor_selection.idx];
184
if let Ok((contributor, mut sprite, mut transform)) = query.get_mut(entity) {
185
deselect(&mut sprite, contributor, &mut transform);
186
}
187
188
// Select the next contributor
189
190
if (contributor_selection.idx + 1) < contributor_selection.order.len() {
191
contributor_selection.idx += 1;
192
} else {
193
contributor_selection.idx = 0;
194
}
195
196
let entity = contributor_selection.order[contributor_selection.idx];
197
198
if let Ok((contributor, mut sprite, mut transform)) = query.get_mut(entity) {
199
let entity = *contributor_root;
200
select(
201
&mut sprite,
202
contributor,
203
&mut transform,
204
entity,
205
&mut writer,
206
);
207
}
208
}
209
210
/// Change the tint color to the "selected" color, bring the object to the front
211
/// and display the name.
212
fn select(
213
sprite: &mut Sprite,
214
contributor: &Contributor,
215
transform: &mut Transform,
216
entity: Entity,
217
writer: &mut TextUiWriter,
218
) {
219
sprite.color = SELECTED.with_hue(contributor.hue).into();
220
221
transform.translation.z += SELECTED_Z_OFFSET;
222
223
writer.text(entity, 0).clone_from(&contributor.name);
224
*writer.text(entity, 1) = format!(
225
"\n{} commit{}",
226
contributor.num_commits,
227
if contributor.num_commits > 1 { "s" } else { "" }
228
);
229
writer.color(entity, 0).0 = sprite.color;
230
}
231
232
/// Change the tint color to the "deselected" color and push
233
/// the object to the back.
234
fn deselect(sprite: &mut Sprite, contributor: &Contributor, transform: &mut Transform) {
235
sprite.color = DESELECTED.with_hue(contributor.hue).into();
236
237
transform.translation.z -= SELECTED_Z_OFFSET;
238
}
239
240
/// Applies gravity to all entities with a velocity.
241
fn gravity(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) {
242
let delta = time.delta_secs();
243
244
for mut velocity in &mut velocity_query {
245
velocity.translation.y -= GRAVITY * delta;
246
}
247
}
248
249
/// Checks for collisions of contributor-birbs.
250
///
251
/// On collision with left-or-right wall it resets the horizontal
252
/// velocity. On collision with the ground it applies an upwards
253
/// force.
254
fn collisions(
255
window: Query<&Window>,
256
mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>,
257
mut rng: ResMut<SharedRng>,
258
) {
259
let Ok(window) = window.single() else {
260
return;
261
};
262
263
let window_size = window.size();
264
265
let collision_area = Aabb2d::new(Vec2::ZERO, (window_size - SPRITE_SIZE) / 2.);
266
267
// The maximum height the birbs should try to reach is one birb below the top of the window.
268
let max_bounce_height = (window_size.y - SPRITE_SIZE * 2.0).max(0.0);
269
let min_bounce_height = max_bounce_height * 0.4;
270
271
for (mut velocity, mut transform) in &mut query {
272
// Clamp the translation to not go out of the bounds
273
if transform.translation.y < collision_area.min.y {
274
transform.translation.y = collision_area.min.y;
275
276
// How high this birb will bounce.
277
let bounce_height = rng.random_range(min_bounce_height..=max_bounce_height);
278
279
// Apply the velocity that would bounce the birb up to bounce_height.
280
velocity.translation.y = (bounce_height * GRAVITY * 2.).sqrt();
281
}
282
283
// Birbs might hit the ceiling if the window is resized.
284
// If they do, bounce them.
285
if transform.translation.y > collision_area.max.y {
286
transform.translation.y = collision_area.max.y;
287
velocity.translation.y *= -1.0;
288
}
289
290
// On side walls flip the horizontal velocity
291
if transform.translation.x < collision_area.min.x {
292
transform.translation.x = collision_area.min.x;
293
velocity.translation.x *= -1.0;
294
velocity.rotation *= -1.0;
295
}
296
if transform.translation.x > collision_area.max.x {
297
transform.translation.x = collision_area.max.x;
298
velocity.translation.x *= -1.0;
299
velocity.rotation *= -1.0;
300
}
301
}
302
}
303
304
/// Apply velocity to positions and rotations.
305
fn movement(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
306
let delta = time.delta_secs();
307
308
for (velocity, mut transform) in &mut query {
309
transform.translation += delta * velocity.translation;
310
transform.rotate_z(velocity.rotation * delta);
311
}
312
}
313
314
#[derive(Debug, thiserror::Error)]
315
enum LoadContributorsError {
316
#[error("An IO error occurred while reading the git log.")]
317
Io(#[from] io::Error),
318
#[error("The CARGO_MANIFEST_DIR environment variable was not set.")]
319
Var(#[from] VarError),
320
#[error("The git process did not return a stdout handle.")]
321
Stdout,
322
}
323
324
/// Get the names and commit counts of all contributors from the git log.
325
///
326
/// This function only works if `git` is installed and
327
/// the program is run through `cargo`.
328
fn contributors() -> Result<Contributors, LoadContributorsError> {
329
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
330
331
let mut cmd = std::process::Command::new("git")
332
.args(["--no-pager", "log", "--pretty=format:%an"])
333
.current_dir(manifest_dir)
334
.stdout(Stdio::piped())
335
.spawn()?;
336
337
let stdout = cmd.stdout.take().ok_or(LoadContributorsError::Stdout)?;
338
339
// Take the list of commit author names and collect them into a HashMap,
340
// keeping a count of how many commits they authored.
341
let contributors = BufReader::new(stdout).lines().map_while(Result::ok).fold(
342
HashMap::new(),
343
|mut acc, word| {
344
*acc.entry(word).or_insert(0) += 1;
345
acc
346
},
347
);
348
349
Ok(contributors.into_iter().collect())
350
}
351
352
/// Get the contributors list, or fall back to a default value if
353
/// it's unavailable or we're in CI
354
fn contributors_or_fallback() -> Contributors {
355
let get_default = || {
356
CONTRIBUTORS_LIST
357
.iter()
358
.cycle()
359
.take(1000)
360
.map(|name| (name.to_string(), 1))
361
.collect()
362
};
363
364
if cfg!(feature = "bevy_ci_testing") {
365
return get_default();
366
}
367
368
contributors().unwrap_or_else(|_| get_default())
369
}
370
371
/// Give each unique contributor name a particular hue that is stable between runs.
372
fn name_to_hue(s: &str) -> f32 {
373
let mut hasher = DefaultHasher::new();
374
s.hash(&mut hasher);
375
hasher.finish() as f32 / u64::MAX as f32 * 360.
376
}
377
378