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