From de5c3ff1320ea0f0452afde4c1f42676d9eeab52 Mon Sep 17 00:00:00 2001 From: HampusM Date: Fri, 16 Feb 2024 19:58:53 +0100 Subject: feat: add entity component system library --- Cargo.lock | 19 +++-- Cargo.toml | 2 +- ecs/Cargo.toml | 7 ++ ecs/examples/simple.rs | 32 ++++++++ ecs/src/component.rs | 114 +++++++++++++++++++++++++++++ ecs/src/lib.rs | 194 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 361 insertions(+), 7 deletions(-) create mode 100644 ecs/Cargo.toml create mode 100644 ecs/examples/simple.rs create mode 100644 ecs/src/component.rs create mode 100644 ecs/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 93ec3d0..884fa3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,13 @@ dependencies = [ "quote", ] +[[package]] +name = "ecs" +version = "0.1.0" +dependencies = [ + "seq-macro", +] + [[package]] name = "engine" version = "0.1.0" @@ -360,18 +367,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.68" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -446,9 +453,9 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "syn" -version = "2.0.38" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9baf278..373c4ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [workspace] -members = ["glfw", "engine"] +members = ["glfw", "engine", "ecs"] [dependencies] engine = { path = "./engine", features = ["debug"] } diff --git a/ecs/Cargo.toml b/ecs/Cargo.toml new file mode 100644 index 0000000..cd25673 --- /dev/null +++ b/ecs/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ecs" +version = "0.1.0" +edition = "2021" + +[dependencies] +seq-macro = "0.3.5" diff --git a/ecs/examples/simple.rs b/ecs/examples/simple.rs new file mode 100644 index 0000000..b58d2ba --- /dev/null +++ b/ecs/examples/simple.rs @@ -0,0 +1,32 @@ +use ecs::{Query, World}; + +struct SomeData +{ + num: u64, +} + +fn say_hello(mut query: Query<(SomeData, String)>) +{ + for (data, text) in query.iter_mut() { + println!("Hello {}: {}", text, data.num); + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +enum Event +{ + Start, +} + +fn main() +{ + let mut world = World::::new(); + + world.register_system(Event::Start, say_hello); + + world.create_entity((SomeData { num: 987_654 }, "Yoo".to_string())); + + world.create_entity((SomeData { num: 345 }, "Haha".to_string())); + + world.emit(&Event::Start); +} diff --git a/ecs/src/component.rs b/ecs/src/component.rs new file mode 100644 index 0000000..4829050 --- /dev/null +++ b/ecs/src/component.rs @@ -0,0 +1,114 @@ +use std::any::{Any, TypeId}; +use std::fmt::Debug; + +use seq_macro::seq; + +pub trait Component: Any +{ + #[doc(hidden)] + fn as_any_mut(&mut self) -> &mut dyn Any; + + #[doc(hidden)] + fn as_any(&self) -> &dyn Any; +} + +impl Component for Value +{ + fn as_any_mut(&mut self) -> &mut dyn Any + { + self + } + + fn as_any(&self) -> &dyn Any + { + self + } +} + +impl dyn Component +{ + pub fn downcast_mut(&mut self) -> Option<&mut Real> + { + self.as_any_mut().downcast_mut() + } + + pub fn is(&self) -> bool + { + self.as_any().is::() + } +} + +impl Debug for dyn Component +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter.debug_struct("Component").finish_non_exhaustive() + } +} + +/// A sequence of components. +pub trait Sequence +{ + type MutRefs<'component> + where + Self: 'component; + + fn into_vec(self) -> Vec>; + + fn type_ids() -> Vec; + + fn from_components<'components>( + components: &'components mut [Box], + ) -> Self::MutRefs<'components>; +} + +macro_rules! inner { + ($c: tt) => { + seq!(I in 0..=$c { + impl<#(Comp~I: Component,)*> Sequence for (#(Comp~I,)*) { + type MutRefs<'component> = (#(&'component mut Comp~I,)*) + where Self: 'component; + + fn into_vec(self) -> Vec> { + Vec::from_iter([#(Box::new(self.I) as Box,)*]) + } + + fn type_ids() -> Vec { + vec![ + #( + TypeId::of::(), + )* + ] + } + + fn from_components<'components>( + components: &'components mut [Box], + ) -> Self::MutRefs<'components> + { + #( + let mut comp_~I = None; + )* + + for comp in components.iter_mut() { + #( + if comp.is::() { + comp_~I = Some(comp); + continue; + } + + )* + } + + + (#( + comp_~I.unwrap().downcast_mut::().unwrap(), + )*) + } + } + }); + }; +} + +seq!(C in 0..=64 { + inner!(C); +}); diff --git a/ecs/src/lib.rs b/ecs/src/lib.rs new file mode 100644 index 0000000..5b8942b --- /dev/null +++ b/ecs/src/lib.rs @@ -0,0 +1,194 @@ +#![deny(clippy::all, clippy::pedantic)] + +use std::any::TypeId; +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; +use std::hash::Hash; +use std::marker::PhantomData; +use std::slice::IterMut as SliceIterMut; + +use crate::component::{Component, Sequence as ComponentSequence}; + +pub mod component; + +#[derive(Debug)] +struct Entity +{ + components: Vec>, +} + +#[derive(Debug)] +pub struct World +{ + systems: Vec>, + events: HashMap>, + extra: WorldExtra, +} + +#[derive(Debug)] +struct WorldExtra +{ + entities: Vec, +} + +impl World +{ + #[must_use] + pub fn new() -> Self + { + Self { + systems: Vec::new(), + extra: WorldExtra { entities: Vec::new() }, + events: HashMap::new(), + } + } + + pub fn create_entity(&mut self, components: Comps) + where + Comps: ComponentSequence, + { + self.extra + .entities + .push(Entity { components: components.into_vec() }); + } + + pub fn register_system(&mut self, event: Event, system: System) + where + Event: Hash + PartialEq + Eq, + Comps: ComponentSequence + 'static, + { + self.systems.push(Box::new(system)); + + self.events + .entry(event) + .or_default() + .push(self.systems.len() - 1); + } + + /// Emits a event, running all systems listening to the event for each compatible + /// entity. + /// + /// # Panics + /// Will panic if a system has dissapeared. + pub fn emit(&mut self, event: &Event) + where + Event: Hash + PartialEq + Eq, + { + let Some(system_indices) = self.events.get(event).cloned() else { + return; + }; + + for system_index in system_indices { + let system = self.systems.get_mut(system_index).unwrap(); + + system.call(&mut self.extra); + } + } + + pub fn query(&mut self) -> Query + where + Comps: ComponentSequence, + { + Query::new(&mut self.extra) + } +} + +impl Default for World +{ + fn default() -> Self + { + Self::new() + } +} + +pub type System = fn(Query); + +trait AnySystem +{ + fn call(&self, world: &mut WorldExtra); +} + +impl AnySystem for System +where + Comps: ComponentSequence, +{ + fn call(&self, world: &mut WorldExtra) + { + self(Query::new(world)); + } +} + +impl Debug for dyn AnySystem +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter.debug_struct("AnySystem").finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub struct Query<'world, Comps> +{ + world: &'world mut WorldExtra, + comps_pd: PhantomData, +} + +impl<'world, Comps> Query<'world, Comps> +{ + fn new(world: &'world mut WorldExtra) -> Self + { + Self { world, comps_pd: PhantomData } + } +} + +impl<'world, Comps> Query<'world, Comps> +where + Comps: ComponentSequence, +{ + pub fn iter_mut(&mut self) -> QueryComponentIter + { + QueryComponentIter { + entity_iter: self.world.entities.iter_mut(), + component_type_ids: Comps::type_ids(), + comps_pd: PhantomData, + } + } +} + +pub struct QueryComponentIter<'world, Comps> +{ + entity_iter: SliceIterMut<'world, Entity>, + component_type_ids: Vec, + comps_pd: PhantomData, +} + +impl<'world, Comps> Iterator for QueryComponentIter<'world, Comps> +where + Comps: ComponentSequence + 'world, +{ + type Item = Comps::MutRefs<'world>; + + fn next(&mut self) -> Option + { + // TODO: This is a really dumb and slow way to do this. Refactor the world + // to store components in archetypes + let entity = + self.entity_iter.find(|entity| { + let entity_components: HashSet<_> = entity + .components + .iter() + .map(|component| component.as_ref().type_id()) + .collect(); + + if self.component_type_ids.iter().all(|component_type_id| { + entity_components.contains(component_type_id) + }) { + return true; + } + + false + })?; + + Some(Comps::from_components(&mut entity.components)) + } +} -- cgit v1.2.3-18-g5258