-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat(pan-cam): add scaffolding for 2D pan camera controller #21520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alice-i-cecile
merged 6 commits into
bevyengine:main
from
syszery:feature/pan-camera-2d
Oct 16, 2025
Merged
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
4b4c66b
feat(pan-cam): add scaffolding for 2D pan camera controller
syszery 9987b87
feat(pan-cam): implement linear zoom and improve example documentation
syszery 00c82fa
feat(pan-cam): implements review feedback and improves doc comments
syszery aa2bde4
feat(pan-cam): implements review feedback
syszery e360432
Update examples/camera/pan_cam_controller.rs
syszery 815e653
refactor(pan-cam): make key bindings optional
syszery File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ libm = ["bevy_math/libm"] | |
|
|
||
| # Camera controllers | ||
| free_cam = [] | ||
| pan_cam = [] | ||
|
|
||
| [lints] | ||
| workspace = true | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,3 +40,6 @@ | |
|
|
||
| #[cfg(feature = "free_cam")] | ||
| pub mod free_cam; | ||
|
|
||
| #[cfg(feature = "pan_cam")] | ||
| pub mod pan_cam; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| //! A camera controller for 2D scenes that supports panning and zooming. | ||
| //! | ||
| //! To use this controller, add [`PanCamPlugin`] to your app, | ||
| //! and insert a [`PanCam`] component into your camera entity. | ||
| //! | ||
| //! To configure the settings of this controller, modify the fields of the [`PanCam`] component. | ||
|
|
||
| use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems}; | ||
| use bevy_camera::Camera; | ||
| use bevy_ecs::prelude::*; | ||
| use bevy_input::keyboard::KeyCode; | ||
| use bevy_input::ButtonInput; | ||
| use bevy_math::{Vec2, Vec3}; | ||
| use bevy_time::{Real, Time}; | ||
| use bevy_transform::prelude::Transform; | ||
|
|
||
| use core::{f32::consts::*, fmt}; | ||
|
|
||
| /// A pancam-style camera controller plugin. | ||
| /// | ||
| /// Use [`PanCam`] to add a pancam controller to a camera entity, | ||
| /// and change its values to customize the controls and change its behavior. | ||
| pub struct PanCamPlugin; | ||
|
|
||
| impl Plugin for PanCamPlugin { | ||
| fn build(&self, app: &mut App) { | ||
| app.add_systems( | ||
| RunFixedMainLoop, | ||
| run_pancam_controller.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /// Pancam controller settings and state. | ||
| /// | ||
| /// Add this component to a [`Camera`] entity and add [`PanCamPlugin`] | ||
| /// to your [`App`] to enable freecam controls. | ||
| #[derive(Component)] | ||
| pub struct PanCam { | ||
| /// Enables this [`PanCam`] when `true`. | ||
| pub enable: bool, | ||
| /// Multiplier for how much each zoom input affects the camera. | ||
| pub zoom_factor: f32, | ||
| /// Minimum allowed zoom level. | ||
| pub min_zoom: f32, | ||
| /// Maximum allowed zoom level. | ||
| pub max_zoom: f32, | ||
| /// [`KeyCode`] to zoom in. | ||
| pub key_zoom_in: KeyCode, | ||
| /// [`KeyCode`] to zoom out. | ||
| pub key_zoom_out: KeyCode, | ||
| /// This [`PanCam`]'s translation speed. | ||
| pub pan_speed: f32, | ||
| /// [`KeyCode`] for upward translation. | ||
| pub key_up: KeyCode, | ||
| /// [`KeyCode`] for backward translation. | ||
| pub key_down: KeyCode, | ||
| /// [`KeyCode`] for leftward translation. | ||
| pub key_left: KeyCode, | ||
| /// [`KeyCode`] for rightward translation. | ||
| pub key_right: KeyCode, | ||
| /// Rotation speed multiplier (in radians per second). | ||
| pub rotation_speed: f32, | ||
| /// [`KeyCode`] for counter-clockwise rotation. | ||
| pub key_rotate_ccw: KeyCode, | ||
| /// [`KeyCode`] for clockwise rotation. | ||
| pub key_rotate_cw: KeyCode, | ||
| } | ||
|
|
||
| impl Default for PanCam { | ||
| fn default() -> Self { | ||
| Self { | ||
| enable: true, | ||
| zoom_factor: 0.1, | ||
| min_zoom: 0.2, | ||
| max_zoom: 5.0, | ||
| key_zoom_in: KeyCode::Equal, | ||
| key_zoom_out: KeyCode::Minus, | ||
| pan_speed: 500.0, | ||
| key_up: KeyCode::KeyW, | ||
| key_down: KeyCode::KeyS, | ||
| key_left: KeyCode::KeyA, | ||
| key_right: KeyCode::KeyD, | ||
| rotation_speed: PI, | ||
| key_rotate_ccw: KeyCode::KeyQ, | ||
| key_rotate_cw: KeyCode::KeyE, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl fmt::Display for PanCam { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| write!( | ||
| f, | ||
| " | ||
| PanCam Controls: | ||
| {:?} & {:?}\t- Move up & down | ||
| {:?} & {:?}\t- Move left & right | ||
| {:?}\t- Rotate counter-clockwise | ||
| {:?}\t- Rotate clockwise | ||
| Mouse Scroll\t- Zoom in & out | ||
| {:?} & {:?}\t- Zoom in & out keys", | ||
| self.key_up, | ||
| self.key_down, | ||
| self.key_left, | ||
| self.key_right, | ||
| self.key_rotate_ccw, | ||
| self.key_rotate_cw, | ||
| self.key_zoom_in, | ||
| self.key_zoom_out, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /// This system is typically added via the [`PanCamPlugin`]. | ||
| /// | ||
| /// Reads inputs and then moves the camera entity according | ||
| /// to the settings given in [`PanCam`]. | ||
| fn run_pancam_controller( | ||
| time: Res<Time<Real>>, | ||
| key_input: Res<ButtonInput<KeyCode>>, | ||
| mut query: Query<(&mut Transform, &PanCam), With<Camera>>, | ||
| ) { | ||
| let dt = time.delta_secs(); | ||
|
|
||
| let Ok((mut transform, controller)) = query.single_mut() else { | ||
| return; | ||
| }; | ||
|
|
||
| if !controller.enable { | ||
| return; | ||
| } | ||
|
|
||
| // === Movement | ||
| let mut movement = Vec2::ZERO; | ||
| if key_input.pressed(controller.key_left) { | ||
| movement.x -= 1.0; | ||
| } | ||
| if key_input.pressed(controller.key_right) { | ||
| movement.x += 1.0; | ||
| } | ||
| if key_input.pressed(controller.key_down) { | ||
| movement.y -= 1.0; | ||
| } | ||
| if key_input.pressed(controller.key_up) { | ||
| movement.y += 1.0; | ||
| } | ||
|
|
||
| // NOTE: Movement is world-axis aligned, not relative to camera rotation | ||
alice-i-cecile marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if movement != Vec2::ZERO { | ||
| let delta = movement.normalize() * controller.pan_speed * dt; | ||
| transform.translation.x += delta.x; | ||
| transform.translation.y += delta.y; | ||
| } | ||
|
|
||
| // === Rotation | ||
| if key_input.pressed(controller.key_rotate_ccw) { | ||
| transform.rotate_z(controller.rotation_speed * dt); | ||
| } | ||
| if key_input.pressed(controller.key_rotate_cw) { | ||
| transform.rotate_z(-controller.rotation_speed * dt); | ||
| } | ||
|
|
||
| // === Zoom | ||
| // TODO: Implement zooming (e.g., adjusting camera scale or projection) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| //! Example for `PanCam`, displays a single [`Sprite`], created from an image. | ||
alice-i-cecile marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| use bevy::prelude::*; | ||
| use bevy::camera_controller::pan_cam::{PanCam, PanCamPlugin}; | ||
|
|
||
| fn main() { | ||
| App::new() | ||
| .add_plugins((DefaultPlugins, PanCamPlugin)) | ||
alice-i-cecile marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .add_systems(Startup, setup) | ||
| .run(); | ||
| } | ||
|
|
||
| fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { | ||
| commands.spawn((Camera2d, PanCam::default())); | ||
|
|
||
| commands.spawn(Sprite::from_image( | ||
| asset_server.load("branding/bevy_bird_dark.png"), | ||
| )); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.