Entities & Components
Entities and components are the primary building blocks of the ECS concept. This chapter explains their representation and manipulation in Arche.
Entities
An Entity (ecs.Entity
) in Arche is merely an ID and contains no data itself.
The only method of an entity is ecs.Entity.IsZero
.
The only entity that can be directly created by the user is the zero entity, in two possible ways:
1var zero1 ecs.Entity
2fmt.Println(zero1.IsZero()) // prints true
3
4zero2 := ecs.Entity{}
5fmt.Println(zero2.IsZero()) // prints true
All other entities must be created through the ecs.World
(see section Create entities below)
Components
With each entity, an arbitrary number of Components can be associated.
Components are simple, user-defined Go struct
s (or other go types):
1// Position component
2type Position struct {
3 X float64
4 Y float64
5}
6
7// Heading component
8type Heading struct {
9 Angle float64
10}
Components are stored in the World and accessed through Queries or through the world itself (see World Entity Access).
Component IDs
Each component type has a unique ID, which is used to access it in the ID-based API.
Component IDs can be registered as well as obtained through ecs.ComponentID
.
1world := ecs.NewWorld()
2
3posID := ecs.ComponentID[Position](&world)
4headID := ecs.ComponentID[Heading](&world)
5
6_, _ = posID, headID
When a type is used as a component the first time, it is automatically registered. Thus, it is not necessary to register all required components during initialization.
Create entities
The most basic way to create an entity is ecs.World.NewEntity
:
1world := ecs.NewWorld()
2
3entity := world.NewEntity()
4_ = entity
Here, we get an entity without any components.
However, NewEntity
takes an arbitrary number of components IDs for the components that should be associated with the entity:
1world := ecs.NewWorld()
2
3posID := ecs.ComponentID[Position](&world)
4headID := ecs.ComponentID[Heading](&world)
5
6_ = world.NewEntity(posID)
7_ = world.NewEntity(posID, headID)
We get an entity with Position
, and another one with Position
and Heading
.
The components of the entity are initialized with their zero values.
Important
Note that entities should always be stored and passed around by value/copy, never via pointers!
Generic API
Creating entities using the generic API requires a generic MapX, like generic.Map2
:
1world := ecs.NewWorld()
2
3builder := generic.NewMap2[Position, Heading](&world)
4
5_ = builder.New()
We get an entity with Position
and Heading
, initialized to their zero values.
Alternatively, entities can be created with initialized components through Map2.NewWith
:
1world := ecs.NewWorld()
2
3builder := generic.NewMap2[Position, Heading](&world)
4
5_ = builder.NewWith(
6 &Position{X: 1, Y: 2},
7 &Heading{Angle: 180},
8)
We get an entity with Position
and Heading
, initialized according to values behind the passed pointers.
Tip
The 2
in Map2
stands for the number of components.
In the generic API, there are also FilterX
and QueryX
.
All these types are available for X
in range 0 (or 1) to 12.
Batch Creation
For faster batch creation of many entities, see chapter Batch Operations.
Add and remove components
Components are added to and removed from entities through the world,
with ecs.World.Add
and ecs.World.Remove
.
With generics, use a generic.Map2
again:
1world := ecs.NewWorld()
2
3mapper := generic.NewMap2[Position, Heading](&world)
4
5entity := world.NewEntity()
6
7mapper.Add(entity)
8mapper.Remove(entity)
1world := ecs.NewWorld()
2
3posID := ecs.ComponentID[Position](&world)
4headID := ecs.ComponentID[Heading](&world)
5
6entity := world.NewEntity()
7
8world.Add(entity, posID, headID)
9world.Remove(entity, posID, headID)
First, we add Position
and Heading
to the entity, then we remove both.
Important
Note that generic types like MapX should be stored and re-used where possible, particularly over time steps.
Using the generic API, it is also possible to assign initialized components with
generic.Map2.Assign
, similar to Map2.NewWith
:
1world := ecs.NewWorld()
2
3mapper := generic.NewMap2[Position, Heading](&world)
4
5entity := world.NewEntity()
6
7mapper.Assign(
8 entity,
9 &Position{X: 1, Y: 2},
10 &Heading{Angle: 180},
11)
Exchange components
Sometimes one or more components should be added to an entity, and others should be removed.
This can be bundled into a single exchange operation for efficiency.
This is done with ecs.World.Exchange
, or using a generic.Exchange
:
1world := ecs.NewWorld()
2
3builder := generic.NewMap1[Position](&world)
4entity := builder.New()
5
6exchange := generic.NewExchange(&world).
7 Adds(generic.T[Heading]()). // Component(s) to add.
8 Removes(generic.T[Position]()) // Component(s) to remove.
9
10exchange.Exchange(entity)
1world := ecs.NewWorld()
2
3posID := ecs.ComponentID[Position](&world)
4headID := ecs.ComponentID[Heading](&world)
5
6entity := world.NewEntity(posID)
7
8world.Exchange(entity,
9 []ecs.ID{headID}, // Component(s) to add.
10 []ecs.ID{posID}, // Component(s) to remove.
11)
Remove entities
Entities can be removed from the world with ecs.World.RemoveEntity
:
1world := ecs.NewWorld()
2
3entity := world.NewEntity()
4world.RemoveEntity(entity)
After removal, the entity will be recycled.
For that sake, each entity has a generation variable which allows to distinguish recycled entities.
With ecs.World.Alive
, it can be tested whether an entity is still alive:
1world := ecs.NewWorld()
2
3entity := world.NewEntity()
4fmt.Println(world.Alive(entity)) // prints true
5
6world.RemoveEntity(entity)
7fmt.Println(world.Alive(entity)) // prints false