Skip to content

Commit

Permalink
added more documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
louisponet committed Apr 26, 2023
1 parent ac9391e commit fc034fb
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 75 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Overseer (Entity Component System)
[![Build Status](https://github.com/louisponet/Overseer.jl/workflows/CI/badge.svg)](https://github.com/louisponet/Overseer.jl/actions?query=workflow%3ACI)
[![codecov](https://codecov.io/gh/louisponet/Overseer.jl/branch/master/graph/badge.svg?token=mVK0aEQGuu)](https://codecov.io/gh/louisponet/Overseer.jl)
[![](https://img.shields.io/badge/docs-stable-blue.svg)](https://louisponet.github.io/Overseer.jl/stable)
[![](https://img.shields.io/badge/docs-latest-blue.svg)](https://louisponet.github.io/Overseer.jl/dev)
[![Package Downloads](https://shields.io/endpoint?url=https://pkgs.genieframework.com/api/v1/badge/Overseer)](https://pkgs.genieframework.com?packages=Overseer&startdate=2015-12-30&enddate=2040-12-31)

This package supplies a lightweight, performant and julian implementation of the [Entity component system](https://en.wikipedia.org/wiki/Entity_component_system) (ECS) paradigm.
Expand Down Expand Up @@ -167,7 +169,7 @@ Entities can be removed from a specific component through
pop!(m[Spring], e2)
```

For more examples please have a look for now in [Glimpse.jl](https://github.com/louisponet/Glimpse.jl).
For more examples please have a look for now at the [Documentation](https://louisponet.github.io/Overseer.jl/stable/).

## Implementation
The implementation is heavily inspired by [EnTT](https://github.com/skypjack/entt), using slightly modified [SparseIntSets](https://juliacollections.github.io/DataStructures.jl/latest/sparse_int_set/#DataStructures.SparseIntSet-1) to track which entities hold which components.
The implementation was originally inspired by [EnTT](https://github.com/skypjack/entt), using slightly modified [SparseIntSets](https://juliacollections.github.io/DataStructures.jl/latest/sparse_int_set/#DataStructures.SparseIntSet-1) to track which entities hold which components.
74 changes: 74 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1 +1,75 @@
# Overseer
```@meta
CurrentModule = Overseer
```
This package supplies a lightweight, performant and julian implementation of the [Entity Component System](https://en.wikipedia.org/wiki/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](https://github.com/louisponet/Glimpse.jl): a mid level rendering toolkit
- [Trading.jl](https://github.com/louisponet/Trading.jl): a comprehensive realtime trading and backtesting framework
- [RomeoDFT.jl](https://github.com/louisponet/RomeoDFT.jl): a robust global DFT based energy optimizer

## Illustrative Example

!!! note
A [`Component`](@ref) is technically the datastructure that holds the data for a given `Type` for the [Entities](@ref) that have that data.
We will thus use the terminology of an [`Entity`](@ref) "having a component" and "being part of a component" interchangeably.

You can think of [Entities](@ref) as simply an identifier into [Components](@ref) which are essentially `Vectors` of data. The key observation is that a
[`System`](@ref) doesn't particularly care which [`Entity`](@ref) it is handling, only that it has the right data, i.e. that it has entries in the right
[Components](@ref).

We illustrate the concept of ECS with a very basic example were a `Mover` system will change the position of [Entities](@ref) based on their velocity.

We start by defining a `Position` and `Velocity` [`Component`](@ref):
```@example initial_example
using Overseer
@component mutable struct Position
position::Vector{Float64}
end
@component struct Velocity
velocity::Vector{Float64}
end
```
[Systems](@ref) are represented by a `subtype` of `System`, usually these are empty since they should signify the purely **functional** part of ECS.

```@example initial_example
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`](@ref) (i.e. have `data` in both components),
and updates their position.

Now we can create a [`Ledger`](@ref) which holds all the [Entities](@ref), [Systems](@ref) and [Components](@ref):
```@example initial_example
l = Ledger(Stage(:basic, [Mover()]))
```
a [`Stage`](@ref) is essentially a way to group a set of [Systems](@ref) with some additional DAG-like features.

We can then add a couple of [Entities](@ref) to our ledger
```@example initial_example
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
```
Then, we can execute all the [Systems](@ref) by calling [`update`](@ref update(::AbstractLedger)) on the ledger
```@example initial_example
update(l)
```
and look at the final positions
```@example initial_example
l[Position][e1], l[Position][e2], l[Position][e3]
```

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.
6 changes: 0 additions & 6 deletions src/Overseer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ module Overseer
using MacroTools

"""
AbstractComponent
Abstract type for all [Components](@ref). For now the only two [`AbstractComponents`](@ref AbstractComponent) are [`Component`](@ref) and [`PooledComponent`](@ref).
Most functionality that is defined for the [`AbstractComponent`](@ref) type assumes that there is a `.indices` member field that is of type
[`Indices`](@ref).
Expand All @@ -17,8 +15,6 @@ abstract type AbstractComponent{T} <: AbstractVector{T} end
abstract type AbstractGroup end

"""
System
Systems represent the part of ECS where the actual "work" happens,
by overloading the [`Overseer.update`](@ref) function for a specific
`System`, with the signature `Overseer.update(::System, m::AbstractLedger)`.
Expand All @@ -31,8 +27,6 @@ some settings parameters.
abstract type System end

"""
AbstractLedger
Abstract type for all ECS ledgers. The easiest way to use the interface is by including a standard
[`Ledger`](@ref) as a member field and defining [`ledger`](@ref) for your new [`AbstractLedger`](@ref)
type to return that field.
Expand Down
73 changes: 35 additions & 38 deletions src/component.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ function test_abstractcomponent_interface(::Type{T}) where {T<:AbstractComponent
@test Entity(2) in c
@test length(c) == 2 == size(c)[1] == length(c.indices) == length(entity_data(c))


@test c[Entity(2)] isa TestCompData

@test entity(c, 1) isa EntityState{Tuple{T{TestCompData}}}
Expand Down Expand Up @@ -59,7 +58,9 @@ Function that can be overloaded to specify what the default [`AbstractComponent`
component_type(::Type{TC}) where {TC} = Component{TC}

@inline indices_iterator(a::AbstractComponent)::Indices = a.indices
@inline reverse_indices_iterator(a::AbstractComponent) = ReverseIndicesIterator(a.indices, i -> true)
@inline function reverse_indices_iterator(a::AbstractComponent)
return ReverseIndicesIterator(a.indices, i -> true)
end

"""
in(entity, component)
Expand All @@ -75,15 +76,15 @@ Base.isempty(c::AbstractComponent) = isempty(c.indices)

Base.eltype(::Type{<:AbstractComponent{T}}) where {T} = T

function Base.delete!(c::AbstractComponent, es::Vector{Entity})
Base.delete!(c::AbstractComponent, es::Vector{Entity}) =
for e in es
if e in c
pop!(c, e)
end
end
end

Base.@propagate_inbounds function Base.getindex(c::AbstractComponent, I::AbstractVector{<:AbstractEntity})
Base.@propagate_inbounds function Base.getindex(c::AbstractComponent,
I::AbstractVector{<:AbstractEntity})
return map(x -> c[x], I)
end

Expand Down Expand Up @@ -112,26 +113,28 @@ function Base.permute!(c::AbstractComponent, permvec::AbstractVector{<:Integer})
return c
end

Base.permute!(c::AbstractComponent, permvec::AbstractVector{<:AbstractEntity}) = permute!(c, map(x -> c.indices[x.id], permvec))
function Base.permute!(c::AbstractComponent, permvec::AbstractVector{<:AbstractEntity})
return permute!(c, map(x -> c.indices[x.id], permvec))
end

Base.sortperm(c::AbstractComponent, args...; kwargs...) = sortperm(entity_data(c), args...; kwargs...)
function Base.sortperm(c::AbstractComponent, args...; kwargs...)
return sortperm(entity_data(c), args...; kwargs...)
end

function Base.:(==)(c1::C1, c2::C2) where {C1<:AbstractComponent,C2<:AbstractComponent}
if eltype(C1) != eltype(C2) || length(c1) != length(c2)
return false
elseif length(c1) > 20 && hash(c1) != hash(c2)
return false
else
return all(i -> (e = entity(c1, i); (e in c2) && (@inbounds c2[e] == c1[e])), eachindex(c1))
return all(i -> (e = entity(c1, i); (e in c2) && (@inbounds c2[e] == c1[e])),
eachindex(c1))
end
end

Base.IndexStyle(::Type{AbstractComponent}) = IndexLinear()


"""
Component
The most basic Component type.
Indexing into a component with an [`Entity`](@ref) will return the data linked to that entity,
Expand Down Expand Up @@ -223,11 +226,7 @@ end
function process_typedef(typedef, mod)
global td = nothing
MacroTools.postwalk(typedef) do x
if @capture(x, struct T_
fields__
end | mutable struct T_
fields__
end)
if @capture(x, struct T_ fields__ end | mutable struct T_ fields__ end)
global td = T
end
x
Expand All @@ -236,13 +235,18 @@ function process_typedef(typedef, mod)
end

"""
@component
This takes a struct definition and register it so that it will be stored inside a [`Component`](@ref) when attached to [Entities](@ref).
# Example
```julia
@component struct MyComp
v::Float64
end
"""
macro component(typedef)
return esc(Overseer._component(typedef, __module__))
end

function _component(typedef, mod)
tn = process_typedef(typedef, mod)
return quote
Expand All @@ -256,16 +260,13 @@ end
# PooledComponent #
# #
########################################

struct ApplyToPool
e::Entity
end

Base.parent(e::Entity) = ApplyToPool(e)

"""
PooledComponent
A [`PooledComponent`](@ref) allows for sharing data between many [Entities](@ref).
Essentially, the indices into the data pool are stored for each [`Entity`](@ref), rather than the data itself.
Expand Down Expand Up @@ -303,14 +304,16 @@ PooledComponent{T}() where {T} = PooledComponent{T}(Indices(), Int[], Int[], T[]
Returns which pool `entity` or the `ith` entity belongs to.
"""
Base.@propagate_inbounds @inline pool(c::PooledComponent, e::AbstractEntity) = c.pool[c.indices[e.id]]
Base.@propagate_inbounds @inline function pool(c::PooledComponent, e::AbstractEntity)
return c.pool[c.indices[e.id]]
end
Base.@propagate_inbounds @inline pool(c::PooledComponent, e::Int) = c.pool[c.indices[e]]

entity_data(c::PooledComponent) = c.pool

npools(c::PooledComponent) = length(c.data)

Base.@propagate_inbounds @inline Base.getindex(c::PooledComponent, i::Integer) = c.data[c.pool[i]]
Base.@propagate_inbounds @inline Base.getindex(c::PooledComponent, i::Integer) = c.data[c.pool[i]]
Base.@propagate_inbounds @inline Base.setindex!(c::PooledComponent, v, i::Integer) = c.data[i] = v

@inline function Base.getindex(c::PooledComponent, e::AbstractEntity)
Expand All @@ -328,7 +331,6 @@ Base.@propagate_inbounds @inline function Base.parent(c::PooledComponent, i::Int
end
Base.@propagate_inbounds @inline Base.parent(c::PooledComponent, e::Entity) = parent(c, pool(c, e))


# c[entity] = value
# set value of <only> this entity
@inline function Base.setindex!(c::PooledComponent{T}, v::T, e::AbstractEntity) where {T}
Expand Down Expand Up @@ -412,13 +414,14 @@ function Base.empty!(c::PooledComponent)
end

function maybe_cleanup_empty_pool!(c::PooledComponent, poolid)
if c.pool_size[poolid] == 0
deleteat!(c.data, poolid)
deleteat!(c.pool_size, poolid)
for i in eachindex(c.pool)
if c.pool[i] > poolid
c.pool[i] -= 1
end
c.pool_size[poolid] != 0 && return

deleteat!(c.data, poolid)
deleteat!(c.pool_size, poolid)

for i in eachindex(c.pool)
if c.pool[i] > poolid
c.pool[i] -= 1
end
end
end
Expand All @@ -442,8 +445,8 @@ function Base.pop!(c::PooledComponent, e::AbstractEntity)

return val
end

end

function Base.pop!(c::PooledComponent)
@boundscheck if isempty(c)
throw(BoundsError(c))
Expand All @@ -467,8 +470,6 @@ end
Base.sortperm(c::PooledComponent) = sortperm(c.pool)

"""
@pooled_component
This takes a struct definition and register it so that it will be stored inside a [`PooledComponent`](@ref) when attached to [Entities](@ref).
"""
macro pooled_component(typedef)
Expand All @@ -484,9 +485,6 @@ function _pooled_component(typedef, mod)
end

"""
make_unique!
Checks whether duplicate data exists in a [`PooledComponent`](@ref) and if so points all [Entities](@ref) to only
a single copy while removing the duplicates.
"""
Expand Down Expand Up @@ -535,4 +533,3 @@ function make_unique!(c::PooledComponent)

return
end

Loading

0 comments on commit fc034fb

Please sign in to comment.