WIP: oh god oh fuck
This commit is contained in:
parent
169b7f343c
commit
89febffcea
8 changed files with 202 additions and 320 deletions
|
@ -40,19 +40,6 @@ func (d *DebugToast) Draw(screen *ebiten.Image, _ *ebiten.DrawImageOptions) {
|
||||||
ebitenutil.DebugPrintAt(screen, d.Text, d.Pos.X, d.Pos.Y)
|
ebitenutil.DebugPrintAt(screen, d.Text, d.Pos.X, d.Pos.Y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw last.
|
|
||||||
func (DebugToast) DrawAfter(x Drawer) bool {
|
|
||||||
switch x.(type) {
|
|
||||||
case *DebugToast, PerfDisplay:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (DebugToast) DrawBefore(x Drawer) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebugToast) String() string {
|
func (d *DebugToast) String() string {
|
||||||
return fmt.Sprintf("DebugToast@%v", d.Pos)
|
return fmt.Sprintf("DebugToast@%v", d.Pos)
|
||||||
}
|
}
|
||||||
|
@ -79,17 +66,4 @@ func (p PerfDisplay) Draw(screen *ebiten.Image, _ *ebiten.DrawImageOptions) {
|
||||||
ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS()))
|
ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw last.
|
|
||||||
func (PerfDisplay) DrawAfter(x Drawer) bool {
|
|
||||||
switch x.(type) {
|
|
||||||
case *DebugToast, PerfDisplay:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (PerfDisplay) DrawBefore(Drawer) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (PerfDisplay) String() string { return "PerfDisplay" }
|
func (PerfDisplay) String() string { return "PerfDisplay" }
|
||||||
|
|
|
@ -7,29 +7,27 @@ import (
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// drawDAG combines a DAG with a spatial index used when adding new vertices
|
// DrawDAG is a DrawLayer that organises DrawBoxer descendants in a directed
|
||||||
|
// acyclic graph (DAG), in order to draw them according to ordering constraints.
|
||||||
|
// It combines a DAG with a spatial index used when adding new vertices
|
||||||
// in order to reduce the number of tests between components.
|
// in order to reduce the number of tests between components.
|
||||||
type drawDAG struct {
|
type DrawDAG struct {
|
||||||
|
ChunkSize int
|
||||||
|
Components []interface{}
|
||||||
|
Hides
|
||||||
|
|
||||||
*dag
|
*dag
|
||||||
chunks map[image.Point]drawerSet
|
chunks map[image.Point]drawerSet // chunk coord -> drawers with bounding rects intersecting chunk
|
||||||
chunksRev map[DrawBoxer]image.Rectangle
|
chunksRev map[DrawBoxer]image.Rectangle // comopnent -> rectangle of chunk coords
|
||||||
chunkSize int
|
|
||||||
parent func(x interface{}) interface{}
|
parent func(x interface{}) interface{}
|
||||||
proj geom.Projector
|
proj geom.Projector
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDrawDAG(game *Game, chunkSize int, π geom.Projector) *drawDAG {
|
// Draw draws everything in the DAG in topological order.
|
||||||
return &drawDAG{
|
func (d *DrawDAG) DrawAll(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
|
||||||
dag: newDAG(),
|
if d.Hidden() {
|
||||||
chunks: make(map[image.Point]drawerSet), // chunk coord -> drawers with bounding rects intersecting chunk
|
return
|
||||||
chunksRev: make(map[DrawBoxer]image.Rectangle), // comopnent -> rectangle of chunk coords
|
|
||||||
chunkSize: chunkSize,
|
|
||||||
parent: game.Parent,
|
|
||||||
proj: π,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (d *drawDAG) Draw(screen *ebiten.Image) {
|
|
||||||
// Hiding a parent component should hide the child objects, and the
|
// Hiding a parent component should hide the child objects, and the
|
||||||
// transform applied to a child should be the cumulative transform of all
|
// transform applied to a child should be the cumulative transform of all
|
||||||
// parents as well.
|
// parents as well.
|
||||||
|
@ -38,18 +36,23 @@ func (d *drawDAG) Draw(screen *ebiten.Image) {
|
||||||
hidden bool
|
hidden bool
|
||||||
opts ebiten.DrawImageOptions
|
opts ebiten.DrawImageOptions
|
||||||
}
|
}
|
||||||
cache := make(map[interface{}]state)
|
cache := map[interface{}]state{
|
||||||
// Draw everything in g.drawList, where not hidden (itself or any parent)
|
d: {
|
||||||
|
hidden: false,
|
||||||
|
opts: *opts,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Draw everything in d.dag, where not hidden (itself or any parent)
|
||||||
d.dag.topIterate(func(x Drawer) {
|
d.dag.topIterate(func(x Drawer) {
|
||||||
// Is d hidden itself?
|
// Is d hidden itself?
|
||||||
if h, ok := x.(Hider); ok && h.Hidden() {
|
if h, ok := x.(Hider); ok && h.Hidden() {
|
||||||
cache[x] = state{hidden: true}
|
cache[x] = state{hidden: true}
|
||||||
return // skip drawing
|
return // skip drawing
|
||||||
}
|
}
|
||||||
// Walk up g.par to find the nearest state in accum.
|
// Walk up game tree to find the nearest state in cache.
|
||||||
var st state
|
var st state
|
||||||
stack := []interface{}{x}
|
stack := []interface{}{x}
|
||||||
for p := d.parent(x); p != nil; p = d.parent(p) {
|
for p := d.parent(x); ; p = d.parent(p) {
|
||||||
if s, found := cache[p]; found {
|
if s, found := cache[p]; found {
|
||||||
st = s
|
st = s
|
||||||
break
|
break
|
||||||
|
@ -83,27 +86,45 @@ func (d *drawDAG) Draw(screen *ebiten.Image) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type DrawBoxer interface {
|
func (d *DrawDAG) Prepare(game *Game) error {
|
||||||
Drawer
|
d.dag = newDAG()
|
||||||
BoundingBoxer
|
d.chunks = make(map[image.Point]drawerSet)
|
||||||
|
d.chunksRev = make(map[DrawBoxer]image.Rectangle)
|
||||||
|
d.parent = game.Parent
|
||||||
|
d.proj = game.Projection
|
||||||
|
|
||||||
|
// Load all descendants into the DAG
|
||||||
|
return PreorderWalk(d, func(c, _ interface{}) error {
|
||||||
|
if db, ok := c.(DrawBoxer); ok {
|
||||||
|
d.Add(db)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// add adds a Drawer and any needed edges to the DAG and chunk map.
|
func (d *DrawDAG) Scan() []interface{} { return d.Components }
|
||||||
func (d *drawDAG) add(x DrawBoxer) {
|
|
||||||
|
// Add adds a Drawer and any needed edges to the DAG and chunk map.
|
||||||
|
func (d *DrawDAG) Add(x DrawBoxer) {
|
||||||
πsign := d.proj.Sign()
|
πsign := d.proj.Sign()
|
||||||
br := x.BoundingBox().BoundingRect(d.proj)
|
br := x.BoundingBox().BoundingRect(d.proj)
|
||||||
// Update the reverse chunk map
|
// Update the reverse chunk map
|
||||||
revr := image.Rectangle{
|
revr := image.Rectangle{
|
||||||
Min: br.Min.Div(d.chunkSize),
|
Min: br.Min.Div(d.ChunkSize),
|
||||||
Max: br.Max.Sub(image.Pt(1, 1)).Div(d.chunkSize),
|
Max: br.Max.Sub(image.Pt(1, 1)).Div(d.ChunkSize),
|
||||||
}
|
}
|
||||||
d.chunksRev[x] = revr
|
d.chunksRev[x] = revr
|
||||||
// Find possible edges between x and items in the overlapping cells.
|
// Find possible edges between x and items in the overlapping cells.
|
||||||
// First, a set of all the items in those cells.
|
// First, a set of all the items in those cells.
|
||||||
cand := make(drawerSet)
|
cand := make(drawerSet)
|
||||||
for j := revr.Min.Y; j <= revr.Max.Y; j++ {
|
var p image.Point
|
||||||
for i := revr.Min.X; i <= revr.Max.X; i++ {
|
for p.Y = revr.Min.Y; p.Y <= revr.Max.Y; p.Y++ {
|
||||||
cell := d.chunks[image.Pt(i, j)]
|
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
|
||||||
|
}
|
||||||
// Merge cell contents into cand
|
// Merge cell contents into cand
|
||||||
for c := range cell {
|
for c := range cell {
|
||||||
cand[c] = struct{}{}
|
cand[c] = struct{}{}
|
||||||
|
@ -115,21 +136,22 @@ func (d *drawDAG) add(x DrawBoxer) {
|
||||||
// Add edges between x and elements of cand
|
// Add edges between x and elements of cand
|
||||||
for c := range cand {
|
for c := range cand {
|
||||||
y := c.(DrawBoxer)
|
y := c.(DrawBoxer)
|
||||||
// Bounding rectangle test
|
// Bounding rectangle overlap test
|
||||||
|
// No overlap, no edge.
|
||||||
if ybr := y.BoundingBox().BoundingRect(d.proj); !br.Overlaps(ybr) {
|
if ybr := y.BoundingBox().BoundingRect(d.proj); !br.Overlaps(ybr) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case edge(y, x, πsign):
|
case drawOrderConstraint(y, x, πsign):
|
||||||
d.dag.addEdge(y, x)
|
d.dag.addEdge(y, x)
|
||||||
case edge(x, y, πsign):
|
case drawOrderConstraint(x, y, πsign):
|
||||||
d.dag.addEdge(x, y)
|
d.dag.addEdge(x, y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove removes a Drawer and all associated edges and metadata.
|
// Remove removes a Drawer and all associated edges and metadata.
|
||||||
func (d *drawDAG) remove(x DrawBoxer) {
|
func (d *DrawDAG) Remove(x DrawBoxer) {
|
||||||
// Remove from chunk map
|
// Remove from chunk map
|
||||||
revr := d.chunksRev[x]
|
revr := d.chunksRev[x]
|
||||||
for j := revr.Min.Y; j <= revr.Max.Y; j++ {
|
for j := revr.Min.Y; j <= revr.Max.Y; j++ {
|
||||||
|
@ -143,6 +165,47 @@ func (d *drawDAG) remove(x DrawBoxer) {
|
||||||
d.dag.removeVertex(x)
|
d.dag.removeVertex(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
type drawerSet map[Drawer]struct{}
|
type drawerSet map[Drawer]struct{}
|
||||||
|
|
||||||
type dag struct {
|
type dag struct {
|
||||||
|
|
50
engine/drawdfs.go
Normal file
50
engine/drawdfs.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import "github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
// DrawDFS is a DrawLayer that does not add any structure. Components are
|
||||||
|
// drawn in the order in which they are encountered by a depth-first search
|
||||||
|
// through the game tree, without any extra sorting based on Z values or
|
||||||
|
// consideration for DrawOrderer).
|
||||||
|
type DrawDFS struct {
|
||||||
|
Components []interface{}
|
||||||
|
Hides
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DrawDFS) DrawAll(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
|
||||||
|
if d.Hidden() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, component := range d.Components {
|
||||||
|
d.draw(component, screen, *opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DrawDFS) Scan() []interface{} { return d.Components }
|
||||||
|
|
||||||
|
func (d *DrawDFS) draw(component interface{}, screen *ebiten.Image, opts ebiten.DrawImageOptions) {
|
||||||
|
// Hidden? stop drawing
|
||||||
|
if h, ok := component.(Hider); ok && h.Hidden() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Has a transform? apply to opts
|
||||||
|
if tf, ok := component.(Transformer); ok {
|
||||||
|
opts = concatOpts(tf.Transform(), opts)
|
||||||
|
}
|
||||||
|
// Is it a DrawLayer? draw all and return
|
||||||
|
if dl, ok := component.(DrawLayer); ok {
|
||||||
|
dl.DrawAll(screen, &opts)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Not a draw layer.
|
||||||
|
// Does it draw itself? Draw
|
||||||
|
if dr, ok := component.(Drawer); ok {
|
||||||
|
dr.Draw(screen, &opts)
|
||||||
|
}
|
||||||
|
// Has subcomponents? recurse
|
||||||
|
if sc, ok := component.(Scanner); ok {
|
||||||
|
for _, ch := range sc.Scan() {
|
||||||
|
d.draw(ch, screen, opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,152 +0,0 @@
|
||||||
package engine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
|
|
||||||
"drjosh.dev/gurgle/geom"
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Drawer = tombstone{}
|
|
||||||
|
|
||||||
type tombstone struct{}
|
|
||||||
|
|
||||||
func (tombstone) Draw(*ebiten.Image, *ebiten.DrawImageOptions) {}
|
|
||||||
|
|
||||||
func (tombstone) DrawAfter(x Drawer) bool { return x != tombstone{} }
|
|
||||||
func (tombstone) DrawBefore(Drawer) bool { return false }
|
|
||||||
|
|
||||||
func (tombstone) String() string { return "tombstone" }
|
|
||||||
|
|
||||||
type drawList struct {
|
|
||||||
list []Drawer
|
|
||||||
rev map[Drawer]int
|
|
||||||
}
|
|
||||||
|
|
||||||
// edge reports if there is a draw ordering constraint between u and v (where
|
|
||||||
// u draws before v).
|
|
||||||
func edge(u, v Drawer, πsign image.Point) bool {
|
|
||||||
// Common logic for known interfaces (BoundingBoxer, ZPositioner), to
|
|
||||||
// simplify DrawOrderer implementations.
|
|
||||||
switch u := u.(type) {
|
|
||||||
case BoundingBoxer:
|
|
||||||
ub := u.BoundingBox()
|
|
||||||
switch v := v.(type) {
|
|
||||||
case BoundingBoxer:
|
|
||||||
vb := 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case ZPositioner:
|
|
||||||
return ub.Max.Z < v.ZPos() // u is before v
|
|
||||||
}
|
|
||||||
|
|
||||||
case ZPositioner:
|
|
||||||
switch y := v.(type) {
|
|
||||||
case BoundingBoxer:
|
|
||||||
return u.ZPos() < y.BoundingBox().Min.Z
|
|
||||||
case ZPositioner:
|
|
||||||
return u.ZPos() < y.ZPos()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback case: 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Topological sort. Uses a projection π to flatten bounding boxes for
|
|
||||||
// overlap tests, in order to reduce edge count.
|
|
||||||
func (d *drawList) topsort(π geom.Projector) {
|
|
||||||
// Produce edge lists and count indegrees - O(|V|^2)
|
|
||||||
// TODO: optimise this
|
|
||||||
edges := make([][]int, len(d.list))
|
|
||||||
indegree := make([]int, len(d.list))
|
|
||||||
for i, u := range d.list {
|
|
||||||
if u == (tombstone{}) {
|
|
||||||
// Prevents processing this vertex later on
|
|
||||||
indegree[i] = -1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// If we can't get a more specific bounding rect, assume entire plane.
|
|
||||||
var ubr image.Rectangle
|
|
||||||
ub, brCheck := u.(BoundingBoxer)
|
|
||||||
if brCheck {
|
|
||||||
ubr = ub.BoundingBox().BoundingRect(π)
|
|
||||||
}
|
|
||||||
// For each possible neighbor...
|
|
||||||
for j, v := range d.list {
|
|
||||||
if i == j || v == (tombstone{}) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Does it have a bounding rect? Do overlap test.
|
|
||||||
if brCheck {
|
|
||||||
if vb, ok := v.(BoundingBoxer); ok {
|
|
||||||
if vbr := vb.BoundingBox().BoundingRect(π); !ubr.Overlaps(vbr) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the edge goes u->v, add it.
|
|
||||||
if edge(u, v, π.Sign()) {
|
|
||||||
edges[i] = append(edges[i], j)
|
|
||||||
indegree[j]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise queue with all the zero-indegree vertices
|
|
||||||
queue := make([]int, 0, len(d.list))
|
|
||||||
for i, n := range indegree {
|
|
||||||
if n == 0 {
|
|
||||||
queue = append(queue, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process into new list. O(|V| + |E|)
|
|
||||||
list := make([]Drawer, 0, len(d.list))
|
|
||||||
for len(queue) > 0 {
|
|
||||||
// Get front of queue.
|
|
||||||
i := queue[0]
|
|
||||||
queue = queue[1:]
|
|
||||||
// Add to output list.
|
|
||||||
d.rev[d.list[i]] = len(list)
|
|
||||||
list = append(list, d.list[i])
|
|
||||||
// Reduce indegree for all outgoing edges, enqueue if indegree now 0.
|
|
||||||
for _, j := range edges[i] {
|
|
||||||
indegree[j]--
|
|
||||||
if indegree[j] == 0 {
|
|
||||||
queue = append(queue, j)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Job done!
|
|
||||||
d.list = list
|
|
||||||
}
|
|
106
engine/game.go
106
engine/game.go
|
@ -13,11 +13,8 @@ import (
|
||||||
|
|
||||||
"drjosh.dev/gurgle/geom"
|
"drjosh.dev/gurgle/geom"
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const showDrawListSize = false
|
|
||||||
|
|
||||||
var _ interface {
|
var _ interface {
|
||||||
Disabler
|
Disabler
|
||||||
Hider
|
Hider
|
||||||
|
@ -42,15 +39,14 @@ type Game struct {
|
||||||
Disables
|
Disables
|
||||||
Hides
|
Hides
|
||||||
ScreenSize image.Point
|
ScreenSize image.Point
|
||||||
Root interface{} // typically a *Scene or SceneRef though
|
Roots []DrawLayer
|
||||||
Projection geom.Projector
|
Projection geom.Projector
|
||||||
VoxelScale geom.Float3
|
VoxelScale geom.Float3
|
||||||
|
|
||||||
dbmu sync.RWMutex
|
dbmu sync.RWMutex
|
||||||
byID map[string]Identifier // Named components by ID
|
byID map[string]Identifier // Named components by ID
|
||||||
byAB map[abKey]map[interface{}]struct{} // Ancestor/behaviour index
|
byAB map[abKey]map[interface{}]struct{} // Ancestor/behaviour index
|
||||||
drawList drawList // draw list :|
|
par map[interface{}]interface{} // par[x] is parent of x
|
||||||
par map[interface{}]interface{} // par[x] is parent of x
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw draws everything.
|
// Draw draws everything.
|
||||||
|
@ -59,64 +55,10 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hiding a parent component should hide the child objects, and the
|
// Make all draw managers draw, in order.
|
||||||
// transform applied to a child should be the cumulative transform of all
|
opts := &ebiten.DrawImageOptions{}
|
||||||
// parents as well.
|
for _, dm := range g.Roots {
|
||||||
// cache memoises the results for each component.
|
dm.DrawAll(screen, opts)
|
||||||
type state struct {
|
|
||||||
hidden bool
|
|
||||||
opts ebiten.DrawImageOptions
|
|
||||||
}
|
|
||||||
cache := map[interface{}]state{
|
|
||||||
g: {hidden: false},
|
|
||||||
}
|
|
||||||
// Draw everything in g.drawList, where not hidden (itself or any parent)
|
|
||||||
for _, d := range g.drawList.list {
|
|
||||||
// Is d hidden itself?
|
|
||||||
if h, ok := d.(Hider); ok && h.Hidden() {
|
|
||||||
cache[d] = state{hidden: true}
|
|
||||||
continue // skip drawing
|
|
||||||
}
|
|
||||||
// Walk up g.par to find the nearest state in accum.
|
|
||||||
var st state
|
|
||||||
stack := []interface{}{d}
|
|
||||||
for p := g.par[d]; ; p = g.par[p] {
|
|
||||||
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 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
d.Draw(screen, &st.opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if showDrawListSize {
|
|
||||||
// Infodump about draw list
|
|
||||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("len(drawList.list) = %d", len(g.drawList.list)), 0, 30)
|
|
||||||
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("len(drawList.rev) = %d", len(g.drawList.list)), 0, 45)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +126,7 @@ func (g *Game) Update() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the draw list (on every frame - this isn't as bad as it sounds)
|
// Sort the draw list (on every frame - this isn't as bad as it sounds)
|
||||||
g.drawList.topsort(g.Projection)
|
//g.drawList.topsort(g.Projection)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +161,13 @@ func (g *Game) Query(ancestorID string, behaviour reflect.Type) map[interface{}]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan implements Scanner.
|
// Scan implements Scanner.
|
||||||
func (g *Game) Scan() []interface{} { return []interface{}{g.Root} }
|
func (g *Game) Scan() []interface{} {
|
||||||
|
rs := make([]interface{}, 0, len(g.Roots))
|
||||||
|
for _, r := range g.Roots {
|
||||||
|
rs = append(rs, r)
|
||||||
|
}
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
// PreorderWalk calls visit with every component and its parent, reachable from
|
// PreorderWalk calls visit with every component and its parent, reachable from
|
||||||
// the given component via Scan, for as long as visit returns nil. The parent
|
// the given component via Scan, for as long as visit returns nil. The parent
|
||||||
|
@ -290,8 +238,6 @@ func (g *Game) LoadAndPrepare(assets fs.FS) error {
|
||||||
g.dbmu.Lock()
|
g.dbmu.Lock()
|
||||||
g.byID = make(map[string]Identifier)
|
g.byID = make(map[string]Identifier)
|
||||||
g.byAB = make(map[abKey]map[interface{}]struct{})
|
g.byAB = make(map[abKey]map[interface{}]struct{})
|
||||||
g.drawList.list = nil
|
|
||||||
g.drawList.rev = make(map[Drawer]int)
|
|
||||||
g.par = make(map[interface{}]interface{})
|
g.par = make(map[interface{}]interface{})
|
||||||
if err := PreorderWalk(g, g.register); err != nil {
|
if err := PreorderWalk(g, g.register); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -344,16 +290,6 @@ func (g *Game) register(component, parent interface{}) error {
|
||||||
g.par[component] = parent
|
g.par[component] = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
// register in g.drawList
|
|
||||||
if d, ok := component.(Drawer); ok {
|
|
||||||
if _, exists := g.drawList.rev[d]; exists {
|
|
||||||
// already registered
|
|
||||||
return fmt.Errorf("double registration of %v", d)
|
|
||||||
}
|
|
||||||
g.drawList.rev[d] = len(g.drawList.list)
|
|
||||||
g.drawList.list = append(g.drawList.list, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// register in g.byAB
|
// register in g.byAB
|
||||||
ct := reflect.TypeOf(component)
|
ct := reflect.TypeOf(component)
|
||||||
for _, b := range Behaviours {
|
for _, b := range Behaviours {
|
||||||
|
@ -414,14 +350,6 @@ func (g *Game) unregister(component interface{}) {
|
||||||
// unregister from g.par
|
// unregister from g.par
|
||||||
delete(g.par, component)
|
delete(g.par, component)
|
||||||
|
|
||||||
// unregister from g.drawList
|
|
||||||
if d, ok := component.(Drawer); ok {
|
|
||||||
if i, found := g.drawList.rev[d]; found {
|
|
||||||
g.drawList.list[i] = tombstone{}
|
|
||||||
delete(g.drawList.rev, d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// unregister from g.byID if needed
|
// unregister from g.byID if needed
|
||||||
if id, ok := component.(Identifier); ok && id.Ident() != "" {
|
if id, ok := component.(Identifier); ok && id.Ident() != "" {
|
||||||
delete(g.byID, id.Ident())
|
delete(g.byID, id.Ident())
|
||||||
|
|
|
@ -67,13 +67,27 @@ type Disabler interface {
|
||||||
Enable()
|
Enable()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drawer components can draw themselves. Draw is called often. Each component
|
// DrawLayer is a component responsible for calling Draw on all Drawer
|
||||||
// must call Draw on any internal components not known to the engine (i.e. not
|
// components beneath it, except those beneath another DrawLayer (it calls
|
||||||
// passed to Game.Register or returned from Scan).
|
// DrawAll on those).
|
||||||
|
type DrawLayer interface {
|
||||||
|
DrawAll(*ebiten.Image, *ebiten.DrawImageOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawer components can draw themselves. Draw is called often. Draw is not
|
||||||
|
// requierd to call Draw on subcomponents, if they are known to the engine
|
||||||
|
// (as part of a DrawManager).
|
||||||
type Drawer interface {
|
type Drawer interface {
|
||||||
Draw(*ebiten.Image, *ebiten.DrawImageOptions)
|
Draw(*ebiten.Image, *ebiten.DrawImageOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DrawBoxer components can both draw and have a bounding box (used for draw
|
||||||
|
// ordering).
|
||||||
|
type DrawBoxer interface {
|
||||||
|
BoundingBoxer
|
||||||
|
Drawer
|
||||||
|
}
|
||||||
|
|
||||||
// DrawOrderer components have more specific ideas about draw ordering than
|
// DrawOrderer components have more specific ideas about draw ordering than
|
||||||
// merely "my Z is bigger than yours".
|
// merely "my Z is bigger than yours".
|
||||||
type DrawOrderer interface {
|
type DrawOrderer interface {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
|
||||||
|
|
||||||
"drjosh.dev/gurgle/engine"
|
"drjosh.dev/gurgle/engine"
|
||||||
"drjosh.dev/gurgle/geom"
|
"drjosh.dev/gurgle/geom"
|
||||||
|
@ -14,11 +13,6 @@ func Level1() *engine.Scene {
|
||||||
ID: "level_1",
|
ID: "level_1",
|
||||||
Bounds: engine.Bounds(image.Rect(-32, -32, 320+32, 240+32)),
|
Bounds: engine.Bounds(image.Rect(-32, -32, 320+32, 240+32)),
|
||||||
Components: []interface{}{
|
Components: []interface{}{
|
||||||
&engine.Fill{
|
|
||||||
ID: "bg_fill",
|
|
||||||
Color: color.Gray{100},
|
|
||||||
ZPosition: -1000,
|
|
||||||
},
|
|
||||||
&engine.Parallax{
|
&engine.Parallax{
|
||||||
CameraID: "game_camera",
|
CameraID: "game_camera",
|
||||||
Child: &engine.Billboard{
|
Child: &engine.Billboard{
|
||||||
|
|
29
main.go
29
main.go
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
@ -61,20 +62,30 @@ func main() {
|
||||||
Y: 1,
|
Y: 1,
|
||||||
},
|
},
|
||||||
VoxelScale: geom.Float3{
|
VoxelScale: geom.Float3{
|
||||||
// Each voxel counts for this much Eucliden space.
|
// Each voxel counts for this much (Euclidean) space.
|
||||||
X: 1,
|
X: 1,
|
||||||
Y: 1,
|
Y: 1,
|
||||||
Z: math.Sqrt(3),
|
Z: math.Sqrt(3),
|
||||||
},
|
},
|
||||||
Root: &engine.Scene{
|
Roots: []engine.DrawLayer{
|
||||||
ID: "root",
|
&engine.DrawDFS{
|
||||||
Components: []interface{}{
|
Components: []interface{}{
|
||||||
&engine.Camera{
|
&engine.Fill{
|
||||||
ID: "game_camera",
|
ID: "bg_fill",
|
||||||
Child: lev1,
|
Color: color.Gray{100},
|
||||||
|
},
|
||||||
|
&engine.DrawDAG{
|
||||||
|
ChunkSize: 16,
|
||||||
|
Components: []interface{}{
|
||||||
|
&engine.Camera{
|
||||||
|
ID: "game_camera",
|
||||||
|
Child: lev1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&engine.DebugToast{ID: "toast", Pos: image.Pt(0, 15)},
|
||||||
|
engine.PerfDisplay{},
|
||||||
},
|
},
|
||||||
&engine.DebugToast{ID: "toast", Pos: image.Pt(0, 15)},
|
|
||||||
engine.PerfDisplay{},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue