From a7cde98a41385f43a5a4c6655320415bdce0658b Mon Sep 17 00:00:00 2001 From: Martin Lange <44003176+mlange-42@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:00:19 +0100 Subject: [PATCH] Fix component subscription logic (#337) * use saparate masks for component addition and removal -- was not taken into account correctly * fix component subscription logic -- some relation subscriptions could get lost * tweak docs of listener implementations # Commits * tweak docs of listener implementations * fix and tweak conditions in determining listener subscription * use only triggered event type bits for component subscription check * move check for event type subscription into utility function * use separate added and removed masks in events and subscription logic * check for trigger before calling subscription logic --- CHANGELOG.md | 2 +- ecs/event.go | 4 +- ecs/event_test.go | 14 +++--- ecs/util.go | 35 ++++++++++++-- ecs/util_test.go | 43 +++++++++++++---- ecs/world.go | 75 ++++++++++++++++-------------- ecs/world_examples_test.go | 2 +- ecs/world_test.go | 94 +++++++++++++++++++------------------- examples/events/main.go | 2 +- listener/callback.go | 2 +- listener/dispatch.go | 17 ++----- listener/util.go | 39 ++++++++++++++++ listener/util_test.go | 79 ++++++++++++++++++++++++++++++++ 13 files changed, 288 insertions(+), 120 deletions(-) create mode 100644 listener/util.go create mode 100644 listener/util_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c817aa..f7e0011e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ This change was necessary to get the same performance as before, despite the mor ### Performance * Reduces archetype memory footprint by using a dynamically sized slice for storage lookup (#327) -* Reduces event listener overhead through granular subscriptions and elimination of a heap allocation (#333, #334, #335) +* Reduces event listener overhead through granular subscriptions and elimination of a heap allocation (#333, #334, #335, #337) ### Other diff --git a/ecs/event.go b/ecs/event.go index efae6fb6..8042116a 100644 --- a/ecs/event.go +++ b/ecs/event.go @@ -25,8 +25,8 @@ import "github.com/mlange-42/arche/ecs/event" // This allows the [World] to be in an unlocked state, and notifies after potential entity initialization. type EntityEvent struct { Entity Entity // The entity that was changed. - Changed Mask // Mask indicating changed components (additions and removals). - Added, Removed []ID // Components added and removed. DO NOT MODIFY! Get the current components with [World.Ids]. + Added, Removed Mask // Masks indicating changed components (additions and removals). + AddedIDs, RemovedIDs []ID // Components added and removed. DO NOT MODIFY! Get the current components with [World.Ids]. OldRelation, NewRelation *ID // Old and new relation component ID. No relation is indicated by nil. OldTarget Entity // Old relation target entity. Get the new target with [World.Relations] and [Relations.Get]. EventTypes event.Subscription // Bit mask of event types. See [event.Subscription]. diff --git a/ecs/event_test.go b/ecs/event_test.go index 3acd6e5c..dac41370 100644 --- a/ecs/event_test.go +++ b/ecs/event_test.go @@ -50,7 +50,7 @@ func BenchmarkEntityEventCreate(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - event = ecs.EntityEvent{Entity: e, Changed: mask, Added: added, Removed: nil} + event = ecs.EntityEvent{Entity: e, Added: mask, Removed: mask, AddedIDs: added, RemovedIDs: nil} } b.StopTimer() _ = event @@ -68,7 +68,7 @@ func BenchmarkEntityEventHeapPointer(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - event = &ecs.EntityEvent{Entity: e, Changed: mask, Added: added, Removed: nil} + event = &ecs.EntityEvent{Entity: e, Added: mask, Removed: mask, AddedIDs: added, RemovedIDs: nil} } b.StopTimer() _ = event @@ -78,13 +78,13 @@ func BenchmarkEntityEventCopy(b *testing.B) { handler := eventHandler{} for i := 0; i < b.N; i++ { - handler.ListenCopy(ecs.EntityEvent{Entity: ecs.Entity{}, Changed: ecs.Mask{}, Added: nil, Removed: nil}) + handler.ListenCopy(ecs.EntityEvent{Entity: ecs.Entity{}, Added: ecs.Mask{}, Removed: ecs.Mask{}, AddedIDs: nil, RemovedIDs: nil}) } } func BenchmarkEntityEventCopyReuse(b *testing.B) { handler := eventHandler{} - event := ecs.EntityEvent{Entity: ecs.Entity{}, Changed: ecs.Mask{}, Added: nil, Removed: nil} + event := ecs.EntityEvent{Entity: ecs.Entity{}, Added: ecs.Mask{}, Removed: ecs.Mask{}, AddedIDs: nil, RemovedIDs: nil} for i := 0; i < b.N; i++ { handler.ListenCopy(event) @@ -95,13 +95,13 @@ func BenchmarkEntityEventPointer(b *testing.B) { handler := eventHandler{} for i := 0; i < b.N; i++ { - handler.ListenPointer(&ecs.EntityEvent{Entity: ecs.Entity{}, Changed: ecs.Mask{}, Added: nil, Removed: nil}) + handler.ListenPointer(&ecs.EntityEvent{Entity: ecs.Entity{}, Added: ecs.Mask{}, Removed: ecs.Mask{}, AddedIDs: nil, RemovedIDs: nil}) } } func BenchmarkEntityEventPointerReuse(b *testing.B) { handler := eventHandler{} - event := ecs.EntityEvent{Entity: ecs.Entity{}, Changed: ecs.Mask{}, Added: nil, Removed: nil} + event := ecs.EntityEvent{Entity: ecs.Entity{}, Added: ecs.Mask{}, Removed: ecs.Mask{}, AddedIDs: nil, RemovedIDs: nil} for i := 0; i < b.N; i++ { handler.ListenPointer(&event) @@ -117,7 +117,7 @@ func ExampleEntityEvent() { world.SetListener(&listener) world.NewEntity() - // Output: {{1 0} {[0 0 0 0]} [] [] {0 0} 1} + // Output: {{1 0} {[0 0 0 0]} {[0 0 0 0]} [] [] {0 0} 1} } func ExampleEntityEvent_Contains() { diff --git a/ecs/util.go b/ecs/util.go index 2f74d519..74652a0b 100644 --- a/ecs/util.go +++ b/ecs/util.go @@ -65,12 +65,37 @@ func subscription(entityCreated, entityRemoved, componentAdded, componentRemoved return bits } -// Returns whether a listener that subscribes to an event is also interested in terms of component subscription. -func subscribes(evtType event.Subscription, changed *Mask, subs *Mask, oldRel *ID, newRel *ID) bool { - if event.Relations.Contains(evtType) { - return subs == nil || (oldRel != nil && subs.Get(*oldRel)) || (newRel != nil && subs.Get(*newRel)) +// Returns whether a listener is interested in an event based on event type and component subscriptions. +// +// Argument trigger should only contain the subscription bits that triggered the event. +// I.e. subscriptions & evenTypes. +func subscribes(trigger event.Subscription, added *Mask, removed *Mask, subs *Mask, oldRel *ID, newRel *ID) bool { + if trigger == 0 { + return false + } + if subs == nil { + // No component subscriptions + return true + } + if trigger.ContainsAny(event.Relations) { + // Contains event.RelationChanged and/or event.TargetChanged + if (oldRel != nil && subs.Get(*oldRel)) || (newRel != nil && subs.Get(*newRel)) { + return true + } + } + if trigger.ContainsAny(event.EntityCreated | event.ComponentAdded) { + // Contains additions-like types + if added != nil && subs.ContainsAny(added) { + return true + } + } + if trigger.ContainsAny(event.EntityRemoved | event.ComponentRemoved) { + // Contains additions-like types + if removed != nil && subs.ContainsAny(removed) { + return true + } } - return subs == nil || subs.ContainsAny(changed) + return false } func maskToTypes(mask Mask, reg *componentRegistry) []componentType { diff --git a/ecs/util_test.go b/ecs/util_test.go index 29dec9c4..8e5e802b 100644 --- a/ecs/util_test.go +++ b/ecs/util_test.go @@ -70,31 +70,58 @@ func TestSubscribes(t *testing.T) { id2 := id(2) id3 := id(3) + assert.False(t, + subscribes(0, all(id1), all(id2), all(id1, id2), nil, nil), + ) + assert.True(t, - subscribes(event.ComponentAdded, all(id1), all(id1, id2), nil, nil), + subscribes(event.ComponentAdded, all(id1), nil, all(id1, id2), nil, nil), + ) + assert.False(t, + subscribes(event.ComponentAdded, nil, all(id1), all(id1, id2), nil, nil), ) assert.True(t, - subscribes(event.ComponentAdded, all(id1, id2), all(id2), nil, nil), + subscribes(event.ComponentAdded, all(id1, id2), nil, all(id2), nil, nil), ) assert.False(t, - subscribes(event.ComponentAdded, all(id1, id2), all(id3), nil, nil), + subscribes(event.ComponentAdded, all(id1, id2), nil, all(id3), nil, nil), ) assert.True(t, - subscribes(event.RelationChanged, &Mask{}, all(id1, id2), nil, &id1), + subscribes(event.ComponentRemoved, nil, all(id1), all(id1, id2), nil, nil), ) + assert.False(t, + subscribes(event.ComponentRemoved, all(id1), nil, all(id1, id2), nil, nil), + ) + assert.True(t, + subscribes(event.ComponentRemoved, nil, all(id1, id2), all(id2), nil, nil), + ) + assert.False(t, + subscribes(event.ComponentRemoved, nil, all(id1, id2), all(id3), nil, nil), + ) + + assert.True(t, + subscribes(event.RelationChanged, &Mask{}, &Mask{}, all(id1, id2), nil, &id1), + ) + assert.True(t, + subscribes(event.RelationChanged, &Mask{}, &Mask{}, all(id1, id2), &id1, &id3), + ) + assert.False(t, + subscribes(event.RelationChanged, &Mask{}, &Mask{}, all(id1), &id2, &id3), + ) + assert.True(t, - subscribes(event.RelationChanged, &Mask{}, all(id1, id2), &id1, &id3), + subscribes(event.TargetChanged, &Mask{}, &Mask{}, all(id1, id2), &id1, &id1), ) assert.False(t, - subscribes(event.RelationChanged, &Mask{}, all(id1), &id2, &id3), + subscribes(event.TargetChanged, &Mask{}, &Mask{}, all(id1, id2), &id3, &id3), ) assert.True(t, - subscribes(event.TargetChanged, &Mask{}, all(id1, id2), &id1, &id1), + subscribes(event.ComponentAdded|event.ComponentRemoved|event.TargetChanged, all(id1, id2), all(id1, id2), all(id3), &id3, &id3), ) assert.False(t, - subscribes(event.TargetChanged, &Mask{}, all(id1, id2), &id3, &id3), + subscribes(event.ComponentAdded|event.ComponentRemoved|event.TargetChanged, all(id1), all(id1), all(id3), &id2, &id2), ) } diff --git a/ecs/world.go b/ecs/world.go index b924ac04..e14b5aea 100644 --- a/ecs/world.go +++ b/ecs/world.go @@ -214,9 +214,9 @@ func (w *World) NewEntity(comps ...ID) Entity { newRel = &arch.RelationComponent } bits := subscription(true, false, len(comps) > 0, false, newRel != nil, false) - if w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &arch.Mask, w.listener.Components(), nil, newRel) { - w.listener.Notify(w, EntityEvent{entity, arch.Mask, comps, nil, nil, newRel, Entity{}, bits}) + trigger := w.listener.Subscriptions() & bits + if trigger != 0 && subscribes(trigger, &arch.Mask, nil, w.listener.Components(), nil, newRel) { + w.listener.Notify(w, EntityEvent{entity, arch.Mask, Mask{}, comps, nil, nil, newRel, Entity{}, bits}) } } return entity @@ -263,9 +263,9 @@ func (w *World) NewEntityWith(comps ...Component) Entity { newRel = &arch.RelationComponent } bits := subscription(true, false, len(comps) > 0, false, newRel != nil, false) - if w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &arch.Mask, w.listener.Components(), nil, newRel) { - w.listener.Notify(w, EntityEvent{entity, arch.Mask, ids, nil, nil, newRel, Entity{}, bits}) + trigger := w.listener.Subscriptions() & bits + if trigger != 0 && subscribes(trigger, &arch.Mask, nil, w.listener.Components(), nil, newRel) { + w.listener.Notify(w, EntityEvent{entity, arch.Mask, Mask{}, ids, nil, nil, newRel, Entity{}, bits}) } } return entity @@ -294,9 +294,9 @@ func (w *World) newEntityTarget(targetID ID, target Entity, comps ...ID) Entity if w.listener != nil { bits := subscription(true, false, len(comps) > 0, false, true, !target.IsZero()) - if w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &arch.Mask, w.listener.Components(), nil, &targetID) { - w.listener.Notify(w, EntityEvent{entity, arch.Mask, comps, nil, nil, &targetID, Entity{}, bits}) + trigger := w.listener.Subscriptions() & bits + if trigger != 0 && subscribes(trigger, &arch.Mask, nil, w.listener.Components(), nil, &targetID) { + w.listener.Notify(w, EntityEvent{entity, arch.Mask, Mask{}, comps, nil, nil, &targetID, Entity{}, bits}) } } return entity @@ -331,9 +331,9 @@ func (w *World) newEntityTargetWith(targetID ID, target Entity, comps ...Compone if w.listener != nil { bits := subscription(true, false, len(comps) > 0, false, true, !target.IsZero()) - if w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &arch.Mask, w.listener.Components(), nil, &targetID) { - w.listener.Notify(w, EntityEvent{entity, arch.Mask, ids, nil, nil, &targetID, Entity{}, bits}) + trigger := w.listener.Subscriptions() & bits + if trigger != 0 && subscribes(trigger, &arch.Mask, nil, w.listener.Components(), nil, &targetID) { + w.listener.Notify(w, EntityEvent{entity, arch.Mask, Mask{}, ids, nil, nil, &targetID, Entity{}, bits}) } } return entity @@ -350,14 +350,14 @@ func (w *World) newEntities(count int, targetID ID, hasTarget bool, target Entit newRel = &arch.RelationComponent } bits := subscription(true, false, len(comps) > 0, false, newRel != nil, !target.IsZero()) - if w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &arch.Mask, w.listener.Components(), nil, newRel) { + trigger := w.listener.Subscriptions() & bits + if trigger != 0 && subscribes(trigger, &arch.Mask, nil, w.listener.Components(), nil, newRel) { cnt := uint32(count) var i uint32 for i = 0; i < cnt; i++ { idx := startIdx + i entity := arch.GetEntity(idx) - w.listener.Notify(w, EntityEvent{entity, arch.Mask, comps, nil, nil, newRel, Entity{}, bits}) + w.listener.Notify(w, EntityEvent{entity, arch.Mask, Mask{}, comps, nil, nil, newRel, Entity{}, bits}) } } } @@ -395,14 +395,14 @@ func (w *World) newEntitiesWith(count int, targetID ID, hasTarget bool, target E newRel = &arch.RelationComponent } bits := subscription(true, false, len(comps) > 0, false, newRel != nil, !target.IsZero()) - if w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &arch.Mask, w.listener.Components(), nil, newRel) { + trigger := w.listener.Subscriptions() & bits + if trigger != 0 && subscribes(trigger, &arch.Mask, nil, w.listener.Components(), nil, newRel) { var i uint32 cnt := uint32(count) for i = 0; i < cnt; i++ { idx := startIdx + i entity := arch.GetEntity(idx) - w.listener.Notify(w, EntityEvent{entity, arch.Mask, ids, nil, nil, newRel, Entity{}, bits}) + w.listener.Notify(w, EntityEvent{entity, arch.Mask, Mask{}, ids, nil, nil, newRel, Entity{}, bits}) } } } @@ -453,10 +453,10 @@ func (w *World) RemoveEntity(entity Entity) { } bits := subscription(false, true, false, len(oldIds) > 0, oldRel != nil, !oldArch.RelationTarget.IsZero()) - if w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &oldArch.Mask, w.listener.Components(), oldRel, nil) { + trigger := w.listener.Subscriptions() & bits + if trigger != 0 && subscribes(trigger, nil, &oldArch.Mask, w.listener.Components(), oldRel, nil) { lock := w.lock() - w.listener.Notify(w, EntityEvent{entity, oldArch.Mask, nil, oldIds, oldRel, nil, oldArch.RelationTarget, bits}) + w.listener.Notify(w, EntityEvent{entity, Mask{}, oldArch.Mask, nil, oldIds, oldRel, nil, oldArch.RelationTarget, bits}) w.unlock(lock) } } @@ -517,15 +517,15 @@ func (w *World) removeEntities(filter Filter) int { oldIds = arch.node.Ids } bits = subscription(false, true, false, len(oldIds) > 0, oldRel != nil, !arch.RelationTarget.IsZero()) - listen = w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &arch.Mask, w.listener.Components(), oldRel, nil) + trigger := w.listener.Subscriptions() & bits + listen = trigger != 0 && subscribes(trigger, nil, &arch.Mask, w.listener.Components(), oldRel, nil) } var j uint32 for j = 0; j < ln; j++ { entity := arch.GetEntity(j) if listen { - w.listener.Notify(w, EntityEvent{entity, arch.Mask, nil, oldIds, oldRel, nil, Entity{}, bits}) + w.listener.Notify(w, EntityEvent{entity, Mask{}, arch.Mask, nil, oldIds, oldRel, nil, Entity{}, bits}) } index := &w.entities[entity.id] index.arch = nil @@ -778,10 +778,13 @@ func (w *World) exchange(entity Entity, add []ID, rem []ID, relation ID, hasRela targChanged := oldTarget != arch.RelationTarget bits := subscription(false, false, len(add) > 0, len(rem) > 0, relChanged, targChanged) - if w.listener.Subscriptions().ContainsAny(bits) { + trigger := w.listener.Subscriptions() & bits + if trigger != 0 { changed := oldMask.Xor(&arch.Mask) - if subscribes(bits, &changed, w.listener.Components(), oldRel, newRel) { - w.listener.Notify(w, EntityEvent{entity, changed, add, rem, oldRel, newRel, oldTarget, bits}) + added := arch.Mask.And(&changed) + removed := oldMask.And(&changed) + if subscribes(trigger, &added, &removed, w.listener.Components(), oldRel, newRel) { + w.listener.Notify(w, EntityEvent{entity, added, removed, add, rem, oldRel, newRel, oldTarget, bits}) } } } @@ -980,9 +983,11 @@ func (w *World) setRelation(entity Entity, comp ID, target Entity) { oldTarget := oldArch.RelationTarget w.cleanupArchetype(oldArch) - if w.listener != nil && w.listener.Subscriptions().Contains(event.TargetChanged) && - subscribes(event.TargetChanged, nil, w.listener.Components(), &comp, &comp) { - w.listener.Notify(w, EntityEvent{entity, Mask{}, nil, nil, &comp, &comp, oldTarget, event.TargetChanged}) + if w.listener != nil { + trigger := w.listener.Subscriptions() & event.TargetChanged + if trigger != 0 && subscribes(trigger, nil, nil, w.listener.Components(), &comp, &comp) { + w.listener.Notify(w, EntityEvent{entity, Mask{}, Mask{}, nil, nil, &comp, &comp, oldTarget, event.TargetChanged}) + } } } @@ -1691,7 +1696,7 @@ func (w *World) notifyQuery(batchArch *batchArchetypes) { } event := EntityEvent{ - Entity{}, arch.Mask, batchArch.Added, batchArch.Removed, + Entity{}, arch.Mask, Mask{}, batchArch.Added, batchArch.Removed, nil, newRel, Entity{}, 0, } @@ -1710,7 +1715,9 @@ func (w *World) notifyQuery(batchArch *batchArchetypes) { relChanged = (oldRel == nil) != (newRel == nil) || *oldRel != *newRel } targChanged = oldArch.RelationTarget != arch.RelationTarget - event.Changed = event.Changed.Xor(&oldArch.node.Mask) + changed := event.Added.Xor(&oldArch.node.Mask) + event.Added = changed.And(&event.Added) + event.Removed = changed.And(&oldArch.node.Mask) event.OldTarget = oldArch.RelationTarget event.OldRelation = oldRel } @@ -1718,8 +1725,8 @@ func (w *World) notifyQuery(batchArch *batchArchetypes) { bits := subscription(oldArch == nil, false, len(batchArch.Added) > 0, len(batchArch.Removed) > 0, relChanged, targChanged) event.EventTypes = bits - if w.listener.Subscriptions().ContainsAny(bits) && - subscribes(bits, &event.Changed, w.listener.Components(), event.OldRelation, event.NewRelation) { + trigger := w.listener.Subscriptions() & bits + if trigger != 0 && subscribes(trigger, &event.Added, &event.Removed, w.listener.Components(), event.OldRelation, event.NewRelation) { start, end := batchArch.StartIndex[i], batchArch.EndIndex[i] var e uint32 for e = start; e < end; e++ { diff --git a/ecs/world_examples_test.go b/ecs/world_examples_test.go index 93464d29..f89e2e40 100644 --- a/ecs/world_examples_test.go +++ b/ecs/world_examples_test.go @@ -284,7 +284,7 @@ func ExampleWorld_SetListener() { world.SetListener(&listener) world.NewEntity() - // Output: {{1 0} {[0 0 0 0]} [] [] {0 0} 1} + // Output: {{1 0} {[0 0 0 0]} {[0 0 0 0]} [] [] {0 0} 1} } func ExampleWorld_Stats() { diff --git a/ecs/world_test.go b/ecs/world_test.go index d7ffb4b3..f9a0da9e 100644 --- a/ecs/world_test.go +++ b/ecs/world_test.go @@ -350,8 +350,8 @@ func TestWorldExchangeBatch(t *testing.T) { assert.Equal(t, 202, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{202, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged | event.TargetChanged, }, events[201]) @@ -382,9 +382,10 @@ func TestWorldExchangeBatch(t *testing.T) { assert.Equal(t, 502, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{102, 0}, - Changed: All(posID, velID), - Added: []ID{posID}, - Removed: []ID{velID}, + Added: All(posID), + Removed: All(velID), + AddedIDs: []ID{posID}, + RemovedIDs: []ID{velID}, OldRelation: &relID, NewRelation: &relID, OldTarget: target1, @@ -404,8 +405,8 @@ func TestWorldExchangeBatch(t *testing.T) { assert.Equal(t, 702, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{102, 0}, - Changed: All(relID), - Removed: []ID{relID}, + Removed: All(relID), + RemovedIDs: []ID{relID}, OldRelation: &relID, NewRelation: nil, OldTarget: target1, @@ -418,13 +419,13 @@ func TestWorldExchangeBatch(t *testing.T) { assert.Equal(t, EntityEvent{ Entity: Entity{102, 0}, - Changed: All(posID), - Removed: []ID{posID}, + Removed: All(posID), + RemovedIDs: []ID{posID}, EventTypes: event.EntityRemoved | event.ComponentRemoved, }, events[801]) - assert.Equal(t, []ID{velID}, events[202].Added) - assert.Equal(t, []ID{posID}, events[202].Removed) + assert.Equal(t, []ID{velID}, events[202].AddedIDs) + assert.Equal(t, []ID{posID}, events[202].RemovedIDs) filter = All(velID) w.Batch().Remove(filter, velID) @@ -818,7 +819,6 @@ func TestWorldRelationSet(t *testing.T) { assert.Equal(t, 4, len(events)) assert.Equal(t, EntityEvent{ Entity: e1, - Changed: All(), OldRelation: &relID, NewRelation: &relID, OldTarget: Entity{}, @@ -836,7 +836,6 @@ func TestWorldRelationSet(t *testing.T) { assert.Equal(t, 5, len(events)) assert.Equal(t, EntityEvent{ Entity: e1, - Changed: All(), OldRelation: &relID, NewRelation: &relID, OldTarget: targ, @@ -861,8 +860,8 @@ func TestWorldRelationSet(t *testing.T) { assert.Equal(t, 6, len(events)) assert.Equal(t, EntityEvent{ Entity: e2, - Changed: All(relID), - Removed: []ID{relID}, + Removed: All(relID), + RemovedIDs: []ID{relID}, OldRelation: &relID, NewRelation: nil, EventTypes: event.ComponentRemoved | event.RelationChanged, @@ -1468,8 +1467,8 @@ func TestWorldBatchRemove(t *testing.T) { assert.Equal(t, 33, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{33, 0}, - Changed: All(rotID, relID), - Added: []ID{rotID, relID}, + Added: All(rotID, relID), + AddedIDs: []ID{rotID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged | event.TargetChanged, }, events[len(events)-1]) @@ -1487,8 +1486,8 @@ func TestWorldBatchRemove(t *testing.T) { assert.Equal(t, 43, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{13, 0}, - Changed: All(rotID, relID), - Removed: []ID{rotID, relID}, + Removed: All(rotID, relID), + RemovedIDs: []ID{rotID, relID}, OldRelation: &relID, EventTypes: event.EntityRemoved | event.ComponentRemoved | event.RelationChanged | event.TargetChanged, }, events[len(events)-1]) @@ -1627,8 +1626,8 @@ func TestWorldListener(t *testing.T) { assert.Equal(t, 3, len(events)) assert.Equal(t, EntityEvent{ Entity: e0, - Changed: All(posID, velID), - Added: []ID{posID, velID}, + Added: All(posID, velID), + AddedIDs: []ID{posID, velID}, EventTypes: event.EntityCreated | event.ComponentAdded, }, events[len(events)-1]) @@ -1636,8 +1635,8 @@ func TestWorldListener(t *testing.T) { assert.Equal(t, 4, len(events)) assert.Equal(t, EntityEvent{ Entity: e0, - Changed: All(posID, velID), - Removed: []ID{posID, velID}, + Removed: All(posID, velID), + RemovedIDs: []ID{posID, velID}, EventTypes: event.EntityRemoved | event.ComponentRemoved, }, events[len(events)-1]) @@ -1645,8 +1644,8 @@ func TestWorldListener(t *testing.T) { assert.Equal(t, 5, len(events)) assert.Equal(t, EntityEvent{ Entity: e0, - Changed: All(posID, velID, relID), - Added: []ID{posID, velID, relID}, + Added: All(posID, velID, relID), + AddedIDs: []ID{posID, velID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged, }, events[len(events)-1]) @@ -1655,8 +1654,8 @@ func TestWorldListener(t *testing.T) { assert.Equal(t, 6, len(events)) assert.Equal(t, EntityEvent{ Entity: e0, - Changed: All(rotID), - Added: []ID{rotID}, + Added: All(rotID), + AddedIDs: []ID{rotID}, OldRelation: &relID, NewRelation: &relID, EventTypes: event.ComponentAdded, @@ -1666,8 +1665,8 @@ func TestWorldListener(t *testing.T) { assert.Equal(t, 7, len(events)) assert.Equal(t, EntityEvent{ Entity: e0, - Changed: All(posID), - Removed: []ID{posID}, + Removed: All(posID), + RemovedIDs: []ID{posID}, OldRelation: &relID, NewRelation: &relID, EventTypes: event.ComponentRemoved, @@ -1678,7 +1677,6 @@ func TestWorldListener(t *testing.T) { assert.Equal(t, 9, len(events)) assert.Equal(t, EntityEvent{ Entity: e0, - Changed: All(), OldRelation: &relID, NewRelation: &relID, EventTypes: event.TargetChanged, @@ -1688,8 +1686,8 @@ func TestWorldListener(t *testing.T) { assert.Equal(t, 10, len(events)) assert.Equal(t, EntityEvent{ Entity: e0, - Changed: All(relID), - Removed: []ID{relID}, + Removed: All(relID), + RemovedIDs: []ID{relID}, OldRelation: &relID, NewRelation: nil, OldTarget: e1, @@ -1718,8 +1716,8 @@ func TestWorldListenerBuilder(t *testing.T) { assert.Equal(t, 11, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{11, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged, }, events[len(events)-1]) @@ -1730,8 +1728,8 @@ func TestWorldListenerBuilder(t *testing.T) { assert.Equal(t, 21, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{21, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged, }, events[len(events)-1]) @@ -1741,8 +1739,8 @@ func TestWorldListenerBuilder(t *testing.T) { assert.Equal(t, 31, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{31, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged | event.TargetChanged, }, events[len(events)-1]) @@ -1753,8 +1751,8 @@ func TestWorldListenerBuilder(t *testing.T) { assert.Equal(t, 41, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{41, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged | event.TargetChanged, }, events[len(events)-1]) @@ -1769,8 +1767,8 @@ func TestWorldListenerBuilder(t *testing.T) { assert.Equal(t, 51, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{51, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged, }, events[len(events)-1]) @@ -1781,8 +1779,8 @@ func TestWorldListenerBuilder(t *testing.T) { assert.Equal(t, 61, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{61, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged, }, events[len(events)-1]) @@ -1792,8 +1790,8 @@ func TestWorldListenerBuilder(t *testing.T) { assert.Equal(t, 71, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{71, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged | event.TargetChanged, }, events[len(events)-1]) @@ -1804,8 +1802,8 @@ func TestWorldListenerBuilder(t *testing.T) { assert.Equal(t, 81, len(events)) assert.Equal(t, EntityEvent{ Entity: Entity{81, 0}, - Changed: All(posID, relID), - Added: []ID{posID, relID}, + Added: All(posID, relID), + AddedIDs: []ID{posID, relID}, NewRelation: &relID, EventTypes: event.EntityCreated | event.ComponentAdded | event.RelationChanged | event.TargetChanged, }, events[len(events)-1]) diff --git a/examples/events/main.go b/examples/events/main.go index d4a53c28..ba66a5ac 100644 --- a/examples/events/main.go +++ b/examples/events/main.go @@ -33,7 +33,7 @@ type EventHandler struct{} func (h *EventHandler) Notify(world *ecs.World, evt ecs.EntityEvent) { fmt.Printf(" Type mask: %06b\n", evt.EventTypes) fmt.Printf(" Entity: %+v\n", evt.Entity) - fmt.Printf(" Added/Removed: %+v / %+v\n", evt.Added, evt.Removed) + fmt.Printf(" Added/Removed: %+v / %+v\n", evt.AddedIDs, evt.RemovedIDs) fmt.Printf(" Relation: %+v -> %+v\n", evt.OldRelation, evt.NewRelation) var target ecs.Entity diff --git a/listener/callback.go b/listener/callback.go index a949d455..efcd518b 100644 --- a/listener/callback.go +++ b/listener/callback.go @@ -18,7 +18,7 @@ type Callback struct { // NewCallback creates a new Callback listener for the given events. // // Subscribes to the specified events with changes on the specified components. -// If no component IDs are given, is subscribes to all components. +// If no component IDs are given, it subscribes to all components. func NewCallback(callback func(*ecs.World, ecs.EntityEvent), events event.Subscription, components ...ecs.ID) Callback { return Callback{ callback: callback, diff --git a/listener/dispatch.go b/listener/dispatch.go index 93da6cd9..c4a2f736 100644 --- a/listener/dispatch.go +++ b/listener/dispatch.go @@ -7,10 +7,10 @@ import ( // Dispatch event listener. // -// Dispatches events to sub-listeners and manages subscription automatically. -// Sub-listeners should not alter their subscriptions or components after adding them. +// Dispatches events to sub-listeners and manages subscription automatically, based on their settings. +// Sub-listeners should not alter their subscriptions or components after being added. // -// To make it possible for systems to add listeners, Dispatch should be added to the [ecs.World] as a resource. +// To make it possible for systems to add listeners, Dispatch can be added to the [ecs.World] as a resource. type Dispatch struct { listeners []ecs.Listener events event.Subscription @@ -56,8 +56,8 @@ func (l *Dispatch) AddListener(ls ecs.Listener) { // Notify the listener. func (l *Dispatch) Notify(world *ecs.World, evt ecs.EntityEvent) { for _, ls := range l.listeners { - if ls.Subscriptions().ContainsAny(evt.EventTypes) && - subscribes(evt.EventTypes, &evt.Changed, ls.Components(), evt.OldRelation, evt.NewRelation) { + trigger := ls.Subscriptions() & evt.EventTypes + if trigger != 0 && subscribes(trigger, &evt.Added, &evt.Removed, ls.Components(), evt.OldRelation, evt.NewRelation) { ls.Notify(world, evt) } } @@ -75,10 +75,3 @@ func (l *Dispatch) Components() *ecs.Mask { } return nil } - -func subscribes(evtType event.Subscription, changed *ecs.Mask, subs *ecs.Mask, oldRel *ecs.ID, newRel *ecs.ID) bool { - if event.Relations.Contains(evtType) { - return subs == nil || (oldRel != nil && subs.Get(*oldRel)) || (newRel != nil && subs.Get(*newRel)) - } - return subs == nil || subs.ContainsAny(changed) -} diff --git a/listener/util.go b/listener/util.go new file mode 100644 index 00000000..83c75d6c --- /dev/null +++ b/listener/util.go @@ -0,0 +1,39 @@ +package listener + +import ( + "github.com/mlange-42/arche/ecs" + "github.com/mlange-42/arche/ecs/event" +) + +// Returns whether a listener is interested in an event based on event type and component subscriptions. +// +// Argument trigger should only contain the subscription bits that triggered the event. +// I.e. subscriptions & evenTypes. +func subscribes(trigger event.Subscription, added *ecs.Mask, removed *ecs.Mask, subs *ecs.Mask, oldRel *ecs.ID, newRel *ecs.ID) bool { + if trigger == 0 { + return false + } + if subs == nil { + // No component subscriptions + return true + } + if trigger.ContainsAny(event.Relations) { + // Contains event.RelationChanged and/or event.TargetChanged + if (oldRel != nil && subs.Get(*oldRel)) || (newRel != nil && subs.Get(*newRel)) { + return true + } + } + if trigger.ContainsAny(event.EntityCreated | event.ComponentAdded) { + // Contains additions-like types + if added != nil && subs.ContainsAny(added) { + return true + } + } + if trigger.ContainsAny(event.EntityRemoved | event.ComponentRemoved) { + // Contains additions-like types + if removed != nil && subs.ContainsAny(removed) { + return true + } + } + return false +} diff --git a/listener/util_test.go b/listener/util_test.go new file mode 100644 index 00000000..0ecb9ddf --- /dev/null +++ b/listener/util_test.go @@ -0,0 +1,79 @@ +package listener + +import ( + "testing" + + "github.com/mlange-42/arche/ecs" + "github.com/mlange-42/arche/ecs/event" + "github.com/stretchr/testify/assert" +) + +type comp1 struct{} +type comp2 struct{} +type comp3 struct{} + +func TestSubscribes(t *testing.T) { + world := ecs.NewWorld() + id1 := ecs.ComponentID[comp1](&world) + id2 := ecs.ComponentID[comp2](&world) + id3 := ecs.ComponentID[comp3](&world) + + m1 := ecs.All(id1) + m12 := ecs.All(id1, id2) + m2 := ecs.All(id2) + m3 := ecs.All(id3) + + assert.False(t, + subscribes(0, &m1, nil, &m12, nil, nil), + ) + + assert.True(t, + subscribes(event.ComponentAdded, &m1, nil, &m12, nil, nil), + ) + assert.False(t, + subscribes(event.ComponentAdded, nil, &m1, &m12, nil, nil), + ) + assert.True(t, + subscribes(event.ComponentAdded, &m12, nil, &m2, nil, nil), + ) + assert.False(t, + subscribes(event.ComponentAdded, &m12, nil, &m3, nil, nil), + ) + + assert.True(t, + subscribes(event.ComponentRemoved, nil, &m1, &m12, nil, nil), + ) + assert.False(t, + subscribes(event.ComponentRemoved, &m1, nil, &m12, nil, nil), + ) + assert.True(t, + subscribes(event.ComponentRemoved, nil, &m12, &m2, nil, nil), + ) + assert.False(t, + subscribes(event.ComponentRemoved, nil, &m12, &m3, nil, nil), + ) + + assert.True(t, + subscribes(event.RelationChanged, nil, nil, &m12, nil, &id1), + ) + assert.True(t, + subscribes(event.RelationChanged, nil, nil, &m12, &id1, &id3), + ) + assert.False(t, + subscribes(event.RelationChanged, nil, nil, &m1, &id2, &id3), + ) + + assert.True(t, + subscribes(event.TargetChanged, nil, nil, &m12, &id1, &id1), + ) + assert.False(t, + subscribes(event.TargetChanged, nil, nil, &m12, &id3, &id3), + ) + + assert.True(t, + subscribes(event.ComponentAdded|event.ComponentRemoved|event.TargetChanged, &m12, &m12, &m3, &id3, &id3), + ) + assert.False(t, + subscribes(event.ComponentAdded|event.ComponentRemoved|event.TargetChanged, &m1, &m1, &m3, &id2, &id2), + ) +}