Make Collision Detection Flawless!
* Pixel perfect collision detection with level geometry. * New shell commands (echo, clear) and help commands
This commit is contained in:
parent
d560670b7b
commit
0efb2ab24f
47
commands.go
47
commands.go
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
4
shell.go
4
shell.go
|
@ -59,10 +59,14 @@ 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)
|
||||||
|
if command.Command == "clear" {
|
||||||
|
s.Output = []string{}
|
||||||
|
} else {
|
||||||
err := command.Run(s.parent)
|
err := command.Run(s.parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Write(err.Error())
|
s.Write(err.Error())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if command.Raw != "" {
|
if command.Raw != "" {
|
||||||
s.History = append(s.History, command.Raw)
|
s.History = append(s.History, command.Raw)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user