Expand description
What is an Entity Component System?
An Entity Component System or ECS is very similar to a relational database like SQL. The
World is the data store where game objects (also known as Entity) live. An Entity
contains data or Components.
The ECS can efficiently query those components.
Give me all entities that have a position and velocity component, and then update the position based on the velocity.
type PosVelQuery = (Write<Pos>, Read<Vel>);
// ^^^^^ ^^^^
// Mutable Immutable
world.matcher::<All<PosVelQuery>>().for_each(|(pos, vel)|{
pos += vel;
})Internals
Overview
- Iteration is always linear.
- Different component combinations live in a separate storage
- Removing entities does not create holes.
- All operations are designed to be used in bulk.
- Borrow rules are enforced at runtime. See
RuntimeBorrow Entityis using a wrapping generational index. SeeEntity::version
// A Storage that contains `Pos`, `Vel`, `Health`.
(
[Pos1, Pos2, Pos3, .., PosN],
[Vel1, Vel2, Vel3, .., VelN],
[Health1, Health2, Health3, .., HealthN],
)
// A Storage that contains `Pos`, `Vel`.
(
[Pos1, Pos2, Pos3, .., PosM]
[Vel1, Vel2, Vel3, .., VelM]
)
Iteration is fully linear with the exception of jumping to different storages.
The iteration pattern from the query above would be
positions: [Pos1, Pos2, Pos3, .., PosN], [Pos1, Pos2, Pos3, .., PosM]
velocities: [Vel1, Vel2, Vel3, .., VelN], [Vel1, Vel2, Vel3, .., VelM]
^
Jump occurs hereThe jump is something like a chain of two iterators. We look at all the storages
that match specific query. If the query would be Write<Position>, then we would
look for all the storages that contain a position array, extract the iterators and chain them
Every combination of components will be in a separate storage. This guarantees that iteration will always be linear.
Benchmarks

Getting started
extern crate pyro;
use pyro::{ World, Entity, Read, Write, All, SoaStorage };
struct Position;
struct Velocity;
// By default creates a world backed by a [`SoaStorage`]
let mut world: World<SoaStorage> = World::new();
let add_pos_vel = (0..99).map(|_| (Position{}, Velocity{}));
// ^^^^^^^^^^^^^^^^^^^^^^^^
// A tuple of (Position, Velocity),
// Note: Order does *not* matter
// Appends 99 entities with a Position and Velocity component.
world.append_components(add_pos_vel);
// Appends a single entity
world.append_components(Some((Position{}, Velocity{})));
// Requests a mutable borrow to Position, and an immutable borrow to Velocity.
// Common queries can be reused with a typedef like this but it is not necessary.
type PosVelQuery = (Write<Position>, Read<Velocity>);
// Retrieves all entities that have a Position and Velocity component as an iterator.
world.matcher::<All<PosVelQuery>>().for_each(|(pos, vel)|{
// ...
});
// The same query as above but also retrieves the entities and collects the entities into a
// `Vec<Entity>`.
let entities: Vec<Entity> =
world.matcher_with_entities::<All<PosVelQuery>>()
.filter_map(|(entity, (pos, vel))|{
Some(entity)
}).collect();
// Removes all the entities
world.remove_entities(entities);
let count = world.matcher::<All<PosVelQuery>>().count();
assert_eq!(count, 0);Structs
Iterator is used to end a borrow from a query like World::matcher.World::matcher, but this is not possible if both borrows would be mutable.
Instead we track active borrows at runtime. Multiple reads are allowed but read/write and
write/write are not.Traits
BuildStorage is used to create different Storages at runtime. See also
AppendComponents and World::append_componentsStorage won’t have any arrays or vectors when it is created. RegisterComponent can
register or add those component arrays. See also EmptyStorage::register_componentStorage allows to abstract over different types of storages. The most common storage that
implements this trait is SoaStorage.