Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/golangci_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ jobs:
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v6.1.1
uses: golangci/golangci-lint-action@v9
with:
version: v1.64.5
version: v2.13.2
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ There is many ways to write an ECS, and Volt is based on the Archetype paradigm.
## Knowledge
### Entity
An entity is the end object in a game (e.g. a character). It is only defined by
its identifier called EntityId. This identifier is generated, its type uint64 avoiding to generate twice the same id.
When an entity is removed, this identifier can be used again for a new one.
its identifier called EntityId: a uint64 handle packing the entity's slot (index, low 32 bits)
and the generation of that slot (high 32 bits, starting at 1).

When an entity is removed, its slot can be used again for a new one, but the generation of the slot is bumped:
a handle kept after its entity was removed is dead, and never refers to the slot's new occupant
(`Exists` reports false, `GetComponent` returns nil, `AddComponent` returns an error).
The zero value `EntityId(0)` is the null handle, which never refers to a live entity: an unset field is null.
`Index()`, `Generation()` and `IsNull()` expose the parts of a handle.

Looking at the benchmark, a scene can handle between 100.000 to 1.000.000 depending on your machine and the complexity of the project.
But of course, the lower the better, as it will allow the project to run on slower computers.
Expand Down
2 changes: 1 addition & 1 deletion archetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (world *World) setArchetype(entityRecord entityRecord, archetype *archetype

entityRecord.key = len(archetype.entities) - 1
entityRecord.archetypeId = archetype.Id
world.entities[entityRecord.Id] = entityRecord
world.entities[entityRecord.Id.Index()] = entityRecord
}

