diff options
author | Silas Bartha <silas@exvacuum.dev> | 2024-08-23 09:30:30 -0400 |
---|---|---|
committer | Silas Bartha <silas@exvacuum.dev> | 2024-08-23 09:30:30 -0400 |
commit | 85c80561669bdf7b0f491ee4d7e49f2cc8f7b81c (patch) | |
tree | 91fd51487ed4c855ed33cc2d7a2ec325aeb377d4 /src |
Initial Commit
Diffstat (limited to 'src')
-rw-r--r-- | src/components.rs | 46 | ||||
-rw-r--r-- | src/events.rs | 19 | ||||
-rw-r--r-- | src/lib.rs | 110 |
3 files changed, 175 insertions, 0 deletions
diff --git a/src/components.rs b/src/components.rs new file mode 100644 index 0000000..1be2f2c --- /dev/null +++ b/src/components.rs @@ -0,0 +1,46 @@ +//! Components used for interactions + +use bevy::{prelude::*, utils::HashSet}; + +/// Component which enables an entity to request interactions. +/// +/// An entity with an `Interactor` component can be passed to an `InteractorFiredEvent` in order to +/// start an interaction with any nearby `Interactable` entities. +#[derive(Component, Default)] +pub struct Interactor { + /// All `Interactable` targets in-range of this interactor. + pub targets: HashSet<Entity>, + /// The closest `Interactable` target to this interactor, if any. + pub closest: Option<Entity>, +} + +/// Component which enables an entity to recieve interactions. +/// +/// An entity with an `Interactable` component might get passed to an `InteractionEvent` when an +/// `Interactor` requests an interaction, if the interactable is in range. +#[derive(Component)] +pub struct Interactable { + pub(crate) exclusive: bool, + pub(crate) max_distance_squared: f32, +} + +impl Interactable { + /// Construct a new instance of this component. + /// + /// If exclusive, this interactable will only be interacted with if it's the closest one to the + /// interactor, and the interaction will *not* be processed for any other in-range + /// interactables. + pub fn new(max_distance: f32, exclusive: bool) -> Self { + Self { + exclusive, + max_distance_squared: max_distance * max_distance, + } + } +} + +impl Default for Interactable { + fn default() -> Self { + Self::new(1.0, false) + } +} + diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..43ca1ba --- /dev/null +++ b/src/events.rs @@ -0,0 +1,19 @@ +//! Events which enable interactions between `Interactor` and `Interactable` entities. + +use bevy::prelude::*; + +/// Event sent by user to request an interaction from the given `Interactor` entity. +#[derive(Event)] +pub struct InteractorFiredEvent(pub Entity); + +/// Event sent by the plugin once an `InteractorFiredEvent` has been processed. It should be caught +/// by the user to perform some action on the affected interactable entity. +/// +/// It is not intended to be invoked directly. +#[derive(Event)] +pub struct InteractionEvent { + /// `Interactor` entity which triggered this interaction. + pub interactor: Entity, + /// `Interactable` entity whicg is receiving this interaction. + pub interactable: Entity, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2e3fc60 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,110 @@ +#![warn(missing_docs)] + +//! This library provides basic "interaction" functionality +//! Entities which can trigger interactions get the `Interactor` component, and entities which can +//! Be interacted with get the `Interactable` component. An `InteractorFiredEvent` can be invoked for +//! a given `Interactor`, which does the following: +//! 1. Checks to make sure there is at least 1 `Interactable` in range +//! 2. If the nearest `Interactable` is "exclusive", an `InteractionEvent` is invoked for only that +//! entity +//! 3. If the nearest `Interactable` is *not* "exclusive", an individual `InteractionEvent` is invoked for +//! that entity and each "non-exclusive" `Interactable` entity within range + +use std::f32::consts::PI; + +use bevy::prelude::*; +use components::{Interactable, Interactor}; +use events::{InteractionEvent, InteractorFiredEvent}; + +pub mod components; +pub mod events; + +/// Plugin which enables interaction functionality. +/// Sets up event handling for `InteractorFiredEvent` to automatically trigger the correct +/// `InteractionEvent`s +pub struct InteractionPlugin; + +impl Plugin for InteractionPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, (handle_interactor_events, update_interactor_targets)) + .add_event::<InteractorFiredEvent>() + .add_event::<InteractionEvent>(); + } +} + +fn handle_interactor_events( + mut interactor_events: EventReader<InteractorFiredEvent>, + interactable_query: Query<&Interactable>, + interactor_query: Query<&Interactor>, + mut event_writer: EventWriter<InteractionEvent>, +) { + for InteractorFiredEvent(interactor_entity) in interactor_events.read() { + let interactor = interactor_query.get(*interactor_entity).unwrap(); + + if let Some(interactable_entity) = interactor.closest { + let interactable = interactable_query.get(interactable_entity).unwrap(); + if interactable.exclusive { + event_writer.send(InteractionEvent { + interactor: *interactor_entity, + interactable: interactable_entity, + }); + continue; + } else { + for interactable_entity in &interactor.targets { + let interactable = interactable_query.get(*interactable_entity).unwrap(); + if !interactable.exclusive { + event_writer.send(InteractionEvent { + interactor: *interactor_entity, + interactable: *interactable_entity, + }); + } + } + } + } + } +} + +fn update_interactor_targets( + mut interactable_query: Query<(Entity, &Interactable)>, + mut interactor_query: Query<(Entity, &mut Interactor)>, + transform_query: Query<&GlobalTransform>, +) { + for (interactor_entity, mut interactor) in interactor_query.iter_mut() { + let interactor_transform = transform_query.get(interactor_entity).unwrap(); + + let mut closest_active_interactable: Option<(f32, Entity)> = None; + for (interactable_entity, interactable) in interactable_query.iter_mut() { + let interactable_transform = transform_query.get(interactable_entity).unwrap(); + let interactable_distance_squared = interactable_transform + .translation() + .distance_squared(interactor_transform.translation()); + let interactable_arccosine = f32::acos( + interactor_transform.forward().dot( + (interactable_transform.translation() - interactor_transform.translation()) + .normalize(), + ), + ); + if interactable_distance_squared < interactable.max_distance_squared + && interactable_arccosine < PI / 8.0 + { + interactor.targets.insert(interactable_entity); + if let Some((arccosine, _)) = closest_active_interactable { + if interactable_arccosine < arccosine { + closest_active_interactable = + Some((interactable_arccosine, interactable_entity)); + } + } else { + closest_active_interactable = + Some((interactable_arccosine, interactable_entity)); + } + } else { + interactor.targets.remove(&interactable_entity); + } + } + interactor.closest = if let Some((_, interactable_entity)) = closest_active_interactable { + Some(interactable_entity) + } else { + None + } + } +} |