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
112 changes: 80 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ 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 randomly generated, its type uint64 avoiding to generate twice the same id.
It is also required to set a name for each entity, only used to easily retrieve them when required.
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.

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 Expand Up @@ -82,14 +82,14 @@ volt.RegisterComponent[transformComponent](world, &ComponentConfig[transformComp
```
- Create the entity
```go
entityId := world.CreateEntity("entityName")
entityId := world.CreateEntity()
```
**Important**: the entity name MUST be unique.
**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.

- Add the component to the entity
```go
component := volt.ConfigureComponent[transformComponent](&scene.World, transformConfiguration{x: 1.0, y: 2.0, z: 3.0})
volt.AddComponent(&scene.World, entity, component)
component := volt.ConfigureComponent[transformComponent](&world, transformConfiguration{x: 1.0, y: 2.0, z: 3.0})
volt.AddComponent(&world, entity, component)
```
- Remove the component to the entity
```go
Expand All @@ -102,15 +102,6 @@ if err != nil {
```go
world.RemoveEntity(entityId)
```
## Searching for an entity
- Knowing an entity by its name, you can get its identifier:
```go
entityId := world.SearchEntity("entityName")
```
- The reversed search is also possible, fetching its name by its idenfier:
```go
entityName := world.GetEntityName(entityId)
```

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

## Events
The lifecycle (creation/deletion) of entities and components can trigger events.
You can configure a callback function for each of these events, to execute your custom code:
```go
world := volt.CreateWorld(100)
world.SetEntityAddedFn(func(entityId volt.EntityId) {
fmt.Println("A new entity has been created", entityId)
})
world.SetEntityRemovedFn(func(entityId volt.EntityId) {
fmt.Println("An entity has been deleted", entityId)
})
world.SetComponentAddedFn(func(entityId volt.EntityId, componentId volt.ComponentId) {
fmt.Println("The component", componentId, "is attached to the entity", entityId)
})
world.SetComponentRemovedFn(func(entityId volt.EntityId, componentId volt.ComponentId) {
fmt.Println("The component", componentId, "is removed from the entity", entityId)
})
```

## Naming entities
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+.
You now have to keep track of the names by yourself in your application:
- 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
of entities, it will probably have a huge impact on the garbage collector.
- 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:
```go
const MetadataComponentId = 0

type MetadataComponent struct {
Name string
}

func (MetadataComponent MetadataComponent) GetComponentId() volt.ComponentId {
return MetadataComponentId
}
volt.RegisterComponent[MetadataComponent](&world, &volt.ComponentConfig[MetadataComponent]{BuilderFn: func(component any, configuration any) {}})

func GetEntityName(world *volt.World, entityId volt.EntityId) string {
if world.HasComponents(entityId, MetadataComponentId) {
metadata := volt.GetComponent[MetadataComponent](world, entityId)

return metadata.Name
}

return ""
}

func (scene *Scene) SearchEntity(name string) volt.EntityId {
q := volt.CreateQuery1[MetadataComponent](&world, volt.QueryConfiguration{})
for result := range q.Foreach(nil) {
if result.A.Name == name {
return result.EntityId
}
}

return 0
}
```

## Benchmark
Few ECS tools exist for Go. Arche and unitoftime/ecs are probably the most looked at, and the most optimized.
In the benchmark folder, this module is compared to both of them.

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

Expand All @@ -207,19 +257,19 @@ 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 |
| BenchmarkCreateEntityArche-16 | 171 | 7138387 | 11096954 | 61 |
| BenchmarkIterateArche-16 | 2798 | 429744 | 354 | 4 |
| BenchmarkAddArche-16 | 253 | 4673362 | 122153 | 100000 |
| BenchmarkRemoveArche-16 | 247 | 4840772 | 100000 | 100000 |
| BenchmarkCreateEntityUECS-16 | 27 | 38852089 | 49119503 | 200146 |
| BenchmarkIterateUECS-16 | 4892 | 235333 | 128 | 3 |
| BenchmarkAddUECS-16 | 28 | 38982533 | 4721942 | 100005 |
| BenchmarkRemoveUECS-16 | 30 | 40290316 | 3336712 | 100000 |
| BenchmarkCreateEntityVolt-16 | 63 | 18836136 | 35181458 | 100101 |
| BenchmarkIterateVolt-16 | 3619 | 337764 | 256 | 8 |
| BenchmarkIterateConcurrentlyVolt-16 | 9164 | 121653 | 3324 | 91 |
| BenchmarkAddVolt-16 | 103 | 11379690 | 4313182 | 300000 |
| BenchmarkRemoveVolt-16 | 146 | 7647252 | 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 All @@ -236,8 +286,6 @@ The creator and maintainer of Arche has published more complex benchmarks availa
https://github.com/mlange-42/go-ecs-benchmarks

## What is to come next ?
- 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.
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.
- 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
using multiples threads/goroutines. I need to figure out how to implement this, though I never met the need for this feature myself.

Expand Down
20 changes: 10 additions & 10 deletions benchmark/volt_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package benchmark

import (
"github.com/akmonengine/volt"
"math/rand/v2"
"strconv"
"testing"

"github.com/akmonengine/volt"
)

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

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

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

for i := 0; i < ENTITIES_COUNT; i++ {
id := world.CreateEntity(strconv.Itoa(i))
id := world.CreateEntity()
volt.AddComponent[testTransform](world, id, testTransform{})
volt.AddComponent[testTag](world, id, testTag{})
}
Expand Down Expand Up @@ -84,8 +84,8 @@ func BenchmarkAddVolt(b *testing.B) {
volt.RegisterComponent[testTag](world, &volt.ComponentConfig[testTag]{})

entities := make([]volt.EntityId, 0, ENTITIES_COUNT)
for j := range ENTITIES_COUNT {
entityId := world.CreateEntity(strconv.Itoa(j))
for range ENTITIES_COUNT {
entityId := world.CreateEntity()
volt.AddComponent(world, entityId, testTag{})
entities = append(entities, entityId)
}
Expand Down Expand Up @@ -113,8 +113,8 @@ func BenchmarkRemoveVolt(b *testing.B) {
volt.RegisterComponent[testTag](world, &volt.ComponentConfig[testTag]{})

entities := make([]volt.EntityId, 0, ENTITIES_COUNT)
for j := range ENTITIES_COUNT {
entityId := world.CreateEntity(strconv.Itoa(j))
for range ENTITIES_COUNT {
entityId := world.CreateEntity()
volt.AddComponent(world, entityId, testTag{})
entities = append(entities, entityId)
}
Expand Down
48 changes: 24 additions & 24 deletions component.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ func ConfigureComponent[T ComponentInterface](world *World, conf any) T {
// - the entity has the component
// - an internal error occurs
func AddComponent[T ComponentInterface](world *World, entityId EntityId, component T) error {
entityRecord, ok := world.entities[entityId]
if !ok {
if int(entityId) >= len(world.entities) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]

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

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

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

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

return addComponents5(world, entityRecord, a, b, c, d, e)
}
Expand Down Expand Up @@ -229,10 +229,10 @@ func addComponents5[A, B, C, D, E ComponentInterface](world *World, entityRecord
//
// This solution is faster than an atomic solution.
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 {
entityRecord, ok := world.entities[entityId]
if !ok {
if int(entityId) >= len(world.entities) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]

return addComponents6(world, entityRecord, a, b, c, d, e, f)
}
Expand Down Expand Up @@ -270,10 +270,10 @@ func addComponents6[A, B, C, D, E, F ComponentInterface](world *World, entityRec
//
// This solution is faster than an atomic solution.
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 {
entityRecord, ok := world.entities[entityId]
if !ok {
if int(entityId) >= len(world.entities) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]

return addComponents7(world, entityRecord, a, b, c, d, e, f, g)
}
Expand Down Expand Up @@ -312,10 +312,10 @@ func addComponents7[A, B, C, D, E, F, G ComponentInterface](world *World, entity
//
// This solution is faster than an atomic solution.
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 {
entityRecord, ok := world.entities[entityId]
if !ok {
if int(entityId) >= len(world.entities) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]

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

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

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

entityRecord, ok := world.entities[entityId]
if !ok {
if int(entityId) >= len(world.entities) {
return fmt.Errorf("entity %v does not exist", entityId)
}
entityRecord := world.entities[entityId]

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

return world.hasComponents(entityRecord, componentsIds...)
}
Expand Down
Loading
Loading