freECS
freecs is a abstraction-free table-based ECS library for Rust, in about ~500 lines
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 internal implementation is ~500 loc (aside from tests, comments, and example code),
and does not use object orientation, generics, traits, or dynamic dispatch.
Quick Start
Add this to your Cargo.toml:
[dependencies]
freecs = "0.4.8"
rayon = "^1.10.0"
And in main.rs:
use freecs::{ecs, table_has_components, EntityId};
use rayon::prelude::*;
ecs! {
World {
position: Position => POSITION,
velocity: Velocity => VELOCITY,
health: Health => HEALTH,
}
Resources {
delta_time: f32
}
}
pub fn main() {
let mut world = World::default();
let entity = world.spawn_entities(POSITION | VELOCITY, 1)[0];
println!(
"Spawned {} with position and velocity",
world.get_all_entities().len()
);
let position = world.get_component::<Position>(entity, POSITION);
println!("Position: {:?}", position);
let position = world.get_position(entity);
println!("Position: {:?}", position);
if let Some(position) = world.get_position_mut(entity) {
position.x += 1.0;
}
println!(
"Component mask before adding health component: {:b}",
world.component_mask(entity).unwrap()
);
world.add_components(entity, HEALTH);
println!(
"Component mask after adding health component: {:b}",
world.component_mask(entity).unwrap()
);
let entities = world.get_all_entities();
println!("All entities: {entities:?}");
let players = world.query_entities(POSITION | VELOCITY | HEALTH);
println!("Player entities: {players:?}");
let first_player_entity = world.query_first_entity(POSITION | VELOCITY | HEALTH);
println!("First player entity : {first_player_entity:?}");
world.remove_components(entity, HEALTH);
systems::run_systems(&mut world);
world.despawn_entities(&[entity]);
}
use components::*;
mod components {
#[derive(Default, Debug, Clone)]
pub struct Position {
pub x: f32,
pub y: f32,
}
#[derive(Default, Debug, Clone)]
pub struct Velocity {
pub x: f32,
pub y: f32,
}
#[derive(Default, Debug, Clone)]
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 table_has_components!(table, POSITION | VELOCITY | HEALTH) {
update_positions_system(&mut table.position, &table.velocity, delta_time);
}
if table_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;
});
}
}
Systems
Systems can iterate over tables, but for most use cases you can just write a function that queries entities:
pub fn update_global_transforms_system(world: &mut World) {
world
.query_entities(LOCAL_TRANSFORM | GLOBAL_TRANSFORM)
.into_iter()
.for_each(|entity| {
let new_global_transform = query_global_transform(world, entity);
let global_transform = world.get_global_transform_mut(entity).unwrap();
*global_transform = GlobalTransform(new_global_transform);
});
}
pub fn query_global_transform(world: &World, entity: EntityId) -> nalgebra_glm::Mat4 {
let Some(local_transform) = world.get_local_transform(entity) else {
return nalgebra_glm::Mat4::identity();
};
if let Some(Parent(parent)) = world.get_parent(entity) {
query_global_transform(world, *parent) * local_transform
} else {
local_transform
}
}
Change Detection
freecs provides an opt-in change detection system that allows you to track when components are modified.
This is useful for systems that only need to process entities when their data has changed.
if let Some(pos) = world.get_component_mut::<Position>(entity, POSITION) {
pos.x += velocity.x * dt;
pos.y += velocity.y * dt;
}
world.mark_changed(entity, POSITION);
while let Some(event) = world.try_next_event() {
match event {
Event::ComponentChanged { kind, entity } => {
println!("Component {:b} changed for entity {:?}", kind, entity);
}
}
}
world.clear_events();
You can mark multiple components as changed in a single call:
world.mark_changed(entity, POSITION | VELOCITY);
The event queue is stored in the world's Resources struct and is automatically available when you create a world with the ecs! macro.
Examples
Simple Example
A basic demonstration of spawning entities, querying components, and running systems.
Boids Example
A flocking simulation using macroquad for visualization, demonstrating parallel systems and spatial partitioning.
Wolfenstein Example
A raycasted 3D engine similar to Wolfenstein 3D, showcasing:
- Player movement and rotation with collision detection
- Raycasting algorithm for 3D rendering
- ECS-based game state management
- Real-time 3D visualization with minimap
- Different wall types with varying colors
- Debug visualization of raycast rays
Run the examples with:
cargo run -r --example simple
cargo run -r --example boids
cargo run -r --example wolfenstein
License
This project is licensed under the MIT License - see the LICENSE file for details.