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.3.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.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
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
License
This project is licensed under the MIT License - see the LICENSE file for details.