├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── src └── lib.rs └── tests └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | 4 | # Backup files from `cargo fmt` 5 | *.rs.bk -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: stable -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "recs" 3 | version = "2.0.1" 4 | authors = ["Andy Barron "] 5 | 6 | description = "Simple, flexible, macro-free entity-component system in pure (stable!) Rust." 7 | repository = "https://github.com/andybarron/rustic-ecs" 8 | documentation = "https://andybarron.github.io/rustic-ecs" 9 | readme = "./README.md" 10 | keywords = ["entity", "entity-component", "component", "system", "ecs"] 11 | license = "MIT" 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rustic Entity-Component System 2 | ============================== 3 | Simple entity-component system in pure Rust. Type reflection - no macros! 4 | 5 | [![Build Status](https://travis-ci.org/AndyBarron/rustic-ecs.svg?branch=master)](https://travis-ci.org/AndyBarron/rustic-ecs) 6 | 7 | Install 8 | ------- 9 | Visit [the crates.io page](https://crates.io/crates/recs), and add the 10 | specified line ("`recs = ...`") to the `[dependencies]` section of your 11 | Cargo.toml. From then on, `cargo build` should automatically download and compile 12 | Rustic ECS. 13 | 14 | Documentation 15 | ------------- 16 | 17 | 18 | Example 19 | ------- 20 | ```rust 21 | extern crate recs; 22 | use recs::{Ecs, EntityId}; 23 | 24 | #[derive(Clone, PartialEq, Debug)] 25 | struct Age{years: u32} 26 | 27 | #[derive(Clone, PartialEq, Debug)] 28 | struct Iq{points: i32} 29 | 30 | fn main() { 31 | 32 | // Create an ECS instance 33 | let mut system: Ecs = Ecs::new(); 34 | 35 | // Add entity to the system 36 | let forrest: EntityId = system.create_entity(); 37 | 38 | // Attach components to the entity 39 | // The Ecs.set method returns an EcsResult that will be set to Err if 40 | // the specified entity does not exist. If you're sure that the entity exists, suppress 41 | // Rust's "unused result" warning by prefixing your calls to set(..) with "let _ = ..." 42 | let _ = system.set(forrest, Age{years: 22}); 43 | let _ = system.set(forrest, Iq{points: 75}); // "I may not be a smart man..." 44 | 45 | // Get clone of attached component data from entity 46 | let age = system.get::(forrest).unwrap(); 47 | assert_eq!(age.years, 22); 48 | 49 | // Annotating the variable's type may let you skip type parameters 50 | let iq: Iq = system.get(forrest).unwrap(); 51 | assert_eq!(iq.points, 75); 52 | 53 | // Modify an entity's component 54 | let older = Age{years: age.years + 1}; 55 | let _ = system.set(forrest, older); 56 | 57 | // Modify a component in-place with a mutable borrow 58 | system.borrow_mut::(forrest).map(|iq| iq.points += 5); 59 | 60 | // Inspect a component in-place without cloning 61 | assert_eq!(system.borrow::(forrest), Ok(&Age{years: 23})); 62 | 63 | // Inspect a component via cloning 64 | assert_eq!(system.get::(forrest), Ok(Iq{points: 80})); 65 | 66 | } 67 | ``` 68 | 69 | License 70 | ------- 71 | MIT. Hooray! 72 | 73 | (See `LICENSE.txt` for details.) -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Simple entity-component system. Macro-free stable Rust using compile-time reflection! 2 | //! 3 | //! # Example 4 | //! ``` 5 | //! extern crate recs; 6 | //! use recs::{Ecs, EntityId}; 7 | //! 8 | //! #[derive(Clone, PartialEq, Debug)] 9 | //! struct Age{years: u32} 10 | //! 11 | //! #[derive(Clone, PartialEq, Debug)] 12 | //! struct Iq{points: i32} 13 | //! 14 | //! fn main() { 15 | //! 16 | //! // Create an ECS instance 17 | //! let mut system: Ecs = Ecs::new(); 18 | //! 19 | //! // Add entity to the system 20 | //! let forrest: EntityId = system.create_entity(); 21 | //! 22 | //! // Attach components to the entity 23 | //! // The Ecs.set method returns an EcsResult that will be set to Err if 24 | //! // the specified entity does not exist. If you're sure that the entity exists, suppress 25 | //! // Rust's "unused result" warning by prefixing your calls to set(..) with "let _ = ..." 26 | //! let _ = system.set(forrest, Age{years: 22}); 27 | //! let _ = system.set(forrest, Iq{points: 75}); // "I may not be a smart man..." 28 | //! 29 | //! // Get clone of attached component data from entity 30 | //! let age = system.get::(forrest).unwrap(); 31 | //! assert_eq!(age.years, 22); 32 | //! 33 | //! // Annotating the variable's type may let you skip type parameters 34 | //! let iq: Iq = system.get(forrest).unwrap(); 35 | //! assert_eq!(iq.points, 75); 36 | //! 37 | //! // Modify an entity's component 38 | //! let older = Age{years: age.years + 1}; 39 | //! let _ = system.set(forrest, older); 40 | //! 41 | //! // Modify a component in-place with a mutable borrow 42 | //! system.borrow_mut::(forrest).map(|iq| iq.points += 5); 43 | //! 44 | //! // Inspect a component in-place without cloning 45 | //! assert_eq!(system.borrow::(forrest), Ok(&Age{years: 23})); 46 | //! 47 | //! // Inspect a component via cloning 48 | //! assert_eq!(system.get::(forrest), Ok(Iq{points: 80})); 49 | //! 50 | //! } 51 | //! ``` 52 | 53 | #![allow(unknown_lints)] // for rust-clippy 54 | #![warn(missing_docs)] 55 | use std::any::{TypeId, Any}; 56 | use std::collections::{HashMap, HashSet}; 57 | 58 | type IdNumber = u64; 59 | 60 | /// Value type representing an entity in the entity-component system. 61 | /// 62 | /// To avoid duplicate entity IDs, these can only be created by calling `Ecs.create_entity()`. 63 | #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] 64 | pub struct EntityId(IdNumber); 65 | 66 | /// Error type for ECS results that require a specific entity or component. 67 | #[derive(Debug, PartialEq, Eq)] 68 | pub enum NotFound { 69 | /// A requested entity ID was not present in the system. 70 | Entity(EntityId), 71 | /// A requested component was not present on an entity. 72 | Component(TypeId), 73 | } 74 | 75 | /// Result type for ECS operations that may fail without a specific entity or component. 76 | pub type EcsResult = Result; 77 | 78 | /// Marker trait for types which can be used as components. 79 | /// 80 | /// `Component` is automatically implemented for all eligible types by the 81 | /// provided `impl`, so you don't have to worry about this. Hooray! 82 | pub trait Component: Any {} 83 | impl Component for T {} 84 | 85 | /// List of component types. 86 | /// 87 | /// The `Ecs` methods `has_all` and `collect_with` each take a `ComponentFilter` instance. The 88 | /// recommended way to actually create a `ComponentFilter` is with the 89 | /// [`component_filter!` macro](macro.component_filter!.html). 90 | #[derive(Default, PartialEq, Eq, Debug, Clone)] 91 | pub struct ComponentFilter { 92 | set: HashSet, 93 | } 94 | 95 | impl ComponentFilter { 96 | /// Create a new component filter. 97 | pub fn new() -> Self { 98 | Default::default() 99 | } 100 | /// Add component type `C` to the filter. 101 | pub fn add(&mut self) { 102 | self.set.insert(TypeId::of::()); 103 | } 104 | /// Remove component type `C` from the filter. 105 | pub fn remove(&mut self) { 106 | self.set.remove(&TypeId::of::()); 107 | } 108 | /// Return `true` if the filter already contains component type `C`; otherwise `false`. 109 | pub fn contains(&mut self) -> bool { 110 | self.set.contains(&TypeId::of::()) 111 | } 112 | /// Create a component filter from a vector/slice of `TypeId` instances. (Not recommended; 113 | /// used by the `component_filter!` macro.) 114 | pub fn from_slice(slice: &[TypeId]) -> Self { 115 | let mut this = Self::new(); 116 | for type_id in slice.iter() { 117 | this.set.insert(*type_id); 118 | } 119 | this 120 | } 121 | /// Return an iterator over all the contained component types. 122 | #[allow(needless_lifetimes)] // https://github.com/Manishearth/rust-clippy/issues/740 123 | pub fn iter<'a>(&'a self) -> Box + 'a> { 124 | Box::new(self.set.iter().cloned()) 125 | } 126 | } 127 | 128 | /// Create a `ComponentFilter` by type name. 129 | /// 130 | /// If you want all entities with components `Foo` and `Bar`: 131 | /// 132 | /// ``` 133 | /// #[macro_use] // The macro won't be imported without this flag! 134 | /// extern crate recs; 135 | /// use recs::{Ecs, EntityId}; 136 | /// 137 | /// struct Foo; 138 | /// struct Bar; 139 | /// 140 | /// fn main() { 141 | /// let sys = Ecs::new(); 142 | /// // ... add some entities and components ... 143 | /// let mut ids: Vec = Vec::new(); 144 | /// let filter = component_filter!(Foo, Bar); 145 | /// sys.collect_with(&filter, &mut ids); 146 | /// for id in ids { 147 | /// // Will only iterate over entities that have been assigned both `Foo` and `Bar` 148 | /// // components 149 | /// } 150 | /// } 151 | /// ``` 152 | #[macro_export] 153 | macro_rules! component_filter { 154 | ($($x:ty),*) => ( 155 | $crate::ComponentFilter::from_slice( 156 | &vec![$(std::any::TypeId::of::<$x>()),*] 157 | ) 158 | ); 159 | ($($x:ty,)*) => (component_filter![$($x),*]) 160 | } 161 | 162 | /// Primary data structure containing entity and component data. 163 | /// 164 | /// Notice that `Ecs` itself has no type parameters. Its methods to interact 165 | /// with components do, but runtime reflection (via `std::any::TypeId`) is 166 | /// used to retrieve components from an internal `HashMap`. Therefore, you 167 | /// can create and use any data structure you want for components. 168 | /// 169 | /// Tip: using `#[derive(Clone)]` on your component types will make your life a little easier by 170 | /// enabling the `get` method, which avoids locking down the `Ecs` with a mutable or immutable 171 | /// borrow. 172 | #[derive(Default)] 173 | pub struct Ecs { 174 | ids: IdNumber, 175 | data: HashMap, 176 | } 177 | 178 | #[derive(Default)] 179 | struct ComponentMap { 180 | map: HashMap>, 181 | } 182 | 183 | impl ComponentMap { 184 | fn set(&mut self, component: C) -> Option { 185 | self.map 186 | .insert(TypeId::of::(), Box::new(component)) 187 | .map(|old| *old.downcast::().expect("ComponentMap.set: internal downcast error")) 188 | } 189 | fn borrow(&self) -> EcsResult<&C> { 190 | self.map 191 | .get(&TypeId::of::()) 192 | .map(|c| { 193 | c.downcast_ref() 194 | .expect("ComponentMap.borrow: internal downcast error") 195 | }) 196 | .ok_or_else(|| NotFound::Component(TypeId::of::())) 197 | } 198 | #[allow(map_clone)] 199 | fn get(&self) -> EcsResult { 200 | self.borrow::() 201 | .map(Clone::clone) 202 | } 203 | fn contains_type_id(&self, id: &TypeId) -> bool { 204 | self.map.contains_key(id) 205 | } 206 | fn contains(&self) -> bool { 207 | self.contains_type_id(&TypeId::of::()) 208 | } 209 | fn borrow_mut(&mut self) -> EcsResult<&mut C> { 210 | match self.map.get_mut(&TypeId::of::()) { 211 | Some(c) => { 212 | Ok(c.downcast_mut() 213 | .expect("ComponentMap.borrow_mut: internal downcast error")) 214 | } 215 | None => Err(NotFound::Component(TypeId::of::())), 216 | } 217 | } 218 | } 219 | 220 | impl Ecs { 221 | /// Create a new and empty entity-component system (ECS). 222 | pub fn new() -> Self { 223 | Default::default() 224 | } 225 | /// Create a new entity in the ECS without components and return its ID. 226 | pub fn create_entity(&mut self) -> EntityId { 227 | let new_id = EntityId(self.ids); 228 | self.ids += 1; 229 | self.data.insert(new_id, Default::default()); 230 | new_id 231 | } 232 | /// Return `true` if the provided entity exists in the system. 233 | pub fn exists(&self, id: EntityId) -> bool { 234 | self.data.contains_key(&id) 235 | } 236 | /// Destroy the provided entity, automatically removing any of its components. 237 | /// 238 | /// Return `NotFound::Entity` if the entity does not exist or was already deleted. 239 | pub fn destroy_entity(&mut self, id: EntityId) -> EcsResult<()> { 240 | self.data.remove(&id).map(|_| ()).ok_or_else(|| NotFound::Entity(id)) 241 | } 242 | /// For the specified entity, add a component of type `C` to the system. 243 | /// 244 | /// If the entity already has a component `prev` of type `C`, return `Some(prev)`. If not, 245 | /// return `None`. If the entity does not exist, return `NotFound::Entity`. 246 | /// 247 | /// To modify an existing component in place, see `borrow_mut`. 248 | pub fn set(&mut self, id: EntityId, comp: C) -> EcsResult> { 249 | self.data 250 | .get_mut(&id) 251 | .ok_or_else(|| NotFound::Entity(id)) 252 | .map(|map| map.set(comp)) 253 | } 254 | /// Return a clone of the requested entity's component of type `C`, or a `NotFound` variant 255 | /// if the entity does not exist or does not have that component. 256 | /// 257 | /// To examine or modify a component without making a clone, see `borrow` and `borrow_mut`. 258 | pub fn get(&self, id: EntityId) -> EcsResult { 259 | self.data 260 | .get(&id) 261 | .ok_or_else(|| NotFound::Entity(id)) 262 | .and_then(|map| map.get()) 263 | } 264 | /// Return `true` if the specified entity has a component of type `C` in the system, or 265 | /// `NotFound::Entity` if the entity does not exist. 266 | pub fn has(&self, id: EntityId) -> EcsResult { 267 | self.data 268 | .get(&id) 269 | .ok_or_else(|| NotFound::Entity(id)) 270 | .map(|map| map.contains::()) 271 | } 272 | /// Return `true` if each component type in the filter is present on the entity `id`. 273 | pub fn has_all(&self, id: EntityId, set: &ComponentFilter) -> EcsResult { 274 | let map = try!(self.data.get(&id).ok_or_else(|| NotFound::Entity(id))); 275 | Ok(set.iter().all(|type_id| map.contains_type_id(&type_id))) 276 | } 277 | /// Return a shared reference to the requested entity's component of type `C`, or a 278 | /// `NotFound` variant if the entity does not exist or does not have that component. 279 | pub fn borrow(&self, id: EntityId) -> EcsResult<&C> { 280 | self.data 281 | .get(&id) 282 | .ok_or_else(|| NotFound::Entity(id)) 283 | .and_then(|map| map.borrow()) 284 | } 285 | /// Return a mutable reference to the requested entity's component of type `C`, or a 286 | /// `NotFound` variant if the entity does not exist or does not have that component. 287 | pub fn borrow_mut(&mut self, id: EntityId) -> EcsResult<&mut C> { 288 | self.data 289 | .get_mut(&id) 290 | .ok_or_else(|| NotFound::Entity(id)) 291 | .and_then(|map| map.borrow_mut()) 292 | } 293 | /// Return an iterator over every ID in the system. 294 | #[allow(needless_lifetimes)] // https://github.com/Manishearth/rust-clippy/issues/740 295 | pub fn iter<'a>(&'a self) -> Box + 'a> { 296 | Box::new(self.data.keys().cloned()) 297 | } 298 | /// Collect all entity IDs into a vector (after emptying the vector). 299 | /// 300 | /// Useful for accessing entity IDs without borrowing the ECS. 301 | pub fn collect(&self, dest: &mut Vec) { 302 | dest.clear(); 303 | dest.extend(self.iter()); 304 | } 305 | /// Collect the IDs of all entities containing a certain set of component types into a vector. 306 | /// 307 | /// After calling this method, the vector `dest` will contain *only* those entities who have 308 | /// at least each type of component specified in the filter. 309 | pub fn collect_with<'a>(&'a self, components: &'a ComponentFilter, dest: &mut Vec) { 310 | let ids = self.data.keys().cloned(); 311 | dest.clear(); 312 | dest.extend(ids.filter(|e| { 313 | self.has_all(*e, components) 314 | .expect("Ecs.collect_with: internal id filter error") 315 | })) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Add; 2 | use std::collections::{HashMap, HashSet}; 3 | #[macro_use] 4 | extern crate recs; 5 | use recs::*; 6 | 7 | #[derive(Copy, Clone, PartialEq, Debug)] 8 | struct Vector2f { 9 | x: f32, 10 | y: f32, 11 | } 12 | 13 | impl Vector2f { 14 | fn new(x: f32, y: f32) -> Self { 15 | Vector2f { x: x, y: y } 16 | } 17 | fn new_i64(x: i64, y: i64) -> Self { 18 | Self::new(x as f32, y as f32) 19 | } 20 | } 21 | 22 | impl Add for Vector2f { 23 | type Output = Vector2f; 24 | fn add(self, other: Self) -> Self { 25 | Vector2f { 26 | x: self.x + other.x, 27 | y: self.y + other.y, 28 | } 29 | } 30 | } 31 | 32 | #[derive(Copy, Clone, PartialEq, Debug)] 33 | struct Position(Vector2f); 34 | 35 | #[derive(Copy, Clone, PartialEq, Debug)] 36 | struct Velocity(Vector2f); 37 | 38 | fn update_position(pos: &Position, vel: &Velocity) -> Position { 39 | Position(pos.0 + vel.0) 40 | } 41 | 42 | #[test] 43 | fn test_update() { 44 | let a_start = Vector2f::new(1., 3.); 45 | let b_start = Vector2f::new(-3., 4.); 46 | let c_start = Vector2f::new(-0., 1.3); 47 | let a_vel = Vector2f::new(0., 2.); 48 | let b_vel = Vector2f::new(1., 9.); 49 | let mut ecs = Ecs::new(); 50 | let a = ecs.create_entity(); 51 | let b = ecs.create_entity(); 52 | let c = ecs.create_entity(); 53 | let _ = ecs.set(a, Position(a_start)); 54 | let _ = ecs.set(a, Velocity(a_vel)); 55 | let _ = ecs.set(b, Position(b_start)); 56 | let _ = ecs.set(b, Velocity(b_vel)); 57 | let _ = ecs.set(c, Position(c_start)); 58 | let mut ids = Vec::new(); 59 | ecs.collect(&mut ids); 60 | for id in ids { 61 | let p = ecs.get::(id); 62 | let v = ecs.get::(id); 63 | if let (Ok(pos), Ok(vel)) = (p, v) { 64 | let _ = ecs.set(id, update_position(&pos, &vel)); 65 | } 66 | } 67 | assert!(ecs.get::(a) == Ok(Position(a_start + a_vel))); 68 | assert!(ecs.get::(b) == Ok(Position(b_start + b_vel))); 69 | assert!(ecs.get::(c) == Ok(Position(c_start))); 70 | } 71 | 72 | #[test] 73 | fn test_collect() { 74 | let count = 500; 75 | let mut ids = Vec::with_capacity(count); 76 | let mut starts = HashMap::with_capacity(count); 77 | let mut speeds = HashMap::with_capacity(count); 78 | let mut system = Ecs::new(); 79 | for c in 0..count { 80 | let i = c as i64; 81 | let id = system.create_entity(); 82 | let pos = Position(Vector2f::new_i64(4 * i - 7, -2 * i + 3)); 83 | let vel = Velocity(Vector2f::new_i64(-100 * i + 350, 500 * i - 900)); 84 | ids.push(id); 85 | starts.insert(id, pos); 86 | speeds.insert(id, vel); 87 | let _ = system.set(id, pos); 88 | let _ = system.set(id, vel); 89 | } 90 | // check that all ids are contained within ECS 91 | assert_eq!(ids.iter().cloned().collect::>(), 92 | system.iter().collect::>()); 93 | let components = component_filter!(Position, Velocity); 94 | let mut to_update = Vec::new(); 95 | system.collect_with(&components, &mut to_update); 96 | for id in to_update.iter().cloned() { 97 | // We can safely call unwrap() here, because 98 | // collect_with(..) guarantees that all of these 99 | // entities have Position and Velocity 100 | let pos: Position = system.get(id).unwrap(); 101 | let vel: Velocity = system.get(id).unwrap(); 102 | let new_pos = Position(pos.0 + vel.0); 103 | let _ = system.set(id, new_pos); 104 | } 105 | // check that all positions are properly updated 106 | for id in system.iter() { 107 | let target_pos = Position(starts[&id].0 + speeds[&id].0); 108 | assert_eq!(Ok(target_pos), system.get(id)); 109 | } 110 | } 111 | --------------------------------------------------------------------------------