arche

package module
v0.4.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 19, 2023 License: MIT Imports: 0 Imported by: 0

README

Arche

Test status Go Reference GitHub MIT license

Arche is an archetype-based Entity Component System for Go.

Arche is designed for the use in simulation models of the Department for Ecological Modelling at the Helmholtz Centre for Environmental Research.

Installations

go get github.com/mlange-42/arche

Features

  • Simple core API. See the API docs.
  • Optional rich filtering and generic query API.
  • Fast iteration and component access via queries (≈2.5ns iterate + get).
  • Fast random access for components of arbitrary entities. Useful for hierarchies.
  • No systems. Just queries. Use your own structure!
  • Not thread-safe. On purpose.
  • No dependencies. Except for unit tests (100% coverage).

For details on Arche's architecture, see section Architecture.

Usage example

Here is a minimal usage example. You will likely create systems with a method that takes a pointer to the World as argument.

See the API docs and examples for details.

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.
		_, pos, vel := mapper.NewEntity()

		// 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
		}
	}
}

Design decisions

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 a focus on simplicity, safety and performance.

Minimal core API

The ecs.World object is a pure and minimal ECS implementation in the sense of a data store for entities and components, with query and iteration capabilities. There is neither an update loop nor systems. These should be implemented by the user.

The packages filter and generic provide a layer around the core for richer and/or safer queries and operations. They are built on top of the ecs package, so they could also be implemented by users.

Determinism

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.

Strict and panic

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 seem not idiomatic for Go. However, explicit error handling in performance hotspots is not an option. Neither is silent failure, given the scientific background.

Other limitations
  • The number of component types per World is limited to 128. This is mainly a performance decision.
  • The number of entities alive at any one time is limited to just under 5 billion (uint32 ID).

Architecture

Arche uses an archetype-based architecture.

The ASCII graph below illustrates the architecture. Components for entities are stored in so-called archetypes, which represent unique combinations of components. In the illustration, the first archetype holds all components for all entities with (only/exactly) the components A, B and C.

 Entities   Archetypes   Bitmasks   Queries

   E         E Comps
  |0|       |2|A|B|C|    111...<-.<--match-.
  |1|---.   |8|A|B|C|            |         |
  |2|   '-->|1|A|B|C|            |         |
  |3|       |3|A|B|C|            |--(A, C) |
  |4|                            |  101... |
  |6|   .-->|7|A|C|      101...<-'         |--(B)
  |7|---'   |6|A|C|                        |  010...
  |8|       |4|A|C|                        |
  |9|---.                                  |
  |.|   |   |5|B|C|      011...   <--------'
  |.|   '-->|9|B|C|
  |.|
  |.| <===> [Entity pool]

The exact composition of each archetype is encoded in a bitmask for fast comparison. Thus, queries can easily identify their relevant archetypes (i.e. query bitmask contained in archetype bitmask), and then iterate entities linearly, and very fast. Components can be accessed through the query in a very efficient way.

For getting components by entity ID, e.g. for hierarchies, the world contains a list that is indexed by the entity ID, and references the entity's archetype and index in the archetype. This way, getting components for entity IDs (i.e. random access) is fast, although not as fast as in queries (≈2ns vs. 1ns).

Obviously, archetypes are an optimization for iteration speed. But they also come with a downside. Adding or removing components to/from an entity requires moving all the components of the entity to another archetype. This takes around 20ns per involved component. It is therefore recommended to add/remove/exchange multiple components at the same time rather than one after the other.

Generic vs. ID access

Arche provides generic functions and types for accessing and modifying components etc., as shown in the Usage example.

Generic access is built on top of ID-based access used by the ecs.World. Generic functions and types provide type-safety and are more user-friendly than ID-based access. However, when querying many components, generic queries have a runtime overhead of around 10-20%. For performance-critical code, the use of the ID-based methods of ecs.World may be worth testing.

For more details, see the API docs and examples.

Benchmarks

Versus Array of Structs

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 requires a constant time of 2.5ns per access, independent of the memory per entity (x axis) and 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 / 8 float64 values), Arche outperforms AoS and AoP, particularly with a large number of entities.

Benchmark vs. AoS and AoP
CPU benchmarks of Arche (black) vs. Array of Structs (AoS, red) and Array of Pointers (AoP, blue).

Versus other Go ECS implementations

See the latest Benchmarks CI run.

Cite as

Lange, M. (2023): Arche – An archetype-based Entity Component System for Go. GitHub repository: https://github.com/mlange-42/arche

License

This project is distributed under the MIT licence.

Documentation

Overview

Package arche is an archetype-based Entity Component System for Go.

See the sub-packages:

Directories

Path Synopsis
benchmark module
ecs
Package ecs contains Arche's core API.
Package ecs contains Arche's core API.
stats
Package stats provides the structs returned by ecs.World.Stats().
Package stats provides the structs returned by ecs.World.Stats().
Package examples contains examples for Arche.
Package examples contains examples for Arche.
base
Demonstrates the core API that uses component IDs for access.
Demonstrates the core API that uses component IDs for access.
change_listener
Demonstrates the use of a EntityEvent listener, notifying on changes to the component composition of entities.
Demonstrates the use of a EntityEvent listener, notifying on changes to the component composition of entities.
filter
Demonstrates the logic filter API, which provides additional query flexibility on top of the core API.
Demonstrates the logic filter API, which provides additional query flexibility on top of the core API.
generic
Demonstrates the generic API, which provides type-safety and convenience over the ID-based core API.
Demonstrates the generic API, which provides type-safety and convenience over the ID-based core API.
locked_world
Demonstrates how to create, remove or alter entities, despite the World is locked during query iteration.
Demonstrates how to create, remove or alter entities, despite the World is locked during query iteration.
random_access
Demonstrates generic access to arbitrary/random entities.
Demonstrates generic access to arbitrary/random entities.
random_sampling
Demonstrates random sampling of a fixed number of entities from a query using Query.Step().
Demonstrates random sampling of a fixed number of entities from a query using Query.Step().
readme
The minimal example from the README using generic access.
The minimal example from the README using generic access.
systems
Demonstrates how to implement systems.
Demonstrates how to implement systems.
world_stats
Demonstrates using world and archetype statistics.
Demonstrates using world and archetype statistics.
Package filter contains Arche's advanced logic filtering API.
Package filter contains Arche's advanced logic filtering API.
Package generic contains Arche's generic API.
Package generic contains Arche's generic API.
generate
Package generate is for generating the boilerplate code required for the generic API.
Package generate is for generating the boilerplate code required for the generic API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL