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