Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,9 @@ bevy_camera_controller = ["bevy_internal/bevy_camera_controller"]
# Enables the free cam from bevy_camera_controller
free_cam = ["bevy_internal/free_cam"]

# Enables the pan cam from bevy_camera_controller
pan_cam = ["bevy_internal/pan_cam"]

# Enable the Bevy Remote Protocol
bevy_remote = ["bevy_internal/bevy_remote"]

Expand Down Expand Up @@ -4826,3 +4829,15 @@ name = "Render Depth to Texture"
description = "Demonstrates how to use depth-only cameras"
category = "Shaders"
wasm = true

[[example]]
name = "pan_cam_controller"
path = "examples/camera/pan_cam_controller.rs"
doc-scrape-examples = true
required-features = ["pan_cam"]

[package.metadata.example.pan_cam_controller]
name = "Pan Cam"
description = "Example Pan-Cam Styled Camera Controller for 2D scenes"
category = "Camera"
wasm = true
1 change: 1 addition & 0 deletions crates/bevy_camera_controller/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ libm = ["bevy_math/libm"]

# Camera controllers
free_cam = []
pan_cam = []

[lints]
workspace = true
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_camera_controller/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@

#[cfg(feature = "free_cam")]
pub mod free_cam;

#[cfg(feature = "pan_cam")]
pub mod pan_cam;
237 changes: 237 additions & 0 deletions crates/bevy_camera_controller/src/pan_cam.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
//! 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::mouse::{AccumulatedMouseScroll, MouseScrollUnit};
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 pancam controls.
#[derive(Component)]
pub struct PanCam {
/// Enables this [`PanCam`] when `true`.
pub enable: bool,
/// Current zoom level (factor applied to camera scale).
pub zoom_factor: f32,
/// Minimum allowed zoom level.
pub min_zoom: f32,
/// Maximum allowed zoom level.
pub max_zoom: f32,
/// This [`PanCam`]'s zoom sensitivity.
pub zoom_speed: f32,
/// [`KeyCode`] to zoom in.
pub key_zoom_in: Option<KeyCode>,
/// [`KeyCode`] to zoom out.
pub key_zoom_out: Option<KeyCode>,
/// This [`PanCam`]'s translation speed.
pub pan_speed: f32,
/// [`KeyCode`] for upward translation.
pub key_up: Option<KeyCode>,
/// [`KeyCode`] for downward translation.
pub key_down: Option<KeyCode>,
/// [`KeyCode`] for leftward translation.
pub key_left: Option<KeyCode>,
/// [`KeyCode`] for rightward translation.
pub key_right: Option<KeyCode>,
/// Rotation speed multiplier (in radians per second).
pub rotation_speed: f32,
/// [`KeyCode`] for counter-clockwise rotation.
pub key_rotate_ccw: Option<KeyCode>,
/// [`KeyCode`] for clockwise rotation.
pub key_rotate_cw: Option<KeyCode>,
}

/// Provides the default values for the `PanCam` controller.
///
/// The default settings are:
/// - Zoom factor: 1.0
/// - Min zoom: 0.1
/// - Max zoom: 5.0
/// - Zoom speed: 0.1
/// - Zoom in/out key: +/-
/// - Pan speed: 500.0
/// - Move up/down: W/S
/// - Move left/right: A/D
/// - Rotation speed: PI (radiradians per second)
/// - Rotation ccw/cw: Q/E
impl Default for PanCam {
/// Provides the default values for the `PanCam` controller.
///
/// Users can override these values by manually creating a `PanCam` instance
/// or modifying the default instance.
fn default() -> Self {
Self {
enable: true,
zoom_factor: 1.0,
min_zoom: 0.1,
max_zoom: 5.0,
zoom_speed: 0.1,
key_zoom_in: Some(KeyCode::Equal),
key_zoom_out: Some(KeyCode::Minus),
pan_speed: 500.0,
key_up: Some(KeyCode::KeyW),
key_down: Some(KeyCode::KeyS),
key_left: Some(KeyCode::KeyA),
key_right: Some(KeyCode::KeyD),
rotation_speed: PI,
key_rotate_ccw: Some(KeyCode::KeyQ),
key_rotate_cw: Some(KeyCode::KeyE),
}
}
}

