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.
var _ Prepper = &Actor{}
var _ interface {
BoundingBoxer
Prepper
} = &Actor{}
func init() {
gob.Register(&Actor{})
@ -26,6 +29,7 @@ type Actor struct {
game *Game
}
// BoundingBox returns the box Bounds.Add(Pos).
func (a *Actor) BoundingBox() geom.Box {
return a.Bounds.Add(a.Pos)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,8 +40,22 @@ func (h *Hidden) Hide() { *h = true }
// Show sets h to false.
func (h *Hidden) Show() { *h = false }
// ZOrder implements DrawOrder (in Drawer) directly (as a numeric value).
type ZOrder float64
// ZPosition implements DrawAfter and DrawPosition as a simple Z coordinate.
type ZPosition int
// DrawOrder returns z.
func (z ZOrder) DrawOrder() float64 { return float64(z) }
// DrawAfter reports if z > x.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"
"fmt"
"image"
"log"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2"
@ -20,6 +21,7 @@ var (
} = &PrismMap{}
_ interface {
BoundingBoxer
Drawer
Transformer
} = &Prism{}
@ -142,9 +144,64 @@ func (p *Prism) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
screen.DrawImage(p.m.Sheet.SubImage(p.Cell), opts)
}
// DrawOrder returns the projected draw distance.
func (p *Prism) DrawOrder() float64 {
return p.m.game.Projection.DrawOrder(p.pos)
func (p *Prism) logue(c Drawer, s string) {
if p.pos.Y != -16 {
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.

View file

@ -40,6 +40,8 @@ func (g *Game) REPL(src io.Reader, dst io.Writer, assets fs.FS) error {
g.cmdHide(dst, argv)
case "show":
g.cmdShow(dst, argv)
case "print":
g.cmdPrint(dst, argv)
}
fmt.Fprint(dst, prompt)
}
@ -184,3 +186,11 @@ func (g *Game) cmdShow(dst io.Writer, argv []string) {
}
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 {
Bounder
BoundingRecter
Disabler
Hider
Identifier

View file

@ -10,6 +10,7 @@ import (
// Ensure Sprite satisfies interfaces.
var _ interface {
BoundingBoxer
Drawer
Scanner
Transformer
@ -30,14 +31,42 @@ type Sprite struct {
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.
func (s *Sprite) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
screen.DrawImage(s.Sheet.SubImage(s.anim.Cell()), opts)
}
// DrawOrder returns the projected draw order.
func (s *Sprite) DrawOrder() float64 {
return s.Actor.game.Projection.DrawOrder(s.Actor.Pos)
// DrawAfter reports if the sprite must be drawn after x.
func (s *Sprite) DrawAfter(x Drawer) bool {
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.

View file

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

View file

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

View file

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

View file

@ -54,9 +54,13 @@ func PolygonRectOverlap(convex []image.Point, rect image.Rectangle) bool {
if PolygonContains(convex, rect.Min) {
return true
}
// Reduced Max (to the inclusive bound).
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
// 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
}
// 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
}