progress on draw ordering

This commit is contained in:
Josh Deprez 2021-09-10 17:18:20 +10:00
parent 550cb5ef73
commit bdb8e6775e
17 changed files with 182 additions and 76 deletions

View file

@ -7,7 +7,10 @@ import (
) )
// Ensure Actor satisfies interfaces. // Ensure Actor satisfies interfaces.
var _ Prepper = &Actor{} var _ interface {
BoundingBoxer
Prepper
} = &Actor{}
func init() { func init() {
gob.Register(&Actor{}) gob.Register(&Actor{})
@ -26,6 +29,7 @@ type Actor struct {
game *Game game *Game
} }
// BoundingBox returns the box Bounds.Add(Pos).
func (a *Actor) BoundingBox() geom.Box { func (a *Actor) BoundingBox() geom.Box {
return a.Bounds.Add(a.Pos) return a.Bounds.Add(a.Pos)
} }

View file

@ -26,7 +26,7 @@ type Billboard struct {
Hidden Hidden
Pos image.Point Pos image.Point
Src ImageRef Src ImageRef
ZOrder ZPosition
} }
// Draw draws the image. // Draw draws the image.

View file

@ -39,7 +39,7 @@ type Camera struct {
// for the bounds of the child component (if available). // for the bounds of the child component (if available).
func (c *Camera) PointAt(centre geom.Int3, zoom float64) { func (c *Camera) PointAt(centre geom.Int3, zoom float64) {
// Special sauce: if Child has a BoundingRect, make some adjustments // Special sauce: if Child has a BoundingRect, make some adjustments
bnd, ok := c.Child.(Bounder) bnd, ok := c.Child.(BoundingRecter)
if !ok { if !ok {
c.Centre = c.game.Projection.Project(centre) c.Centre = c.game.Projection.Project(centre)
c.Zoom = zoom c.Zoom = zoom

View file

@ -4,7 +4,6 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"image" "image"
"math"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/ebitenutil"
@ -41,9 +40,9 @@ 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)
} }
func (d *DebugToast) DrawOrder() float64 { func (DebugToast) DrawAfter(x Drawer) bool {
// Always draw on top // Always draw on top
return math.MaxFloat64 return x != Tombstone{}
} }
func (d *DebugToast) Toast(text string) { func (d *DebugToast) Toast(text string) {
@ -68,7 +67,7 @@ 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()))
} }
func (PerfDisplay) DrawOrder() float64 { func (PerfDisplay) DrawAfter(x Drawer) bool {
// Always draw on top // Always draw on top
return math.MaxFloat64 return x != Tombstone{}
} }

View file