impl PanCam {
fn key_to_string(key: &Option<KeyCode>) -> String {
key.map_or("None".to_string(), |k| format!("{:?}", k))
}
}

impl fmt::Display for PanCam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"
PanCam Controls:
Move Up / Down - {} / {}
Move Left / Right - {} / {}
Rotate CCW / CW - {} / {}
Zoom - Mouse Scroll + {} / {}
",
Self::key_to_string(&self.key_up),
Self::key_to_string(&self.key_down),
Self::key_to_string(&self.key_left),
Self::key_to_string(&self.key_right),
Self::key_to_string(&self.key_rotate_ccw),
Self::key_to_string(&self.key_rotate_cw),
Self::key_to_string(&self.key_zoom_in),
Self::key_to_string(&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`].
///
/// **Note**: The zoom applied in this controller is linear. The zoom factor is directly adjusted
/// based on the input (either from the mouse scroll or keyboard).
fn run_pancam_controller(
time: Res<Time<Real>>,
key_input: Res<ButtonInput<KeyCode>>,
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
mut query: Query<(&mut Transform, &mut PanCam), With<Camera>>,
) {
let dt = time.delta_secs();

let Ok((mut transform, mut controller)) = query.single_mut() else {
return;
};

if !controller.enable {
return;
}

// === Movement
let mut movement = Vec2::ZERO;
if let Some(key) = controller.key_left {
if key_input.pressed(key) {
movement.x -= 1.0;
}
}
if let Some(key) = controller.key_right {
if key_input.pressed(key) {
movement.x += 1.0;
}
}
if let Some(key) = controller.key_down {
if key_input.pressed(key) {
movement.y -= 1.0;
}
}
if let Some(key) = controller.key_up {
if key_input.pressed(key) {
movement.y += 1.0;
}
}

if movement != Vec2::ZERO {
let right = transform.right();
let up = transform.up();

let delta = (right * movement.x + up * movement.y).normalize() * controller.pan_speed * dt;

transform.translation.x += delta.x;
transform.translation.y += delta.y;
}

// === Rotation
if let Some(key) = controller.key_rotate_ccw {
if key_input.pressed(key) {
transform.rotate_z(controller.rotation_speed * dt);
}
}
if let Some(key) = controller.key_rotate_cw {
if key_input.pressed(key) {
transform.rotate_z(-controller.rotation_speed * dt);
}
}

// === Zoom
let mut zoom_amount = 0.0;

// (with keys)
if let Some(key) = controller.key_zoom_in {
if key_input.pressed(key) {
zoom_amount -= controller.zoom_speed;
}
}
if let Some(key) = controller.key_zoom_out {
if key_input.pressed(key) {
zoom_amount += controller.zoom_speed;
}
}

// (with mouse wheel)
let mouse_scroll = match accumulated_mouse_scroll.unit {
MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
MouseScrollUnit::Pixel => {
accumulated_mouse_scroll.delta.y / MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR
}
};
zoom_amount += mouse_scroll * controller.zoom_speed;

controller.zoom_factor =
(controller.zoom_factor - zoom_amount).clamp(controller.min_zoom, controller.max_zoom);

transform.scale = Vec3::splat(controller.zoom_factor);
}
1 change: 1 addition & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ bevy_dev_tools = ["dep:bevy_dev_tools"]
# Provides a collection of prebuilt camera controllers
bevy_camera_controller = ["dep:bevy_camera_controller"]
free_cam = ["bevy_camera_controller/free_cam"]
pan_cam = ["bevy_camera_controller/pan_cam"]

# Enable support for the Bevy Remote Protocol
bevy_remote = ["dep:bevy_remote", "serialize"]
Expand Down
1 change: 1 addition & 0 deletions docs/cargo_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio
|morph_animation|Enables bevy_mesh and bevy_animation morph weight support|
|mp3|MP3 audio format support|
|multi_threaded|Enables multithreaded parallelism in the engine. Disabling it forces all engine tasks to run on a single thread.|
|pan_cam|Enables the pan cam from bevy_camera_controller|
|pbr_anisotropy_texture|Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs|
|pbr_clustered_decals|Enable support for Clustered Decals|
|pbr_light_textures|Enable support for Light Textures|
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ Example | Description
[Custom Projection](../examples/camera/custom_projection.rs) | Shows how to create custom camera projections.
[First person view model](../examples/camera/first_person_view_model.rs) | A first-person camera that uses a world model and a view model with different field of views (FOV)
[Free cam camera controller](../examples/camera/free_cam_controller.rs) | Shows the default free cam camera controller.
[Pan Cam](../examples/camera/pan_cam_controller.rs) | Example Pan-Cam Styled Camera Controller for 2D scenes
[Projection Zoom](../examples/camera/projection_zoom.rs) | Shows how to zoom orthographic and perspective projection cameras.
[Screen Shake](../examples/camera/2d_screen_shake.rs) | A simple 2D screen shake effect

Expand Down
42 changes: 42 additions & 0 deletions examples/camera/pan_cam_controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Example for `PanCam`, demonstrating basic camera controls such as panning and zooming.
//!
//! This example shows how to use the `PanCam` controller on a 2D camera in Bevy. The camera
//! can be panned with keyboard inputs (arrow keys or WASD) and zoomed in/out using the mouse
//! wheel or the +/- keys. The camera starts with the default `PanCam` settings, which can
//! be customized.
//!
//! Controls:
//! - Arrow keys (or WASD) to pan the camera.
//! - Mouse scroll wheel or +/- to zoom in/out.

use bevy::camera_controller::pan_cam::{PanCam, PanCamPlugin};
use bevy::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(PanCamPlugin) // Adds the PanCam plugin to enable camera panning and zooming controls.
.add_systems(Startup, (setup, spawn_text).chain())
.run();
}

fn spawn_text(mut commands: Commands, camera: Query<&PanCam>) {
commands.spawn((
Node {
position_type: PositionType::Absolute,
top: px(-16),
left: px(12),
..default()
},
children![Text::new(format!("{}", camera.single().unwrap()))],
));
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Spawn a 2D Camera with default PanCam settings
commands.spawn((Camera2d, PanCam::default()));

commands.spawn(Sprite::from_image(
asset_server.load("branding/bevy_bird_dark.png"),
));
}
17 changes: 15 additions & 2 deletions release-content/release-notes/camera_controllers.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: First-party camera controllers
authors: ["@alice-i-cecile"]
pull_requests: [20215, 21450]
authors: ["@alice-i-cecile", "@syszery"]
pull_requests: [20215, 21450, 21520]
---

To understand a scene, you must look at it through the lens of a camera: explore it, and interact with it.
Expand Down Expand Up @@ -34,6 +34,19 @@ To configure the settings (speed, behavior, keybindings) or enable / disable the
We've done our best to select good defaults, but the details of your scene (especially the scale!) will make a big
difference to what feels right.

### `PanCam`

The `PanCam` controller is a simple and effective tool designed for 2D games or any project where you need
to pan the camera and zoom in/out with ease. It allows you to move the camera using the WASD keys and zoom
in and out with the mouse wheel or +/- keys.

By adding the `PanCamPlugin` and attaching the `PanCam` component to your camera entity, you can quickly add
this controller to your project.

To configure the camera's zoom levels, speed, or keybindings, simply modify the `PanCam` component. The default
settings should work well for most use cases, but you can adjust them based on your specific needs, especially
for large-scale or high-resolution 2D scenes.

### Using `bevy_camera_controller` in your own projects

The provided camera controllers are designed to be functional, pleasant debug and dev tools:
Expand Down