From 2077d88cb75b9fdd19e015b2a74a53a9b4a4a9b3 Mon Sep 17 00:00:00 2001 From: JoJoJet Date: Tue, 25 Jan 2022 13:17:35 -0500 Subject: [PATCH 1/7] track mouse positions for each camera separately --- src/mouse_pos.rs | 89 ++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/src/mouse_pos.rs b/src/mouse_pos.rs index 79d3c20..2194ac8 100644 --- a/src/mouse_pos.rs +++ b/src/mouse_pos.rs @@ -5,39 +5,31 @@ use bevy::prelude::*; use crate::MouseTrackingSystem; /// Plugin that tracks the mouse location. -#[non_exhaustive] -pub enum MousePosPlugin { - /// Track the mouse without transforming it to worldspace. - None, - /// Transform the mouse position into worldspace, using an orthographic camera. - Orthographic, -} +pub struct MousePosPlugin; impl Plugin for MousePosPlugin { fn build(&self, app: &mut App) { - app.insert_resource(MousePos::default()); + // system to add mouse tracking components. + // runs once after startup, and then once at the end of each frame. + app.add_startup_system_to_stage(StartupStage::PostStartup, add_pos_components); + app.add_system_to_stage(CoreStage::Last, add_pos_components); + app.add_system_to_stage( CoreStage::First, update_pos.label(MouseTrackingSystem::ScreenPos), ); - // - // Optionally add features for converting to worldspace. - match *self { - MousePosPlugin::None => {} - MousePosPlugin::Orthographic => { - app.insert_resource(MousePosWorld::default()); - app.add_system_to_stage( - CoreStage::First, - update_pos_ortho.label(MouseTrackingSystem::WorldPos), - ); - } - } + app.add_system_to_stage( + CoreStage::First, + update_pos_ortho + .label(MouseTrackingSystem::WorldPos) + .after(MouseTrackingSystem::ScreenPos), + ); } } /// The location of the mouse in screenspace. -#[derive(Clone, Copy, PartialEq, Default, Debug)] -pub struct MousePos(pub Vec2); +#[derive(Debug, Clone, Copy, PartialEq, Component)] +pub struct MousePos(Vec2); impl Deref for MousePos { type Target = Vec2; @@ -52,15 +44,42 @@ impl Display for MousePos { } } -fn update_pos(mut mouse_loc: ResMut, mut cursor_moved: EventReader) { - for event in cursor_moved.iter() { - mouse_loc.0 = event.position; +fn add_pos_components( + cameras1: Query<(Entity, &Camera), Without>, + cameras2: Query, Without)>, + windows: Res, + mut commands: Commands, +) { + for (e, camera) in cameras1.iter() { + // get the initial position of the cursor. + let position = windows + .get(camera.window) + .and_then(|w| w.cursor_position()) + .unwrap_or_default(); + commands.entity(e).insert(MousePos(position)); + } + for cam in cameras2.iter() { + commands + .entity(cam) + .insert(MousePosWorld(Default::default())); + } +} + +fn update_pos( + mut movement: EventReader, + mut cameras: Query<(&Camera, &mut MousePos)>, +) { + for &CursorMoved { id, position } in movement.iter() { + // find all cameras corresponding to the window on which the cursor moved. + for (_, mut pos) in cameras.iter_mut().filter(|(c, ..)| c.window == id) { + pos.0 = position; + } } } /// The location of the mouse in worldspace. -#[derive(Clone, Copy, PartialEq, Default, Debug)] -pub struct MousePosWorld(pub Vec3); +#[derive(Debug, Clone, Copy, PartialEq, Component)] +pub struct MousePosWorld(Vec3); impl Display for MousePosWorld { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -76,16 +95,14 @@ impl Deref for MousePosWorld { } fn update_pos_ortho( - mut mouse_world: ResMut, - mut cursor_moved: EventReader, - cameras: Query<(&GlobalTransform, &OrthographicProjection), With>, + mut tracking: Query<(Entity, &mut MousePosWorld, &MousePos), Changed>, + cameras: Query<(&GlobalTransform, &OrthographicProjection)>, ) { - if let Some(event) = cursor_moved.iter().next_back() { + for (camera, mut world, screen) in tracking.iter_mut() { let (camera, proj) = cameras - .iter() - .next() - .expect("could not find an orthographic camera"); - mouse_world.0 = camera - .mul_vec3(event.position.extend(0.0) + Vec3::new(proj.left, proj.bottom, proj.near)); + .get(camera) + .expect("only orthographic cameras are supported"); + world.0 = + camera.mul_vec3(screen.0.extend(0.0) + Vec3::new(proj.left, proj.bottom, proj.near)); } } From 71c755e6bc42aeb06cf846c7d61e04bcb145d9bc Mon Sep 17 00:00:00 2001 From: JoJoJet Date: Tue, 25 Jan 2022 18:06:10 -0500 Subject: [PATCH 2/7] add global resources for tracking the main camera --- src/lib.rs | 2 +- src/mouse_pos.rs | 79 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 98b7ad6..0e34686 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,7 +103,7 @@ pub enum MouseTrackingSystem { } mod mouse_pos; -pub use mouse_pos::{MousePos, MousePosPlugin, MousePosWorld}; +pub use mouse_pos::{MainCamera, MousePos, MousePosPlugin, MousePosWorld}; mod mouse_motion; pub use bevy::input::mouse::MouseMotion; diff --git a/src/mouse_pos.rs b/src/mouse_pos.rs index 2194ac8..3ac3c2a 100644 --- a/src/mouse_pos.rs +++ b/src/mouse_pos.rs @@ -5,25 +5,52 @@ use bevy::prelude::*; use crate::MouseTrackingSystem; /// Plugin that tracks the mouse location. -pub struct MousePosPlugin; +pub enum MousePosPlugin { + /// Configuration for apps that have a single main camera. + /// Provides global Resources for [`MousePos`] and [`MousePosWorld`]. + SingleCamera, + /// Configuration for apps that have multiple cameras which must be handled separately. + MultiCamera, +} impl Plugin for MousePosPlugin { fn build(&self, app: &mut App) { - // system to add mouse tracking components. - // runs once after startup, and then once at the end of each frame. - app.add_startup_system_to_stage(StartupStage::PostStartup, add_pos_components); - app.add_system_to_stage(CoreStage::Last, add_pos_components); + // System to add mouse tracking components. + // Runs once at the end of each frame. This means that no cameras will have + // mouse tracking components until after the first frame. + // This might cause some issues, but it's probably for the best since, + // during the first frame, nothing has been rendered yet. + app.add_system_to_stage(CoreStage::PostUpdate, add_pos_components); - app.add_system_to_stage( - CoreStage::First, - update_pos.label(MouseTrackingSystem::ScreenPos), + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, StageLabel)] + struct MouseStage; + + app.add_stage_before( + CoreStage::PreUpdate, + MouseStage, + SystemStage::single_threaded(), ); + + app.add_system_to_stage(MouseStage, update_pos.label(MouseTrackingSystem::ScreenPos)); app.add_system_to_stage( - CoreStage::First, + MouseStage, update_pos_ortho .label(MouseTrackingSystem::WorldPos) .after(MouseTrackingSystem::ScreenPos), ); + + match self { + Self::SingleCamera => { + app.insert_resource(MousePos(Default::default())); + app.insert_resource(MousePosWorld(Default::default())); + // + app.add_system_to_stage( + MouseStage, + update_main_camera.after(MouseTrackingSystem::WorldPos), + ); + } + Self::MultiCamera => {} + } } } @@ -106,3 +133,37 @@ fn update_pos_ortho( camera.mul_vec3(screen.0.extend(0.0) + Vec3::new(proj.left, proj.bottom, proj.near)); } } + +/// Marker component for the primary camera in the world. +/// If only one camera exists, this is optional. +#[derive(Debug, Clone, Copy, Component)] +pub struct MainCamera; + +fn update_main_camera( + mut screen_res: ResMut, + mut world_res: ResMut, + cameras: Query<(&MousePos, &MousePosWorld, Option<&MainCamera>)>, +) { + use bevy::ecs::system::QuerySingleError; + let (screen, world, ..) = match cameras.get_single() { + Ok(x) => x, + Err(QuerySingleError::NoEntities(_)) => { + // this is okay, try again next frame. + return; + } + Err(QuerySingleError::MultipleEntities(_)) => { + // try to disambiguate + let mut mains = cameras.iter().filter(|(.., main)| main.is_some()); + + let main = mains.next().unwrap_or_else(||panic!("cannot identify main camera -- consider adding the MainCamera component to one of the cameras") ); + if mains.next().is_some() { + panic!("only one camera may be marked with the MainCamera component"); + } + main + + // ambiguous! very bad + } + }; + screen_res.0 = screen.0; + world_res.0 = world.0; +} From 38320998faf82b2d4f573b3cffccc14087d51653 Mon Sep 17 00:00:00 2001 From: JoJoJet Date: Tue, 25 Jan 2022 20:39:44 -0500 Subject: [PATCH 3/7] remove public SystemLabels --- src/lib.rs | 17 +---------------- src/mouse_motion.rs | 9 +++------ src/mouse_pos.rs | 16 ++++++++++------ 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0e34686..8abad03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,23 +88,8 @@ //! mouse_tracking = { package = "bevy_mouse_tracking_plugin", version = "..." } //! ``` -use bevy::prelude::*; - -/// Labels for the various mouse tracking systems, to be used for explicit ordering within stages. -/// The use of this is usually not necessary, as the mouse tracking systems reside in [`bevy::app::CoreStage::First`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemLabel)] -pub enum MouseTrackingSystem { - /// The system that updates the screen position each frame. - ScreenPos, - /// The system that updates the world position each frame. - WorldPos, - /// The system that updates the mouse motion each frame. - Motion, -} - mod mouse_pos; pub use mouse_pos::{MainCamera, MousePos, MousePosPlugin, MousePosWorld}; mod mouse_motion; -pub use bevy::input::mouse::MouseMotion; -pub use mouse_motion::MouseMotionPlugin; +pub use mouse_motion::{MouseMotion, MouseMotionPlugin}; diff --git a/src/mouse_motion.rs b/src/mouse_motion.rs index 52627be..d3688c2 100644 --- a/src/mouse_motion.rs +++ b/src/mouse_motion.rs @@ -1,17 +1,14 @@ use bevy::prelude::*; -use crate::{MouseMotion, MouseTrackingSystem}; - /// Plugin that tracks mouse motion. pub struct MouseMotionPlugin; +pub use bevy::input::mouse::MouseMotion; + impl bevy::app::Plugin for MouseMotionPlugin { fn build(&self, app: &mut bevy::app::App) { app.insert_resource(MouseMotion { delta: Vec2::ZERO }); - app.add_system_to_stage( - CoreStage::First, - update_mouse_motion.label(MouseTrackingSystem::Motion), - ); + app.add_system_to_stage(CoreStage::First, update_mouse_motion); } } diff --git a/src/mouse_pos.rs b/src/mouse_pos.rs index 3ac3c2a..f995c4a 100644 --- a/src/mouse_pos.rs +++ b/src/mouse_pos.rs @@ -2,8 +2,6 @@ use std::{fmt::Display, ops::Deref}; use bevy::prelude::*; -use crate::MouseTrackingSystem; - /// Plugin that tracks the mouse location. pub enum MousePosPlugin { /// Configuration for apps that have a single main camera. @@ -15,6 +13,12 @@ pub enum MousePosPlugin { impl Plugin for MousePosPlugin { fn build(&self, app: &mut App) { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemLabel)] + enum MouseSystem { + ScreenPos, + WorldPos, + } + // System to add mouse tracking components. // Runs once at the end of each frame. This means that no cameras will have // mouse tracking components until after the first frame. @@ -31,12 +35,12 @@ impl Plugin for MousePosPlugin { SystemStage::single_threaded(), ); - app.add_system_to_stage(MouseStage, update_pos.label(MouseTrackingSystem::ScreenPos)); + app.add_system_to_stage(MouseStage, update_pos.label(MouseSystem::ScreenPos)); app.add_system_to_stage( MouseStage, update_pos_ortho - .label(MouseTrackingSystem::WorldPos) - .after(MouseTrackingSystem::ScreenPos), + .label(MouseSystem::WorldPos) + .after(MouseSystem::ScreenPos), ); match self { @@ -46,7 +50,7 @@ impl Plugin for MousePosPlugin { // app.add_system_to_stage( MouseStage, - update_main_camera.after(MouseTrackingSystem::WorldPos), + update_main_camera.after(MouseSystem::WorldPos), ); } Self::MultiCamera => {} From 33ef7c0f3fbe1a342d7d86f822e1b016a9be5118 Mon Sep 17 00:00:00 2001 From: JoJoJet Date: Tue, 25 Jan 2022 22:59:14 -0500 Subject: [PATCH 4/7] update documentation and examples --- README.md | 133 +++++++++++++++++++++++++++++------ examples/screen.rs | 2 +- examples/world.rs | 2 +- src/lib.rs | 169 ++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 264 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d461378..a8bea3b 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,27 @@ get called when the mouse actually *moves*. [`EventReader`]: bevy::app::EventReader This crate aims to make this as easy as possible, by providing a -static resource that tracks the mouse position every frame. +static [resource](bevy::ecs::system::Res) that tracks the mouse position every frame. + +This crate also supports more complex use cases such as multiple cameras, which are discussed further down. + +## Basics + First, add the plugin to your app: ```rust use bevy::prelude::*; use bevy_mouse_tracking_plugin::MousePosPlugin; fn main() { - App::build() + App::new() .add_plugins(DefaultPlugins) - .add_plugin(MousePosPlugin::None); + .add_plugin(MousePosPlugin::SingleCamera); } ``` Now, you can access the resource in your [`System`]s: -[`System`]: bevy::ecs::System +[`System`]: bevy::ecs::system::System ```rust use bevy_mouse_tracking_plugin::MousePos; @@ -33,30 +38,19 @@ fn dbg_mouse(mouse: Res) { ``` ...and don't forget to add the system to your app: ```rust - .add_plugin(MousePosPlugin::None) + .add_plugin(MousePosPlugin::SingleCamera) .add_system(dbg_mouse.system()); ``` This will print the screen-space location of the mouse on every frame. However, we can do better than just screen-space: we support automatic -transformation to world-space coordinates. -Change the plugin to this: - -```rust -fn main() { - App::build() - .add_plugins(DefaultPlugins) - .add_plugin(MousePosPlugin::Orthographic); -} -``` +transformation to world-space coordinates via the [`MousePosWorld`] resource. -In a system... ```rust use bevy_mouse_tracking_plugin::MousePosWorld; fn dbg_world(mouse: Res) { eprintln!("{}", *mouse); - // Note: the screen-space position is still accessible } ``` @@ -64,13 +58,108 @@ This will print the world-space location of the mouse on every frame. Note that this is only supported for two-dimensional, orthographic camera, but pull requests for 3D support are welcome! -Additionally, we also support a resource that tracks mouse motion, via [`MouseMotionPlugin`]. -The motion can be accessed from any system in a [`MouseMotion`] [`Res`]. +## Multiple cameras + +You may notice that if you try to use this plugin in an app that has multiple cameras, it crashes! + +```rust +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(MousePosPlugin::SingleCamera) + .add_startup_system(setup) + .run(); +} +fn setup(mut commands: Commands) { + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + commands.spawn_bundle(UiCameraBundle::default()); +} +``` + +This panics with the following output: + +``` +thread 'main' panicked at 'cannot identify main camera -- consider adding the MainCamera component to one of the cameras', src\mouse_pos.rs:163:13 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +This is because the plugin doesn't know which of the two cameras to use when figuring out +the values of the `MousePos` and `MousePosWorld` resources. Let's take the panic message's advice. + +```rust + commands.spawn_bundle(OrthographicCameraBundle::new_2d()) + .insert(MainCamera); // added this line + commands.spawn_bundle(UiCameraBundle::default()); +``` + +### Queries -[`Res`]: bevy::ecs::Res +If you want to get mouse tracking information relative to each camera individually, +simply [query](bevy::ecs::system::Query) for a `MousePos` or `MousePosWorld` as a +_component_ instead of as a resource. + +```rust +fn main() { + App::new() + // plugins omitted... + .add_system(dbg_for_each); +} +fn dbg_for_each(mouse_pos: Query<&MousePosWorld>) { + for pos in mouse_pos.iter() { + // This prints the mouse position twice per frame: + // once relative to the UI camera, and once relative to the physical camera. + eprintln!("{}", *pos); + } +} +``` + +If you want the mouse position for a specific camera, you can add query filters as always. +Note that as of `bevy 0.6`, the only way to tell the difference between a UI camera and +an orthographic camera is by checking for the [`Frustum`] component. + +[`Frustum`]: bevy::render::primitives::Frustum + +```rust +use bevy::render::primitives::Frustum; +fn dbg_ui_pos(mouse_pos: Query<&MousePosWorld, Without>) { + // query for the UI camera, which doesn't have a Frustum component. + let pos = mouse_pos.single(); + eprintln!("{}", *pos); +} +``` + +### No main camera + +Let's say you have multiple cameras in your app, and you want to treat them all equally, +without declaring any one of them as the main camera. +Change the plugin to this: + +```rust +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(MousePosPlugin::MultiCamera) // SingleCamera -> MultiCamera + .add_startup_system(setup) + // ... +} +``` + +Now, you can add as many cameras as you want, without having to worry about marking any +of them as the main camera. +Note that `MousePos` and `MousePosWorld` will no longer be accessible as global resources +-- you can only access them by `Query`ing camera entities. + +## Mouse motion + +This crate supports a resource that tracks mouse motion, via [`MouseMotionPlugin`]. +The motion can be accessed from any system in a [`MouseMotion`] resource. + +[`Res`]: bevy::ecs::system::Res + +## Crate name As a final aside: the name of this crate is intentionally verbose. -This is because I don't want to steal a crate name, especially since +This is because I didn't want to steal a crate name, especially since it is very likely that this crate will eventually be made redundant by future updates to `bevy`. I recommend renaming the crate in your `Cargo.toml`: @@ -78,3 +167,5 @@ I recommend renaming the crate in your `Cargo.toml`: [dependencies] mouse_tracking = { package = "bevy_mouse_tracking_plugin", version = "..." } ``` + +License: MIT diff --git a/examples/screen.rs b/examples/screen.rs index adfa086..7b9dec0 100644 --- a/examples/screen.rs +++ b/examples/screen.rs @@ -13,7 +13,7 @@ fn main() { .add_plugins(DefaultPlugins) .insert_resource(ClearColor(Color::BLACK)) .insert_resource(WindowDescriptor::default()) - .add_plugin(MousePosPlugin::None) + .add_plugin(MousePosPlugin::SingleCamera) .add_startup_system(setup) .add_system(bevy::input::system::exit_on_esc_system.system()) .add_system(run) diff --git a/examples/world.rs b/examples/world.rs index e4600fc..82c9b0a 100644 --- a/examples/world.rs +++ b/examples/world.rs @@ -13,7 +13,7 @@ fn main() { .add_plugins(DefaultPlugins) .insert_resource(ClearColor(Color::BLACK)) .insert_resource(WindowDescriptor::default()) - .add_plugin(MousePosPlugin::Orthographic) + .add_plugin(MousePosPlugin::SingleCamera) .add_startup_system(setup) .add_system(bevy::input::system::exit_on_esc_system.system()) .add_system(run) diff --git a/src/lib.rs b/src/lib.rs index 8abad03..6a4d57c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,12 @@ //! [`EventReader`]: bevy::app::EventReader //! //! This crate aims to make this as easy as possible, by providing a -//! static resource that tracks the mouse position every frame. +//! static [resource](bevy::ecs::system::Res) that tracks the mouse position every frame. +//! +//! This crate also supports more complex use cases such as multiple cameras, which are discussed further down. +//! +//! # Basics +//! //! First, add the plugin to your app: //! //! ``` @@ -15,13 +20,13 @@ //! fn main() { //! App::new() //! .add_plugins(DefaultPlugins) -//! .add_plugin(MousePosPlugin::None); +//! .add_plugin(MousePosPlugin::SingleCamera); //! } //! ``` //! //! Now, you can access the resource in your [`System`]s: //! -//! [`System`]: bevy::ecs::System +//! [`System`]: bevy::ecs::system::System //! //! ``` //! # use bevy::prelude::*; @@ -37,7 +42,7 @@ //! # fn main() { //! # App::new() //! # .add_plugins(DefaultPlugins) -//! .add_plugin(MousePosPlugin::None) +//! .add_plugin(MousePosPlugin::SingleCamera) //! .add_system(dbg_mouse.system()); //! # } //! # fn dbg_mouse() { } @@ -46,40 +51,166 @@ //! This will print the screen-space location of the mouse on every frame. //! //! However, we can do better than just screen-space: we support automatic -//! transformation to world-space coordinates. -//! Change the plugin to this: +//! transformation to world-space coordinates via the [`MousePosWorld`] resource. //! //! ``` //! # use bevy::prelude::*; +//! use bevy_mouse_tracking_plugin::MousePosWorld; +//! fn dbg_world(mouse: Res) { +//! eprintln!("{}", *mouse); +//! } +//! ``` +//! +//! This will print the world-space location of the mouse on every frame. +//! Note that this is only supported for two-dimensional, orthographic camera, +//! but pull requests for 3D support are welcome! +//! +//! # Multiple cameras +//! +//! You may notice that if you try to use this plugin in an app that has multiple cameras, it crashes! +//! +//! ```should_panic +//! # use bevy::prelude::*; //! # use bevy_mouse_tracking_plugin::MousePosPlugin; //! fn main() { //! App::new() //! .add_plugins(DefaultPlugins) -//! .add_plugin(MousePosPlugin::Orthographic); +//! .add_plugin(MousePosPlugin::SingleCamera) +//! .add_startup_system(setup) +//! .run(); //! } +//! fn setup(mut commands: Commands) { +//! commands.spawn_bundle(OrthographicCameraBundle::new_2d()); +//! commands.spawn_bundle(UiCameraBundle::default()); +//! } +//! ``` +//! +//! This panics with the following output: +//! +//! ```text +//! thread 'main' panicked at 'cannot identify main camera -- consider adding the MainCamera component to one of the cameras', src\mouse_pos.rs:163:13 +//! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace //! ``` //! -//! In a system... +//! This is because the plugin doesn't know which of the two cameras to use when figuring out +//! the values of the `MousePos` and `MousePosWorld` resources. Let's take the panic message's advice. +//! //! ``` //! # use bevy::prelude::*; -//! use bevy_mouse_tracking_plugin::MousePosWorld; -//! fn dbg_world(mouse: Res) { -//! eprintln!("{}", *mouse); -//! // Note: the screen-space position is still accessible +//! # use bevy_mouse_tracking_plugin::{MousePosPlugin, MainCamera}; +//! # fn main() { +//! # App::new() +//! # .add_plugins(DefaultPlugins) +//! # .add_plugin(MousePosPlugin::SingleCamera) +//! # .add_startup_system(setup) +//! # .add_system(exit_if_successful) +//! # .run(); +//! # } +//! # fn setup(mut commands: Commands) { +//! commands.spawn_bundle(OrthographicCameraBundle::new_2d()) +//! .insert(MainCamera); // added this line +//! commands.spawn_bundle(UiCameraBundle::default()); +//! # } +//! # fn exit_if_successful(time: Res