Skip to content

Commit

Permalink
Improve mouse object picking logic
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Indy2222 committed Apr 5, 2022
1 parent 7d703ef commit 22f2983
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 58 deletions.
13 changes: 10 additions & 3 deletions src/game/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,6 +15,7 @@ mod collisions;
mod mapdescr;
mod maploader;
mod objects;
mod pointer;
mod selection;
mod terrain;

Expand All @@ -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 {
Expand Down
90 changes: 90 additions & 0 deletions src/game/pointer.rs
Original file line number Diff line number Diff line change
@@ -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::<Pointer>().add_system_set(
SystemSet::on_update(GameStates::Playing)
.with_system(mouse_move_handler.label(Labels::PreInputUpdate)),
);
}
}

#[derive(Default)]
pub struct Pointer {
entity: Option<Entity>,
}

impl Pointer {
pub fn entity(&self) -> Option<Entity> {
self.entity
}

fn set_entity(&mut self, entity: Option<Entity>) {
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<Ray> {
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<Pointer>,
event: EventReader<MouseMotion>,
mouse: MouseInWorld,
playable: Intersector<With<Playable>>,
) {
if event.len() == 0 {
return;
}

let entity = mouse
.mouse_ray()
.and_then(|ray| playable.ray_intersection(&ray))
.map(|(entity, _)| entity);
resource.set_entity(entity);
}
62 changes: 7 additions & 55 deletions src/game/selection.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,28 @@
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;

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)),
);
}
}

#[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<Ray> {
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,
Expand Down Expand Up @@ -99,8 +62,7 @@ impl<'w, 's> Selector<'w, 's> {
fn mouse_click_handler(
mut event: EventReader<MouseButtonInput>,
keys: Res<Input<KeyCode>>,
playable: Intersector<With<Playable>>,
mouse: MouseInWorld,
pointer: Res<Pointer>,
mut selector: Selector,
) {
if !event
Expand All @@ -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);
}

0 comments on commit 22f2983

Please sign in to comment.