Arche is an archetype-based Entity Component System for Go.
Arche is designed for the use in simulation models of the Department of Ecological Modelling at the Helmholtz Centre for Environmental Research.
To use Arche in a Go project, run:
go get github.com/mlange-42/arche
Here is the classical Position/Velocity example that every ECS shows in the docs. It uses the type-safe generic API.
See the User Guide, API docs and examples for details. For more complex examples, see arche-demo.
package main
import (
"math/rand"
"github.com/mlange-42/arche/ecs"
"github.com/mlange-42/arche/generic"
)
// Position component
type Position struct {
X float64
Y float64
}
// Velocity component
type Velocity struct {
X float64
Y float64
}
func main() {
// Create a World.
world := ecs.NewWorld()
// Create a component mapper.
mapper := generic.NewMap2[Position, Velocity](&world)
// Create entities.
for i := 0; i < 1000; i++ {
// Create a new Entity with components.
entity := mapper.New()
// Get the components
pos, vel := mapper.Get(entity)
// Initialize component fields.
pos.X = rand.Float64() * 100
pos.Y = rand.Float64() * 100
vel.X = rand.NormFloat64()
vel.Y = rand.NormFloat64()
}
// Create a generic filter.
filter := generic.NewFilter2[Position, Velocity]()
// Time loop.
for t := 0; t < 1000; t++ {
// Get a fresh query.
query := filter.Query(&world)
// Iterate it
for query.Next() {
// Component access through the Query.
pos, vel := query.Get()
// Update component fields.
pos.X += vel.X
pos.Y += vel.Y
}
}
}
Several tools for Arche are provided in separate modules:
Unlike most other ECS implementations, Arche is designed for the development of scientific, individual-based models rather than for game development. This motivates some design decisions, with an emphasis on simplicity, safety and performance. Nevertheless, Arche can also be used for game development.
The ecs.World
object is a pure and simple ECS implementation in the sense of a data store
for entities and components, with query and iteration capabilities.
More advanced features like batch operations or entity relations are provided through separate objects.
There is neither an update loop nor systems. These should be implemented by the user. For a batteries-included implementation, see module arche-model.
The packages filter
and generic
provide a layer around the core for richer resp. generic queries and manipulation.
They are built on top of the ecs
package, so they could also be implemented by a user.
Iteration order in Arche is deterministic and reproducible.
This does not mean that entities are iterated in their order of insertion, nor in the same order in successive iterations.
However, given the same operations on the ecs.World
, iteration order will always be the same.
Arche puts an emphasis on safety and on avoiding undefined behavior. It panics on unexpected operations, like removing a dead entity, adding a component that is already present, or attempting to change a locked world. This may not seem idiomatic for Go. However, explicit error handling in performance hotspots is not an option. Neither is silent failure, given the scientific background.
World
is limited to 256. This is mainly a performance decision.uint32
ID).A tabular overview of the runtime cost of typical Arche ECS operations is provided under benchmarks in the Arche's User Guide.
See also the latest Benchmarks CI run.
To the best of the author's knowledge, there are only a handful of ECS implementations in Go that are serious and somewhat maintained:
Here, Arche is benchmarked against these implementations. Feel free to open an issue if you have suggestions for improvements on the benchmarking code or other engines to include.
Build:
Pos{float64, float64}
and Vel{float64, float64}
.Pos{float64, float64}
.Iterate:
Pos
and Vel
, and add Vel
to Pos
.Benchmark code: benchmark/competition/pos_vel
.
Build:
Pos{float64, float64}
.Iterate:
Pos
, and add Vel{float64, float64}
component.Pos
and Vel
, and remove Vel
component.Note: The iteration is performed once before benchmarking, to avoid biasing slower implementations through one-time allocations.
Benchmark code: benchmark/competition/add_remove
.
The plot below shows CPU time benchmarks of Arche (black) vs. Array of Structs (AoS, red) and Array of Pointers (AoP, blue) (with structs escaped to the heap).
Arche takes a constant time of just over 2ns per entity, regardless of the memory per entity (x-axis) and the number of entities (line styles). For AoS and AoP, time per access increases with memory per entity as well as number of entities, due to cache misses.
In the given example with components of 16 bytes each, from 64 bytes per entity onwards (i.e. 4 components or 8 float64
values),
Arche outperforms AoS and AoP, particularly with a large number of entities.
Note that the maximum shown here corresponds to only 25 MB of entity data!
Benchmark code: benchmark/competition/array_of_structs
.
Lange, M. (2023): Arche – An archetype-based Entity Component System for Go. DOI 10.5281/zenodo.7656484, GitHub repository: https://github.com/mlange-42/arche
This project is distributed under the MIT license.