Skip to content

Commit

Permalink
Ordered queries support (#136)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
imthatgin authored May 23, 2024
1 parent e631a83 commit 680901d
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 35 deletions.
4 changes: 4 additions & 0 deletions examples/bunnymark_ecs/component/position.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
4 changes: 2 additions & 2 deletions examples/bunnymark_ecs/system/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 37 additions & 16 deletions examples/bunnymark_ecs/system/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
})
}
}
34 changes: 22 additions & 12 deletions examples/bunnymark_ecs/system/spawn.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/yohamta/donburi/examples/bunnymark_ecs/layers"
)

var UsePositionOrdering bool

type Spawn struct {
settings *component.SettingsData
}
Expand All @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions ordered_iterator.go
Original file line number Diff line number Diff line change
@@ -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
}
47 changes: 47 additions & 0 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
Expand Down
66 changes: 61 additions & 5 deletions query_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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()

Expand Down

0 comments on commit 680901d

Please sign in to comment.