Skip to main content

Crate freecs

Crate freecs 

Source
Expand description

A high-performance, archetype-based Entity Component System (ECS) for Rust.

freecs provides a table-based storage system where entities with identical component sets are stored together in contiguous memory (Structure of Arrays layout), optimizing for cache coherency and SIMD operations.

§Key Features

  • Zero-cost Abstractions: Fully statically dispatched, no custom traits
  • Parallel Processing: Multi-threaded iteration using Rayon (automatically enabled on non-WASM platforms)
  • Sparse Set Tags: Lightweight markers that don’t fragment archetypes
  • Command Buffers: Queue structural changes during iteration
  • Change Detection: Track component modifications for incremental updates
  • Events: 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.

§Quick Start

use freecs::{ecs, Entity};

// First, define components (must implement Default)
#[derive(Default, Clone, Debug)]
struct Position { x: f32, y: f32 }

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

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

// Then, create a world with the `ecs!` macro
ecs! {
  World {
    position: Position => POSITION,
    velocity: Velocity => VELOCITY,
    health: Health => HEALTH,
  }
  Tags {
    player => PLAYER,
    enemy => ENEMY,
  }
  Events {
    collision: CollisionEvent,
  }
  Resources {
    delta_time: f32
  }
}

#[derive(Debug, Clone)]
struct CollisionEvent {
    entity_a: Entity,
    entity_b: Entity,
}

§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 using generated methods
if let Some(pos) = world.get_position_mut(entity) {
    pos.x += 1.0;
}

// Read components
if let Some(pos) = world.get_position(entity) {
    println!("Position: ({}, {})", pos.x, pos.y);
}

// Set components (adds if not present)
world.set_position(entity, Position { x: 10.0, y: 20.0 });
world.set_velocity(entity, Velocity { x: 1.0, y: 0.0 });

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

// Or use the generated add methods
world.add_health(entity);

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

// Or use the generated remove methods
world.remove_velocity(entity);

// Check if entity has components
if world.entity_has_position(entity) {
    println!("Entity has position component");
}

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

§Tags

Tags are lightweight markers that don’t cause archetype fragmentation:

// Add tags to entities
world.add_player(entity);

// Check if entity has a tag
if world.has_player(entity) {
    println!("Entity is a player");
}

// Query entities by tag
for entity in world.query_player() {
    println!("Player: {:?}", entity);
}

// Remove tags
world.remove_player(entity);

§Events

Events provide type-safe communication between systems:

// Send events
world.send_collision(CollisionEvent {
    entity_a: entity,
    entity_b: entity,
});

// Process events in systems
for event in world.collect_collision() {
    println!("Collision: {:?} and {:?}", event.entity_a, event.entity_b);
}

// Clean up events and increment tick at end of frame
world.step();

§Systems

Systems are functions that query entities and transform their components. For maximum performance, use the query builder API for direct table access:

fn physics_system(world: &mut World) {
    let dt = world.resources.delta_time;

    // Method 1: High-performance query builder (recommended)
    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;
        });

    // Method 2: Per-entity lookups (simpler but slower)
    for entity in world.query_entities(POSITION | VELOCITY) {
        if let Some(position) = world.get_position_mut(entity) {
            if let Some(velocity) = world.get_velocity(entity) {
                position.x += velocity.x * dt;
                position.y += velocity.y * dt;
            }
        }
    }
}

§Parallel Processing

Process large entity counts across multiple CPU cores using Rayon. Parallel iteration is automatically available on non-WASM platforms:

use freecs::rayon::prelude::*;

fn parallel_physics(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;
    });
}

Parallel iteration is best suited for processing 100K+ entities with non-trivial per-entity computation.

§Command Buffers

Queue structural changes during iteration to avoid borrow conflicts:

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

§Change Detection

Track which components have been modified since the last frame:

// Process only entities with changed components
world.for_each_mut_changed(POSITION, 0, |entity, table, idx| {
    // Only processes entities where position changed since last step()
});

// Automatically increments tick counter
world.step();

§System Scheduling

Organize systems into a schedule:

let mut world = World::default();
let mut schedule = Schedule::new();

schedule
    .add_system_mut(input_system)
    .add_system_mut(physics_system)
    .add_system(render_system);

// Game loop
loop {
    schedule.run(&mut world);
    world.step();
}

§Entity Builder

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

// Access the spawned entities
let first_pos = world.get_position(entities[0]).unwrap();
assert_eq!(first_pos.x, 1.0);

§Advanced Features

§Batch Spawning

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

§Per-Component Iteration

// Iterate over single component
world.iter_position_mut(|position| {
    position.x += 1.0;
});

// Slice-based iteration (most efficient)
for slice in world.iter_position_slices_mut() {
    for position in slice {
        position.x *= 2.0;
    }
}

§Low-Level Iteration

// Include/exclude with masks
world.for_each_mut(POSITION | VELOCITY, PLAYER, |entity, table, idx| {
    // Process non-player entities
    table.position[idx].x += table.velocity[idx].x;
});

§Advanced Command Operations

// Queue batch operations
world.queue_spawn_entities(POSITION, 100);
world.queue_set_position(entity, Position { x: 10.0, y: 20.0 });
world.queue_add_player(entity);

// Check command buffer status
if world.command_count() > 100 {
    world.apply_commands();
}

// Clear without applying
world.clear_commands();

§Event Management

// Peek at events without consuming
if let Some(event) = world.peek_collision() {
    println!("Next collision: {:?}", event.entity_a);
}

// Check event count
if !world.is_empty_collision() {
    let count = world.len_collision();
    println!("Processing {} events", count);
}

// Drain events (takes ownership)
for event in world.drain_collision() {
    // Process event
}

§Conditional Compilation

Both components and resources support #[cfg(...)] attributes for conditional compilation. This is useful for debug-only components, optional features, or platform-specific functionality:

ecs! {
    World {
        position: Position => POSITION,
        velocity: Velocity => VELOCITY,
        #[cfg(debug_assertions)]
        debug_info: DebugInfo => DEBUG_INFO,
        #[cfg(feature = "physics")]
        rigid_body: RigidBody => RIGID_BODY,
    }
    Resources {
        delta_time: f32,
        #[cfg(feature = "audio")]
        audio_engine: AudioEngine,
    }
}

When a component or resource has a #[cfg(...)] attribute, all related generated code (struct fields, accessor methods, mask constants, etc.) is conditionally compiled.

Re-exports§

pub use paste;
pub use rayon;

Macros§

ecs
ecs_impl
table_has_components

Structs§

Entity
EventQueue
Double-buffered event queue for inter-system communication.
Schedule