#ecs

pyro

Entity component system

7 releases

✓ Uses Rust 2018 edition

0.2.5 Jan 6, 2019
0.2.4 Oct 30, 2018
0.1.0 Oct 19, 2018

#63 in Game engines

Download history 12/week @ 2018-10-22 9/week @ 2018-10-29 90/week @ 2018-11-05 24/week @ 2018-11-12 20/week @ 2018-11-19 13/week @ 2018-11-26 27/week @ 2018-12-03 13/week @ 2018-12-10 17/week @ 2018-12-17 29/week @ 2018-12-24 14/week @ 2019-01-07

86 downloads per month

MIT license

411KB
936 lines

Pyro

A linear Entity Component System

LICENSE LICENSE Documentation Crates.io Version

Benchmarks

ecs_bench

bench defense


lib.rs:

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 [Component]s. 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);

Dependencies

~3MB
~46K SLoC