From 680901d36f4ad7baa7fcf059a1cfb0a5f32af14d Mon Sep 17 00:00:00 2001 From: gin Date: Thu, 23 May 2024 04:39:29 +0200 Subject: [PATCH] Ordered queries support (#136) * Work in progress ordered query iteration * Fastest yet * Forgot this * Update query.go * New approach * New iteration system * Improve documentation * Fix a crash * Support ordered queries --- examples/bunnymark_ecs/component/position.go | 4 ++ examples/bunnymark_ecs/system/metrics.go | 4 +- examples/bunnymark_ecs/system/render.go | 53 +++++++++++----- examples/bunnymark_ecs/system/spawn.go | 34 ++++++---- ordered_iterator.go | 43 +++++++++++++ query.go | 47 ++++++++++++++ query_test.go | 66 ++++++++++++++++++-- 7 files changed, 216 insertions(+), 35 deletions(-) create mode 100644 ordered_iterator.go diff --git a/examples/bunnymark_ecs/component/position.go b/examples/bunnymark_ecs/component/position.go index 342b295..336909b 100644 --- a/examples/bunnymark_ecs/component/position.go +++ b/examples/bunnymark_ecs/component/position.go @@ -8,4 +8,8 @@ type PositionData struct { X, Y float64 } +func (p PositionData) Order() int { + return int(p.Y * 600) +} + var Position = donburi.NewComponentType[PositionData]() diff --git a/examples/bunnymark_ecs/system/metrics.go b/examples/bunnymark_ecs/system/metrics.go index 83e4291..13df930 100644 --- a/examples/bunnymark_ecs/system/metrics.go +++ b/examples/bunnymark_ecs/system/metrics.go @@ -42,10 +42,10 @@ func (m *Metrics) Update(ecs *ecs.ECS) { func (m *Metrics) Draw(ecs *ecs.ECS, screen *ebiten.Image) { str := fmt.Sprintf( - "GPU: %s\nTPS: %.2f, FPS: %.2f, Objects: %.f\nBatching: %t, Amount: %d\nResolution: %dx%d", + "GPU: %s\nTPS: %.2f, FPS: %.2f, Objects: %.f\nBatching: %t, Amount: %d\nResolution: %dx%d\nUsePositionOrdering: %t", m.settings.Gpu, m.settings.Tps.Last(), m.settings.Fps.Last(), m.settings.Objects.Last(), !m.settings.Colorful, m.settings.Amount, - m.bounds.Dx(), m.bounds.Dy(), + m.bounds.Dx(), m.bounds.Dy(), UsePositionOrdering, ) rect := text.BoundString(basicfont.Face7x13, str) diff --git a/examples/bunnymark_ecs/system/render.go b/examples/bunnymark_ecs/system/render.go index e9e6a57..9b852fc 100644 --- a/examples/bunnymark_ecs/system/render.go +++ b/examples/bunnymark_ecs/system/render.go @@ -6,17 +6,22 @@ import ( "github.com/yohamta/donburi" "github.com/yohamta/donburi/ecs" "github.com/yohamta/donburi/examples/bunnymark_ecs/component" - "github.com/yohamta/donburi/examples/bunnymark_ecs/layers" "github.com/yohamta/donburi/filter" ) type render struct { - query *donburi.Query + query *donburi.Query + orderedQuery *donburi.OrderedQuery[component.PositionData] } var Render = &render{ - query: ecs.NewQuery( - layers.LayerBunnies, + query: donburi.NewQuery( + filter.Contains( + component.Position, + component.Hue, + component.Sprite, + )), + orderedQuery: donburi.NewOrderedQuery[component.PositionData]( filter.Contains( component.Position, component.Hue, @@ -25,17 +30,33 @@ var Render = &render{ } func (r *render) Draw(ecs *ecs.ECS, screen *ebiten.Image) { - r.query.Each(ecs.World, func(entry *donburi.Entry) { - position := component.Position.Get(entry) - hue := component.Hue.Get(entry) - sprite := component.Sprite.Get(entry) + if !UsePositionOrdering { + r.query.Each(ecs.World, func(entry *donburi.Entry) { + position := component.Position.Get(entry) + hue := component.Hue.Get(entry) + sprite := component.Sprite.Get(entry) + + op := &ebiten.DrawImageOptions{} + sw, sh := float64(screen.Bounds().Dx()), float64(screen.Bounds().Dy()) + op.GeoM.Translate(position.X*sw, position.Y*sh) + if *hue.Colorful { + op.ColorM.RotateHue(hue.Value) + } + screen.DrawImage(sprite.Image, op) + }) + } else { + r.orderedQuery.EachOrdered(ecs.World, component.Position, func(entry *donburi.Entry) { + position := component.Position.Get(entry) + hue := component.Hue.Get(entry) + sprite := component.Sprite.Get(entry) - op := &ebiten.DrawImageOptions{} - sw, sh := float64(screen.Bounds().Dx()), float64(screen.Bounds().Dy()) - op.GeoM.Translate(position.X*sw, position.Y*sh) - if *hue.Colorful { - op.ColorM.RotateHue(hue.Value) - } - screen.DrawImage(sprite.Image, op) - }) + op := &ebiten.DrawImageOptions{} + sw, sh := float64(screen.Bounds().Dx()), float64(screen.Bounds().Dy()) + op.GeoM.Translate(position.X*sw, position.Y*sh) + if *hue.Colorful { + op.ColorM.RotateHue(hue.Value) + } + screen.DrawImage(sprite.Image, op) + }) + } } diff --git a/examples/bunnymark_ecs/system/spawn.go b/examples/bunnymark_ecs/system/spawn.go index 84b5a0d..3b058ed 100644 --- a/examples/bunnymark_ecs/system/spawn.go +++ b/examples/bunnymark_ecs/system/spawn.go @@ -12,6 +12,8 @@ import ( "github.com/yohamta/donburi/examples/bunnymark_ecs/layers" ) +var UsePositionOrdering bool + type Spawn struct { settings *component.SettingsData } @@ -23,32 +25,40 @@ func NewSpawn() *Spawn { } func (s *Spawn) Update(ecs *ecs.ECS) { + if s.settings == nil { + if entry, ok := component.Settings.First(ecs.World); ok { + s.settings = component.Settings.Get(entry) + } + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { s.addBunnies(ecs) } + if inpututil.IsKeyJustPressed(ebiten.KeyF) { + UsePositionOrdering = !UsePositionOrdering + } + if ids := ebiten.AppendTouchIDs(nil); len(ids) > 0 { s.addBunnies(ecs) // not accurate, cause no input manager for this } - if _, offset := ebiten.Wheel(); offset != 0 { - s.settings.Amount += int(offset * 10) - if s.settings.Amount < 0 { - s.settings.Amount = 0 + if s.settings != nil { + if _, offset := ebiten.Wheel(); offset != 0 { + s.settings.Amount += int(offset * 10) + if s.settings.Amount < 0 { + s.settings.Amount = 0 + } } - } - if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { - s.settings.Colorful = !s.settings.Colorful + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + s.settings.Colorful = !s.settings.Colorful + } } + } func (s *Spawn) addBunnies(ecs *ecs.ECS) { - if s.settings == nil { - if entry, ok := component.Settings.First(ecs.World); ok { - s.settings = component.Settings.Get(entry) - } - } entities := ecs.CreateMany( layers.LayerBunnies, diff --git a/ordered_iterator.go b/ordered_iterator.go new file mode 100644 index 0000000..50da912 --- /dev/null +++ b/ordered_iterator.go @@ -0,0 +1,43 @@ +package donburi + +import "sort" + +// OrderedEntryIterator is an iterator for entries from a list of `[]Entity`. +type OrderedEntryIterator[T IOrderable] struct { + current int + entries []*Entry +} + +// OrderedEntryIterator is an iterator for entries based on a list of `[]Entity`. +func NewOrderedEntryIterator[T IOrderable](current int, w World, entities []Entity, orderedBy *ComponentType[T]) OrderedEntryIterator[T] { + entLen := len(entities) + entries := make([]*Entry, entLen) + orders := make([]int, entLen) + + for i := 0; i < entLen; i++ { + entry := w.Entry(entities[i]) + entries[i] = entry + orders[i] = (*orderedBy.Get(entry)).Order() + } + + sort.Slice(entries, func(i, j int) bool { + return orders[i] < orders[j] + }) + + return OrderedEntryIterator[T]{ + entries: entries, + current: current, + } +} + +// HasNext returns true if there are more entries to iterate over. +func (it *OrderedEntryIterator[T]) HasNext() bool { + return it.current < len(it.entries) +} + +// Next returns the next entry. +func (it *OrderedEntryIterator[T]) Next() *Entry { + nextIndex := it.entries[it.current] + it.current++ + return nextIndex +} diff --git a/query.go b/query.go index 4290cc0..c93d133 100644 --- a/query.go +++ b/query.go @@ -10,6 +10,10 @@ type cache struct { seen int } +type IOrderable interface { + Order() int +} + // Query represents a query for entities. // It is used to filter entities based on their components. // It receives arbitrary filters that are used to filter entities. @@ -30,6 +34,49 @@ func NewQuery(filter filter.LayoutFilter) *Query { } } +// OrderedQuery is a special extension of Query which has a type parameter used +// when running ordered queries using `EachOrdered`. +type OrderedQuery[T IOrderable] struct { + Query +} + +// NewOrderedQuery creates a new ordered query. +// It takes a filter parameter that is used when evaluating the query. +// Use `OrderedQuery.EachOrdered` to run a Each query in ordered mode. +func NewOrderedQuery[T IOrderable](filter filter.LayoutFilter) *OrderedQuery[T] { + return &OrderedQuery[T]{ + //orderedBy: orderedBy, + Query: Query{ + layoutMatches: make(map[WorldId]*cache), + filter: filter, + }, + } +} + +// EachOrdered iterates over all entities within the query filter, and uses the `orderBy` parameter to +// figure out which property to order using. +// `T` must implement `IOrderable` +func (q *OrderedQuery[T]) EachOrdered(w World, orderBy *ComponentType[T], callback func(*Entry)) { + accessor := w.StorageAccessor() + iter := storage.NewEntityIterator(0, accessor.Archetypes, q.evaluateQuery(w, &accessor)) + + for iter.HasNext() { + archetype := iter.Next() + archetype.Lock() + + ents := archetype.Entities() + entrIter := NewOrderedEntryIterator(0, w, ents, orderBy) + for entrIter.HasNext() { + e := entrIter.Next() + if e.entity.IsReady() { + callback(e) + } + } + + archetype.Unlock() + } +} + // Each iterates over all entities that match the query. func (q *Query) Each(w World, callback func(*Entry)) { accessor := w.StorageAccessor() diff --git a/query_test.go b/query_test.go index 0dab4ac..679ab0b 100644 --- a/query_test.go +++ b/query_test.go @@ -1,16 +1,25 @@ package donburi_test import ( - "testing" - "github.com/yohamta/donburi" "github.com/yohamta/donburi/filter" + "testing" + "time" ) +type orderableComponentTest struct { + time.Time +} + +func (o orderableComponentTest) Order() int { + return int(time.Since(o.Time).Milliseconds()) +} + var ( - queryTagA = donburi.NewTag() - queryTagB = donburi.NewTag() - queryTagC = donburi.NewTag() + queryTagA = donburi.NewTag() + queryTagB = donburi.NewTag() + queryTagC = donburi.NewTag() + orderableTest = donburi.NewComponentType[orderableComponentTest]() ) func TestQuery(t *testing.T) { @@ -33,6 +42,53 @@ func TestQuery(t *testing.T) { } } +func BenchmarkQuery_EachOrdered(b *testing.B) { + world := donburi.NewWorld() + for i := 0; i < 30000; i++ { + e := world.Create(orderableTest) + entr := world.Entry(e) + donburi.SetValue(entr, orderableTest, orderableComponentTest{time.Now()}) + } + + query := donburi.NewQuery(filter.Contains(orderableTest)) + orderedQuery := donburi.NewOrderedQuery[orderableComponentTest](filter.Contains(orderableTest)) + countNormal := 0 + countOrdered := 0 + b.Run("Each", func(b *testing.B) { + for i := 0; i < b.N; i++ { + query.Each(world, func(entry *donburi.Entry) { + countNormal++ + }) + } + }) + b.Run("EachOrdered", func(b *testing.B) { + for i := 0; i < b.N; i++ { + orderedQuery.EachOrdered(world, orderableTest, func(entry *donburi.Entry) { + countOrdered++ + }) + } + }) +} + +func BenchmarkQuery_OnlyEachOrdered(b *testing.B) { + world := donburi.NewWorld() + for i := 0; i < 30000; i++ { + e := world.Create(orderableTest) + entr := world.Entry(e) + donburi.SetValue(entr, orderableTest, orderableComponentTest{time.Now()}) + } + + orderedQuery := donburi.NewOrderedQuery[orderableComponentTest](filter.Contains(orderableTest)) + countOrdered := 0 + b.Run("EachOrdered", func(b *testing.B) { + for i := 0; i < b.N; i++ { + orderedQuery.EachOrdered(world, orderableTest, func(entry *donburi.Entry) { + countOrdered++ + }) + } + }) +} + func TestQueryMultipleComponent(t *testing.T) { world := donburi.NewWorld()