early version of ecs working

This commit is contained in:
SeanOMik 2023-05-25 00:11:16 -04:00
parent ad35015478
commit cde7d140ea
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
12 changed files with 472 additions and 0 deletions

1
lyra-ecs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

45
lyra-ecs/.vscode/launch.json vendored Normal file
View File

@ -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}"
}
]
}

3
lyra-ecs/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix"
}

7
lyra-ecs/Cargo.lock generated Normal file
View File

@ -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"

8
lyra-ecs/Cargo.toml Normal file
View File

@ -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]

21
lyra-ecs/LICENSE Normal file
View File

@ -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.

21
lyra-ecs/shell.nix Normal file
View File

@ -0,0 +1,21 @@
{ pkgs ? import <nixpkgs> { } }:
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;
}

182
lyra-ecs/src/archetype.rs Normal file
View File

@ -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<dyn ComponentColumn>;
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<T: Send + Sync + 'static> ComponentColumn for Vec<T> {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn new_empty_column(&self) -> Box<dyn ComponentColumn> {
Box::new(Vec::<T>::new())
}
fn is_same_type(&self, column: &dyn ComponentColumn) -> bool {
column.as_any().downcast_ref::<Self>().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::<T>().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<Entity>,
pub(crate) columns: Vec<Box<dyn ComponentColumn>>,
}
impl Archetype {
/// Create a new archetype from another archetype and add a column
pub fn new_archetype_add<T: Send + Sync + 'static>(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::<Vec<T>>())
.is_none());
columns.push(Box::new(Vec::<T>::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<T: Send + Sync + 'static>(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::<Vec<T>>())
.unwrap();
columns.remove(idx);
Archetype {
id: new_id,
entities: Vec::new(),
columns,
}
}
pub fn from_columns(new_id: ArchetypeId, columns: Vec<Box<dyn ComponentColumn>>) -> Archetype {
Archetype {
id: new_id,
entities: Vec::new(),
columns,
}
}
pub fn get_component_mut<T: Send + Sync + 'static>(&mut self, entity: ArchetypeEntityId) -> Option<&mut T> {
for col in self.columns.iter_mut() {
if col.as_any().is::<Vec<T>>() {
let components: &mut Vec<T> = col.as_any_mut().downcast_mut().unwrap();
return components.get_mut(entity.0 as usize);
}
}
None
}
pub fn get_component<T: Send + Sync + 'static>(&self, entity: ArchetypeEntityId) -> Option<&T> {
for col in self.columns.iter() {
if col.as_ref().as_any().is::<Vec<T>>() {
let components: &Vec<T> = col.as_any().downcast_ref().unwrap();
return components.get(entity.0 as usize);
}
}
None
}
pub(crate) fn add_entity(&mut self, components: Vec<Box<dyn ComponentColumn>>) -> ArchetypeEntityId {
let mut created_entity: Option<ArchetypeEntityId> = 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<TypeId>) -> bool {
let types_iter = types.into_iter();
self.columns
.iter()
.map(|c| c.component_type_id())
.eq(types_iter)
}
}

43
lyra-ecs/src/bundle.rs Normal file
View File

@ -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<TypeId>;
/// 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<Box<dyn ComponentColumn>>;
}
macro_rules! impl_bundle_tuple {
( $(($name: ident, $index: tt))+ ) => (
impl<$($name: Send + Sync + 'static),+> Bundle for ($($name,)+) {
fn types(&self) -> Vec<TypeId> {
vec![$(self.$index.type_id()),+]
}
fn take_components(self) -> Vec<Box<dyn ComponentColumn>> {
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) }

View File

@ -0,0 +1,5 @@
use std::any::Any;
pub trait Component : Any {
}

24
lyra-ecs/src/main.rs Normal file
View File

@ -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::<Position2d>(e) {
println!("Got Position2d: {:?}", pos);
} else {
println!("no component found :(");
}
}

112
lyra-ecs/src/world.rs Normal file
View File

@ -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<ArchetypeId, Archetype>,
next_archetype_id: ArchetypeId,
entity_index: HashMap<EntityId, Record>,
dead_entities: VecDeque<Entity>,
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<B>(&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<T: Send + Sync + 'static>(&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)
}
}