A high-performance, archetype-based Entity Component System (ECS) for Rust
Key Features:
- Zero-cost abstractions with static dispatch
 - Multi-threaded parallel processing using Rayon (optional 
parallelfeature) - Sparse set tags that don't fragment archetypes
 - Command buffers for deferred structural changes
 - Change detection for incremental updates
 - Type-safe double-buffered event system
 
The ecs! macro generates the entire ECS at compile time. The core implementation is ~1,350 LOC,
contains only plain data structures and functions, and uses zero unsafe code.
Add this to your Cargo.toml:
[dependencies]
freecs = "1.2.0"And in main.rs:
use freecs::{ecs, Entity};
ecs! {
    World {
        position: Position => POSITION,
        velocity: Velocity => VELOCITY,
        health: Health => HEALTH,
    }
    Tags {
        player => PLAYER,
        enemy => ENEMY,
    }
    Events {
        collision: CollisionEvent,
    }
    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];
    // Or use the entity builder
    let entity = EntityBuilder::new()
        .with_position(Position { x: 1.0, y: 2.0 })
        .spawn(&mut world, 1)[0];
    // Read components using the generated methods
    let position = world.get_position(entity);
    println!("Position: {:?}", position);
    // Set components (adds if not present)
    world.set_position(entity, Position { x: 1.0, y: 2.0 });
    // Mutate a component
    if let Some(position) = world.get_position_mut(entity) {
        position.x += 1.0;
    }
    // Get an entity's component mask
    let _component_mask = world.component_mask(entity).unwrap();
    // Add a new component to an entity
    world.add_components(entity, HEALTH);
    // Or use the generated add method
    world.add_health(entity);
    // Query all entities
    let _entities = world.get_all_entities();
    // Query all entities with a specific component
    let _players = world.query_entities(POSITION | VELOCITY | HEALTH);
    // 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);
    // Remove a component from an entity
    world.remove_components(entity, HEALTH);
    // Or use the generated remove method
    world.remove_health(entity);
    // Check if entity has components
    if world.entity_has_position(entity) {
        println!("Entity has position component");
    }
    // Add tags to entities (lightweight markers)
    world.add_player(entity);
    // Check if entity has a tag
    if world.has_player(entity) {
        println!("Entity is a player");
    }
    // Remove tags
    world.remove_player(entity);
    // Send events
    world.send_collision(CollisionEvent {
        entity_a: entity,
        entity_b: entity,
    });
    // Systems are functions that transform component data
    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, Copy)]
    pub struct Position {
        pub x: f32,
        pub y: f32,
    }
    #[derive(Default, Debug, Clone, Copy)]
    pub struct Velocity {
        pub x: f32,
        pub y: f32,
    }
    #[derive(Default, Debug, Clone, Copy)]
    pub struct Health {
        pub value: f32,
    }
}
use events::*;
mod events {
    use super::*;
    #[derive(Debug, Clone)]
    pub struct CollisionEvent {
        pub entity_a: Entity,
        pub entity_b: Entity,
    }
}
mod systems {
    use super::*;
    pub fn run_systems(world: &mut World) {
        // Systems use queries and component accessors
        example_system(world);
        update_positions_system(world);
        collision_handler_system(world);
        health_system(world);
    }
    fn example_system(world: &mut World) {
        for entity in world.query_entities(POSITION | VELOCITY) {
            if let Some(position) = world.get_position_mut(entity) {
                position.x += 1.0;
            }
        }
    }
    fn update_positions_system(world: &mut World) {
        let dt = world.resources.delta_time;
        // Collect entities with their velocities first to avoid borrow conflicts
        let updates: Vec<(Entity, Velocity)> = world
            .query_entities(POSITION | VELOCITY)
            .into_iter()
            .filter_map(|entity| {
                world.get_velocity(entity).map(|vel| (entity, *vel))
            })
            .collect();
        // Now update positions
        for (entity, vel) in updates {
            if let Some(pos) = world.get_position_mut(entity) {
                pos.x += vel.x * dt;
                pos.y += vel.y * dt;
            }
        }
    }
    fn collision_handler_system(world: &mut World) {
        // Process collision events
        for event in world.collect_collision() {
            println!("Collision detected between {:?} and {:?}", event.entity_a, event.entity_b);
        }
    }
    fn health_system(world: &mut World) {
        for entity in world.query_entities(HEALTH) {
            if let Some(health) = world.get_health_mut(entity) {
                health.value *= 0.98;
            }
        }
    }
}The ecs! macro generates type-safe methods for each component:
// For each component, you get:
world.get_position(entity)        // -> Option<&Position>
world.get_position_mut(entity)    // -> Option<&mut Position>
world.set_position(entity, pos)   // Sets or adds the component
world.add_position(entity)        // Adds with default value
world.remove_position(entity)     // Removes the component
world.entity_has_position(entity) // Checks if entity has componentSystems are functions that query entities and transform their components:
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
    }
}For performance-critical systems with large numbers of entities, you can batch process components:
fn batched_physics_system(world: &mut World) {
    let dt = world.resources.delta_time;
    
    // Collect entity data
    let mut entities: Vec<(Entity, Position, Velocity)> = world
        .query_entities(POSITION | VELOCITY)
        .into_iter()
        .filter_map(|entity| {
            match (world.get_position(entity), world.get_velocity(entity)) {
                (Some(pos), Some(vel)) => Some((entity, *pos, *vel)),
                _ => None
            }
        })
        .collect();
    
    // Process all entities
    for (_, pos, vel) in &mut entities {
        pos.x += vel.x * dt;
        pos.y += vel.y * dt;
    }
    
    // Write back results
    for (entity, new_pos, _) in entities {
        world.set_position(entity, new_pos);
    }
}This approach minimizes borrowing conflicts and can improve performance by processing data in batches.
Events provide a type-safe way to communicate between systems:
ecs! {
    World {
        position: Position => POSITION,
        velocity: Velocity => VELOCITY,
    }
    Events {
        collision: CollisionEvent,
        damage: DamageEvent,
    }
}
use events::*;
mod events {
    use super::*;
    #[derive(Debug, Clone)]
    pub struct CollisionEvent {
        pub entity_a: Entity,
        pub entity_b: Entity,
    }
    #[derive(Debug, Clone)]
    pub struct DamageEvent {
        pub entity: Entity,
        pub amount: f32,
    }
}
fn physics_system(world: &mut World) {
    for entity_a in world.query_entities(POSITION) {
        for entity_b in world.query_entities(POSITION) {
            if check_collision(entity_a, entity_b) {
                world.send_collision(CollisionEvent { entity_a, entity_b });
            }
        }
    }
}
fn damage_system(world: &mut World) {
    for event in world.collect_collision() {
        world.send_damage(DamageEvent {
            entity: event.entity_a,
            amount: 10.0
        });
    }
}
fn health_system(world: &mut World) {
    for event in world.collect_damage() {
        if let Some(health) = world.get_health_mut(event.entity) {
            health.value -= event.amount;
        }
    }
}Each event type gets these generated methods:
send_<event>(event)- Queue an eventread_<event>()- Get an iterator over all queued eventscollect_<event>()- Collect events into a Vec (eliminates boilerplate)peek_<event>()- Get reference to first event without consumingdrain_<event>()- Consume all events (takes ownership)update_<event>()- Swap buffers (old events cleared, current becomes previous)clear_<event>()- Immediately clear all eventslen_<event>()- Get count of all queued eventsis_empty_<event>()- Check if queue is empty
Call world.step() at the end of each frame to handle event cleanup:
loop {
    input_system(&mut world);
    physics_system(&mut world);
    collision_system(&mut world);
    world.step();  // Cleans up events and increments tick counter
}The step() method handles event lifecycle and tick counter automatically. For fine-grained control, you can use update_<event>() to update individual event types.
Events use double buffering to prevent systems from missing events in parallel execution. Events persist for 2 frames by default, then auto-clear on the next step() call. For immediate clearing, use clear_<event>().
For maximum performance, use the query builder which provides direct table access:
fn physics_update_system(world: &mut World) {
    let dt = world.resources.delta_time;
    world.query()
        .with(POSITION | VELOCITY)
        .iter(|entity, table, idx| {
            table.position[idx].x += table.velocity[idx].x * dt;
            table.position[idx].y += table.velocity[idx].y * dt;
        });
}This eliminates per-entity lookups and provides cache-friendly sequential access.
The query builder also supports filtering:
// Exclude entities with specific components
world.query()
    .with(POSITION | VELOCITY)
    .without(PLAYER)
    .iter(|entity, table, idx| {
        // Only processes entities that have position and velocity but NOT player
    });You can also use the lower-level iteration methods directly:
