Path: blob/main/crates/bevy_camera_controller/src/pan_camera.rs
7228 views
//! A controller for 2D cameras that supports panning, zooming, and rotation.1//!2//! To use this controller, add [`PanCameraPlugin`] to your app,3//! and insert a [`PanCamera`] component into your camera entity.4//!5//! To configure the settings of this controller, modify the fields of the [`PanCamera`] component.67use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems};8use bevy_camera::Camera;9use bevy_ecs::prelude::*;10use bevy_input::keyboard::KeyCode;11use bevy_input::mouse::{AccumulatedMouseScroll, MouseScrollUnit};12use bevy_input::ButtonInput;13use bevy_math::{Vec2, Vec3};14use bevy_time::{Real, Time};15use bevy_transform::prelude::Transform;1617use core::{f32::consts::*, fmt};1819/// A plugin that enables 2D camera panning and zooming controls.20///21/// Add this plugin to your [`App`] to enable [`PanCamera`] behavior22/// on any camera entity that has the [`PanCamera`] component.23pub struct PanCameraPlugin;2425impl Plugin for PanCameraPlugin {26fn build(&self, app: &mut App) {27app.add_systems(28RunFixedMainLoop,29run_pancamera_controller.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),30);31}32}3334/// Configuration and state for a 2D panning camera controller.35///36/// Add this component to a [`Camera`] entity to enable keyboard and mouse controls37/// for panning, zooming, and optional rotation. Requires the [`PanCameraPlugin`].38#[derive(Component)]39pub struct PanCamera {40/// Enables this [`PanCamera`] when `true`.41pub enabled: bool,42/// Current zoom level (factor applied to camera scale).43pub zoom_factor: f32,44/// Minimum allowed zoom level.45pub min_zoom: f32,46/// Maximum allowed zoom level.47pub max_zoom: f32,48/// Translation speed for panning movement.49pub zoom_speed: f32,50/// [`KeyCode`] to zoom in.51pub key_zoom_in: Option<KeyCode>,52/// [`KeyCode`] to zoom out.53pub key_zoom_out: Option<KeyCode>,54/// This [`PanCamera`]'s translation speed.55pub pan_speed: f32,56/// [`KeyCode`] for upward translation.57pub key_up: Option<KeyCode>,58/// [`KeyCode`] for downward translation.59pub key_down: Option<KeyCode>,60/// [`KeyCode`] for leftward translation.61pub key_left: Option<KeyCode>,62/// [`KeyCode`] for rightward translation.63pub key_right: Option<KeyCode>,64/// Rotation speed multiplier (in radians per second).65pub rotation_speed: f32,66/// [`KeyCode`] for counter-clockwise rotation.67pub key_rotate_ccw: Option<KeyCode>,68/// [`KeyCode`] for clockwise rotation.69pub key_rotate_cw: Option<KeyCode>,70}7172/// Provides the default values for the `PanCamera` controller.73///74/// The default settings are:75/// - Zoom factor: 1.076/// - Min zoom: 0.177/// - Max zoom: 5.078/// - Zoom speed: 0.179/// - Zoom in/out key: +/-80/// - Pan speed: 500.081/// - Move up/down: W/S82/// - Move left/right: A/D83/// - Rotation speed: PI (radiradians per second)84/// - Rotation ccw/cw: Q/E85impl Default for PanCamera {86/// Provides the default values for the `PanCamera` controller.87///88/// Users can override these values by manually creating a `PanCamera` instance89/// or modifying the default instance.90fn default() -> Self {91Self {92enabled: true,93zoom_factor: 1.0,94min_zoom: 0.1,95max_zoom: 5.0,96zoom_speed: 0.1,97key_zoom_in: Some(KeyCode::Equal),98key_zoom_out: Some(KeyCode::Minus),99pan_speed: 500.0,100key_up: Some(KeyCode::KeyW),101key_down: Some(KeyCode::KeyS),102key_left: Some(KeyCode::KeyA),103key_right: Some(KeyCode::KeyD),104rotation_speed: PI,105key_rotate_ccw: Some(KeyCode::KeyQ),106key_rotate_cw: Some(KeyCode::KeyE),107}108}109}110111impl PanCamera {112fn key_to_string(key: &Option<KeyCode>) -> String {113key.map_or("None".to_string(), |k| format!("{:?}", k))114}115}116117impl fmt::Display for PanCamera {118fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {119write!(120f,121"122PanCamera Controls:123Move Up / Down - {} / {}124Move Left / Right - {} / {}125Rotate CCW / CW - {} / {}126Zoom - Mouse Scroll + {} / {}127",128Self::key_to_string(&self.key_up),129Self::key_to_string(&self.key_down),130Self::key_to_string(&self.key_left),131Self::key_to_string(&self.key_right),132Self::key_to_string(&self.key_rotate_ccw),133Self::key_to_string(&self.key_rotate_cw),134Self::key_to_string(&self.key_zoom_in),135Self::key_to_string(&self.key_zoom_out),136)137}138}139140/// This system is typically added via the [`PanCameraPlugin`].141///142/// Reads inputs and then moves the camera entity according143/// to the settings given in [`PanCamera`].144///145/// **Note**: The zoom applied in this controller is linear. The zoom factor is directly adjusted146/// based on the input (either from the mouse scroll or keyboard).147fn run_pancamera_controller(148time: Res<Time<Real>>,149key_input: Res<ButtonInput<KeyCode>>,150accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,151mut query: Query<(&mut Transform, &mut PanCamera), With<Camera>>,152) {153let dt = time.delta_secs();154155let Ok((mut transform, mut controller)) = query.single_mut() else {156return;157};158159if !controller.enabled {160return;161}162163// === Movement164let mut movement = Vec2::ZERO;165if let Some(key) = controller.key_left {166if key_input.pressed(key) {167movement.x -= 1.0;168}169}170if let Some(key) = controller.key_right {171if key_input.pressed(key) {172movement.x += 1.0;173}174}175if let Some(key) = controller.key_down {176if key_input.pressed(key) {177movement.y -= 1.0;178}179}180if let Some(key) = controller.key_up {181if key_input.pressed(key) {182movement.y += 1.0;183}184}185186if movement != Vec2::ZERO {187let right = transform.right();188let up = transform.up();189190let delta = (right * movement.x + up * movement.y).normalize() * controller.pan_speed * dt;191192transform.translation.x += delta.x;193transform.translation.y += delta.y;194}195196// === Rotation197if let Some(key) = controller.key_rotate_ccw {198if key_input.pressed(key) {199transform.rotate_z(controller.rotation_speed * dt);200}201}202if let Some(key) = controller.key_rotate_cw {203if key_input.pressed(key) {204transform.rotate_z(-controller.rotation_speed * dt);205}206}207208// === Zoom209let mut zoom_amount = 0.0;210211// (with keys)212if let Some(key) = controller.key_zoom_in {213if key_input.pressed(key) {214zoom_amount -= controller.zoom_speed;215}216}217if let Some(key) = controller.key_zoom_out {218if key_input.pressed(key) {219zoom_amount += controller.zoom_speed;220}221}222223// (with mouse wheel)224let mouse_scroll = match accumulated_mouse_scroll.unit {225MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,226MouseScrollUnit::Pixel => {227accumulated_mouse_scroll.delta.y / MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR228}229};230zoom_amount += mouse_scroll * controller.zoom_speed;231232controller.zoom_factor =233(controller.zoom_factor - zoom_amount).clamp(controller.min_zoom, controller.max_zoom);234235transform.scale = Vec3::splat(controller.zoom_factor);236}237238239