2021-09-20 10:13:43 +10:00
|
|
|
package engine
|
|
|
|
|
|
|
|
import (
|
2021-09-20 14:24:32 +10:00
|
|
|
"fmt"
|
2021-09-20 10:13:43 +10:00
|
|
|
"image"
|
2021-09-20 14:24:32 +10:00
|
|
|
"strings"
|
2021-09-20 10:13:43 +10:00
|
|
|
|
|
|
|
"drjosh.dev/gurgle/geom"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
|
|
)
|
|
|
|
|
2021-09-21 12:36:49 +10:00
|
|
|
var _ interface {
|
|
|
|
Drawer
|
|
|
|
DrawManager
|
|
|
|
Hider
|
|
|
|
Prepper
|
2021-09-21 14:42:18 +10:00
|
|
|
Registrar
|
2021-09-21 12:36:49 +10:00
|
|
|
Scanner
|
|
|
|
Updater
|
|
|
|
} = &DrawDAG{}
|
|
|
|
|
2021-09-20 12:18:03 +10:00
|
|
|
// DrawDAG is a DrawLayer that organises DrawBoxer descendants in a directed
|
|
|
|
// acyclic graph (DAG), in order to draw them according to ordering constraints.
|
2021-09-20 15:03:33 +10:00
|
|
|
// It combines a DAG with a spatial index used when updating vertices to reduce
|
|
|
|
// the number of tests between components.
|
2021-09-20 12:18:03 +10:00
|
|
|
type DrawDAG struct {
|
2021-09-21 14:42:18 +10:00
|
|
|
ChunkSize int
|
|
|
|
Components
|
2021-09-20 12:18:03 +10:00
|
|
|
Hides
|
|
|
|
|
2021-09-20 10:13:43 +10:00
|
|
|
*dag
|
2021-09-20 16:02:01 +10:00
|
|
|
boxCache map[DrawBoxer]geom.Box // used to find components that moved
|
2021-09-20 12:18:03 +10:00
|
|
|
chunks map[image.Point]drawerSet // chunk coord -> drawers with bounding rects intersecting chunk
|
|
|
|
chunksRev map[DrawBoxer]image.Rectangle // comopnent -> rectangle of chunk coords
|
2021-09-20 15:03:33 +10:00
|
|
|
game *Game
|
2021-09-20 10:13:43 +10:00
|
|
|
}
|
|
|
|
|
2021-09-20 12:18:03 +10:00
|
|
|
// Draw draws everything in the DAG in topological order.
|
2021-09-21 12:36:49 +10:00
|
|
|
func (d *DrawDAG) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
|
2021-09-20 12:18:03 +10:00
|
|
|
if d.Hidden() {
|
|
|
|
return
|
2021-09-20 10:13:43 +10:00
|
|
|
}
|
|
|
|
// Hiding a parent component should hide the child objects, and the
|
|
|
|
// transform applied to a child should be the cumulative transform of all
|
|
|
|
// parents as well.
|
|
|
|
// cache memoises the results for each component.
|
|
|
|
type state struct {
|
|
|
|
hidden bool
|
|
|
|
opts ebiten.DrawImageOptions
|
|
|
|
}
|
2021-09-20 12:18:03 +10:00
|
|
|
cache := map[interface{}]state{
|
|
|
|
d: {
|
|
|
|
hidden: false,
|
|
|
|
opts: *opts,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
// Draw everything in d.dag, where not hidden (itself or any parent)
|
2021-09-20 16:02:01 +10:00
|
|
|
// TODO: handle descendant DrawLayers
|
2021-09-21 15:17:59 +10:00
|
|
|
d.dag.topWalk(func(x Drawer) {
|
2021-09-20 10:13:43 +10:00
|
|
|
// Is d hidden itself?
|
|
|
|
if h, ok := x.(Hider); ok && h.Hidden() {
|
|
|
|
cache[x] = state{hidden: true}
|
|
|
|
return // skip drawing
|
|
|
|
}
|
2021-09-20 12:18:03 +10:00
|
|
|
// Walk up game tree to find the nearest state in cache.
|
2021-09-20 10:13:43 +10:00
|
|
|
var st state
|
|
|
|
stack := []interface{}{x}
|
2021-09-21 16:53:04 +10:00
|
|
|
for p := d.game.Parent(x); p != nil; p = d.game.Parent(p) {
|
2021-09-20 10:13:43 +10:00
|
|
|
if s, found := cache[p]; found {
|
|
|
|
st = s
|
|
|
|
break
|
|
|
|
}
|
|
|
|
stack = append(stack, p)
|
|
|
|
}
|
|
|
|
// Unwind the stack, accumulating state along the way.
|
|
|
|
for len(stack) > 0 {
|
|
|
|
l1 := len(stack) - 1
|
|
|
|
p := stack[l1]
|
|
|
|
stack = stack[:l1]
|
|
|
|
if h, ok := p.(Hider); ok {
|
|
|
|
st.hidden = st.hidden || h.Hidden()
|
|
|
|
}
|
|
|
|
if st.hidden {
|
|
|
|
cache[p] = state{hidden: true}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// p is not hidden, so compute its cumulative opts.
|
|
|
|
if tf, ok := p.(Transformer); ok {
|
|
|
|
st.opts = concatOpts(tf.Transform(), st.opts)
|
|
|
|
}
|
|
|
|
cache[p] = st
|
|
|
|
}
|
|
|
|
|
|
|
|
// Skip drawing if hidden.
|
|
|
|
if st.hidden {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
x.Draw(screen, &st.opts)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-09-21 12:36:49 +10:00
|
|
|
// exists to satisfy interface
|
|
|
|
func (DrawDAG) ManagesDrawingSubcomponents() {}
|
|
|
|
|
2021-09-20 16:02:01 +10:00
|
|
|
// Prepare adds all subcomponents to the DAG.
|
2021-09-20 12:18:03 +10:00
|
|
|
func (d *DrawDAG) Prepare(game *Game) error {
|
|
|
|
d.dag = newDAG()
|
2021-09-20 14:24:32 +10:00
|
|
|
d.boxCache = make(map[DrawBoxer]geom.Box)
|
2021-09-20 12:18:03 +10:00
|
|
|
d.chunks = make(map[image.Point]drawerSet)
|
|
|
|
d.chunksRev = make(map[DrawBoxer]image.Rectangle)
|
2021-09-20 15:03:33 +10:00
|
|
|
d.game = game
|
2021-09-20 12:18:03 +10:00
|
|
|
|
2021-09-20 15:52:44 +10:00
|
|
|
// Because Game.LoadAndPrepare calls Prepare in a post-order walk, all the
|
2021-09-21 14:42:18 +10:00
|
|
|
// descendants should be prepared, meaning BoundingBox (hence Register) is
|
|
|
|
// likely to be a safe call.
|
|
|
|
return d.Register(d, nil)
|
2021-09-20 10:13:43 +10:00
|
|
|
}
|
|
|
|
|
2021-09-21 13:00:05 +10:00
|
|
|
// Update checks for any changes to descendants, and updates its internal
|
|
|
|
// data structures accordingly.
|
2021-09-20 14:24:32 +10:00
|
|
|
func (d *DrawDAG) Update() error {
|
|
|
|
// Re-evaluate bounding boxes for all descendants. If a box has changed,
|
|
|
|
// fix up the edges by removing and re-adding the vertex.
|
2021-09-20 16:02:01 +10:00
|
|
|
// Thanks once again to postorder traversal, this happens after all
|
|
|
|
// descendant updates.
|
2021-09-20 14:24:32 +10:00
|
|
|
var readd []DrawBoxer
|
|
|
|
for db, bb := range d.boxCache {
|
|
|
|
nbb := db.BoundingBox()
|
|
|
|
if bb != nbb {
|
2021-09-21 14:42:18 +10:00
|
|
|
d.Unregister(db)
|
2021-09-20 14:24:32 +10:00
|
|
|
readd = append(readd, db)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, db := range readd {
|
2021-09-21 14:42:18 +10:00
|
|
|
d.Register(db, nil)
|
2021-09-20 14:24:32 +10:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-21 14:42:18 +10:00
|
|
|
// Register recursively registers compponent and all descendants that are
|
|
|
|
// DrawBoxers into internal data structures (the DAG, etc) unless they are
|
|
|
|
// descendants of a different DrawManager.
|
|
|
|
func (d *DrawDAG) Register(component, _ interface{}) error {
|
|
|
|
if db, ok := component.(DrawBoxer); ok {
|
|
|
|
d.registerOne(db)
|
|
|
|
}
|
|
|
|
if _, ok := component.(DrawManager); ok && component != d {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if sc, ok := component.(Scanner); ok {
|
|
|
|
for _, x := range sc.Scan() {
|
|
|
|
if err := d.Register(x, nil); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// registerOne adds component and any needed edges to the DAG and chunk map.
|
|
|
|
func (d *DrawDAG) registerOne(x DrawBoxer) {
|
2021-09-20 14:36:00 +10:00
|
|
|
// Ensure vertex is present
|
|
|
|
d.dag.addVertex(x)
|
|
|
|
|
2021-09-20 14:24:32 +10:00
|
|
|
// Update the box cache
|
2021-09-20 14:36:00 +10:00
|
|
|
xb := x.BoundingBox()
|
|
|
|
d.boxCache[x] = xb
|
2021-09-20 14:24:32 +10:00
|
|
|
|
2021-09-20 10:13:43 +10:00
|
|
|
// Update the reverse chunk map
|
2021-09-20 15:03:33 +10:00
|
|
|
xbr := xb.BoundingRect(d.game.Projection)
|
2021-09-20 10:13:43 +10:00
|
|
|
revr := image.Rectangle{
|
2021-09-20 14:36:00 +10:00
|
|
|
Min: xbr.Min.Div(d.ChunkSize),
|
|
|
|
Max: xbr.Max.Sub(image.Pt(1, 1)).Div(d.ChunkSize),
|
2021-09-20 10:13:43 +10:00
|
|
|
}
|
|
|
|
d.chunksRev[x] = revr
|
2021-09-20 14:24:32 +10:00
|
|
|
|
2021-09-20 10:13:43 +10:00
|
|
|
// Find possible edges between x and items in the overlapping cells.
|
|
|
|
// First, a set of all the items in those cells.
|
|
|
|
cand := make(drawerSet)
|
2021-09-20 12:18:03 +10:00
|
|
|
var p image.Point
|
|
|
|
for p.Y = revr.Min.Y; p.Y <= revr.Max.Y; p.Y++ {
|
|
|
|
for p.X = revr.Min.X; p.X <= revr.Max.X; p.X++ {
|
|
|
|
cell := d.chunks[p]
|
|
|
|
if cell == nil {
|
|
|
|
cell = make(drawerSet)
|
|
|
|
d.chunks[p] = cell
|
|
|
|
}
|
2021-09-20 10:13:43 +10:00
|
|
|
// Merge cell contents into cand
|
|
|
|
for c := range cell {
|
|
|
|
cand[c] = struct{}{}
|
|
|
|
}
|
|
|
|
// Add x to cell
|
|
|
|
cell[x] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Add edges between x and elements of cand
|
2021-09-20 15:03:33 +10:00
|
|
|
πsign := d.game.Projection.Sign()
|
2021-09-20 10:13:43 +10:00
|
|
|
for c := range cand {
|
|
|
|
y := c.(DrawBoxer)
|
2021-09-20 12:18:03 +10:00
|
|
|
// Bounding rectangle overlap test
|
|
|
|
// No overlap, no edge.
|
2021-09-20 15:03:33 +10:00
|
|
|
if ybr := y.BoundingBox().BoundingRect(d.game.Projection); !xbr.Overlaps(ybr) {
|
2021-09-20 10:13:43 +10:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
switch {
|
2021-09-20 12:18:03 +10:00
|
|
|
case drawOrderConstraint(x, y, πsign):
|
2021-09-20 10:13:43 +10:00
|
|
|
d.dag.addEdge(x, y)
|
2021-09-20 14:36:00 +10:00
|
|
|
case drawOrderConstraint(y, x, πsign):
|
|
|
|
d.dag.addEdge(y, x)
|
2021-09-20 10:13:43 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-21 14:42:18 +10:00
|
|
|
// Unregister unregisters the component and all subcomponents.
|
|
|
|
func (d *DrawDAG) Unregister(component interface{}) {
|
|
|
|
if db, ok := component.(DrawBoxer); ok {
|
|
|
|
d.unregisterOne(db)
|
2021-09-21 13:00:05 +10:00
|
|
|
}
|
2021-09-21 14:42:18 +10:00
|
|
|
if _, ok := component.(DrawManager); ok && component != d {
|
2021-09-21 13:00:05 +10:00
|
|
|
return
|
|
|
|
}
|
2021-09-21 14:42:18 +10:00
|
|
|
if sc, ok := component.(Scanner); ok {
|
2021-09-21 13:00:05 +10:00
|
|
|
for _, x := range sc.Scan() {
|
2021-09-21 14:42:18 +10:00
|
|
|
d.Unregister(x)
|
2021-09-21 13:00:05 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-21 14:42:18 +10:00
|
|
|
func (d *DrawDAG) unregisterOne(x DrawBoxer) {
|
2021-09-20 10:13:43 +10:00
|
|
|
// Remove from chunk map
|
|
|
|
revr := d.chunksRev[x]
|
|
|
|
for j := revr.Min.Y; j <= revr.Max.Y; j++ {
|
|
|
|
for i := revr.Min.X; i <= revr.Max.X; i++ {
|
|
|
|
delete(d.chunks[image.Pt(i, j)], x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Remove from reverse chunk map
|
|
|
|
delete(d.chunksRev, x)
|
2021-09-21 16:53:04 +10:00
|
|
|
// Remove from box cache
|
|
|
|
delete(d.boxCache, x)
|
2021-09-20 10:13:43 +10:00
|
|
|
// Remove from DAG
|
|
|
|
d.dag.removeVertex(x)
|
|
|
|
}
|
|
|
|
|
2021-09-20 12:18:03 +10:00
|
|
|
// drawOrderConstraint reports if there is a draw ordering constraint between u
|
|
|
|
// and v (where u must draw before v).
|
|
|
|
func drawOrderConstraint(u, v DrawBoxer, πsign image.Point) bool {
|
|
|
|
// Common logic for known interfaces (BoundingBoxer, ZPositioner), to
|
|
|
|
// simplify DrawOrderer implementations.
|
|
|
|
ub, vb := u.BoundingBox(), v.BoundingBox()
|
|
|
|
if ub.Min.Z >= vb.Max.Z { // u is in front of v
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if ub.Max.Z <= vb.Min.Z { // u is behind v
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if πsign.X != 0 {
|
|
|
|
if ub.Max.X*πsign.X <= vb.Min.X*πsign.X { // u is to the left of v
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if ub.Min.X*πsign.X >= vb.Max.X*πsign.X { // u is to the right of v
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if πsign.Y != 0 {
|
|
|
|
if ub.Max.Y*πsign.Y <= vb.Min.Y*πsign.Y { // u is above v
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if ub.Min.Y*πsign.Y >= vb.Max.Y*πsign.Y { // u is below v
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ask the components themselves if they have an opinion
|
|
|
|
if do, ok := u.(DrawOrderer); ok && do.DrawBefore(v) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if do, ok := v.(DrawOrderer); ok && do.DrawAfter(u) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// No relation
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-09-20 10:13:43 +10:00
|
|
|
type drawerSet map[Drawer]struct{}
|
|
|
|
|
2021-09-20 14:24:32 +10:00
|
|
|
func (s drawerSet) String() string {
|
|
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("{ ")
|
|
|
|
for x := range s {
|
|
|
|
fmt.Fprintf(&sb, "%v ", x)
|
|
|
|
}
|
|
|
|
sb.WriteString("}")
|
|
|
|
return sb.String()
|
|
|
|
}
|
|
|
|
|
2021-09-20 10:13:43 +10:00
|
|
|
type dag struct {
|
2021-09-20 14:24:32 +10:00
|
|
|
all drawerSet
|
2021-09-20 10:13:43 +10:00
|
|
|
in, out map[Drawer]drawerSet
|
|
|
|
}
|
|
|
|
|
|
|
|
func newDAG() *dag {
|
|
|
|
return &dag{
|
2021-09-20 14:24:32 +10:00
|
|
|
all: make(drawerSet),
|
2021-09-20 10:13:43 +10:00
|
|
|
in: make(map[Drawer]drawerSet),
|
|
|
|
out: make(map[Drawer]drawerSet),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-20 14:24:32 +10:00
|
|
|
func (d *dag) String() string {
|
|
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("digraph {\n")
|
|
|
|
for v, e := range d.out {
|
|
|
|
fmt.Fprintf(&sb, "%v -> %v\n", v, e)
|
|
|
|
}
|
|
|
|
sb.WriteString(" }\n")
|
|
|
|
return sb.String()
|
|
|
|
}
|
|
|
|
|
2021-09-20 10:13:43 +10:00
|
|
|
// addEdge adds the edge u-v in O(1).
|
|
|
|
func (d *dag) addEdge(u, v Drawer) {
|
2021-09-20 14:24:32 +10:00
|
|
|
d.all[u], d.all[v] = struct{}{}, struct{}{}
|
2021-09-20 10:13:43 +10:00
|
|
|
if d.in[v] == nil {
|
|
|
|
d.in[v] = make(drawerSet)
|
|
|
|
}
|
|
|
|
if d.out[u] == nil {
|
|
|
|
d.out[u] = make(drawerSet)
|
|
|
|
}
|
|
|
|
d.in[v][u] = struct{}{}
|
|
|
|
d.out[u][v] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// removeEdge removes the edge u-v in O(1).
|
|
|
|
func (d *dag) removeEdge(u, v Drawer) {
|
|
|
|
delete(d.in[v], u)
|
|
|
|
delete(d.out[u], v)
|
|
|
|
}
|
|
|
|
|
2021-09-20 14:36:00 +10:00
|
|
|
// addVertex ensures the vertex is present, even if there are no edges.
|
|
|
|
func (d *dag) addVertex(v Drawer) {
|
|
|
|
d.all[v] = struct{}{}
|
|
|
|
}
|
|
|
|
|
2021-09-20 10:13:43 +10:00
|
|
|
// removeVertex removes all in and out edges associated with v in O(degree(v)).
|
|
|
|
func (d *dag) removeVertex(v Drawer) {
|
|
|
|
for u := range d.in[v] {
|
2021-09-20 15:52:44 +10:00
|
|
|
d.removeEdge(u, v)
|
2021-09-20 10:13:43 +10:00
|
|
|
}
|
|
|
|
for w := range d.out[v] {
|
2021-09-20 15:52:44 +10:00
|
|
|
d.removeEdge(v, w)
|
2021-09-20 10:13:43 +10:00
|
|
|
}
|
2021-09-20 14:24:32 +10:00
|
|
|
delete(d.all, v)
|
2021-09-20 10:13:43 +10:00
|
|
|
}
|
|
|
|
|
2021-09-21 15:17:59 +10:00
|
|
|
// topWalk visits each vertex in topological order, in time O(|V| + |E|) and
|
2021-09-20 10:13:43 +10:00
|
|
|
// O(|V|) temporary memory.
|
2021-09-21 15:17:59 +10:00
|
|
|
func (d *dag) topWalk(visit func(Drawer)) {
|
|
|
|
// Count indegrees - indegree(v) = len(d.in[v]) for each vertex v.
|
2021-09-20 10:13:43 +10:00
|
|
|
// If indegree(v) = 0, enqueue. Total: O(|V|).
|
|
|
|
queue := make([]Drawer, 0, len(d.in))
|
|
|
|
indegree := make(map[Drawer]int)
|
2021-09-20 14:24:32 +10:00
|
|
|
for u := range d.all {
|
|
|
|
// NB: zero indegree vertices may be missing from d.in
|
|
|
|
e := d.in[u]
|
2021-09-20 10:13:43 +10:00
|
|
|
if len(e) == 0 {
|
|
|
|
queue = append(queue, u)
|
|
|
|
} else {
|
|
|
|
indegree[u] = len(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Visit every vertex (O(|V|)) and decrement indegrees for every out edge
|
|
|
|
// of each vertex visited (O(|E|)). Total: O(|V|+|E|).
|
|
|
|
for len(queue) > 0 {
|
|
|
|
u := queue[0]
|
|
|
|
visit(u)
|
|
|
|
queue = queue[1:]
|
|
|
|
|
|
|
|
// Decrement indegree for all out edges, and enqueue target if its
|
|
|
|
// indegree is now 0.
|
|
|
|
for v := range d.out[u] {
|
|
|
|
indegree[v]--
|
|
|
|
if indegree[v] == 0 {
|
|
|
|
queue = append(queue, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|