From f17497dca8b82060d7436bcd7cfc0d6953525ef0 Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Thu, 22 Oct 2020 00:30:32 +0200 Subject: [PATCH] Implemented creation of entities and components --- async-ecs/src/entity/builder.rs | 47 +++++ async-ecs/src/entity/entities.rs | 53 +++++ async-ecs/src/entity/entity.rs | 99 +++++++++ async-ecs/src/entity/mod.rs | 10 +- async-ecs/src/error.rs | 9 + async-ecs/src/lib.rs | 1 + async-ecs/src/main.rs | 11 +- async-ecs/src/resource/entry.rs | 5 +- async-ecs/src/resource/mod.rs | 240 +-------------------- async-ecs/src/resource/resources.rs | 256 +++++++++++++++++++++++ async-ecs/src/storage/masked_storage.rs | 19 +- async-ecs/src/storage/mod.rs | 8 +- async-ecs/src/storage/storage_wrapper.rs | 23 +- async-ecs/src/storage/vec_storage.rs | 30 ++- async-ecs/src/world/mod.rs | 6 +- 15 files changed, 560 insertions(+), 257 deletions(-) create mode 100644 async-ecs/src/entity/builder.rs create mode 100644 async-ecs/src/entity/entities.rs create mode 100644 async-ecs/src/entity/entity.rs create mode 100644 async-ecs/src/error.rs create mode 100644 async-ecs/src/resource/resources.rs diff --git a/async-ecs/src/entity/builder.rs b/async-ecs/src/entity/builder.rs new file mode 100644 index 0000000..9933725 --- /dev/null +++ b/async-ecs/src/entity/builder.rs @@ -0,0 +1,47 @@ +use crate::{access::WriteStorage, component::Component, system::SystemData, world::World}; + +use super::Entity; + +pub struct Builder<'a> { + world: &'a World, + entity: Entity, + built: bool, +} + +impl<'a> Builder<'a> { + pub fn new(world: &'a World) -> Self { + let entity = world.entities_mut().allocate(); + + Self { + world, + entity, + built: false, + } + } + + #[inline] + pub fn with(self, c: T) -> Self { + { + let mut storage = WriteStorage::::fetch(&self.world); + + storage.insert(self.entity, c).unwrap(); + } + + self + } + + #[inline] + pub fn build(mut self) -> Entity { + self.built = true; + + self.entity + } +} + +impl Drop for Builder<'_> { + fn drop(&mut self) { + if !self.built { + self.world.entities_mut().kill(self.entity); + } + } +} diff --git a/async-ecs/src/entity/entities.rs b/async-ecs/src/entity/entities.rs new file mode 100644 index 0000000..d97e634 --- /dev/null +++ b/async-ecs/src/entity/entities.rs @@ -0,0 +1,53 @@ +use hibitset::BitSet; + +use super::Entity; + +#[derive(Default)] +pub struct Entities { + alive: BitSet, + cache: Vec, + generations: Vec, +} + +impl Entities { + pub fn is_alive(&self, entity: Entity) -> bool { + let i = entity.index(); + let g = entity.generation(); + + self.alive.contains(i) && self.generations.get(i as usize) == Some(&g) + } + + pub fn allocate(&mut self) -> Entity { + let i = match self.cache.pop() { + Some(i) => i, + None => { + let i = self.generations.len() as u32; + let c = i.checked_add(1).expect("No entity left to allocate"); + + self.generations.resize(c as usize, 0); + + i + } + }; + + let g = self.generations[i as usize].wrapping_add(1); + + self.generations[i as usize] = g; + self.alive.add(i); + + Entity::from_parts(i, g) + } + + pub fn kill(&mut self, entity: Entity) -> bool { + if self.is_alive(entity) { + let i = entity.index(); + + self.alive.remove(i); + self.cache.push(i); + + true + } else { + false + } + } +} diff --git a/async-ecs/src/entity/entity.rs b/async-ecs/src/entity/entity.rs new file mode 100644 index 0000000..779db23 --- /dev/null +++ b/async-ecs/src/entity/entity.rs @@ -0,0 +1,99 @@ +use std::cmp::Ordering; +use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; +use std::hash::{Hash, Hasher}; + +#[derive(Clone, Copy)] +pub struct Entity(EntityRaw); + +pub type Index = u32; +pub type Generation = u32; + +#[repr(C, packed)] +#[derive(Clone, Copy)] +union EntityRaw { + id: u64, + data: EntityData, +} + +#[repr(C, packed)] +#[derive(Clone, Copy)] +struct EntityData { + index: Index, + generation: Generation, +} + +impl Entity { + pub fn from_id(id: u64) -> Self { + Self(EntityRaw { id }) + } + + pub fn from_parts(index: Index, generation: Generation) -> Self { + Self(EntityRaw { + data: EntityData { index, generation }, + }) + } + + #[inline] + pub fn id(&self) -> u64 { + unsafe { self.0.id } + } + + #[inline] + pub fn index(&self) -> Index { + unsafe { self.0.data.index } + } + + #[inline] + pub fn generation(&self) -> Generation { + unsafe { self.0.data.generation } + } +} + +impl Display for Entity { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{:08X}", self.id()) + } +} + +impl Debug for Entity { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{:08X}", self.id()) + } +} + +impl Hash for Entity { + fn hash(&self, state: &mut H) { + self.index().hash(state); + self.generation().hash(state); + } +} + +impl Eq for Entity {} + +impl PartialEq for Entity { + fn eq(&self, other: &Entity) -> bool { + self.id() == other.id() + } +} + +impl Ord for Entity { + fn cmp(&self, other: &Self) -> Ordering { + if self.generation() < other.generation() { + Ordering::Less + } else if self.generation() > other.generation() { + Ordering::Greater + } else if self.index() < other.index() { + Ordering::Less + } else if self.index() > other.index() { + Ordering::Greater + } else { + Ordering::Equal + } + } +} + +impl PartialOrd for Entity { + fn partial_cmp(&self, other: &Entity) -> Option { + Some(Ord::cmp(self, other)) + } +} diff --git a/async-ecs/src/entity/mod.rs b/async-ecs/src/entity/mod.rs index 4b785b5..fd0f874 100644 --- a/async-ecs/src/entity/mod.rs +++ b/async-ecs/src/entity/mod.rs @@ -1,2 +1,8 @@ -#[derive(Default)] -pub struct Entities; +pub mod builder; +pub mod entities; +#[allow(clippy::module_inception)] +pub mod entity; + +pub use builder::Builder; +pub use entities::Entities; +pub use entity::{Entity, Generation, Index}; diff --git a/async-ecs/src/error.rs b/async-ecs/src/error.rs new file mode 100644 index 0000000..1226b57 --- /dev/null +++ b/async-ecs/src/error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +use crate::entity::Entity; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Entity is not alive: {0}!")] + EntityIsNotAlive(Entity), +} diff --git a/async-ecs/src/lib.rs b/async-ecs/src/lib.rs index 491084e..87ea763 100644 --- a/async-ecs/src/lib.rs +++ b/async-ecs/src/lib.rs @@ -1,6 +1,7 @@ pub mod access; pub mod component; pub mod entity; +pub mod error; pub mod misc; pub mod resource; pub mod storage; diff --git a/async-ecs/src/main.rs b/async-ecs/src/main.rs index 9395631..806fb08 100644 --- a/async-ecs/src/main.rs +++ b/async-ecs/src/main.rs @@ -4,13 +4,6 @@ use async_ecs::{VecStorage, World}; use async_ecs_derive::Component; use rand::random; -/* -use std::time::{Duration, Instant}; - -use async_ecs::Resources; - -*/ - fn main() { env_logger::builder() .filter_level(log::LevelFilter::Info) @@ -24,7 +17,6 @@ fn main() { world.register_component::(); world.register_component::(); - /* for _ in 0..ENTITY_COUNT { world .create_entity() @@ -36,6 +28,7 @@ fn main() { info!("World initialized!"); + /* let mut dispatcher = DispatcherBuilder::new() .with(Move, "move", &[]) .with(Accelerate, "accelerate", &["move"]) @@ -65,7 +58,7 @@ fn main() { */ } -const ENTITY_COUNT: usize = 3_000_000; +const ENTITY_COUNT: usize = 100_000; const REPEAT_COUNT: u32 = 100; #[derive(Debug, Component)] diff --git a/async-ecs/src/resource/entry.rs b/async-ecs/src/resource/entry.rs index 0dbe156..954b903 100644 --- a/async-ecs/src/resource/entry.rs +++ b/async-ecs/src/resource/entry.rs @@ -33,9 +33,6 @@ where let inner = self.inner.or_insert_with(move || Cell::new(Box::new(f()))); let inner = inner.borrow_mut().map(Box::as_mut); - RefMut { - inner, - phantom: PhantomData, - } + RefMut::new(inner) } } diff --git a/async-ecs/src/resource/mod.rs b/async-ecs/src/resource/mod.rs index f454ced..fc1bc5e 100644 --- a/async-ecs/src/resource/mod.rs +++ b/async-ecs/src/resource/mod.rs @@ -1,137 +1,18 @@ pub mod cell; pub mod entry; +pub mod resources; + +pub use resources::{Ref, RefMut, Resources}; use std::any::TypeId; -use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; -use hashbrown::HashMap; use mopa::Any; -use cell::{Cell, Ref as CellRef, RefMut as CellRefMut}; -use entry::Entry; - -#[derive(Default)] -pub struct Resources { - resources: HashMap>>, -} +pub trait Resource: Any + Send + Sync + 'static {} #[derive(Debug, Hash, Eq, PartialEq)] pub struct ResourceId(TypeId); -pub trait Resource: Any + Send + Sync + 'static {} - -pub struct Ref<'a, R: 'a> { - inner: CellRef<'a, dyn Resource>, - phantom: PhantomData<&'a R>, -} - -pub struct RefMut<'a, R: 'a> { - inner: CellRefMut<'a, dyn Resource>, - phantom: PhantomData<&'a R>, -} - -macro_rules! fetch_panic { - () => {{ - panic!( - "\ - Tried to fetch resource from the resources map, but the resource does not exist.\n\ -\n\ - Resource: `{resource_name_full}`\n\ -\n\ - You may ensure the resource exists!\ - ", - resource_name_full = std::any::type_name::(), - ) - }}; -} - -/* Resources */ - -impl Resources { - pub fn entry(&mut self) -> Entry - where - R: Resource, - { - Entry::new(self.resources.entry(ResourceId::new::())) - } - - pub fn insert(&mut self, r: R) - where - R: Resource, - { - self.resources - .insert(ResourceId::new::(), Cell::new(Box::new(r))); - } - - pub fn remove(&mut self) -> Option - where - R: Resource, - { - self.resources - .remove(&ResourceId::new::()) - .map(Cell::into_inner) - .map(|x: Box| x.downcast()) - .map(|x: Result, _>| x.ok().unwrap()) - .map(|x| *x) - } - - pub fn contains(&self) -> bool - where - R: Resource, - { - self.resources.contains_key(&ResourceId::new::()) - } - - pub fn borrow(&self) -> Ref - where - R: Resource, - { - self.try_borrow().unwrap_or_else(|| fetch_panic!()) - } - - pub fn try_borrow(&self) -> Option> - where - R: Resource, - { - self.resources.get(&ResourceId::new::()).map(|r| Ref { - inner: CellRef::map(r.borrow(), Box::as_ref), - phantom: PhantomData, - }) - } - - pub fn borrow_mut(&self) -> RefMut - where - R: Resource, - { - self.try_borrow_mut().unwrap_or_else(|| fetch_panic!()) - } - - pub fn try_borrow_mut(&self) -> Option> - where - R: Resource, - { - self.resources.get(&ResourceId::new::()).map(|r| RefMut { - inner: r.borrow_mut().map(Box::as_mut), - phantom: PhantomData, - }) - } - - pub fn get_mut(&mut self) -> Option<&mut R> { - self.get_resource_mut(ResourceId::new::()) - .map(|res| res.downcast_mut().unwrap()) - } - - pub fn get_resource_mut(&mut self, id: ResourceId) -> Option<&mut dyn Resource> { - self.resources - .get_mut(&id) - .map(Cell::get_mut) - .map(Box::as_mut) - } -} - -/* ResourceId */ - impl ResourceId { pub fn new() -> Self where @@ -140,116 +21,3 @@ impl ResourceId { Self(TypeId::of::()) } } - -/* Resource */ - -impl Resource for T where T: Any + Send + Sync {} - -mod __resource_mopafy_scope { - #![allow(clippy::all)] - - use mopa::mopafy; - - use super::Resource; - - mopafy!(Resource); -} - -/* Ref */ - -impl<'a, R> Deref for Ref<'a, R> -where - R: Resource, -{ - type Target = R; - - fn deref(&self) -> &R { - unsafe { self.inner.downcast_ref_unchecked() } - } -} - -impl<'a, R> Clone for Ref<'a, R> { - fn clone(&self) -> Self { - Ref { - inner: self.inner.clone(), - phantom: PhantomData, - } - } -} - -/* RefMut */ - -impl<'a, R> Deref for RefMut<'a, R> -where - R: Resource, -{ - type Target = R; - - fn deref(&self) -> &R { - unsafe { self.inner.downcast_ref_unchecked() } - } -} - -impl<'a, R> DerefMut for RefMut<'a, R> -where - R: Resource, -{ - fn deref_mut(&mut self) -> &mut R { - unsafe { self.inner.downcast_mut_unchecked() } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Default)] - struct Res; - - #[test] - fn insert() { - struct Foo; - - let mut resources = Resources::default(); - resources.insert(Res); - - assert!(resources.contains::()); - assert!(!resources.contains::()); - } - - #[test] - #[should_panic(expected = "but it was already borrowed")] - fn read_write_fails() { - let mut resources = Resources::default(); - resources.insert(Res); - - let _read = resources.borrow::(); - let _write = resources.borrow_mut::(); - } - - #[test] - #[should_panic(expected = "but it was already borrowed mutably")] - fn write_read_fails() { - let mut resources = Resources::default(); - resources.insert(Res); - - let _write = resources.borrow_mut::(); - let _read = resources.borrow::(); - } - - #[test] - fn remove_insert() { - let mut resources = Resources::default(); - resources.insert(Res); - - assert!(resources.contains::()); - - resources.remove::().unwrap(); - - assert!(!resources.contains::()); - - resources.insert(Res); - - assert!(resources.contains::()); - } -} diff --git a/async-ecs/src/resource/resources.rs b/async-ecs/src/resource/resources.rs new file mode 100644 index 0000000..57015dc --- /dev/null +++ b/async-ecs/src/resource/resources.rs @@ -0,0 +1,256 @@ +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; + +use hashbrown::HashMap; +use mopa::Any; + +use super::{ + cell::{Cell, Ref as CellRef, RefMut as CellRefMut}, + entry::Entry, + Resource, ResourceId, +}; + +#[derive(Default)] +pub struct Resources { + resources: HashMap>>, +} + +pub struct Ref<'a, R: 'a> { + inner: CellRef<'a, dyn Resource>, + phantom: PhantomData<&'a R>, +} + +pub struct RefMut<'a, R: 'a> { + inner: CellRefMut<'a, dyn Resource>, + phantom: PhantomData<&'a R>, +} + +macro_rules! fetch_panic { + () => {{ + panic!( + "\ + Tried to fetch resource from the resources map, but the resource does not exist.\n\ +\n\ + Resource: `{resource_name_full}`\n\ +\n\ + You may ensure the resource exists!\ + ", + resource_name_full = std::any::type_name::(), + ) + }}; +} + +/* Resources */ + +impl Resources { + pub fn entry(&mut self) -> Entry + where + R: Resource, + { + Entry::new(self.resources.entry(ResourceId::new::())) + } + + pub fn insert(&mut self, r: R) + where + R: Resource, + { + self.resources + .insert(ResourceId::new::(), Cell::new(Box::new(r))); + } + + pub fn remove(&mut self) -> Option + where + R: Resource, + { + self.resources + .remove(&ResourceId::new::()) + .map(Cell::into_inner) + .map(|x: Box| x.downcast()) + .map(|x: Result, _>| x.ok().unwrap()) + .map(|x| *x) + } + + pub fn contains(&self) -> bool + where + R: Resource, + { + self.resources.contains_key(&ResourceId::new::()) + } + + pub fn borrow(&self) -> Ref + where + R: Resource, + { + self.try_borrow().unwrap_or_else(|| fetch_panic!()) + } + + pub fn try_borrow(&self) -> Option> + where + R: Resource, + { + self.resources.get(&ResourceId::new::()).map(|r| Ref { + inner: CellRef::map(r.borrow(), Box::as_ref), + phantom: PhantomData, + }) + } + + pub fn borrow_mut(&self) -> RefMut + where + R: Resource, + { + self.try_borrow_mut().unwrap_or_else(|| fetch_panic!()) + } + + pub fn try_borrow_mut(&self) -> Option> + where + R: Resource, + { + self.resources.get(&ResourceId::new::()).map(|r| RefMut { + inner: r.borrow_mut().map(Box::as_mut), + phantom: PhantomData, + }) + } + + pub fn get_mut(&mut self) -> Option<&mut R> { + self.get_resource_mut(ResourceId::new::()) + .map(|res| res.downcast_mut().unwrap()) + } + + pub fn get_resource_mut(&mut self, id: ResourceId) -> Option<&mut dyn Resource> { + self.resources + .get_mut(&id) + .map(Cell::get_mut) + .map(Box::as_mut) + } +} + +/* Resource */ + +impl Resource for T where T: Any + Send + Sync {} + +mod __resource_mopafy_scope { + #![allow(clippy::all)] + + use mopa::mopafy; + + use super::super::Resource; + + mopafy!(Resource); +} + +/* Ref */ + +impl<'a, R> Ref<'a, R> { + pub fn new(inner: CellRef<'a, dyn Resource>) -> Self { + Self { + inner, + phantom: PhantomData, + } + } +} + +impl<'a, R> Deref for Ref<'a, R> +where + R: Resource, +{ + type Target = R; + + fn deref(&self) -> &R { + unsafe { self.inner.downcast_ref_unchecked() } + } +} + +impl<'a, R> Clone for Ref<'a, R> { + fn clone(&self) -> Self { + Ref { + inner: self.inner.clone(), + phantom: PhantomData, + } + } +} + +/* RefMut */ + +impl<'a, R> RefMut<'a, R> { + pub fn new(inner: CellRefMut<'a, dyn Resource>) -> Self { + Self { + inner, + phantom: PhantomData, + } + } +} + +impl<'a, R> Deref for RefMut<'a, R> +where + R: Resource, +{ + type Target = R; + + fn deref(&self) -> &R { + unsafe { self.inner.downcast_ref_unchecked() } + } +} + +impl<'a, R> DerefMut for RefMut<'a, R> +where + R: Resource, +{ + fn deref_mut(&mut self) -> &mut R { + unsafe { self.inner.downcast_mut_unchecked() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Default)] + struct Res; + + #[test] + fn insert() { + struct Foo; + + let mut resources = Resources::default(); + resources.insert(Res); + + assert!(resources.contains::()); + assert!(!resources.contains::()); + } + + #[test] + #[should_panic(expected = "but it was already borrowed")] + fn read_write_fails() { + let mut resources = Resources::default(); + resources.insert(Res); + + let _read = resources.borrow::(); + let _write = resources.borrow_mut::(); + } + + #[test] + #[should_panic(expected = "but it was already borrowed mutably")] + fn write_read_fails() { + let mut resources = Resources::default(); + resources.insert(Res); + + let _write = resources.borrow_mut::(); + let _read = resources.borrow::(); + } + + #[test] + fn remove_insert() { + let mut resources = Resources::default(); + resources.insert(Res); + + assert!(resources.contains::()); + + resources.remove::().unwrap(); + + assert!(!resources.contains::()); + + resources.insert(Res); + + assert!(resources.contains::()); + } +} diff --git a/async-ecs/src/storage/masked_storage.rs b/async-ecs/src/storage/masked_storage.rs index 5393bb2..f2df91e 100644 --- a/async-ecs/src/storage/masked_storage.rs +++ b/async-ecs/src/storage/masked_storage.rs @@ -1,6 +1,8 @@ +use std::mem::swap; + use hibitset::BitSet; -use crate::component::Component; +use crate::{component::Component, entity::Entity, storage::Storage}; pub struct MaskedStorage { mask: BitSet, @@ -14,4 +16,19 @@ impl MaskedStorage { inner, } } + + pub fn insert(&mut self, entity: Entity, mut component: T) -> Option { + let index = entity.index(); + + if self.mask.contains(index) { + swap(&mut component, self.inner.get_mut(index)); + + Some(component) + } else { + self.mask.add(index); + self.inner.insert(index, component); + + None + } + } } diff --git a/async-ecs/src/storage/mod.rs b/async-ecs/src/storage/mod.rs index 7d684f5..4d5ba04 100644 --- a/async-ecs/src/storage/mod.rs +++ b/async-ecs/src/storage/mod.rs @@ -6,6 +6,10 @@ pub use masked_storage::MaskedStorage; pub use storage_wrapper::StorageWrapper; pub use vec_storage::VecStorage; -use crate::misc::TryDefault; +use crate::{entity::Index, misc::TryDefault}; -pub trait Storage: TryDefault {} +pub trait Storage: TryDefault { + fn get(&self, index: Index) -> &T; + fn get_mut(&mut self, index: Index) -> &mut T; + fn insert(&mut self, index: Index, value: T); +} diff --git a/async-ecs/src/storage/storage_wrapper.rs b/async-ecs/src/storage/storage_wrapper.rs index 98299ab..322b18f 100644 --- a/async-ecs/src/storage/storage_wrapper.rs +++ b/async-ecs/src/storage/storage_wrapper.rs @@ -1,6 +1,13 @@ use std::marker::PhantomData; +use std::ops::DerefMut; -use crate::{entity::Entities, resource::Ref}; +use crate::{ + component::Component, + entity::{Entities, Entity}, + error::Error, + resource::Ref, + storage::MaskedStorage, +}; pub struct StorageWrapper<'a, T, D> { data: D, @@ -17,3 +24,17 @@ impl<'a, T, D> StorageWrapper<'a, T, D> { } } } + +impl<'a, T, D> StorageWrapper<'a, T, D> +where + T: Component, + D: DerefMut>, +{ + pub fn insert(&mut self, entity: Entity, component: T) -> Result, Error> { + if !self.entities.is_alive(entity) { + return Err(Error::EntityIsNotAlive(entity)); + } + + Ok(self.data.insert(entity, component)) + } +} diff --git a/async-ecs/src/storage/vec_storage.rs b/async-ecs/src/storage/vec_storage.rs index 843f898..2e3972f 100644 --- a/async-ecs/src/storage/vec_storage.rs +++ b/async-ecs/src/storage/vec_storage.rs @@ -1,10 +1,38 @@ use std::mem::MaybeUninit; +use crate::entity::Index; + use super::Storage; pub struct VecStorage(Vec>); -impl Storage for VecStorage {} +impl Storage for VecStorage { + fn get(&self, index: Index) -> &T { + unsafe { &*self.0.get_unchecked(index as usize).as_ptr() } + } + + fn get_mut(&mut self, index: Index) -> &mut T { + unsafe { &mut *self.0.get_unchecked_mut(index as usize).as_mut_ptr() } + } + + fn insert(&mut self, index: Index, value: T) { + let index = index as usize; + + if self.0.len() <= index { + let delta = index + 1 - self.0.len(); + + self.0.reserve(delta); + + unsafe { + self.0.set_len(index + 1); + } + } + + unsafe { + *self.0.get_unchecked_mut(index) = MaybeUninit::new(value); + } + } +} impl Default for VecStorage { fn default() -> Self { diff --git a/async-ecs/src/world/mod.rs b/async-ecs/src/world/mod.rs index 2b329d6..8904e87 100644 --- a/async-ecs/src/world/mod.rs +++ b/async-ecs/src/world/mod.rs @@ -7,7 +7,7 @@ use std::ops::{Deref, DerefMut}; use crate::{ access::{Read, ReadStorage, WriteStorage}, component::Component, - entity::Entities, + entity::{Builder, Entities}, resource::{Ref, RefMut, Resource, Resources}, storage::MaskedStorage, system::SystemData, @@ -59,6 +59,10 @@ impl World { pub fn component_mut(&self) -> WriteStorage { WriteStorage::fetch(&self) } + + pub fn create_entity(&mut self) -> Builder { + Builder::new(self) + } } impl Default for World {