aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components.rs46
-rw-r--r--src/events.rs19
-rw-r--r--src/lib.rs110
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
+ }
+ }
+}