Expand description
freecs is an abstraction-free ECS library for Rust, designed for high performance and simplicity.
It provides an archetypal table-based storage system for components, allowing for fast queries, fast system iteration, and parallel processing. Entities with the same components are stored together in contiguous memory, optimizing for cache coherency and SIMD operations.
A macro is used to define the world and its components, generating the entire entity component system at compile time. The generated code contains only plain data structures and free functions that transform them.
The core implementation is ~500 loc, is fully statically dispatched and does not use object orientation, generics, or traits.
§Creating a World
use freecs::{ecs, table_has_components, EntityId};
// First, define components.
// They must implement: `Default`
#[derive(Default, Clone, Debug)]
struct Position { x: f32, y: f32 }
#[derive(Default, Clone, Debug)]
struct Velocity { x: f32, y: f32 }
// Then, create a world with the `ecs!` macro.
// Resources are stored independently of component data.
// The `World` and `Resources` type names can be customized.
ecs! {
World {
position: Position => POSITION,
velocity: Velocity => VELOCITY,
health: Health => HEALTH,
}
Resources {
delta_time: f32
}
}§Entity and Component Access
let mut world = World::default();
// Spawn entities with components by mask
let entity = world.spawn_entities(POSITION | VELOCITY, 1)[0];
// Lookup and modify a component
if let Some(pos) = world.get_component_mut::<Position>(entity, POSITION) {
pos.x += 1.0;
}
// Add new components to an entity by mask
world.add_components(entity, HEALTH | VELOCITY);
// Remove components from an entity by mask
world.remove_components(entity, VELOCITY | POSITION);
// Query all entities
let entities = world.get_all_entities();
println!("All entities: {entities:?}");
// Query entities, iterating over all entities matching the component mask
let entities = world.query_entities(POSITION | VELOCITY);
// Query for the first entity matching the component mask, returning early when found
let player = world.query_first_entity(POSITION | VELOCITY);§Systems
A system is any function that takes a mutable reference to a world, querying the world for entities to process and operating on their components.
fn example_system(world: &mut World) {
for entity in world.query_entities(POSITION | VELOCITY) {
if let Some(position) = world.get_component_mut::<Position>(entity, POSITION) {
position.x += 1.0;
}
}
}§Parallel Processing
Systems are plain functions that iterate over the component tables and transform component data.
Parallelization of systems can be done with rayon.
which lets you replace .iter_mut() with .par_iter_mut() when iterating over tables.
In practice, you should use
.iter_mut()instead of.par_iter_mut()because sequential access is typically more performant until you are working with extreme numbers of entities.
pub fn run_systems(world: &mut World, dt: f32) {
use rayon::prelude::*;
world.tables.par_iter_mut().for_each(|table| {
if table_has_components!(table, POSITION | VELOCITY | HEALTH) {
update_positions_system(&mut table.position, &table.velocity, dt);
}
if table_has_components!(table, HEALTH) {
health_system(&mut table.health);
}
});
}
// The system itself can also access components in parallel and be inlined for performance.
#[inline]
pub fn update_positions_system(positions: &mut [Position], velocities: &[Velocity], dt: f32) {
positions
.par_iter_mut()
.zip(velocities.par_iter())
.for_each(|(pos, vel)| {
pos.x += vel.x * dt;
pos.y += vel.y * dt;
});
}
#[inline]
pub fn health_system(health: &mut [Health]) {
health.par_iter_mut().for_each(|health| {
health.value *= 0.98;
});
}§Change Detection
freecs provides an opt-in change detection system that allows you to track when components are modified. This is useful for systems that only need to process entities when their data has changed.
§Basic Usage
// Get mutable access and modify a component
if let Some(pos) = world.get_component_mut::<Position>(entity, POSITION) {
pos.x += velocity.x * dt;
pos.y += velocity.y * dt;
}
// Explicitly mark the component as changed
world.mark_changed(entity, POSITION);
// Later, process change events
while let Some(event) = world.try_next_event() {
match event {
Event::ComponentChanged { kind, entity } => {
println!("Component {:b} changed for entity {:?}", kind, entity);
}
}
}§API
world.mark_changed(entity, mask)- Mark one or more components as changedworld.try_next_event()- Pop the next change event from the queueworld.clear_events()- Clear all pending change events
§Design
Change detection in freecs is explicit rather than automatic. This gives you full control over when change events are generated, avoiding spurious events when components are accessed but not modified.
You can mark multiple components as changed in a single call:
// Mark both position and velocity as changed
world.mark_changed(entity, POSITION | VELOCITY);The event queue is stored in the world’s Resources struct and is automatically available
when you create a world with the ecs! macro.
Re-exports§
pub use paste;