commit
5ace8a2b7b
14 changed files with 1190 additions and 0 deletions
@ -0,0 +1,2 @@ |
||||
/target/ |
||||
/Cargo.lock |
@ -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" |
@ -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 |
@ -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<ComponentInfo>, |
||||
columns: Vec<Column>, |
||||
entities: Vec<Entity>, |
||||
} |
||||
|
||||
pub type EntityRow = usize; |
||||
|
||||
struct ComponentInfo { |
||||
entity: Entity, |
||||
type_id: Option<TypeId>, |
||||
column_index: Option<usize>, |
||||
} |
||||
|
||||
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<Entity> { |
||||
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<T: 'static>(&self) -> Option<&ComponentInfo> { |
||||
let type_id = Some(TypeId::of::<T>()); |
||||
self.components.iter().find(|e| e.type_id == type_id) |
||||
} |
||||
|
||||
#[must_use] |
||||
pub fn get_component_column<T: 'static>(&self) -> Option<&[T]> { |
||||
let info = self.find_component_info::<T>()?; |
||||
let column = &self.columns[info.column_index?]; |
||||
Some(unsafe { column.data.as_typed_slice::<T>() }) |
||||
} |
||||
|
||||
#[must_use] |
||||
pub fn get_component_column_mut<T: 'static>(&mut self) -> Option<&mut [T]> { |
||||
let info = self.find_component_info::<T>()?; |
||||
let column_index = info.column_index?; // Avoid borrow.
|
||||
let column = &mut self.columns[column_index]; |
||||
Some(unsafe { column.data.as_typed_slice_mut::<T>() }) |
||||
} |
||||
} |
@ -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<Component>, |
||||
} |
||||
|
||||
impl Archetype { |
||||
pub const EMPTY: Archetype = Self { |
||||
components: Vec::new(), |
||||
}; |
||||
|
||||
#[must_use] |
||||
pub fn new(iter: impl IntoIterator<Item = impl Into<Component>>) -> 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<Component>) -> bool { |
||||
self.components.binary_search(&component.into()).is_ok() |
||||
} |
||||
} |
||||
|
||||
impl<T> FromIterator<T> for Archetype |
||||
where |
||||
T: Into<Component>, |
||||
{ |
||||
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self { |
||||
Self::new(iter) |
||||
} |
||||
} |
@ -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<std::cmp::Ordering> { |
||||
self.to_bits().partial_cmp(&other.to_bits()) |
||||
} |
||||
} |
||||
|
||||
impl std::hash::Hash for Component { |
||||
#[inline] |
||||
fn hash<H: std::hash::Hasher>(&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<u32> { |
||||
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() |
||||
} |
||||
} |
@ -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<Self> { |
||||
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<Self> { |
||||
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<Component> for Entity { |
||||
type Error = (); |
||||
#[inline] |
||||
fn try_from(component: Component) -> Result<Self, Self::Error> { |
||||
Self::from_bits(component.to_bits()).ok_or(()) |
||||
} |
||||
} |
||||
|
||||
impl From<Entity> 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<std::cmp::Ordering> { |
||||
self.to_bits().partial_cmp(&other.to_bits()) |
||||
} |
||||
} |
||||
|
||||
impl std::hash::Hash for Entity { |
||||
#[inline] |
||||
fn hash<H: std::hash::Hasher>(&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<Self> { |
||||
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<u32> for Generation { |
||||
type Error = (); |
||||
#[inline] |
||||
fn try_from(gen: u32) -> Result<Self, Self::Error> { |
||||
Self::new(gen).ok_or(()) |
||||
} |
||||
} |
||||
|
||||
impl From<Generation> 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); |
||||
} |
||||
} |
@ -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::<usize>::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<T: Copy + Default> { |
||||
dense: Vec<Entity>, |
||||
alive_count: usize, |
||||
sparse: Vec<SparseEntry<T>>, // TODO: Use a paginated data structure.
|
||||
} |
||||
|
||||
impl<T: Copy + Default> EntityIndex<T> { |
||||
#[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<Entry<T>> { |
||||
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<EntryMut<T>> { |
||||
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<T> { |
||||
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<T> { |
||||
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<T, DestroyError> { |
||||
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<T, DestroyError> { |
||||
self.destroy_at_options(id, None) |
||||
} |
||||
} |
||||
|
||||
pub struct Entry<T> { |
||||
pub entity: Entity, |
||||
pub data: T, |
||||
} |
||||
|
||||
pub struct EntryMut<'a, T> { |
||||
pub entity: Entity, |
||||
pub data: &'a mut T, |
||||
} |
||||
|
||||
impl<T> 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<T: Copy + Default> EntityIndex<T> { |
||||
fn destroy_at_options( |
||||
&mut self, |
||||
id: EntityId, |
||||
gen: Option<Generation>, |
||||
) -> Result<T, DestroyError> { |
||||
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<T> { |
||||
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, |
||||
} |
@ -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; |
||||
} |
@ -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<Self> { |
||||
let high = Flags::pack(kind.get(), Flags::RELATION)?; |
||||
Some(Self { target, high }) |
||||
} |
||||
|
||||
#[must_use] |
||||
pub fn new_checked(kind: u32, target: u32) -> Option<Self> { |
||||
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<Self> { |
||||
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<Component> for Relation { |
||||
type Error = (); |
||||
#[inline] |
||||
fn try_from(component: Component) -> Result<Self, Self::Error> { |
||||
Self::from_bits(component.to_bits()).ok_or(()) |
||||
} |
||||
} |
||||
|
||||
impl From<Relation> 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<std::cmp::Ordering> { |
||||
self.to_bits().partial_cmp(&other.to_bits()) |
||||
} |
||||
} |
||||
|
||||
impl std::hash::Hash for Relation { |
||||
#[inline] |
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
||||
self.to_bits().hash(state); |
||||
} |
||||
} |
@ -0,0 +1,3 @@ |
||||
mod raw_component_vec; |
||||
|
||||
pub use raw_component_vec::RawComponentVec; |
@ -0,0 +1,68 @@ |
||||
use aligned_vec::{AVec, RuntimeAlign}; |
||||
use std::alloc::Layout; |
||||
|
||||
#[derive(Clone, Debug)] |
||||
pub struct RawComponentVec { |
||||
layout: Layout, |
||||
vec: AVec<u8, RuntimeAlign>, |
||||
} |
||||
|
||||
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<T>(&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<T>(&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() |
||||
} |
||||
} |
@ -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<usize>, |
||||
empty_archetable: Archetable, |
||||
type_id_lookup: HashMap<TypeId, Entity>, |
||||
// Temporary. To be removed once we can build and use Archetables that hold this information.
|
||||
component_lookup: HashMap<Entity, (Layout, TypeId)>, |
||||
} |
||||
|
||||
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::<Layout>(); |
||||
world.register_component::<TypeId>(); |
||||
|
||||
world |
||||
} |
||||
|
||||
#[must_use] |
||||
pub fn lookup_alive(&self, id: EntityId) -> Option<Entity> { |
||||
self.entity_index.get(id).map(|e| e.entity) |
||||
} |
||||
|
||||
#[must_use] |
||||
pub fn lookup_by_type<T: 'static>(&self) -> Option<Entity> { |
||||
self.type_id_lookup.get(&TypeId::of::<T>()).copied() |
||||
} |
||||
|
||||
#[must_use] |
||||
pub fn lookup_component_info(&self, entity: Entity) -> Option<(Layout, TypeId)> { |
||||
self.component_lookup.get(&entity).copied() |
||||
} |
||||
|
||||
pub fn register_component<T: 'static>(&mut self) -> Entity { |
||||
assert_ne!(0, size_of::<T>()); |
||||
let entity = self.create(); |
||||
let component_info = (Layout::new::<T>(), TypeId::of::<T>()); |
||||
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 |
||||
} |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
pub mod ecs; |
Loading…
Reference in new issue