Skip to content

Commit ebb6e56

Browse files
authored
Feat: Improve performances for add/remove (#26)
* feat: improve add/remove performances The pool of entities ids (filled with deleted entities) allows to use a slice instead of a map to store entities & component register. The names are not manager by Volt anymore, the common method would be to add a MetadataComponent to the tracked entities (if necessary to fetch by names) * chore: remove profiling in volt_test * chore: documentation of events & naming Clean documentation, removing entities names in the API * chore: update benchmark values * chore: clean documentation BREAKING CHANGE: the naming of entities is removed
1 parent c822905 commit ebb6e56

File tree

13 files changed

+267
-334
lines changed

13 files changed

+267
-334
lines changed

README.md

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ There is many ways to write an ECS, and Volt is based on the Archetype paradigm.
1717
## Knowledge
1818
### Entity
1919
An entity is the end object in a game (e.g. a character). It is only defined by
20-
its identifier called EntityId. This identifier is randomly generated, its type uint64 avoiding to generate twice the same id.
21-
It is also required to set a name for each entity, only used to easily retrieve them when required.
20+
its identifier called EntityId. This identifier is generated, its type uint64 avoiding to generate twice the same id.
21+
When an entity is removed, this identifier can be used again for a new one.
2222

2323
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.
2424
But of course, the lower the better, as it will allow the project to run on slower computers.
@@ -82,14 +82,14 @@ volt.RegisterComponent[transformComponent](world, &ComponentConfig[transformComp
8282
```
8383
- Create the entity
8484
```go
85-
entityId := world.CreateEntity("entityName")
85+
entityId := world.CreateEntity()
8686
```
87-
**Important**: the entity name MUST be unique.
87+
**Important**: the entity will receive a unique identifier. When the entity is removed, this id can be used again and assigned to a new entity.
8888

8989
- Add the component to the entity
9090
```go
91-
component := volt.ConfigureComponent[transformComponent](&scene.World, transformConfiguration{x: 1.0, y: 2.0, z: 3.0})
92-
volt.AddComponent(&scene.World, entity, component)
91+
component := volt.ConfigureComponent[transformComponent](&world, transformConfiguration{x: 1.0, y: 2.0, z: 3.0})
92+
volt.AddComponent(&world, entity, component)
9393
```
9494
- Remove the component to the entity
9595
```go
@@ -102,15 +102,6 @@ if err != nil {
102102
```go
103103
world.RemoveEntity(entityId)
104104
```
105-
## Searching for an entity
106-
- Knowing an entity by its name, you can get its identifier:
107-
```go
108-
entityId := world.SearchEntity("entityName")
109-
```
110-
- The reversed search is also possible, fetching its name by its idenfier:
111-
```go
112-
entityName := world.GetEntityName(entityId)
113-
```
114105

115106
## Queries
116107
The most powerful feature is the possibility to query entities with a given set of Components.
@@ -189,12 +180,71 @@ world.HasTag(TAG_STATIC_ID, entityId)
189180
world.RemoveTag(TAG_STATIC_ID, entityId)
190181
```
191182

183+
## Events
184+
The lifecycle (creation/deletion) of entities and components can trigger events.
185+
You can configure a callback function for each of these events, to execute your custom code:
186+
```go
187+
world := volt.CreateWorld(100)
188+
world.SetEntityAddedFn(func(entityId volt.EntityId) {
189+
fmt.Println("A new entity has been created", entityId)
190+
})
191+
world.SetEntityRemovedFn(func(entityId volt.EntityId) {
192+
fmt.Println("An entity has been deleted", entityId)
193+
})
194+
world.SetComponentAddedFn(func(entityId volt.EntityId, componentId volt.ComponentId) {
195+
fmt.Println("The component", componentId, "is attached to the entity", entityId)
196+
})
197+
world.SetComponentRemovedFn(func(entityId volt.EntityId, componentId volt.ComponentId) {
198+
fmt.Println("The component", componentId, "is removed from the entity", entityId)
199+
})
200+
```
201+
202+
## Naming entities
203+
Volt managed the naming of entities up to the version 1.6.0. For performances reasons, this feature is removed from the v1.7.0+.
204+
You now have to keep track of the names by yourself in your application:
205+
- Having a simple map[name string]volt.EntityId, you can react to the events and register these. Keep in mind that if your scene has a lot
206+
of entities, it will probably have a huge impact on the garbage collector.
207+
- Add a MetadataComponent. To fetch an entity by its name can be very slow, so you probably do not want to name all your entities. For example:
208+
```go
209+
const MetadataComponentId = 0
210+
211+
type MetadataComponent struct {
212+
Name string
213+
}
214+
215+
func (MetadataComponent MetadataComponent) GetComponentId() volt.ComponentId {
216+
return MetadataComponentId
217+
}
218+
volt.RegisterComponent[MetadataComponent](&world, &volt.ComponentConfig[MetadataComponent]{BuilderFn: func(component any, configuration any) {}})
219+
220+
func GetEntityName(world *volt.World, entityId volt.EntityId) string {
221+
if world.HasComponents(entityId, MetadataComponentId) {
222+
metadata := volt.GetComponent[MetadataComponent](world, entityId)
223+
224+
return metadata.Name
225+
}
226+
227+
return ""
228+
}
229+
230+
func (scene *Scene) SearchEntity(name string) volt.EntityId {
231+
q := volt.CreateQuery1[MetadataComponent](&world, volt.QueryConfiguration{})
232+
for result := range q.Foreach(nil) {
233+
if result.A.Name == name {
234+
return result.EntityId
235+
}
236+
}
237+
238+
return 0
239+
}
240+
```
241+
192242
## Benchmark
193243
Few ECS tools exist for Go. Arche and unitoftime/ecs are probably the most looked at, and the most optimized.
194244
In the benchmark folder, this module is compared to both of them.
195245

196-
- Go - v1.24.0
197-
- Volt - v1.5.0
246+
- Go - v1.25.3
247+
- Volt - v1.7.0
198248
- [Arche - v0.15.3](https://github.com/mlange-42/arche)
199249
- [UECS - v0.0.3](https://github.com/unitoftime/ecs)
200250

@@ -207,19 +257,19 @@ cpu: AMD Ryzen 7 5800X 8-Core Processor
207257

208258
| Benchmark | Iterations | ns/op | B/op | Allocs/op |
209259
|---------------------------------|------------|-----------|------------|-----------|
210-
| BenchmarkCreateEntityArche-16 | 171 | 6948273 | 11096966 | 61 |
211-
| BenchmarkIterateArche-16 | 2704 | 426795 | 354 | 4 |
212-
| BenchmarkAddArche-16 | 279 | 4250519 | 120089 | 100000 |
213-
| BenchmarkRemoveArche-16 | 249 | 4821120 | 100000 | 100000 |
214-
| BenchmarkCreateEntityUECS-16 | 34 | 37943381 | 49119549 | 200146 |
215-
| BenchmarkIterateUECS-16 | 3885 | 287027 | 128 | 3 |
216-
| BenchmarkAddUECS-16 | 30 | 38097927 | 4620476 | 100004 |
217-
| BenchmarkRemoveUECS-16 | 40 | 31008811 | 3302536 | 100000 |
218-
| BenchmarkCreateEntityVolt-16 | 49 | 27246822 | 41214216 | 200259 |
219-
| BenchmarkIterateVolt-16 | 3651 | 329858 | 264 | 9 |
220-
| BenchmarkIterateConcurrentlyVolt-16 | 10000 | 102732 | 3330 | 93 |
221-
| BenchmarkAddVolt-16 | 54 | 22508281 | 4597363 | 300001 |
222-
| BenchmarkRemoveVolt-16 | 72 | 17219355 | 400001 | 100000 |
260+
| BenchmarkCreateEntityArche-16 | 171 | 7138387 | 11096954 | 61 |
261+
| BenchmarkIterateArche-16 | 2798 | 429744 | 354 | 4 |
262+
| BenchmarkAddArche-16 | 253 | 4673362 | 122153 | 100000 |
263+
| BenchmarkRemoveArche-16 | 247 | 4840772 | 100000 | 100000 |
264+
| BenchmarkCreateEntityUECS-16 | 27 | 38852089 | 49119503 | 200146 |
265+
| BenchmarkIterateUECS-16 | 4892 | 235333 | 128 | 3 |
266+
| BenchmarkAddUECS-16 | 28 | 38982533 | 4721942 | 100005 |
267+
| BenchmarkRemoveUECS-16 | 30 | 40290316 | 3336712 | 100000 |
268+
| BenchmarkCreateEntityVolt-16 | 63 | 18836136 | 35181458 | 100101 |
269+
| BenchmarkIterateVolt-16 | 3619 | 337764 | 256 | 8 |
270+
| BenchmarkIterateConcurrentlyVolt-16 | 9164 | 121653 | 3324 | 91 |
271+
| BenchmarkAddVolt-16 | 103 | 11379690 | 4313182 | 300000 |
272+
| BenchmarkRemoveVolt-16 | 146 | 7647252 | 400001 | 100000 |
223273

224274
These results show a few things:
225275
- 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.
@@ -236,8 +286,6 @@ The creator and maintainer of Arche has published more complex benchmarks availa
236286
https://github.com/mlange-42/go-ecs-benchmarks
237287

238288
## What is to come next ?
239-
- Tags (zero sized types) are useful to query entities with specific features: for example, in a renderer, to get only the entities with the boolean isCulled == false.
240-
This would hugely reduce the loops operations in some scenarios. Currently we can use the filters on the iterators, but it does not avoid the fact that every entity (with the given components) is looped by the renderer.
241289
- For now the system is not designed to manage writes on a concurrent way: it means it is not safe to add/remove components in queries
242290
using multiples threads/goroutines. I need to figure out how to implement this, though I never met the need for this feature myself.
243291

benchmark/volt_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package benchmark
22

33
import (
4-
"github.com/akmonengine/volt"
54
"math/rand/v2"
6-
"strconv"
75
"testing"
6+
7+
"github.com/akmonengine/volt"
88
)
99

1010
func BenchmarkCreateEntityVolt(b *testing.B) {
@@ -13,8 +13,8 @@ func BenchmarkCreateEntityVolt(b *testing.B) {
1313
volt.RegisterComponent[testTransform](world, &volt.ComponentConfig[testTransform]{})
1414
volt.RegisterComponent[testTag](world, &volt.ComponentConfig[testTag]{})
1515

16-
for j := range ENTITIES_COUNT {
17-
volt.CreateEntityWithComponents2(world, strconv.Itoa(j),
16+
for range ENTITIES_COUNT {
17+
volt.CreateEntityWithComponents2(world,
1818
testTransform{
1919
x: rand.Float64() * 100,
2020
y: rand.Float64() * 100,
@@ -34,7 +34,7 @@ func BenchmarkIterateVolt(b *testing.B) {
3434
volt.RegisterComponent[testTag](world, &volt.ComponentConfig[testTag]{})
3535

3636
for i := 0; i < ENTITIES_COUNT; i++ {
37-
id := world.CreateEntity(strconv.Itoa(i))
37+
id := world.CreateEntity()
3838
volt.AddComponent[testTransform](world, id, testTransform{})
3939
volt.AddComponent[testTag](world, id, testTag{})
4040
}
@@ -55,7 +55,7 @@ func BenchmarkIterateConcurrentlyVolt(b *testing.B) {
5555
volt.RegisterComponent[testTag](world, &volt.ComponentConfig[testTag]{})
5656

5757
for i := 0; i < ENTITIES_COUNT; i++ {
58-
id := world.CreateEntity(strconv.Itoa(i))
58+
id := world.CreateEntity()
5959
volt.AddComponent[testTransform](world, id, testTransform{})
6060
volt.AddComponent[testTag](world, id, testTag{})
6161
}
@@ -84,8 +84,8 @@ func BenchmarkAddVolt(b *testing.B) {
8484
volt.RegisterComponent[testTag](world, &volt.ComponentConfig[testTag]{})
8585

8686
entities := make([]volt.EntityId, 0, ENTITIES_COUNT)
87-
for j := range ENTITIES_COUNT {
88-
entityId := world.CreateEntity(strconv.Itoa(j))
87+
for range ENTITIES_COUNT {
88+
entityId := world.CreateEntity()
8989
volt.AddComponent(world, entityId, testTag{})
9090
entities = append(entities, entityId)
9191
}
@@ -113,8 +113,8 @@ func BenchmarkRemoveVolt(b *testing.B) {
113113
volt.RegisterComponent[testTag](world, &volt.ComponentConfig[testTag]{})
114114

115115
entities := make([]volt.EntityId, 0, ENTITIES_COUNT)
116-
for j := range ENTITIES_COUNT {
117-
entityId := world.CreateEntity(strconv.Itoa(j))
116+
for range ENTITIES_COUNT {
117+
entityId := world.CreateEntity()
118118
volt.AddComponent(world, entityId, testTag{})
119119
entities = append(entities, entityId)
120120
}

component.go

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ func ConfigureComponent[T ComponentInterface](world *World, conf any) T {
4646
// - the entity has the component
4747
// - an internal error occurs
4848
func AddComponent[T ComponentInterface](world *World, entityId EntityId, component T) error {
49-
entityRecord, ok := world.entities[entityId]
50-
if !ok {
49+
if int(entityId) >= len(world.entities) {
5150
return fmt.Errorf("entity %v does not exist", entityId)
5251
}
52+
entityRecord := world.entities[entityId]
5353

5454
componentId := component.GetComponentId()
5555
if world.hasComponents(entityRecord, componentId) {
@@ -76,10 +76,10 @@ func AddComponent[T ComponentInterface](world *World, entityId EntityId, compone
7676
//
7777
// This solution is faster than an atomic solution.
7878
func AddComponents2[A, B ComponentInterface](world *World, entityId EntityId, a A, b B) error {
79-
entityRecord, ok := world.entities[entityId]
80-
if !ok {
79+
if int(entityId) >= len(world.entities) {
8180
return fmt.Errorf("entity %v does not exist", entityId)
8281
}
82+
entityRecord := world.entities[entityId]
8383

8484
return addComponents2(world, entityRecord, a, b)
8585
}
@@ -112,10 +112,10 @@ func addComponents2[A, B ComponentInterface](world *World, entityRecord entityRe
112112
//
113113
// This solution is faster than an atomic solution.
114114
func AddComponents3[A, B, C ComponentInterface](world *World, entityId EntityId, a A, b B, c C) error {
115-
entityRecord, ok := world.entities[entityId]
116-
if !ok {
115+
if int(entityId) >= len(world.entities) {
117116
return fmt.Errorf("entity %v does not exist", entityId)
118117
}
118+
entityRecord := world.entities[entityId]
119119

120120
return addComponents3(world, entityRecord, a, b, c)
121121
}
@@ -150,10 +150,10 @@ func addComponents3[A, B, C ComponentInterface](world *World, entityRecord entit
150150
//
151151
// This solution is faster than an atomic solution.
152152
func AddComponents4[A, B, C, D ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D) error {
153-
entityRecord, ok := world.entities[entityId]
154-
if !ok {
153+
if int(entityId) >= len(world.entities) {
155154
return fmt.Errorf("entity %v does not exist", entityId)
156155
}
156+
entityRecord := world.entities[entityId]
157157

158158
return addComponents4(world, entityRecord, a, b, c, d)
159159
}
@@ -189,10 +189,10 @@ func addComponents4[A, B, C, D ComponentInterface](world *World, entityRecord en
189189
//
190190
// This solution is faster than an atomic solution.
191191
func AddComponents5[A, B, C, D, E ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D, e E) error {
192-
entityRecord, ok := world.entities[entityId]
193-
if !ok {
192+
if int(entityId) >= len(world.entities) {
194193
return fmt.Errorf("entity %v does not exist", entityId)
195194
}
195+
entityRecord := world.entities[entityId]
196196

197197
return addComponents5(world, entityRecord, a, b, c, d, e)
198198
}
@@ -229,10 +229,10 @@ func addComponents5[A, B, C, D, E ComponentInterface](world *World, entityRecord
229229
//
230230
// This solution is faster than an atomic solution.
231231
func AddComponents6[A, B, C, D, E, F ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D, e E, f F) error {
232-
entityRecord, ok := world.entities[entityId]
233-
if !ok {
232+
if int(entityId) >= len(world.entities) {
234233
return fmt.Errorf("entity %v does not exist", entityId)
235234
}
235+
entityRecord := world.entities[entityId]
236236

237237
return addComponents6(world, entityRecord, a, b, c, d, e, f)
238238
}
@@ -270,10 +270,10 @@ func addComponents6[A, B, C, D, E, F ComponentInterface](world *World, entityRec
270270
//
271271
// This solution is faster than an atomic solution.
272272
func AddComponents7[A, B, C, D, E, F, G ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D, e E, f F, g G) error {
273-
entityRecord, ok := world.entities[entityId]
274-
if !ok {
273+
if int(entityId) >= len(world.entities) {
275274
return fmt.Errorf("entity %v does not exist", entityId)
276275
}
276+
entityRecord := world.entities[entityId]
277277

278278
return addComponents7(world, entityRecord, a, b, c, d, e, f, g)
279279
}
@@ -312,10 +312,10 @@ func addComponents7[A, B, C, D, E, F, G ComponentInterface](world *World, entity
312312
//
313313
// This solution is faster than an atomic solution.
314314
func AddComponents8[A, B, C, D, E, F, G, H ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D, e E, f F, g G, h H) error {
315-
entityRecord, ok := world.entities[entityId]
316-
if !ok {
315+
if int(entityId) >= len(world.entities) {
317316
return fmt.Errorf("entity %v does not exist", entityId)
318317
}
318+
entityRecord := world.entities[entityId]
319319

320320
return addComponents8(world, entityRecord, a, b, c, d, e, f, g, h)
321321
}
@@ -354,10 +354,10 @@ func addComponents8[A, B, C, D, E, F, G, H ComponentInterface](world *World, ent
354354
// - the componentId is not registered in the World
355355
// - an internal error occurs
356356
func (world *World) AddComponent(entityId EntityId, componentId ComponentId, conf any) error {
357-
entityRecord, ok := world.entities[entityId]
358-
if !ok {
357+
if int(entityId) >= len(world.entities) {
359358
return fmt.Errorf("entity %v does not exist", entityId)
360359
}
360+
entityRecord := world.entities[entityId]
361361

362362
if world.hasComponents(entityRecord, componentId) {
363363
return fmt.Errorf("the entity %d already owns the component %d", entityId, componentId)
@@ -385,10 +385,10 @@ func (world *World) AddComponent(entityId EntityId, componentId ComponentId, con
385385
// - the componentsIds are not registered in the World
386386
// - an internal error occurs
387387
func (world *World) AddComponents(entityId EntityId, componentsIdsConfs ...ComponentIdConf) error {
388-
entityRecord, ok := world.entities[entityId]
389-
if !ok {
388+
if int(entityId) >= len(world.entities) {
390389
return fmt.Errorf("entity %v does not exist", entityId)
391390
}
391+
entityRecord := world.entities[entityId]
392392

393393
var componentsIds []ComponentId
394394
for _, componentIdConf := range componentsIdsConfs {
@@ -423,10 +423,10 @@ func RemoveComponent[T ComponentInterface](world *World, entityId EntityId) erro
423423
var t T
424424
componentId := t.GetComponentId()
425425

426-
entityRecord, ok := world.entities[entityId]
427-
if !ok {
426+
if int(entityId) >= len(world.entities) {
428427
return fmt.Errorf("entity %v does not exist", entityId)
429428
}
429+
entityRecord := world.entities[entityId]
430430

431431
if !world.hasComponents(entityRecord, componentId) {
432432
return fmt.Errorf("the entity %d doesn't own the component %d", entityId, componentId)
@@ -486,10 +486,10 @@ func removeComponent(world *World, s storage, entityRecord entityRecord, compone
486486
//
487487
// It returns false if at least one ComponentId is not owned.
488488
func (world *World) HasComponents(entityId EntityId, componentsIds ...ComponentId) bool {
489-
entityRecord, ok := world.entities[entityId]
490-
if !ok {
489+
if int(entityId) >= len(world.entities) {
491490
return false
492491
}
492+
entityRecord := world.entities[entityId]
493493

494494
return world.hasComponents(entityRecord, componentsIds...)
495495
}

0 commit comments

Comments
 (0)