RECS: A Reactive ECS Framework for High-Performance Simulations in Julia

Gesee
Sun, 22 Jun 2025 00:32:32 +0000
Package Announcements

I am thrilled to introduce RECS (Reactive Entity-Component-System), a new Julia package designed for building high-performance, modular, and reactive simulations, with a focus on game development and scientific applications. RECS combines the data-oriented efficiency of ECS architectures with a reactive, asynchronous execution model, making it ideal for complex systems requiring dynamic data flows and scalability.

Repository: github.com/Gesee-y/ReactiveECS.jl
Documentation: ReactiveECS Guide | Reactive Architecture
Get Started: ]add ReactiveECS or ]add https://github.com/Gesee-y/ReactiveECS.jl for the development version

Why RECS?

RECS is built to address the limitations of traditional ECS frameworks, such as rigid update loops and static system orchestration, while leveraging Julia’s strengths for performance and flexibility. Key features include:

Reactive System Chaining: Use listen_to to create dynamic data pipelines between systems, enabling modular workflows without tight coupling.

Asynchronous Execution: Systems run as independent coroutines, supporting parallelism and real-time responsiveness.

Cache-Friendly Design: Components are stored in a Struct of Arrays (SoA) layout, optimized for vectorization and SIMD (via LoopVectorization.jl).

Dynamic Extensibility: Add or modify systems at runtime, ideal for iterative scientific experiments.

Fast Entity Management: Entity pooling and precomputed archetypes ensure low overhead (e.g., ~869 ns to add an entity, ~168 ns to remove).

RECS in Scientific Computing

RECS excels in scientific simulations where large datasets and complex interactions are common. Below is an example of using RECS for a molecular dynamics simulation, modeling particles under gravitational forces—a common task in computational physics and chemistry.

Example: Molecular Dynamics Simulation

This example simulates a system of particles interacting via gravitational forces, with components for position, velocity, and mass, and systems for force computation and motion integration.

using ReactiveECS
using LinearAlgebra

# Define components
@component Position begin
    x::Float64
    y::Float64
end

@component Velocity begin
    vx::Float64
    vy::Float64
end

@component Mass begin
    m::Float64
end

# Define systems
@system ForceSystem
@system MotionSystem

# Compute gravitational forces between particles
function ReactiveECS.run!(::ForceSystem, ref::WeakRef)
    indices = ref.value
    positions = get_component(world, :Position)
    masses = get_component(world, :Mass)
    forces = zeros(Float64, length(indices), 2)  # Store forces [fx, fy]

    @inbounds for i in eachindex(indices)
        for j in i+1:length(indices)
            idx1, idx2 = indices[i], indices[j]
            dx = positions[idx2].x - positions[idx1].x
            dy = positions[idx2].y - positions[idx1].y
            r = sqrt(dx^2 + dy^2)
            if r > 1e-6  # Avoid division by zero
                G = 6.67430e-11  # Gravitational constant
                force = G * masses[idx1].m * masses[idx2].m / r^2
                fx = force * dx / r
                fy = force * dy / r
                forces[i, 1] += fx
                forces[i, 2] += fy
                forces[j, 1] -= fx
                forces[j, 2] -= fy
            end
        end
    end
    return forces
end

# Update velocities and positions
function ReactiveECS.run!(sys::MotionSystem, data)
    indices = get_indices(world, sys) # Returns the indices of the entities matching the subscription of the system
    forces = data  # Received from ForceSystem
    velocities = get_component(world, :Velocity)
    positions = get_component(world, :Position)
    masses = get_component(world, :Mass)
    dt = 0.01  # Time step

    @inbounds for i in eachindex(indices)
        idx = indices[i]
        # Update velocity (F = ma => a = F/m)
        velocities[idx].vx += forces[i, 1] / masses[idx].m * dt
        velocities[idx].vy += forces[i, 2] / masses[idx].m * dt
        # Update position
        positions[idx].x += velocities[idx].vx * dt
        positions[idx].y += velocities[idx].vy * dt
    end
end

# Setup
world = ECSManager()
force_sys = ForceSystem()
motion_sys = MotionSystem()

subscribe!(world, force_sys, (PositionComponent, MassComponent))
listen_to(force_sys, motion_sys)

run_system!(force_sys)
run_system!(motion_sys)

# Create 100 particles with random positions and masses
for i in 1:100
    create_entity!(world;
        Position=PositionComponent(rand() * 10, rand() * 10),
        Velocity=VelocityComponent(0.0, 0.0),
        Mass=MassComponent(1.0 + rand() * 10)
    )
end

# Simulate for 10 frames
for frame in 1:10
    println("Frame $frame")
    dispatch_data(world)
    blocker(world)
end

Why RECS for Science?

Scalability: RECS’s SoA layout and vectorization support (e.g., @inbounds, @turbo) make it ideal for large-scale simulations, like modeling thousands of particles.

Reactivity: The listen_to mechanism allows systems to chain computations (e.g., forces → motion), mimicking the iterative workflows of scientific simulations.

Flexibility: Add new systems (e.g., a visualization system) at runtime without modifying existing code, perfect for iterative experiments.

Performance: Benchmarks show RECS handles 10K entities in ~766 μs for updates, outperforming other Julia ECS frameworks like Overseer.jl for large datasets.

For more examples, including Conway’s Game of Life and physics simulations, check our case studies.

Get Involved

We’re excited to see how the community uses RECS for games, simulations, and beyond! Here’s how you can contribute:

Report bugs or suggest features via GitHub issues.

Contribute code to enhance RECS’s features or add new examples.

Share your projects using RECS in the Discussions tab.

Try RECS today and let us know your feedback! For technical details, see our reactive architecture article or dive into the user guide

17 posts - 4 participants