Crate pyro

source ·
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
  • Entity is using a wrapping generational index. See Entity::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 here

The 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

Is satisfied when a storages contains all of the specified components.
The Iterator is used to end a borrow from a query like World::matcher.
Serves as an ID to lookup components for entities which can be in different storages.
Implements Fetch and allows components to be borrowed immutable.
Rust’s borrowing rules are not flexible enough for an ECS. Often it would preferred to nest multiple queries like 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.
A runtime SoA storage. It stands for Structure of Arrays.
World is the heart of this library. It owns all the Components and Storages. It also manages entities and allows Components to be safely queried.
Implements Fetch and allows components to be borrowed mutable.

Traits

BuildStorage is used to create different Storages at runtime. See also AppendComponents and World::append_components
A helper trait that works in lockstep with Read and Write to borrow components either mutable or immutable.
Allows to match over different Storages. See also All.
Is implemented for Read and Write and is used to insert reads and writes into the correct HashSet.
Allows to query multiple components from a Storage. See also All.
A Storage won’t have any arrays or vectors when it is created. RegisterComponent can register or add those component arrays. See also EmptyStorage::register_component
Storage allows to abstract over different types of storages. The most common storage that implements this trait is SoaStorage.

Type Definitions