Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/mirror.rs
9308 views
1
//! Demonstrates how to create a mirror with a second camera.
2
3
use std::f32::consts::FRAC_PI_2;
4
5
use crate::widgets::{RadioButton, WidgetClickEvent, WidgetClickSender};
6
use bevy::camera::RenderTarget;
7
use bevy::{
8
asset::RenderAssetUsages,
9
color::palettes::css::GREEN,
10
input::mouse::AccumulatedMouseMotion,
11
math::{reflection_matrix, uvec2, vec3},
12
pbr::{ExtendedMaterial, MaterialExtension},
13
prelude::*,
14
render::render_resource::{
15
AsBindGroup, Extent3d, TextureDimension, TextureFormat, TextureUsages,
16
},
17
shader::ShaderRef,
18
window::{PrimaryWindow, WindowResized},
19
};
20
21
#[path = "../helpers/widgets.rs"]
22
mod widgets;
23
24
/// A resource that stores a handle to the image that contains the rendered
25
/// mirror world.
26
#[derive(Resource)]
27
struct MirrorImage(Handle<Image>);
28
29
/// A marker component for the camera that renders the mirror world.
30
#[derive(Component)]
31
struct MirrorCamera;
32
33
/// A marker component for the mirror mesh itself.
34
#[derive(Component)]
35
struct Mirror;
36
37
/// The dummy material extension that we use for the mirror surface.
38
///
39
/// This shader samples its emissive texture at the screen space position of
40
/// each fragment rather than at the UVs. Effectively, this uses a PBR shader as
41
/// a mask that copies a portion of the emissive texture to the screen, all in
42
/// screen space.
43
///
44
/// We use [`ExtendedMaterial`], as that's the easiest way to implement custom
45
/// shaders that modify the built-in [`StandardMaterial`]. We don't require any
46
/// extra data to be passed to the shader beyond the [`StandardMaterial`] PBR
47
/// fields, but currently Bevy requires at least one field to be present in the
48
/// extended material, so we simply have an unused field.
49
#[derive(Clone, AsBindGroup, Asset, Reflect)]
50
struct ScreenSpaceTextureExtension {
51
/// An unused value that we have just to satisfy [`ExtendedMaterial`]
52
/// requirements.
53
#[uniform(100)]
54
dummy: f32,
55
}
56
57
impl MaterialExtension for ScreenSpaceTextureExtension {
58
fn fragment_shader() -> ShaderRef {
59
"shaders/screen_space_texture_material.wgsl".into()
60
}
61
}
62
63
/// The action that will be performed when the user drags the mouse: either
64
/// moving the camera or moving the rigged model.
65
#[derive(Clone, Copy, PartialEq, Default)]
66
enum DragAction {
67
/// Dragging will move the camera.
68
#[default]
69
MoveCamera,
70
/// Dragging will move the animated fox.
71
MoveFox,
72
}
73
74
/// The settings that the user has currently chosen.
75
///
76
/// Currently, this just consists of the [`DragAction`].
77
#[derive(Resource, Default)]
78
struct AppStatus {
79
/// The action that will be performed when the user drags the mouse: either
80
/// moving the camera or moving the rigged model.
81
drag_action: DragAction,
82
}
83
84
/// A marker component for the help text at the top of the screen.
85
#[derive(Clone, Copy, Component)]
86
struct HelpText;
87
88
/// The coordinates that the camera looks at.
89
const CAMERA_TARGET: Vec3 = vec3(-25.0, 20.0, 0.0);
90
/// The camera stays this distance in meters from the camera target.
91
const CAMERA_ORBIT_DISTANCE: f32 = 500.0;
92
/// The speed at which the user can move the camera vertically, in radians per
93
/// mouse input unit.
94
const CAMERA_PITCH_SPEED: f32 = 0.003;
95
/// The speed at which the user can move the camera horizontally, in radians per
96
/// mouse input unit.
97
const CAMERA_YAW_SPEED: f32 = 0.004;
98
// Limiting pitch stops some unexpected rotation past 90° up or down.
99
const CAMERA_PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
100
101
/// The angle that the mirror faces.
102
///
103
/// The mirror is rotated across the X axis in this many radians.
104
const MIRROR_ROTATION_ANGLE: f32 = -FRAC_PI_2;
105
const MIRROR_POSITION: Vec3 = vec3(-25.0, 75.0, 0.0);
106
107
/// The path to the animated fox model.
108
static FOX_ASSET_PATH: &str = "models/animated/Fox.glb";
109
110
/// The app entry point.
111
fn main() {
112
App::new()
113
.add_plugins(DefaultPlugins.set(WindowPlugin {
114
primary_window: Some(Window {
115
title: "Bevy Mirror Example".into(),
116
..default()
117
}),
118
..default()
119
}))
120
.add_plugins(MaterialPlugin::<
121
ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>,
122
>::default())
123
.init_resource::<AppStatus>()
124
.add_message::<WidgetClickEvent<DragAction>>()
125
.add_systems(Startup, setup)
126
.add_systems(Update, handle_window_resize_messages)
127
.add_systems(Update, (move_camera_on_mouse_down, move_fox_on_mouse_down))
128
.add_systems(Update, widgets::handle_ui_interactions::<DragAction>)
129
.add_systems(
130
Update,
131
(handle_mouse_action_change, update_radio_buttons)
132
.after(widgets::handle_ui_interactions::<DragAction>),
133
)
134
.add_systems(
135
Update,
136
update_mirror_camera_on_main_camera_transform_change.after(move_camera_on_mouse_down),
137
)
138
.add_systems(Update, play_fox_animation)
139
.add_systems(Update, update_help_text)
140
.run();
141
}
142
143
/// A startup system that spawns the scene and sets up the mirror render target.
144
fn setup(
145
mut commands: Commands,
146
windows_query: Query<&Window>,
147
asset_server: Res<AssetServer>,
148
mut meshes: ResMut<Assets<Mesh>>,
149
mut standard_materials: ResMut<Assets<StandardMaterial>>,
150
mut screen_space_texture_materials: ResMut<
151
Assets<ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>>,
152
>,
153
mut images: ResMut<Assets<Image>>,
154
app_status: Res<AppStatus>,
155
) {
156
// Spawn the main camera.
157
let camera_projection = PerspectiveProjection::default();
158
let camera_transform = spawn_main_camera(&mut commands, &camera_projection);
159
160
// Spawn the light.
161
spawn_light(&mut commands);
162
163
// Spawn the objects reflected in the mirror.
164
spawn_ground_plane(&mut commands, &mut meshes, &mut standard_materials);
165
spawn_fox(&mut commands, &asset_server);
166
167
// Spawn the mirror and associated camera.
168
let mirror_render_target_image =
169
create_mirror_texture_resource(&mut commands, &windows_query, &mut images);
170
let mirror_transform = spawn_mirror(
171
&mut commands,
172
&mut meshes,
173
&mut screen_space_texture_materials,
174
mirror_render_target_image.clone(),
175
);
176
spawn_mirror_camera(
177
&mut commands,
178
&camera_transform,
179
&camera_projection,
180
&mirror_transform,
181
mirror_render_target_image,
182
);
183
184
// Spawn the UI.
185
spawn_buttons(&mut commands);
186
spawn_help_text(&mut commands, &app_status);
187
}
188
189
/// Spawns the main camera (not the mirror camera).
190
fn spawn_main_camera(
191
commands: &mut Commands,
192
camera_projection: &PerspectiveProjection,
193
) -> Transform {
194
let camera_transform = Transform::from_translation(
195
vec3(-2.0, 1.0, -2.0).normalize_or_zero() * CAMERA_ORBIT_DISTANCE,
196
)
197
.looking_at(CAMERA_TARGET, Vec3::Y);
198
199
commands.spawn((
200
Camera3d::default(),
201
camera_transform,
202
Projection::Perspective(camera_projection.clone()),
203
));
204
205
camera_transform
206
}
207
208
/// Spawns a directional light to illuminate the scene.
209
fn spawn_light(commands: &mut Commands) {
210
commands.spawn((
211
DirectionalLight {
212
illuminance: 5000.0,
213
..default()
214
},
215
Transform::from_xyz(-85.0, 16.0, -200.0).looking_at(vec3(-50.0, 0.0, 100.0), Vec3::Y),
216
));
217
}
218
219
/// Spawns the circular ground plane object.
220
fn spawn_ground_plane(
221
commands: &mut Commands,
222
meshes: &mut Assets<Mesh>,
223
standard_materials: &mut Assets<StandardMaterial>,
224
) {
225
commands.spawn((
226
Mesh3d(meshes.add(Circle::new(200.0))),
227
MeshMaterial3d(standard_materials.add(Color::from(GREEN))),
228
Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2))
229
.with_translation(vec3(-25.0, 0.0, 0.0)),
230
));
231
}
232
233
/// Creates the initial image that the mirror camera will render the mirror
234
/// world to.
235
fn create_mirror_texture_resource(
236
commands: &mut Commands,
237
windows_query: &Query<&Window>,
238
images: &mut Assets<Image>,
239
) -> Handle<Image> {
240
let window = windows_query.iter().next().expect("No window found");
241
let window_size = uvec2(window.physical_width(), window.physical_height());
242
let image = create_mirror_texture_image(images, window_size);
243
commands.insert_resource(MirrorImage(image.clone()));
244
image
245
}
246
247
/// Spawns the camera that renders the mirror world.
248
fn spawn_mirror_camera(
249
commands: &mut Commands,
250
camera_transform: &Transform,
251
camera_projection: &PerspectiveProjection,
252
mirror_transform: &Transform,
253
mirror_render_target: Handle<Image>,
254
) {
255
let (mirror_camera_transform, mirror_camera_projection) =
256
calculate_mirror_camera_transform_and_projection(
257
camera_transform,
258
camera_projection,
259
mirror_transform,
260
);
261
262
commands.spawn((
263
Camera3d::default(),
264
Camera {
265
order: -1,
266
// Reflecting the model across the mirror will flip the winding of
267
// all the polygons. Therefore, in order to properly backface cull,
268
// we need to turn on `invert_culling`.
269
invert_culling: true,
270
..default()
271
},
272
RenderTarget::Image(mirror_render_target.clone().into()),
273
mirror_camera_transform,
274
Projection::Perspective(mirror_camera_projection),
275
MirrorCamera,
276
));
277
}
278
279
/// Spawns the animated fox.
280
///
281
/// Note that this doesn't play the animation; that's handled in
282
/// [`play_fox_animation`].
283
fn spawn_fox(commands: &mut Commands, asset_server: &AssetServer) {
284
commands.spawn((
285
SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_ASSET_PATH))),
286
Transform::from_xyz(-50.0, 0.0, -100.0),
287
));
288
}
289
290
/// Spawns the mirror plane mesh and returns its transform.
291
fn spawn_mirror(
292
commands: &mut Commands,
293
meshes: &mut Assets<Mesh>,
294
screen_space_texture_materials: &mut Assets<
295
ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>,
296
>,
297
mirror_render_target: Handle<Image>,
298
) -> Transform {
299
let mirror_transform = Transform::from_scale(vec3(300.0, 1.0, 150.0))
300
.with_rotation(Quat::from_rotation_x(MIRROR_ROTATION_ANGLE))
301
.with_translation(MIRROR_POSITION);
302
303
commands.spawn((
304
Mesh3d(meshes.add(Plane3d::default().mesh().size(1.0, 1.0))),
305
MeshMaterial3d(screen_space_texture_materials.add(ExtendedMaterial {
306
base: StandardMaterial {
307
base_color: Color::BLACK,
308
emissive: Color::WHITE.into(),
309
emissive_texture: Some(mirror_render_target),
310
perceptual_roughness: 0.0,
311
metallic: 1.0,
312
..default()
313
},
314
extension: ScreenSpaceTextureExtension { dummy: 0.0 },
315
})),
316
mirror_transform,
317
Mirror,
318
));
319
320
mirror_transform
321
}
322
323
/// Spawns the buttons at the bottom of the screen.
324
fn spawn_buttons(commands: &mut Commands) {
325
// Spawn the radio buttons that allow the user to select an object to
326
// control.
327
commands.spawn((
328
widgets::main_ui_node(),
329
children![widgets::option_buttons(
330
"Drag Action",
331
&[
332
(DragAction::MoveCamera, "Move Camera"),
333
(DragAction::MoveFox, "Move Fox"),
334
],
335
)],
336
));
337
}
338
339
/// Given the transform and projection of the main camera, returns an
340
/// appropriate transform and projection for the mirror camera.
341
fn calculate_mirror_camera_transform_and_projection(
342
main_camera_transform: &Transform,
343
main_camera_projection: &PerspectiveProjection,
344
mirror_transform: &Transform,
345
) -> (Transform, PerspectiveProjection) {
346
// Calculate the reflection matrix (a.k.a. Householder matrix) that will
347
// reflect the scene across the mirror plane.
348
//
349
// Note that you must calculate this in *matrix* form and only *afterward*
350
// convert to a `Transform` instead of composing `Transform`s. This is
351
// because the reflection matrix has non-uniform scale, and composing
352
// transforms can't always handle composition of matrices with non-uniform
353
// scales.
354
let mirror_camera_transform = Transform::from_matrix(
355
Mat4::from_mat3a(reflection_matrix(Vec3::NEG_Z)) * main_camera_transform.to_matrix(),
356
);
357
358
// Compute the distance from the camera to the mirror plane. This will be
359
// used to calculate the distance to the near clip plane for the mirror
360
// world.
361
let distance_from_camera_to_mirror = InfinitePlane3d::new(mirror_transform.rotation * Vec3::Y)
362
.signed_distance(
363
Isometry3d::IDENTITY,
364
mirror_transform.translation - main_camera_transform.translation,
365
);
366
367
// Compute the normal of the mirror plane in view space.
368
let view_from_world = main_camera_transform.compute_affine().matrix3.inverse();
369
let mirror_projection_plane_normal =
370
(view_from_world * (mirror_transform.rotation * Vec3::NEG_Y)).normalize();
371
372
// Compute the final projection. It should match the main camera projection,
373
// except that `near` and `near_normal` should be set to the updated near
374
// plane and near normal plane as above.
375
let mirror_camera_projection = PerspectiveProjection {
376
near_clip_plane: mirror_projection_plane_normal.extend(distance_from_camera_to_mirror),
377
..*main_camera_projection
378
};
379
380
(mirror_camera_transform, mirror_camera_projection)
381
}
382
383
/// A system that resizes the render target image when the user resizes the window.
384
///
385
/// Since the image that stores the rendered mirror world has the same physical
386
/// size as the window, we need to reallocate it and reattach it to the mirror
387
/// material whenever the window size changes.
388
fn handle_window_resize_messages(
389
windows_query: Query<&Window>,
390
mut mirror_cameras_query: Query<&mut RenderTarget, With<MirrorCamera>>,
391
mut images: ResMut<Assets<Image>>,
392
mut mirror_image: ResMut<MirrorImage>,
393
mut screen_space_texture_materials: ResMut<
394
Assets<ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>>,
395
>,
396
mut resize_messages: MessageReader<WindowResized>,
397
) {
398
// We run at most once, regardless of the number of window resize messages
399
// there were this frame.
400
let Some(resize_message) = resize_messages.read().next() else {
401
return;
402
};
403
let Ok(window) = windows_query.get(resize_message.window) else {
404
return;
405
};
406
407
let window_size = uvec2(window.physical_width(), window.physical_height());
408
let image = create_mirror_texture_image(&mut images, window_size);
409
images.remove(mirror_image.0.id());
410
411
mirror_image.0 = image.clone();
412
413
for mut target in mirror_cameras_query.iter_mut() {
414
*target = image.clone().into();
415
}
416
417
for (_, material) in screen_space_texture_materials.iter_mut() {
418
material.base.emissive_texture = Some(image.clone());
419
}
420
}
421
422
/// Creates the image that will be used to store the reflected scene.
423
fn create_mirror_texture_image(images: &mut Assets<Image>, window_size: UVec2) -> Handle<Image> {
424
let mirror_image_extent = Extent3d {
425
width: window_size.x,
426
height: window_size.y,
427
depth_or_array_layers: 1,
428
};
429
430
let mut image = Image::new_uninit(
431
mirror_image_extent,
432
TextureDimension::D2,
433
TextureFormat::Bgra8UnormSrgb,
434
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
435
);
436
image.texture_descriptor.usage |=
437
TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT;
438
439
images.add(image)
440
}
441
442
// Moves the fox when the user moves the mouse with the left button down.
443
fn move_fox_on_mouse_down(
444
mut scene_roots_query: Query<&mut Transform, With<SceneRoot>>,
445
windows_query: Query<&Window, With<PrimaryWindow>>,
446
cameras_query: Query<(&Camera, &GlobalTransform)>,
447
interactions_query: Query<&Interaction, With<RadioButton>>,
448
buttons: Res<ButtonInput<MouseButton>>,
449
app_status: Res<AppStatus>,
450
) {
451
// Only process the mouse motion if the left mouse button is pressed, the
452
// mouse action is set to move the fox, and the pointer isn't over a UI
453
// widget.
454
if app_status.drag_action != DragAction::MoveFox
455
|| !buttons.pressed(MouseButton::Left)
456
|| interactions_query
457
.iter()
458
.any(|interaction| *interaction != Interaction::None)
459
{
460
return;
461
}
462
463
// Find out where the user clicked the mouse.
464
let Some(mouse_position) = windows_query
465
.iter()
466
.next()
467
.and_then(Window::cursor_position)
468
else {
469
return;
470
};
471
472
// Grab the camera.
473
let Some((camera, camera_transform)) = cameras_query.iter().next() else {
474
return;
475
};
476
477
// Figure out where the user clicked on the plane.
478
let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else {
479
return;
480
};
481
let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else {
482
return;
483
};
484
let plane_intersection = ray.origin + ray.direction.normalize() * ray_distance;
485
486
// Move the fox.
487
for mut transform in scene_roots_query.iter_mut() {
488
transform.translation = transform.translation.with_xz(plane_intersection.xz());
489
}
490
}
491
492
/// A system that changes the drag action when the user clicks on one of the
493
/// radio buttons.
494
fn handle_mouse_action_change(
495
mut app_status: ResMut<AppStatus>,
496
mut messages: MessageReader<WidgetClickEvent<DragAction>>,
497
) {
498
for message in messages.read() {
499
app_status.drag_action = **message;
500
}
501
}
502
503
/// A system that updates the radio buttons at the bottom of the screen to
504
/// reflect the current drag action.
505
fn update_radio_buttons(
506
mut widgets_query: Query<(
507
Entity,
508
Option<&mut BackgroundColor>,
509
Has<Text>,
510
&WidgetClickSender<DragAction>,
511
)>,
512
app_status: Res<AppStatus>,
513
mut text_ui_writer: TextUiWriter,
514
) {
515
for (entity, maybe_bg_color, has_text, sender) in &mut widgets_query {
516
let selected = app_status.drag_action == **sender;
517
if let Some(mut bg_color) = maybe_bg_color {
518
widgets::update_ui_radio_button(&mut bg_color, selected);
519
}
520
if has_text {
521
widgets::update_ui_radio_button_text(entity, &mut text_ui_writer, selected);
522
}
523
}
524
}
525
526
/// A system that processes user mouse actions that move the camera.
527
///
528
/// This is mostly copied from `examples/camera/camera_orbit.rs`.
529
fn move_camera_on_mouse_down(
530
mut main_cameras_query: Query<&mut Transform, (With<Camera>, Without<MirrorCamera>)>,
531
interactions_query: Query<&Interaction, With<RadioButton>>,
532
mouse_buttons: Res<ButtonInput<MouseButton>>,
533
mouse_motion: Res<AccumulatedMouseMotion>,
534
app_status: Res<AppStatus>,
535
) {
536
// Only process the mouse motion if the left mouse button is pressed, the
537
// mouse action is set to move the fox, and the pointer isn't over a UI
538
// widget.
539
if app_status.drag_action != DragAction::MoveCamera
540
|| !mouse_buttons.pressed(MouseButton::Left)
541
|| interactions_query
542
.iter()
543
.any(|interaction| *interaction != Interaction::None)
544
{
545
return;
546
}
547
548
let delta = mouse_motion.delta;
549
550
// Mouse motion is one of the few inputs that should not be multiplied by delta time,
551
// as we are already receiving the full movement since the last frame was rendered. Multiplying
552
// by delta time here would make the movement slower that it should be.
553
let delta_pitch = delta.y * CAMERA_PITCH_SPEED;
554
let delta_yaw = delta.x * CAMERA_YAW_SPEED;
555
556
for mut main_camera_transform in &mut main_cameras_query {
557
// Obtain the existing pitch and yaw values from the transform.
558
let (yaw, pitch, _) = main_camera_transform.rotation.to_euler(EulerRot::YXZ);
559
560
// Establish the new yaw and pitch, preventing the pitch value from exceeding our limits.
561
let pitch = (pitch + delta_pitch).clamp(-CAMERA_PITCH_LIMIT, CAMERA_PITCH_LIMIT);
562
let yaw = yaw + delta_yaw;
563
main_camera_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0);
564
565
// Adjust the translation to maintain the correct orientation toward the orbit target.
566
// In our example it's a static target, but this could easily be customized.
567
let target = Vec3::ZERO;
568
main_camera_transform.translation =
569
target - main_camera_transform.forward() * CAMERA_ORBIT_DISTANCE;
570
}
571
}
572
573
/// Updates the position, rotation, and projection of the mirror camera when the
574
/// main camera is moved.
575
///
576
/// When the main camera is moved, the mirror camera must be moved to match it.
577
/// The *projection* on the mirror camera must also be altered, because the
578
/// projection takes the view-space rotation of and distance to the mirror into
579
/// account.
580
fn update_mirror_camera_on_main_camera_transform_change(
581
main_cameras_query: Query<
582
(&Transform, &Projection),
583
(Changed<Transform>, With<Camera>, Without<MirrorCamera>),
584
>,
585
mut mirror_cameras_query: Query<
586
(&mut Transform, &mut Projection),
587
(With<Camera>, With<MirrorCamera>, Without<Mirror>),
588
>,
589
mirrors_query: Query<&Transform, (Without<MirrorCamera>, With<Mirror>)>,
590
) {
591
let Some((main_camera_transform, Projection::Perspective(main_camera_projection))) =
592
main_cameras_query.iter().next()
593
else {
594
return;
595
};
596
597
let Some(mirror_transform) = mirrors_query.iter().next() else {
598
return;
599
};
600
601
// Here we need the transforms of both the camera and the mirror in order to
602
// properly calculate the new projection.
603
let (new_mirror_camera_transform, new_mirror_camera_projection) =
604
calculate_mirror_camera_transform_and_projection(
605
main_camera_transform,
606
main_camera_projection,
607
mirror_transform,
608
);
609
610
for (mut mirror_camera_transform, mut mirror_camera_projection) in &mut mirror_cameras_query {
611
*mirror_camera_transform = new_mirror_camera_transform;
612
*mirror_camera_projection = Projection::Perspective(new_mirror_camera_projection.clone());
613
}
614
}
615
616
/// Plays the initial animation on the fox model.
617
fn play_fox_animation(
618
mut commands: Commands,
619
mut animation_players_query: Query<
620
(Entity, &mut AnimationPlayer),
621
Without<AnimationGraphHandle>,
622
>,
623
asset_server: Res<AssetServer>,
624
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
625
) {
626
// Only pick up animation players that don't already have an animation graph
627
// handle.
628
// This ensures that we only start playing the animation once.
629
if animation_players_query.is_empty() {
630
return;
631
}
632
633
let fox_animation = asset_server.load(GltfAssetLabel::Animation(0).from_asset(FOX_ASSET_PATH));
634
let (fox_animation_graph, fox_animation_node) =
635
AnimationGraph::from_clip(fox_animation.clone());
636
let fox_animation_graph = animation_graphs.add(fox_animation_graph);
637
638
for (entity, mut animation_player) in animation_players_query.iter_mut() {
639
commands
640
.entity(entity)
641
.insert(AnimationGraphHandle(fox_animation_graph.clone()));
642
animation_player.play(fox_animation_node).repeat();
643
}
644
}
645
646
/// Spawns the help text at the top of the screen.
647
fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
648
commands.spawn((
649
Text::new(create_help_string(app_status)),
650
Node {
651
position_type: PositionType::Absolute,
652
top: px(12),
653
left: px(12),
654
..default()
655
},
656
HelpText,
657
));
658
}
659
660
/// Creates the help string at the top left of the screen.
661
fn create_help_string(app_status: &AppStatus) -> String {
662
format!(
663
"Click and drag to move the {}",
664
match app_status.drag_action {
665
DragAction::MoveCamera => "camera",
666
DragAction::MoveFox => "fox",
667
}
668
)
669
}
670
671
/// Updates the help text in the top left of the screen to reflect the current
672
/// drag mode.
673
fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
674
for mut text in &mut help_text {
675
text.0 = create_help_string(&app_status);
676
}
677
}
678
679