Better Collision Detection (Bouncy Jumps Up Hills)
* Add a debug view that draws the player bounding boxes. * Improve the collision detection to add support for: * Doodads being "Grounded" so gravity need not apply. * Walking up hills, albeit a bit "bouncily" * Harder to clip out of bounds
This commit is contained in:
parent
c3fd2e63cb
commit
d560670b7b
|
@ -1,6 +1,8 @@
|
||||||
package doodads
|
package doodads
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
@ -15,6 +17,8 @@ type Doodad interface {
|
||||||
Position() render.Point
|
Position() render.Point
|
||||||
Velocity() render.Point
|
Velocity() render.Point
|
||||||
Size() render.Rect
|
Size() render.Rect
|
||||||
|
Grounded() bool
|
||||||
|
SetGrounded(bool)
|
||||||
|
|
||||||
// Movement commands.
|
// Movement commands.
|
||||||
MoveBy(render.Point) // Add {X,Y} to current Position.
|
MoveBy(render.Point) // Add {X,Y} to current Position.
|
||||||
|
@ -26,89 +30,231 @@ type Doodad interface {
|
||||||
|
|
||||||
// Collide describes how a collision occurred.
|
// Collide describes how a collision occurred.
|
||||||
type Collide struct {
|
type Collide struct {
|
||||||
X int32
|
Top bool
|
||||||
Y int32
|
TopPoint render.Point
|
||||||
W int32
|
Left bool
|
||||||
H int32
|
LeftPoint render.Point
|
||||||
Top bool
|
Right bool
|
||||||
Left bool
|
RightPoint render.Point
|
||||||
Right bool
|
Bottom bool
|
||||||
Bottom bool
|
BottomPoint render.Point
|
||||||
|
MoveTo render.Point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// CollidesWithGrid checks if a Doodad collides with level geometry.
|
||||||
func CollidesWithGrid(d Doodad, grid *render.Grid) (Collide, bool) {
|
func CollidesWithGrid(d Doodad, grid *render.Grid, target render.Point) (*Collide, bool) {
|
||||||
var (
|
var (
|
||||||
P = d.Position()
|
P = d.Position()
|
||||||
S = d.Size()
|
S = d.Size()
|
||||||
topLeft = P
|
|
||||||
topRight = render.Point{
|
result = &Collide{
|
||||||
X: P.X + S.W,
|
MoveTo: P,
|
||||||
Y: P.Y,
|
|
||||||
}
|
|
||||||
bottomLeft = render.Point{
|
|
||||||
X: P.X,
|
|
||||||
Y: P.Y + S.H,
|
|
||||||
}
|
|
||||||
bottomRight = render.Point{
|
|
||||||
X: bottomLeft.X + S.W,
|
|
||||||
Y: P.Y + S.H,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bottom edge.
|
// Test all of the bounding boxes for a collision with level geometry.
|
||||||
for point := range render.IterLine2(bottomLeft, bottomRight) {
|
if ok := result.ScanBoundingBox(GetBoundingRect(d), grid); ok {
|
||||||
if grid.Exists(level.Pixel{
|
// We've already collided! Try to wiggle free.
|
||||||
X: point.X,
|
if result.Bottom {
|
||||||
Y: point.Y,
|
if !d.Grounded() {
|
||||||
}) {
|
d.SetGrounded(true)
|
||||||
return Collide{
|
} else {
|
||||||
Bottom: true,
|
// result.Bottom = false
|
||||||
X: point.X,
|
}
|
||||||
Y: point.Y,
|
} else {
|
||||||
}, true
|
d.SetGrounded(false)
|
||||||
|
}
|
||||||
|
if result.Top {
|
||||||
|
P.Y++
|
||||||
|
}
|
||||||
|
if result.Left {
|
||||||
|
P.X++
|
||||||
|
}
|
||||||
|
if result.Right {
|
||||||
|
P.X--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top edge.
|
// If grounded, cap our Y position.
|
||||||
for point := range render.IterLine2(topLeft, topRight) {
|
if d.Grounded() {
|
||||||
if grid.Exists(level.Pixel{
|
if !result.Bottom {
|
||||||
X: point.X,
|
// We've fallen off a ledge.
|
||||||
Y: point.Y,
|
d.SetGrounded(false)
|
||||||
}) {
|
} else if target.Y < P.Y {
|
||||||
return Collide{
|
// We're moving upward.
|
||||||
Top: true,
|
d.SetGrounded(false)
|
||||||
X: point.X,
|
} else {
|
||||||
Y: point.Y,
|
// Cap our downward motion to our current position.
|
||||||
}, true
|
target.Y = P.Y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for point := range render.IterLine2(topLeft, bottomLeft) {
|
// Cap our horizontal movement if we're touching walls.
|
||||||
if grid.Exists(level.Pixel{
|
if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) {
|
||||||
X: point.X,
|
// If the step is short enough, try and jump up.
|
||||||
Y: point.Y,
|
relPoint := P.Y + S.H
|
||||||
}) {
|
if result.Left && target.X < P.X {
|
||||||
return Collide{
|
relPoint -= result.LeftPoint.Y
|
||||||
Left: true,
|
} else {
|
||||||
X: point.X,
|
relPoint -= result.RightPoint.Y
|
||||||
Y: point.Y,
|
}
|
||||||
}, true
|
fmt.Printf("Touched a wall at %d pixels height (P=%s)\n", relPoint, P)
|
||||||
|
if S.H-relPoint > S.H-8 {
|
||||||
|
target.Y -= 12
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for point := range render.IterLine2(topRight, bottomRight) {
|
// Trace a line from where we are to where we wanna go.
|
||||||
if grid.Exists(level.Pixel{
|
result.MoveTo = P
|
||||||
|
for point := range render.IterLine2(P, target) {
|
||||||
|
if ok := result.ScanBoundingBox(render.Rect{
|
||||||
X: point.X,
|
X: point.X,
|
||||||
Y: point.Y,
|
Y: point.Y,
|
||||||
}) {
|
W: S.W,
|
||||||
return Collide{
|
H: S.H,
|
||||||
Right: true,
|
}, grid); ok {
|
||||||
X: point.X,
|
if d.Grounded() {
|
||||||
Y: point.Y,
|
if !result.Bottom {
|
||||||
}, true
|
d.SetGrounded(false)
|
||||||
|
}
|
||||||
|
} else if result.Bottom {
|
||||||
|
d.SetGrounded(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
result.MoveTo = point
|
||||||
}
|
}
|
||||||
|
|
||||||
return Collide{}, false
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCollisionBox 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 + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: box.X,
|
||||||
|
Y: box.Y + box.H - 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Right: []render.Point{
|
||||||
|
{
|
||||||
|
X: box.X + box.W,
|
||||||
|
Y: box.Y + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: box.X + box.W,
|
||||||
|
Y: box.Y + box.H - 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 *render.Grid) 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 *render.Grid, side Side) {
|
||||||
|
for point := range render.IterLine2(p1, p2) {
|
||||||
|
if grid.Exists(level.Pixel{
|
||||||
|
X: point.X,
|
||||||
|
Y: point.Y,
|
||||||
|
}) {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ type Player struct {
|
||||||
point render.Point
|
point render.Point
|
||||||
velocity render.Point
|
velocity render.Point
|
||||||
size render.Rect
|
size render.Rect
|
||||||
|
grounded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPlayer creates the special Player Character doodad.
|
// NewPlayer creates the special Player Character doodad.
|
||||||
|
@ -22,8 +23,8 @@ func NewPlayer() *Player {
|
||||||
Y: 100,
|
Y: 100,
|
||||||
},
|
},
|
||||||
size: render.Rect{
|
size: render.Rect{
|
||||||
W: 16,
|
W: 32,
|
||||||
H: 16,
|
H: 32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,9 +60,19 @@ func (p *Player) Size() render.Rect {
|
||||||
return p.size
|
return p.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grounded returns if the player is grounded.
|
||||||
|
func (p *Player) Grounded() bool {
|
||||||
|
return p.grounded
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGrounded sets if the player is grounded.
|
||||||
|
func (p *Player) SetGrounded(v bool) {
|
||||||
|
p.grounded = v
|
||||||
|
}
|
||||||
|
|
||||||
// Draw the player sprite.
|
// Draw the player sprite.
|
||||||
func (p *Player) Draw(e render.Engine) {
|
func (p *Player) Draw(e render.Engine) {
|
||||||
e.DrawRect(render.Magenta, render.Rect{
|
e.DrawBox(render.Color{255, 255, 153, 255}, render.Rect{
|
||||||
X: p.point.X,
|
X: p.point.X,
|
||||||
Y: p.point.Y,
|
Y: p.point.Y,
|
||||||
W: p.size.W,
|
W: p.size.W,
|
||||||
|
|
14
fps.go
14
fps.go
|
@ -3,6 +3,7 @@ package doodle
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/doodads"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,6 +50,19 @@ func (d *Doodle) DrawDebugOverlay() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DrawCollisionBox draws the collision box around a Doodad.
|
||||||
|
func (d *Doodle) DrawCollisionBox(actor doodads.Doodad) {
|
||||||
|
var (
|
||||||
|
rect = doodads.GetBoundingRect(actor)
|
||||||
|
box = doodads.GetCollisionBox(rect)
|
||||||
|
)
|
||||||
|
|
||||||
|
d.Engine.DrawLine(render.DarkGreen, box.Top[0], box.Top[1])
|
||||||
|
d.Engine.DrawLine(render.DarkBlue, box.Bottom[0], box.Bottom[1])
|
||||||
|
d.Engine.DrawLine(render.DarkYellow, box.Left[0], box.Left[1])
|
||||||
|
d.Engine.DrawLine(render.Red, box.Right[0], box.Right[1])
|
||||||
|
}
|
||||||
|
|
||||||
// TrackFPS shows the current FPS once per second.
|
// TrackFPS shows the current FPS once per second.
|
||||||
func (d *Doodle) TrackFPS(skipped uint32) {
|
func (d *Doodle) TrackFPS(skipped uint32) {
|
||||||
fpsFrames++
|
fpsFrames++
|
||||||
|
|
|
@ -82,6 +82,9 @@ func (s *PlayScene) Draw(d *Doodle) error {
|
||||||
// Draw our hero.
|
// Draw our hero.
|
||||||
s.player.Draw(d.Engine)
|
s.player.Draw(d.Engine)
|
||||||
|
|
||||||
|
// Draw out bounding boxes.
|
||||||
|
d.DrawCollisionBox(s.player)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,20 +108,20 @@ func (s *PlayScene) movePlayer(ev *events.State) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply gravity.
|
// Apply gravity.
|
||||||
delta.Y += gravity
|
// var onFloor bool
|
||||||
|
|
||||||
// Draw a ray and check for collision.
|
info, ok := doodads.CollidesWithGrid(s.player, &s.canvas, delta)
|
||||||
var lastOk = s.player.Position()
|
if ok {
|
||||||
for point := range render.IterLine2(s.player.Position(), delta) {
|
// Collision happened with world.
|
||||||
s.player.MoveTo(point)
|
}
|
||||||
if _, ok := doodads.CollidesWithGrid(s.player, &s.canvas); ok {
|
delta = info.MoveTo
|
||||||
s.player.MoveTo(lastOk)
|
|
||||||
} else {
|
// Apply gravity if not grounded.
|
||||||
lastOk = s.player.Position()
|
if !s.player.Grounded() {
|
||||||
}
|
delta.Y += gravity
|
||||||
}
|
}
|
||||||
|
|
||||||
s.player.MoveTo(lastOk)
|
s.player.MoveTo(delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadLevel loads a level from disk.
|
// LoadLevel loads a level from disk.
|
||||||
|
|
|
@ -45,6 +45,16 @@ type Color struct {
|
||||||
Alpha uint8
|
Alpha uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RGBA creates a new Color.
|
||||||
|
func RGBA(r, g, b, a uint8) Color {
|
||||||
|
return Color{
|
||||||
|
Red: r,
|
||||||
|
Green: g,
|
||||||
|
Blue: b,
|
||||||
|
Alpha: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c Color) String() string {
|
func (c Color) String() string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"Color<#%02x%02x%02x>",
|
"Color<#%02x%02x%02x>",
|
||||||
|
@ -91,18 +101,24 @@ func (t Text) String() string {
|
||||||
|
|
||||||
// Common color names.
|
// Common color names.
|
||||||
var (
|
var (
|
||||||
Invisible = Color{}
|
Invisible = Color{}
|
||||||
White = Color{255, 255, 255, 255}
|
White = RGBA(255, 255, 255, 255)
|
||||||
Grey = Color{153, 153, 153, 255}
|
Grey = RGBA(153, 153, 153, 255)
|
||||||
Black = Color{0, 0, 0, 255}
|
Black = RGBA(0, 0, 0, 255)
|
||||||
SkyBlue = Color{0, 153, 255, 255}
|
SkyBlue = RGBA(0, 153, 255, 255)
|
||||||
Blue = Color{0, 0, 255, 255}
|
Blue = RGBA(0, 0, 255, 255)
|
||||||
Red = Color{255, 0, 0, 255}
|
DarkBlue = RGBA(0, 0, 153, 255)
|
||||||
Green = Color{0, 255, 0, 255}
|
Red = RGBA(255, 0, 0, 255)
|
||||||
Cyan = Color{0, 255, 255, 255}
|
DarkRed = RGBA(153, 0, 0, 255)
|
||||||
Yellow = Color{255, 255, 0, 255}
|
Green = RGBA(0, 255, 0, 255)
|
||||||
Magenta = Color{255, 0, 255, 255}
|
DarkGreen = RGBA(0, 153, 0, 255)
|
||||||
Pink = Color{255, 153, 255, 255}
|
Cyan = RGBA(0, 255, 255, 255)
|
||||||
|
DarkCyan = RGBA(0, 153, 153, 255)
|
||||||
|
Yellow = RGBA(255, 255, 0, 255)
|
||||||
|
DarkYellow = RGBA(153, 153, 0, 255)
|
||||||
|
Magenta = RGBA(255, 0, 255, 255)
|
||||||
|
Purple = RGBA(153, 0, 153, 255)
|
||||||
|
Pink = RGBA(255, 153, 255, 255)
|
||||||
)
|
)
|
||||||
|
|
||||||
// IterLine is a generator that returns the X,Y coordinates to draw a line.
|
// IterLine is a generator that returns the X,Y coordinates to draw a line.
|
||||||
|
|
|
@ -25,7 +25,7 @@ func (r *Renderer) DrawPoint(color render.Color, point render.Point) {
|
||||||
// DrawLine draws a line between two points.
|
// DrawLine draws a line between two points.
|
||||||
func (r *Renderer) DrawLine(color render.Color, a, b render.Point) {
|
func (r *Renderer) DrawLine(color render.Color, a, b render.Point) {
|
||||||
if color != r.lastColor {
|
if color != r.lastColor {
|
||||||
r.renderer.SetDrawColor(color.Red, color.Blue, color.Green, color.Alpha)
|
r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
|
||||||
}
|
}
|
||||||
r.renderer.DrawLine(a.X, a.Y, b.X, b.Y)
|
r.renderer.DrawLine(a.X, a.Y, b.X, b.Y)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user