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