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}