freECS
freecs is a zero-unsafe, table-based ECS library for Rust, in about ~600 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 minimal and does not use object orientation, generics, traits, or dynamic dispatch.
Quick Start
Add this to your Cargo.toml:
[dependencies]
freecs = "0.6.0"
And in main.rs:
use freecs::{ecs, Entity};
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];
let entity = EntityBuilder::new()
.with_position(Position { x: 1.0, y: 2.0 })
.spawn(&mut world, 1)[0];
let position = world.get_position(entity);
println!("Position: {:?}", position);
world.set_position(entity, Position { x: 1.0, y: 2.0 });
if let Some(position) = world.get_position_mut(entity) {
position.x += 1.0;
}
let _component_mask = world.component_mask(entity).unwrap();
world.add_components(entity, HEALTH);
world.add_health(entity);
let _entities = world.get_all_entities();
let _players = world.query_entities(POSITION | VELOCITY | HEALTH);
let _first_player_entity = world.query_first_entity(POSITION | VELOCITY | HEALTH);
world.remove_components(entity, HEALTH);
world.remove_health(entity);
if world.entity_has_position(entity) {
println!("Entity has position component");
}
systems::run_systems(&mut world);
world.despawn_entities(&[entity]);
}
use components::*;
mod components {
#[derive(Default, Debug, Clone, Copy)]
pub struct Position {
pub x: f32,
pub y: f32,
}
#[derive(Default, Debug, Clone, Copy)]
pub struct Velocity {
pub x: f32,
pub y: f32,
}
#[derive(Default, Debug, Clone, Copy)]
pub struct Health {
pub value: f32,
}
}
mod systems {
use super::*;
pub fn run_systems(world: &mut World) {
example_system(world);
update_positions_system(world);
health_system(world);
}
fn example_system(world: &mut World) {
for entity in world.query_entities(POSITION | VELOCITY) {
if let Some(position) = world.get_position_mut(entity) {
position.x += 1.0;
}
}
}
fn update_positions_system(world: &mut World) {
let dt = world.resources.delta_time;
let updates: Vec<(Entity, Velocity)> = world
.query_entities(POSITION | VELOCITY)
.into_iter()
.filter_map(|entity| {
world.get_velocity(entity).map(|vel| (entity, *vel))
})
.collect();
for (entity, vel) in updates {
if let Some(pos) = world.get_position_mut(entity) {
pos.x += vel.x * dt;
pos.y += vel.y * dt;
}
}
}
fn health_system(world: &mut World) {
for entity in world.query_entities(HEALTH) {
if let Some(health) = world.get_health_mut(entity) {
health.value *= 0.98;
}
}
}
}
Generated API
The ecs! macro generates type-safe methods for each component:
world.get_position(entity) world.get_position_mut(entity) world.set_position(entity, pos) world.add_position(entity) world.remove_position(entity) world.entity_has_position(entity)
Systems
Systems are functions that query entities and transform their components:
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
}
}
Batched Processing
For performance-critical systems with large numbers of entities, you can batch process components:
fn batched_physics_system(world: &mut World) {
let dt = world.resources.delta_time;
let mut entities: Vec<(Entity, Position, Velocity)> = world
.query_entities(POSITION | VELOCITY)
.into_iter()
.filter_map(|entity| {
match (world.get_position(entity), world.get_velocity(entity)) {
(Some(pos), Some(vel)) => Some((entity, *pos, *vel)),
_ => None
}
})
.collect();
for (_, pos, vel) in &mut entities {
pos.x += vel.x * dt;
pos.y += vel.y * dt;
}
for (entity, new_pos, _) in entities {
world.set_position(entity, new_pos);
}
}
This approach minimizes borrowing conflicts and can improve performance by processing data in batches.
Entity Builder
An entity builder is generated automatically:
let mut world = World::default();
let entities = EntityBuilder::new()
.with_position(Position { x: 1.0, y: 2.0 })
.with_velocity(Velocity { x: 0.0, y: 1.0 })
.spawn(&mut world, 2);
assert_eq!(world.get_position(entities[0]).unwrap().x, 1.0);
assert_eq!(world.get_position(entities[1]).unwrap().y, 2.0);
License
This project is licensed under the MIT License - see the LICENSE file for details.