From 22f298346d54b0935c4800f3a6e22ee978fac337 Mon Sep 17 00:00:00 2001 From: Martin Indra Date: Tue, 5 Apr 2022 21:09:50 +0200 Subject: [PATCH] Improve mouse object picking logic There are to benefits for doing this: * Simplification of systems which need access to pointed to entities. * Intersection is computed only once and used from multiple places. The full benefit described above will be seized once there are new systems working with pointed to entities (and terrain). An example is a system which changes displayed cursor (for example: attack vs move) based on pointed to object / terrain. Relates to #11. --- src/game/mod.rs | 13 +++++-- src/game/pointer.rs | 90 +++++++++++++++++++++++++++++++++++++++++++ src/game/selection.rs | 62 ++++------------------------- 3 files changed, 107 insertions(+), 58 deletions(-) create mode 100644 src/game/pointer.rs diff --git a/src/game/mod.rs b/src/game/mod.rs index e9a274c31..fa934ae00 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,11 +1,11 @@ use self::{ - camera::CameraPlugin, config::GameConfig, maploader::MapLoaderPlugin, + camera::CameraPlugin, config::GameConfig, maploader::MapLoaderPlugin, pointer::PointerPlugin, selection::SelectionPlugin, }; use crate::AppStates; use bevy::{ app::PluginGroupBuilder, - prelude::{App, Plugin, PluginGroup, ResMut, State, SystemSet}, + prelude::{App, Plugin, PluginGroup, ResMut, State, SystemLabel, SystemSet}, }; pub mod config; @@ -15,6 +15,7 @@ mod collisions; mod mapdescr; mod maploader; mod objects; +mod pointer; mod selection; mod terrain; @@ -26,10 +27,16 @@ impl PluginGroup for GamePluginGroup { .add(GamePlugin) .add(MapLoaderPlugin) .add(CameraPlugin) - .add(SelectionPlugin); + .add(SelectionPlugin) + .add(PointerPlugin); } } +#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, SystemLabel)] +enum Labels { + PreInputUpdate, +} + struct GamePlugin; impl Plugin for GamePlugin { diff --git a/src/game/pointer.rs b/src/game/pointer.rs new file mode 100644 index 000000000..985c388f5 --- /dev/null +++ b/src/game/pointer.rs @@ -0,0 +1,90 @@ +use super::{collisions::Intersector, objects::Playable, GameStates, Labels}; +use crate::math::ray::Ray; +use bevy::{ + ecs::system::SystemParam, + input::mouse::MouseMotion, + prelude::{ + App, Camera, Entity, EventReader, GlobalTransform, ParallelSystemDescriptorCoercion, + Plugin, Query, Res, ResMut, SystemSet, With, + }, + window::Windows, +}; +use glam::{Vec2, Vec3}; + +pub struct PointerPlugin; + +impl Plugin for PointerPlugin { + fn build(&self, app: &mut App) { + app.init_resource::().add_system_set( + SystemSet::on_update(GameStates::Playing) + .with_system(mouse_move_handler.label(Labels::PreInputUpdate)), + ); + } +} + +#[derive(Default)] +pub struct Pointer { + entity: Option, +} + +impl Pointer { + pub fn entity(&self) -> Option { + self.entity + } + + fn set_entity(&mut self, entity: Option) { + self.entity = entity; + } +} + +#[derive(SystemParam)] +struct MouseInWorld<'w, 's> { + windows: Res<'w, Windows>, + cameras: Query<'w, 's, (&'static GlobalTransform, &'static Camera)>, +} + +impl<'w, 's> MouseInWorld<'w, 's> { + fn mouse_ray(&self) -> Option { + let window = self.windows.get_primary().unwrap(); + + // Normalized to values between -1.0 to 1.0 with (0.0, 0.0) in the + // middle of the screen. + let cursor_position = match window.cursor_position() { + Some(position) => { + let screen_size = Vec2::new(window.width() as f32, window.height() as f32); + (position / screen_size) * 2.0 - Vec2::ONE + } + None => return None, + }; + + let (camera_transform, camera) = self.cameras.single(); + let camera_transform_mat = camera_transform.compute_matrix(); + let camera_projection = camera.projection_matrix; + + let screen_to_world = camera_transform_mat * camera_projection.inverse(); + let world_to_screen = camera_projection * camera_transform_mat; + + // Depth of camera near plane in screen coordinates. + let near_plane = world_to_screen.transform_point3(-Vec3::Z * camera.near).z; + let ray_origin = screen_to_world.transform_point3(cursor_position.extend(near_plane)); + let ray_direction = ray_origin - camera_transform.translation; + Some(Ray::new(ray_origin, ray_direction)) + } +} + +fn mouse_move_handler( + mut resource: ResMut, + event: EventReader, + mouse: MouseInWorld, + playable: Intersector>, +) { + if event.len() == 0 { + return; + } + + let entity = mouse + .mouse_ray() + .and_then(|ray| playable.ray_intersection(&ray)) + .map(|(entity, _)| entity); + resource.set_entity(entity); +} diff --git a/src/game/selection.rs b/src/game/selection.rs index 0dbfe1d54..8c98a5c51 100644 --- a/src/game/selection.rs +++ b/src/game/selection.rs @@ -1,15 +1,12 @@ -use super::{collisions::Intersector, objects::Playable, GameStates}; -use crate::math::ray::Ray; +use super::{pointer::Pointer, GameStates, Labels}; use bevy::{ ecs::system::SystemParam, input::{mouse::MouseButtonInput, ElementState, Input}, prelude::{ - App, Camera, Commands, Component, Entity, EventReader, GlobalTransform, KeyCode, - MouseButton, Plugin, Query, Res, SystemSet, With, + App, Commands, Component, Entity, EventReader, KeyCode, MouseButton, + ParallelSystemDescriptorCoercion, Plugin, Query, Res, SystemSet, With, }, - window::Windows, }; -use glam::{Vec2, Vec3}; use std::collections::HashSet; pub struct SelectionPlugin; @@ -17,7 +14,8 @@ pub struct SelectionPlugin; impl Plugin for SelectionPlugin { fn build(&self, app: &mut App) { app.add_system_set( - SystemSet::on_update(GameStates::Playing).with_system(mouse_click_handler), + SystemSet::on_update(GameStates::Playing) + .with_system(mouse_click_handler.after(Labels::PreInputUpdate)), ); } } @@ -25,41 +23,6 @@ impl Plugin for SelectionPlugin { #[derive(Component)] pub struct Selected; -#[derive(SystemParam)] -struct MouseInWorld<'w, 's> { - windows: Res<'w, Windows>, - cameras: Query<'w, 's, (&'static GlobalTransform, &'static Camera)>, -} - -impl<'w, 's> MouseInWorld<'w, 's> { - fn mouse_ray(&self) -> Option { - let window = self.windows.get_primary().unwrap(); - - // Normalized to values between -1.0 to 1.0 with (0.0, 0.0) in the - // middle of the screen. - let cursor_position = match window.cursor_position() { - Some(position) => { - let screen_size = Vec2::new(window.width() as f32, window.height() as f32); - (position / screen_size) * 2.0 - Vec2::ONE - } - None => return None, - }; - - let (camera_transform, camera) = self.cameras.single(); - let camera_transform_mat = camera_transform.compute_matrix(); - let camera_projection = camera.projection_matrix; - - let screen_to_world = camera_transform_mat * camera_projection.inverse(); - let world_to_screen = camera_projection * camera_transform_mat; - - // Depth of camera near plane in screen coordinates. - let near_plane = world_to_screen.transform_point3(-Vec3::Z * camera.near).z; - let ray_origin = screen_to_world.transform_point3(cursor_position.extend(near_plane)); - let ray_direction = ray_origin - camera_transform.translation; - Some(Ray::new(ray_origin, ray_direction)) - } -} - #[derive(Clone, Copy, PartialEq)] enum SelectionMode { Replace, @@ -99,8 +62,7 @@ impl<'w, 's> Selector<'w, 's> { fn mouse_click_handler( mut event: EventReader, keys: Res>, - playable: Intersector>, - mouse: MouseInWorld, + pointer: Res, mut selector: Selector, ) { if !event @@ -110,21 +72,11 @@ fn mouse_click_handler( return; } - let mouse_ray = match mouse.mouse_ray() { - Some(ray) => ray, - None => return, - }; - let mode = if keys.pressed(KeyCode::LControl) { SelectionMode::Add } else { SelectionMode::Replace }; - selector.select_single( - playable - .ray_intersection(&mouse_ray) - .map(|(entity, _)| entity), - mode, - ); + selector.select_single(pointer.entity(), mode); }