From f8a83cbad9ae2d1f5b4de811cc678c0c303dd739 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 15 Apr 2019 19:12:25 -0700 Subject: [PATCH] Detect Collision Between Actors * Move all collision code into the pkg/collision package. * pkg/doodads/collision.go -> pkg/collision/collide_level.go * pkg/doodads/collide_actors.go for new Actor collide support * Add initial collision detection code between actors in Play Mode. --- pkg/collision/collide_actors.go | 34 +++++++ .../collide_level.go} | 99 ++++++++----------- pkg/collision/debug_box.go | 58 +++++++++++ pkg/doodads/actor.go | 38 ------- pkg/fps.go | 3 +- pkg/uix/canvas.go | 93 ++++++++--------- pkg/uix/canvas_wallpaper.go | 41 ++++++++ 7 files changed, 217 insertions(+), 149 deletions(-) create mode 100644 pkg/collision/collide_actors.go rename pkg/{doodads/collision.go => collision/collide_level.go} (76%) create mode 100644 pkg/collision/debug_box.go diff --git a/pkg/collision/collide_actors.go b/pkg/collision/collide_actors.go new file mode 100644 index 0000000..a8672f5 --- /dev/null +++ b/pkg/collision/collide_actors.go @@ -0,0 +1,34 @@ +package collision + +import ( + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/log" +) + +// IndexTuple holds two integers used as array indexes. +type IndexTuple [2]int + +// BetweenBoxes checks if there is a collision between any +// two bounding rectangles. +// +// This returns a generator that spits out indexes of the +// intersecting boxes. +func BetweenBoxes(boxes []render.Rect) chan IndexTuple { + generator := make(chan IndexTuple) + + go func() { + // Outer loop: test each box for intersection with the others. + for i, box := range boxes { + for j := i + 1; j < len(boxes); j++ { + if box.Intersects(boxes[j]) { + log.Info("Actor %d intersects %d", i, j) + generator <- IndexTuple{i, j} + } + } + } + + close(generator) + }() + + return generator +} diff --git a/pkg/doodads/collision.go b/pkg/collision/collide_level.go similarity index 76% rename from pkg/doodads/collision.go rename to pkg/collision/collide_level.go index ac57713..0f88e88 100644 --- a/pkg/doodads/collision.go +++ b/pkg/collision/collide_level.go @@ -1,7 +1,8 @@ -package doodads +package collision import ( "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" ) @@ -30,61 +31,6 @@ func (c *Collide) Reset() { c.Bottom = false } -// 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 -} - -// GetCollisionBox returns a CollisionBox with the four coordinates. -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 + box.H - 1, - }, - { - X: box.X, - Y: box.Y + 1, - }, - }, - Right: []render.Point{ - { - X: box.X + box.W, - Y: box.Y + box.H - 1, - }, - { - X: box.X + box.W, - Y: box.Y + 1, - }, - }, - } -} - // Side of the collision box (top, bottom, left, right) type Side uint8 @@ -101,7 +47,7 @@ CollidesWithGrid checks if a Doodad collides with level geometry. The `target` is the point the actor wants to move to on this tick. */ -func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Collide, bool) { +func CollidesWithGrid(d doodads.Actor, grid *level.Chunker, target render.Point) (*Collide, bool) { var ( P = d.Position() S = d.Size() @@ -120,7 +66,7 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli ) // Test all of the bounding boxes for a collision with level geometry. - if ok := result.ScanBoundingBox(GetBoundingRect(d), grid); ok { + if ok := result.ScanBoundingBox(doodads.GetBoundingRect(d), grid); ok { // We've already collided! Try to wiggle free. if result.Bottom { if !d.Grounded() { @@ -250,3 +196,40 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli func (c *Collide) IsColliding() bool { return c.Top || c.Bottom || c.Left || c.Right } + +// 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 *level.Chunker) 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 *level.Chunker, side Side) { + for point := range render.IterLine2(p1, p2) { + if _, err := grid.Get(point); err == nil { + // 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 + } + } + } +} diff --git a/pkg/collision/debug_box.go b/pkg/collision/debug_box.go new file mode 100644 index 0000000..b99aef2 --- /dev/null +++ b/pkg/collision/debug_box.go @@ -0,0 +1,58 @@ +package collision + +import "git.kirsle.net/apps/doodle/lib/render" + +// 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 +} + +// GetCollisionBox returns a CollisionBox with the four coordinates. +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 + box.H - 1, + }, + { + X: box.X, + Y: box.Y + 1, + }, + }, + Right: []render.Point{ + { + X: box.X + box.W, + Y: box.Y + box.H - 1, + }, + { + X: box.X + box.W, + Y: box.Y + 1, + }, + }, + } +} diff --git a/pkg/doodads/actor.go b/pkg/doodads/actor.go index 233c79d..09f65e4 100644 --- a/pkg/doodads/actor.go +++ b/pkg/doodads/actor.go @@ -2,7 +2,6 @@ package doodads import ( "git.kirsle.net/apps/doodle/lib/render" - "git.kirsle.net/apps/doodle/pkg/level" ) // Actor is a reusable run-time drawing component used in Doodle. Actors are an @@ -36,40 +35,3 @@ func GetBoundingRect(d Actor) render.Rect { H: S.H, } } - -// 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 *level.Chunker) 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 *level.Chunker, side Side) { - for point := range render.IterLine2(p1, p2) { - if _, err := grid.Get(point); err == nil { - // 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 - } - } - } -} diff --git a/pkg/fps.go b/pkg/fps.go index 6244c05..75daa43 100644 --- a/pkg/fps.go +++ b/pkg/fps.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/doodads" ) @@ -142,7 +143,7 @@ func (d *Doodle) DrawCollisionBox(actor doodads.Actor) { var ( rect = doodads.GetBoundingRect(actor) - box = doodads.GetCollisionBox(rect) + box = collision.GetCollisionBox(rect) ) d.Engine.DrawLine(render.DarkGreen, box.Top[0], box.Top[1]) diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index d5eaa5e..ef070c6 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -9,6 +9,7 @@ import ( "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" @@ -167,60 +168,48 @@ func (w *Canvas) Loop(ev *events.State) error { log.Debug("loopConstrainScroll: %s", err) } - // Move any actors. - for _, a := range w.actors { - if v := a.Velocity(); v != render.Origin { - // Create a delta point from their current location to where they - // want to move to this tick. - delta := a.Position() - delta.Add(v) + // Move any actors. As we iterate over all actors, track their bounding + // rectangles so we can later see if any pair of actors intersect each other. + boxes := make([]render.Rect, len(w.actors)) + for i, a := range w.actors { + // Get the actor's velocity to see if it's moving this tick. + v := a.Velocity() - // Check collision with level geometry. - info, ok := doodads.CollidesWithGrid(a, w.chunks, delta) - if ok { - // Collision happened with world. - log.Error("COLLIDE %+v", info) - } - delta = info.MoveTo // Move us back where the collision check put us - - // Move the actor's World Position to the new location. - a.MoveTo(delta) - - // Keep them contained inside the level. - if w.wallpaper.pageType > level.Unbounded { - var ( - orig = a.Position() // Actor's World Position - moveBy render.Point - size = a.Size() - ) - - // Bound it on the top left edges. - if orig.X < 0 { - moveBy.X = -orig.X - } - if orig.Y < 0 { - moveBy.Y = -orig.Y - } - - // Bound it on the right bottom edges. XXX: downcast from int64! - if w.wallpaper.maxWidth > 0 { - if int64(orig.X+size.W) > w.wallpaper.maxWidth { - var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W)) - moveBy.X = delta - } - } - if w.wallpaper.maxHeight > 0 { - if int64(orig.Y+size.H) > w.wallpaper.maxHeight { - var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H)) - moveBy.Y = delta - } - } - - if !moveBy.IsZero() { - a.MoveBy(moveBy) - } - } + // If not moving, grab the bounding box right now. + if v == render.Origin { + boxes[i] = doodads.GetBoundingRect(a) + continue } + + // Create a delta point from their current location to where they + // want to move to this tick. + delta := a.Position() + delta.Add(v) + + // Check collision with level geometry. + info, ok := collision.CollidesWithGrid(a, w.chunks, delta) + if ok { + // Collision happened with world. + log.Error("COLLIDE %+v", info) + } + delta = info.MoveTo // Move us back where the collision check put us + + // Move the actor's World Position to the new location. + a.MoveTo(delta) + + // Keep the actor from leaving the world borders of bounded maps. + w.loopContainActorsInsideLevel(a) + + // Store this actor's bounding box after they've moved. + boxes[i] = doodads.GetBoundingRect(a) + } + + // Check collisions between actors. + for tuple := range collision.BetweenBoxes(boxes) { + log.Error("Actor %s collides with %s", + w.actors[tuple[0]].ID(), + w.actors[tuple[1]].ID(), + ) } // If the canvas is editable, only care if it's over our space. diff --git a/pkg/uix/canvas_wallpaper.go b/pkg/uix/canvas_wallpaper.go index 179b42e..a1f623c 100644 --- a/pkg/uix/canvas_wallpaper.go +++ b/pkg/uix/canvas_wallpaper.go @@ -23,6 +23,47 @@ func (wp *Wallpaper) Valid() bool { return wp.repeat != nil } +// Canvas Loop() task that keeps mobile actors constrained inside the borders +// of the world for bounded map types. +func (w *Canvas) loopContainActorsInsideLevel(a *Actor) { + // Infinite maps do not need to constrain the actors. + if w.wallpaper.pageType == level.Unbounded { + return + } + + var ( + orig = a.Position() // Actor's World Position + moveBy render.Point + size = a.Size() + ) + + // Bound it on the top left edges. + if orig.X < 0 { + moveBy.X = -orig.X + } + if orig.Y < 0 { + moveBy.Y = -orig.Y + } + + // Bound it on the right bottom edges. XXX: downcast from int64! + if w.wallpaper.maxWidth > 0 { + if int64(orig.X+size.W) > w.wallpaper.maxWidth { + var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W)) + moveBy.X = delta + } + } + if w.wallpaper.maxHeight > 0 { + if int64(orig.Y+size.H) > w.wallpaper.maxHeight { + var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H)) + moveBy.Y = delta + } + } + + if !moveBy.IsZero() { + a.MoveBy(moveBy) + } +} + // PresentWallpaper draws the wallpaper. func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { var (