| Crates.io | brood |
| lib.rs | brood |
| version | 0.9.1 |
| created_at | 2022-08-21 06:35:25.351385+00 |
| updated_at | 2023-08-10 00:53:37.339591+00 |
| description | A fast and flexible entity component system library. |
| homepage | |
| repository | https://github.com/Anders429/brood |
| max_upload_size | |
| id | 649646 |
| size | 1,034,806 |
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.
serde, providing pain-free serialization and deserialization of World containers.rayon.no_std compatible.There are two main sides to using brood: storing entities and operating on entities.
Before storing entities, there are a few definitions that should be established:
Any trait.entity!() macro.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.
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.
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.
brood supports parallel processing through rayon. By enabling the rayon crate feature, operations on a World can be parallelized.
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.
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.
This crate is guaranteed to compile on stable rustc 1.65.0 and up.
This project is licensed under either of
at your option.
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.