12 releases (7 breaking)

0.9.1 Aug 10, 2023
0.9.0 Apr 22, 2023
0.8.2 Apr 3, 2023
0.7.0 Mar 28, 2023
0.3.0 Oct 29, 2022

#196 in Game dev

MIT/Apache

1MB
21K SLoC

brood

GitHub Workflow Status codecov.io crates.io docs.rs MSRV License

A fast and flexible entity component system library.

brood is built from the ground-up with the main goals of being ergonomic to use while also being as fast as, if not faster than, other popular entity component system (commonly abbreviated as ECS) libraries. brood is built with heterogeneous lists to allow for sets of arbitrary numbers of components, meaning there are no limitations on the size of your entities or the scope of your system views. All features you would expect from a standard ECS library are present, including interoperation with the serde and rayon libraries for serialization and parallel processing respectively.

Key Features

  • Entities made up of an arbitrary number of components.
  • Built-in support for serde, providing pain-free serialization and deserialization of World containers.
  • Inner- and outer-parallelism using rayon.
  • Minimal boilerplate.
  • no_std compatible.

Usage

There are two main sides to using brood: storing entities and operating on entities.

Storing Entities

Before storing entities, there are a few definitions that should be established:

  • Component: A single piece of data. In terms of this library, it is any type that implements the Any trait.
  • Entity: A set of components. These are defined using the entity!() macro.
  • World: A container of entities.

Components are defined by simply defining their types. For example, the following structs are components:

struct Position {
    x: f32,
    y: f32,
}

struct Velocity {
    x: f32,
    y: f32,
}

In order to use these components within a World container, they will need to be contained in a Registry, provided to a World on creation. A Registry can be created using the Registry!() macro.

use brood::Registry;

type Registry = Registry!(Position, Velocity);

A World can then be created using this Registry, and entities can be stored inside it.

use brood::{entity, World};

let mut world = World::<Registry>::new();

// Store an entity inside the newly created World.
let position = Position {
    x: 3.5,
    y: 6.2,
};
let velocity = Velocity {
    x: 1.0,
    y: 2.5,
};
world.insert(entity!(position, velocity));

Note that entities stored in world above can be made up of any subset of the Registry's components, and can be provided in any order.

Operating on Entities

To operate on the entities stored in a World, a System must be used. Systems are defined to operate on any entities containing a specified set of components, reading and modifying those components. An example system could be defined and run as follows:

use brood::{query::{filter, result, Views}, registry, system::System};

struct UpdatePosition;

impl System for UpdatePosition {
    type Filter: filter::None;
    type Views<'a>: Views!(&'a mut Position, &'a Velocity);
    type ResourceViews: Views!();
    type EntryViews: Views!();

    fn run<'a, R, S, I, E>(
        &mut self,
        query_results: Result<R, S, I, Self::ResourceViews<'a>, Self::EntryViews<'a>, E>,
    ) where
        R: registry::Registry,
        I: Iterator<Item = Self::Views<'a>>,
    {
        for result!(position, velocity) in query_results.iter {
            position.x += velocity.x;
            position.y += velocity.y;
        }
    }
}

world.run_system(&mut UpdatePosition);

This system will operate on every entity that contains both the Position and Velocity components (regardless of what other components they may contain), updating the Position component in-place using the value contained in the Velocity component.

There are lots of options for more complicated Systems, including optional components, custom filters, and post-processing logic. See the documentation for more information.

Serialization/Deserialization

brood provides first-class support for serialization and deserialization using serde. By enabling the serde crate feature, World containers and their contained entities can be serialized and deserialized using serde Serializers and Deserializers. Note that a World is (de)serializable as long as every component in the World's Registry is (de)serializable.

For example, a World can be serialized to bincode (and deserialized from the same) as follows:

use brood::{entity, Registry, World};

#[derive(Deserialize, Serialize)]
struct Position {
    x: f32,
    y: f32,
}

#[derive(Deserialize, Serialize)]
struct Velocity {
    x: f32,
    y: f32,
}

type Registry = Registry!(Position, Velocity);

let mut world = World::<Registry>::new();

