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
- 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,500 LOC,
contains only plain data structures and functions, and uses zero unsafe code.
Quick Start
Add this to your Cargo.toml:
[]
= "1.0.2"
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 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:
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.
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 a specific tick. Useful for incremental updates, networking, or rendering optimizations:
// You can also use for_each_mut_changed for direct iteration
world.for_each_mut_changed;
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 (~20 Melem/s vs normal iteration). Only use it when you need to track changes.
System Scheduling
Organize systems into a schedule for automatic execution:
use Schedule;
Systems in a schedule execute sequentially in the order they were added. This provides a simple way to organize your game loop without manually calling each system.
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
Advanced tick management for change detection:
let current = world.current_tick;
let previous = world.last_tick;
// Process only entities changed in the last frame
world.for_each_mut_changed;
// Manually increment tick
world.increment_tick;
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.