From 0efb2ab24fc222e20a74ee9e40f578d8d8aa6928 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Tue, 24 Jul 2018 22:26:27 -0700 Subject: [PATCH] Make Collision Detection Flawless! * Pixel perfect collision detection with level geometry. * New shell commands (echo, clear) and help commands --- commands.go | 47 +++++++++++++++++++++- doodads/doodads.go | 99 ++++++++++++++++++++++++++++++++++++---------- play_scene.go | 2 + shell.go | 10 +++-- 4 files changed, 134 insertions(+), 24 deletions(-) diff --git a/commands.go b/commands.go index 9f96387..31e9649 100644 --- a/commands.go +++ b/commands.go @@ -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 ") + d.Flash("Open a file on disk in Edit Mode") + case "play": + d.Flash("Usage: play ") + d.Flash("Open a map from disk in Play Mode") + case "echo": + d.Flash("Usage: echo ") + 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 ") + 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 { diff --git a/doodads/doodads.go b/doodads/doodads.go index aaef15c..167af25 100644 --- a/doodads/doodads.go +++ b/doodads/doodads.go @@ -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, }, }, } diff --git a/play_scene.go b/play_scene.go index faaaf25..7b825ee 100644 --- a/play_scene.go +++ b/play_scene.go @@ -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 } diff --git a/shell.go b/shell.go index 10418de..e2abcc9 100644 --- a/shell.go +++ b/shell.go @@ -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 != "" {