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