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