mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
feat(pan-cam): add scaffolding for 2D pan camera controller (#21520)
# Objective Implements scaffolding for a 2D pan camera controller. Fixes #21468 ## Solution - Introduced a `PanCam` component with settings for panning, zooming, and rotation via keyboard input, following the design of the existing `FreeCam`. - Added a `PanCamPlugin` to register the controller system. - Implemented keyboard-based panning and rotation. ## TODOs - Movement is currently world-axis aligned. - TODO: Consider movement relative to camera rotation. - Zooming support is scaffolded with config fields but not yet implemented. ## Testing Unfortunately, I was unable to fully test this implementation due to issues running graphical output with GPU acceleration under WSL. As a result, zoom behavior and rotation effects remain TODOs, and the whole code could not be fully verified. Once I resolve the GPU passthrough issues, I plan to complete and test the remaining features (with a more meaningful example). --- I'm happy to hear any suggestions or feedback in the meantime! --------- Co-authored-by: syszery <syszery@users.noreply.github.com> Co-authored-by: Janis <130913856+janis-bhm@users.noreply.github.com>
This commit is contained in:
+15
@@ -386,6 +386,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"]
|
||||
|
||||
@@ -4831,3 +4834,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
|
||||
|
||||
@@ -30,6 +30,7 @@ libm = ["bevy_math/libm"]
|
||||
|
||||
# Camera controllers
|
||||
free_cam = []
|
||||
pan_cam = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -40,3 +40,6 @@
|
||||
|
||||
#[cfg(feature = "free_cam")]
|
||||
pub mod free_cam;
|
||||
|
||||
#[cfg(feature = "pan_cam")]
|
||||
pub mod pan_cam;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -144,6 +144,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|
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"),
|
||||
));
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user