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.
stash-ui-rework
Noah 2019-04-15 19:12:25 -07:00
parent 241186209c
commit f8a83cbad9
7 changed files with 217 additions and 149 deletions

View File

@ -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
}

View File

@ -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
}
}
}
}

View File

@ -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,
},
},
}
}

View File

@ -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
}
}
}
}

View File

@ -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])

View File

@ -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.

View File

@ -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 (