Crate freecs

Source
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, has_components};
use serde::{Serialize, Deserialize};

// First, define components.
// They must implement: `Default + Clone + Serialize + Deserialize`

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

#[derive(Default, Clone, Debug, Serialize, Deserialize)]
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
    // This will not be serialized
    #[serde(skip)] map: HashMap<String, Box<dyn Any>>,
  }
}

§Entity and Component Access

let mut world = World::default();

// Spawn entities with components by mask
let entity = spawn_entities(&mut world, POSITION | VELOCITY, 1)[0];

// Lookup and modify a component
if let Some(pos) = get_component_mut::<Position>(&mut world, entity, POSITION) {
    pos.x += 1.0;

}

// Add new components to an entity by mask
add_components(&mut world, entity, HEALTH | VELOCITY);

// Remove components from an entity by mask
remove_components(&mut world, entity, VELOCITY | POSITION);

// Query all entities
let entities = query_entities(&world, ALL);
println!("All entities: {entities:?}");

// Query entities, iterating over all entities matching the component mask
let entities = query_entities(&world, POSITION | VELOCITY);

// Query for the first entity matching the component mask, returning early when found
let player = query_first_entity(&world, POSITION | VELOCITY);

§Systems and 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 is useful when working with more than 3 million entities. In practice, you should use .iter_mut() instead of .par_iter_mut() unless you have a large number of entities, because sequential access is more performant until you are working with extreme numbers of entities.

The example function below invokes two systems in parallel for each table in the world, filtered by component mask.

pub fn run_systems(world: &mut World, dt: f32) {
    use rayon::prelude::*;

    world.tables.par_iter_mut().for_each(|table| {
        if has_components!(table, POSITION | VELOCITY | HEALTH) {
            update_positions_system(&mut table.position, &table.velocity, dt);
        }
        if 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; // gradually decline health value
    });
}

§Performance

The table-based design means entities with the same components are stored together in contiguous memory, maximizing cache utilization. Component access and queries are O(1), with table transitions being the only O(n) operations.

Macros§