1use core::time::Duration;
6use std::str::FromStr;
7
8use argh::FromArgs;
9use bevy::{
10 color::palettes::basic::*,
11 diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
12 prelude::*,
13 render::{
14 render_asset::RenderAssetUsages,
15 render_resource::{Extent3d, TextureDimension, TextureFormat},
16 },
17 sprite::AlphaMode2d,
18 window::{PresentMode, WindowResolution},
19 winit::{UpdateMode, WinitSettings},
20};
21use rand::{seq::SliceRandom, Rng, SeedableRng};
22use rand_chacha::ChaCha8Rng;
23
24const BIRDS_PER_SECOND: u32 = 10000;
25const GRAVITY: f32 = -9.8 * 100.0;
26const MAX_VELOCITY: f32 = 750.;
27const BIRD_SCALE: f32 = 0.15;
28const BIRD_TEXTURE_SIZE: usize = 256;
29const HALF_BIRD_SIZE: f32 = BIRD_TEXTURE_SIZE as f32 * BIRD_SCALE * 0.5;
30
31#[derive(Resource)]
32struct BevyCounter {
33 pub count: usize,
34 pub color: Color,
35}
36
37#[derive(Component)]
38struct Bird {
39 velocity: Vec3,
40}
41
42#[derive(FromArgs, Resource)]
43struct Args {
45 #[argh(option, default = "Mode::Sprite")]
47 mode: Mode,
48
49 #[argh(switch)]
53 benchmark: bool,
54
55 #[argh(option, default = "0")]
57 per_wave: usize,
58
59 #[argh(option, default = "0")]
61 waves: usize,
62
63 #[argh(switch)]
65 vary_per_instance: bool,
66
67 #[argh(option, default = "1")]
69 material_texture_count: usize,
70
71 #[argh(switch)]
73 ordered_z: bool,
74
75 #[argh(option, default = "AlphaMode::Blend")]
77 alpha_mode: AlphaMode,
78}
79
80#[derive(Default, Clone)]
81enum Mode {
82 #[default]
83 Sprite,
84 Mesh2d,
85}
86
87impl FromStr for Mode {
88 type Err = String;
89
90 fn from_str(s: &str) -> Result<Self, Self::Err> {
91 match s {
92 "sprite" => Ok(Self::Sprite),
93 "mesh2d" => Ok(Self::Mesh2d),
94 _ => Err(format!(
95 "Unknown mode: '{s}', valid modes: 'sprite', 'mesh2d'"
96 )),
97 }
98 }
99}
100
101#[derive(Default, Clone)]
102enum AlphaMode {
103 Opaque,
104 #[default]
105 Blend,
106 AlphaMask,
107}
108
109impl FromStr for AlphaMode {
110 type Err = String;
111
112 fn from_str(s: &str) -> Result<Self, Self::Err> {
113 match s {
114 "opaque" => Ok(Self::Opaque),
115 "blend" => Ok(Self::Blend),
116 "alpha_mask" => Ok(Self::AlphaMask),
117 _ => Err(format!(
118 "Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'"
119 )),
120 }
121 }
122}
123
124const FIXED_TIMESTEP: f32 = 0.2;
125
126fn main() {
127 #[cfg(not(target_arch = "wasm32"))]
129 let args: Args = argh::from_env();
130 #[cfg(target_arch = "wasm32")]
131 let args = Args::from_args(&[], &[]).unwrap();
132
133 App::new()
134 .add_plugins((
135 DefaultPlugins.set(WindowPlugin {
136 primary_window: Some(Window {
137 title: "BevyMark".into(),
138 resolution: WindowResolution::new(1920.0, 1080.0)
139 .with_scale_factor_override(1.0),
140 present_mode: PresentMode::AutoNoVsync,
141 ..default()
142 }),
143 ..default()
144 }),
145 FrameTimeDiagnosticsPlugin::default(),
146 LogDiagnosticsPlugin::default(),
147 ))
148 .insert_resource(WinitSettings {
149 focused_mode: UpdateMode::Continuous,
150 unfocused_mode: UpdateMode::Continuous,
151 })
152 .insert_resource(args)
153 .insert_resource(BevyCounter {
154 count: 0,
155 color: Color::WHITE,
156 })
157 .add_systems(Startup, setup)
158 .add_systems(FixedUpdate, scheduled_spawner)
159 .add_systems(
160 Update,
161 (
162 mouse_handler,
163 movement_system,
164 collision_system,
165 counter_system,
166 ),
167 )
168 .insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
169 FIXED_TIMESTEP,
170 )))
171 .run();
172}
173
174#[derive(Resource)]
175struct BirdScheduled {
176 waves: usize,
177 per_wave: usize,
178}
179
180fn scheduled_spawner(
181 mut commands: Commands,
182 args: Res<Args>,
183 window: Single<&Window>,
184 mut scheduled: ResMut<BirdScheduled>,
185 mut counter: ResMut<BevyCounter>,
186 bird_resources: ResMut<BirdResources>,
187) {
188 if scheduled.waves > 0 {
189 let bird_resources = bird_resources.into_inner();
190 spawn_birds(
191 &mut commands,
192 args.into_inner(),
193 &window.resolution,
194 &mut counter,
195 scheduled.per_wave,
196 bird_resources,
197 None,
198 scheduled.waves - 1,
199 );
200
201 scheduled.waves -= 1;
202 }
203}
204
205#[derive(Resource)]
206struct BirdResources {
207 textures: Vec<Handle<Image>>,
208 materials: Vec<Handle<ColorMaterial>>,
209 quad: Handle<Mesh>,
210 color_rng: ChaCha8Rng,
211 material_rng: ChaCha8Rng,
212 velocity_rng: ChaCha8Rng,
213 transform_rng: ChaCha8Rng,
214}
215
216#[derive(Component)]
217struct StatsText;
218
219fn setup(
220 mut commands: Commands,
221 args: Res<Args>,
222 asset_server: Res<AssetServer>,
223 mut meshes: ResMut<Assets<Mesh>>,
224 material_assets: ResMut<Assets<ColorMaterial>>,
225 images: ResMut<Assets<Image>>,
226 window: Single<&Window>,
227 counter: ResMut<BevyCounter>,
228) {
229 warn!(include_str!("warning_string.txt"));
230
231 let args = args.into_inner();
232 let images = images.into_inner();
233
234 let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
235 if matches!(args.mode, Mode::Sprite) || args.material_texture_count > 0 {
236 textures.push(asset_server.load("branding/icon.png"));
237 }
238 init_textures(&mut textures, args, images);
239
240 let material_assets = material_assets.into_inner();
241 let materials = init_materials(args, &textures, material_assets);
242
243 let mut bird_resources = BirdResources {
244 textures,
245 materials,
246 quad: meshes.add(Rectangle::from_size(Vec2::splat(BIRD_TEXTURE_SIZE as f32))),
247 color_rng: ChaCha8Rng::seed_from_u64(42),
250 material_rng: ChaCha8Rng::seed_from_u64(42),
251 velocity_rng: ChaCha8Rng::seed_from_u64(42),
252 transform_rng: ChaCha8Rng::seed_from_u64(42),
253 };
254
255 let font = TextFont {
256 font_size: 40.0,
257 ..Default::default()
258 };
259
260 commands.spawn(Camera2d);
261 commands
262 .spawn((
263 Node {
264 position_type: PositionType::Absolute,
265 padding: UiRect::all(Val::Px(5.0)),
266 ..default()
267 },
268 BackgroundColor(Color::BLACK.with_alpha(0.75)),
269 GlobalZIndex(i32::MAX),
270 ))
271 .with_children(|p| {
272 p.spawn((Text::default(), StatsText)).with_children(|p| {
273 p.spawn((
274 TextSpan::new("Bird Count: "),
275 font.clone(),
276 TextColor(LIME.into()),
277 ));
278 p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
279 p.spawn((
280 TextSpan::new("\nFPS (raw): "),
281 font.clone(),
282 TextColor(LIME.into()),
283 ));
284 p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
285 p.spawn((
286 TextSpan::new("\nFPS (SMA): "),
287 font.clone(),
288 TextColor(LIME.into()),
289 ));
290 p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
291 p.spawn((
292 TextSpan::new("\nFPS (EMA): "),
293 font.clone(),
294 TextColor(LIME.into()),
295 ));
296 p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
297 });
298 });
299
300 let mut scheduled = BirdScheduled {
301 per_wave: args.per_wave,
302 waves: args.waves,
303 };
304
305 if args.benchmark {
306 let counter = counter.into_inner();
307 for wave in (0..scheduled.waves).rev() {
308 spawn_birds(
309 &mut commands,
310 args,
311 &window.resolution,
312 counter,
313 scheduled.per_wave,
314 &mut bird_resources,
315 Some(wave),
316 wave,
317 );
318 }
319 scheduled.waves = 0;
320 }
321 commands.insert_resource(bird_resources);
322 commands.insert_resource(scheduled);
323}
324
325fn mouse_handler(
326 mut commands: Commands,
327 args: Res<Args>,
328 time: Res<Time>,
329 mouse_button_input: Res<ButtonInput<MouseButton>>,
330 window: Query<&Window>,
331 bird_resources: ResMut<BirdResources>,
332 mut counter: ResMut<BevyCounter>,
333 mut rng: Local<Option<ChaCha8Rng>>,
334 mut wave: Local<usize>,
335) {
336 let Ok(window) = window.single() else {
337 return;
338 };
339
340 if rng.is_none() {
341 *rng = Some(ChaCha8Rng::seed_from_u64(42));
344 }
345 let rng = rng.as_mut().unwrap();
346
347 if mouse_button_input.just_released(MouseButton::Left) {
348 counter.color = Color::linear_rgb(rng.r#gen(), rng.r#gen(), rng.r#gen());
349 }
350
351 if mouse_button_input.pressed(MouseButton::Left) {
352 let spawn_count = (BIRDS_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
353 spawn_birds(
354 &mut commands,
355 args.into_inner(),
356 &window.resolution,
357 &mut counter,
358 spawn_count,
359 bird_resources.into_inner(),
360 None,
361 *wave,
362 );
363 *wave += 1;
364 }
365}
366
367fn bird_velocity_transform(
368 half_extents: Vec2,
369 mut translation: Vec3,
370 velocity_rng: &mut ChaCha8Rng,
371 waves: Option<usize>,
372 dt: f32,
373) -> (Transform, Vec3) {
374 let mut velocity = Vec3::new(MAX_VELOCITY * (velocity_rng.r#gen::<f32>() - 0.5), 0., 0.);
375
376 if let Some(waves) = waves {
377 for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
380 step_movement(&mut translation, &mut velocity, dt);
381 handle_collision(half_extents, &translation, &mut velocity);
382 }
383 }
384 (
385 Transform::from_translation(translation).with_scale(Vec3::splat(BIRD_SCALE)),
386 velocity,
387 )
388}
389
390const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
391
392fn spawn_birds(
393 commands: &mut Commands,
394 args: &Args,
395 primary_window_resolution: &WindowResolution,
396 counter: &mut BevyCounter,
397 spawn_count: usize,
398 bird_resources: &mut BirdResources,
399 waves_to_simulate: Option<usize>,
400 wave: usize,
401) {
402 let bird_x = (primary_window_resolution.width() / -2.) + HALF_BIRD_SIZE;
403 let bird_y = (primary_window_resolution.height() / 2.) - HALF_BIRD_SIZE;
404
405 let half_extents = 0.5 * primary_window_resolution.size();
406
407 let color = counter.color;
408 let current_count = counter.count;
409
410 match args.mode {
411 Mode::Sprite => {
412 let batch = (0..spawn_count)
413 .map(|count| {
414 let bird_z = if args.ordered_z {
415 (current_count + count) as f32 * 0.00001
416 } else {
417 bird_resources.transform_rng.r#gen::<f32>()
418 };
419
420 let (transform, velocity) = bird_velocity_transform(
421 half_extents,
422 Vec3::new(bird_x, bird_y, bird_z),
423 &mut bird_resources.velocity_rng,
424 waves_to_simulate,
425 FIXED_DELTA_TIME,
426 );
427
428 let color = if args.vary_per_instance {
429 Color::linear_rgb(
430 bird_resources.color_rng.r#gen(),
431 bird_resources.color_rng.r#gen(),
432 bird_resources.color_rng.r#gen(),
433 )
434 } else {
435 color
436 };
437 (
438 Sprite {
439 image: bird_resources
440 .textures
441 .choose(&mut bird_resources.material_rng)
442 .unwrap()
443 .clone(),
444 color,
445 ..default()
446 },
447 transform,
448 Bird { velocity },
449 )
450 })
451 .collect::<Vec<_>>();
452 commands.spawn_batch(batch);
453 }
454 Mode::Mesh2d => {
455 let batch = (0..spawn_count)
456 .map(|count| {
457 let bird_z = if args.ordered_z {
458 (current_count + count) as f32 * 0.00001
459 } else {
460 bird_resources.transform_rng.r#gen::<f32>()
461 };
462
463 let (transform, velocity) = bird_velocity_transform(
464 half_extents,
465 Vec3::new(bird_x, bird_y, bird_z),
466 &mut bird_resources.velocity_rng,
467 waves_to_simulate,
468 FIXED_DELTA_TIME,
469 );
470
471 let material =
472 if args.vary_per_instance || args.material_texture_count > args.waves {
473 bird_resources
474 .materials
475 .choose(&mut bird_resources.material_rng)
476 .unwrap()
477 .clone()
478 } else {
479 bird_resources.materials[wave % bird_resources.materials.len()].clone()
480 };
481 (
482 Mesh2d(bird_resources.quad.clone()),
483 MeshMaterial2d(material),
484 transform,
485 Bird { velocity },
486 )
487 })
488 .collect::<Vec<_>>();
489 commands.spawn_batch(batch);
490 }
491 }
492
493 counter.count += spawn_count;
494 counter.color = Color::linear_rgb(
495 bird_resources.color_rng.r#gen(),
496 bird_resources.color_rng.r#gen(),
497 bird_resources.color_rng.r#gen(),
498 );
499}
500
501fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
502 translation.x += velocity.x * dt;
503 translation.y += velocity.y * dt;
504 velocity.y += GRAVITY * dt;
505}
506
507fn movement_system(
508 args: Res<Args>,
509 time: Res<Time>,
510 mut bird_query: Query<(&mut Bird, &mut Transform)>,
511) {
512 let dt = if args.benchmark {
513 FIXED_DELTA_TIME
514 } else {
515 time.delta_secs()
516 };
517 for (mut bird, mut transform) in &mut bird_query {
518 step_movement(&mut transform.translation, &mut bird.velocity, dt);
519 }
520}
521
522fn handle_collision(half_extents: Vec2, translation: &Vec3, velocity: &mut Vec3) {
523 if (velocity.x > 0. && translation.x + HALF_BIRD_SIZE > half_extents.x)
524 || (velocity.x <= 0. && translation.x - HALF_BIRD_SIZE < -half_extents.x)
525 {
526 velocity.x = -velocity.x;
527 }
528 let velocity_y = velocity.y;
529 if velocity_y < 0. && translation.y - HALF_BIRD_SIZE < -half_extents.y {
530 velocity.y = -velocity_y;
531 }
532 if translation.y + HALF_BIRD_SIZE > half_extents.y && velocity_y > 0.0 {
533 velocity.y = 0.0;
534 }
535}
536fn collision_system(window: Query<&Window>, mut bird_query: Query<(&mut Bird, &Transform)>) {
537 let Ok(window) = window.single() else {
538 return;
539 };
540
541 let half_extents = 0.5 * window.size();
542
543 for (mut bird, transform) in &mut bird_query {
544 handle_collision(half_extents, &transform.translation, &mut bird.velocity);
545 }
546}
547
548fn counter_system(
549 diagnostics: Res<DiagnosticsStore>,
550 counter: Res<BevyCounter>,
551 query: Single<Entity, With<StatsText>>,
552 mut writer: TextUiWriter,
553) {
554 let text = *query;
555
556 if counter.is_changed() {
557 *writer.text(text, 2) = counter.count.to_string();
558 }
559
560 if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
561 if let Some(raw) = fps.value() {
562 *writer.text(text, 4) = format!("{raw:.2}");
563 }
564 if let Some(sma) = fps.average() {
565 *writer.text(text, 6) = format!("{sma:.2}");
566 }
567 if let Some(ema) = fps.smoothed() {
568 *writer.text(text, 8) = format!("{ema:.2}");
569 }
570 };
571}
572
573fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
574 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
577 while textures.len() < args.material_texture_count {
578 let pixel = [color_rng.r#gen(), color_rng.r#gen(), color_rng.r#gen(), 255];
579 textures.push(images.add(Image::new_fill(
580 Extent3d {
581 width: BIRD_TEXTURE_SIZE as u32,
582 height: BIRD_TEXTURE_SIZE as u32,
583 depth_or_array_layers: 1,
584 },
585 TextureDimension::D2,
586 &pixel,
587 TextureFormat::Rgba8UnormSrgb,
588 RenderAssetUsages::RENDER_WORLD,
589 )));
590 }
591}
592
593fn init_materials(
594 args: &Args,
595 textures: &[Handle<Image>],
596 assets: &mut Assets<ColorMaterial>,
597) -> Vec<Handle<ColorMaterial>> {
598 let capacity = if args.vary_per_instance {
599 args.per_wave * args.waves
600 } else {
601 args.material_texture_count.max(args.waves)
602 }
603 .max(1);
604
605 let alpha_mode = match args.alpha_mode {
606 AlphaMode::Opaque => AlphaMode2d::Opaque,
607 AlphaMode::Blend => AlphaMode2d::Blend,
608 AlphaMode::AlphaMask => AlphaMode2d::Mask(0.5),
609 };
610
611 let mut materials = Vec::with_capacity(capacity);
612 materials.push(assets.add(ColorMaterial {
613 color: Color::WHITE,
614 texture: textures.first().cloned(),
615 alpha_mode,
616 ..default()
617 }));
618
619 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
622 let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
623 materials.extend(
624 std::iter::repeat_with(|| {
625 assets.add(ColorMaterial {
626 color: Color::srgb_u8(color_rng.r#gen(), color_rng.r#gen(), color_rng.r#gen()),
627 texture: textures.choose(&mut texture_rng).cloned(),
628 alpha_mode,
629 ..default()
630 })
631 })
632 .take(capacity - materials.len()),
633 );
634
635 materials
636}