engine/
game_objects.rs

1// SPDX-FileCopyrightText: 2025 Jens Pitkänen <jens.pitkanen@helsinki.fi>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5//!
6//! This module makes wide use of macros, which means that there's quite a bit
7//! of the "plumbing" exposed in this module. The main parts to look into are
8//! [`Scene`](crate::game_objects::Scene), [`define_system`], and
9//! [`impl_game_object`].
10
11mod game_object;
12mod scene_builder;
13
14use core::{
15    any::{Any, TypeId},
16    cmp::{Ordering, Reverse},
17};
18
19use arrayvec::ArrayVec;
20use bytemuck::Pod;
21
22use crate::collections::FixedVec;
23
24pub use game_object::{impl_game_object, ComponentInfo, GameObject};
25pub use scene_builder::SceneBuilder;
26
27/// The maximum amount of components in a [`GameObject`] type.
28pub const MAX_COMPONENTS: usize = 32;
29
30/// An [`ArrayVec`] with capacity for [`MAX_COMPONENTS`] elements.
31///
32/// This exists since these are used throughout the game_objects module, and
33/// this allows dependents to e.g. implement the [`GameObject`] trait without
34/// depending on [`arrayvec`].
35pub type ComponentVec<T> = ArrayVec<T, MAX_COMPONENTS>;
36
37/// Generic storage for the components inside [`Scene`].
38///
39/// This type generally doesn't need to be interfaced with directly, as
40/// [`define_system`] can check and cast these into properly typed slices.
41pub struct ComponentColumn<'a> {
42    component_info: ComponentInfo,
43    data: FixedVec<'a, u8>,
44}
45
46impl ComponentColumn<'_> {
47    /// Returns the type of the components contained in this struct.
48    pub fn component_type(&self) -> TypeId {
49        self.component_info.type_id
50    }
51
52    /// If the [`TypeId`] of `C` is the same as
53    /// [`ComponentColumn::component_type`], returns a mutable borrow of the
54    /// components in this column.
55    ///
56    /// This function generally doesn't need to be interfaced with directly, as
57    /// [`define_system`] is a straightforward wrapper for iterating through the
58    /// columns and calling this on the right one.
59    pub fn get_mut<C: Any + Pod>(&mut self) -> Option<&mut [C]> {
60        if self.component_info.type_id == TypeId::of::<C>() {
61            Some(bytemuck::cast_slice_mut::<u8, C>(&mut self.data))
62        } else {
63            None
64        }
65    }
66}
67
68struct GameObjectTable<'a> {
69    game_object_type: TypeId,
70    columns: ComponentVec<ComponentColumn<'a>>,
71}
72
73impl GameObjectTable<'_> {
74    /// Swaps the components in all component between the first and second
75    /// index.
76    ///
77    /// If the indices are the same, this does nothing.
78    fn swap(&mut self, index_a: usize, index_b: usize) {
79        match index_a.cmp(&index_b) {
80            Ordering::Equal => {}
81            Ordering::Greater => self.swap(index_b, index_a),
82            Ordering::Less => {
83                for col in &mut self.columns {
84                    let size = col.component_info.size;
85                    let a_byte_index = size * index_a;
86                    let b_byte_index = size * index_b;
87                    let (contains_a, starts_with_b) = col.data.split_at_mut(b_byte_index);
88                    let a = &mut contains_a[a_byte_index..a_byte_index + size];
89                    let b = &mut starts_with_b[..size];
90                    a.swap_with_slice(b);
91                }
92            }
93        }
94    }
95
96    /// Truncates the component columns to only contain `new_len` components,
97    /// i.e. deletes game objects from the end of this table to have `new_len`
98    /// game objects at maximum.
99    fn truncate(&mut self, new_len: usize) {
100        for col in &mut self.columns {
101            let new_bytes_len = new_len * col.component_info.size;
102            col.data.truncate(new_bytes_len);
103        }
104    }
105
106    fn len(&self) -> usize {
107        if self.columns.is_empty() {
108            0
109        } else {
110            let col = &self.columns[0];
111            col.data.len() / col.component_info.size
112        }
113    }
114}
115
116/// Error type returned by [`Scene::spawn`].
117#[derive(Debug, PartialEq)]
118pub enum SpawnError {
119    /// Attempted to spawn a game object that wasn't registered for the
120    /// [`Scene`] with [`SceneBuilder::with_game_object_type`]. This generally
121    /// hints at a bug in the game's scene initialization code.
122    UnregisteredGameObjectType,
123    /// The [`Scene`]'s storage limit for the [`GameObject`] type has been
124    /// reached.
125    ///
126    /// This can be avoided by reserving more space in the first place (via the
127    /// `count` parameter of [`SceneBuilder::with_game_object_type`]), or at
128    /// runtime by removing at least one existing game object of the same type
129    /// to make room. Game objects can be removed with the [`Scene::delete`]
130    /// function.
131    NoSpace,
132}
133
134/// Temporary handle for operating on specific game objects. Invalidated by
135/// [`Scene::delete`].
136///
137/// After invalidation, these handles don't refer to anything.
138#[derive(Clone, Copy, Debug)]
139pub struct GameObjectHandle {
140    scene_id: u32,
141    scene_generation: u64,
142    game_object_table_index: u32,
143    game_object_index: usize,
144}
145
146/// Returns [`GameObjectHandle`]s for referring to the game objects in e.g.
147/// [`Scene::delete`].
148///
149/// When iterated alongside the component slices in [`Scene::run_system`] (e.g.
150/// with a [`Zip`](core::iter::Zip) iterator), the returned handles correspond
151/// to the game objects whose components are being used on any particular
152/// iteration.
153pub struct GameObjectHandleIterator {
154    scene_id: u32,
155    scene_generation: u64,
156    game_object_table_index: u32,
157    next_game_object_index: usize,
158    total_game_objects: usize,
159}
160
161impl Iterator for GameObjectHandleIterator {
162    type Item = GameObjectHandle;
163    fn next(&mut self) -> Option<Self::Item> {
164        if self.next_game_object_index < self.total_game_objects {
165            let game_object_index = self.next_game_object_index;
166            self.next_game_object_index += 1;
167            Some(GameObjectHandle {
168                scene_id: self.scene_id,
169                scene_generation: self.scene_generation,
170                game_object_table_index: self.game_object_table_index,
171                game_object_index,
172            })
173        } else {
174            None
175        }
176    }
177}
178
179/// Container for [`GameObject`]s.
180///
181/// A scene is initialized with [`Scene::builder`], which is used to register
182/// the [`GameObject`] types which can be spawned into the scene. The memory for
183/// the game objects is allocated at the end in [`SceneBuilder::build`].
184///
185/// Game objects are spawned with [`Scene::spawn`], after which they can be
186/// accessed by running *systems* (in the Entity-Component-System sense) with
187/// [`Scene::run_system`]. To skip the boilerplate, the [`define_system`] macro
188/// is recommended for defining system functions.
189///
190/// ### Example
191///
192/// ```
193/// # static ARENA: &engine::allocators::LinearAllocator = engine::static_allocator!(100_000);
194/// # let (arena, temp_arena) = (ARENA, ARENA);
195/// use engine::{game_objects::Scene, define_system, impl_game_object};
196///
197/// // Define some component types:
198///
199/// // NOTE: Zeroable and Pod are manually implemented here to avoid
200/// // the engine depending on proc macros. They should generally be
201/// // derived, if compile times allow, as Pod has a lot of
202/// // requirements that are easy to forget.
203///
204/// #[derive(Debug, Clone, Copy)]
205/// #[repr(C)]
206/// struct Position { pub x: i32, pub y: i32 }
207/// unsafe impl bytemuck::Zeroable for Position {}
208/// unsafe impl bytemuck::Pod for Position {}
209///
210/// #[derive(Debug, Clone, Copy)]
211/// #[repr(C)]
212/// struct Velocity { pub x: i32, pub y: i32 }
213/// unsafe impl bytemuck::Zeroable for Velocity {}
214/// unsafe impl bytemuck::Pod for Velocity {}
215///
216/// // Define the "Foo" game object:
217///
218/// #[derive(Debug)]
219/// struct Foo {
220///     pub position: Position,
221///     pub velocity: Velocity,
222/// }
223///
224/// impl_game_object! {
225///     impl GameObject for Foo using components {
226///         position: Position,
227///         velocity: Velocity,
228///     }
229/// }
230///
231/// // Create a Scene that five game objects of type Foo can be spawned in:
232/// let mut scene = Scene::builder()
233///     .with_game_object_type::<Foo>(5)
234///     .build(arena, temp_arena)
235///     .unwrap();
236///
237/// // Spawn a game object of type Foo:
238/// scene.spawn(Foo {
239///     position: Position { x: 100, y: 100 },
240///     velocity: Velocity { x: 20,  y: -10 },
241/// }).unwrap();
242///
243/// // Run a "physics simulation" system for all game objects which
244/// // have a Position and Velocity component:
245/// scene.run_system(define_system!(|_, pos: &mut [Position], vel: &[Velocity]| {
246///     // This closure gets called once for each game object type with a Position
247///     // and a Velocity, passing in that type's components, which can be zipped
248///     // and iterated through to operate on a single game object's data at a
249///     // time. In this case, the closure only gets called for Foo as it's our
250///     // only game object type, and these slices are 1 long, as we only spawned
251///     // one game object.
252///     for (pos, vel) in pos.iter_mut().zip(vel) {
253///         pos.x += vel.x;
254///         pos.y += vel.y;
255///     }
256/// }));
257///
258/// // Just assert that we ended up where we intended to end up.
259/// let mut positions_in_scene = 0;
260/// scene.run_system(define_system!(|_, pos: &[Position]| {
261///     for pos in pos {
262///         assert_eq!(120, pos.x);
263///         assert_eq!(90, pos.y);
264///         positions_in_scene += 1;
265///     }
266/// }));
267/// assert_eq!(1, positions_in_scene);
268///
269/// // Game objects can be deleted by collecting and deleting them in batches:
270/// use engine::collections::FixedVec;
271/// let mut handles_to_delete = FixedVec::new(temp_arena, 1).unwrap();
272/// scene.run_system(define_system!(|handles, pos: &[Position]| {
273///     for (handle, pos) in handles.zip(pos) {
274///         if pos.x == 120 {
275///             handles_to_delete.push(handle).unwrap();
276///         }
277///     }
278/// }));
279///
280/// // NOTE: After deletion, all handles get invalidated, so
281/// // handles_to_delete would need to be re-acquired from a run_system call.
282/// scene.delete(&mut handles_to_delete).unwrap();
283/// ```
284pub struct Scene<'a> {
285    /// A unique identifier for distinguishing between [`GameObjectHandle`]s
286    /// acquired from different scenes.
287    id: u32,
288    /// An incrementing value for detecting invalidated [`GameObjectHandle`]s.
289    /// Incremented whenever indexes to game_object_tables or the tables' inner
290    /// vecs are invalidated.
291    generation: u64,
292    game_object_tables: FixedVec<'a, GameObjectTable<'a>>,
293}
294
295impl Scene<'_> {
296    /// Spawns the game object into this scene if there's space for it.
297    ///
298    /// See the [`Scene`] documentation for example usage.
299    pub fn spawn<G: GameObject>(&mut self, object: G) -> Result<(), SpawnError> {
300        self.spawn_inner(object.type_id(), &object.components())
301    }
302
303    fn spawn_inner(
304        &mut self,
305        game_object_type: TypeId,
306        components: &[(TypeId, &[u8])],
307    ) -> Result<(), SpawnError> {
308        let Some(table) = (self.game_object_tables.iter_mut())
309            .find(|table| table.game_object_type == game_object_type)
310        else {
311            return Err(SpawnError::UnregisteredGameObjectType);
312        };
313
314        if table.columns.is_empty() || table.columns[0].data.is_full() {
315            return Err(SpawnError::NoSpace);
316        }
317
318        for (col, (c_type, c_data)) in table.columns.iter_mut().zip(components) {
319            assert_eq!(col.component_info.type_id, *c_type);
320            let write_succeeded = col.data.extend_from_slice(c_data);
321            assert!(write_succeeded, "component should fit");
322        }
323
324        Ok(())
325    }
326
327    /// Runs `system_func` for each game object type in this [`Scene`], passing
328    /// in the components for each.
329    ///
330    /// Returns `false` if all `system_func` invocations return `false`. When
331    /// using [`define_system`], this happens when the scene doesn't contain any
332    /// game object types with the set of components requested.
333    ///
334    /// The [`GameObjectHandleIterator`] returns handles to the game objects
335    /// associated with the components in a particular iteration, if iterated
336    /// through at the same pace as the component columns.
337    ///
338    /// Each [`ComponentColumn`] contains tightly packed data for a specific
339    /// component type, and the columns can be zipped together to iterate
340    /// through sets of components belonging to a single game object, as
341    /// component A at index N belongs to the same game object as component B at
342    /// index N.
343    ///
344    /// This is intended to be used with [`define_system`], which can extract
345    /// the relevant components from the component columns. See the [`Scene`]
346    /// documentation for example usage.
347    pub fn run_system<F>(&mut self, mut system_func: F) -> bool
348    where
349        F: FnMut(GameObjectHandleIterator, ComponentVec<&mut ComponentColumn>) -> bool,
350    {
351        profiling::function_scope!();
352        let mut matched_any_components = false;
353        for (table_index, table) in self.game_object_tables.iter_mut().enumerate() {
354            let handle_iter = GameObjectHandleIterator {
355                scene_id: self.id,
356                scene_generation: self.generation,
357                game_object_table_index: table_index as u32,
358                next_game_object_index: 0,
359                total_game_objects: table.len(),
360            };
361
362            let mut columns = ArrayVec::new();
363            for col in &mut *table.columns {
364                columns.push(col);
365            }
366
367            matched_any_components |= system_func(handle_iter, columns);
368        }
369        matched_any_components
370    }
371
372    /// Deletes the game objects referred to by the given handles.
373    ///
374    /// If any handles are invalid (e.g. have been invalidated by a previous
375    /// call to [`Scene::delete`]), the amount of invalid handles is returned in
376    /// an Err.
377    ///
378    /// The slice of handles is mutable to allow sorting the slice, which is
379    /// needed for a performant implementation of this function.
380    pub fn delete(&mut self, handles: &mut [GameObjectHandle]) -> Result<(), usize> {
381        profiling::function_scope!();
382        let mut invalid_handles = 0;
383
384        // Sort the handles, so that deletions are grouped by table index (not
385        // necessary for the algorithm, but seems a bit better for data
386        // locality), and the individual game object indices are processed in
387        // descending order from the end (which allows deleting by
388        // swap-and-truncate without invalidating any future indexes to delete).
389        handles.sort_unstable_by_key(|handle| {
390            (
391                handle.game_object_table_index,
392                Reverse(handle.game_object_index),
393            )
394        });
395
396        for handle in handles {
397            if handle.scene_id != self.id || handle.scene_generation != self.generation {
398                invalid_handles += 1;
399                continue;
400            }
401
402            let table = &mut self.game_object_tables[handle.game_object_table_index as usize];
403            let table_last_index = table.len() - 1;
404            table.swap(handle.game_object_index, table_last_index);
405            table.truncate(table_last_index);
406        }
407
408        self.generation += 1;
409
410        if invalid_handles == 0 {
411            Ok(())
412        } else {
413            Err(invalid_handles)
414        }
415    }
416
417    /// Deletes all game objects in this scene.
418    pub fn reset(&mut self) {
419        for table in self.game_object_tables.iter_mut() {
420            table.truncate(0);
421        }
422    }
423}
424
425/// Searches the columns for one containing components of type `C`, and returns
426/// it as a properly typed slice.
427pub fn extract_component_column<'a, C: Pod + Any>(
428    columns: &mut ComponentVec<&'a mut ComponentColumn>,
429) -> Option<&'a mut [C]> {
430    let index = columns
431        .iter()
432        .position(|col| col.component_type() == TypeId::of::<C>())?;
433    let col = columns.swap_remove(index);
434    Some(col.get_mut().unwrap())
435}
436
437/// Gutputs a closure that can be passed into [`Scene::run_system`], handling
438/// extracting properly typed component columns based on the parameter list.
439///
440/// The [`GameObjectHandleIterator`] parameter from [`Scene::run_system`] is
441/// always assigned to first parameter of the closure.
442///
443/// The generated closure extracts the relevant component slices from the
444/// anonymous [`ComponentColumn`]s, and makes them available to the closure body
445/// as variables, using the names from the parameter list.
446///
447/// For simplicity, the parameters after the first one can only be mutable
448/// slices, but note that [`Scene::run_system`] takes a [`FnMut`], so the
449/// closure can borrow and even mutate their captured environment.
450///
451/// ### Example
452/// ```
453/// # static ARENA: &engine::allocators::LinearAllocator = engine::static_allocator!(100_000);
454/// # use engine::{game_objects::Scene, define_system, impl_game_object};
455/// # #[derive(Debug, Clone, Copy)]
456/// # #[repr(C)]
457/// # struct Position { pub x: i32, pub y: i32 }
458/// # unsafe impl bytemuck::Zeroable for Position {}
459/// # unsafe impl bytemuck::Pod for Position {}
460/// # #[derive(Debug, Clone, Copy)]
461/// # #[repr(C)]
462/// # struct Velocity { pub x: i32, pub y: i32 }
463/// # unsafe impl bytemuck::Zeroable for Velocity {}
464/// # unsafe impl bytemuck::Pod for Velocity {}
465/// # let mut scene = Scene::builder().build(ARENA, ARENA).unwrap();
466/// let mut game_object_handle = None;
467/// scene.run_system(define_system!(|handles, pos: &mut [Position], vel: &[Velocity]| {
468///     for ((handle, pos), vel) in handles.zip(pos).zip(vel) {
469///         pos.x += vel.x;
470///         pos.y += vel.y;
471///         game_object_handle = Some(handle);
472///     }
473/// }));
474/// if let Some(handle) = game_object_handle {
475///     scene.delete(&mut [handle]).unwrap();
476/// }
477/// ```
478#[macro_export]
479macro_rules! define_system {
480    (/param_defs/ $table:ident / $func_body:block / |$param_name:ident: $param_type:ty|) => {{
481        let col: Option<&mut [_]> = $crate::game_objects::extract_component_column(&mut $table);
482        let Some(col) = col else {
483            return false;
484        };
485        let $param_name: $param_type = col;
486        $func_body
487    }};
488    (/param_defs/ $table:ident / $func_body:block / |$param_name:ident: $param_type:ty, $($rest_names:ident: $rest_types:ty),+|) => {
489        define_system!(/param_defs/ $table / {
490            define_system!(/param_defs/ $table / $func_body / |$param_name: $param_type|)
491        } / |$($rest_names: $rest_types),+|)
492    };
493
494    (|$handle_name:pat_param, $($param_name:ident: $param_type:ty),+| $func_body:block) => {
495        |#[allow(unused_variables)] handle_iter: $crate::game_objects::GameObjectHandleIterator,
496         mut table: $crate::game_objects::ComponentVec<&mut $crate::game_objects::ComponentColumn>| {
497            $crate::profiling::scope!("system_func", concat!(file!(), ":", line!()));
498            let $handle_name = handle_iter;
499            define_system!(/param_defs/ table / $func_body / |$($param_name: $param_type),+|);
500            true
501        }
502    };
503}
504
505pub use define_system;
506
507#[cfg(test)]
508mod tests {
509    use arrayvec::ArrayVec;
510    use bytemuck::{Pod, Zeroable};
511
512    use crate::{
513        allocators::LinearAllocator, game_objects::GameObjectHandle, impl_game_object,
514        static_allocator,
515    };
516
517    use super::{Scene, SpawnError};
518
519    #[test]
520    fn run_scene() {
521        #[derive(Clone, Copy, Debug)]
522        struct ComponentA {
523            value: i64,
524        }
525        unsafe impl Zeroable for ComponentA {}
526        unsafe impl Pod for ComponentA {}
527
528        #[derive(Clone, Copy, Debug)]
529        struct ComponentB {
530            value: u32,
531        }
532        unsafe impl Zeroable for ComponentB {}
533        unsafe impl Pod for ComponentB {}
534
535        #[derive(Debug)]
536        struct GameObjectX {
537            a: ComponentA,
538        }
539        impl_game_object! {
540            impl GameObject for GameObjectX using components {
541                a: ComponentA,
542            }
543        }
544
545        #[derive(Debug)]
546        struct GameObjectY {
547            a: ComponentA,
548            b: ComponentB,
549        }
550        impl_game_object! {
551            impl GameObject for GameObjectY using components {
552                a: ComponentA,
553                b: ComponentB,
554            }
555        }
556
557        static ARENA: &LinearAllocator = static_allocator!(10_000);
558        let temp_arena = LinearAllocator::new(ARENA, 1000).unwrap();
559        let mut scene = Scene::builder()
560            .with_game_object_type::<GameObjectX>(10)
561            .with_game_object_type::<GameObjectY>(5)
562            .build(ARENA, &temp_arena)
563            .unwrap();
564
565        for i in 0..10 {
566            let object_x = GameObjectX {
567                a: ComponentA { value: -i },
568            };
569            scene.spawn(object_x).unwrap();
570        }
571
572        for i in 0..5 {
573            let object_y = GameObjectY {
574                a: ComponentA { value: -10 },
575                b: ComponentB { value: 5 * i },
576            };
577            scene.spawn(object_y).unwrap();
578        }
579
580        assert_eq!(
581            Err(SpawnError::NoSpace),
582            scene.spawn(GameObjectX {
583                a: ComponentA { value: 0 },
584            }),
585            "the 10 reserved slots for GameObjectX should already be in use",
586        );
587
588        assert_eq!(
589            Err(SpawnError::NoSpace),
590            scene.spawn(GameObjectY {
591                a: ComponentA { value: 0 },
592                b: ComponentB { value: 0 },
593            }),
594            "the 5 reserved slots for GameObjectY should already be in use",
595        );
596
597        // Assert that there aren't any ComponentA's with positive values:
598        let mut processed_count = 0;
599        scene.run_system(define_system!(|_, a: &[ComponentA]| {
600            for a in a {
601                assert!(a.value <= 0);
602                processed_count += 1;
603            }
604        }));
605        assert!(processed_count > 0);
606
607        // Apply some changes to GameObjectY's:
608        let system = define_system!(|_, a: &mut [ComponentA], b: &[ComponentB]| {
609            for (a, b) in a.iter_mut().zip(b) {
610                a.value += b.value as i64;
611            }
612        });
613        scene.run_system(system);
614
615        // Assert that there *are* positive values now, and delete them:
616        let mut processed_count = 0;
617        let mut handles_to_delete: ArrayVec<GameObjectHandle, 15> = ArrayVec::new();
618        scene.run_system(define_system!(|handles, a: &[ComponentA]| {
619            for (handle, a) in handles.zip(a) {
620                if a.value > 0 {
621                    handles_to_delete.push(handle);
622                }
623                processed_count += 1;
624            }
625        }));
626        scene.delete(&mut handles_to_delete).unwrap();
627        assert!(processed_count > 0);
628
629        // Assert that there aren't any positive values anymore, now that they
630        // were deleted:
631        let mut processed_count = 0;
632        scene.run_system(define_system!(|_, a: &[ComponentA]| {
633            for a in a {
634                assert!(a.value <= 0);
635                processed_count += 1;
636            }
637        }));
638        assert!(processed_count > 0);
639    }
640}