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