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

Note

A Component is technically the datastructure that holds the data for a given Type for the Entities that have that data. We will thus use the terminology of an Entity "having a component" and "being part of a component" interchangeably.

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.