Skip to content

Commit

Permalink
Add a way to iterate observers
Browse files Browse the repository at this point in the history
  • Loading branch information
kelindar committed Nov 10, 2024
1 parent cb2af96 commit 701df41
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 66 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
})

Expand Down
26 changes: 13 additions & 13 deletions grid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions point.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
129 changes: 84 additions & 45 deletions view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Expand All @@ -64,55 +92,60 @@ 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),
}, fn)
}

// 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
}
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -171,42 +206,46 @@ 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)
return len(s.subs) > 0 // At least one
}

// 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()

Expand Down
Loading

0 comments on commit 701df41

Please sign in to comment.