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 {
case "echo":
d.Flash(c.ArgsLiteral)
return nil
case "new":
return c.New(d)
case "save":
@ -31,6 +34,8 @@ func (c Command) Run(d *Doodle) error {
case "exit":
case "quit":
return c.Quit()
case "help":
return c.Help(d)
default:
return c.Default()
}
@ -39,11 +44,51 @@ func (c Command) Run(d *Doodle) error {
// New opens a new map in the editor mode.
func (c Command) New(d *Doodle) error {
d.shell.Write("Starting a new map")
d.Flash("Starting a new map")
d.NewMap()
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.
func (c Command) Save(d *Doodle) error {
if scene, ok := d.scene.(*EditorScene); ok {

View File

@ -1,8 +1,6 @@
package doodads
import (
"fmt"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
)
@ -41,6 +39,14 @@ type Collide struct {
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 {
@ -70,6 +76,14 @@ func CollidesWithGrid(d Doodad, grid *render.Grid, target render.Point) (*Collid
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.
@ -85,7 +99,7 @@ func CollidesWithGrid(d Doodad, grid *render.Grid, target render.Point) (*Collid
d.SetGrounded(false)
}
if result.Top {
P.Y++
// Never seen it touch the top.
}
if result.Left {
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.
if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) {
// 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 {
relPoint -= result.LeftPoint.Y
height -= result.LeftPoint.Y
} else {
relPoint -= result.RightPoint.Y
height -= result.RightPoint.Y
}
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 height <= 8 {
target.Y -= height
if target.X < P.X {
target.X-- // push along to the left
} 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.
result.Reset()
result.MoveTo = P
for point := range render.IterLine2(P, target) {
if ok := result.ScanBoundingBox(render.Rect{
if has := result.ScanBoundingBox(render.Rect{
X: point.X,
Y: point.Y,
W: S.W,
H: S.H,
}, grid); ok {
if d.Grounded() {
if !result.Bottom {
d.SetGrounded(false)
}, grid); has {
if result.Bottom {
if !hitFloor {
hitFloor = true
capFloor = result.BottomPoint.Y - S.H
}
} else if result.Bottom {
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()
@ -159,7 +218,7 @@ 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
// GetBoundingRect computes the full pairs of points for the collision box
// around a doodad.
func GetBoundingRect(d Doodad) render.Rect {
var (
@ -199,21 +258,21 @@ func GetCollisionBox(box render.Rect) CollisionBox {
Left: []render.Point{
{
X: box.X,
Y: box.Y + 1,
Y: box.Y + box.H - 1,
},
{
X: box.X,
Y: box.Y + box.H - 1,
Y: box.Y + 1,
},
},
Right: []render.Point{
{
X: box.X + box.W,
Y: box.Y + 1,
Y: box.Y + box.H - 1,
},
{
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.
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
}

View File

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