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
39 changes: 23 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ volt.RegisterComponent[transformComponent](world, &ComponentConfig[transformComp
```go
entityId := world.CreateEntity("entityName")
```
**Important**: the entity name MUST be unique.

- Add the component to the entity
```go
component := volt.ConfigureComponent[transformComponent](&scene.World, transformConfiguration{x: 1.0, y: 2.0, z: 3.0})
Expand Down Expand Up @@ -192,27 +194,32 @@ Few ECS tools exist for Go. Arche and unitoftime/ecs are probably the most looke
In the benchmark folder, this module is compared to both of them.

- Go - v1.24.0
- Volt - v1.4.0
- Volt - v1.5.0
- [Arche - v0.15.3](https://github.com/mlange-42/arche)
- [UECS - v0.0.3](https://github.com/unitoftime/ecs)

The given results were produced by a ryzen 7 5800x, with 100.000 entities:

| Benchmark feature (entities count) | Time/Operation | Bytes/Operation | Allocations/Operation |
|-------------------------------------------------------------------|----------------|-----------------|-----------------------|
| BenchmarkCreateEntityVolt (100000) | 41716162 ns/op | 57996246 B/op | 200517 allocs/op |
| BenchmarkCreateEntityArche (100000) | 6922002 ns/op | 11096962 B/op | 61 allocs/op |
| BenchmarkCreateEntityUECS (100000) | 34596263 ns/op | 49119538 B/op | 200146 allocs/op |
| BenchmarkIterateVolt (100000) | 317646 ns/op | 264 B/op | 9 allocs/op |
| BenchmarkIterateConcurrentlyVolt (100000) - 16 concurrent workers | 95976 ns/op | 3327 B/op | 93 allocs/op |
| BenchmarkIterateArche (100000) | 429663 ns/op | 354 B/op | 4 allocs/op |
| BenchmarkIterateUECS (100000) | 234043 ns/op | 128 B/op | 3 allocs/op |
| BenchmarkAddVolt (100000) | 29081055 ns/op | 4806438 B/op | 300002 allocs/op |
| BenchmarkAddArche (100000) | 4262538 ns/op | 119805 B/op | 100000 allocs/op |
| BenchmarkAddUECS (100000) | 37041871 ns/op | 4574654 B/op | 100004 allocs/op |
| BenchmarkRemoveVolt (100000) | 21988113 ns/op | 400000 B/op | 100000 allocs/op |
| BenchmarkRemoveArche (100000) | 4749902 ns/op | 100000 B/op | 100000 allocs/op |
| BenchmarkRemoveUECS (100000) | 31742113 ns/op | 3328168 B/op | 100000 allocs/op |
goos: linux
goarch: amd64
pkg: benchmark
cpu: AMD Ryzen 7 5800X 8-Core Processor

| Benchmark | Iterations | ns/op | B/op | Allocs/op |
|---------------------------------|------------|-----------|------------|-----------|
| BenchmarkCreateEntityArche-16 | 171 | 6948273 | 11096966 | 61 |
| BenchmarkIterateArche-16 | 2704 | 426795 | 354 | 4 |
| BenchmarkAddArche-16 | 279 | 4250519 | 120089 | 100000 |
| BenchmarkRemoveArche-16 | 249 | 4821120 | 100000 | 100000 |
| BenchmarkCreateEntityUECS-16 | 34 | 37943381 | 49119549 | 200146 |
| BenchmarkIterateUECS-16 | 3885 | 287027 | 128 | 3 |
| BenchmarkAddUECS-16 | 30 | 38097927 | 4620476 | 100004 |
| BenchmarkRemoveUECS-16 | 40 | 31008811 | 3302536 | 100000 |
| BenchmarkCreateEntityVolt-16 | 49 | 27246822 | 41214216 | 200259 |
| BenchmarkIterateVolt-16 | 3651 | 329858 | 264 | 9 |
| BenchmarkIterateConcurrentlyVolt-16 | 10000 | 102732 | 3330 | 93 |
| BenchmarkAddVolt-16 | 54 | 22508281 | 4597363 | 300001 |
| BenchmarkRemoveVolt-16 | 72 | 17219355 | 400001 | 100000 |

These results show a few things:
- Arche is the fastest tool for writes operations. In our game development though we would rather lean towards fastest read operations, because the games loops will read way more often than write.
Expand Down
94 changes: 33 additions & 61 deletions world.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
package volt

import (
"math/rand"
"hash/fnv"
"slices"
"strings"
)

// uint16 identifier, for small scoped data.
Expand Down Expand Up @@ -44,14 +43,12 @@
//
// It avoids the garbage collector to analyze this data constantly,
// at the price of a fixed data size.
type entityName [64]byte
type entitiesNames map[entityName]EntityId
type entityName = string
type entities map[EntityId]entityRecord

// World representation, container of all the data related to entities and their Components.
type World struct {
componentsRegistry ComponentsRegister
entitiesNames entitiesNames
entities entities
archetypes []archetype
storage []storage
Expand All @@ -67,7 +64,6 @@
// It preallocates initialCapacity in memory.
func CreateWorld(initialCapacity int) *World {
world := &World{
entitiesNames: make(entitiesNames, initialCapacity),
entities: make(entities, initialCapacity),
archetypes: make([]archetype, 0, 1024),
storage: make([]storage, TAGS_INDICES),
Expand Down Expand Up @@ -102,21 +98,19 @@
world.componentRemovedFn = componentRemovedFn
}

func newEntityId() EntityId {
return EntityId(rand.Uint64())
}

// CreateEntity creates a new Entity in World;
// It is linked to no Component.
func (world *World) CreateEntity(name string) EntityId {
entityName := stringToEntityName(name)
entityId := newEntityId()
if existingId := world.SearchEntity(name); existingId != 0 {
return existingId
}

Check warning on line 106 in world.go

View check run for this annotation

Codecov / codecov/patch

world.go#L105-L106

Added lines #L105 - L106 were not covered by tests

entityId := hashEntityName(name)
archetype := world.getArchetypeForComponentsIds()

world.entitiesNames[entityName] = entityId
entityRecord := entityRecord{
Id: entityId,
name: entityName,
name: name,
}
world.entities[entityId] = entityRecord
world.setArchetype(entityRecord, archetype)
Expand All @@ -127,11 +121,9 @@
// CreateEntityWithComponents2 creates an entity in World;
// It sets the components A, B to the entity, for faster performances than the atomic version.
func CreateEntityWithComponents2[A, B ComponentInterface](world *World, name string, a A, b B) (EntityId, error) {
entityName := stringToEntityName(name)
entityId := newEntityId()
entityId := hashEntityName(name)

world.entitiesNames[entityName] = entityId
entityRecord := entityRecord{Id: entityId, name: entityName}
entityRecord := entityRecord{Id: entityId, name: name}
world.entities[entityId] = entityRecord

err := addComponents2(world, entityRecord, a, b)
Expand All @@ -146,11 +138,9 @@
//
// It sets the components A, B, C to the entity, for faster performances than the atomic version.
func CreateEntityWithComponents3[A, B, C ComponentInterface](world *World, name string, a A, b B, c C) (EntityId, error) {
entityName := stringToEntityName(name)
entityId := newEntityId()
entityId := hashEntityName(name)

world.entitiesNames[entityName] = entityId
entityRecord := entityRecord{Id: entityId, name: entityName}
entityRecord := entityRecord{Id: entityId, name: name}
world.entities[entityId] = entityRecord

err := addComponents3(world, entityRecord, a, b, c)
Expand All @@ -165,11 +155,9 @@
//
// It sets the components A, B, C, D to the entity, for faster performances than the atomic version.
func CreateEntityWithComponents4[A, B, C, D ComponentInterface](world *World, name string, a A, b B, c C, d D) (EntityId, error) {
entityName := stringToEntityName(name)
entityId := newEntityId()
entityId := hashEntityName(name)

world.entitiesNames[entityName] = entityId
entityRecord := entityRecord{Id: entityId, name: entityName}
entityRecord := entityRecord{Id: entityId, name: name}
world.entities[entityId] = entityRecord

err := addComponents4(world, entityRecord, a, b, c, d)
Expand All @@ -184,11 +172,9 @@
//
// It sets the components A, B, C, D, E to the entity, for faster performances than the atomic version.
func CreateEntityWithComponents5[A, B, C, D, E ComponentInterface](world *World, name string, a A, b B, c C, d D, e E) (EntityId, error) {
entityName := stringToEntityName(name)
entityId := newEntityId()
entityId := hashEntityName(name)

world.entitiesNames[entityName] = entityId
entityRecord := entityRecord{Id: entityId, name: entityName}
entityRecord := entityRecord{Id: entityId, name: name}
world.entities[entityId] = entityRecord

err := addComponents5(world, entityRecord, a, b, c, d, e)
Expand All @@ -203,11 +189,8 @@
//
// It sets the components A, B, C, D, E, F to the entity, for faster performances than the atomic version.
func CreateEntityWithComponents6[A, B, C, D, E, F ComponentInterface](world *World, name string, a A, b B, c C, d D, e E, f F) (EntityId, error) {
entityName := stringToEntityName(name)
entityId := newEntityId()

world.entitiesNames[entityName] = entityId
entityRecord := entityRecord{Id: entityId, name: entityName}
entityId := hashEntityName(name)
entityRecord := entityRecord{Id: entityId, name: name}
world.entities[entityId] = entityRecord

err := addComponents6(world, entityRecord, a, b, c, d, e, f)
Expand All @@ -222,11 +205,8 @@
//
// It sets the components A, B, C, D, E, F, G to the entity, for faster performances than the atomic version.
func CreateEntityWithComponents7[A, B, C, D, E, F, G ComponentInterface](world *World, name string, a A, b B, c C, d D, e E, f F, g G) (EntityId, error) {
entityName := stringToEntityName(name)
entityId := newEntityId()

world.entitiesNames[entityName] = entityId
entityRecord := entityRecord{Id: entityId, name: entityName}
entityId := hashEntityName(name)
entityRecord := entityRecord{Id: entityId, name: name}
world.entities[entityId] = entityRecord

err := addComponents7(world, entityRecord, a, b, c, d, e, f, g)
Expand All @@ -241,11 +221,8 @@
//
// It sets the components A, B, C, D, E, F, G, H to the entity, for faster performances than the atomic version.
func CreateEntityWithComponents8[A, B, C, D, E, F, G, H ComponentInterface](world *World, name string, a A, b B, c C, d D, e E, f F, g G, h H) (EntityId, error) {
entityName := stringToEntityName(name)
entityId := newEntityId()

world.entitiesNames[entityName] = entityId
entityRecord := entityRecord{Id: entityId, name: entityName}
entityId := hashEntityName(name)
entityRecord := entityRecord{Id: entityId, name: name}
world.entities[entityId] = entityRecord

err := addComponents8(world, entityRecord, a, b, c, d, e, f, g, h)
Expand Down Expand Up @@ -290,15 +267,14 @@
world.archetypes[archetype.Id] = archetype
}

delete(world.entitiesNames, world.entities[entityId].name)
delete(world.entities, entityId)
}

// SearchEntity returns the EntityId named by name.
// If not found, returns 0.
func (world *World) SearchEntity(name string) EntityId {
entityName := stringToEntityName(name)
if entityId, ok := world.entitiesNames[entityName]; ok {
entityId := hashEntityName(name)
if _, ok := world.entities[entityId]; ok {
return entityId
}

Expand All @@ -309,34 +285,30 @@
// If not found, returns an empty string.
func (world *World) GetEntityName(entityId EntityId) string {
if entity, ok := world.entities[entityId]; ok {
return entityNameToString(entity.name)
return entity.name
}

return ""
}

// SetEntityName sets the name for an EntityId.
func (world *World) SetEntityName(entityId EntityId, name string) {
entityName := stringToEntityName(name)

entityRecord := world.entities[entityId]
entityRecord.name = entityName
entityRecord.name = name

Check warning on line 297 in world.go

View check run for this annotation

Codecov / codecov/patch

world.go#L297

Added line #L297 was not covered by tests
world.entities[entityId] = entityRecord
world.entitiesNames[entityName] = entityId
}

// Count returns the number of entities in World.
func (world *World) Count() int {
return len(world.entities)
}

func stringToEntityName(name string) entityName {
var nameByte entityName
copy(nameByte[:], name)

return nameByte
}
func hashEntityName(name entityName) EntityId {
h := fnv.New64()
_, err := h.Write([]byte(name))
if err != nil {
return EntityId(0)
}

Check warning on line 311 in world.go

View check run for this annotation

Codecov / codecov/patch

world.go#L310-L311

Added lines #L310 - L311 were not covered by tests

func entityNameToString(entityName entityName) string {
return strings.TrimRight(string(entityName[:]), "\x00")
return EntityId(h.Sum64())
}
Loading