refacteur
This commit is contained in:
parent
dccd13b623
commit
6686c24cd6
21 changed files with 216 additions and 166 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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...
|
||||||
|
|
|
@ -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)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package engine
|
package geom
|
||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package engine
|
package geom
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
25
geom/misc.go
Normal file
25
geom/misc.go
Normal 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
|
||||||
|
}
|
|
@ -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
80
geom/polygon.go
Normal 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
28
geom/polygon_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package engine
|
package geom
|
||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package engine
|
package geom
|
||||||
|
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
Loading…
Reference in a new issue