Skip to content

Commit

Permalink
Improve EntityEvent fields (#333)
Browse files Browse the repository at this point in the history
* Pass events by value/copy instead of by pointer. It torned out heap escape is much more expensive than copying.
* Removes event fields that are accessible from the world.
* Adds event fields to better identify relation changes.
* Reduces the size of `EntityEvent` by 30%.

# Commits

* simplify EntityEvent, add relation fileds
* remove event fields that are accessible from the world
* add event fields to better identify relation changes
* add tests for events on batch operations
* tweak EntityEvent docstrings
* fix entity creation events
* tests and fixes for batch creation events
* improve invalid relation error messages
* add docs on behavior when an exchange changes relation components
* add more tests for batch operations
* add more benchmarks for entity events
* make listener event argument pass by value instead of pointer
  • Loading branch information
mlange-42 authored Jan 16, 2024
1 parent ff1782a commit 9db71a4
Show file tree
Hide file tree
Showing 10 changed files with 713 additions and 92 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* `Filters.Matches(Mask)` became `Filters.Matches(*Mask)`; same for all `Filter` implementations (#313)
This change was necessary to get the same performance as before, despite the more heavyweight implementation of the now 256 bits `Mask`.
* Component and resource IDs are now opaque types instead of type aliases for `uint8` (#329)
* Restructures `EntityEvent` to remove redundant information and better handle relation changes (#333)
* World event listener function signature changed from `func(*EntityEvent)` to `func(EntityEvent)` (#333)

### Features

Expand Down
30 changes: 29 additions & 1 deletion ecs/bitmask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ func TestBitMask(t *testing.T) {
assert.False(t, mask.ContainsAny(&other2))
}

func TestBitMaskCopy(t *testing.T) {
mask := All(id(1), id(2), id(13), id(27), id(200))
mask2 := mask
mask3 := &mask

mask2.Set(id(1), false)
mask3.Set(id(2), false)

assert.True(t, mask.Get(id(1)))
assert.False(t, mask2.Get(id(1)))

assert.True(t, mask2.Get(id(2)))
assert.False(t, mask.Get(id(2)))
assert.False(t, mask3.Get(id(2)))
}

func TestBitMaskWithoutExclusive(t *testing.T) {
mask := All(id(1), id(2), id(13))

Expand Down Expand Up @@ -234,7 +250,7 @@ func BenchmarkMaskPointer(b *testing.B) {
_ = v
}

func BenchmarkMask(b *testing.B) {
func BenchmarkMaskMatch(b *testing.B) {
b.StopTimer()
mask := All(id(0), id(1), id(2))
bits := All(id(0), id(1), id(2))
Expand All @@ -248,6 +264,18 @@ func BenchmarkMask(b *testing.B) {
_ = v
}

func BenchmarkMaskCopy(b *testing.B) {
b.StopTimer()
mask := All(id(0), id(1), id(2))
var tempMask Mask
b.StartTimer()
for i := 0; i < b.N; i++ {
tempMask = mask
}
b.StopTimer()
_ = tempMask
}

// bitMask64 is there just for performance comparison with the new 256 bit Mask.
type bitMask64 uint64

Expand Down
21 changes: 10 additions & 11 deletions ecs/event.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package ecs

// EntityEvent contains information about component changes to an [Entity].
// EntityEvent contains information about component and relation changes to an [Entity].
//
// To receive change events, register a function func(e *EntityEvent) with [World.SetListener].
//
// Events notified are entity creation, removal and changes to the component composition.
// Events notified are entity creation, removal, changes to the component composition and change of relation targets.
// Events are emitted immediately after the change is applied.
//
// Except for removed entities, events are always fired when the [World] is in an unlocked state.
Expand All @@ -15,16 +15,15 @@ package ecs
// Events for batch-creation of entities using a [Builder] are fired after all entities are created.
// For batch methods that return a [Query], events are fired after the [Query] is closed (or fully iterated).
// This allows the [World] to be in an unlocked state, and notifies after potential entity initialization.
//
// Note that the event pointer received by the listener function should not be stored,
// as the instance behind the pointer might be reused for further notifications.
type EntityEvent struct {
Entity Entity // The entity that was changed.
OldMask, NewMask Mask // The old and new component masks.
Added, Removed, Current []ID // Components added, removed, and after the change. DO NOT MODIFY!
AddedRemoved int // Whether the entity itself was added (> 0), removed (< 0), or only changed (= 0).
OldTarget, NewTarget Entity // Old and new target entity
TargetChanged bool // Whether this is (only) a change of the relation target.
Entity Entity // The entity that was changed.
OldMask Mask // The old component masks. Get the new mask with [World.Mask].
Added, Removed []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].
AddedRemoved int8 // Whether the entity itself was added (> 0), removed (< 0), or only changed (= 0).
RelationChanged bool // Whether the relation component has changed.
TargetChanged bool // Whether the relation target has changed. Will be false if the relation component changes, but the target does not.
}

// EntityAdded reports whether the entity was newly added.
Expand Down
48 changes: 42 additions & 6 deletions ecs/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,53 @@ func (h *eventHandler) ListenPointer(e *ecs.EntityEvent) {
h.LastEntity = e.Entity
}

func BenchmarkEntityEventCreate(b *testing.B) {
b.StopTimer()
world := ecs.NewWorld()
posID := ecs.ComponentID[Position](&world)
e := world.NewEntity()
mask := ecs.All(posID)
added := []ecs.ID{posID}

var event ecs.EntityEvent

b.StartTimer()
for i := 0; i < b.N; i++ {
event = ecs.EntityEvent{Entity: e, OldMask: mask, Added: added, Removed: nil, AddedRemoved: 0}
}
b.StopTimer()
_ = event
}

func BenchmarkEntityEventHeapPointer(b *testing.B) {
b.StopTimer()
world := ecs.NewWorld()
posID := ecs.ComponentID[Position](&world)
e := world.NewEntity()
mask := ecs.All(posID)
added := []ecs.ID{posID}

var event *ecs.EntityEvent

b.StartTimer()
for i := 0; i < b.N; i++ {
event = &ecs.EntityEvent{Entity: e, OldMask: mask, Added: added, Removed: nil, AddedRemoved: 0}
}
b.StopTimer()
_ = event
}

func BenchmarkEntityEventCopy(b *testing.B) {
handler := eventHandler{}

for i := 0; i < b.N; i++ {
handler.ListenCopy(ecs.EntityEvent{Entity: ecs.Entity{}, OldMask: ecs.Mask{}, NewMask: ecs.Mask{}, Added: nil, Removed: nil, Current: nil, AddedRemoved: 0})
handler.ListenCopy(ecs.EntityEvent{Entity: ecs.Entity{}, OldMask: ecs.Mask{}, Added: nil, Removed: nil, AddedRemoved: 0})
}
}

func BenchmarkEntityEventCopyReuse(b *testing.B) {
handler := eventHandler{}
event := ecs.EntityEvent{Entity: ecs.Entity{}, OldMask: ecs.Mask{}, NewMask: ecs.Mask{}, Added: nil, Removed: nil, Current: nil, AddedRemoved: 0}
event := ecs.EntityEvent{Entity: ecs.Entity{}, OldMask: ecs.Mask{}, Added: nil, Removed: nil, AddedRemoved: 0}

for i := 0; i < b.N; i++ {
handler.ListenCopy(event)
Expand All @@ -58,13 +94,13 @@ func BenchmarkEntityEventPointer(b *testing.B) {
handler := eventHandler{}

for i := 0; i < b.N; i++ {
handler.ListenPointer(&ecs.EntityEvent{Entity: ecs.Entity{}, OldMask: ecs.Mask{}, NewMask: ecs.Mask{}, Added: nil, Removed: nil, Current: nil, AddedRemoved: 0})
handler.ListenPointer(&ecs.EntityEvent{Entity: ecs.Entity{}, OldMask: ecs.Mask{}, Added: nil, Removed: nil, AddedRemoved: 0})
}
}

func BenchmarkEntityEventPointerReuse(b *testing.B) {
handler := eventHandler{}
event := ecs.EntityEvent{Entity: ecs.Entity{}, OldMask: ecs.Mask{}, NewMask: ecs.Mask{}, Added: nil, Removed: nil, Current: nil, AddedRemoved: 0}
event := ecs.EntityEvent{Entity: ecs.Entity{}, OldMask: ecs.Mask{}, Added: nil, Removed: nil, AddedRemoved: 0}

for i := 0; i < b.N; i++ {
handler.ListenPointer(&event)
Expand All @@ -74,11 +110,11 @@ func BenchmarkEntityEventPointerReuse(b *testing.B) {
func ExampleEntityEvent() {
world := ecs.NewWorld()

listener := func(evt *ecs.EntityEvent) {
listener := func(evt ecs.EntityEvent) {
fmt.Println(evt)
}
world.SetListener(listener)

world.NewEntity()
// Output: &{{1 0} {[0 0 0 0]} {[0 0 0 0]} [] [] [] 1 {0 0} {0 0} false}
// Output: {{1 0} {[0 0 0 0]} [] [] <nil> <nil> {0 0} 1 false false}
}
Loading

0 comments on commit 9db71a4

Please sign in to comment.