Overseer
This package supplies a lightweight, performant and julian implementation of the Entity Component System (ECS) programming paradigm. It is most well known for its applications in game development, but I believe it's a programming paradigm that can benefit a broad range of applications. It results in a very clean and flexible way to gradually build up applications in well separated blocks, while remaining inherently performant due to the way data is structured and accessed.
The API and performance of this package are being thoroughly tested in practice in the development of:
- Glimpse.jl: a mid level rendering toolkit
- Trading.jl: a comprehensive realtime trading and backtesting framework
- RomeoDFT.jl: a robust global DFT based energy optimizer
Illustrative Example
You can think of Entities as simply an identifier into Components which are essentially Vectors
of data. The key observation is that a System
doesn't particularly care which Entity
it is handling, only that it has the right data, i.e. that it has entries in the right Components.
We illustrate the concept of ECS with a very basic example were a Mover
system will change the position of Entities based on their velocity.
We start by defining a Position
and Velocity
Component
:
using Overseer
@component mutable struct Position
position::Vector{Float64}
end
@component struct Velocity
velocity::Vector{Float64}
end
Systems are represented by a subtype
of System
, usually these are empty since they should signify the purely functional part of ECS.
struct Mover <: System end
function Overseer.update(::Mover, l::AbstractLedger)
dt = 0.01
for e in @entities_in(l, Position && Velocity)
e.position .+= e.velocity .* dt
end
end
We see that the Mover
system iterates through all entities that have both the Position
and Velocity
Component
(i.e. have data
in both components), and updates their position.
Now we can create a Ledger
which holds all the Entities, Systems and Components:
l = Ledger(Stage(:basic, [Mover()]))
Ledger
Components:
Total entities: 0
a Stage
is essentially a way to group a set of Systems with some additional DAG-like features.
We can then add a couple of Entities to our ledger
e1 = Entity(l, Position([1,0,0]), Velocity([1,1,1]))
e2 = Entity(l, Position([2,0,1]), Velocity([1,-1,1]))
e3 = Entity(l, Position([2,0,1]))
l
Ledger
Components:
3-element Component{Main.Position}
2-element Component{Main.Velocity}
Total entities: 3
Then, we can execute all the Systems by calling update
on the ledger
update(l)
and look at the final positions
l[Position][e1], l[Position][e2], l[Position][e3]
(Main.Position([1.01, 0.01, 0.01]), Main.Position([2.01, -0.01, 1.01]), Main.Position([2.0, 0.0, 1.0]))
We see that the position of e3
did not get updated since it did not have the Velocity
component and as such was not touched by the Mover
system.