Anyone interested in coding a Julia ECS?

That‘s no an issue, but I assume that the feature set should be roughly equivalent. Can‘t wait to see more code.

Why do you think you need to invert the architecture?

I hope that I can get away with less type assertions/casts that way.

Particularly, I found this way now:

@generated function _cast_to(::Type{T}, x::Any) where T
    :(x::T)
end

function _get_storage(world::World, id::UInt8, ::Type{C})::_ComponentStorage{C} where C
    return _cast_to(_ComponentStorage{C}, world._storages[id])
end

I hope this is faster than the previously used

storage = world._storages[id]::_ComponentStorage{C}

So we need function specialization only for the (limited number of) component types, not for the potentially quite high number of possible “archetype types”.

Personally I‘ve yet to use @generated in any of my own code after 8 years of Julia use, so I‘m just slightly surprised it would show up so early. But I’m unable to see how this fits into the bigger picture, so if it looks to you like it is required/does what you need, don‘t let that stop you!

My mind is still pondering a very basic question. When is it necessary to iterate/index over all entities indirectly (potentially type unstable) as opposed to over all entities in their current archetypes directly (without indirection over entities)? Basically, if entities would store an index to their archetype as a storage location, most things in the world actually wouldn‘t need to use that, only the manager of entities. The latter is only needed when an entity changes archetype, so you look up in which archetype it currently resides and then make changes accordingly. In all other situations I can think of, you can iterate over archetypes directly. E.g. you want to show all states of all entities, you want to execute a system etc. I may be missing something important. But to me it seems like you could get away without any major problems by having some mechanism to subscribe each system to a subset of all archetypes it can act on, which under the hood will be a concretely typed subset of components. So for a system concerned about component of type T, it holds a Vector{Vector{T}}. I‘ll try to put this in code.

Ok, here we go: GitHub - mlange-42/Ark.jl: Ark.jl -- Archetype-based Entity Component System (ECS) for Julia.

All basic ECS functionality is implemented:

  • Create entities with and without components
  • Add components to and remove components from entities
  • Remove entities from the world
  • Get and set/update components for an entity
  • Simple queries for 1-8 components (oh, 0 components still missing…)
  • World lock to prevent mutation during iterations

The implementation went quite smooth, but there are certainly still a lot of places with dump code, as this is really the first bit of Julia am I writing.

Also, I did not do any benchmarks yet, and no optimizations except for those things I know to take care from previous ECS projects, as far as they were transferable to Julia.

I would be particularly interested in getting rid of the Map1, Map2, … Query1, Query2, … things and use variadic parameters or tuples. However, I was not able to get this working without losing compile-time type information.

Very keen to read all your thoughts and feedback on it!

6 Likes

:tada: Nice! First benchmarks show that the basic Position/Velocity is already a bit faster than in the Go versions, with 1.5ns per entity, compared to 1.8ns in Go (in the GitHub CI).

-----------------------------------------------
              Query Pos/Vel
-----------------------------------------------

Benchmarking with 100 entities...
Mean time per entity: 1.5601611686460808 ns
BenchmarkTools.Trial: 10000 samples with 842 evaluations per sample.
 Range (min … max):  143.546 ns … 289.519 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     152.506 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   156.016 ns ±  15.363 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

   ▁██  ▂▃                                                       
  ▂████▆███▆▅▃▃▃▂▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▂
  144 ns           Histogram: frequency by time          232 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

Benchmarking with 1000 entities...
Mean time per entity: 1.4703388799999995 ns
BenchmarkTools.Trial: 10000 samples with 10 evaluations per sample.
 Range (min … max):  1.369 μs …  13.108 μs  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.452 μs               ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.470 μs ± 186.487 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

   █   ▃                                                       
  ▅█▅▅███▅▅▄▄▃▃▃▂▂▂▂▂▂▂▂▂▂▂▂▂▁▂▂▂▂▂▂▂▂▁▁▂▁▁▁▂▁▂▂▁▂▂▁▁▁▁▂▂▂▂▂▂ ▃
  1.37 μs         Histogram: frequency by time        2.32 μs <

 Memory estimate: 0 bytes, allocs estimate: 0.

