doodle/doodads/doodads.go
Noah Petherbridge e25869644c Fix Play Mode, Level Handover & Collision Detection
* Edit Mode now uses the Level object itself to keep the drawing data
  rather than pull its Palette and Chunks out, so it can hang on to more
  information. The Canvas widget is given references to the
  Level.Palette and Level.Chunker via Canvas.LoadLevel()
* Fix the handoff between Edit Mode and Play Mode. They pass the Level
  object back and forth and the Filename, because it's not part of the
  Level. You can save the map with its original settings after returning
  from Play Mode.
* Fix the collision detection in Play Mode. It broke previously when
  palettes were added because of the difference between a render.Point
  and a level.Pixel and it couldn't easily look up coordinates. The new
  Chunker system provides a render.Point lookup API.
* All pixels are solid for collision right now, TODO is to return Swatch
  information from the pixels touching the player character and react
  accordingly (non-solid, fire flag, etc.)
* Remove the level.Grid type as it has been replaced by the Chunker.
* Clean up some unused variables and functions.
2018-09-25 09:40:34 -07:00

321 lines
7.0 KiB
Go

package doodads
import (
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
)
// Doodad is a reusable drawing component used in Doodle. Doodads are buttons,
// doors, switches, the player characters themselves, anything that isn't a part
// of the level geometry.
type Doodad interface {
ID() string
// Position and velocity, not saved to disk.
Position() render.Point
Velocity() render.Point
Size() render.Rect
Grounded() bool
SetGrounded(bool)
// Movement commands.
MoveBy(render.Point) // Add {X,Y} to current Position.
MoveTo(render.Point) // Set current Position to {X,Y}.
// Implement the Draw function.
Draw(render.Engine)
}
// Collide describes how a collision occurred.
type Collide struct {
Top bool
TopPoint render.Point
TopPixel *level.Swatch
Left bool
LeftPoint render.Point
LeftPixel *level.Swatch
Right bool
RightPoint render.Point
RightPixel *level.Swatch
Bottom bool
BottomPoint render.Point
BottomPixel *level.Swatch
MoveTo render.Point
}
// Reset a Collide struct flipping all the bools off, but keeping MoveTo.
func (c *Collide) Reset() {
c.Top = false
c.Left = false
c.Right = false
c.Bottom = false
}
// CollisionBox holds all of the coordinate pairs to draw the collision box
// around a doodad.
type CollisionBox struct {
Top []render.Point
Bottom []render.Point
Left []render.Point
Right []render.Point
}
// Side of the collision box (top, bottom, left, right)
type Side uint8
// Options for the Side type.
const (
Top Side = iota
Bottom
Left
Right
)
// CollidesWithGrid checks if a Doodad collides with level geometry.
func CollidesWithGrid(d Doodad, grid *level.Chunker, target render.Point) (*Collide, bool) {
var (
P = d.Position()
S = d.Size()
result = &Collide{
MoveTo: P,
}
ceiling bool // Has hit a ceiling?
capHeight int32 // Stop vertical movement thru a ceiling
capLeft int32 // Stop movement thru a wall
capRight int32
hitLeft bool // Has hit an obstacle on the left
hitRight bool // or right
hitFloor bool
capFloor int32
)
// Test all of the bounding boxes for a collision with level geometry.
if ok := result.ScanBoundingBox(GetBoundingRect(d), grid); ok {
// We've already collided! Try to wiggle free.
if result.Bottom {
if !d.Grounded() {
d.SetGrounded(true)
} else {
// result.Bottom = false
}
} else {
d.SetGrounded(false)
}
if result.Top {
// Never seen it touch the top.
}
if result.Left {
P.X++
}
if result.Right {
P.X--
}
}
// If grounded, cap our Y position.
if d.Grounded() {
if !result.Bottom {
// We've fallen off a ledge.
d.SetGrounded(false)
} else if target.Y < P.Y {
// We're moving upward.
d.SetGrounded(false)
} else {
// Cap our downward motion to our current position.
target.Y = P.Y
}
}
// Cap our horizontal movement if we're touching walls.
if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) {
// If the step is short enough, try and jump up.
height := P.Y + S.H
if result.Left && target.X < P.X {
height -= result.LeftPoint.Y
} else {
height -= result.RightPoint.Y
}
if height <= 8 {
target.Y -= height
if target.X < P.X {
target.X-- // push along to the left
} else if target.X > P.X {
target.X++ // push along to the right
}
} else {
target.X = P.X
}
}
// Cap our vertical movement if we're touching ceilings.
if ceiling {
// The existing box intersects a ceiling, this will almost never
// happen because gravity will always pull you away at the last frame.
// But if we do somehow get here, may as well cap it where it's at.
capHeight = P.Y
}
// Trace a line from where we are to where we wanna go.
result.Reset()
result.MoveTo = P
for point := range render.IterLine2(P, target) {
if has := result.ScanBoundingBox(render.Rect{
X: point.X,
Y: point.Y,
W: S.W,
H: S.H,
}, grid); has {
if result.Bottom {
if !hitFloor {
hitFloor = true
capFloor = result.BottomPoint.Y - S.H
}
d.SetGrounded(true)
}
if result.Top && !ceiling {
// This is a newly discovered ceiling.
ceiling = true
capHeight = result.TopPoint.Y
}
if result.Left && !hitLeft {
hitLeft = true
capLeft = result.LeftPoint.X
}
if result.Right && !hitRight {
hitRight = true
capRight = result.RightPoint.X - S.W
}
}
// So far so good, keep following the MoveTo to
// the last good point before a collision.
result.MoveTo = point
}
// If they hit the roof, cap them to the roof.
if ceiling && result.MoveTo.Y < capHeight {
result.Top = true
result.MoveTo.Y = capHeight
}
if hitFloor && result.MoveTo.Y > capFloor {
result.Bottom = true
result.MoveTo.Y = capFloor
}
if hitLeft {
result.Left = true
result.MoveTo.X = capLeft
}
if hitRight {
result.Right = true
result.MoveTo.X = capRight
}
return result, result.IsColliding()
}
// IsColliding returns whether any sort of collision has occurred.
func (c *Collide) IsColliding() bool {
return c.Top || c.Bottom || c.Left || c.Right
}
// GetBoundingRect computes the full pairs of points for the collision box
// around a doodad.
func GetBoundingRect(d Doodad) render.Rect {
var (
P = d.Position()
S = d.Size()
)
return render.Rect{
X: P.X,
Y: P.Y,
W: S.W,
H: S.H,
}
}
func GetCollisionBox(box render.Rect) CollisionBox {
return CollisionBox{
Top: []render.Point{
{
X: box.X,
Y: box.Y,
},
{
X: box.X + box.W,
Y: box.Y,
},
},
Bottom: []render.Point{
{
X: box.X,
Y: box.Y + box.H,
},
{
X: box.X + box.W,
Y: box.Y + box.H,
},
},
Left: []render.Point{
{
X: box.X,
Y: box.Y + box.H - 1,
},
{
X: box.X,
Y: box.Y + 1,
},
},
Right: []render.Point{
{
X: box.X + box.W,
Y: box.Y + box.H - 1,
},
{
X: box.X + box.W,
Y: box.Y + 1,
},
},
}
}
// ScanBoundingBox scans all of the pixels in a bounding box on the grid and
// returns if any of them intersect with level geometry.
func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
col := GetCollisionBox(box)
c.ScanGridLine(col.Top[0], col.Top[1], grid, Top)
c.ScanGridLine(col.Bottom[0], col.Bottom[1], grid, Bottom)
c.ScanGridLine(col.Left[0], col.Left[1], grid, Left)
c.ScanGridLine(col.Right[0], col.Right[1], grid, Right)
return c.IsColliding()
}
// ScanGridLine scans all of the pixels between p1 and p2 on the grid and tests
// for any pixels to be set, implying a collision between level geometry and the
// bounding boxes of the doodad.
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) {
for point := range render.IterLine2(p1, p2) {
if _, err := grid.Get(point); err == nil {
// A hit!
switch side {
case Top:
c.Top = true
c.TopPoint = point
case Bottom:
c.Bottom = true
c.BottomPoint = point
case Left:
c.Left = true
c.LeftPoint = point
case Right:
c.Right = true
c.RightPoint = point
}
}
}
}