diff --git a/README.md b/README.md index 986318f..6878de3 100644 --- a/README.md +++ b/README.md @@ -129,15 +129,15 @@ m.Around(point, radius, func(v tile.Value) uint16{ # Observers -Given that the `Grid` is mutable and you can make changes to it from various goroutines, I have implemented a way to "observe" tile changes through a `View()` method which creates a `View` structure and can be used to observe changes within a bounding box. For example, you might want your player to have a view port and be notified if something changes on the map so you can do something about it. +Given that the `Grid` is mutable and you can make changes to it from various goroutines, I have implemented a way to "observe" tile changes through a `NewView()` method which creates an `Observer` and can be used to observe changes within a bounding box. For example, you might want your player to have a view port and be notified if something changes on the map so you can do something about it. -In order to use these observers, you need to first call the `View()` method and start polling from the `Inbox` channel which will contain the tile update notifications as they happen. This channel has a small buffer, but if not read it will block the update, so make sure you always poll everything from it. +In order to use these observers, you need to first call the `NewView()` function and start polling from the `Inbox` channel which will contain the tile update notifications as they happen. This channel has a small buffer, but if not read it will block the update, so make sure you always poll everything from it. Note that `NewView[S, T]` takes two type parameters, the first one is the type of the state object and the second one is the type of the tile value. The state object is used to store additional information about the view itself, such as the name of the view or a pointer to a socket that is used to send updates to the client. In the example below we create a new 20x20 view on the grid and iterate through all of the tiles in the view. ```go -rect := tile.NewRect(0, 0, 20, 20) -view := grid.View(rect, func(p tile.Point, t tile.Tile){ +view := tile.NewView[string, string](grid, "My View #1") +view.Resize(tile.NewRect(0, 0, 20, 20), func(p tile.Point, t tile.Tile){ // Optional, all of the tiles that are in the view now }) diff --git a/grid.go b/grid.go index 714992a..7d7cf06 100644 --- a/grid.go +++ b/grid.go @@ -146,19 +146,6 @@ func (m *Grid[T]) Neighbors(x, y int16, fn func(Point, Tile[T])) { } } -// View creates a new view of the map. -func (m *Grid[T]) View(rect Rect, fn func(Point, Tile[T])) *View[T] { - view := &View[T]{ - Grid: m, - Inbox: make(chan Update[T], 32), - rect: NewRect(-1, -1, -1, -1), - } - - // Call the resize method - view.Resize(rect, fn) - return view -} - // pageAt loads a page at a given page location func (m *Grid[T]) pageAt(x, y int16) *page[T] { index := int(x) + int(m.pageWidth)*int(y) @@ -380,6 +367,19 @@ func (c Tile[T]) Range(fn func(T) error) error { return nil } +// Observers iterates over all views observing this tile +func (c Tile[T]) Observers(fn func(view Observer[T])) { + if !c.data.IsObserved() { + return + } + + c.grid.observers.Each(c.data.point, func(sub Observer[T]) { + if view, ok := sub.(Observer[T]); ok { + fn(view) + } + }) +} + // Add adds object to the set func (c Tile[T]) Add(v T) { c.data.addObject(c.grid, c.idx, v) diff --git a/point.go b/point.go index a4c85b6..d8a900b 100644 --- a/point.go +++ b/point.go @@ -211,6 +211,19 @@ func (a Rect) Difference(b Rect) (result [4]Rect) { return } +// Pack returns a packed representation of a rectangle +func (a Rect) pack() uint64 { + return uint64(a.Min.Integer())<<32 | uint64(a.Max.Integer()) +} + +// Unpack returns a rectangle from a packed representation +func unpackRect(v uint64) Rect { + return Rect{ + Min: unpackPoint(uint32(v >> 32)), + Max: unpackPoint(uint32(v)), + } +} + // ----------------------------------------------------------------------------- // Diretion represents a direction diff --git a/view.go b/view.go index 940e3d2..12494f5 100644 --- a/view.go +++ b/view.go @@ -5,8 +5,16 @@ package tile import ( "sync" + "sync/atomic" ) +// Observer represents a tile update Observer. +type Observer[T comparable] interface { + Viewport() Rect + Resize(Rect, func(Point, Tile[T])) + onUpdate(*Update[T]) +} + // Update represents a tile update notification. type Update[T comparable] struct { Point // The tile location @@ -16,37 +24,57 @@ type Update[T comparable] struct { Del T // An object was removed from the tile } -// View represents a view which can monitor a collection of tiles. -type View[T comparable] struct { +var _ Observer[string] = (*View[string, string])(nil) + +// View represents a view which can monitor a collection of tiles. Type parameters +// S and T are the state and tile types respectively. +type View[S any, T comparable] struct { Grid *Grid[T] // The associated map Inbox chan Update[T] // The update inbox for the view - rect Rect // The view box + State S // The state of the view + rect atomic.Uint64 // The view box +} + +// NewView creates a new view for a map with a given state. State can be anything +// that is passed to the view and can be used to store additional information. +func NewView[S any, T comparable](m *Grid[T], state S) *View[S, T] { + v := &View[S, T]{ + Grid: m, + Inbox: make(chan Update[T], 32), + State: state, + } + v.rect.Store(NewRect(-1, -1, -1, -1).pack()) + return v +} + +// Viewport returns the current viewport of the view. +func (v *View[S, T]) Viewport() Rect { + return unpackRect(v.rect.Load()) } -// Resize resizes the viewport. -func (v *View[T]) Resize(view Rect, fn func(Point, Tile[T])) { - owner := v.Grid // The parent map - prev := v.rect // Previous bounding box - v.rect = view // New bounding box +// Resize resizes the viewport and notifies the observers of the changes. +func (v *View[S, T]) Resize(view Rect, fn func(Point, Tile[T])) { + grid := v.Grid + prev := unpackRect(v.rect.Swap(view.pack())) for _, diff := range view.Difference(prev) { if diff.IsZero() { continue // Skip zero-value rectangles } - owner.pagesWithin(diff.Min, diff.Max, func(page *page[T]) { + grid.pagesWithin(diff.Min, diff.Max, func(page *page[T]) { r := page.Bounds() switch { // Page is now in view case view.Intersects(r) && !prev.Intersects(r): - if owner.observers.Subscribe(page.point, v) { + if grid.observers.Subscribe(page.point, v) { page.SetObserved(true) // Mark the page as being observed } // Page is no longer in view case !view.Intersects(r) && prev.Intersects(r): - if owner.observers.Unsubscribe(page.point, v) { + if grid.observers.Unsubscribe(page.point, v) { page.SetObserved(false) // Mark the page as not being observed } } @@ -64,25 +92,28 @@ func (v *View[T]) Resize(view Rect, fn func(Point, Tile[T])) { } // MoveTo moves the viewport towards a particular direction. -func (v *View[T]) MoveTo(angle Direction, distance int16, fn func(Point, Tile[T])) { +func (v *View[S, T]) MoveTo(angle Direction, distance int16, fn func(Point, Tile[T])) { p := angle.Vector(distance) + r := v.Viewport() v.Resize(Rect{ - Min: v.rect.Min.Add(p), - Max: v.rect.Max.Add(p), + Min: r.Min.Add(p), + Max: r.Max.Add(p), }, fn) } // MoveBy moves the viewport towards a particular direction. -func (v *View[T]) MoveBy(x, y int16, fn func(Point, Tile[T])) { +func (v *View[S, T]) MoveBy(x, y int16, fn func(Point, Tile[T])) { + r := v.Viewport() v.Resize(Rect{ - Min: v.rect.Min.Add(At(x, y)), - Max: v.rect.Max.Add(At(x, y)), + Min: r.Min.Add(At(x, y)), + Max: r.Max.Add(At(x, y)), }, fn) } // MoveAt moves the viewport to a specific coordinate. -func (v *View[T]) MoveAt(nw Point, fn func(Point, Tile[T])) { - size := v.rect.Max.Subtract(v.rect.Min) +func (v *View[S, T]) MoveAt(nw Point, fn func(Point, Tile[T])) { + r := v.Viewport() + size := r.Max.Subtract(r.Min) v.Resize(Rect{ Min: nw, Max: nw.Add(size), @@ -90,29 +121,31 @@ func (v *View[T]) MoveAt(nw Point, fn func(Point, Tile[T])) { } // Each iterates over all of the tiles in the view. -func (v *View[T]) Each(fn func(Point, Tile[T])) { - v.Grid.Within(v.rect.Min, v.rect.Max, fn) +func (v *View[S, T]) Each(fn func(Point, Tile[T])) { + r := v.Viewport() + v.Grid.Within(r.Min, r.Max, fn) } // At returns the tile at a specified position. -func (v *View[T]) At(x, y int16) (Tile[T], bool) { +func (v *View[S, T]) At(x, y int16) (Tile[T], bool) { return v.Grid.At(x, y) } // WriteAt updates the entire tile at a specific coordinate. -func (v *View[T]) WriteAt(x, y int16, tile Value) { +func (v *View[S, T]) WriteAt(x, y int16, tile Value) { v.Grid.WriteAt(x, y, tile) } // MergeAt updates the bits of tile at a specific coordinate. The bits are specified // by the mask. The bits that need to be updated should be flipped on in the mask. -func (v *View[T]) MergeAt(x, y int16, tile, mask Value) { +func (v *View[S, T]) MergeAt(x, y int16, tile, mask Value) { v.Grid.MaskAt(x, y, tile, mask) } // Close closes the view and unsubscribes from everything. -func (v *View[T]) Close() error { - v.Grid.pagesWithin(v.rect.Min, v.rect.Max, func(page *page[T]) { +func (v *View[S, T]) Close() error { + r := v.Viewport() + v.Grid.pagesWithin(r.Min, r.Max, func(page *page[T]) { if v.Grid.observers.Unsubscribe(page.point, v) { page.SetObserved(false) // Mark the page as not being observed } @@ -121,19 +154,14 @@ func (v *View[T]) Close() error { } // onUpdate occurs when a tile has updated. -func (v *View[T]) onUpdate(ev *Update[T]) { - if v.rect.Contains(ev.Point) { +func (v *View[S, T]) onUpdate(ev *Update[T]) { + if v.Viewport().Contains(ev.Point) { v.Inbox <- *ev // (copy) } } // ----------------------------------------------------------------------------- -// observer represents a tile update observer. -type observer[T comparable] interface { - onUpdate(*Update[T]) -} - // Pubsub represents a publish/subscribe layer for observers. type pubsub[T comparable] struct { m sync.Map @@ -146,8 +174,15 @@ func (p *pubsub[T]) Notify(page Point, ev *Update[T]) { } } +// Each iterates over each observer in a page +func (p *pubsub[T]) Each(page Point, fn func(sub Observer[T])) { + if v, ok := p.m.Load(page.Integer()); ok { + v.(*observers[T]).Each(fn) + } +} + // Subscribe registers an event listener on a system -func (p *pubsub[T]) Subscribe(at Point, sub observer[T]) bool { +func (p *pubsub[T]) Subscribe(at Point, sub Observer[T]) bool { if v, ok := p.m.Load(at.Integer()); ok { return v.(*observers[T]).Subscribe(sub) } @@ -158,7 +193,7 @@ func (p *pubsub[T]) Subscribe(at Point, sub observer[T]) bool { } // Unsubscribe deregisters an event listener from a system -func (p *pubsub[T]) Unsubscribe(at Point, sub observer[T]) bool { +func (p *pubsub[T]) Unsubscribe(at Point, sub Observer[T]) bool { if v, ok := p.m.Load(at.Integer()); ok { return v.(*observers[T]).Unsubscribe(sub) } @@ -171,34 +206,38 @@ func (p *pubsub[T]) Unsubscribe(at Point, sub observer[T]) bool { // a specific tile is updated. type observers[T comparable] struct { sync.Mutex - subs []observer[T] + subs []Observer[T] } // newObservers creates a new instance of an change observer. func newObservers[T comparable]() *observers[T] { return &observers[T]{ - subs: make([]observer[T], 0, 8), + subs: make([]Observer[T], 0, 8), } } // Notify notifies listeners of an update that happened. func (s *observers[T]) Notify(ev *Update[T]) { + s.Each(func(sub Observer[T]) { + sub.onUpdate(ev) + }) +} + +// Each iterates over each observer +func (s *observers[T]) Each(fn func(sub Observer[T])) { if s == nil { return } s.Lock() - subs := s.subs - s.Unlock() - - // Update every subscriber - for _, sub := range subs { - sub.onUpdate(ev) + defer s.Unlock() + for _, sub := range s.subs { + fn(sub) } } // Subscribe registers an event listener on a system -func (s *observers[T]) Subscribe(sub observer[T]) bool { +func (s *observers[T]) Subscribe(sub Observer[T]) bool { s.Lock() defer s.Unlock() s.subs = append(s.subs, sub) @@ -206,7 +245,7 @@ func (s *observers[T]) Subscribe(sub observer[T]) bool { } // Unsubscribe deregisters an event listener from a system -func (s *observers[T]) Unsubscribe(sub observer[T]) bool { +func (s *observers[T]) Unsubscribe(sub Observer[T]) bool { s.Lock() defer s.Unlock() diff --git a/view_test.go b/view_test.go index 7001896..98e4baa 100644 --- a/view_test.go +++ b/view_test.go @@ -17,7 +17,9 @@ BenchmarkView/move-24 16141 74408 ns/op 0 B/op 0 */ func BenchmarkView(b *testing.B) { m := mapFrom("300x300.png") - v := m.View(NewRect(100, 0, 200, 100), nil) + v := NewView[string, string](m, "view 1") + v.Resize(NewRect(100, 0, 200, 100), nil) + go func() { for range v.Inbox { } @@ -50,7 +52,8 @@ func TestView(t *testing.T) { // Create a new view c := counter(0) - v := m.View(NewRect(100, 0, 200, 100), c.count) + v := NewView[string, string](m, "view 1") + v.Resize(NewRect(100, 0, 200, 100), c.count) assert.NotNil(t, v) assert.Equal(t, 10000, int(c)) @@ -138,7 +141,9 @@ func TestStateUpdates(t *testing.T) { // Create a new view c := counter(0) - v := m.View(NewRect(0, 0, 10, 10), c.count) + v := NewView[string, string](m, "view 1") + v.Resize(NewRect(0, 0, 10, 10), c.count) + assert.NotNil(t, v) assert.Equal(t, 100, int(c)) @@ -192,25 +197,39 @@ func TestObservers_MoveIncremental(t *testing.T) { // Create a new view c := counter(0) - v := m.View(NewRect(10, 10, 12, 12), c.count) + v := NewView[string, string](m, "view 1") + v.Resize(NewRect(10, 10, 12, 12), c.count) + assert.NotNil(t, v) assert.Equal(t, 4, int(c)) assert.Equal(t, 9, countObservers(m)) const distance = 10 + + assert.Equal(t, 1, countObserversAt(m, 10, 10)) for i := 0; i < distance; i++ { v.MoveTo(East, 1, c.count) } + + assert.Equal(t, 0, countObserversAt(m, 10, 10)) for i := 0; i < distance; i++ { v.MoveTo(South, 1, c.count) } + + assert.Equal(t, 0, countObserversAt(m, 10, 10)) for i := 0; i < distance; i++ { v.MoveTo(West, 1, c.count) } + + assert.Equal(t, 0, countObserversAt(m, 10, 10)) for i := 0; i < distance; i++ { v.MoveTo(North, 1, c.count) } + // Start should have the observer attached + assert.Equal(t, 1, countObserversAt(m, 10, 10)) + assert.Equal(t, 0, countObserversAt(m, 100, 100)) + // Count the number of observers, should be the same as before assert.Equal(t, 9, countObservers(m)) assert.NoError(t, v.Close()) @@ -218,6 +237,14 @@ func TestObservers_MoveIncremental(t *testing.T) { // ---------------------------------- Mocks ---------------------------------- +func countObserversAt(m *Grid[string], x, y int16) (count int) { + start, _ := m.At(x, y) + start.Observers(func(view Observer[string]) { + count++ + }) + return count +} + func countObservers(m *Grid[string]) int { var observers int m.Each(func(p Point, t Tile[string]) { @@ -230,6 +257,14 @@ func countObservers(m *Grid[string]) int { type fakeView[T comparable] func(*Update[T]) +func (f fakeView[T]) Viewport() Rect { + return Rect{} +} + +func (f fakeView[T]) Resize(r Rect, fn func(Point, Tile[T])) { + // Do nothing +} + func (f fakeView[T]) onUpdate(e *Update[T]) { f(e) }