@ -25,7 +25,7 @@ type Fill struct {
ID ID
Color color.Color Color color.Color
Hidden Hidden
ZOrder ZPosition
} }
func (f *Fill) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) { func (f *Fill) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {

View file

@ -7,7 +7,6 @@ import (
"image" "image"
"io/fs" "io/fs"
"log" "log"
"math"
"reflect" "reflect"
"sort" "sort"
"sync" "sync"
@ -180,7 +179,7 @@ func (g *Game) Update() error {
sort.Stable(g.drawList) sort.Stable(g.drawList)
// Truncate tombstones from the end. // Truncate tombstones from the end.
for i := len(g.drawList) - 1; i >= 0; i-- { for i := len(g.drawList) - 1; i >= 0; i-- {
if g.drawList[i] == (tombstone{}) { if g.drawList[i] == (Tombstone{}) {
g.drawList = g.drawList[:i] g.drawList = g.drawList[:i]
} }
} }
@ -408,7 +407,7 @@ func (g *Game) unregister(component interface{}) {
// unregister from g.drawList // unregister from g.drawList
for i, d := range g.drawList { for i, d := range g.drawList {
if d == component { if d == component {
g.drawList[i] = tombstone{} g.drawList[i] = Tombstone{}
} }
} }
@ -425,25 +424,23 @@ type abKey struct {
behaviour reflect.Type behaviour reflect.Type
} }
var _ Drawer = tombstone{} var _ Drawer = Tombstone{}
type tombstone struct{} type Tombstone struct{}
func (tombstone) Draw(*ebiten.Image, *ebiten.DrawImageOptions) {} func (Tombstone) Draw(*ebiten.Image, *ebiten.DrawImageOptions) {}
func (tombstone) DrawOrder() float64 { return math.Inf(1) } func (Tombstone) DrawAfter(x Drawer) bool {
return x != Tombstone{}
}
type drawList []Drawer type drawList []Drawer
func (d drawList) Less(i, j int) bool { func (d drawList) Less(i, j int) bool { return d[j].DrawAfter(d[i]) }
return d[i].DrawOrder() < d[j].DrawOrder() func (d drawList) Len() int { return len(d) }
} func (d drawList) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func (d drawList) Len() int { return len(d) } // ConcatOpts returns the combined options (as though a was applied and then b).
func (d drawList) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
// ConcatOpts returns the combined options (as though a was applied and then
// b).
func ConcatOpts(a, b ebiten.DrawImageOptions) ebiten.DrawImageOptions { func ConcatOpts(a, b ebiten.DrawImageOptions) ebiten.DrawImageOptions {
a.ColorM.Concat(b.ColorM) a.ColorM.Concat(b.ColorM)
a.GeoM.Concat(b.GeoM) a.GeoM.Concat(b.GeoM)

View file

@ -13,22 +13,24 @@ import (
var ( var (
// TypeOf(pointer to interface).Elem() is "idiomatic" - // TypeOf(pointer to interface).Elem() is "idiomatic" -
// see https://pkg.go.dev/reflect#example-TypeOf // see https://pkg.go.dev/reflect#example-TypeOf
BounderType = reflect.TypeOf((*Bounder)(nil)).Elem() BoundingRecterType = reflect.TypeOf((*BoundingRecter)(nil)).Elem()
ColliderType = reflect.TypeOf((*Collider)(nil)).Elem() BoundingBoxerType = reflect.TypeOf((*BoundingBoxer)(nil)).Elem()
DisablerType = reflect.TypeOf((*Disabler)(nil)).Elem() ColliderType = reflect.TypeOf((*Collider)(nil)).Elem()
DrawerType = reflect.TypeOf((*Drawer)(nil)).Elem() DisablerType = reflect.TypeOf((*Disabler)(nil)).Elem()
HiderType = reflect.TypeOf((*Hider)(nil)).Elem() DrawerType = reflect.TypeOf((*Drawer)(nil)).Elem()
IdentifierType = reflect.TypeOf((*Identifier)(nil)).Elem() HiderType = reflect.TypeOf((*Hider)(nil)).Elem()
LoaderType = reflect.TypeOf((*Loader)(nil)).Elem() IdentifierType = reflect.TypeOf((*Identifier)(nil)).Elem()
PrepperType = reflect.TypeOf((*Prepper)(nil)).Elem() LoaderType = reflect.TypeOf((*Loader)(nil)).Elem()
ScannerType = reflect.TypeOf((*Scanner)(nil)).Elem() PrepperType = reflect.TypeOf((*Prepper)(nil)).Elem()
SaverType = reflect.TypeOf((*Saver)(nil)).Elem() ScannerType = reflect.TypeOf((*Scanner)(nil)).Elem()
TransformerType = reflect.TypeOf((*Transformer)(nil)).Elem() SaverType = reflect.TypeOf((*Saver)(nil)).Elem()
UpdaterType = reflect.TypeOf((*Updater)(nil)).Elem() TransformerType = reflect.TypeOf((*Transformer)(nil)).Elem()
UpdaterType = reflect.TypeOf((*Updater)(nil)).Elem()
// Behaviours lists all the behaviours that can be queried with Game.Query. // Behaviours lists all the behaviours that can be queried with Game.Query.
Behaviours = []reflect.Type{ Behaviours = []reflect.Type{
BounderType, BoundingRecterType,
BoundingBoxerType,
ColliderType, ColliderType,
DisablerType, DisablerType,
DrawerType, DrawerType,
@ -43,11 +45,16 @@ var (
} }
) )
// Bounder components have a bounding rectangle. // BoundingRecter components have a bounding rectangle.
type Bounder interface { type BoundingRecter interface {
BoundingRect() image.Rectangle BoundingRect() image.Rectangle
} }
// BoundingBoxer components have a bounding box.
type BoundingBoxer interface {
BoundingBox() geom.Box
}
// Collider components have tangible form. // Collider components have tangible form.
type Collider interface { type Collider interface {
CollidesWith(geom.Box) bool CollidesWith(geom.Box) bool
@ -65,7 +72,7 @@ type Disabler interface {
// passed to Game.Register or returned from Scan). // passed to Game.Register or returned from Scan).
type Drawer interface { type Drawer interface {
Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions)
DrawOrder() float64 DrawAfter(x Drawer) bool
} }
// Hider components can be hidden. // Hider components can be hidden.

View file

@ -40,8 +40,22 @@ func (h *Hidden) Hide() { *h = true }
// Show sets h to false. // Show sets h to false.
func (h *Hidden) Show() { *h = false } func (h *Hidden) Show() { *h = false }
// ZOrder implements DrawOrder (in Drawer) directly (as a numeric value). // ZPosition implements DrawAfter and DrawPosition as a simple Z coordinate.
type ZOrder float64 type ZPosition int
// DrawOrder returns z. // DrawAfter reports if z > x.Z.
func (z ZOrder) DrawOrder() float64 { return float64(z) } func (z ZPosition) DrawAfter(x Drawer) bool {
switch d := x.(type) {
case BoundingBoxer:
return int(z) > d.BoundingBox().Max.Z
case zpositioner:
return z.zposition() > d.zposition()
}
return false
}
func (z ZPosition) zposition() int { return int(z) }
type zpositioner interface {
zposition() int
}

View file

@ -4,6 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"image" "image"
"log"
"drjosh.dev/gurgle/geom" "drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
@ -20,6 +21,7 @@ var (
} = &PrismMap{} } = &PrismMap{}
_ interface { _ interface {
BoundingBoxer
Drawer Drawer
Transformer Transformer
} = &Prism{} } = &Prism{}
@ -142,9 +144,64 @@ func (p *Prism) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
screen.DrawImage(p.m.Sheet.SubImage(p.Cell), opts) screen.DrawImage(p.m.Sheet.SubImage(p.Cell), opts)
} }
// DrawOrder returns the projected draw distance. func (p *Prism) logue(c Drawer, s string) {
func (p *Prism) DrawOrder() float64 { if p.pos.Y != -16 {
return p.m.game.Projection.DrawOrder(p.pos) return
}
if _, ok := c.(*Sprite); ok {
log.Print(s)
}
}
// DrawAfter reports if the prism should be drawn after x.
func (p *Prism) DrawAfter(x Drawer) bool {
pb := p.BoundingBox()
switch d := x.(type) {
case *Prism:
if p.pos.Z == d.pos.Z {
return p.pos.Y < d.pos.Y
}
return p.pos.Z > d.pos.Z
case BoundingBoxer:
xb := d.BoundingBox()
/*// No X overlap - no comparison needed
if pb.Max.X <= xb.Min.X || pb.Min.X >= xb.Max.X {
p.logue(x, "no x overlap")
return false
}*/
// Z ?
if pb.Min.Z >= xb.Max.Z { // p is unambiguously in front
p.logue(x, "prism unambiguously in front")
return true
}
if pb.Max.Z <= xb.Min.Z { // p is unambiguously behind
p.logue(x, "prism unambiguously behind")
return false
}
// Y ? (NB: up is negative)
if pb.Max.Y <= xb.Min.Y { // p is above
p.logue(x, "prism unambiguously above")
return true
}
if pb.Min.Y >= xb.Max.Y { // p is below
p.logue(x, "prism unambiguously below")
return false
}
// Try Z again
if pb.Min.Z+8 >= xb.Max.Z {
p.logue(x, "prism midsection after max Z")
return true
}
if pb.Min.Z+8 <= xb.Min.Z {
p.logue(x, "prism midsection before min Z")
return false
}
case zpositioner:
p.logue(x, "zpositioner test??")
return pb.Min.Z > int(d.zposition())
}
p.logue(x, "no tests at all???")
return false
} }
// Transform returns a translation by the projected position. // Transform returns a translation by the projected position.

View file

@ -40,6 +40,8 @@ func (g *Game) REPL(src io.Reader, dst io.Writer, assets fs.FS) error {
g.cmdHide(dst, argv) g.cmdHide(dst, argv)
case "show": case "show":
g.cmdShow(dst, argv) g.cmdShow(dst, argv)
case "print":
g.cmdPrint(dst, argv)
} }
fmt.Fprint(dst, prompt) fmt.Fprint(dst, prompt)
} }
@ -184,3 +186,11 @@ func (g *Game) cmdShow(dst io.Writer, argv []string) {
} }
h.Show() h.Show()
} }
func (g *Game) cmdPrint(dst io.Writer, argv []string) {
c := g.cmdutilComponentArg1(dst, argv)
if c == nil {
return
}
fmt.Fprintf(dst, "%#v\n", c)
}

