diff --git a/lyra-ecs/.gitignore b/lyra-ecs/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/lyra-ecs/.gitignore @@ -0,0 +1 @@ +/target diff --git a/lyra-ecs/.vscode/launch.json b/lyra-ecs/.vscode/launch.json new file mode 100644 index 0000000..d971c77 --- /dev/null +++ b/lyra-ecs/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'lyra-ecs'", + "cargo": { + "args": [ + "build", + "--bin=lyra-ecs", + "--package=lyra-ecs" + ], + "filter": { + "name": "lyra-ecs", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'lyra-ecs'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=lyra-ecs", + "--package=lyra-ecs" + ], + "filter": { + "name": "lyra-ecs", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/lyra-ecs/.vscode/settings.json b/lyra-ecs/.vscode/settings.json new file mode 100644 index 0000000..12b229e --- /dev/null +++ b/lyra-ecs/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix" +} \ No newline at end of file diff --git a/lyra-ecs/Cargo.lock b/lyra-ecs/Cargo.lock new file mode 100644 index 0000000..f9ca240 --- /dev/null +++ b/lyra-ecs/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "lyra-ecs" +version = "0.1.0" diff --git a/lyra-ecs/Cargo.toml b/lyra-ecs/Cargo.toml new file mode 100644 index 0000000..edc4d79 --- /dev/null +++ b/lyra-ecs/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "lyra-ecs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/lyra-ecs/LICENSE b/lyra-ecs/LICENSE new file mode 100644 index 0000000..dbd9e43 --- /dev/null +++ b/lyra-ecs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 SeanOMik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lyra-ecs/shell.nix b/lyra-ecs/shell.nix new file mode 100644 index 0000000..8008d63 --- /dev/null +++ b/lyra-ecs/shell.nix @@ -0,0 +1,21 @@ +{ pkgs ? import { } }: + +with pkgs; + +mkShell rec { + nativeBuildInputs = [ + pkg-config + openssl + wasm-pack + trunk + valgrind + heaptrack + mold + ]; + buildInputs = [ + udev alsa-lib vulkan-loader + xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr # To use the x11 feature + libxkbcommon wayland # To use the wayland feature + ]; + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; +} \ No newline at end of file diff --git a/lyra-ecs/src/archetype.rs b/lyra-ecs/src/archetype.rs new file mode 100644 index 0000000..7a9c16d --- /dev/null +++ b/lyra-ecs/src/archetype.rs @@ -0,0 +1,182 @@ +use std::any::{Any, TypeId}; + +use crate::{world::{Entity, ArchetypeEntityId}, bundle::Bundle}; + +pub trait ComponentColumn: Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn new_empty_column(&self) -> Box; + fn is_same_type(&self, column: &dyn ComponentColumn) -> bool; + fn len(&self) -> usize; + fn append(&mut self, column: &mut dyn ComponentColumn); + + fn component_type_id(&self) -> TypeId; + // used for debugging + fn component_type_name(&self) -> String; +} + +impl ComponentColumn for Vec { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn new_empty_column(&self) -> Box { + Box::new(Vec::::new()) + } + + fn is_same_type(&self, column: &dyn ComponentColumn) -> bool { + column.as_any().downcast_ref::().is_some() + } + + fn len(&self) -> usize { + Vec::len(self) + } + + fn append(&mut self, column: &mut dyn ComponentColumn) { + let column: &mut Self = column.as_any_mut().downcast_mut() + .expect("Attempt at appending an different column type!"); + + self.append(column); + } + + fn component_type_id(&self) -> TypeId { + self.first().unwrap().type_id() + } + + fn component_type_name(&self) -> String { + //self.first().unwrap().type_id() + std::any::type_name::().to_string() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct ArchetypeId(pub u64); + +impl ArchetypeId { + /// Increments the id and returns a new id with the value it was before incrementing. + pub(crate) fn increment(&mut self) -> Self { + let v = self.0; + self.0 += 1; + + ArchetypeId(v) + } +} + +pub struct Archetype { + pub(crate) id: ArchetypeId, + entities: Vec, + pub(crate) columns: Vec>, +} + +impl Archetype { + /// Create a new archetype from another archetype and add a column + pub fn new_archetype_add(new_id: ArchetypeId, archetype: &Archetype) -> Archetype { + let mut columns: Vec<_> = archetype + .columns + .iter() + .map(|c| c.new_empty_column()) + .collect(); + + assert!(columns + .iter() + .find(|column| column.as_any().is::>()) + .is_none()); + columns.push(Box::new(Vec::::new())); + + Archetype { + id: new_id, + entities: Vec::new(), + columns, + } + } + + /// Create a new archetype from another archetype and remove a column + pub fn new_archetype_remove(new_id: ArchetypeId, archetype: &Archetype) -> Archetype { + let mut columns: Vec<_> = archetype + .columns + .iter() + .map(|c| c.new_empty_column()) + .collect(); + + let idx = columns + .iter() + .position(|column| column.as_any().is::>()) + .unwrap(); + columns.remove(idx); + + Archetype { + id: new_id, + entities: Vec::new(), + columns, + } + } + + pub fn from_columns(new_id: ArchetypeId, columns: Vec>) -> Archetype { + Archetype { + id: new_id, + entities: Vec::new(), + columns, + } + } + + pub fn get_component_mut(&mut self, entity: ArchetypeEntityId) -> Option<&mut T> { + for col in self.columns.iter_mut() { + if col.as_any().is::>() { + let components: &mut Vec = col.as_any_mut().downcast_mut().unwrap(); + + return components.get_mut(entity.0 as usize); + } + } + + None + } + + pub fn get_component(&self, entity: ArchetypeEntityId) -> Option<&T> { + for col in self.columns.iter() { + if col.as_ref().as_any().is::>() { + let components: &Vec = col.as_any().downcast_ref().unwrap(); + + return components.get(entity.0 as usize); + } + } + + None + } + + pub(crate) fn add_entity(&mut self, components: Vec>) -> ArchetypeEntityId { + let mut created_entity: Option = None; + + for mut component in components.into_iter() { + for col in self.columns.iter_mut() { + if col.is_same_type(component.as_ref()) { + match created_entity { + Some(e) => { + assert!(e.0 == col.len() as u64); + }, + None => { + created_entity = Some(ArchetypeEntityId(col.len() as u64)); + } + } + + col.append(component.as_mut()); + } + } + } + + created_entity.expect("Failure to create entity!") + } + + /// returns a boolean indicating whether this archetype can store the TypeIds given + pub(crate) fn is_archetype_for(&self, types: Vec) -> bool { + let types_iter = types.into_iter(); + + self.columns + .iter() + .map(|c| c.component_type_id()) + .eq(types_iter) + } +} \ No newline at end of file diff --git a/lyra-ecs/src/bundle.rs b/lyra-ecs/src/bundle.rs new file mode 100644 index 0000000..6f3b3e9 --- /dev/null +++ b/lyra-ecs/src/bundle.rs @@ -0,0 +1,43 @@ +use std::any::{TypeId, Any}; + +use crate::archetype::ComponentColumn; + +pub trait Bundle { + // Get a list of type ids that this bundle is storing + fn types(&self) -> Vec; + /// Take components into a list. + /// The return value could be seen as a list of a list of components, but that inner list + /// only contains a single value to make it easier to add it to an archetype. + fn take_components(self) -> Vec>; +} + +macro_rules! impl_bundle_tuple { + ( $(($name: ident, $index: tt))+ ) => ( + impl<$($name: Send + Sync + 'static),+> Bundle for ($($name,)+) { + fn types(&self) -> Vec { + vec![$(self.$index.type_id()),+] + } + + fn take_components(self) -> Vec> { + vec![$(Box::new(vec![self.$index])),+] + } + } + ); +} + +// hopefully 16 components in a bundle is enough +impl_bundle_tuple! { (C1, 0) } +impl_bundle_tuple! { (C1, 0) (C2, 1) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) (C8, 7) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) (C8, 7) (C9, 8) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) (C8, 7) (C9, 8) (C10, 9) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) (C8, 7) (C9, 8) (C10, 9) (C11, 10) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) (C8, 7) (C9, 8) (C10, 9) (C11, 10) (C12, 11) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) (C8, 7) (C9, 8) (C10, 9) (C11, 10) (C12, 11) (C13, 12) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) (C8, 7) (C9, 8) (C10, 9) (C11, 10) (C12, 11) (C13, 12) (C14, 13) } +impl_bundle_tuple! { (C1, 0) (C2, 1) (C3, 2) (C4, 3) (C5, 4) (C6, 5) (C7, 6) (C8, 7) (C9, 8) (C10, 9) (C11, 10) (C12, 11) (C13, 12) (C14, 13) (C15, 14) } diff --git a/lyra-ecs/src/component.rs b/lyra-ecs/src/component.rs new file mode 100644 index 0000000..e262482 --- /dev/null +++ b/lyra-ecs/src/component.rs @@ -0,0 +1,5 @@ +use std::any::Any; + +pub trait Component : Any { + +} \ No newline at end of file diff --git a/lyra-ecs/src/main.rs b/lyra-ecs/src/main.rs new file mode 100644 index 0000000..9a3a5aa --- /dev/null +++ b/lyra-ecs/src/main.rs @@ -0,0 +1,24 @@ +use std::any::Any; + +use crate::world::World; + +mod archetype; +mod world; +mod bundle; +mod component; + +#[derive(Debug)] +pub struct Position2d(i32, i32); + +fn main() { + let mut world = World::new(); + + let pos = Position2d(50, 50); + let e = world.spawn((pos,)); + + if let Some(pos) = world.get_component::(e) { + println!("Got Position2d: {:?}", pos); + } else { + println!("no component found :("); + } +} diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs new file mode 100644 index 0000000..0386921 --- /dev/null +++ b/lyra-ecs/src/world.rs @@ -0,0 +1,112 @@ +use std::{collections::{HashMap, VecDeque}, any::{Any, TypeId}}; + +use crate::{archetype::{ArchetypeId, Archetype}, bundle::Bundle, component::Component}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct EntityId(pub u64); + +/// The id of the entity for the Archetype. +/// The Archetype struct uses this as the index in the component columns +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct ArchetypeEntityId(pub u64); + +pub struct Entity { + id: EntityId, + generation: u64, +} + +struct Record { + id: ArchetypeId, + index: ArchetypeEntityId, +} + +pub struct World { + archetypes: HashMap, + next_archetype_id: ArchetypeId, + entity_index: HashMap, + dead_entities: VecDeque, + next_entity_id: EntityId, +} + +impl World { + pub fn new() -> Self { + Self { + archetypes: HashMap::new(), + next_archetype_id: ArchetypeId(0), + entity_index: HashMap::new(), + dead_entities: VecDeque::new(), + next_entity_id: EntityId(0), + } + } + + fn get_new_entity(&mut self) -> Entity { + match self.dead_entities.pop_front() { + Some(e) => e, + None => { + let new_id = self.next_entity_id; + self.next_entity_id.0 += 1; + + Entity { + id: new_id, + generation: 0, + } + } + } + } + + pub fn spawn(&mut self, bundle: B) -> Entity + where + B: Bundle + { + let bundle_types = bundle.types(); + let new_entity = self.get_new_entity(); + + // try to find an archetype + let archetype = self.archetypes + .values_mut() + .find(|a| a.is_archetype_for(bundle_types.clone())); + + if let Some(archetype) = archetype { + // take components from the bundle and add it to the archetype + let columns = bundle.take_components(); + let arche_idx = archetype.add_entity(columns); + + // Create entity record and store it + let record = Record { + id: archetype.id, + index: arche_idx, + }; + + self.entity_index.insert(new_entity.id, record); + } + // create a new archetype if one isn't found + else { + let columns = bundle.take_components(); + + // create archetype + let new_arch_id = self.next_archetype_id.increment(); + let archetype = Archetype::from_columns(new_arch_id, columns); + + // store archetype + self.archetypes.insert(new_arch_id, archetype); + + // Create entity record and store it + let record = Record { + id: new_arch_id, + // this is the first entity in the archetype + index: ArchetypeEntityId(0), + }; + + self.entity_index.insert(new_entity.id, record); + } + + new_entity + } + + pub fn get_component(&self, entity: Entity) -> Option<&T> { + let record = self.entity_index.get(&entity.id)?; + let archetype = self.archetypes.get(&record.id)?; + + archetype.get_component(record.index) + } +} \ No newline at end of file