Crate freecs

Source
Expand description

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, 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

§Creating a World

use freecs::{world, has_components};
use serde::{Serialize, Deserialize};

// First, define components.
// They must implement: `Default + Clone + Serialize + Deserialize`

#[derive(Default, Clone, Debug, Serialize, Deserialize)]
struct Position { x: f32, y: f32 }

#[derive(Default, Clone, Debug, Serialize, Deserialize)]
struct Velocity { x: f32, y: f32 }

// Then, create a world with the `world!` macro.
// Resources are stored independently of component data and are not serialized.
// The `World` and `Resources` type names can be customized.
world! {
  World {
      components {
        position: Position => POSITION,
        velocity: Velocity => VELOCITY,
        health: Health => HEALTH,
      },
      Resources {
          delta_time: f32
      }
  }
}

§Entity and Component Access

let mut world = World::default();

// Spawn entities with components by mask
let entity = spawn_entities(&mut world, POSITION | VELOCITY, 1)[0];

// Lookup and modify a component
if let Some(pos) = get_component_mut::<Position>(&mut world, entity, POSITION) {
    pos.x += 1.0;

}

// Add new components to an entity by mask
add_components(&mut world, entity, HEALTH | VELOCITY);

// Remove components from an entity by mask
remove_components(&mut world, entity, VELOCITY | POSITION);

// Query all entities
let entities = query_entities(&world, ALL);
println!("All entities: {entities:?}");

// Query entities, iterating over all entities matching the component mask
let entities = query_entities(&world, POSITION | VELOCITY);

// Query for the first entity matching the component mask, returning early when found
let player = query_first_entity(&world, POSITION | VELOCITY);

§Systems and Parallel Processing

Systems are plain functions that iterate over the component tables and transform component data.

The example function below invokes two systems in parallel for each table in the world, filtered by component mask.

pub fn run_systems(world: &mut World, dt: f32) {
    use rayon::prelude::*;
    world.tables.par_iter_mut().for_each(|table| {
        if has_components!(table, POSITION | VELOCITY | HEALTH) {
            update_positions_system(&mut table.position, &table.velocity, dt);
        }
        if has_components!(table, HEALTH) {
            health_system(&mut table.health);
        }
    });
}

// The system itself can also access components in parallel and be inlined for performance.
#[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; // gradually decline health value
    });
}

Macros§