Crate rin::ecs [−][src]
Rinecs
rinecs is a fast and easy to use ECS library oriented to work mostly in conjuntion with rin and associated crates
ECS
ECS stands for Entity Component System, understanding what this three elements mean and how they are used in rinecs will help you understand how to use rinecs
Entity
An entity in ECS is just an id, a number identifying such entity. In your application an entity could be a particle in a particle system or a character in a video game
Component
A component in ECS is the data that the application uses, each component is associated to an entity.
For example in a particle system there could be components for the Position, Velocity, Size and Color.
Each particle would have these components but there could be other entities with only some of this components or others in the same world
System
A system in ECS is the functionality that reads and modifies the components in the world
For example in our particle system a ParticleUpdateSystem could add the Velocity to the Position of every entity that have both a Position and a Velocity
Another system could for example change the Color based on the Velocity for entities that have both Color and Velocity
Differences with object oriented programming
In object oriented programming each object contains it’s own data, for example in the particle system we are using as example if we used OO each particle would have it’s own position, velocity, size, something like:
struct Position{x: f32, y: f32} struct Velocity{x: f32, y: f32} struct Particle { position: Position, velocity: Velocity, size: f32, }
Now if we wanted to update this particle each frame to change it’s postion based on it’s velocity we would usually create an update method that updated the position by adding the velocity to it. Let’s say that we also want to update the size relative to the velocity of the particle:
impl Particle{ fn update(&mut self){ self.position.x += self.velocity.x; self.position.x += self.velocity.y; self.size = self.velocity.x + self.velocity.y; } }
Now we would create a vector of particles, update them all every frame and draw them afterwards probably through a draw method in the particle:
let mut particles: Vec<Particle> = vec![]; for particle in particles.iter_mut() { particle.update(); } for particle in particles.iter() { particle.draw(); }
If we now wanted to add a new type of object that updated it’s position in the same way that the particle does but didn’t change it’s size, lets say a FixedSizeParticle, we would need to reimplement all of the update and even the draw logic.
In ECS the data for each particle is not contained on a specific object which solves this problem:
#[macro_use] extern crate rinecs_derive; use rinecs::*; #[derive(Component, Debug, Clone, Copy)] pub struct Position{pub x: f32, pub y: f32} #[derive(Component, Debug, Clone, Copy)] pub struct Velocity{pub x: f32, pub y: f32} #[derive(Component, Debug, Clone, Copy)] pub struct Size(pub f32); #[system(name = "update position")] #[needs("Velocity")] #[updates("Position")] fn update_position_system(mut entities: Entities, resources: Resources){ for (pos, vel) in entities.iter_for_mut::<(Write<Position>, Read<Velocity>)>(){ pos.x += vel.x; pos.y += vel.y; } } #[system(name = "update size")] #[needs("Velocity")] #[updates("Size")] fn update_size_system(mut entities: Entities, resources: Resources){ for (size, vel) in entities.iter_for_mut::<(Write<Size>, Read<Velocity>)>(){ size.0 = vel.x + vel.y; } } #[system(name = "update velocity")] #[updates("Velocity")] fn update_velocity_system(mut entities: Entities, resources: Resources){ for vel in entities.iter_for_mut::<Write<Velocity>>(){ vel.x *= 0.9999; vel.y *= 0.9999; } } #[system_thread_local(name = "update size")] #[needs("Position", "Size")] fn renderer(entities: EntitiesThreadLocal, resources: ResourcesThreadLocal){ for (pos, size) in entities.iter_for::<(Read<Position>, ReadOption<Size>)>(){ let size = size.unwrap_or(&Size(10.)); // draw circle at pos with radius size } } fn main(){ let mut world = World::new(); world.new_entity() .add(Position{x: 0., y: 0.}) .add(Velocity{x: 10., y: 10.}) .add(Size(0.)); world.new_entity() .add(Position{x: 0., y: 0.}) .add(Velocity{x: -10., y: -10.}) .add(Size(0.)); world.new_entity() .add(Position{x: 0., y: 0.}) .add(Velocity{x: 10., y: -10.}); world.add_system(update_velocity_system); world.add_system(update_position_system); world.add_system(update_size_system); world.add_system_thread_local(renderer); while !done { world.run_once(); } }
Now the logic to update the position based on the velocity is independent from the particle, as well as all the other logic. We divide it in systems and each system knows what to do based on the data it uses. It doesn’t know if the position belongs to a particle, a character or any other element in the system.
Also now the updating of the postion and the size happen in parallel, in different threads, for free. Which can accelerate greatly the performance of the application if used properly.
Apart from this, the memory layout in the ECS version is potentially much faster since all the positions, velocities and sizes are contiguous in memory. For something like the particle in our example it probably won’t make any difference but for more complex obejcts with much more data the fact that each system only uses the data that it needs and that that data is contiguous in memory can make a big difference in terms of performance.
Finally you might have noticed that the renderer in our example is added as a thread local system, this makes this system always run on the main thread which might be needed depending on the rendering technology you are using (for example with opengl).
Operators
In the previous example the different systems iterate over the data they use by using certain operators.
For example the update_velocity system, only uses the Write
operator with a Velocity
parameter,
this gives it access to all the velocity components in the world no matter to which entity they belong.
In that example we’ve also used Read, which is similar to Write but gives read only access to the components and ReadOption that reads the component if that specific entity has it or returns None if not.
Components can be grouped using tuples of types when specifying the operators.
For the full documentation of all the operators in the system see operators
Filters
In some cases it might be useful to treat a set of operators as an object. For example in the previous example we might be doing some operations in different systems. Instead of replicating the functionality we can use a Filter to group a set of components and implement methods over that object:
#[derive(Filter)] struct Particle<'a>{ position: Read<'a, Position>, size: ReadOption<'a, Size>, } impl<'a> Particle<'a>{ fn render(&self){ let size = self.size.unwrap_or(&Size(10.)); println!("{},{} {}", self.position.x, self.position.y, size.0); } } fn renderer(entities: EntitiesThreadLocal, resources: ResourcesThreadLocal){ for particle in entities.iter_for::<Particle>(){ particle.render() } }
As you can see in the previous example Filters can be used anywhere you would use an operator and equivalent to a tuple of all their members in terms of which entities they’ll match
Systems run order
System’s run order is determined by creating a dependencies graph from anotations in the system implementation.
In the following example:
#[macro_use] extern crate rinecs_derive; use rinecs::*; #[derive(Component, Debug, Clone, Copy)] pub struct Position{pub x: f32, pub y: f32} #[derive(Component, Debug, Clone, Copy)] pub struct Velocity{pub x: f32, pub y: f32} #[derive(Component, Debug, Clone, Copy)] pub struct Size(pub f32); #[system(name = "update position")] #[needs("Velocity")] #[updates("Position")] fn update_position_system(mut entities: Entities, resources: Resources){ for (pos, vel) in entities.iter_for_mut::<(Write<Position>, Read<Velocity>)>(){ pos.x += vel.x; pos.y += vel.y; } } #[system(name = "update size")] #[needs("Velocity")] #[updates("Size")] fn update_size_system(mut entities: Entities, resources: Resources){ for (size, vel) in entities.iter_for_mut::<(Write<Size>, Read<Velocity>)>(){ size.0 = vel.x + vel.y; } } #[system(name = "update velocity")] #[updates("Velocity")] fn update_velocity_system(mut entities: Entities, resources: Resources){ for vel in entities.iter_for_mut::<Write<Velocity>>(){ vel.x *= 0.9999; vel.y *= 0.9999; } } #[system_thread_local(name = "update size")] #[needs("Position", "Size")] fn renderer(entities: EntitiesThreadLocal, resources: ResourcesThreadLocal){ for (pos, size) in entities.iter_for::<(Read<Position>, ReadOption<Size>)>(){ let size = size.unwrap_or(&Size(10.)); // draw circle at pos with radius size } }
update_position_system
specifies that it updates Position
and needs Velocity
and
update_velocity_system
updates velocity and doesn’t need anything. That means that
update_velocity_system
will run before update_position_system
.
update_size_system
needs Velocity
and updates Size
so it will run after
update_velocity_system
and in parallel with update_position_system
since they don’t have any
dependency in common.
Apart from needs
and updates
a system can also specify the following annotations:
#[after("update_size_system")]
specifies that a system will run afterupdate_size_system
.#[before("update_size_system")]
specifies that a system will run beforeupdate_size_system
.#[after_name("update size")] specifies that a system will run after the system with name "update size" as specified in it's
#[system(name=“update size”]` anotation.#[before_name("update size")]
specifies that a system will run before the system with name “update size”.#[read("Position")]
specifies that this system reads the Position component. This won’t change the dependencies graph. It is only used to avoid running this systems in parallel with others thatupdates
orwrites
to thePosition
components.#[writes("Position")]
specifies that this system writes the Position component. This won’t change the dependencies graph. It is only used to avoid running this systems in parallel with others thatneeds
,reads
,updates
orwrites
to thePosition
components.
Although most times needs and updates are enough, sometimes a needs
/ updates
graph can produce
a cycle which is not allowed. In that case when running the world for the first time, the
application will panic specifying the systems involved in that cycle. To solve it usually we can
identify a system that doesn’t need the very last information from the conflictive component and
substitute a needs
for a reads
. Or perhaps identify a system which update is not really
important for the dependency graph, and change an updates
for a writes
.
The dependency graph can be debugged by printing it after adding all the systems using:
world.render_systems_dependency_graph("systems.dot");
Which creates a vizgraph file with the systems dependency graph that can be viewed with any vizgraph application, or even plugins for several IDEs or text editors.
Modules
component | Different component traits usually implmented using the corresponding derive attribute. |
entity | |
operators | Operators are the way to access the different components in rinecs |
storage | Different storage types used internally to store entities components |
streaming_iterator | Streaming iterators. |
Macros
cast | |
unwrap_or_return |
Structs
BuilderConditions | |
BuilderConditionsCreation | |
BuilderConditionsCreationOnce | |
BuilderConditionsThreadLocal | |
BuilderElse | |
BuilderElseCreation | |
BuilderElseCreationOnce | |
BuilderElseThreadLocal | |
CacheGuard | |
CreationSto | |
Entities | Allows to access and modify the |
EntitiesCreation |
|
EntitiesDebug |
|
EntitiesThreadLocal | EntitiesThreadLocal allows to access and modify any entities components. |
Entity | The id of an entity. |
EntityBuilder | Adds an entity and it’s components to the world. |
EntityStorages | Allows to access and modify the |
EntityStoragesCreation |
|
EntityStoragesThreadLocal | EntitiesThreadLocal allows to access and modify any entities components. |
Has | Operator that matches every entity that doe has this component without locking it’s storage |
HasOption | Operator that matches every entity no matter if it has the component or not but doesn’t give access to the component, only returns a boolean specifying if the component is present or not. |
HasOr | Matches entities that have any of the passed components with read only access to those components |
NodeId | A node identifier within a particular |
NodePtr | |
NodePtrMut | |
NodeRef | |
NodeRefMut | |
Not | Operator that matches every entity that doesn’t have this component |
Ptr | |
PtrMut | |
Read | Operator that matches all entities that contain this component for read only access |
ReadAndParent | Operator over hierarchical components that will select an entity’s component and it’s parent for read only |
ReadAndParentRef | Operator over hierarchical components that will select an entity’s component for read only and apply another operator on it’s parent |
ReadGuardRef | |
ReadHierarchical | |
ReadNot | Operator that matches every entity that has the first component and not the second |
ReadOption | Operator that matches every entity no matter if it has the component or not with read only access to that component |
ReadOr | Matches entities that have any of the passed components with read only access to those components |
Ref | Applies a component in another entity referenced through a component that Derefs to an Entity: |
RefN | |
Res | Marker to access resources from functions that use signature to access them instead of resources parameter |
ResMut | Marker to access resources mutably from functions that use signature to access them instead of resources parameter |
Resources | Gives access to the world global |
ResourcesCreation | Gives access to the world global |
ResourcesThreadLocal | Gives access to the world global |
Sto | Used as a argument in storages systems, wraps the storage for an operator or query |
SystemBuilder | |
SystemBuilderCreation | |
SystemBuilderThreadLocal | |
URef | Matches a component in another entity referenced through a component that Derefs to an Entity: |
UniqueEntities | |
UniqueEntitiesIterWrapper | |
UniqueEntity | |
World | Main rinecs object that contains all entities, resources and systems. |
Write | Operator that matches all entity’s that contain this component for read and write access |
WriteAndParent | Operator over hierarchical components that will select an entity’s component and it’s parent for read and write |
WriteGuardRef | |
WriteHierarchical | |
WriteOption | Operator that matches every entity no matter if it has the component or not with read and write access to that component |
Enums
Bitmask | |
SystemCondition | |
SystemId |
Traits
Barrier | A Barrier is one more way to synchronize systems. |
ChangedData | |
ChangedDataSend | |
ChangedOrderedData | |
ChangedOrderedDataSend | |
Changes | |
ChangesData | |
ChangesDataSend | |
ChangesOrderedData | |
ChangesOrderedDataSend | |
Component | Main Component trait. |
ComponentSend |
|
ComponentThreadLocal | Non |
CreationContextExt | |
CreationSystem | Trait for systems that can create entities and resources. |
DataAccesses | |
DebugParameter | |
EntitiesCreationExt | Trait implemented by |
EntitiesExt | Common trait for all Entities like objects. |
EntityStoragesExt | Common trait for all Entities like objects. |
FromComponent | |
HierarchicalOneToNStorage | |
HierarchicalStorage | |
IntoEntitiesIterator | |
IntoIter | |
IntoIterMut | |
IntoOrderedIter | |
IntoOrderedIterMut | |
IntoSendStorage | |
IntoStorages | |
IntoUniqueIterator | |
IntoUniqueParallelIterator | |
NToOneComponent | N to One components are just references to other entities. |
NToOneComponentSend |
|
NToOneComponentThreadLocal | Non |
OneToNComponent | One to N Component trait. |
OneToNComponentSend |
|
OneToNComponentThreadLocal | Non |
OneToNStorage | |
OneToOneComponent | One to One components are just references to other entities where the reference must be unique. |
OneToOneComponentSend |
|
OneToOneComponentThreadLocal | Non |
OrderedData | |
OrderedDataSend | |
ReferenceType | |
ResourcesCreationExt | |
ResourcesExt | |
ResourcesThreadLocalExt | |
Storage | |
StorageEntitiesExt | |
StorageRef | |
StreamingIterator | An interface for dealing with streaming iterators. |
StreamingIteratorMut | An interface for dealing with mutable streaming iterators. |
System | Trait for systems that can run from any thread in parallel with other systems. |
SystemDebug | System that gives access to debug information for entites and their components |
SystemOnce | Trait for systems that will run only once from any thread in parallel with other systems. |
SystemOnceThreadLocal | Trait for systems that will run only once from the main thread but parallel with |
SystemThreadLocal | Trait for systems that can only run from the main thread but in parallel with
|
TupleStorageEntities | |
UnorderedData | |
UnorderedDataSend |
Type Definitions
MaskType | |
ReadRef | |
SystemConditionCreation | |
SystemConditionSend | |
SystemConditionThreadLocal |
Attribute Macros
Derive Macros
Component | |
Filter | |
HierarchicalComponent | |
HierarchicalOneToNComponent | |
NToOneComponent | |
OneToNComponent | |
OneToOneComponent | |
Tag |