|
| 1 | +//! This example showcases a 2D top-down camera with smooth player tracking. |
| 2 | +//! |
| 3 | +//! ## Controls |
| 4 | +//! |
| 5 | +//! | Key Binding | Action | |
| 6 | +//! |:---------------------|:--------------| |
| 7 | +//! | `Z`(azerty), `W`(US) | Move forward | |
| 8 | +//! | `S` | Move backward | |
| 9 | +//! | `Q`(azerty), `A`(US) | Move left | |
| 10 | +//! | `D` | Move right | |
| 11 | +
|
| 12 | +use bevy::core_pipeline::bloom::BloomSettings; |
| 13 | +use bevy::math::vec3; |
| 14 | +use bevy::prelude::*; |
| 15 | +use bevy::sprite::{MaterialMesh2dBundle, Mesh2dHandle}; |
| 16 | + |
| 17 | +/// Player movement speed factor. |
| 18 | +const PLAYER_SPEED: f32 = 100.; |
| 19 | + |
| 20 | +/// Camera lerp factor. |
| 21 | +const CAM_LERP_FACTOR: f32 = 2.; |
| 22 | + |
| 23 | +#[derive(Component)] |
| 24 | +struct Player; |
| 25 | + |
| 26 | +fn main() { |
| 27 | + App::new() |
| 28 | + .add_plugins(DefaultPlugins) |
| 29 | + .add_systems(Startup, (setup_scene, setup_instructions, setup_camera)) |
| 30 | + .add_systems(Update, (move_player, update_camera).chain()) |
| 31 | + .run(); |
| 32 | +} |
| 33 | + |
| 34 | +fn setup_scene( |
| 35 | + mut commands: Commands, |
| 36 | + mut meshes: ResMut<Assets<Mesh>>, |
| 37 | + mut materials: ResMut<Assets<ColorMaterial>>, |
| 38 | +) { |
| 39 | + // World where we move the player |
| 40 | + commands.spawn(MaterialMesh2dBundle { |
| 41 | + mesh: Mesh2dHandle(meshes.add(Rectangle::new(1000., 700.))), |
| 42 | + material: materials.add(Color::srgb(0.2, 0.2, 0.3)), |
| 43 | + ..default() |
| 44 | + }); |
| 45 | + |
| 46 | + // Player |
| 47 | + commands.spawn(( |
| 48 | + Player, |
| 49 | + MaterialMesh2dBundle { |
| 50 | + mesh: meshes.add(Circle::new(25.)).into(), |
| 51 | + material: materials.add(Color::srgb(6.25, 9.4, 9.1)), // RGB values exceed 1 to achieve a bright color for the bloom effect |
| 52 | + transform: Transform { |
| 53 | + translation: vec3(0., 0., 2.), |
| 54 | + ..default() |
| 55 | + }, |
| 56 | + ..default() |
| 57 | + }, |
| 58 | + )); |
| 59 | +} |
| 60 | + |
| 61 | +fn setup_instructions(mut commands: Commands) { |
| 62 | + commands.spawn( |
| 63 | + TextBundle::from_section( |
| 64 | + "Move the light with ZQSD or WASD.\nThe camera will smoothly track the light.", |
| 65 | + TextStyle::default(), |
| 66 | + ) |
| 67 | + .with_style(Style { |
| 68 | + position_type: PositionType::Absolute, |
| 69 | + bottom: Val::Px(12.0), |
| 70 | + left: Val::Px(12.0), |
| 71 | + ..default() |
| 72 | + }), |
| 73 | + ); |
| 74 | +} |
| 75 | + |
| 76 | +fn setup_camera(mut commands: Commands) { |
| 77 | + commands.spawn(( |
| 78 | + Camera2dBundle { |
| 79 | + camera: Camera { |
| 80 | + hdr: true, // HDR is required for the bloom effect |
| 81 | + ..default() |
| 82 | + }, |
| 83 | + ..default() |
| 84 | + }, |
| 85 | + BloomSettings::NATURAL, |
| 86 | + )); |
| 87 | +} |
| 88 | + |
| 89 | +/// Update the camera position by tracking the player. |
| 90 | +fn update_camera( |
| 91 | + mut camera: Query<&mut Transform, (With<Camera2d>, Without<Player>)>, |
| 92 | + player: Query<&Transform, (With<Player>, Without<Camera2d>)>, |
| 93 | + time: Res<Time>, |
| 94 | +) { |
| 95 | + let Ok(mut camera) = camera.get_single_mut() else { |
| 96 | + return; |
| 97 | + }; |
| 98 | + |
| 99 | + let Ok(player) = player.get_single() else { |
| 100 | + return; |
| 101 | + }; |
| 102 | + |
| 103 | + let Vec3 { x, y, .. } = player.translation; |
| 104 | + let direction = Vec3::new(x, y, camera.translation.z); |
| 105 | + |
| 106 | + // Applies a smooth effect to camera movement using interpolation between |
| 107 | + // the camera position and the player position on the x and y axes. |
| 108 | + // Here we use the in-game time, to get the elapsed time (in seconds) |
| 109 | + // since the previous update. This avoids jittery movement when tracking |
| 110 | + // the player. |
| 111 | + camera.translation = camera |
| 112 | + .translation |
| 113 | + .lerp(direction, time.delta_seconds() * CAM_LERP_FACTOR); |
| 114 | +} |
| 115 | + |
| 116 | +/// Update the player position with keyboard inputs. |
| 117 | +fn move_player( |
| 118 | + mut player: Query<&mut Transform, With<Player>>, |
| 119 | + time: Res<Time>, |
| 120 | + kb_input: Res<ButtonInput<KeyCode>>, |
| 121 | +) { |
| 122 | + let Ok(mut player) = player.get_single_mut() else { |
| 123 | + return; |
| 124 | + }; |
| 125 | + |
| 126 | + let mut direction = Vec2::ZERO; |
| 127 | + |
| 128 | + if kb_input.pressed(KeyCode::KeyW) { |
| 129 | + direction.y += 1.; |
| 130 | + } |
| 131 | + |
| 132 | + if kb_input.pressed(KeyCode::KeyS) { |
| 133 | + direction.y -= 1.; |
| 134 | + } |
| 135 | + |
| 136 | + if kb_input.pressed(KeyCode::KeyA) { |
| 137 | + direction.x -= 1.; |
| 138 | + } |
| 139 | + |
| 140 | + if kb_input.pressed(KeyCode::KeyD) { |
| 141 | + direction.x += 1.; |
| 142 | + } |
| 143 | + |
| 144 | + // Progressively update the player's position over time. Normalize the |
| 145 | + // direction vector to prevent it from exceeding a magnitude of 1 when |
| 146 | + // moving diagonally. |
| 147 | + let move_delta = direction.normalize_or_zero() * PLAYER_SPEED * time.delta_seconds(); |
| 148 | + player.translation += move_delta.extend(0.); |
| 149 | +} |
0 commit comments