Crate rinecs[][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:

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.

Re-exports

pub use operators::EntitiesDataParallel;
pub use entity::Entity;
pub use entity::Entities;
pub use entity::EntitiesThreadLocal;
pub use entity::EntitiesExt;
pub use entity::EntityBuilder;
pub use entity::EntitiesCreation;
pub use entity::EntityStorages;
pub use entity::EntityStoragesThreadLocal;
pub use entity::EntityStoragesCreation;
pub use entity::EntityStoragesExt;
pub use streaming_iterator;

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

Macros

cast
unwrap_or_return

Structs

BuilderConditions
BuilderConditionsCreation
BuilderConditionsCreationOnce
BuilderConditionsThreadLocal
BuilderElse
BuilderElseCreation
BuilderElseCreationOnce
BuilderElseThreadLocal
EntitiesDebug

EntitiesDebug is passed to DebugSystems when they are run and allows to access debug information for entities’ compoenents

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 Send resources for reading and writing

ResourcesCreation

Gives access to the world global Send and thread local resources for reading and writing

ResourcesThreadLocal

Gives access to the world global Send and thread local resources for reading and writing

SystemBuilder
SystemBuilderCreation
SystemBuilderThreadLocal
UniqueEntities
UniqueEntitiesIterWrapper
UniqueEntity
World

Main rinecs object that contains all entities, resources and systems.

Enums

SystemCondition
SystemId

Traits

Barrier

A Barrier is one more way to synchronize systems.

CreationContextExt
CreationSystem

Trait for systems that can create entities and resources.

DebugParameter
EntitiesCreationExt

Trait implemented by World and CreationProxy allows to create functions that can create new entities, components and resources without relying on an specific object

IntoEntitiesIterator
IntoUniqueIterator
IntoUniqueParallelIterator
ResourcesCreationExt
ResourcesExt
ResourcesThreadLocalExt
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 Send systems.

SystemThreadLocal

Trait for systems that can only run from the main thread but in parallel with Send systems.

Type Definitions

SystemConditionCreation
SystemConditionSend
SystemConditionThreadLocal