freecs 0.4.8

A high-performance, archetype-based Entity Component System (ECS) written in Rust.
Documentation

freECS

freecs is a abstraction-free table-based ECS library for Rust, in about ~500 lines

A macro is used to define the world and its components, and generates the entity component system as part of your source code at compile time.

The internal implementation is ~500 loc (aside from tests, comments, and example code), and does not use object orientation, generics, traits, or dynamic dispatch.

Quick Start

Add this to your Cargo.toml:

[dependencies]

freecs = "0.4.8"



# (optional) add rayon if you want to parallelize systems

rayon = "^1.10.0"

And in main.rs:

use freecs::{ecs, table_has_components, EntityId};
use rayon::prelude::*;

ecs! {
    World {
        position: Position => POSITION,
        velocity: Velocity => VELOCITY,
        health: Health => HEALTH,
    }
    Resources {
        delta_time: f32
    }
}

pub fn main() {
    let mut world = World::default();

    // Spawn entities with components
    let entity = world.spawn_entities(POSITION | VELOCITY, 1)[0];
    println!(
        "Spawned {} with position and velocity",
        world.get_all_entities().len()
    );

    // Read arbitrary components
    let position = world.get_component::<Position>(entity, POSITION);
    println!("Position: {:?}", position);

    // Same as the above but more concise, these are generated for each component
    let position = world.get_position(entity);
    println!("Position: {:?}", position);

    // Mutate a component
    if let Some(position) = world.get_position_mut(entity) {
        position.x += 1.0;
    }

    // Get an entity's component mask
    println!(
        "Component mask before adding health component: {:b}",
        world.component_mask(entity).unwrap()
    );

    // Add a new component to an entity
    world.add_components(entity, HEALTH);

    println!(
        "Component mask after adding health component: {:b}",
        world.component_mask(entity).unwrap()
    );

    // Query all entities
    let entities = world.get_all_entities();
    println!("All entities: {entities:?}");

    // Query all entities with a specific component
    let players = world.query_entities(POSITION | VELOCITY | HEALTH);
    println!("Player entities: {players:?}");

    // Query the first entity with a specific component,
    // returning early instead of checking remaining entities
    let first_player_entity = world.query_first_entity(POSITION | VELOCITY | HEALTH);
    println!("First player entity : {first_player_entity:?}");

    // Remove a component from an entity
    world.remove_components(entity, HEALTH);

    // Systems are functions that iterate over
    // the component tables and transform component data.
    // This function invokes two systems in parallel
    // for each table in the world filtered by component mask.
    systems::run_systems(&mut world);

    // Despawn entities, freeing their table slots for reuse
    world.despawn_entities(&[entity]);
}

use components::*;
mod components {
    #[derive(Default, Debug, Clone)]
    pub struct Position {
        pub x: f32,
        pub y: f32,
    }

    #[derive(Default, Debug, Clone)]
    pub struct Velocity {
        pub x: f32,
        pub y: f32,
    }

    #[derive(Default, Debug, Clone)]
    pub struct Health {
        pub value: f32,
    }
}

mod systems {
    use super::*;

    pub fn run_systems(world: &mut World) {
        let delta_time = world.resources.delta_time;
        world.tables.par_iter_mut().for_each(|table| {
            if table_has_components!(table, POSITION | VELOCITY | HEALTH) {
                update_positions_system(&mut table.position, &table.velocity, delta_time);
            }
            if table_has_components!(table, HEALTH) {
                health_system(&mut table.health);
            }
        });
    }

    // The system itself can also access components in parallel
    #[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;
        });
    }
}

Systems

Systems can iterate over tables, but for most use cases you can just write a function that queries entities:

pub fn update_global_transforms_system(world: &mut World) {
    world
        .query_entities(LOCAL_TRANSFORM | GLOBAL_TRANSFORM)
        .into_iter()
        .for_each(|entity| {
            // The entities we queried for are guaranteed to have
            // a local transform and global transform here
            let new_global_transform = query_global_transform(world, entity);
            let global_transform = world.get_global_transform_mut(entity).unwrap();
            *global_transform = GlobalTransform(new_global_transform);
        });
}

pub fn query_global_transform(world: &World, entity: EntityId) -> nalgebra_glm::Mat4 {
    let Some(local_transform) = world.get_local_transform(entity) else {
        return nalgebra_glm::Mat4::identity();
    };
    if let Some(Parent(parent)) = world.get_parent(entity) {
        query_global_transform(world, *parent) * local_transform
    } else {
        local_transform
    }
}

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.

// 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);
        }
    }
}

// You can also clear the event queue
world.clear_events();

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.

Examples

Simple Example

A basic demonstration of spawning entities, querying components, and running systems.

Boids Example

A flocking simulation using macroquad for visualization, demonstrating parallel systems and spatial partitioning.

Wolfenstein Example

A raycasted 3D engine similar to Wolfenstein 3D, showcasing:

  • Player movement and rotation with collision detection
  • Raycasting algorithm for 3D rendering
  • ECS-based game state management
  • Real-time 3D visualization with minimap
  • Different wall types with varying colors
  • Debug visualization of raycast rays

Run the examples with:

cargo run -r --example simple
cargo run -r --example boids
cargo run -r --example wolfenstein

License

This project is licensed under the MIT License - see the LICENSE file for details.