// Mutable iteration
world.for_each_mut(POSITION | VELOCITY, 0, |entity, table, idx| {
    table.position[idx].x += table.velocity[idx].x;
});
// Read-only iteration
for entity in world.query_entities(POSITION | VELOCITY) {
    let pos = world.get_position(entity).unwrap();
    let vel = world.get_velocity(entity).unwrap();
    println!("Entity {:?} at ({}, {})", entity, pos.x, pos.y);
}Spawn multiple entities efficiently (5.5x faster than individual spawns):
// Method 1: spawn_batch with initialization callback
let entities = world.spawn_batch(POSITION | VELOCITY, 1000, |table, idx| {
    table.position[idx] = Position { x: idx as f32, y: 0.0 };
    table.velocity[idx] = Velocity { x: 1.0, y: 0.0 };
});
// Method 2: spawn_entities (uses component defaults)
let entities = world.spawn_entities(POSITION | VELOCITY, 1000);
// Method 3: entity builder for small batches
let entities = EntityBuilder::new()
    .with_position(Position { x: 0.0, y: 0.0 })
    .with_velocity(Velocity { x: 1.0, y: 1.0 })
    .spawn(&mut world, 100);Optimized iteration for single components:
world.for_each_position(|position| {
    position.x += 1.0;
});
world.for_each_position_mut(|position| {
    position.y *= 0.99;
});Enable the parallel feature to process large entity counts across multiple CPU cores using Rayon:
[dependencies]
freecs = { version = "1.2.0", features = ["parallel"] }use freecs::rayon::prelude::*;
fn parallel_physics_system(world: &mut World) {
    let dt = world.resources.delta_time;
    world.par_for_each_mut(POSITION | VELOCITY, 0, |entity, table, idx| {
        table.position[idx].x += table.velocity[idx].x * dt;
        table.position[idx].y += table.velocity[idx].y * dt;
    });
}Best for 100K+ entities with non-trivial per-entity computation. For smaller entity counts, serial iteration may be more efficient due to parallelization overhead.
Tags are lightweight markers stored in sparse sets rather than archetypes. This means adding/removing tags doesn't trigger archetype migrations, avoiding fragmentation:
ecs! {
    World {
        position: Position => POSITION,
        velocity: Velocity => VELOCITY,
    }
    Tags {
        player => PLAYER,
        enemy => ENEMY,
        selected => SELECTED,
    }
}
// Adding tags doesn't move entities between archetypes
world.add_player(entity);
world.add_selected(entity);
// Check if entity has a tag
if world.has_player(entity) {
    println!("Entity is a player");
}
// Query entities by component and filter by tag
for entity in world.query_entities(POSITION | VELOCITY) {
    if world.has_enemy(entity) {
        // Process enemies
    }
}
// Remove tags
world.remove_player(entity);
world.remove_selected(entity);Tags are perfect for:
- Runtime categorization (player, enemy, npc)
 - Selection/highlighting states
 - Temporary status flags
 - Any marker that changes frequently
 
