freECS
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 (automatically enabled on non-WASM platforms)
- 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.
Quick Start
Add this to your Cargo.toml:
[]
= "1.5.0"
And in main.rs:
use ;
ecs!
use *;
use *;
Generated API
The ecs! macro generates type-safe methods for each component:
// For each component, you get:
world.get_position // -> Option<&Position>
world.get_position_mut // -> Option<&mut Position>
world.modify_position // -> Option<R> - mutate via closure, returns closure result
world.set_position // Sets or adds the component
world.add_position // Adds with default value
world.remove_position // Removes the component
world.entity_has_position // Checks if entity has component
Closure-Based Mutation
The modify_<component> methods allow you to mutate a component via a closure, which automatically releases the borrow when done. This is useful when you need to mutate a component and then immediately access the world again:
// Instead of this pattern (requires explicit drop):
let player = world.get_player_mut.unwrap;
player.stamina -= 10.0;
let _ = player; // Must drop to release borrow
let pos = world.get_position;
// Use modify for cleaner code:
world.modify_player;
let pos = world.get_position; // No drop needed
// The closure can return values:
let old_health = world.modify_health;
Systems
Systems are functions that query entities and transform their components:
Batched Processing
For performance-critical systems with large numbers of entities, you can batch process components:
This approach minimizes borrowing conflicts and can improve performance by processing data in batches.
Events
Events provide a type-safe way to communicate between systems:
ecs!
use *;
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
Game Loop Integration
Call world.step() at the end of each frame to handle event cleanup:
loop
The step() method handles event lifecycle and tick counter automatically. For fine-grained control, you can use update_<event>() to update individual event types.
Double Buffering
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>().
High-Performance Features
Query Builder API
For maximum performance, use the query builder which provides direct table access:
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
.without
.iter;
You can also use the lower-level iteration methods directly:
// Mutable iteration
world.for_each_mut;
// Read-only iteration
for entity in world.query_entities
Batch Spawning
Spawn multiple entities efficiently (5.5x faster than individual spawns):
// Method 1: spawn_batch with initialization callback
let entities = world.spawn_batch;
// Method 2: spawn_entities (uses component defaults)
let entities = world.spawn_entities;
// Method 3: entity builder for small batches
let entities = new
.with_position
.with_velocity
.spawn;
Single-Component Iteration
Optimized iteration for single components:
world.for_each_position;
world.for_each_position_mut;
Parallel Iteration
Process large entity counts across multiple CPU cores using Rayon. Parallel iteration is automatically available on non-WASM platforms:
use *;
Best for 100K+ entities with non-trivial per-entity computation. For smaller entity counts, serial iteration may be more efficient due to parallelization overhead.
Note: Parallel methods are only available when targeting non-WASM platforms. On WASM targets, use the regular serial iteration methods instead.
Sparse Set Tags
Tags are lightweight markers stored in sparse sets rather than archetypes. This means adding/removing tags doesn't trigger archetype migrations, avoiding fragmentation:
ecs!
// Adding tags doesn't move entities between archetypes
world.add_player;
world.add_selected;
// Check if entity has a tag
if world.has_player
// Query entities by component and filter by tag
for entity in world.query_entities
// Remove tags
world.remove_player;
world.remove_selected;
Tags are perfect for:
- Runtime categorization (player, enemy, npc)
- Selection/highlighting states
- Temporary status flags
- Any marker that changes frequently
Command Buffers
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:
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
Change Detection
Track which components have been modified since the last frame. Useful for incremental updates, networking, or rendering optimizations:
// At the end of your game loop
world.step; // Increments tick counter and swaps event buffers
Change 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.
System Scheduling
Organize systems into a schedule for automatic execution:
use Schedule;
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.
Entity Builder
An entity builder is generated automatically:
let mut world = default;
let entities = new
.with_position
.with_velocity
.spawn;
assert_eq!;
assert_eq!;
Advanced Features
Per-Component Iteration
For iterating over a single component type, specialized methods are generated:
// Read-only iteration
world.iter_position;
// Mutable iteration
world.iter_position_mut;
// Slice-based iteration (most efficient)
for slice in world.iter_position_slices
for slice in world.iter_position_slices_mut
// Query entities with specific component
for entity in world.query_position
Tag Queries
Query entities by specific tags:
// Get all entities with a specific tag
for entity in world.query_player
for entity in world.query_enemy
Advanced Command Buffer Operations
Beyond the basic command buffer operations, you can queue additional operations:
// Queue batch spawns
world.queue_spawn_entities;
// Queue batch despawns
let entities_to_remove = vec!;
world.queue_despawn_entities;
// Queue component sets (generated per component)
world.queue_set_position;
world.queue_set_velocity;
// Queue tag operations
world.queue_add_player;
world.queue_remove_enemy;
// Check command buffer status
if world.command_count > 100
// Clear pending commands without applying
world.clear_commands;
Query Builder (Advanced)
The query builder provides a fluent API for complex queries:
// Mutable query builder
world.query_mut
.with
.without
.iter;
// Read-only query builder
world.query
.with
.without
.iter;
Low-Level Iteration
For maximum control, use the low-level iteration methods:
// Read-only iteration with include/exclude masks
world.for_each;
// Mutable iteration with include/exclude masks
world.for_each_mut;
// Check if entity has multiple components
if world.entity_has_components
Tick Management
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;
// Tick is automatically incremented by world.step()
world.step;
Event Peeking
Preview events without consuming them:
// Peek at the first event
if let Some = world.peek_collision
// Check if events exist
if !world.is_empty_collision
// Drain events (takes ownership)
for event in world.drain_collision
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!
When a component or resource has a #[cfg(...)] attribute, all related generated code (struct fields, accessor methods, mask constants, enum variants, etc.) is conditionally compiled based on the feature flag or target configuration.
License
This project is licensed under the MIT License - see the LICENSE file for details.