// Insert several entities made of different components.
world.insert(entity!(Position {
    x: 1.0,
    y: 1.1,    
});
world.insert(entity!(Velocity {
    x: 0.0,
    y: 5.0,
});
world.insert(entity!(Position {
    x: 4.2,
    y: 0.1,
}, Velocity {
    x: 1.1,
    y: 0.4,
});

let encoded = bincode::serialize(&world).unwrap();

let decoded_world = bincode::deserialize(&encoded).unwrap();

Note that there are two modes for serialization, depending on whether the serializer and deserializer is human readable. Human readable serialization will serialize entities row-wise, which is slower but easier to read by a human. Non-human readable serialization will serialize entities column-wise, which is much faster but much more difficult to read manually.

Parallel Processing

brood supports parallel processing through rayon. By enabling the rayon crate feature, operations on a World can be parallelized.

Operating on Entities in Parallel

To parallelize system operations on entities (commonly referred to as inner-parallelism), a ParSystem can be used instead of a standard System. This will allow the ParSystem's operations to be spread across multiple CPUs. For example, a ParSystem can be defined as follows:

use brood::{entity, query::{filter, result, Views}, Registry, registry, World, system::ParSystem};
use rayon::iter::ParallelIterator;

struct Position {
    x: f32,
    y: f32,
}

struct Velocity {
    x: f32,
    y: f32,
}

type Registry = Registry!(Position, Velocity);

let mut world = World::<Registry>::new();

// Store an entity inside the newly created World.
let position = Position {
    x: 3.5,
    y: 6.2,
};
let velocity = Velocity {
    x: 1.0,
    y: 2.5,
};
world.insert(entity!(position, velocity));

struct UpdatePosition;

impl ParSystem for UpdatePosition {
    type Filter: filter::None;
    type Views<'a>: Views!(&'a mut Position, &'a Velocity);
    type ResourceViews: Views!();
    type EntryViews: Views!();

    fn run<'a, R, S, I, E>(
        &mut self,
        query_results: Result<R, S, I, Self::ResourceViews<'a>, Self::EntryViews<'a>, E>,
    ) where
        R: registry::Registry,
        I: ParallelIterator<Item = Self::Views<'a>>,
    {
        query_results.iter.for_each(|result!(position, velocity)| {
            position.x += velocity.x;
            position.y += velocity.y;
        });
    }
}

world.run_par_system(&mut UpdatePosition);

Defining ParSystems is very similar to defining Systems. See the documentation for more definition options.

Running Systems in Parallel

Multiple Systems and ParSystems can be run in parallel as well by defining a Schedule. A Schedule will automatically divide Systems into stages which can each be run all at the same time. These stages are designed to ensure they do not violate Rust's borrowing and mutability rules and are completely safe to use.

Define and run a Schedule that contains multiple Systems as follows:

use brood::{entity, query::{filter, result, Views}, Registry, registry, World, system::{schedule, schedule::task, System}};

struct Position {
    x: f32,
    y: f32,
}

struct Velocity {
    x: f32,
    y: f32,
}

struct IsMoving(bool);

type Registry = Registry!(Position, Velocity, IsMoving);

let mut world = World::<Registry>::new();

// Store an entity inside the newly created World.
let position = Position {
    x: 3.5,
    y: 6.2,
};
let velocity = Velocity {
    x: 1.0,
    y: 2.5,
};
world.insert(entity!(position, velocity, IsMoving(false)));

struct UpdatePosition;

impl System for UpdatePosition {
    type Filter: filter::None;
    type Views<'a>: Views!(&'a mut Position, &'a Velocity);
    type ResourceViews: Views!();
    type EntryViews: Views!();

    fn run<'a, R, S, I, E>(
        &mut self,
        query_results: Result<R, S, I, Self::ResourceViews<'a>, Self::EntryViews<'a>, E>,
    ) where
        R: registry::Registry,
        I: Iterator<Item = Self::Views<'a>>,
    {
        for result!(position, velocity) in query_results.iter {
            position.x += velocity.x;
            position.y += velocity.y;
        }
    }
}

struct UpdateIsMoving;

impl System for UpdateIsMoving {
    type Filter: filter::None;
    type Views<'a>: Views!(&'a Velocity, &'a mut IsMoving);
    type ResourceViews: Views!();
    type EntryViews: Views!();

    fn run<'a, R, S, I, E>(
        &mut self,
        query_results: Result<R, S, I, Self::ResourceViews<'a>, Self::EntryViews<'a>, E>,
    ) where
        R: registry::Registry,
        I: Iterator<Item = Self::Views<'a>>,
    {
        for result!(velocity, is_moving) in query_results.iter {
            is_moving.0 = velocity.x != 0.0 || velocity.y != 0.0;
        }
    }
}

let mut schedule = schedule!(task::System(UpdatePosition), task::System(UpdateIsMoving));

world.run_schedule(&mut schedule);

Note that stages are determined by the Views of each System. Systems whose Views do not contain conflicting mutable borrows of components are grouped together into a single stage.

Minimum Supported Rust Version

This crate is guaranteed to compile on stable rustc 1.65.0 and up.

License

This project is licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Dependencies

~0.7–1.2MB
~20K SLoC