Command buffers allow you to queue structural changes (spawn, despawn, add/remove components) during iteration, then apply them all at once. This avoids borrowing conflicts and archetype invalidation during queries:
fn death_system(world: &mut World) {
    // Queue despawns during iteration
    let entities_to_despawn: Vec<Entity> = world
        .query_entities(HEALTH)
        .filter(|&entity| {
            world.get_health(entity).map_or(false, |h| h.value <= 0.0)
        })
        .collect();
    for entity in entities_to_despawn {
        world.queue_despawn_entity(entity);
    }
    // Apply all queued commands at once
    world.apply_commands();
}
fn spawn_system(world: &mut World) {
    // Queue entity spawns
    for _ in 0..10 {
        world.queue_spawn(POSITION | VELOCITY);
    }
    // Queue component additions
    for entity in world.query_entities(POSITION) {
        if should_add_health(entity) {
            world.queue_add_components(entity, HEALTH);
        }
    }
    // Queue component removals
    for entity in world.query_entities(VELOCITY) {
        if should_stop(entity) {
            world.queue_remove_components(entity, VELOCITY);
        }
    }
    world.apply_commands();
}Available command buffer operations:
queue_spawn(mask)- Queue entity spawnqueue_despawn_entity(entity)- Queue entity despawnqueue_add_components(entity, mask)- Queue component additionqueue_remove_components(entity, mask)- Queue component removalqueue_set_component(entity, component)- Queue component set/updateapply_commands()- Apply all queued commands
Track which components have been modified since the last frame. Useful for incremental updates, networking, or rendering optimizations:
fn render_system(world: &mut World) {
    // Process only entities whose components changed since last step()
    world.for_each_mut_changed(POSITION, 0, |entity, table, idx| {
        // Only processes entities where position changed
        update_sprite_position(&table.position[idx]);
    });
}
fn network_sync_system(world: &mut World) {
    // Sync changed entities to network clients
    world.for_each_mut_changed(POSITION | VELOCITY, 0, |entity, table, idx| {
        sync_to_network(entity, &table.position[idx], &table.velocity[idx]);
    });
}
// At the end of your game loop
world.step();  // Increments tick counter and swaps event buffersChange detection tracks modifications at the component table level. Any mutation via get_*_mut() or table access marks that component slot as changed for the current tick.
Performance note: Change detection adds a small overhead. Only use it when you need to track changes.
Organize systems into a schedule for automatic execution:
use freecs::Schedule;
fn main() {
    let mut world = World::default();
    // Create separate schedules for game logic and rendering
    let mut game_schedule = Schedule::new();
    game_schedule
        .add_system_mut(input_system)      // Mutable systems
        .add_system_mut(physics_system)
        .add_system_mut(collision_system);
    let mut render_schedule = Schedule::new();
    render_schedule
        .add_system(render_grid)           // Read-only systems
        .add_system(render_entities);
    // Game loop
    loop {
        game_schedule.run(&mut world);     // Run game logic
        render_schedule.run(&mut world);   // Run rendering
        world.step();
    }
}
fn input_system(world: &mut World) {
    // Handle input - mutates world state
}
fn physics_system(world: &mut World) {
    // Update physics - mutates positions
}
fn collision_system(world: &mut World) {
    // Check collisions - sends events
}
fn render_grid(world: &World) {
    // Render grid - read-only
}
fn render_entities(world: &World) {
    // Render entities - read-only
}Schedule API:
add_system_mut(fn(&mut World))- Add a system that can mutate world stateadd_system(fn(&World))- Add a read-only system (enforces immutability)
Systems in a schedule execute sequentially in the order they were added. Use add_system_mut for game logic systems that modify state, and add_system for rendering and query-only systems that don't need mutation.
An entity builder is generated automatically:
let mut world = World::default();
let entities = EntityBuilder::new()
    .with_position(Position { x: 1.0, y: 2.0 })
    .with_velocity(Velocity { x: 0.0, y: 1.0 })
    .spawn(&mut world, 2);
