diff --git a/README.md b/README.md index b68329c..ce44ff6 100644 --- a/README.md +++ b/README.md @@ -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}) @@ -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. diff --git a/world.go b/world.go index 207cabb..60cf0c4 100644 --- a/world.go +++ b/world.go @@ -2,9 +2,8 @@ package volt import ( - "math/rand" + "hash/fnv" "slices" - "strings" ) // uint16 identifier, for small scoped data. @@ -44,14 +43,12 @@ type entityRecord struct { // // 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 @@ -67,7 +64,6 @@ type World struct { // 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), @@ -102,21 +98,19 @@ func (world *World) SetComponentRemovedFn(componentRemovedFn func(entityId Entit 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 + } + + 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) @@ -127,11 +121,9 @@ func (world *World) CreateEntity(name string) EntityId { // 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) @@ -146,11 +138,9 @@ func CreateEntityWithComponents2[A, B ComponentInterface](world *World, name str // // 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) @@ -165,11 +155,9 @@ func CreateEntityWithComponents3[A, B, C ComponentInterface](world *World, name // // 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) @@ -184,11 +172,9 @@ func CreateEntityWithComponents4[A, B, C, D ComponentInterface](world *World, na // // 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) @@ -203,11 +189,8 @@ func CreateEntityWithComponents5[A, B, C, D, E ComponentInterface](world *World, // // 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) @@ -222,11 +205,8 @@ func CreateEntityWithComponents6[A, B, C, D, E, F ComponentInterface](world *Wor // // 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) @@ -241,11 +221,8 @@ func CreateEntityWithComponents7[A, B, C, D, E, F, G ComponentInterface](world * // // 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) @@ -290,15 +267,14 @@ func (world *World) RemoveEntity(entityId EntityId) { 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 } @@ -309,7 +285,7 @@ func (world *World) SearchEntity(name string) EntityId { // 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 "" @@ -317,12 +293,9 @@ func (world *World) GetEntityName(entityId EntityId) string { // 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 world.entities[entityId] = entityRecord - world.entitiesNames[entityName] = entityId } // Count returns the number of entities in World. @@ -330,13 +303,12 @@ 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) + } -func entityNameToString(entityName entityName) string { - return strings.TrimRight(string(entityName[:]), "\x00") + return EntityId(h.Sum64()) }