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 Creating 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
.
In this case, the components are initialized with their zero value.
Alternatively, entities can be created with initialized components through
ecs.World.NewEntityWith
:
1world := ecs.NewWorld()
2
3posID := ecs.ComponentID[Position](&world)
4headID := ecs.ComponentID[Heading](&world)
5
6_ = world.NewEntityWith(
7 ecs.Component{ID: posID, Comp: &Position{X: 1, Y: 2}},
8 ecs.Component{ID: headID, Comp: &Heading{Angle: 180}},
9)
We get an entity with Position
and Heading
, initialized according to the passed 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.
Equivalent to
ecs.World.NewEntityWith
, generic MapX’s have
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)
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.
Note that generic types like MapX should be stored and re-used where possible, particularly over time steps.
It is also possible to assign initialized components with
ecs.World.Assign
/
generic.Map2.Assign
,
similar to
ecs.World.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)
1world := ecs.NewWorld()
2
3posID := ecs.ComponentID[Position](&world)
4headID := ecs.ComponentID[Heading](&world)
5
6entity := world.NewEntity()
7
8world.Assign(
9 entity,
10 ecs.Component{ID: posID, Comp: &Position{X: 1, Y: 2}},
11 ecs.Component{ID: headID, Comp: &Heading{Angle: 180}},
12)
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