assert_eq!(world.get_position(entities[0]).unwrap().x, 1.0);
assert_eq!(world.get_position(entities[1]).unwrap().y, 2.0);For iterating over a single component type, specialized methods are generated:
// Read-only iteration
world.iter_position(|position| {
    println!("Position: ({}, {})", position.x, position.y);
});
// Mutable iteration
world.iter_position_mut(|position| {
    position.x += 1.0;
});
// Slice-based iteration (most efficient)
for slice in world.iter_position_slices() {
    for position in slice {
        println!("Position: ({}, {})", position.x, position.y);
    }
}
for slice in world.iter_position_slices_mut() {
    for position in slice {
        position.x *= 2.0;
    }
}
// Query entities with specific component
for entity in world.query_position() {
    println!("Entity with position: {:?}", entity);
}Query entities by specific tags:
// Get all entities with a specific tag
for entity in world.query_player() {
    println!("Player entity: {:?}", entity);
}
for entity in world.query_enemy() {
    if let Some(pos) = world.get_position(entity) {
        println!("Enemy at ({}, {})", pos.x, pos.y);
    }
}Beyond the basic command buffer operations, you can queue additional operations:
// Queue batch spawns
world.queue_spawn_entities(POSITION | VELOCITY, 100);
// Queue batch despawns
let entities_to_remove = vec![entity1, entity2, entity3];
world.queue_despawn_entities(entities_to_remove);
// Queue component sets (generated per component)
world.queue_set_position(entity, Position { x: 10.0, y: 20.0 });
world.queue_set_velocity(entity, Velocity { x: 1.0, y: 0.0 });
// Queue tag operations
world.queue_add_player(entity);
world.queue_remove_enemy(entity);
// Check command buffer status
if world.command_count() > 100 {
    world.apply_commands();
}
// Clear pending commands without applying
world.clear_commands();The query builder provides a fluent API for complex queries:
// Mutable query builder
world.query_mut()
    .with(POSITION | VELOCITY)
    .without(PLAYER)
    .iter(|entity, table, idx| {
        table.position[idx].x += table.velocity[idx].x;
    });
