From 5ace8a2b7b9c78a731a41c33b4476a442f961dd0 Mon Sep 17 00:00:00 2001 From: copygirl Date: Sat, 21 Sep 2024 10:47:30 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + Cargo.toml | 9 + README.md | 23 ++ src/ecs/archetable.rs | 114 ++++++++++ src/ecs/archetype.rs | 44 ++++ src/ecs/component.rs | 125 +++++++++++ src/ecs/entity.rs | 272 ++++++++++++++++++++++++ src/ecs/entity_index.rs | 303 +++++++++++++++++++++++++++ src/ecs/mod.rs | 17 ++ src/ecs/relation.rs | 135 ++++++++++++ src/ecs/utility/mod.rs | 3 + src/ecs/utility/raw_component_vec.rs | 68 ++++++ src/ecs/world.rs | 74 +++++++ src/lib.rs | 1 + 14 files changed, 1190 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/ecs/archetable.rs create mode 100644 src/ecs/archetype.rs create mode 100644 src/ecs/component.rs create mode 100644 src/ecs/entity.rs create mode 100644 src/ecs/entity_index.rs create mode 100644 src/ecs/mod.rs create mode 100644 src/ecs/relation.rs create mode 100644 src/ecs/utility/mod.rs create mode 100644 src/ecs/utility/raw_component_vec.rs create mode 100644 src/ecs/world.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9e2199 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target/ +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1e11e90 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "gaemstone" +version = "0.1.0" +edition = "2021" + +[dependencies] +aligned-vec = "0.6.1" +bitflags = "2.6.0" +thiserror = "1.0.63" diff --git a/README.md b/README.md new file mode 100644 index 0000000..aefda1e --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# gæmstone Game Engine + +Similarly to a certain billionare with his obsession with the letter 𝕏, I have +my own issues. More specifically, I want a game engine that allows for content +creation and scripting at runtime, and while in multiplayer, without any +significant interruptions. It doesn't exist, so I have to write my own. + +This has been going on for over a decade, and the name "gæmstone" stuck, thus +there's a couple of failed iterations, such as one written in C#. Considering +the lack of professionalism in this readme, and by how long this has been going +on, you can likely tell the chances of any semblance of success are very low. +However I've had a lot of fun writing my own "toy" ECS in Rust, and I just like +sharing my code for others to see, maybe get inspired by, so here you go! + +The idea is to have a sufficiently dynamic [Entity Component System (ECS)](ECS) +at the core, inspired by [Flecs], to support both runtime creation and deletion +of components, as well as first-class support for entity relationships. Then +throw in some [wasmtime] for scripting. As for windowing, events, rendering and +such.. who knows? Maybe WebGPU. + +[ECS]: https://en.wikipedia.org/wiki/Entity_component_system +[Flecs]: https://github.com/SanderMertens/flecs +[wasmtime]: https://github.com/bytecodealliance/wasmtime diff --git a/src/ecs/archetable.rs b/src/ecs/archetable.rs new file mode 100644 index 0000000..53ea9db --- /dev/null +++ b/src/ecs/archetable.rs @@ -0,0 +1,114 @@ +use std::any::TypeId; + +use super::archetype::Archetype; +use super::entity::Entity; +use super::world::World; + +use super::utility::RawComponentVec; + +pub struct Archetable { + archetype: Archetype, + components: Vec, + columns: Vec, + entities: Vec, +} + +pub type EntityRow = usize; + +struct ComponentInfo { + entity: Entity, + type_id: Option, + column_index: Option, +} + +struct Column { + data: RawComponentVec, + component_index: usize, +} + +impl Archetable { + #[must_use] + pub fn new_empty() -> Self { + Self { + archetype: Archetype::EMPTY, + components: Vec::new(), + columns: Vec::new(), + entities: Vec::new(), + } + } + + #[must_use] + pub fn new(world: &World, archetype: Archetype) -> Self { + let mut components = Vec::new(); + let mut columns = Vec::new(); + + for component in archetype.components() { + let entity = (*component).try_into().unwrap(); + let (layout, type_id) = world.lookup_component_info(entity).unzip(); + + // If the entity has `Layout`, add a column for it. + let column_index = if let Some(layout) = layout { + columns.push(Column { + data: RawComponentVec::new(layout), + component_index: components.len(), + }); + Some(columns.len() - 1) + } else { + None + }; + + components.push(ComponentInfo { + entity, + column_index, + type_id, + }); + } + + Self { + archetype, + components, + columns, + entities: Vec::new(), + } + } + + pub fn push(&mut self, entity: Entity) -> EntityRow { + self.entities.push(entity); + let new_len = self.entities.len(); + for column in self.columns.iter_mut() { + column.data.resize(new_len); + } + new_len - 1 + } + + /// Removes an entity from the specified `row`, including its component values. + /// Potentially swaps the last alive entity into the vacant spot, then shrinks the `entities` vector. + /// If a swap occurred, the entity swapped into `row` is returned, so references to it can be updated. + pub fn swap_remove(&mut self, row: EntityRow) -> Option { + self.entities.swap_remove(row); + for column in self.columns.iter_mut() { + column.data.swap_remove(row); + } + self.entities.get(row).copied() + } + + fn find_component_info(&self) -> Option<&ComponentInfo> { + let type_id = Some(TypeId::of::()); + self.components.iter().find(|e| e.type_id == type_id) + } + + #[must_use] + pub fn get_component_column(&self) -> Option<&[T]> { + let info = self.find_component_info::()?; + let column = &self.columns[info.column_index?]; + Some(unsafe { column.data.as_typed_slice::() }) + } + + #[must_use] + pub fn get_component_column_mut(&mut self) -> Option<&mut [T]> { + let info = self.find_component_info::()?; + let column_index = info.column_index?; // Avoid borrow. + let column = &mut self.columns[column_index]; + Some(unsafe { column.data.as_typed_slice_mut::() }) + } +} diff --git a/src/ecs/archetype.rs b/src/ecs/archetype.rs new file mode 100644 index 0000000..fc36fca --- /dev/null +++ b/src/ecs/archetype.rs @@ -0,0 +1,44 @@ +use super::component::Component; + +/// Represents a set of components an [`Entity`] may have. +/// +/// [`Entity`]: super::entity::Entity +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Archetype { + components: Vec, +} + +impl Archetype { + pub const EMPTY: Archetype = Self { + components: Vec::new(), + }; + + #[must_use] + pub fn new(iter: impl IntoIterator>) -> Self { + let iter = iter.into_iter().map(Into::into).collect(); + let mut components: Vec<_> = iter; + components.sort(); + components.dedup(); + Self { components } + } + + #[inline] + #[must_use] + pub fn components(&self) -> &[Component] { + &self.components + } + + #[must_use] + pub fn has(&self, component: impl Into) -> bool { + self.components.binary_search(&component.into()).is_ok() + } +} + +impl FromIterator for Archetype +where + T: Into, +{ + fn from_iter>(iter: I) -> Self { + Self::new(iter) + } +} diff --git a/src/ecs/component.rs b/src/ecs/component.rs new file mode 100644 index 0000000..cff7fa4 --- /dev/null +++ b/src/ecs/component.rs @@ -0,0 +1,125 @@ +/// Components are plain identifiers which represent anything that can be +/// added to an [`Entity`], which includes other entities and [`Relation`]s. +/// +/// They are either a "tag component" or a "value component" depending on +/// whether they can hold a value when added to an entity. Components become +/// "value components" if the built-in [`Layout`] component is present on them, +/// which describes how it will be stored inside of [`Archetable`]s. +/// +/// [`Entity`]: super::entity::Entity +/// [`Relation`]: super::relation::Relation +/// [`Archetable`]: super::archetable::Archetable +/// [`Layout`]: std::alloc::Layout +#[repr(C)] +#[derive(Copy, Clone, Eq)] +pub struct Component { + low: u32, + high: u32, +} + +bitflags::bitflags! { + /// Component flags are an implementation detail, currently only used to + /// determine whether the `Component` represents an `Entity` or `Relation`. + #[repr(C)] + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] + pub struct Flags: u32 { + const NONE = 0; + const RELATION = 1 << 31; + } +} + +impl Component { + #[inline] + #[must_use] + pub fn flags(&self) -> Flags { + // SAFETY: If component is valid, masking the high bits + // should produce a valid flags value as well. + unsafe { Flags::from_bits_masked(self.high) } + } + + #[inline] + #[must_use] + pub fn to_bits(&self) -> u64 { + // SAFETY: Any representation of bits is a valid u64. + unsafe { std::mem::transmute(*self) } + } + + // NOTE: Checked `from_bits` is non-trivial to create and would likely + // require referencing `Entity` and `Relation` which we were able + // to avoid so far. Not implementing it for now until necessary. + + #[inline] + #[must_use] + pub unsafe fn from_bits_unchecked(bits: u64) -> Self { + std::mem::transmute(bits) + } +} + +impl PartialEq for Component { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.to_bits() == other.to_bits() + } +} + +impl Ord for Component { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.to_bits().cmp(&other.to_bits()) + } +} + +impl PartialOrd for Component { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.to_bits().partial_cmp(&other.to_bits()) + } +} + +impl std::hash::Hash for Component { + #[inline] + fn hash(&self, state: &mut H) { + self.to_bits().hash(state); + } +} + +impl Flags { + // NOTE: When changing this be sure to update documentation below. + pub(crate) const MASK: u32 = 0xF0000000; + + /// Extracts only the flags from the specified `u32`. + /// No sanity checking to see if it only has known bits set. + #[inline] + #[must_use] + pub unsafe fn from_bits_masked(bits: u32) -> Self { + Self::from_bits_retain(bits & Self::MASK) + } + + /// Extracts a `u32` into a 28-bit value (stored as a `u32`) and `Flags`. + /// Returns `None` if the resulting `Flags` would have unknown bits set. + #[must_use] + pub fn unpack(bits: u32) -> Option<(u32, Self)> { + let flags = Self::from_bits(bits & Self::MASK)?; + let value = bits & !Self::MASK; + Some((value, flags)) + } + + /// Packs a 28-bit value (stored as a `u32`) and `Flags` into a `u32`. + /// Returns `None` if `value` has bits set outside of the valid range. + #[must_use] + pub fn pack(value: u32, flags: Self) -> Option { + if value & Self::MASK == 0 { + // SAFETY: Manually checked if value does not have flag bits set. + Some(unsafe { Self::pack_unchecked(value, flags) }) + } else { + None + } + } + + /// Packs a 28-bit value (stored as a `u32`) and `Flags` into a `u32`. + /// No sanity checking to see if `value` has bits set outside of the valid range. + #[must_use] + pub unsafe fn pack_unchecked(value: u32, flags: Self) -> u32 { + value | flags.bits() + } +} diff --git a/src/ecs/entity.rs b/src/ecs/entity.rs new file mode 100644 index 0000000..c07fc1e --- /dev/null +++ b/src/ecs/entity.rs @@ -0,0 +1,272 @@ +use super::component::{Component, Flags}; +use std::num::NonZeroU32; + +/// Entities are plain identifiers that represent an object within a [`World`]. +/// +/// In a world, entities can be created and destroyed, and [`Component`]s can +/// be associated with alive entities, which describe the nature, state or +/// capabilities of the entity as well as its [`Relation`]s to other entities. +/// +/// Entities are [`Component`]s themselves. For example an `IsFrozen` entity +/// added to the entity `Alice` means that "Alice is frozen in place". Or a +/// `Position` entity added to her would mean "Alice has a Position". In this +/// case, `Position` is a so-called "value entity" because it holds a value, +/// and `IsFrozen` is a "tag entity" because it does not. +/// +/// [`World`]: super::world::World +/// [`Relation`]: super::relation::Relation +#[repr(C)] +#[derive(Copy, Clone, Eq, Debug)] +pub struct Entity { + id: EntityId, + gen: Generation, +} + +/// Represents the raw ID of an [`Entity`]. +/// +/// These IDs can be reused within a [`World`], but only one +/// alive entity with a specific ID may exist at at any time. +/// +/// [`World`]: super::world::World +pub type EntityId = NonZeroU32; + +/// Represents the generation of an [`Entity`]. +/// +/// When an entity dies, its `generation` is increased. This way, once the +/// [`EntityId`] is recycled to represent a new entity, you can differenciate +/// it from the previous entity, even though their `id`s are the same. +/// +/// Note that generation is not guaranteed to only be increasing, since when +/// dealing with very long running applications it can could potentially wrap +/// back around to 0. This is also the reason why this type does not implement +/// the `Ord` trait. +/// +/// Even though it's unlikely that any once dead [`Entity`] value would stay +/// around long enough to suddenly be considered alive again, avoid storing +/// them in components and instead rely on [`Relation`]s and their automatic +/// clean-up functionalty. +/// +/// [`Relation`]: super::relation::Relation +#[repr(transparent)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub struct Generation(u32); + +impl Entity { + #[inline] + #[must_use] + pub fn new(id: EntityId, gen: Generation) -> Self { + Self { id, gen } + } + + #[must_use] + pub fn new_checked(id: u32, gen: u32) -> Option { + let id = EntityId::new(id)?; + let gen = Generation::new(gen)?; + Some(Entity::new(id, gen)) + } + + #[inline] + #[must_use] + pub const unsafe fn new_unchecked(id: u32, gen: u32) -> Self { + let id = EntityId::new_unchecked(id); + let gen = Generation::new_unchecked(gen); + Self { id, gen } + } + + #[inline] + #[must_use] + pub fn id(&self) -> EntityId { + self.id + } + + #[inline] + #[must_use] + pub fn generation(&self) -> Generation { + self.gen + } + + #[must_use] + pub fn inc_generation(&self) -> Entity { + Self::new(self.id, self.gen.inc()) + } + + #[inline] + #[must_use] + pub fn to_bits(&self) -> u64 { + // SAFETY: Any representation of bits is a valid u64. + unsafe { std::mem::transmute(*self) } + } + + #[inline] + #[must_use] + pub fn from_bits(bits: u64) -> Option { + let id = bits as u32; + let gen = (bits >> 32) as u32; + Self::new_checked(id, gen) + } + + #[inline] + #[must_use] + pub unsafe fn from_bits_unchecked(bits: u64) -> Self { + std::mem::transmute(bits) + } +} + +impl TryFrom for Entity { + type Error = (); + #[inline] + fn try_from(component: Component) -> Result { + Self::from_bits(component.to_bits()).ok_or(()) + } +} + +impl From for Component { + #[inline] + fn from(entity: Entity) -> Self { + // SAFETY: A valid entity is always a valid component. + unsafe { Component::from_bits_unchecked(entity.to_bits()) } + } +} + +impl PartialEq for Entity { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.to_bits() == other.to_bits() + } +} + +impl Ord for Entity { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.to_bits().cmp(&other.to_bits()) + } +} + +impl PartialOrd for Entity { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.to_bits().partial_cmp(&other.to_bits()) + } +} + +impl std::hash::Hash for Entity { + #[inline] + fn hash(&self, state: &mut H) { + self.to_bits().hash(state); + } +} + +impl Generation { + pub(crate) const MASK: u32 = !Flags::MASK; + pub const MIN: Generation = unsafe { Self::new_unchecked(0) }; + pub const MAX: Generation = unsafe { Self::new_unchecked(Self::MASK) }; + + #[must_use] + pub fn new(gen: u32) -> Option { + if Self::is_valid(gen) { + Some(Self(gen)) + } else { + None + } + } + + #[inline] + #[must_use] + pub const unsafe fn new_unchecked(gen: u32) -> Self { + Self(gen) + } + + #[inline] + #[must_use] + pub fn inc(&self) -> Self { + // SAFETY: Manually ensures that value is in range. Wraps on overflow. + unsafe { Self::new_unchecked((self.0 + 1) & Self::MASK) } + } + + #[inline] + #[must_use] + pub fn is_valid(gen: u32) -> bool { + gen & !Self::MASK == 0 + } + + #[inline] + #[must_use] + pub fn get(&self) -> u32 { + self.0 + } +} + +impl TryFrom for Generation { + type Error = (); + #[inline] + fn try_from(gen: u32) -> Result { + Self::new(gen).ok_or(()) + } +} + +impl From for u32 { + #[inline] + fn from(gen: Generation) -> Self { + gen.get() + } +} + +impl std::fmt::Display for Generation { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn entity_new() { + let id = EntityId::new(100).unwrap(); + let gen = Generation::new(20).unwrap(); + let entity = Entity::new(id, gen); + assert_eq!((100, 20), (entity.id().get(), entity.generation().get())); + } + + #[test] + fn entity_new_checked() { + let valid = Entity::new_checked(100, 20); + assert!(valid.is_some()); + + let invalid_id = Entity::new_checked(0, 20); + let invalid_generation = Entity::new_checked(100, 0x1000_0000); + assert!(invalid_id.is_none()); + assert!(invalid_generation.is_none()); + } + + #[test] + fn entity_inc_generation() { + let old = Entity::new_checked(100, 20).unwrap(); + let new = old.inc_generation().inc_generation(); + assert_eq!(22, new.generation().get()); + + // Once generation hits the maximum value, it wraps around. + let really_high_gen = Entity::new(EntityId::MIN, Generation::MAX); + let wrapped_around_gen = really_high_gen.inc_generation(); + assert_eq!(0, wrapped_around_gen.generation().get()); + // Also ensure no other bits have been touched. + assert_eq!(1, wrapped_around_gen.to_bits()); + } + + #[test] + fn entity_to_and_from_bits() { + let entity = Entity::new_checked(100, 20).unwrap(); + let bits = entity.to_bits(); + + assert_eq!(0x0_0000014_00000064, bits); + // ^^^^^^^^- id (32 bits) + // ^^^^^^^---------- generation (28 bits) + // ^------------------ flags (4 bits) + + // Converting back to bits should yield the same entity value. + let entity_from_bits = Entity::from_bits(bits).unwrap(); + assert_eq!(entity, entity_from_bits); + } +} diff --git a/src/ecs/entity_index.rs b/src/ecs/entity_index.rs new file mode 100644 index 0000000..f77d67c --- /dev/null +++ b/src/ecs/entity_index.rs @@ -0,0 +1,303 @@ +use thiserror::Error; + +use super::entity::{Entity, EntityId, Generation}; + +/// Internal data structure that keeps track of alive [entities](Entity), +/// generation count, and is used to create, destroy and recycle entities. +/// +/// Each alive entity is also associated with a custom value of type `T` that +/// is used to store its [`Archetype`] and the `row` into a [`Archetable`] in +/// which the entity's components are stored. This is also the canonical way to +/// look up an entity by its [`EntityId`], or find out which components it has +/// to potentially access them. +/// +/// # Example +/// +/// ``` +/// use gaemstone::ecs::entity_index::{EntityIndex, DestroyError}; +/// use gaemstone::ecs::entity::{Entity, Generation}; +/// +/// let mut entity_index = EntityIndex::::new(); +/// +/// let entity_a = entity_index.create().entity; +/// // Update the data associated with the entity. +/// *entity_index.get_mut(entity_a.id()).unwrap().data = 32; +/// // Simpler way to update the data after creation. +/// let entity_b = entity_index.create().update(64); +/// +/// assert_eq!(2, entity_index.alive_count()); +/// assert_eq!(Entity::new_checked(1, 0).unwrap(), entity_a); +/// assert_eq!(Entity::new_checked(2, 0).unwrap(), entity_b); +/// assert_eq!(32, entity_index.get(entity_a.id()).unwrap().data); +/// assert_eq!(64, entity_index.get(entity_b.id()).unwrap().data); +/// +/// // Destroying an entity returns the associated value. +/// assert_eq!(Ok(64), entity_index.destroy(entity_b)); +/// assert_eq!(1, entity_index.alive_count()); +/// +/// // You can also destroy by `EntityId`, ignoring the generation. +/// assert_eq!(Ok(32), entity_index.destroy_at(entity_a.id())); +/// assert_eq!(0, entity_index.alive_count()); +/// +/// // Dead entities automatically get recycled. +/// let entity_c = entity_index.create().entity; +/// assert_eq!(entity_a.id(), entity_c.id()); +/// assert_eq!(entity_a.generation().inc(), entity_c.generation()); +/// +/// // Attempting to destroy a dead entity (even if recycled) returns an error. +/// let mismatch = DestroyError::GenerationMismatch { found: entity_c, expected: entity_a.generation() }; +/// assert_eq!(Err(mismatch), entity_index.destroy(entity_a)); +/// ``` +/// +/// [`Archetype`]: super::archetype::Archetype +/// [`Archetable`]: super::archetable::Archetable +pub struct EntityIndex { + dense: Vec, + alive_count: usize, + sparse: Vec>, // TODO: Use a paginated data structure. +} + +impl EntityIndex { + #[must_use] + pub fn new() -> Self { + Self { + dense: Vec::new(), + alive_count: 0, + sparse: Vec::new(), + } + } + + /// Returns the number of currently alive entities. + #[inline] + #[must_use] + pub fn alive_count(&self) -> usize { + self.alive_count + } + + /// Returns a slice of the currently alive entities. + #[inline] + #[must_use] + pub fn entities(&self) -> &[Entity] { + &self.dense[0..self.alive_count] + } + + /// Returns whether the specified [`Entity`] is alive. + #[must_use] + pub fn is_alive(&self, entity: Entity) -> bool { + self.sparse + .get((entity.id().get() - 1) as usize) + .filter(|entry| entry.state == SparseEntryState::Alive) + .map(|entry| self.dense[entry.dense_index]) + .map(|found| found.generation() == entity.generation()) + .unwrap_or(false) + } + + /// Returns whether an entity with the specified [`EntityId`] is alive. + #[must_use] + pub fn is_alive_at(&self, id: EntityId) -> bool { + self.sparse + .get((id.get() - 1) as usize) + .map(|entry| entry.state == SparseEntryState::Alive) + .unwrap_or(false) + } + + /// Attempts to get the [`Entry`] for the alive entity with the specified + /// [`EntityId`], which allows for inspecting the [`Entity`] and the data + /// associated with it. + #[must_use] + pub fn get(&self, id: EntityId) -> Option> { + let sparse_index = (id.get() - 1) as usize; + self.sparse.get(sparse_index).map(|entry| Entry { + entity: self.dense[entry.dense_index], + data: entry.data, + }) + } + + /// Attempts to get an [`EntryMut`] for the alive entity with the specified + /// [`EntityId`], which allows for inspecting the [`Entity`], and mutable + /// access to the data associated with it. + #[must_use] + pub fn get_mut(&mut self, id: EntityId) -> Option> { + let sparse_index = (id.get() - 1) as usize; + self.sparse.get_mut(sparse_index).map(|entry| EntryMut { + entity: self.dense[entry.dense_index], + data: &mut entry.data, + }) + } + + /// Creates a new [`Entity`], by either reusing an [`EntityId`] or using a + /// fresh one, and returns an [`EntryMut`] that allows for inspecting the + /// `Entity` value created, as well as mutable access to its data. + #[inline] + pub fn create(&mut self) -> EntryMut { + self.lookup_or_create(self.next_free_entity_id()) + } + + /// Looks up an [`Entity`] with the specified [`EntityId`], creating it if + /// necessary, and returns an [`EntryMut`] that allows for inspecting the + /// `Entity` value, as well as mutable access to its data. + pub fn lookup_or_create(&mut self, id: EntityId) -> EntryMut { + let sparse_index = (id.get() - 1) as usize; + if sparse_index >= self.sparse.len() { + // Grow `sparse` vector to be able to fit `id`. + self.sparse.resize(sparse_index + 1, Default::default()); + } + + let entry = &mut self.sparse[sparse_index]; + if entry.state == SparseEntryState::Unused { + // `dense_index` is unset, `dense` needs to store a new entity. + entry.dense_index = self.dense.len(); + // Create a fresh entity with generation 0. + let entity = Entity::new(id, Generation::MIN); + self.dense.push(entity); + } + + // Make some copies so we satisfy the borrow checker. + let dense_index = entry.dense_index; + let entity = self.dense[dense_index]; + + if entry.state != SparseEntryState::Alive { + entry.state = SparseEntryState::Alive; + // Swap the created / recycled entity into the alive entity range. + self.swap_dense(dense_index, self.alive_count); + self.alive_count += 1; + } + + // Data should be `Default` even after recycled, + // as it gets reset when the entity is destroyed. + let data = &mut self.sparse[sparse_index].data; + EntryMut { entity, data } + } + + /// Destroys the specified alive entity. + #[inline] + pub fn destroy(&mut self, entity: Entity) -> Result { + self.destroy_at_options(entity.id(), Some(entity.generation())) + } + + /// Destroys the alive entity with the specified [EntityId]. + #[inline] + pub fn destroy_at(&mut self, id: EntityId) -> Result { + self.destroy_at_options(id, None) + } +} + +pub struct Entry { + pub entity: Entity, + pub data: T, +} + +pub struct EntryMut<'a, T> { + pub entity: Entity, + pub data: &'a mut T, +} + +impl EntryMut<'_, T> { + /// Updates the entry's `data`, returning its `entity`, consuming it in the process. + pub fn update(self, value: T) -> Entity { + *self.data = value; + self.entity + } +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum DestroyError { + #[error("entity with id `{id}` not found")] + EntityNotFound { id: EntityId }, + #[error("entity with id `{id}` not alive")] + EntityNotAlive { id: EntityId }, + #[error("entity `{found:?}` found, expected generation `{expected}`")] + GenerationMismatch { found: Entity, expected: Generation }, +} + +impl EntityIndex { + fn destroy_at_options( + &mut self, + id: EntityId, + gen: Option, + ) -> Result { + let sparse_index = (id.get() - 1) as usize; + let Some(entry) = self.sparse.get_mut(sparse_index) else { + return Err(DestroyError::EntityNotFound { id }); + }; + + match entry.state { + SparseEntryState::Unused => return Err(DestroyError::EntityNotFound { id }), + SparseEntryState::Dead => return Err(DestroyError::EntityNotAlive { id }), + _ => {} + }; + + let dense_index = entry.dense_index; + let entity = &mut self.dense[dense_index]; + + if let Some(expected) = gen { + if expected != entity.generation() { + return Err(DestroyError::GenerationMismatch { + found: *entity, + expected, + }); + } + } + + let previous = entry.data; + entry.state = SparseEntryState::Dead; + entry.data = Default::default(); + + // Increase the generation of the destroyed entity. + *entity = entity.inc_generation(); + // Swap the destroyed entity into the recycled entity range. + self.swap_dense(dense_index, self.alive_count - 1); + + self.alive_count -= 1; + Ok(previous) + } + + /// Returns the next available entity ID, either using that + /// of a dead entity to be recycled a completely fresh one. + fn next_free_entity_id(&self) -> EntityId { + if self.dense.len() > self.alive_count { + self.dense[self.alive_count].id() + } else { + let entity_id = (self.alive_count + 1) as u32; + // SAFETY: ID is guaranteed to be greater than 0. + unsafe { EntityId::new_unchecked(entity_id) } + } + } + + fn swap_dense(&mut self, dense_index_a: usize, dense_index_b: usize) { + if dense_index_a == dense_index_b { + return; + } + + // This assumes the dense indices are valid. + let entity_a = self.dense[dense_index_a]; + let entity_b = self.dense[dense_index_b]; + let sparse_index_a = (entity_a.id().get() - 1) as usize; + let sparse_index_b = (entity_b.id().get() - 1) as usize; + + // Swap entries in `dense` vector. + self.dense.swap(dense_index_a, dense_index_b); + + // Swap indices pointing to `dense` in `sparse` vector. + self.sparse[sparse_index_a].dense_index = dense_index_b; + self.sparse[sparse_index_b].dense_index = dense_index_a; + } +} + +#[derive(Copy, Clone, Default)] +struct SparseEntry { + state: SparseEntryState, + dense_index: usize, + data: T, +} + +#[derive(Copy, Clone, Default, Eq, PartialEq)] +enum SparseEntryState { + /// Entry hasn't been populated yet. + #[default] + Unused, + /// Entry contains a dead [`Entity`] that may be recycled. + Dead, + /// Entry contains an alive [`Entity`]. + Alive, +} diff --git a/src/ecs/mod.rs b/src/ecs/mod.rs new file mode 100644 index 0000000..925fd7b --- /dev/null +++ b/src/ecs/mod.rs @@ -0,0 +1,17 @@ +pub mod archetable; +pub mod archetype; +pub mod component; +pub mod entity; +pub mod entity_index; +pub mod relation; +pub mod world; + +pub(crate) mod utility; + +pub mod prelude { + pub use super::archetype::Archetype; + pub use super::component::Component; + pub use super::entity::Entity; + pub use super::relation::Relation; + pub use super::world::World; +} diff --git a/src/ecs/relation.rs b/src/ecs/relation.rs new file mode 100644 index 0000000..cba2af6 --- /dev/null +++ b/src/ecs/relation.rs @@ -0,0 +1,135 @@ +use super::component::{Component, Flags}; +use super::entity::EntityId; + +/// Represents a relation of one [`Entity`] to another. +/// +/// Relations are made up like so: +/// - **source**: The entity this relation is added to as a component. +/// - **target**: The target entity of this relation. +/// - **kind**: An entity representing the kind of relation. +/// +/// For example, to describe the relation "Alice likes Bob", you would need +/// the entities `Alice` (source) and `Bob` (target), but also a third entity +/// `Likes` (kind). You then add the relation `(Likes, Bob)` to `Alice`. +/// +/// Note that relations by default are neither symmetric (so `Bob` might not +/// like `Alice` back) nor exclusive (so `Alice` might also like `Chloe`), +/// however this behavior can be adjusted by adding the `Symmetric` and +/// `Exclusive` tags to the relation kind entity. +/// +/// Being a [`Component`], relations can hold values when added to entities, +/// making them a so-called "value relation". The opposite, a relation without +/// value, is called a "tag relation". See also the documentation for the +/// [`Component`] type. +/// **TODO:** Describe the mechanism through which relations decide whether to +/// use either `kind` or `target` for the type of the value held. +/// (That is, once such a mechanism actually exists.) +/// +/// [`Entity`]: super::entity::Entity +#[repr(C)] +#[derive(Copy, Clone, Eq)] +pub struct Relation { + target: EntityId, + high: u32, +} + +impl Relation { + #[must_use] + pub fn new(kind: EntityId, target: EntityId) -> Option { + let high = Flags::pack(kind.get(), Flags::RELATION)?; + Some(Self { target, high }) + } + + #[must_use] + pub fn new_checked(kind: u32, target: u32) -> Option { + let high = Flags::pack(kind, Flags::RELATION)?; + let target = EntityId::new(target)?; + Some(Self { target, high }) + } + + #[inline] + #[must_use] + pub unsafe fn new_unchecked(kind: u32, target: u32) -> Self { + let high = Flags::pack_unchecked(kind, Flags::RELATION); + let target = EntityId::new_unchecked(target); + Self { target, high } + } + + #[inline] + #[must_use] + pub fn kind(&self) -> EntityId { + // SAFETY: Valid relations will always have valid relation kind. + unsafe { EntityId::new_unchecked(self.high & !Flags::MASK) } + } + + #[inline] + #[must_use] + pub fn target(&self) -> EntityId { + self.target + } + + #[inline] + #[must_use] + pub fn to_bits(&self) -> u64 { + // SAFETY: Any representation of bits is a valid u64. + unsafe { std::mem::transmute(*self) } + } + + #[inline] + #[must_use] + pub fn from_bits(bits: u64) -> Option { + let kind = (bits >> 32) as u32; + let target = bits as u32; + Self::new_checked(kind, target) + } + + #[inline] + #[must_use] + pub unsafe fn from_bits_unchecked(bits: u64) -> Self { + std::mem::transmute(bits) + } +} + +impl TryFrom for Relation { + type Error = (); + #[inline] + fn try_from(component: Component) -> Result { + Self::from_bits(component.to_bits()).ok_or(()) + } +} + +impl From for Component { + #[inline] + fn from(relation: Relation) -> Self { + // SAFETY: A valid relation is always a valid component. + unsafe { Component::from_bits_unchecked(relation.to_bits()) } + } +} + +impl PartialEq for Relation { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.to_bits() == other.to_bits() + } +} + +impl Ord for Relation { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.to_bits().cmp(&other.to_bits()) + } +} + +impl PartialOrd for Relation { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.to_bits().partial_cmp(&other.to_bits()) + } +} + +impl std::hash::Hash for Relation { + #[inline] + fn hash(&self, state: &mut H) { + self.to_bits().hash(state); + } +} diff --git a/src/ecs/utility/mod.rs b/src/ecs/utility/mod.rs new file mode 100644 index 0000000..ff9aaf9 --- /dev/null +++ b/src/ecs/utility/mod.rs @@ -0,0 +1,3 @@ +mod raw_component_vec; + +pub use raw_component_vec::RawComponentVec; diff --git a/src/ecs/utility/raw_component_vec.rs b/src/ecs/utility/raw_component_vec.rs new file mode 100644 index 0000000..364244a --- /dev/null +++ b/src/ecs/utility/raw_component_vec.rs @@ -0,0 +1,68 @@ +use aligned_vec::{AVec, RuntimeAlign}; +use std::alloc::Layout; + +#[derive(Clone, Debug)] +pub struct RawComponentVec { + layout: Layout, + vec: AVec, +} + +impl RawComponentVec { + #[must_use] + pub fn new(layout: Layout) -> Self { + assert_ne!(0, layout.size()); + let vec = AVec::new(layout.align()); + Self { layout, vec } + } + + #[inline] + #[must_use] + pub fn layout(&self) -> Layout { + self.layout + } + + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.vec.len() / self.layout.size() + } + + pub fn resize(&mut self, new_len: usize) { + self.vec.resize(new_len * self.layout.size(), 0); + } + + pub fn swap_remove(&mut self, index: usize) { + let size = self.layout.size(); + let vec_index = index * size; + let vec_len = self.vec.len(); + assert!(vec_index >= vec_len); + let new_vec_len = vec_len - size; + unsafe { + let base_ptr = self.vec.as_mut_ptr(); + std::ptr::copy(base_ptr.add(new_vec_len), base_ptr.add(vec_index), size); + self.vec.set_len(new_vec_len); + } + } + + pub unsafe fn as_typed_slice(&self) -> &[T] { + // SAFETY: Assuming `T` is correct, pointer should be aligned correctly. + unsafe { core::slice::from_raw_parts(self.as_ptr() as *const T, self.len()) } + } + + pub unsafe fn as_typed_slice_mut(&mut self) -> &mut [T] { + // SAFETY: Assuming `T` is correct, pointer should be aligned correctly. + unsafe { core::slice::from_raw_parts_mut(self.as_ptr() as *mut T, self.len()) } + } + + #[inline] + #[must_use] + pub fn as_ptr(&self) -> *const u8 { + self.vec.as_ptr() + } + + #[inline] + #[must_use] + pub fn as_mut_ptr(&mut self) -> *mut u8 { + self.vec.as_mut_ptr() + } +} diff --git a/src/ecs/world.rs b/src/ecs/world.rs new file mode 100644 index 0000000..3f8677a --- /dev/null +++ b/src/ecs/world.rs @@ -0,0 +1,74 @@ +use std::alloc::Layout; +use std::any::TypeId; +use std::collections::HashMap; + +use super::archetable::Archetable; +use super::entity::{Entity, EntityId}; +use super::entity_index::EntityIndex; + +pub struct World { + entity_index: EntityIndex, + empty_archetable: Archetable, + type_id_lookup: HashMap, + // Temporary. To be removed once we can build and use Archetables that hold this information. + component_lookup: HashMap, +} + +impl World { + #[must_use] + pub fn new() -> Self { + let mut world = Self { + entity_index: EntityIndex::new(), + empty_archetable: Archetable::new_empty(), + type_id_lookup: HashMap::new(), + component_lookup: HashMap::new(), + }; + + world.register_component::(); + world.register_component::(); + + world + } + + #[must_use] + pub fn lookup_alive(&self, id: EntityId) -> Option { + self.entity_index.get(id).map(|e| e.entity) + } + + #[must_use] + pub fn lookup_by_type(&self) -> Option { + self.type_id_lookup.get(&TypeId::of::()).copied() + } + + #[must_use] + pub fn lookup_component_info(&self, entity: Entity) -> Option<(Layout, TypeId)> { + self.component_lookup.get(&entity).copied() + } + + pub fn register_component(&mut self) -> Entity { + assert_ne!(0, size_of::()); + let entity = self.create(); + let component_info = (Layout::new::(), TypeId::of::()); + self.component_lookup.insert(entity, component_info); + entity + } + + #[must_use] + pub fn create(&mut self) -> Entity { + let entry = self.entity_index.create(); + let row = self.empty_archetable.push(entry.entity); + entry.update(row) + } + + pub fn destroy(&mut self, entity: Entity) -> bool { + if let Ok(row) = self.entity_index.destroy(entity) { + if let Some(swapped) = self.empty_archetable.swap_remove(row) { + // Assume that `swapped` we got from the archetype table is alive. + self.entity_index.get(swapped.id()).unwrap().data = row; + } + true + } else { + false + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eae413c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod ecs;