func (world *World) getArchetypeForComponentsIds(componentsIds ...ComponentId) *archetype {
Expand Down
6 changes: 3 additions & 3 deletions archetype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ func TestArchetypeGraph_EdgesAreReused(t *testing.T) {
if err := AddComponent(world, e, testComponent2{}); err != nil {
t.Fatalf("%s", err.Error())
}
base := world.entities[e].archetypeId
base := world.entities[e.Index()].archetypeId
var withC1 archetypeId

for i := 0; i < 5; i++ {
if err := AddComponent(world, e, testComponent1{}); err != nil {
t.Fatalf("add iteration %d: %s", i, err.Error())
}
with := world.entities[e].archetypeId
with := world.entities[e.Index()].archetypeId
if i == 0 {
withC1 = with
} else if with != withC1 {
Expand All @@ -88,7 +88,7 @@ func TestArchetypeGraph_EdgesAreReused(t *testing.T) {
if err := RemoveComponent[testComponent1](world, e); err != nil {
t.Fatalf("remove iteration %d: %s", i, err.Error())
}
if back := world.entities[e].archetypeId; back != base {
if back := world.entities[e.Index()].archetypeId; back != base {
t.Fatalf("iteration %d: entity did not return to base archetype (%d != %d)", i, back, base)
}
}
Expand Down
23 changes: 23 additions & 0 deletions benchmark/volt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,26 @@ func BenchmarkRemoveVolt(b *testing.B) {

b.ReportAllocs()
}

// BenchmarkCreateRemoveVolt measures a full recycle cycle: every entity is
// removed then recreated with the same components, so the id pool is exercised
// on both sides (free then reuse) on every iteration.
func BenchmarkCreateRemoveVolt(b *testing.B) {
world := volt.CreateWorld(ENTITIES_COUNT)
volt.RegisterComponent[testTransform](world, &volt.ComponentConfig[testTransform]{})
volt.RegisterComponent[testTag](world, &volt.ComponentConfig[testTag]{})

entities := make([]volt.EntityId, ENTITIES_COUNT)
for i := range entities {
entities[i], _ = volt.CreateEntityWithComponents2(world, testTransform{}, testTag{})
}

for b.Loop() {
for i, entityId := range entities {
world.RemoveEntity(entityId)
entities[i], _ = volt.CreateEntityWithComponents2(world, testTransform{}, testTag{})
}
}

b.ReportAllocs()
}
66 changes: 33 additions & 33 deletions component.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func AddComponent[T ComponentInterface](world *World, entityId EntityId, compone
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

componentId := component.GetComponentId()
if world.hasComponents(entityRecord, componentId) {
Expand Down Expand Up @@ -79,7 +79,7 @@ func AddComponents2[A, B ComponentInterface](world *World, entityId EntityId, a
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

return addComponents2(world, entityRecord, a, b)
}
Expand Down Expand Up @@ -115,7 +115,7 @@ func AddComponents3[A, B, C ComponentInterface](world *World, entityId EntityId,
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

return addComponents3(world, entityRecord, a, b, c)
}
Expand Down Expand Up @@ -153,7 +153,7 @@ func AddComponents4[A, B, C, D ComponentInterface](world *World, entityId Entity
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

return addComponents4(world, entityRecord, a, b, c, d)
}
Expand Down Expand Up @@ -192,7 +192,7 @@ func AddComponents5[A, B, C, D, E ComponentInterface](world *World, entityId Ent
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

return addComponents5(world, entityRecord, a, b, c, d, e)
}
Expand Down Expand Up @@ -232,7 +232,7 @@ func AddComponents6[A, B, C, D, E, F ComponentInterface](world *World, entityId
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

return addComponents6(world, entityRecord, a, b, c, d, e, f)
}
Expand Down Expand Up @@ -273,7 +273,7 @@ func AddComponents7[A, B, C, D, E, F, G ComponentInterface](world *World, entity
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

return addComponents7(world, entityRecord, a, b, c, d, e, f, g)
}
Expand Down Expand Up @@ -315,7 +315,7 @@ func AddComponents8[A, B, C, D, E, F, G, H ComponentInterface](world *World, ent
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

return addComponents8(world, entityRecord, a, b, c, d, e, f, g, h)
}
Expand Down Expand Up @@ -357,7 +357,7 @@ func (world *World) AddComponent(entityId EntityId, componentId ComponentId, con
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

if world.hasComponents(entityRecord, componentId) {
return fmt.Errorf("the entity %d already owns the component %d", entityId, componentId)
Expand Down Expand Up @@ -388,7 +388,7 @@ func (world *World) AddComponents(entityId EntityId, componentsIdsConfs ...Compo
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

var componentsIds []ComponentId
for _, componentIdConf := range componentsIdsConfs {
Expand Down Expand Up @@ -426,7 +426,7 @@ func RemoveComponent[T ComponentInterface](world *World, entityId EntityId) erro
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

if !world.hasComponents(entityRecord, componentId) {
return fmt.Errorf("the entity %d doesn't own the component %d", entityId, componentId)
Expand All @@ -449,7 +449,7 @@ func (world *World) RemoveComponent(entityId EntityId, componentId ComponentId)
if !world.Exists(entityId) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

if !world.hasComponents(entityRecord, componentId) {
return fmt.Errorf("the entity %d doesn't own the component %d", entityId, componentId)
Expand Down Expand Up @@ -489,7 +489,7 @@ func (world *World) HasComponents(entityId EntityId, componentsIds ...ComponentI
if !world.Exists(entityId) {
return false
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

return world.hasComponents(entityRecord, componentsIds...)
}
Expand Down Expand Up @@ -517,7 +517,7 @@ func GetComponent[T ComponentInterface](world *World, entityId EntityId) *T {
if s == nil {
return nil
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]

if !s.hasArchetype(entityRecord.archetypeId) {
return nil
Expand All @@ -537,7 +537,7 @@ func (world *World) GetComponent(entityId EntityId, componentId ComponentId) (an
if !world.Exists(entityId) {
return nil, fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]
entityRecord := world.entities[entityId.Index()]
s, err := world.getStorageForComponentId(componentId)
if err != nil {
return nil, err
Expand All @@ -558,8 +558,8 @@ func addComponentsToArchetype1[A ComponentInterface](world *World, entityRecord
return fmt.Errorf("no storage found for component %d", componentId)
}

// If the entity has no component, simply add it the archetype
if entityRecord.archetypeId == 0 {
// An unplaced entity (fresh slot) is listed in no archetype yet: just place it.
if entityRecord.key < 0 {
world.setArchetype(entityRecord, archetype)
} else {
oldArchetype := world.getArchetype(entityRecord)
Expand All @@ -582,8 +582,8 @@ func addComponentsToArchetype2[A, B ComponentInterface](world *World, entityReco
return fmt.Errorf("no storage found for component %v", componentsIds)
}

// If the entity has no component, simply add it the archetype
if entityRecord.archetypeId == 0 {
// An unplaced entity (fresh slot) is listed in no archetype yet: just place it.
if entityRecord.key < 0 {
world.setArchetype(entityRecord, archetype)
} else {
oldArchetype := world.getArchetype(entityRecord)
Expand All @@ -609,8 +609,8 @@ func addComponentsToArchetype3[A, B, C ComponentInterface](world *World, entityR
return fmt.Errorf("no storage found for components %v", componentsIds)
}

// If the entity has no component, simply add it the archetype
if entityRecord.archetypeId == 0 {
// An unplaced entity (fresh slot) is listed in no archetype yet: just place it.
if entityRecord.key < 0 {
world.setArchetype(entityRecord, archetype)
} else {
oldArchetype := world.getArchetype(entityRecord)
Expand Down Expand Up @@ -638,8 +638,8 @@ func addComponentsToArchetype4[A, B, C, D ComponentInterface](world *World, enti
return fmt.Errorf("no storage found for components %v", componentsIds)
}

// If the entity has no component, simply add it the archetype
if entityRecord.archetypeId == 0 {
// An unplaced entity (fresh slot) is listed in no archetype yet: just place it.
if entityRecord.key < 0 {
world.setArchetype(entityRecord, archetype)
} else {
oldArchetype := world.getArchetype(entityRecord)
Expand Down Expand Up @@ -669,8 +669,8 @@ func addComponentsToArchetype5[A, B, C, D, E ComponentInterface](world *World, e
return fmt.Errorf("no storage found for components %v", componentsIds)
}

// If the entity has no component, simply add it the archetype
if entityRecord.archetypeId == 0 {
// An unplaced entity (fresh slot) is listed in no archetype yet: just place it.
if entityRecord.key < 0 {
world.setArchetype(entityRecord, archetype)
} else {
oldArchetype := world.getArchetype(entityRecord)
Expand Down Expand Up @@ -702,8 +702,8 @@ func addComponentsToArchetype6[A, B, C, D, E, F ComponentInterface](world *World
return fmt.Errorf("no storage found for components %v", componentsIds)
}

// If the entity has no component, simply add it the archetype
if entityRecord.archetypeId == 0 {
// An unplaced entity (fresh slot) is listed in no archetype yet: just place it.
if entityRecord.key < 0 {
world.setArchetype(entityRecord, archetype)
} else {
oldArchetype := world.getArchetype(entityRecord)
Expand Down Expand Up @@ -737,8 +737,8 @@ func addComponentsToArchetype7[A, B, C, D, E, F, G ComponentInterface](world *Wo
return fmt.Errorf("no storage found for components %v", componentsIds)
}

// If the entity has no component, simply add it the archetype
if entityRecord.archetypeId == 0 {
// An unplaced entity (fresh slot) is listed in no archetype yet: just place it.
if entityRecord.key < 0 {
world.setArchetype(entityRecord, archetype)
} else {
oldArchetype := world.getArchetype(entityRecord)
Expand Down Expand Up @@ -774,8 +774,8 @@ func addComponentsToArchetype8[A, B, C, D, E, F, G, H ComponentInterface](world
return fmt.Errorf("no storage found for components %v", componentsIds)
}

// If the entity has no component, simply add it the archetype
if entityRecord.archetypeId == 0 {
// An unplaced entity (fresh slot) is listed in no archetype yet: just place it.
if entityRecord.key < 0 {
world.setArchetype(entityRecord, archetype)
} else {
oldArchetype := world.getArchetype(entityRecord)
Expand Down Expand Up @@ -820,9 +820,9 @@ func moveComponentsToArchetype(world *World, entityRecord entityRecord, oldArche
lastEntityKey = len(oldArchetype.entities) - 1

lastEntityId := oldArchetype.entities[lastEntityKey]
lastEntity := world.entities[lastEntityId]
lastEntity := world.entities[lastEntityId.Index()]
lastEntity.key = entityRecord.key
world.entities[lastEntityId] = lastEntity
world.entities[lastEntityId.Index()] = lastEntity

oldArchetype.entities[entityRecord.key] = lastEntityId
oldArchetype.entities = oldArchetype.entities[:lastEntityKey]
Expand Down
Loading
Loading