refacteur

This commit is contained in:
Josh Deprez 2021-09-08 20:08:57 +10:00
parent dccd13b623
commit 6686c24cd6
21 changed files with 216 additions and 166 deletions

View file

@ -2,6 +2,8 @@ package engine
import ( import (
"encoding/gob" "encoding/gob"
"drjosh.dev/gurgle/geom"
) )
// Ensure Actor satisfies interfaces. // Ensure Actor satisfies interfaces.
@ -17,16 +19,16 @@ func init() {
// Actor handles basic movement. // Actor handles basic movement.
type Actor struct { type Actor struct {
CollisionDomain string // id of component to look for colliders inside of CollisionDomain string // id of component to look for colliders inside of
Pos, Size Int3 // in voxels; multiply by game.VoxelScale for regular Euclidean space Pos, Size geom.Int3 // in voxels; multiply by game.VoxelScale for regular Euclidean space
rem Float3 rem geom.Float3
game *Game game *Game
} }
// CollidesAt runs a collision test of the actor, supposing the actor is at a // CollidesAt runs a collision test of the actor, supposing the actor is at a
// given position (not necessarily a.Pos). // given position (not necessarily a.Pos).
func (a *Actor) CollidesAt(p Int3) bool { func (a *Actor) CollidesAt(p geom.Int3) bool {
bounds := Box{Min: p, Max: p.Add(a.Size)} bounds := geom.Box{Min: p, Max: p.Add(a.Size)}
for c := range a.game.Query(a.CollisionDomain, ColliderType) { for c := range a.game.Query(a.CollisionDomain, ColliderType) {
if c.(Collider).CollidesWith(bounds) { if c.(Collider).CollidesWith(bounds) {
return true return true
@ -46,7 +48,7 @@ func (a *Actor) MoveX(x float64, onCollide func()) {
return return
} }
a.rem.X -= float64(move) a.rem.X -= float64(move)
sign := sign(move) sign := geom.Sign(move)
for move != 0 { for move != 0 {
a.Pos.X += sign a.Pos.X += sign
move -= sign move -= sign
@ -70,7 +72,7 @@ func (a *Actor) MoveY(y float64, onCollide func()) {
return return
} }
a.rem.Y -= float64(move) a.rem.Y -= float64(move)
sign := sign(move) sign := geom.Sign(move)
for move != 0 { for move != 0 {
a.Pos.Y += sign a.Pos.Y += sign
move -= sign move -= sign
@ -94,7 +96,7 @@ func (a *Actor) MoveZ(z float64, onCollide func()) {
return return
} }
a.rem.Z -= float64(move) a.rem.Z -= float64(move)
sign := sign(move) sign := geom.Sign(move)
for move != 0 { for move != 0 {
a.Pos.Z += sign a.Pos.Z += sign
move -= sign move -= sign

View file

@ -4,6 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"image" "image"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -37,6 +38,6 @@ func (b *Billboard) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
func (b *Billboard) Scan() []interface{} { return []interface{}{&b.Src} } func (b *Billboard) Scan() []interface{} { return []interface{}{&b.Src} }
func (b *Billboard) Transform() (opts ebiten.DrawImageOptions) { func (b *Billboard) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(b.Pos)) opts.GeoM.Translate(geom.CFloat(b.Pos))
return opts return opts
} }

View file

@ -4,6 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"image" "image"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -36,7 +37,7 @@ type Camera struct {
// PointAt points the camera at a particular centre point and zoom, but adjusts // PointAt points the camera at a particular centre point and zoom, but adjusts
// for the bounds of the child component (if available). // for the bounds of the child component (if available).
func (c *Camera) PointAt(centre 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.(Bounder)
if !ok { if !ok {
@ -60,7 +61,7 @@ func (c *Camera) PointAt(centre Int3, zoom float64) {
// If the configured centre puts the camera out of bounds, move it. // If the configured centre puts the camera out of bounds, move it.
// Camera frame currently Rectangle{ centre ± (screen/(2*zoom)) }. // Camera frame currently Rectangle{ centre ± (screen/(2*zoom)) }.
sw2, sh2 := cfloat(c.game.ScreenSize.Div(2)) sw2, sh2 := geom.CFloat(c.game.ScreenSize.Div(2))
swz, shz := int(sw2/zoom), int(sh2/zoom) swz, shz := int(sw2/zoom), int(sh2/zoom)
cent := c.game.Projection.Project(centre) cent := c.game.Projection.Project(centre)
if cent.X-swz < br.Min.X { if cent.X-swz < br.Min.X {
@ -89,9 +90,9 @@ func (c *Camera) Scan() []interface{} { return []interface{}{c.Child} }
// Transform returns the camera transform. // Transform returns the camera transform.
func (c *Camera) Transform() (opts ebiten.DrawImageOptions) { func (c *Camera) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(c.Centre.Mul(-1))) opts.GeoM.Translate(geom.CFloat(c.Centre.Mul(-1)))
opts.GeoM.Scale(c.Zoom, c.Zoom) opts.GeoM.Scale(c.Zoom, c.Zoom)
opts.GeoM.Rotate(c.Rotation) opts.GeoM.Rotate(c.Rotation)
opts.GeoM.Translate(cfloat(c.game.ScreenSize.Div(2))) opts.GeoM.Translate(geom.CFloat(c.game.ScreenSize.Div(2)))
return opts return opts
} }

View file

@ -13,6 +13,7 @@ import (
"sync" "sync"
"time" "time"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -41,8 +42,8 @@ type Game struct {
Hidden Hidden
ScreenSize image.Point ScreenSize image.Point
Root interface{} // typically a *Scene or SceneRef though Root interface{} // typically a *Scene or SceneRef though
Projection IntProjection Projection geom.IntProjection
VoxelScale 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
@ -266,8 +267,8 @@ func postorderWalk(component, parent interface{}, visit func(component, parent i
// builds the component databases and then calls Prepare on every Preparer. // builds the component databases and then calls Prepare on every Preparer.
// LoadAndPrepare must be called before any calls to Component or Query. // LoadAndPrepare must be called before any calls to Component or Query.
func (g *Game) LoadAndPrepare(assets fs.FS) error { func (g *Game) LoadAndPrepare(assets fs.FS) error {
if g.VoxelScale == (Float3{}) { if g.VoxelScale == (geom.Float3{}) {
g.VoxelScale = Float3{1, 1, 1} g.VoxelScale = geom.Float3{1, 1, 1}
} }
// Load all the Loaders. // Load all the Loaders.

View file

@ -5,6 +5,7 @@ import (
"io/fs" "io/fs"
"reflect" "reflect"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -49,7 +50,7 @@ type Bounder interface {
// Collider components have tangible form. // Collider components have tangible form.
type Collider interface { type Collider interface {
CollidesWith(Box) bool CollidesWith(geom.Box) bool
} }
// Disabler components can be disabled. // Disabler components can be disabled.

View file

@ -45,102 +45,3 @@ type ZOrder int
// DrawOrder returns z as a int with 0 bias. // DrawOrder returns z as a int with 0 bias.
func (z ZOrder) DrawOrder() (int, int) { return int(z), 0 } func (z ZOrder) DrawOrder() (int, int) { return int(z), 0 }
// ---------- Some helpers for image.Point ----------
// cmul performs componentwise multiplication of two image.Points.
func cmul(p, q image.Point) image.Point {
return image.Point{p.X * q.X, p.Y * q.Y}
}
// cdiv performs componentwise division of two image.Points.
func cdiv(p, q image.Point) image.Point {
return image.Point{p.X / q.X, p.Y / q.Y}
}
// cfloat returns the components of an image.Point as two floats.
func cfloat(p image.Point) (x, y float64) {
return float64(p.X), float64(p.Y)
}
// dot returns the dot product of two image.Points.
func dot(p, q image.Point) int {
return p.X*q.X + p.Y*q.Y
}
// polygonContains reports if a polygon contains a point
func polygonContains(polygon []image.Point, p image.Point) bool {
for i, p1 := range polygon {
p2 := polygon[(i+1)%len(polygon)]
// ∆(p p1 p2) should have positive signed area
p1, p2 = p1.Sub(p), p2.Sub(p)
if p2.X*p1.Y-p1.X*p2.Y < 0 {
return false
}
}
return true
}
// polygonRectOverlap reports if a polygon overlaps a rectangle.
func polygonRectOverlap(polygon []image.Point, rect image.Rectangle) bool {
if polygon[0].In(rect) {
return true
}
// Check if any vertex of the rect is inside the polygon.
if polygonContains(polygon, rect.Min) {
return true
}
// Reduced Max (to the inclusive bound).
rmax := rect.Max.Sub(image.Pt(1, 1))
// Only remaining cases involve line intersection between the rect and poly
// having eliminated the possibility that one is entirely within another.
// Since rect is an axis-aligned rectangle, we only need vertical and
// horizontal line intersection tests.
// Walk each edge of polygon.
for i, p := range polygon {
q := polygon[(i+1)%len(polygon)]
// Pretend the edge is a rectangle. Exclude those that don't overlap.
if !rect.Overlaps(image.Rectangle{p, q}.Canon()) {
continue
}
d := q.Sub(p)
// If the polygon edge is not vertical, test left and right sides
if d.X != 0 {
if d.X < 0 {
d = d.Mul(-1)
}
min := (rect.Min.Y - p.Y) * d.X
max := (rect.Max.Y - p.Y) * d.X
// Test left side of rect
if t := (rect.Min.X - p.X) * d.Y; min <= t && t < max {
return true
}
// Test right side of rect
if t := (rmax.X - p.X) * d.Y; min <= t && t < max {
return true
}
}
// If the polygon edge is not horizontal, test the top and bottom sides
if d.Y != 0 {
if d.Y < 0 {
d = d.Mul(-1)
}
min := (rect.Min.X - p.X) * d.Y
max := (rect.Max.X - p.X) * d.Y
// Test top side of rect
if t := (rect.Min.Y - p.Y) * d.X; min <= t && t < max {
return true
}
// Test bottom side of rect
if t := (rmax.Y - p.Y) * d.X; min <= t && t < max {
return true
}
}
}
return false
}

View file

@ -4,6 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -42,7 +43,7 @@ func (p *Parallax) Scan() []interface{} { return []interface{}{p.Child} }
// Transform returns a GeoM translation of Factor * camera.Centre. // Transform returns a GeoM translation of Factor * camera.Centre.
func (p *Parallax) Transform() (opts ebiten.DrawImageOptions) { func (p *Parallax) Transform() (opts ebiten.DrawImageOptions) {
x, y := cfloat(p.camera.Centre) x, y := geom.CFloat(p.camera.Centre)
opts.GeoM.Translate(x*p.Factor, y*p.Factor) opts.GeoM.Translate(x*p.Factor, y*p.Factor)
return opts return opts
} }

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"image" "image"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -35,19 +36,19 @@ type PrismMap struct {
Disabled Disabled
Hidden Hidden
Ersatz bool Ersatz bool
Map map[Int3]*Prism // pos -> prism Map map[geom.Int3]*Prism // pos -> prism
DrawOrderBias image.Point // dot with pos.XY() = bias value DrawOrderBias image.Point // dot with pos.XY() = bias value
DrawOffset image.Point // offset applies to whole map DrawOffset image.Point // offset applies to whole map
PosToWorld IntMatrix3x4 // p.pos -> world voxelspace PosToWorld geom.IntMatrix3x4 // p.pos -> world voxelspace
PrismSize Int3 // in world voxelspace units PrismSize geom.Int3 // in world voxelspace units
PrismTop []image.Point // polygon vertices anticlockwise, Y means Z PrismTop []image.Point // polygon vertices anticlockwise, Y means Z
Sheet Sheet Sheet Sheet
game *Game game *Game
pwinverse RatMatrix3 pwinverse geom.RatMatrix3
} }
func (m *PrismMap) CollidesWith(b Box) bool { func (m *PrismMap) CollidesWith(b geom.Box) bool {
if m.Ersatz { if m.Ersatz {
return false return false
} }
@ -65,10 +66,10 @@ func (m *PrismMap) CollidesWith(b Box) bool {
rb = rb.Canon() // inverse might flip the corners around... rb = rb.Canon() // inverse might flip the corners around...
// Check neighboring prisms too because there's a fencepost somewhere here // Check neighboring prisms too because there's a fencepost somewhere here
rb.Min = rb.Min.Sub(Int3{1, 1, 1}) rb.Min = rb.Min.Sub(geom.Int3{1, 1, 1})
rb.Max = rb.Max.Add(Int3{1, 1, 1}) rb.Max = rb.Max.Add(geom.Int3{1, 1, 1})
var pp Int3 var pp geom.Int3
for pp.Z = rb.Min.Z; pp.Z <= rb.Max.Z; pp.Z++ { for pp.Z = rb.Min.Z; pp.Z <= rb.Max.Z; pp.Z++ {
for pp.Y = rb.Min.Y; pp.Y <= rb.Max.Y; pp.Y++ { for pp.Y = rb.Min.Y; pp.Y <= rb.Max.Y; pp.Y++ {
for pp.X = rb.Min.X; pp.X <= rb.Max.X; pp.X++ { for pp.X = rb.Min.X; pp.X <= rb.Max.X; pp.X++ {
@ -78,13 +79,13 @@ func (m *PrismMap) CollidesWith(b Box) bool {
} }
// Map it back to worldspace to get a bounding box for the prism // Map it back to worldspace to get a bounding box for the prism
wp := m.PosToWorld.Apply(pp) wp := m.PosToWorld.Apply(pp)
cb := Box{Min: wp, Max: wp.Add(m.PrismSize)} cb := geom.Box{Min: wp, Max: wp.Add(m.PrismSize)}
if !b.Overlaps(cb) { if !b.Overlaps(cb) {
continue continue
} }
// Take into account the prism shape // Take into account the prism shape
r := b.XZ().Sub(wp.XZ()) r := b.XZ().Sub(wp.XZ())
if polygonRectOverlap(m.PrismTop, r) { if geom.PolygonRectOverlap(m.PrismTop, r) {
return true return true
} }
} }
@ -102,7 +103,7 @@ func (m *PrismMap) CollidesWith(b Box) bool {
} }
// Take into account the prism shape // Take into account the prism shape
r := b.XZ().Sub(wp.XZ()) r := b.XZ().Sub(wp.XZ())
if polygonRectOverlap(m.PrismTop, r) { if geom.PolygonRectOverlap(m.PrismTop, r) {
return true return true
} }
} }
@ -134,14 +135,14 @@ func (m *PrismMap) Scan() []interface{} {
} }
func (m *PrismMap) Transform() (opts ebiten.DrawImageOptions) { func (m *PrismMap) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(m.DrawOffset)) opts.GeoM.Translate(geom.CFloat(m.DrawOffset))
return opts return opts
} }
type Prism struct { type Prism struct {
Cell int Cell int
pos Int3 pos geom.Int3
m *PrismMap m *PrismMap
} }
@ -151,11 +152,11 @@ func (p *Prism) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
func (p *Prism) DrawOrder() (int, int) { func (p *Prism) DrawOrder() (int, int) {
return p.m.PosToWorld.Apply(p.pos).Z, return p.m.PosToWorld.Apply(p.pos).Z,
dot(p.pos.XY(), p.m.DrawOrderBias) geom.Dot(p.pos.XY(), p.m.DrawOrderBias)
} }
func (p *Prism) Transform() (opts ebiten.DrawImageOptions) { func (p *Prism) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat( opts.GeoM.Translate(geom.CFloat(
p.m.game.Projection.Project(p.m.PosToWorld.Apply(p.pos)), p.m.game.Projection.Project(p.m.PosToWorld.Apply(p.pos)),
)) ))
return opts return opts

View file

@ -3,6 +3,7 @@ package engine
import ( import (
"image" "image"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -50,7 +51,7 @@ func (s *Sheet) Scan() []interface{} { return []interface{}{&s.Src} }
// SubImage returns an *ebiten.Image corresponding to the given cell index. // SubImage returns an *ebiten.Image corresponding to the given cell index.
func (s *Sheet) SubImage(i int) *ebiten.Image { func (s *Sheet) SubImage(i int) *ebiten.Image {
p := cmul(image.Pt(i%s.w, i/s.w), s.CellSize) p := geom.CMul(image.Pt(i%s.w, i/s.w), s.CellSize)
r := image.Rectangle{p, p.Add(s.CellSize)} r := image.Rectangle{p, p.Add(s.CellSize)}
return s.Src.Image().SubImage(r).(*ebiten.Image) return s.Src.Image().SubImage(r).(*ebiten.Image)
} }

View file

@ -2,6 +2,8 @@ package engine
import ( import (
"encoding/gob" "encoding/gob"
"drjosh.dev/gurgle/geom"
) )
var _ Collider = SolidRect{} var _ Collider = SolidRect{}
@ -12,9 +14,9 @@ func init() {
type SolidRect struct { type SolidRect struct {
ID ID
Box geom.Box
} }
func (s SolidRect) CollidesWith(r Box) bool { func (s SolidRect) CollidesWith(r geom.Box) bool {
return s.Box.Overlaps(r) return s.Box.Overlaps(r)
} }

View file

@ -4,6 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"image" "image"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -58,7 +59,7 @@ func (s *Sprite) SetAnim(a *Anim) {
// Transform returns a translation by the DrawOffset and Actor.Pos projected // Transform returns a translation by the DrawOffset and Actor.Pos projected
func (s *Sprite) Transform() (opts ebiten.DrawImageOptions) { func (s *Sprite) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat( opts.GeoM.Translate(geom.CFloat(
// Reaching into Actor for a reference to Game so I don't have to // Reaching into Actor for a reference to Game so I don't have to
// implement Prepare in this file, but writing this long comment // implement Prepare in this file, but writing this long comment
// providing exposition... // providing exposition...

View file

@ -6,6 +6,7 @@ import (
"image" "image"
"io/fs" "io/fs"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -47,15 +48,15 @@ type Tilemap struct {
} }
// CollidesWith implements Collider. // CollidesWith implements Collider.
func (t *Tilemap) CollidesWith(b Box) bool { func (t *Tilemap) CollidesWith(b geom.Box) bool {
if t.Ersatz { if t.Ersatz {
return false return false
} }
// Probe the map at all tilespace coordinates overlapping the rect. // Probe the map at all tilespace coordinates overlapping the rect.
r := b.XY().Sub(t.Offset) // TODO: pretend tilemap is a plane in 3D? r := b.XY().Sub(t.Offset) // TODO: pretend tilemap is a plane in 3D?
min := cdiv(r.Min, t.Sheet.CellSize) min := geom.CDiv(r.Min, t.Sheet.CellSize)
max := cdiv(r.Max.Sub(image.Pt(1, 1)), t.Sheet.CellSize) // NB: fencepost max := geom.CDiv(r.Max.Sub(image.Pt(1, 1)), t.Sheet.CellSize) // NB: fencepost
for j := min.Y; j <= max.Y; j++ { for j := min.Y; j <= max.Y; j++ {
for i := min.X; i <= max.X; i++ { for i := min.X; i <= max.X; i++ {
@ -74,10 +75,10 @@ func (t *Tilemap) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
if tile == nil { if tile == nil {
continue continue
} }
var geom ebiten.GeoM var mat ebiten.GeoM
geom.Translate(cfloat(cmul(p, t.Sheet.CellSize))) mat.Translate(geom.CFloat(geom.CMul(p, t.Sheet.CellSize)))
geom.Concat(og) mat.Concat(og)
opts.GeoM = geom opts.GeoM = mat
src := t.Sheet.SubImage(tile.Cell()) src := t.Sheet.SubImage(tile.Cell())
screen.DrawImage(src, opts) screen.DrawImage(src, opts)
@ -111,24 +112,24 @@ func (t *Tilemap) Scan() []interface{} {
// Transform returns a translation by t.Offset. // Transform returns a translation by t.Offset.
func (t *Tilemap) Transform() (opts ebiten.DrawImageOptions) { func (t *Tilemap) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(t.Offset)) opts.GeoM.Translate(geom.CFloat(t.Offset))
return opts return opts
} }
// TileAt returns the tile present at the given world coordinate. // TileAt returns the tile present at the given world coordinate.
func (t *Tilemap) TileAt(wc image.Point) Tile { func (t *Tilemap) TileAt(wc image.Point) Tile {
return t.Map[cdiv(wc.Sub(t.Offset), t.Sheet.CellSize)] return t.Map[geom.CDiv(wc.Sub(t.Offset), t.Sheet.CellSize)]
} }
// SetTileAt sets the tile at the given world coordinate. // SetTileAt sets the tile at the given world coordinate.
func (t *Tilemap) SetTileAt(wc image.Point, tile Tile) { func (t *Tilemap) SetTileAt(wc image.Point, tile Tile) {
t.Map[cdiv(wc.Sub(t.Offset), t.Sheet.CellSize)] = tile t.Map[geom.CDiv(wc.Sub(t.Offset), t.Sheet.CellSize)] = tile
} }
// TileBounds returns a rectangle describing the tile boundary for the tile // TileBounds returns a rectangle describing the tile boundary for the tile
// at the given world coordinate. // at the given world coordinate.
func (t *Tilemap) TileBounds(wc image.Point) image.Rectangle { func (t *Tilemap) TileBounds(wc image.Point) image.Rectangle {
p := cmul(cdiv(wc.Sub(t.Offset), t.Sheet.CellSize), t.Sheet.CellSize).Add(t.Offset) p := geom.CMul(geom.CDiv(wc.Sub(t.Offset), t.Sheet.CellSize), t.Sheet.CellSize).Add(t.Offset)
return image.Rectangle{p, p.Add(t.Sheet.CellSize)} return image.Rectangle{p, p.Add(t.Sheet.CellSize)}
} }

View file

@ -3,6 +3,7 @@ package engine
import ( import (
"image" "image"
"drjosh.dev/gurgle/geom"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -39,15 +40,15 @@ type Wall struct {
} }
// CollidesWith implements a tilerange collosion check, similar to Tilemap. // CollidesWith implements a tilerange collosion check, similar to Tilemap.
func (w *Wall) CollidesWith(b Box) bool { func (w *Wall) CollidesWith(b geom.Box) bool {
if w.Ersatz { if w.Ersatz {
return false return false
} }
// Probe the map at all tilespace coordinates overlapping the rect. // Probe the map at all tilespace coordinates overlapping the rect.
r := b.XY().Sub(w.Offset) r := b.XY().Sub(w.Offset)
min := cdiv(r.Min, w.UnitSize) min := geom.CDiv(r.Min, w.UnitSize)
max := cdiv(r.Max.Sub(image.Pt(1, 1)), w.UnitSize) // NB: fencepost max := geom.CDiv(r.Max.Sub(image.Pt(1, 1)), w.UnitSize) // NB: fencepost
for j := min.Y; j <= max.Y; j++ { for j := min.Y; j <= max.Y; j++ {
for i := min.X; i <= max.X; i++ { for i := min.X; i <= max.X; i++ {
@ -81,7 +82,7 @@ func (w *Wall) Prepare(*Game) error {
// Transform returns a GeoM translation by Offset. // Transform returns a GeoM translation by Offset.
func (w *Wall) Transform() (opts ebiten.DrawImageOptions) { func (w *Wall) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat(w.Offset)) opts.GeoM.Translate(geom.CFloat(w.Offset))
return opts return opts
} }
@ -106,8 +107,8 @@ func (u *WallUnit) Draw(screen *ebiten.Image, opts *ebiten.DrawImageOptions) {
func (u *WallUnit) Scan() []interface{} { return []interface{}{u.Tile} } func (u *WallUnit) Scan() []interface{} { return []interface{}{u.Tile} }
func (u *WallUnit) Transform() (opts ebiten.DrawImageOptions) { func (u *WallUnit) Transform() (opts ebiten.DrawImageOptions) {
opts.GeoM.Translate(cfloat( opts.GeoM.Translate(geom.CFloat(
cmul(u.pos, u.wall.UnitSize).Add(u.wall.UnitOffset), geom.CMul(u.pos, u.wall.UnitSize).Add(u.wall.UnitOffset),
)) ))
return opts return opts
} }

View file

@ -1,4 +1,4 @@
package engine package geom
import "image" import "image"

View file

@ -1,4 +1,4 @@
package engine package geom
import ( import (
"errors" "errors"

25
geom/misc.go Normal file
View file

@ -0,0 +1,25 @@
package geom
import "image"
// ---------- Some helpers for image.Point ----------
// CMul performs componentwise multiplication of two image.Points.
func CMul(p, q image.Point) image.Point {
return image.Point{p.X * q.X, p.Y * q.Y}
}
// CDiv performs componentwise division of two image.Points.
func CDiv(p, q image.Point) image.Point {
return image.Point{p.X / q.X, p.Y / q.Y}
}
// CFloat returns the components of an image.Point as two floats.
func CFloat(p image.Point) (x, y float64) {
return float64(p.X), float64(p.Y)
}
// Dot returns the Dot product of two image.Points.
func Dot(p, q image.Point) int {
return p.X*q.X + p.Y*q.Y
}

View file

@ -1,4 +1,4 @@
package engine package geom
import ( import (
"fmt" "fmt"
@ -73,7 +73,7 @@ func (p Int3) Coord() (x, y, z int) {
// Sign returns a sign vector. // Sign returns a sign vector.
func (p Int3) Sign() Int3 { func (p Int3) Sign() Int3 {
return Int3{sign(p.X), sign(p.Y), sign(p.Z)} return Int3{Sign(p.X), Sign(p.Y), Sign(p.Z)}
} }
// Dot returns the dot product of the two vectors. // Dot returns the dot product of the two vectors.
@ -81,7 +81,8 @@ func (p Int3) Dot(q Int3) int {
return p.X*q.X + p.Y*q.Y + p.Z*q.Z return p.X*q.X + p.Y*q.Y + p.Z*q.Z
} }
func sign(m int) int { // Sign returns the sign of the int (-1, 0, or 1).
func Sign(m int) int {
if m == 0 { if m == 0 {
return 0 return 0
} }
@ -91,7 +92,8 @@ func sign(m int) int {
return 1 return 1
} }
func signf(m float64) float64 { // FSign returns the sign of the float64 (-1, 0, or 1).
func FSign(m float64) float64 {
if m == 0 { if m == 0 {
return 0 return 0
} }
@ -153,7 +155,7 @@ func (p Float3) Coord() (x, y, z float64) {
// Sign returns a sign vector. // Sign returns a sign vector.
func (p Float3) Sign() Float3 { func (p Float3) Sign() Float3 {
return Float3{signf(p.X), signf(p.Y), signf(p.Z)} return Float3{FSign(p.X), FSign(p.Y), FSign(p.Z)}
} }
// Dot returns the dot product of the two vectors. // Dot returns the dot product of the two vectors.

80
geom/polygon.go Normal file
View file

@ -0,0 +1,80 @@
package geom
import "image"
// PolygonContains reports if a polygon contains a point
func PolygonContains(polygon []image.Point, p image.Point) bool {
for i, p1 := range polygon {
p2 := polygon[(i+1)%len(polygon)]
// ∆(p p1 p2) should have positive signed area
p1, p2 = p1.Sub(p), p2.Sub(p)
if p2.X*p1.Y-p1.X*p2.Y < 0 {
return false
}
}
return true
}
// PolygonRectOverlap reports if a polygon overlaps a rectangle.
func PolygonRectOverlap(polygon []image.Point, rect image.Rectangle) bool {
if polygon[0].In(rect) {
return true
}
// Check if any vertex of the rect is inside the polygon.
if PolygonContains(polygon, rect.Min) {
return true
}
// Reduced Max (to the inclusive bound).
rmax := rect.Max.Sub(image.Pt(1, 1))
// Only remaining cases involve line intersection between the rect and poly
// having eliminated the possibility that one is entirely within another.
// Since rect is an axis-aligned rectangle, we only need vertical and
// horizontal line intersection tests.
// Walk each edge of polygon.
for i, p := range polygon {
q := polygon[(i+1)%len(polygon)]
// Pretend the edge is a rectangle. Exclude those that don't overlap.
if !rect.Overlaps(image.Rectangle{p, q}.Canon()) {
continue
}
d := q.Sub(p)
// If the polygon edge is not vertical, test left and right sides
if d.X != 0 {
if d.X < 0 {
d = d.Mul(-1)
}
min := (rect.Min.Y - p.Y) * d.X
max := (rect.Max.Y - p.Y) * d.X
// Test left side of rect
if t := (rect.Min.X - p.X) * d.Y; min <= t && t < max {
return true
}
// Test right side of rect
if t := (rmax.X - p.X) * d.Y; min <= t && t < max {
return true
}
}
// If the polygon edge is not horizontal, test the top and bottom sides
if d.Y != 0 {
if d.Y < 0 {
d = d.Mul(-1)
}
min := (rect.Min.X - p.X) * d.Y
max := (rect.Max.X - p.X) * d.Y
// Test top side of rect
if t := (rect.Min.Y - p.Y) * d.X; min <= t && t < max {
return true
}
// Test bottom side of rect
if t := (rmax.Y - p.Y) * d.X; min <= t && t < max {
return true
}
}
}
return false
}

28
geom/polygon_test.go Normal file
View file

@ -0,0 +1,28 @@
package geom
import (
"image"
"testing"
)
func TestPolygonContains(t *testing.T) {
square5 := []image.Point{
{X: 0, Y: 0},
{X: 5, Y: 0},
{X: 5, Y: 5},
{X: 0, Y: 5},
}
tests := []struct {
polygon []image.Point
point image.Point
want bool
}{
{square5, image.Pt(2, 3), true},
}
for _, test := range tests {
if got, want := PolygonContains(test.polygon, test.point), test.want; got != want {
t.Errorf("polygonContains(%v, %v) = %v, want %v", test.polygon, test.point, got, want)
}
}
}

View file

@ -1,4 +1,4 @@
package engine package geom
import "image" import "image"

View file

@ -1,4 +1,4 @@
package engine package geom
import "strconv" import "strconv"