Benchmarking with 10000 entities...
Mean time per entity: 1.5076993867924529 ns
BenchmarkTools.Trial: 2120 samples with 1 evaluation per sample.
 Range (min … max):  14.187 μs … 33.131 μs  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     14.748 μs              ┊ GC (median):    0.00%
 Time  (mean ± σ):   15.077 μs ±  1.451 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

   ██▁ ▃▂                                                      
  ▄███▇██▅▄▃▃▃▂▂▂▂▂▂▂▂▁▁▂▁▂▂▁▁▂▁▂▁▁▂▁▁▂▂▂▂▂▂▁▁▁▂▂▂▂▁▂▁▂▂▂▂▂▂▂ ▃
  14.2 μs         Histogram: frequency by time        23.4 μs <

 Memory estimate: 0 bytes, allocs estimate: 0.

Benchmarking with 100000 entities...
Mean time per entity: 1.552946507936508 ns
BenchmarkTools.Trial: 189 samples with 1 evaluation per sample.
 Range (min … max):  145.733 μs … 233.456 μs  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     154.288 μs               ┊ GC (median):    0.00%
 Time  (mean ± σ):   155.295 μs ±   8.425 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

        █ ▂▂    ▂▄ ▇▇▂      ▂                                    
  ▅▆▄▇█▇█▇██▅▃▃▆██▅███▆▆▇▆█▄█▇▆▃▁▄▄▇▄▅▅▄▃▁▁▁▃▄▃▁▁▁▃▁▁▃▃▁▁▃▁▁▁▁▃ ▃
  146 μs           Histogram: frequency by time          175 μs <

 Memory estimate: 0 bytes, allocs estimate: 0.
8 Likes

Nice, that was fast (in at least two ways)!

Yes, 2 days for all the basic functionality. Seems like Julia is not that bad of a language for a beginner (that worked with many other langages before, though…)

2 Likes

Hey about this. Actually the architecture doc is quite deprecated (I know ai should have removed that)
Next the architecture keeps everything densely packed.
In fact I made it in such way that I could have the benefits of archetype ECS + Sparse set ECS

About reactivity, it’s just about system orchestration.
As with Ark.jl, you can jus don’t care about systems and do it your own way but I used reactive pipelines because it allows way more flexibility than static scheduling and more control than dynamic scheduling.

Yeah it does introduce a synchronization overhead but I think the benefits are pretty much above the roof so.

You should read the package readme. It will provides all the informations you need

@Gesee Thank you for the clarifications!

Regarding archetypes and sparse-set, I do not really understand that. The advantage of sparse-set is that you don’t need to move entities and components around when changing an entity, making these operations faster. But then, you don’t have similar entities contiguous in memory anymore and need to apply filters to individual entities. So how can you combine that?

In fact I would rather say my ECS try his best to keep structural changes cheap compared to archetypes ECS.
For example adding an entity is a cheap operations that can be done at at blazing speed (and about the 800 ns to make an entity, it was unoptimized and done on an archaic computer).
Adding an entity is about marking rows as in use.
Removing is just shrinking some range
Changing archetype is just 2 override.
Etc.
All that while still ensuring fast iterations.

It also makes querying simpler since systems have access for any components for any entity, we don’t need optionals in queries which are often slow to iterate on.

800ns for creating an entity is incredibly slow. You probably have a type stability problem there. Ark.jl creates an entity with 1 component in 30ns, and <100ns for 5 components.

It’s fixed now
100k entities with 3 components are created in ~110us using multithreading on 4 core.

And that’s because it initialize the components.
Without initializing them it takes something like 14us on my computer

Are you sure? 1ns per component? Or let’s say 3 given the multithreading. Getting a (pooled) entity ID, plus determining the archetype it should go to? This seems impossible to me. Maybe for an entity without any components, ok.

BTW my intention was not to vote down your project. I just was asked why I want to build another ECS when there is ReactiveECS, and I expressed my honest thoughts. Let’s just wait a bit until Ark.jl stabilzed more and then let’s do a contest like this one that covers common usage patterns.

1 Like

Just depends on what you do. If you make 100k indentical entities with 3 components, in my ECS it’s just about resizing the table (since every entity is in the same table even those with different components combination) then we just set the columns of the table for our new entities.
Resizing + setting index in arrays on multithread make 110us really plausible for 100k entities.

Try reading my readme for more informations