From 5892d1493bcf865381c88e70c67e09f1c31f1383 Mon Sep 17 00:00:00 2001 From: mlange-42 Date: Thu, 9 Feb 2023 11:35:38 +0100 Subject: [PATCH 1/3] implement archetype graph --- ecs/archetype.go | 20 ++++++++++++++++++++ ecs/world.go | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/ecs/archetype.go b/ecs/archetype.go index c83395e1..64d0fba6 100644 --- a/ecs/archetype.go +++ b/ecs/archetype.go @@ -14,6 +14,8 @@ type archetype struct { indices [MaskTotalBits]uint8 entities storage components []storage + toAdd [MaskTotalBits]*archetype + toRemove [MaskTotalBits]*archetype } var entityType = reflect.TypeOf(Entity{}) @@ -110,3 +112,21 @@ func (a *archetype) Len() uint32 { func (a *archetype) Set(index uint32, id ID, comp interface{}) unsafe.Pointer { return a.components[a.indices[id]].set(index, comp) } + +func (a *archetype) GetTransitionAdd(id ID) (*archetype, bool) { + p := a.toAdd[id] + return p, p != nil +} + +func (a *archetype) GetTransitionRemove(id ID) (*archetype, bool) { + p := a.toRemove[id] + return p, p != nil +} + +func (a *archetype) SetTransitionAdd(id ID, to *archetype) { + a.toAdd[id] = to +} + +func (a *archetype) SetTransitionRemove(id ID, to *archetype) { + a.toRemove[id] = to +} diff --git a/ecs/world.go b/ecs/world.go index b9ddc3aa..335e0e46 100644 --- a/ecs/world.go +++ b/ecs/world.go @@ -201,7 +201,7 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) { } } - arch := w.findOrCreateArchetype(mask) + arch := w.findOrCreateArchetype(oldArch, addIDs, rem) allComps := make([]componentPointer, 0, len(keepIDs)+len(addIDs)) for _, id := range keepIDs { @@ -231,11 +231,39 @@ func (w *World) copyTo(entity Entity, id ID, comp interface{}) unsafe.Pointer { return arch.Set(index.index, id, comp) } -func (w *World) findOrCreateArchetype(mask bitMask) *archetype { +func (w *World) findOrCreateArchetype(start *archetype, add []ID, rem []ID) *archetype { + curr := start + mask := start.mask + for _, id := range rem { + mask.Set(id, false) + if next, ok := curr.GetTransitionRemove(id); ok { + curr = next + } else { + next, _ := w.findOrCreateArchetypeSlow(mask) + next.SetTransitionAdd(id, curr) + curr.SetTransitionRemove(id, next) + curr = next + } + } + for _, id := range add { + mask.Set(id, true) + if next, ok := curr.GetTransitionAdd(id); ok { + curr = next + } else { + next, _ := w.findOrCreateArchetypeSlow(mask) + next.SetTransitionRemove(id, curr) + curr.SetTransitionAdd(id, next) + curr = next + } + } + return curr +} + +func (w *World) findOrCreateArchetypeSlow(mask bitMask) (*archetype, bool) { if arch, ok := w.findArchetype(mask); ok { - return arch + return arch, false } - return w.createArchetype(mask) + return w.createArchetype(mask), true } func (w *World) findArchetype(mask bitMask) (*archetype, bool) { From 91a49af8ce0c775cb6e748680f942529507c7e1e Mon Sep 17 00:00:00 2001 From: mlange-42 Date: Thu, 9 Feb 2023 11:41:04 +0100 Subject: [PATCH 2/3] add tests for archetype graph --- ecs/world_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ecs/world_test.go b/ecs/world_test.go index 7d052e2d..0fe6b6fb 100644 --- a/ecs/world_test.go +++ b/ecs/world_test.go @@ -281,6 +281,28 @@ func TestRegisterComponents(t *testing.T) { assert.Equal(t, ID(1), ComponentID[rotation](&world)) } +func TestArchetypeGraph(t *testing.T) { + world := NewWorld() + + posID := ComponentID[position](&world) + velID := ComponentID[velocity](&world) + rotID := ComponentID[rotation](&world) + + archEmpty := world.archetypes.Get(0) + arch0 := world.findOrCreateArchetype(archEmpty, []ID{posID}, []ID{}) + archEmpty2 := world.findOrCreateArchetype(arch0, []ID{}, []ID{posID}) + + assert.Equal(t, archEmpty, archEmpty2) + + arch01 := world.findOrCreateArchetype(arch0, []ID{velID}, []ID{}) + arch012 := world.findOrCreateArchetype(arch01, []ID{rotID}, []ID{}) + + assert.Equal(t, []ID{0, 1, 2}, arch012.ids) + + archEmpty3 := world.findOrCreateArchetype(arch012, []ID{}, []ID{posID, rotID, velID}) + assert.Equal(t, archEmpty, archEmpty3) +} + func TestTypeSizes(t *testing.T) { printTypeSize[World]() printTypeSizeName[pagedArr32[archetype]]("PagedArr32") From 90d6806102a026adfd868ecb1853e6c75771cf62 Mon Sep 17 00:00:00 2001 From: mlange-42 Date: Thu, 9 Feb 2023 11:46:52 +0100 Subject: [PATCH 3/3] use maps instead of fixed-size arrays for archetype graph --- CHANGELOG.md | 4 +++- ecs/archetype.go | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a5cdf8..388b8dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## [[unpublished]](https://github.com/mlange-42/arche/compare/v0.2.0...main) -Nothing +### Other + +* Use of an archetype graph to speed up finding the target archetype for component addition/removal (#42) ## [[v0.2.0]](https://github.com/mlange-42/arche/compare/v0.1.4...v0.2.0) diff --git a/ecs/archetype.go b/ecs/archetype.go index 64d0fba6..2556de4d 100644 --- a/ecs/archetype.go +++ b/ecs/archetype.go @@ -14,8 +14,8 @@ type archetype struct { indices [MaskTotalBits]uint8 entities storage components []storage - toAdd [MaskTotalBits]*archetype - toRemove [MaskTotalBits]*archetype + toAdd map[ID]*archetype + toRemove map[ID]*archetype } var entityType = reflect.TypeOf(Entity{}) @@ -42,6 +42,8 @@ func (a *archetype) init(capacityIncrement int, components ...componentType) { a.mask = mask a.components = comps a.entities = storage{} + a.toAdd = map[ID]*archetype{} + a.toRemove = map[ID]*archetype{} a.entities.init(entityType, capacityIncrement) } @@ -114,13 +116,13 @@ func (a *archetype) Set(index uint32, id ID, comp interface{}) unsafe.Pointer { } func (a *archetype) GetTransitionAdd(id ID) (*archetype, bool) { - p := a.toAdd[id] - return p, p != nil + p, ok := a.toAdd[id] + return p, ok } func (a *archetype) GetTransitionRemove(id ID) (*archetype, bool) { - p := a.toRemove[id] - return p, p != nil + p, ok := a.toRemove[id] + return p, ok } func (a *archetype) SetTransitionAdd(id ID, to *archetype) {