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}