1use std::{f32::consts::PI, time::Duration};
5
6use argh::FromArgs;
7use bevy::{
8 diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
9 light::CascadeShadowConfigBuilder,
10 prelude::*,
11 scene::SceneInstanceReady,
12 window::{PresentMode, WindowResolution},
13 winit::WinitSettings,
14};
15
16#[derive(FromArgs, Resource)]
17struct Args {
19 #[argh(switch)]
21 sync: bool,
22
23 #[argh(option, default = "1000")]
25 count: usize,
26}
27
28#[derive(Resource)]
29struct Foxes {
30 count: usize,
31 speed: f32,
32 moving: bool,
33 sync: bool,
34}
35
36fn main() {
37 #[cfg(not(target_arch = "wasm32"))]
39 let args: Args = argh::from_env();
40 #[cfg(target_arch = "wasm32")]
41 let args = Args::from_args(&[], &[]).unwrap();
42
43 App::new()
44 .add_plugins((
45 DefaultPlugins.set(WindowPlugin {
46 primary_window: Some(Window {
47 title: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".into(),
48 present_mode: PresentMode::AutoNoVsync,
49 resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
50 ..default()
51 }),
52 ..default()
53 }),
54 FrameTimeDiagnosticsPlugin::default(),
55 LogDiagnosticsPlugin::default(),
56 ))
57 .insert_resource(WinitSettings::continuous())
58 .insert_resource(Foxes {
59 count: args.count,
60 speed: 2.0,
61 moving: true,
62 sync: args.sync,
63 })
64 .add_systems(Startup, setup)
65 .add_systems(
66 Update,
67 (
68 keyboard_animation_control,
69 update_fox_rings.after(keyboard_animation_control),
70 ),
71 )
72 .run();
73}
74
75#[derive(Resource)]
76struct Animations {
77 node_indices: Vec<AnimationNodeIndex>,
78 graph: Handle<AnimationGraph>,
79}
80
81const RING_SPACING: f32 = 2.0;
82const FOX_SPACING: f32 = 2.0;
83
84#[derive(Component, Clone, Copy)]
85enum RotationDirection {
86 CounterClockwise,
87 Clockwise,
88}
89
90impl RotationDirection {
91 fn sign(&self) -> f32 {
92 match self {
93 RotationDirection::CounterClockwise => 1.0,
94 RotationDirection::Clockwise => -1.0,
95 }
96 }
97}
98
99#[derive(Component)]
100struct Ring {
101 radius: f32,
102}
103
104fn setup(
105 mut commands: Commands,
106 asset_server: Res<AssetServer>,
107 mut meshes: ResMut<Assets<Mesh>>,
108 mut materials: ResMut<Assets<StandardMaterial>>,
109 mut animation_graphs: ResMut<Assets<AnimationGraph>>,
110 foxes: Res<Foxes>,
111) {
112 warn!(include_str!("warning_string.txt"));
113
114 let animation_clips = [
116 asset_server.load(GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb")),
117 asset_server.load(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb")),
118 asset_server.load(GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb")),
119 ];
120 let mut animation_graph = AnimationGraph::new();
121 let node_indices = animation_graph
122 .add_clips(animation_clips.iter().cloned(), 1.0, animation_graph.root)
123 .collect();
124 commands.insert_resource(Animations {
125 node_indices,
126 graph: animation_graphs.add(animation_graph),
127 });
128
129 let fox_handle =
135 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb"));
136
137 let ring_directions = [
138 (
139 Quat::from_rotation_y(PI),
140 RotationDirection::CounterClockwise,
141 ),
142 (Quat::IDENTITY, RotationDirection::Clockwise),
143 ];
144
145 let mut ring_index = 0;
146 let mut radius = RING_SPACING;
147 let mut foxes_remaining = foxes.count;
148
149 info!("Spawning {} foxes...", foxes.count);
150
151 while foxes_remaining > 0 {
152 let (base_rotation, ring_direction) = ring_directions[ring_index % 2];
153 let ring_parent = commands
154 .spawn((
155 Transform::default(),
156 Visibility::default(),
157 ring_direction,
158 Ring { radius },
159 ))
160 .id();
161
162 let circumference = PI * 2. * radius;
163 let foxes_in_ring = ((circumference / FOX_SPACING) as usize).min(foxes_remaining);
164 let fox_spacing_angle = circumference / (foxes_in_ring as f32 * radius);
165
166 for fox_i in 0..foxes_in_ring {
167 let fox_angle = fox_i as f32 * fox_spacing_angle;
168 let (s, c) = ops::sin_cos(fox_angle);
169 let (x, z) = (radius * c, radius * s);
170
171 commands.entity(ring_parent).with_children(|builder| {
172 builder
173 .spawn((
174 SceneRoot(fox_handle.clone()),
175 Transform::from_xyz(x, 0.0, z)
176 .with_scale(Vec3::splat(0.01))
177 .with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)),
178 ))
179 .observe(setup_scene_once_loaded);
180 });
181 }
182
183 foxes_remaining -= foxes_in_ring;
184 radius += RING_SPACING;
185 ring_index += 1;
186 }
187
188 let zoom = 0.8;
190 let translation = Vec3::new(
191 radius * 1.25 * zoom,
192 radius * 0.5 * zoom,
193 radius * 1.5 * zoom,
194 );
195 commands.spawn((
196 Camera3d::default(),
197 Transform::from_translation(translation)
198 .looking_at(0.2 * Vec3::new(translation.x, 0.0, translation.z), Vec3::Y),
199 ));
200
201 commands.spawn((
203 Mesh3d(meshes.add(Plane3d::default().mesh().size(5000.0, 5000.0))),
204 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
205 ));
206
207 commands.spawn((
209 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
210 DirectionalLight {
211 shadows_enabled: true,
212 ..default()
213 },
214 CascadeShadowConfigBuilder {
215 first_cascade_far_bound: 0.9 * radius,
216 maximum_distance: 2.8 * radius,
217 ..default()
218 }
219 .build(),
220 ));
221
222 println!("Animation controls:");
223 println!(" - spacebar: play / pause");
224 println!(" - arrow up / down: speed up / slow down animation playback");
225 println!(" - arrow left / right: seek backward / forward");
226 println!(" - return: change animation");
227}
228
229fn setup_scene_once_loaded(
231 scene_ready: On<SceneInstanceReady>,
232 animations: Res<Animations>,
233 foxes: Res<Foxes>,
234 mut commands: Commands,
235 children: Query<&Children>,
236 mut players: Query<&mut AnimationPlayer>,
237) {
238 for child in children.iter_descendants(scene_ready.entity) {
239 if let Ok(mut player) = players.get_mut(child) {
240 let playing_animation = player.play(animations.node_indices[0]).repeat();
241 if !foxes.sync {
242 playing_animation.seek_to(scene_ready.entity.index() as f32 / 10.0);
243 }
244 commands
245 .entity(child)
246 .insert(AnimationGraphHandle(animations.graph.clone()));
247 }
248 }
249}
250
251fn update_fox_rings(
252 time: Res<Time>,
253 foxes: Res<Foxes>,
254 mut rings: Query<(&Ring, &RotationDirection, &mut Transform)>,
255) {
256 if !foxes.moving {
257 return;
258 }
259
260 let dt = time.delta_secs();
261 for (ring, rotation_direction, mut transform) in &mut rings {
262 let angular_velocity = foxes.speed / ring.radius;
263 transform.rotate_y(rotation_direction.sign() * angular_velocity * dt);
264 }
265}
266
267fn keyboard_animation_control(
268 keyboard_input: Res<ButtonInput<KeyCode>>,
269 mut animation_player: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
270 animations: Res<Animations>,
271 mut current_animation: Local<usize>,
272 mut foxes: ResMut<Foxes>,
273) {
274 if keyboard_input.just_pressed(KeyCode::Space) {
275 foxes.moving = !foxes.moving;
276 }
277
278 if keyboard_input.just_pressed(KeyCode::ArrowUp) {
279 foxes.speed *= 1.25;
280 }
281
282 if keyboard_input.just_pressed(KeyCode::ArrowDown) {
283 foxes.speed *= 0.8;
284 }
285
286 if keyboard_input.just_pressed(KeyCode::Enter) {
287 *current_animation = (*current_animation + 1) % animations.node_indices.len();
288 }
289
290 for (mut player, mut transitions) in &mut animation_player {
291 if keyboard_input.just_pressed(KeyCode::Space) {
292 if player.all_paused() {
293 player.resume_all();
294 } else {
295 player.pause_all();
296 }
297 }
298
299 if keyboard_input.just_pressed(KeyCode::ArrowUp) {
300 player.adjust_speeds(1.25);
301 }
302
303 if keyboard_input.just_pressed(KeyCode::ArrowDown) {
304 player.adjust_speeds(0.8);
305 }
306
307 if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
308 player.seek_all_by(-0.1);
309 }
310
311 if keyboard_input.just_pressed(KeyCode::ArrowRight) {
312 player.seek_all_by(0.1);
313 }
314
315 if keyboard_input.just_pressed(KeyCode::Enter) {
316 transitions
317 .play(
318 &mut player,
319 animations.node_indices[*current_animation],
320 Duration::from_millis(250),
321 )
322 .repeat();
323 }
324 }
325}