freecs
freecs is a zero-abstraction ECS library for Rust, designed for high performance and simplicity. 🚀
It provides an archetypal table-based storage system for components, allowing for fast queries,
fast system iteration, and parallel processing.
A macro is used to define the world and its components, and generates
the entity component system as part of your source code at compile time. The generated code
contains only plain data structures (no methods) and free functions that transform them, achieving static dispatch.
The internal implementation is ~500 loc (aside from tests, comments, and example code),
and does not use object orientation, generics, traits, or dynamic dispatch.
Key Features
- Table-based Storage: Entities with the same components are stored together in memory
- Raw Access: Functions work directly on the underlying vectors of components
- Parallel Processing: Built-in support for processing tables in parallel with rayon
- Simple Queries: Find entities by their components using bit masks
- Serialization: Save and load worlds using serde
- World Merging: Clone and remap entity hierarchies between worlds
- Zero Overhead: No dynamic dispatch, traits, or runtime abstractions
- Data Oriented: Focus on cache coherence and performance
Quick Start
Add this to your Cargo.toml:
[dependencies]
freecs = "0.2.17"
serde = { version = "1.0", features = ["derive"] }
rayon = "1.10.0"
And in main.rs:
use freecs::{has_components, world};
use rayon::prelude::*;
world! {
World {
components {
position: Position => POSITION,
velocity: Velocity => VELOCITY,
health: Health => HEALTH,
},
Resources {
delta_time: f32
}
}
}
pub fn main() {
let mut world = World::default();
world.resources.delta_time = 0.016;
let entity = spawn_entities(&mut world, POSITION | VELOCITY, 1)[0];
println!(
"Spawned {} with position and velocity",
query_entities(&world, ALL).len(),
);
let position = get_component::<Position>(&world, entity, POSITION);
println!("Position: {:?}", position);
if let Some(position) = get_component_mut::<Position>(&mut world, entity, POSITION) {
position.x += 1.0;
}
println!(
"Component mask before adding health component: {:b}",
component_mask(&world, entity).unwrap()
);
add_components(&mut world, entity, HEALTH);
println!(
"Component mask after adding health component: {:b}",
component_mask(&world, entity).unwrap()
);
let players = query_entities(&world, POSITION | VELOCITY | HEALTH);
println!("Player entities: {players:?}");
let first_player_entity = query_first_entity(&world, POSITION | VELOCITY | HEALTH);
println!("First player entity : {first_player_entity:?}");
remove_components(&mut world, entity, HEALTH);
systems::run_systems(&mut world, 0.01);
despawn_entities(&mut world, &[entity]);
}
use components::*;
mod components {
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Position {
pub x: f32,
pub y: f32,
}
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Velocity {
pub x: f32,
pub y: f32,
}
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Health {
pub value: f32,
}
}
mod systems {
use super::*;
pub fn run_systems(world: &mut World) {
let delta_time = world.resources.delta_time;
world.tables.par_iter_mut().for_each(|table| {
if has_components!(table, POSITION | VELOCITY | HEALTH) {
update_positions_system(&mut table.position, &table.velocity, delta_time);
}
if has_components!(table, HEALTH) {
health_system(&mut table.health);
}
});
}
#[inline]
pub fn update_positions_system(positions: &mut [Position], velocities: &[Velocity], dt: f32) {
positions
.par_iter_mut()
.zip(velocities.par_iter())
.for_each(|(pos, vel)| {
pos.x += vel.x * dt;
pos.y += vel.y * dt;
});
}
#[inline]
pub fn health_system(health: &mut [Health]) {
health.par_iter_mut().for_each(|health| {
health.value *= 0.98; });
}
}
Examples
Run the examples with:
cargo run -r --example cubes
World Merging
The ECS supports cloning entities from one world to another while maintaining their relationships.
This is useful for implementing prefabs, prototypes, and scene loading.
let mut source = World::default();
let mut game_world = World::default();
let [root, child1, child2] = spawn_entities(&mut source, POSITION | NODE, 3)[..] else {
panic!("Failed to spawn entities");
};
if let Some(node) = get_component_mut::<Node>(&mut source, root, NODE) {
node.id = root;
node.children = vec![child1, child2];
}
let mapping = merge_worlds(&mut game_world, &source);
Remapping Entity References
When components contain EntityIds (for parent-child relationships, inventories, etc),
these need to be updated to point to the newly spawned entities:
#[derive(Default, Clone)]
struct Node {
id: EntityId,
parent: Option<EntityId>,
children: Vec<EntityId>,
}
remap_entity_refs(&mut game_world, &mapping, |mapping, table| {
if table.mask & NODE != 0 {
for node in &mut table.node {
if let Some(new_id) = remap_entity(mapping, node.id) {
node.id = new_id;
}
if let Some(ref mut parent_id) = node.parent {
if let Some(new_id) = remap_entity(mapping, *parent_id) {
*parent_id = new_id;
}
}
for child_id in &mut node.children {
if let Some(new_id) = remap_entity(mapping, *child_id) {
*child_id = new_id;
}
}
}
}
});
Example: Character Prefab
fn spawn_character(world: &mut World, position: Vec2) -> EntityId {
let mut prefab = World::default();
let [root, weapon, effects] = spawn_entities(&mut prefab, MODEL | NODE, 3)[..] else {
panic!("Failed to spawn prefab");
};
if let Some(root_node) = get_component_mut::<Node>(&mut prefab, root, NODE) {
root_node.id = root;
root_node.children = vec![weapon, effects];
}
let mapping = merge_worlds(world, &prefab);
remap_entity_refs(world, &mapping, |mapping, table| {
if table.mask & NODE != 0 {
for node in &mut table.node {
if let Some(new_id) = remap_entity(mapping, node.id) {
node.id = new_id;
}
for child_id in &mut node.children {
if let Some(new_id) = remap_entity(mapping, *child_id) {
*child_id = new_id;
}
}
}
}
});
remap_entity(&mapping, root).unwrap()
}
Performance Notes
- Entities and components are copied in bulk using table-based storage
- Component data is copied directly with no individual allocations
- Entity remapping is O(n) where n is the number of references
- No additional overhead beyond the new entities and temporary mapping table
License
This project is licensed under the MIT License - see the LICENSE file for details.