Make Collision Detection Flawless!

* Pixel perfect collision detection with level geometry.
* New shell commands (echo, clear) and help commands
This commit is contained in:
Noah 2018-07-24 22:26:27 -07:00
parent d560670b7b
commit 0efb2ab24f
4 changed files with 134 additions and 24 deletions

View File

@ -20,6 +20,9 @@ func (c Command) Run(d *Doodle) error {
} }
switch c.Command { switch c.Command {
case "echo":
d.Flash(c.ArgsLiteral)
return nil
case "new": case "new":
return c.New(d) return c.New(d)
case "save": case "save":
@ -31,6 +34,8 @@ func (c Command) Run(d *Doodle) error {
case "exit": case "exit":
case "quit": case "quit":
return c.Quit() return c.Quit()
case "help":
return c.Help(d)
default: default:
return c.Default() return c.Default()
} }
@ -39,11 +44,51 @@ func (c Command) Run(d *Doodle) error {
// New opens a new map in the editor mode. // New opens a new map in the editor mode.
func (c Command) New(d *Doodle) error { func (c Command) New(d *Doodle) error {
d.shell.Write("Starting a new map") d.Flash("Starting a new map")
d.NewMap() d.NewMap()
return nil return nil
} }
// Help prints the help info.
func (c Command) Help(d *Doodle) error {
if len(c.Args) == 0 {
d.Flash("Available commands: new save edit play quit echo clear help")
d.Flash("Type `help` and then the command, like: `help edit`")
return nil
}
switch c.Args[0] {
case "new":
d.Flash("Usage: new")
d.Flash("Create a new drawing in Edit Mode")
case "save":
d.Flash("Usage: save [filename.json]")
d.Flash("Save the map to disk (in Edit Mode only)")
case "edit":
d.Flash("Usage: edit <filename.json>")
d.Flash("Open a file on disk in Edit Mode")
case "play":
d.Flash("Usage: play <filename.json>")
d.Flash("Open a map from disk in Play Mode")
case "echo":
d.Flash("Usage: echo <message>")
d.Flash("Flash a message back to the console")
case "quit":
case "exit":
d.Flash("Usage: quit")
d.Flash("Closes the dev console")
case "clear":
d.Flash("Usage: clear")
d.Flash("Clears the terminal output history")
case "help":
d.Flash("Usage: help <command>")
default:
d.Flash("Unknown help topic.")
}
return nil
}
// Save the current map to disk. // Save the current map to disk.
func (c Command) Save(d *Doodle) error { func (c Command) Save(d *Doodle) error {
if scene, ok := d.scene.(*EditorScene); ok { if scene, ok := d.scene.(*EditorScene); ok {

View File

@ -1,8 +1,6 @@
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"
) )
@ -41,6 +39,14 @@ type Collide struct {
MoveTo render.Point 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 // CollisionBox holds all of the coordinate pairs to draw the collision box
// around a doodad. // around a doodad.
type CollisionBox struct { type CollisionBox struct {
@ -70,6 +76,14 @@ func CollidesWithGrid(d Doodad, grid *render.Grid, target render.Point) (*Collid
result = &Collide{ result = &Collide{
MoveTo: P, 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. // Test all of the bounding boxes for a collision with level geometry.
@ -85,7 +99,7 @@ func CollidesWithGrid(d Doodad, grid *render.Grid, target render.Point) (*Collid
d.SetGrounded(false) d.SetGrounded(false)
} }
if result.Top { if result.Top {
P.Y++ // Never seen it touch the top.
} }
if result.Left { if result.Left {
P.X++ P.X++
@ -112,15 +126,14 @@ func CollidesWithGrid(d Doodad, grid *render.Grid, target render.Point) (*Collid
// Cap our horizontal movement if we're touching walls. // Cap our horizontal movement if we're touching walls.
if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) { if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) {
// If the step is short enough, try and jump up. // If the step is short enough, try and jump up.
relPoint := P.Y + S.H height := P.Y + S.H
if result.Left && target.X < P.X { if result.Left && target.X < P.X {
relPoint -= result.LeftPoint.Y height -= result.LeftPoint.Y
} else { } else {
relPoint -= result.RightPoint.Y height -= result.RightPoint.Y
} }
fmt.Printf("Touched a wall at %d pixels height (P=%s)\n", relPoint, P) if height <= 8 {
if S.H-relPoint > S.H-8 { target.Y -= height
target.Y -= 12
if target.X < P.X { if target.X < P.X {
target.X-- // push along to the left target.X-- // push along to the left
} else if target.X > P.X { } else if target.X > P.X {
@ -131,24 +144,70 @@ func CollidesWithGrid(d Doodad, grid *render.Grid, target render.Point) (*Collid
} }
} }
// 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. // Trace a line from where we are to where we wanna go.
result.Reset()
result.MoveTo = P result.MoveTo = P
for point := range render.IterLine2(P, target) { for point := range render.IterLine2(P, target) {
if ok := result.ScanBoundingBox(render.Rect{ if has := result.ScanBoundingBox(render.Rect{
X: point.X, X: point.X,
Y: point.Y, Y: point.Y,
W: S.W, W: S.W,
H: S.H, H: S.H,
}, grid); ok { }, grid); has {
if d.Grounded() { if result.Bottom {
if !result.Bottom { if !hitFloor {
d.SetGrounded(false) hitFloor = true
capFloor = result.BottomPoint.Y - S.H
} }
} else if result.Bottom {
d.SetGrounded(true) 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 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() return result, result.IsColliding()
@ -159,7 +218,7 @@ func (c *Collide) IsColliding() bool {
return c.Top || c.Bottom || c.Left || c.Right return c.Top || c.Bottom || c.Left || c.Right
} }
// GetCollisionBox computes the full pairs of points for the collision box // GetBoundingRect computes the full pairs of points for the collision box
// around a doodad. // around a doodad.
func GetBoundingRect(d Doodad) render.Rect { func GetBoundingRect(d Doodad) render.Rect {
var ( var (
@ -199,21 +258,21 @@ func GetCollisionBox(box render.Rect) CollisionBox {
Left: []render.Point{ Left: []render.Point{
{ {
X: box.X, X: box.X,
Y: box.Y + 1, Y: box.Y + box.H - 1,
}, },
{ {
X: box.X, X: box.X,
Y: box.Y + box.H - 1, Y: box.Y + 1,
}, },
}, },
Right: []render.Point{ Right: []render.Point{
{ {
X: box.X + box.W, X: box.X + box.W,
Y: box.Y + 1, Y: box.Y + box.H - 1,
}, },
{ {
X: box.X + box.W, X: box.X + box.W,
Y: box.Y + box.H - 1, Y: box.Y + 1,
}, },
}, },
} }

View File

@ -118,6 +118,8 @@ func (s *PlayScene) movePlayer(ev *events.State) {
// Apply gravity if not grounded. // Apply gravity if not grounded.
if !s.player.Grounded() { if !s.player.Grounded() {
// Gravity has to pipe through the collision checker, too, so it
// can't give us a cheated downward boost.
delta.Y += gravity delta.Y += gravity
} }

View File

@ -59,9 +59,13 @@ func (s *Shell) Close() {
// Execute a command in the shell. // Execute a command in the shell.
func (s *Shell) Execute(input string) { func (s *Shell) Execute(input string) {
command := s.Parse(input) command := s.Parse(input)
err := command.Run(s.parent) if command.Command == "clear" {
if err != nil { s.Output = []string{}
s.Write(err.Error()) } else {
err := command.Run(s.parent)
if err != nil {
s.Write(err.Error())
}
} }
if command.Raw != "" { if command.Raw != "" {