View file

@ -17,7 +17,7 @@ var (
) )
type scener interface { type scener interface {
Bounder BoundingRecter
Disabler Disabler
Hider Hider
Identifier Identifier

View file

@ -10,6 +10,7 @@ import (
// Ensure Sprite satisfies interfaces. // Ensure Sprite satisfies interfaces.
var _ interface { var _ interface {
BoundingBoxer
Drawer Drawer
Scanner Scanner
Transformer Transformer
@ -30,14 +31,42 @@ type Sprite struct {
anim *Anim anim *Anim
} }
// BoundingBox forwards the call to Actor.
func (s *Sprite) BoundingBox() geom.Box { return s.Actor.BoundingBox() }
// Draw draws the current cell to the screen. // Draw draws the current cell to the screen.
func (s *Sprite) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) { func (s *Sprite) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
screen.DrawImage(s.Sheet.SubImage(s.anim.Cell()), opts) screen.DrawImage(s.Sheet.SubImage(s.anim.Cell()), opts)
} }
// DrawOrder returns the projected draw order. // DrawAfter reports if the sprite must be drawn after x.
func (s *Sprite) DrawOrder() float64 { func (s *Sprite) DrawAfter(x Drawer) bool {
return s.Actor.game.Projection.DrawOrder(s.Actor.Pos) sb := s.BoundingBox()
switch d := x.(type) {
case BoundingBoxer:
xb := d.BoundingBox()
/*// No X overlap - no comparison needed
if sb.Max.X <= xb.Min.X || sb.Min.X >= xb.Max.X {
return false
}*/
// Z ?
if sb.Min.Z >= xb.Max.Z { // s is unambiguously in front
return true
}
if sb.Max.Z <= xb.Min.Z { // s is unambiguously behind
return false
}
// Y ? (NB: up is negative)
if sb.Max.Y <= xb.Min.Y { // s is unambiguously above
return true
}
if sb.Min.Y >= xb.Max.Y { // s is unambiguously below
return false
}
case zpositioner:
return sb.Min.Z > int(d.zposition())
}
return false
} }
// Scan returns the Actor and the Sheet. // Scan returns the Actor and the Sheet.

View file

@ -44,7 +44,7 @@ type Tilemap struct {
Ersatz bool // disables collisions ("fake wall") Ersatz bool // disables collisions ("fake wall")
Offset image.Point // world coordinates Offset image.Point // world coordinates
Sheet Sheet Sheet Sheet
ZOrder ZPosition
} }
// CollidesWith implements Collider. // CollidesWith implements Collider.

View file

@ -92,7 +92,7 @@ type WallUnit struct {
Disabled Disabled
Hidden Hidden
Tile Tile // chooses which cell in wall.Sheet to draw Tile Tile // chooses which cell in wall.Sheet to draw
ZOrder ZPosition
pos image.Point // tilespace coordinates pos image.Point // tilespace coordinates
wall *Wall wall *Wall

View file

@ -15,23 +15,22 @@ func Level1() *engine.Scene {
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{ &engine.Fill{
ID: "bg_fill", ID: "bg_fill",
Color: color.Gray{100}, Color: color.Gray{100},
ZOrder: -1000, ZPosition: -1000,
}, },
&engine.Parallax{ &engine.Parallax{
CameraID: "game_camera", CameraID: "game_camera",
Child: &engine.Billboard{ Child: &engine.Billboard{
ID: "bg_image", ID: "bg_image",
ZOrder: -900, ZPosition: -900,
Pos: image.Pt(-160, -120), Pos: image.Pt(-160, -120),
Src: engine.ImageRef{Path: "assets/space.png"}, Src: engine.ImageRef{Path: "assets/space.png"},
}, },
Factor: 0.5, Factor: 0.5,
}, },
&engine.PrismMap{ &engine.PrismMap{
ID: "hexagons", ID: "hexagons",
//DrawOrderBias: image.Pt(0, -1), // draw higher Y after lower Y
PosToWorld: geom.IntMatrix3x4{ PosToWorld: geom.IntMatrix3x4{
// For each tile in the X direction, go right by 24 and // For each tile in the X direction, go right by 24 and
// forward by 8, etc // forward by 8, etc

View file

@ -54,9 +54,13 @@ func PolygonRectOverlap(convex []image.Point, rect image.Rectangle) bool {
if PolygonContains(convex, rect.Min) { if PolygonContains(convex, rect.Min) {
return true return true
} }
// Reduced Max (to the inclusive bound). // Reduced Max (to the inclusive bound).
rmax := rect.Max.Sub(image.Pt(1, 1)) rmax := rect.Max.Sub(image.Pt(1, 1))
// Since we went to the trouble of computing another point...
// TODO: this shouldn't be necessary
if PolygonContains(convex, rmax) {
return true
}
// Only remaining cases involve line intersection between the rect and poly // Only remaining cases involve line intersection between the rect and poly
// having eliminated the possibility that one is entirely within another. // having eliminated the possibility that one is entirely within another.

View file

@ -29,17 +29,3 @@ func (π IntProjection) Project(p Int3) image.Point {
} }
return q return q
} }
// DrawOrder computes a draw-order value for a point under this projection.
// Each projection has an implied camera angle - Z alone is insufficient to
// order things properly.
func (π IntProjection) DrawOrder(p Int3) float64 {
z := float64(p.Z)
if π.X != 0 {
z -= float64(p.X) / float64(π.X)
}
if π.Y != 0 {
z -= float64(p.Y) / float64(π.Y)
}
return z
}