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: Deterministic, generation-checked markers that don’t fragment archetypes
  • Command Buffers: Queue structural changes during iteration
  • Change Detection: Track component modifications for incremental updates
  • Events: Sequence-numbered channels with exactly-once cursor consumption
  • Structural Change Log: Cursor-based log of spawns, despawns, component moves, and tag flips
  • Multi-World: Split components across multiple worlds for >64 component types
  • Dynamic Worlds (optional dynamic feature): the primary entry point. Runtime component registration with bundle spawns, typed queries, joins, prepared queries, snapshots and deltas, same storage underneath; new surface lands here first, and the macro tier stays the executable specification it is tested against

The ecs! macro generates the entire ECS at compile time using only plain data structures, functions, and zero unsafe code.

§Quick Start

use freecs::{ecs, Entity};

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

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

#[derive(Default, Clone, Debug)]
pub struct Health { pub 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)]
pub struct CollisionEvent {
    pub entity_a: Entity,
    pub 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_mut()
        .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)
    let entities: Vec<_> = world.query_entities(POSITION | VELOCITY).collect();
    for entity in entities {
        if let Some(velocity) = world.get_velocity(entity).cloned() {
            if let Some(position) = world.get_position_mut(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();

Writes through set_*, get_*_mut, and modify_* mark the slot as changed, as do spawns and component add/remove migrations. Raw table access does not mark. Changed queries skip whole tables that no write has touched since the last step(), using a per-table high-water tick per component.

§System Scheduling

Organize systems into a schedule:

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

schedule
    .push("input", input_system)
    .push("physics", physics_system)
    .push_readonly("render", 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(|_entity, 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);
}

// Cursor-based, exactly-once consumption: record where you left off,
// read everything newer, then advance your cursor.
let mut cursor = 0;
for event in world.read_collision_since(cursor) {
    // Process event
}
cursor = world.sequence_collision();
assert!(world.read_collision_since(cursor).is_empty());

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

Modules§

dynamic
Runtime-registered components over the same archetype kernel.
system_param
Bevy-style system parameters over DynWorld: functions whose arguments are Res, ResMut, and Query resolve into runnable systems the existing Schedule accepts, with no unsafe and no new executor.

Macros§

dynamic_accessors
Generates the macro-world accessor ergonomics over the keyed tier: a keys struct holding one dynamic::ComponentKey or dynamic::TagKey per entry, a resolve constructor registering them in declaration order, and named methods on your wrapper type — get_<name> / get_<name>_mut / set_<name> / remove_<name> / has_<name> per component, add_<name> / remove_<name> / has_<name> / query_<name> per tag. The wrapper must expose the named world and keys fields. Accessors run at keyed speed and stamp change ticks exactly like the generated macro world’s. Component and tag names share one method namespace (remove_ and has_ exist for both), so an identifier may appear in only one of the two blocks; reusing one is a duplicate-method compile error.
dynamic_schema
Declares a dynamic world’s schema in one place: the mask constants (bits assigned in declaration order, which is the registration order and therefore the snapshot schema) and the registration function that builds a dynamic::ComponentRegistry in that exact order, asserting each key’s mask against its constant. Declare every component on every build configuration, and only ever append, so masks stay identical across feature sets and saves stay loadable.
dynamic_worlds
Declares a dynamic::DynEcs group’s member worlds in one place: the index constants in declaration order and the build function that adds each member’s registry at its asserted index, replacing the hand-written const-add-assert dance. Pair each member with a registration function, typically from dynamic_schema!. Apps extending a built group add their own member with dynamic::DynEcs::add_world_at.
ecs
ecs_impl
ecs_multi_impl
table_has_components

Structs§

ArchetypeEdges
Archetype graph edges for one table: which table an entity lands in when a single component bit is added or removed, plus memoized targets for multi-bit changes. Shared by the macro-generated worlds and the dynamic world, since none of it depends on component types.
ArchetypeRouting
Registers a newly pushed table with the archetype routing structures: inserts the mask lookup, appends the table’s edge record, extends every query-cache entry the new table satisfies, and wires single-component edges from existing tables toward the new one. table_masks must iterate every table including the new one, in index order.
Entity
EntityAllocator
Allocates generational entity handles and tracks which handles are live.
EntityLocation
EntityLocations
EntitySlot
Liveness record for one entity id: the generation currently associated with the id and whether that handle is live.
EventChannel
A sequence-numbered event channel for inter-system communication.
Schedule
SparseTagSet
A sparse set of entities backing one tag.
Stages
An ordered set of named stages, each its own Schedule, for programs assembled from independent parts: the app declares the stage order once, and each part pushes systems into stages by name, so composition stays deterministic without labels or ordering constraints. Stages run in declaration order; systems within a stage run in push order. Plain data, inspectable through stages.
StructuralChange
One structural mutation recorded by the world: a spawn, a despawn, or a component add or remove. mask holds the components involved: the full mask for spawns and despawns, the delta for adds and removes. Consumers track their own sequence cursor via structural_changes_since and the owner trims consumed entries with trim_structural_log.

Enums§

StructuralChangeKind

Constants§

EVENT_CHANNEL_CAPACITY
Backstop for event channels whose events are never consumed or expired. When the buffer reaches this length the oldest events are dropped, so a channel with no consumer stays bounded instead of leaking.
STRUCTURAL_LOG_CAPACITY
Backstop for worlds whose structural log is never consumed. When the log reaches this length it is cleared wholesale, so a world with no consumer stays bounded instead of leaking. Consumers that drain every frame never come near it.

Functions§

archetype_cached_tables
Returns the memoized list of table indices whose masks contain mask, computing and caching it on first use. Taking the cache and the table masks as separate parameters keeps the borrows disjoint, so callers can mutate tables while holding the returned slice.
archetype_register_table
tick_is_newer
Returns true if tick was stamped after since_tick, treating ticks as a wrapping sequence so detection keeps working after u32 overflow.