Skip to main content

goud_engine/ecs/
component.rs

1//! Component trait and utilities for the ECS.
2//!
3//! Components are data types that can be attached to entities. This module defines
4//! the [`Component`] trait that marks types as valid components, along with
5//! component identification utilities.
6//!
7//! # Design Philosophy
8//!
9//! Unlike some ECS implementations that use blanket implementations, GoudEngine
10//! requires explicit opt-in for component types. This provides:
11//!
12//! - **Safety**: Only intentionally marked types become components
13//! - **Clarity**: Clear distinction between data and component types
14//! - **Control**: Future derive macro can add custom behavior
15//!
16//! # Example
17//!
18//! ```
19//! use goud_engine::ecs::Component;
20//!
21//! // Define a position component
22//! #[derive(Debug, Clone, Copy)]
23//! struct Position {
24//!     x: f32,
25//!     y: f32,
26//! }
27//!
28//! // Explicitly implement Component
29//! impl Component for Position {}
30//!
31//! // Define a velocity component
32//! #[derive(Debug, Clone, Copy)]
33//! struct Velocity {
34//!     x: f32,
35//!     y: f32,
36//! }
37//!
38//! impl Component for Velocity {}
39//! ```
40//!
41//! # Future: Derive Macro
42//!
43//! In the future, a derive macro will simplify component definition:
44//!
45//! ```ignore
46//! #[derive(Component)]
47//! struct Position { x: f32, y: f32 }
48//! ```
49//!
50//! The derive macro will automatically implement the Component trait and
51//! potentially add reflection, serialization, or other capabilities.
52
53use std::any::TypeId;
54
55/// Marker trait for types that can be used as ECS components.
56///
57/// Components must be:
58/// - `Send`: Can be transferred between threads
59/// - `Sync`: Can be shared between threads via references
60/// - `'static`: No borrowed data (required for type erasure and storage)
61///
62/// # Thread Safety
63///
64/// The `Send + Sync` bounds enable parallel system execution. Systems can
65/// safely access components from multiple threads when access patterns
66/// don't conflict (multiple readers or single writer).
67///
68/// # Implementation
69///
70/// Components require explicit opt-in via implementation:
71///
72/// ```
73/// use goud_engine::ecs::Component;
74///
75/// struct Health(pub f32);
76/// impl Component for Health {}
77/// ```
78///
79/// This is intentional - not all `Send + Sync + 'static` types should
80/// automatically be components. The explicit implementation:
81///
82/// 1. Documents intent that this type is meant for ECS use
83/// 2. Allows future derive macro to add behavior
84/// 3. Prevents accidental use of inappropriate types as components
85///
86/// # What Makes a Good Component?
87///
88/// - **Data-only**: Components should be pure data, no behavior
89/// - **Small**: Prefer many small components over few large ones
90/// - **Focused**: Each component represents one aspect of an entity
91///
92/// Good:
93/// ```
94/// # use goud_engine::ecs::Component;
95/// struct Position { x: f32, y: f32, z: f32 }
96/// impl Component for Position {}
97///
98/// struct Velocity { x: f32, y: f32, z: f32 }
99/// impl Component for Velocity {}
100/// ```
101///
102/// Avoid:
103/// ```ignore
104/// // Too large - combines unrelated data
105/// struct Entity {
106///     position: (f32, f32, f32),
107///     velocity: (f32, f32, f32),
108///     health: f32,
109///     name: String,
110/// }
111/// ```
112pub trait Component: Send + Sync + 'static {}
113
114// NOTE: We intentionally do NOT provide a blanket implementation.
115// Components must be explicitly opted-in. This differs from the Event trait
116// which uses a blanket impl because events are more transient and any
117// Send + Sync + 'static type is a reasonable event.
118//
119// For components, explicit opt-in provides:
120// 1. Documentation that the type is intended as a component
121// 2. Future extensibility for derive macros
122// 3. Prevention of accidental component use
123
124/// Unique identifier for a component type at runtime.
125///
126/// `ComponentId` wraps a [`TypeId`] and provides component-specific operations.
127/// It's used as a key in archetype definitions and storage lookups.
128///
129/// # Example
130///
131/// ```
132/// use goud_engine::ecs::{Component, ComponentId};
133///
134/// struct Position { x: f32, y: f32 }
135/// impl Component for Position {}
136///
137/// let id = ComponentId::of::<Position>();
138/// println!("Position component ID: {:?}", id);
139/// ```
140#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
141pub struct ComponentId(TypeId);
142
143impl ComponentId {
144    /// Creates a `ComponentId` for the given component type.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use goud_engine::ecs::{Component, ComponentId};
150    ///
151    /// struct Health(f32);
152    /// impl Component for Health {}
153    ///
154    /// let id = ComponentId::of::<Health>();
155    /// ```
156    #[inline]
157    pub fn of<T: Component>() -> Self {
158        ComponentId(TypeId::of::<T>())
159    }
160
161    /// Returns the inner `TypeId`.
162    ///
163    /// Useful for advanced use cases or debugging.
164    #[inline]
165    pub fn type_id(&self) -> TypeId {
166        self.0
167    }
168}
169
170impl std::fmt::Debug for ComponentId {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        // TypeId doesn't provide type name, but Debug shows enough for debugging
173        write!(f, "ComponentId({:?})", self.0)
174    }
175}
176
177/// Metadata about a component type.
178///
179/// Provides runtime information about a component including its name, size,
180/// and alignment. Useful for debugging, reflection, and memory layout calculations.
181///
182/// # Example
183///
184/// ```
185/// use goud_engine::ecs::{Component, ComponentInfo};
186///
187/// struct Position { x: f32, y: f32 }
188/// impl Component for Position {}
189///
190/// let info = ComponentInfo::of::<Position>();
191/// println!("Component: {} (size: {}, align: {})", info.name, info.size, info.align);
192/// ```
193#[derive(Debug, Clone)]
194pub struct ComponentInfo {
195    /// Unique identifier for this component type.
196    pub id: ComponentId,
197    /// Type name (from `std::any::type_name`).
198    pub name: &'static str,
199    /// Size in bytes.
200    pub size: usize,
201    /// Memory alignment in bytes.
202    pub align: usize,
203}
204
205impl ComponentInfo {
206    /// Creates `ComponentInfo` for the given component type.
207    ///
208    /// # Example
209    ///
210    /// ```
211    /// use goud_engine::ecs::{Component, ComponentInfo};
212    ///
213    /// struct Velocity { x: f32, y: f32 }
214    /// impl Component for Velocity {}
215    ///
216    /// let info = ComponentInfo::of::<Velocity>();
217    /// assert_eq!(info.size, std::mem::size_of::<Velocity>());
218    /// ```
219    #[inline]
220    pub fn of<T: Component>() -> Self {
221        ComponentInfo {
222            id: ComponentId::of::<T>(),
223            name: std::any::type_name::<T>(),
224            size: std::mem::size_of::<T>(),
225            align: std::mem::align_of::<T>(),
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    // Example components for testing
235    #[derive(Debug, Clone, Copy, PartialEq)]
236    struct Position {
237        x: f32,
238        y: f32,
239    }
240    impl Component for Position {}
241
242    #[derive(Debug, Clone, Copy, PartialEq)]
243    struct Velocity {
244        x: f32,
245        y: f32,
246    }
247    impl Component for Velocity {}
248
249    #[derive(Debug, Clone, PartialEq)]
250    struct Name(String);
251    impl Component for Name {}
252
253    // Zero-sized component (marker/tag)
254    #[derive(Debug, Clone, Copy, PartialEq)]
255    struct Player;
256    impl Component for Player {}
257
258    // ==========================================================================
259    // Component Trait Tests
260    // ==========================================================================
261
262    mod component_trait {
263        use super::*;
264
265        #[test]
266        fn test_component_is_send() {
267            fn assert_send<T: Send>() {}
268            assert_send::<Position>();
269            assert_send::<Velocity>();
270            assert_send::<Name>();
271            assert_send::<Player>();
272        }
273
274        #[test]
275        fn test_component_is_sync() {
276            fn assert_sync<T: Sync>() {}
277            assert_sync::<Position>();
278            assert_sync::<Velocity>();
279            assert_sync::<Name>();
280            assert_sync::<Player>();
281        }
282
283        #[test]
284        fn test_component_is_static() {
285            fn assert_static<T: 'static>() {}
286            assert_static::<Position>();
287            assert_static::<Velocity>();
288            assert_static::<Name>();
289            assert_static::<Player>();
290        }
291
292        #[test]
293        fn test_component_can_be_boxed_as_any() {
294            // Components can be type-erased and stored
295            use std::any::Any;
296
297            fn store_component<T: Component>(component: T) -> Box<dyn Any + Send + Sync> {
298                Box::new(component)
299            }
300
301            let pos = Position { x: 1.0, y: 2.0 };
302            let boxed = store_component(pos);
303
304            // Can downcast back
305            let recovered = boxed.downcast::<Position>().unwrap();
306            assert_eq!(*recovered, pos);
307        }
308    }
309
310    // ==========================================================================
311    // ComponentId Tests
312    // ==========================================================================
313
314    mod component_id {
315        use super::*;
316
317        #[test]
318        fn test_component_id_of_returns_same_id_for_same_type() {
319            let id1 = ComponentId::of::<Position>();
320            let id2 = ComponentId::of::<Position>();
321            assert_eq!(id1, id2);
322        }
323
324        #[test]
325        fn test_component_id_differs_between_types() {
326            let pos_id = ComponentId::of::<Position>();
327            let vel_id = ComponentId::of::<Velocity>();
328            assert_ne!(pos_id, vel_id);
329        }
330
331        #[test]
332        fn test_component_id_hash_consistency() {
333            use std::collections::HashMap;
334
335            let mut map: HashMap<ComponentId, &str> = HashMap::new();
336            map.insert(ComponentId::of::<Position>(), "position");
337            map.insert(ComponentId::of::<Velocity>(), "velocity");
338
339            assert_eq!(map.get(&ComponentId::of::<Position>()), Some(&"position"));
340            assert_eq!(map.get(&ComponentId::of::<Velocity>()), Some(&"velocity"));
341        }
342
343        #[test]
344        fn test_component_id_ordering() {
345            // ComponentId implements Ord for use in BTreeSet/BTreeMap
346            use std::collections::BTreeSet;
347
348            let mut set: BTreeSet<ComponentId> = BTreeSet::new();
349            set.insert(ComponentId::of::<Position>());
350            set.insert(ComponentId::of::<Velocity>());
351            set.insert(ComponentId::of::<Name>());
352
353            assert_eq!(set.len(), 3);
354
355            // Inserting same ID again doesn't increase size
356            set.insert(ComponentId::of::<Position>());
357            assert_eq!(set.len(), 3);
358        }
359
360        #[test]
361        fn test_component_id_debug_format() {
362            let id = ComponentId::of::<Position>();
363            let debug_str = format!("{:?}", id);
364            assert!(debug_str.contains("ComponentId"));
365        }
366
367        #[test]
368        fn test_component_id_type_id() {
369            let id = ComponentId::of::<Position>();
370            assert_eq!(id.type_id(), TypeId::of::<Position>());
371        }
372
373        #[test]
374        fn test_component_id_zero_sized_type() {
375            // Zero-sized types (like marker components) should work
376            let id = ComponentId::of::<Player>();
377            assert_eq!(id, ComponentId::of::<Player>());
378        }
379
380        #[test]
381        fn test_component_id_generic_types_differ() {
382            // Generic instantiations should have different IDs
383            #[derive(Debug)]
384            struct Container<T>(T);
385            impl<T: Send + Sync + 'static> Component for Container<T> {}
386
387            let id_u32 = ComponentId::of::<Container<u32>>();
388            let id_f32 = ComponentId::of::<Container<f32>>();
389            assert_ne!(id_u32, id_f32);
390        }
391    }
392
393    // ==========================================================================
394    // ComponentInfo Tests
395    // ==========================================================================
396
397    mod component_info {
398        use super::*;
399
400        #[test]
401        fn test_component_info_of() {
402            let info = ComponentInfo::of::<Position>();
403
404            assert_eq!(info.id, ComponentId::of::<Position>());
405            assert!(info.name.contains("Position"));
406            assert_eq!(info.size, std::mem::size_of::<Position>());
407            assert_eq!(info.align, std::mem::align_of::<Position>());
408        }
409
410        #[test]
411        fn test_component_info_size_and_align() {
412            let pos_info = ComponentInfo::of::<Position>();
413            // Position has 2 f32 fields = 8 bytes, align 4
414            assert_eq!(pos_info.size, 8);
415            assert_eq!(pos_info.align, 4);
416
417            let name_info = ComponentInfo::of::<Name>();
418            // Name contains String which is 24 bytes on 64-bit
419            assert_eq!(name_info.size, std::mem::size_of::<Name>());
420        }
421
422        #[test]
423        fn test_component_info_zero_sized() {
424            let info = ComponentInfo::of::<Player>();
425            assert_eq!(info.size, 0);
426            assert_eq!(info.align, 1);
427        }
428
429        #[test]
430        fn test_component_info_clone() {
431            let info1 = ComponentInfo::of::<Position>();
432            let info2 = info1.clone();
433
434            assert_eq!(info1.id, info2.id);
435            assert_eq!(info1.name, info2.name);
436            assert_eq!(info1.size, info2.size);
437            assert_eq!(info1.align, info2.align);
438        }
439
440        #[test]
441        fn test_component_info_debug() {
442            let info = ComponentInfo::of::<Position>();
443            let debug_str = format!("{:?}", info);
444
445            assert!(debug_str.contains("ComponentInfo"));
446            assert!(debug_str.contains("Position"));
447        }
448    }
449
450    // ==========================================================================
451    // No Blanket Implementation Tests
452    // ==========================================================================
453
454    mod no_blanket_impl {
455        use super::*;
456
457        // This type is Send + Sync + 'static but NOT a Component
458        #[derive(Debug)]
459        struct NotAComponent {
460            data: i32,
461        }
462
463        // Compile-time test: This should NOT compile if uncommented,
464        // proving there's no blanket implementation
465        //
466        // fn try_use_as_component<T: Component>(_: T) {}
467        // fn test_not_a_component() {
468        //     try_use_as_component(NotAComponent { data: 42 });
469        // }
470
471        #[test]
472        fn test_explicit_impl_required() {
473            // This test documents that Component requires explicit implementation.
474            // The commented code above would fail to compile because NotAComponent
475            // doesn't implement Component, even though it's Send + Sync + 'static.
476
477            // We can still use NotAComponent normally, just not as a component
478            let _not_component = NotAComponent { data: 42 };
479
480            // But types that DO implement Component work fine
481            fn use_component<T: Component>(_: T) {}
482            use_component(Position { x: 0.0, y: 0.0 });
483        }
484    }
485}