// Read-only query builder
world.query()
    .with(POSITION)
    .without(ENEMY)
    .iter(|entity, table, idx| {
        println!("Position: ({}, {})", table.position[idx].x, table.position[idx].y);
    });For maximum control, use the low-level iteration methods:
// Read-only iteration with include/exclude masks
world.for_each(POSITION | VELOCITY, PLAYER, |entity, table, idx| {
    let pos = &table.position[idx];
    let vel = &table.velocity[idx];
    println!("Non-player entity at ({}, {})", pos.x, pos.y);
});
// Mutable iteration with include/exclude masks
world.for_each_mut(POSITION | VELOCITY, 0, |entity, table, idx| {
    table.position[idx].x += table.velocity[idx].x;
    table.position[idx].y += table.velocity[idx].y;
});
// Check if entity has multiple components
if world.entity_has_components(entity, POSITION | VELOCITY | HEALTH) {
    println!("Entity has all required components");
}Query the current and previous tick counters for advanced change detection:
let current = world.current_tick();
let previous = world.last_tick();
// Process only entities changed since last frame
world.for_each_mut_changed(POSITION, 0, |entity, table, idx| {
    sync_transform(entity, &table.position[idx]);
});
// Tick is automatically incremented by world.step()
world.step();Preview events without consuming them:
// Peek at the first event
if let Some(event) = world.peek_collision() {
    println!("Next collision: {:?} and {:?}", event.entity_a, event.entity_b);
}
// Check if events exist
if !world.is_empty_collision() {
    let count = world.len_collision();
    println!("Processing {} collision events", count);
}
// Drain events (takes ownership)
for event in world.drain_collision() {
    process_collision(event);
}This project is licensed under the MIT License - see the LICENSE file for details.