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}