Skip to main content

godot_bevy/plugins/
collisions.rs

1//! Collision detection for godot-bevy.
2//!
3//! This module bridges Godot's collision detection with Bevy's ECS patterns,
4//! providing multiple ways to detect and respond to collisions.
5//!
6//! # Accessing Collisions
7//!
8//! godot-bevy provides a [`Collisions`] system parameter for querying collision state.
9//! This is the primary way to check what entities are currently colliding.
10//!
11//! ```ignore
12//! fn my_system(collisions: Collisions) {
13//!     // Iterate all currently touching pairs
14//!     for (entity_a, entity_b) in collisions.iter() {
15//!         // Handle collision
16//!     }
17//!
18//!     // Check if two specific entities are colliding
19//!     if collisions.contains(player, enemy) {
20//!         // Player is touching enemy
21//!     }
22//!
23//!     // Get all entities colliding with a specific entity
24//!     for other in collisions.colliding_with(player) {
25//!         // other is colliding with player
26//!     }
27//! }
28//! ```
29//!
30//! # Collision Events
31//!
32//! For reacting to collision start/end events, use [`CollisionStarted`] and
33//! [`CollisionEnded`]. These can be read as Messages or observed as Events.
34//!
35//! ## Reading as Messages
36//!
37//! ```ignore
38//! fn handle_hits(mut started: MessageReader<CollisionStarted>) {
39//!     for event in started.read() {
40//!         println!("{:?} started colliding with {:?}", event.0, event.1);
41//!     }
42//! }
43//! ```
44//!
45//! ## Using Observers
46//!
47//! ```ignore
48//! app.add_observer(|trigger: Trigger<CollisionStarted>| {
49//!     let (a, b) = (trigger.event().0, trigger.event().1);
50//!     println!("{a:?} started colliding with {b:?}");
51//! });
52//! ```
53
54use crate::interop::GodotNodeHandle;
55use crate::plugins::core::PrePhysicsUpdate;
56use crate::plugins::scene_tree::NodeEntityIndex;
57use bevy_app::{App, Plugin};
58use bevy_ecs::{
59    entity::Entity,
60    event::Event,
61    message::{Message, MessageReader, MessageWriter, message_update_system},
62    prelude::Resource,
63    schedule::IntoScheduleConfigs,
64    system::{Commands, Res, ResMut, SystemParam},
65};
66use crossbeam_channel::Receiver;
67use godot::prelude::*;
68use parking_lot::Mutex;
69use std::collections::{HashMap, HashSet};
70use tracing::trace;
71
72// Collision signal constants
73pub const BODY_ENTERED: &str = "body_entered";
74pub const BODY_EXITED: &str = "body_exited";
75pub const AREA_ENTERED: &str = "area_entered";
76pub const AREA_EXITED: &str = "area_exited";
77
78/// All collision signals that indicate collision start
79pub const COLLISION_START_SIGNALS: &[&str] = &[BODY_ENTERED, AREA_ENTERED];
80
81/// Event-count cutoff for batch neighbor rebuild mode.
82/// Batches at or above this size rebuild adjacency from `active_pairs`.
83const COLLISION_NEIGHBOR_REBUILD_THRESHOLD: usize = 512;
84
85// ============================================================================
86// EVENTS
87// ============================================================================
88
89/// Event fired when two entities start colliding.
90///
91/// Can be read as a [`Message`] with [`MessageReader`] or observed with
92/// Bevy's observer system.
93///
94/// # Example
95///
96/// ```ignore
97/// // As a message
98/// fn handle_collision_start(mut events: MessageReader<CollisionStarted>) {
99///     for event in events.read() {
100///         println!("{:?} hit {:?}", event.entity1, event.entity2);
101///     }
102/// }
103///
104/// // As an observer
105/// app.add_observer(|trigger: Trigger<CollisionStarted>| {
106///     let event = trigger.event();
107///     println!("{:?} hit {:?}", event.entity1, event.entity2);
108/// });
109/// ```
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Message, Event)]
111pub struct CollisionStarted {
112    /// The first entity in the collision.
113    pub entity1: Entity,
114    /// The second entity in the collision.
115    pub entity2: Entity,
116}
117
118/// Event fired when two entities stop colliding.
119///
120/// Can be read as a [`Message`] with [`MessageReader`] or observed with
121/// Bevy's observer system.
122///
123/// # Example
124///
125/// ```ignore
126/// // As a message
127/// fn handle_collision_end(mut events: MessageReader<CollisionEnded>) {
128///     for event in events.read() {
129///         println!("{:?} separated from {:?}", event.entity1, event.entity2);
130///     }
131/// }
132///
133/// // As an observer
134/// app.add_observer(|trigger: Trigger<CollisionEnded>| {
135///     let event = trigger.event();
136///     println!("{:?} separated from {:?}", event.entity1, event.entity2);
137/// });
138/// ```
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Message, Event)]
140pub struct CollisionEnded {
141    /// The first entity in the collision.
142    pub entity1: Entity,
143    /// The second entity in the collision.
144    pub entity2: Entity,
145}
146
147// ============================================================================
148// COLLISION STATE RESOURCE
149// ============================================================================
150
151/// Resource that tracks all current collision pairs.
152///
153/// This is automatically updated each frame based on Godot's collision events.
154/// Use the [`Collisions`] system parameter for convenient access.
155#[derive(Resource, Default, Debug)]
156pub struct CollisionState {
157    /// Currently active collision pairs (origin_entity, target_entity)
158    /// We store both directions for O(1) lookup
159    active_pairs: HashSet<(Entity, Entity)>,
160
161    /// Collisions that started this frame
162    started_this_frame: Vec<(Entity, Entity)>,
163
164    /// Collisions that ended this frame
165    ended_this_frame: Vec<(Entity, Entity)>,
166
167    /// Map from entity to all entities it's currently colliding with
168    entity_collisions: HashMap<Entity, Vec<Entity>>,
169}
170
171impl CollisionState {
172    /// Clear per-frame data (called at start of update)
173    fn begin_frame(&mut self) {
174        self.started_this_frame.clear();
175        self.ended_this_frame.clear();
176    }
177
178    /// Record a collision start
179    fn add_collision(&mut self, origin: Entity, target: Entity) -> bool {
180        self.add_collision_internal(origin, target, true)
181    }
182
183    fn add_collision_without_neighbors(&mut self, origin: Entity, target: Entity) -> bool {
184        self.add_collision_internal(origin, target, false)
185    }
186
187    fn add_collision_internal(
188        &mut self,
189        origin: Entity,
190        target: Entity,
191        update_neighbors: bool,
192    ) -> bool {
193        // Normalize pair order for consistent storage
194        let pair = normalize_pair(origin, target);
195
196        if self.active_pairs.insert(pair) {
197            self.started_this_frame.push(pair);
198
199            if update_neighbors {
200                self.entity_collisions
201                    .entry(origin)
202                    .or_default()
203                    .push(target);
204                self.entity_collisions
205                    .entry(target)
206                    .or_default()
207                    .push(origin);
208            }
209            true
210        } else {
211            false
212        }
213    }
214
215    /// Record a collision end
216    fn remove_collision(&mut self, origin: Entity, target: Entity) -> bool {
217        self.remove_collision_internal(origin, target, true)
218    }
219
220    fn remove_collision_without_neighbors(&mut self, origin: Entity, target: Entity) -> bool {
221        self.remove_collision_internal(origin, target, false)
222    }
223
224    fn remove_collision_internal(
225        &mut self,
226        origin: Entity,
227        target: Entity,
228        update_neighbors: bool,
229    ) -> bool {
230        let pair = normalize_pair(origin, target);
231
232        if self.active_pairs.remove(&pair) {
233            self.ended_this_frame.push(pair);
234
235            if update_neighbors {
236                if let Some(collisions) = self.entity_collisions.get_mut(&origin) {
237                    collisions.retain(|&e| e != target);
238                }
239                if let Some(collisions) = self.entity_collisions.get_mut(&target) {
240                    collisions.retain(|&e| e != origin);
241                }
242            }
243            true
244        } else {
245            false
246        }
247    }
248
249    fn rebuild_entity_collisions(&mut self) {
250        self.entity_collisions.clear();
251        self.entity_collisions.reserve(self.active_pairs.len() * 2);
252        for &(entity1, entity2) in &self.active_pairs {
253            self.entity_collisions
254                .entry(entity1)
255                .or_default()
256                .push(entity2);
257            self.entity_collisions
258                .entry(entity2)
259                .or_default()
260                .push(entity1);
261        }
262    }
263
264    /// Check if two entities are currently colliding
265    pub fn contains(&self, a: Entity, b: Entity) -> bool {
266        self.active_pairs.contains(&normalize_pair(a, b))
267    }
268
269    /// Get all entities currently colliding with the given entity
270    pub fn colliding_with(&self, entity: Entity) -> &[Entity] {
271        self.entity_collisions
272            .get(&entity)
273            .map(|v| v.as_slice())
274            .unwrap_or(&[])
275    }
276
277    /// Iterate over all currently active collision pairs
278    pub fn iter(&self) -> impl Iterator<Item = (Entity, Entity)> + '_ {
279        self.active_pairs.iter().copied()
280    }
281
282    /// Iterate over collision pairs that started this frame
283    pub fn started(&self) -> impl Iterator<Item = (Entity, Entity)> + '_ {
284        self.started_this_frame.iter().copied()
285    }
286
287    /// Iterate over collision pairs that ended this frame
288    pub fn ended(&self) -> impl Iterator<Item = (Entity, Entity)> + '_ {
289        self.ended_this_frame.iter().copied()
290    }
291
292    /// Returns true if there are no active collisions
293    pub fn is_empty(&self) -> bool {
294        self.active_pairs.is_empty()
295    }
296
297    /// Returns the number of active collision pairs
298    pub fn len(&self) -> usize {
299        self.active_pairs.len()
300    }
301}
302
303/// Normalize a pair of entities to a consistent order for storage
304#[inline]
305fn normalize_pair(a: Entity, b: Entity) -> (Entity, Entity) {
306    if a < b { (a, b) } else { (b, a) }
307}
308
309// ============================================================================
310// COLLISIONS SYSTEM PARAM
311// ============================================================================
312
313/// System parameter for querying collision state.
314///
315/// This provides a convenient API for checking collisions in systems.
316///
317/// # Example
318///
319/// ```ignore
320/// fn my_system(collisions: Collisions) {
321///     // Check all active collisions
322///     for (a, b) in collisions.iter() {
323///         println!("{a:?} is colliding with {b:?}");
324///     }
325///
326///     // Check if specific entities are colliding
327///     if collisions.contains(player, enemy) {
328///         // Take damage!
329///     }
330///
331///     // Get everything colliding with player
332///     for &other in collisions.colliding_with(player) {
333///         // Process each collision
334///     }
335/// }
336/// ```
337#[derive(SystemParam)]
338pub struct Collisions<'w> {
339    state: Res<'w, CollisionState>,
340}
341
342impl Collisions<'_> {
343    /// Check if two entities are currently colliding.
344    #[inline]
345    pub fn contains(&self, a: Entity, b: Entity) -> bool {
346        self.state.contains(a, b)
347    }
348
349    /// Get all entities currently colliding with the given entity.
350    #[inline]
351    pub fn colliding_with(&self, entity: Entity) -> &[Entity] {
352        self.state.colliding_with(entity)
353    }
354
355    /// Iterate over all currently active collision pairs.
356    #[inline]
357    pub fn iter(&self) -> impl Iterator<Item = (Entity, Entity)> + '_ {
358        self.state.iter()
359    }
360
361    /// Returns true if there are no active collisions.
362    #[inline]
363    pub fn is_empty(&self) -> bool {
364        self.state.is_empty()
365    }
366
367    /// Returns the number of active collision pairs.
368    #[inline]
369    pub fn len(&self) -> usize {
370        self.state.len()
371    }
372}
373
374// ============================================================================
375// INTERNAL: GODOT MESSAGE BRIDGE
376// ============================================================================
377
378/// Internal message type for receiving collision events from Godot.
379/// This is not part of the public API - use CollisionStarted/CollisionEnded instead.
380#[doc(hidden)]
381#[derive(Debug)]
382pub struct RawCollisionMessage {
383    pub event_type: CollisionMessageType,
384    pub origin: GodotNodeHandle,
385    pub target: GodotNodeHandle,
386}
387
388/// Resource for receiving collision messages from Godot.
389/// Wrapped in Mutex to be Send+Sync, allowing it to be a regular Bevy Resource.
390#[derive(Resource)]
391pub struct CollisionMessageReader(pub Mutex<Receiver<RawCollisionMessage>>);
392
393impl CollisionMessageReader {
394    pub fn new(receiver: Receiver<RawCollisionMessage>) -> Self {
395        Self(Mutex::new(receiver))
396    }
397}
398
399#[doc(hidden)]
400#[derive(Debug, GodotConvert)]
401#[godot(via = GString)]
402pub enum CollisionMessageType {
403    Started,
404    Ended,
405}
406
407// ============================================================================
408// PLUGIN
409// ============================================================================
410
411/// Plugin that enables collision detection between Godot physics bodies and Bevy entities.
412///
413/// This plugin automatically tracks collisions for entities that have collision
414/// signals (Area2D, Area3D, RigidBody2D, RigidBody3D, etc.).
415///
416/// # Usage
417///
418/// Add the plugin to your app:
419///
420/// ```ignore
421/// app.add_plugins(GodotCollisionsPlugin);
422/// ```
423///
424/// Then use the [`Collisions`] system parameter or collision events:
425///
426/// ```ignore
427/// fn detect_hits(
428///     collisions: Collisions,
429///     mut started: MessageReader<CollisionStarted>,
430/// ) {
431///     // Query current state
432///     if collisions.contains(player, enemy) {
433///         // Currently colliding
434///     }
435///
436///     // React to events
437///     for event in started.read() {
438///         // Just started colliding
439///     }
440/// }
441/// ```
442#[derive(Default)]
443pub struct GodotCollisionsPlugin;
444
445impl Plugin for GodotCollisionsPlugin {
446    fn build(&self, app: &mut App) {
447        app.init_resource::<CollisionState>()
448            .add_message::<CollisionStarted>()
449            .add_message::<CollisionEnded>()
450            .add_systems(
451                PrePhysicsUpdate,
452                (
453                    process_godot_collisions.before(message_update_system),
454                    trigger_collision_observers.after(process_godot_collisions),
455                ),
456            );
457    }
458}
459
460/// System that processes raw Godot collision events and updates state + messages
461fn process_godot_collisions(
462    events: Option<Res<CollisionMessageReader>>,
463    mut collision_state: ResMut<CollisionState>,
464    mut started_writer: MessageWriter<CollisionStarted>,
465    mut ended_writer: MessageWriter<CollisionEnded>,
466    node_index: Res<NodeEntityIndex>,
467) {
468    // Clear per-frame data
469    collision_state.begin_frame();
470
471    let Some(events) = events else {
472        return;
473    };
474
475    let receiver = events.0.lock();
476    let use_rebuild_path = receiver.len() >= COLLISION_NEIGHBOR_REBUILD_THRESHOLD;
477
478    for event in receiver.try_iter() {
479        trace!(target: "godot_collisions", event = ?event);
480
481        // Look up entities for both nodes
482        let origin_entity = node_index.get(event.origin.instance_id());
483        let target_entity = node_index.get(event.target.instance_id());
484
485        let (origin, target) = match (origin_entity, target_entity) {
486            (Some(o), Some(t)) => (o, t),
487            _ => continue,
488        };
489
490        match event.event_type {
491            CollisionMessageType::Started => {
492                let changed = if use_rebuild_path {
493                    collision_state.add_collision_without_neighbors(origin, target)
494                } else {
495                    collision_state.add_collision(origin, target)
496                };
497                if changed {
498                    started_writer.write(CollisionStarted {
499                        entity1: origin,
500                        entity2: target,
501                    });
502                }
503            }
504            CollisionMessageType::Ended => {
505                let changed = if use_rebuild_path {
506                    collision_state.remove_collision_without_neighbors(origin, target)
507                } else {
508                    collision_state.remove_collision(origin, target)
509                };
510                if changed {
511                    ended_writer.write(CollisionEnded {
512                        entity1: origin,
513                        entity2: target,
514                    });
515                }
516            }
517        }
518    }
519
520    if use_rebuild_path {
521        collision_state.rebuild_entity_collisions();
522    }
523}
524
525/// System that triggers observers for collision events.
526fn trigger_collision_observers(
527    mut commands: Commands,
528    mut started_reader: MessageReader<CollisionStarted>,
529    mut ended_reader: MessageReader<CollisionEnded>,
530) {
531    for &event in started_reader.read() {
532        commands.trigger(event);
533    }
534    for &event in ended_reader.read() {
535        commands.trigger(event);
536    }
537}
538
539// ============================================================================
540// TESTS
541// ============================================================================
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    #[test]
548    fn test_collision_state_add_remove() {
549        let mut state = CollisionState::default();
550        let e1 = Entity::from_bits(1);
551        let e2 = Entity::from_bits(2);
552        let e3 = Entity::from_bits(3);
553
554        // Add collision
555        state.add_collision(e1, e2);
556        assert!(state.contains(e1, e2));
557        assert!(state.contains(e2, e1)); // Symmetric
558        assert!(!state.contains(e1, e3));
559
560        // Check colliding_with
561        assert_eq!(state.colliding_with(e1), &[e2]);
562        assert_eq!(state.colliding_with(e2), &[e1]);
563        assert!(state.colliding_with(e3).is_empty());
564
565        // Check started
566        assert_eq!(state.started_this_frame.len(), 1);
567
568        // Remove collision
569        state.remove_collision(e1, e2);
570        assert!(!state.contains(e1, e2));
571        assert!(state.colliding_with(e1).is_empty());
572
573        // Check ended
574        assert_eq!(state.ended_this_frame.len(), 1);
575    }
576
577    #[test]
578    fn test_collision_state_begin_frame() {
579        let mut state = CollisionState::default();
580        let e1 = Entity::from_bits(1);
581        let e2 = Entity::from_bits(2);
582
583        state.add_collision(e1, e2);
584        assert_eq!(state.started_this_frame.len(), 1);
585
586        // Begin new frame
587        state.begin_frame();
588        assert!(state.started_this_frame.is_empty());
589        assert!(state.ended_this_frame.is_empty());
590
591        // But collision should still be active
592        assert!(state.contains(e1, e2));
593    }
594
595    #[test]
596    fn test_normalize_pair() {
597        let e1 = Entity::from_bits(1);
598        let e2 = Entity::from_bits(2);
599
600        // Should always return same order regardless of input order
601        assert_eq!(normalize_pair(e1, e2), normalize_pair(e2, e1));
602    }
603
604    #[test]
605    fn test_collision_state_multiple_collisions() {
606        let mut state = CollisionState::default();
607        let e1 = Entity::from_bits(1);
608        let e2 = Entity::from_bits(2);
609        let e3 = Entity::from_bits(3);
610
611        state.add_collision(e1, e2);
612        state.add_collision(e1, e3);
613
614        // e1 collides with both
615        let colliding = state.colliding_with(e1);
616        assert_eq!(colliding.len(), 2);
617        assert!(colliding.contains(&e2));
618        assert!(colliding.contains(&e3));
619
620        // e2 only collides with e1
621        assert_eq!(state.colliding_with(e2), &[e1]);
622
623        // e3 only collides with e1
624        assert_eq!(state.colliding_with(e3), &[e1]);
625    }
626
627    #[test]
628    fn test_duplicate_collision_ignored() {
629        let mut state = CollisionState::default();
630        let e1 = Entity::from_bits(1);
631        let e2 = Entity::from_bits(2);
632
633        state.add_collision(e1, e2);
634        state.add_collision(e1, e2); // Duplicate
635        state.add_collision(e2, e1); // Same pair, different order
636
637        // Should only have one collision
638        assert_eq!(state.len(), 1);
639        assert_eq!(state.started_this_frame.len(), 1);
640    }
641}