From e0b7efd89d27923b3ded70af4f7e1b5732622ff9 Mon Sep 17 00:00:00 2001 From: sedyh Date: Tue, 28 Dec 2021 17:38:21 +0300 Subject: [PATCH] Fixed incorrect work of the views and more tests added --- pkg/engine/entity.go | 15 ++++++-- pkg/engine/mask.go | 10 +++++ pkg/engine/view.go | 16 +++++++- pkg/engine/world.go | 11 +++--- test/system_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++ test/view_test.go | 74 ++++++++++++++++++++++++++++++++++++ 6 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 test/system_test.go create mode 100644 test/view_test.go diff --git a/pkg/engine/entity.go b/pkg/engine/entity.go index 33d5b53..c9fa6fd 100644 --- a/pkg/engine/entity.go +++ b/pkg/engine/entity.go @@ -35,13 +35,20 @@ func makeEntity(w *world, components ...interface{}) *entity { // entity.Get(&pos, &rad) func (e *entity) Get(components ...interface{}) { for _, component := range components { - componentValue := reflect.ValueOf(component).Elem() - componentType := componentValue.Type().Elem() + componentValue := reflect.ValueOf(component) + if componentValue.Kind() != reflect.Ptr { + panic(fmt.Sprintf("received entity component %s must be a pointer", typeName(componentValue.Type()))) + } + componentValueElem := componentValue.Elem() + if componentValueElem.Kind() != reflect.Ptr { + panic(fmt.Sprintf("received entity component %s must be a pointer to pointer", typeName(componentValue.Type()))) + } + componentType := componentValueElem.Type().Elem() componentId := e.w.componentIds[componentType] if e.mask.get(componentId) { - e.w.stores[componentId].get(e.id, componentValue) + e.w.stores[componentId].get(e.id, componentValueElem) } else { - componentValue.Set(reflect.Zero(reflect.PtrTo(componentType))) + componentValueElem.Set(reflect.Zero(reflect.PtrTo(componentType))) } } } diff --git a/pkg/engine/mask.go b/pkg/engine/mask.go index 847f129..fb0d514 100644 --- a/pkg/engine/mask.go +++ b/pkg/engine/mask.go @@ -1,5 +1,7 @@ package engine +import "fmt" + // mask is an implementation of an expanding bitmask. type mask []uint64 @@ -32,3 +34,11 @@ func (m mask) contains(mask mask) bool { } return true } + +func (m mask) String() string { + str := "" + for _, bits := range m { + str += fmt.Sprintf("%064b", bits) + } + return str +} diff --git a/pkg/engine/view.go b/pkg/engine/view.go index c1648c2..11838bf 100644 --- a/pkg/engine/view.go +++ b/pkg/engine/view.go @@ -9,6 +9,7 @@ import ( // This is useful when you need to make requests to different sets // of components in the same system. type View interface { + Each(consumer func(entity Entity)) Filter() []Entity } @@ -24,14 +25,27 @@ func makeView(world *world, components ...interface{}) *view { for _, component := range components { componentType := reflect.TypeOf(component) - componentId := world.componentIds[componentType] + componentId, ok := world.componentIds[componentType] + if !ok { + continue + } m.set(componentId) } return &view{w: world, mask: m} } +// Each iterates all entities with the previously selected components. +func (v *view) Each(consumer func(entity Entity)) { + for _, e := range v.w.entities { + if e.mask.contains(v.mask) { + consumer(e) + } + } +} + // Filter returns a list of entities with the previously selected components for separate sorting and iteration. +// It is save to delete from here func (v *view) Filter() []Entity { entities := make([]Entity, 0, 2) for _, e := range v.w.entities { diff --git a/pkg/engine/world.go b/pkg/engine/world.go index b1985d8..57d9ea0 100644 --- a/pkg/engine/world.go +++ b/pkg/engine/world.go @@ -100,14 +100,13 @@ func (w *world) Bounds() image.Rectangle { // View creates a query to filter entities by their components. func (w *world) View(components ...interface{}) View { - return makeView(w, components) + return makeView(w, components...) } // AddComponents registers the used components that will represent your object properties. func (w *world) AddComponents(components ...interface{}) { for _, component := range components { - componentValue := reflect.ValueOf(component) - componentType := componentValue.Type() + componentType := reflect.TypeOf(component) if _, ok := w.componentIds[componentType]; ok { continue } @@ -148,9 +147,9 @@ func (w *world) RemoveEntity(e Entity) { w.entities[len(w.entities)-1] = nil w.entities = w.entities[:len(w.entities)-1] - for i := 0; i < len(w.stores); i++ { - if v.mask.get(i) { - w.stores[i].rem(v.id) + for j := 0; j < len(w.stores); j++ { + if v.mask.get(j) { + w.stores[j].rem(v.id) } } w.entitiesIds.rem(v.id) diff --git a/test/system_test.go b/test/system_test.go new file mode 100644 index 0000000..ed4b68a --- /dev/null +++ b/test/system_test.go @@ -0,0 +1,90 @@ +package test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/sedyh/mizu/pkg/engine" + "github.com/sedyh/mizu/test/helper" +) + +type Position struct { + X, Y int +} + +type Velocity struct { + X, Y int +} + +type Gravity struct { + Value int +} + +type First struct { + Value bool +} + +type Ball struct { + First + Position + Velocity + Gravity +} + +type Movement struct { + *Position + *Velocity +} + +func (m *Movement) Update(_ engine.World) { + m.Position.X += m.Velocity.X + m.Position.Y += m.Velocity.Y +} + +type Falling struct { + *Velocity + *Gravity +} + +func (f *Falling) Update(_ engine.World) { + f.Velocity.Y += f.Gravity.Value +} + +var _ = Describe("Game world", func() { + It("Should be able to create a system that traverses each entity", func() { + var world engine.World + + // Run the world with two systems in two entities + helper.RunSingleSceneGame(func(w engine.World) { + w.AddComponents(First{}, Position{}, Velocity{}, Gravity{}) + w.AddEntities( + &Ball{First{true}, Position{1, 5}, Velocity{-3, 4}, Gravity{2}}, + &Ball{First{}, Position{-5, 3}, Velocity{1, 8}, Gravity{3}}, + ) + w.AddSystems(&Movement{}, &Falling{}) + world = w + }) + + // Check that each system done it work right + world.View(First{}, Position{}, Velocity{}).Each(func(e engine.Entity) { + var first *First + var position *Position + var velocity *Velocity + e.Get(&first, &position, &velocity) + if first.Value { + Expect(*position).To(Equal(Position{-2, 9})) + Expect(*velocity).To(Equal(Velocity{-3, 6})) + } + }) + world.View(First{}, Position{}, Velocity{}).Each(func(e engine.Entity) { + var first *First + var position *Position + var velocity *Velocity + e.Get(&first, &position, &velocity) + if !first.Value { + Expect(*position).To(Equal(Position{-4, 11})) + Expect(*velocity).To(Equal(Velocity{1, 11})) + } + }) + }) +}) diff --git a/test/view_test.go b/test/view_test.go new file mode 100644 index 0000000..2489686 --- /dev/null +++ b/test/view_test.go @@ -0,0 +1,74 @@ +package test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/sedyh/mizu/pkg/engine" + "github.com/sedyh/mizu/test/helper" +) + +var _ = Describe("Game world", func() { + It("Should be able to create the correct View", func() { + type ComponentA struct { + Value int + } + type ComponentB struct { + Value int + } + type ComponentC struct { + Value int + } + type ComponentD struct { + Value int + } + type EntityA struct { + ComponentA + ComponentB + ComponentC + } + type EntityB struct { + ComponentD + } + i := 0 + helper.RunSingleSceneGame(func(w engine.World) { + w.AddComponents(ComponentA{}, ComponentB{}, ComponentC{}, ComponentD{}) + w.AddEntities(&EntityA{ComponentA{}, ComponentB{}, ComponentC{}}, &EntityB{ComponentD{}}) + for j := 0; j < 100; j++ { + // Each view should fire only one time per iteration + for _, e := range w.View(ComponentA{}).Filter() { + var a *ComponentA + e.Get(&a) + i++ + } + for _, e := range w.View(ComponentA{}, ComponentB{}).Filter() { + var a *ComponentA + var b *ComponentB + e.Get(&a, &b) + i++ + } + // The order of the components is not important + for _, e := range w.View(ComponentB{}, ComponentC{}).Filter() { + var b *ComponentB + var c *ComponentC + e.Get(&b, &c) + i++ + } + for _, e := range w.View(ComponentC{}, ComponentB{}).Filter() { + var c *ComponentC + var b *ComponentB + e.Get(&c, &b) + i++ + } + for _, e := range w.View(ComponentA{}, ComponentB{}, ComponentB{}).Filter() { + var a *ComponentA + var b *ComponentB + var c *ComponentC + e.Get(&c, &b, &a) + i++ + } + } + }) + Expect(i).To(Equal(500)) + }) +})