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§
Macros§
Structs§
- Entity
- Event
Queue - Double-buffered event queue for inter-system communication.
- Schedule