Actor collision detection with offset hitboxes #96

Open
kirsle wants to merge 2 commits from actor-collision into master
5 changed files with 85 additions and 22 deletions
Showing only changes of commit 618d4b07c5 - Show all commits

View File

@ -95,7 +95,7 @@ var (
AutoSaveInterval = 5 * time.Minute AutoSaveInterval = 5 * time.Minute
// Default player character doodad in Play Mode. // Default player character doodad in Play Mode.
PlayerCharacterDoodad = "boy.doodad" PlayerCharacterDoodad = "example-mario.doodad"
// Levelpack and level names for the title screen. // Levelpack and level names for the title screen.
DemoLevelPack = "assets/levelpacks/builtin-Tutorial.levelpack" DemoLevelPack = "assets/levelpacks/builtin-Tutorial.levelpack"

View File

@ -24,7 +24,9 @@ func GetBoundingRect(a Actor) render.Rect {
// account their self-declared collision hitbox. // account their self-declared collision hitbox.
// //
// The rect returned has the X,Y coordinate set to the actor's position, plus // The rect returned has the X,Y coordinate set to the actor's position, plus
// the X,Y of their hitbox, if any. // the X,Y of their hitbox, if any. For example, their sprite size could be 64x32
// and their hitbox is the lower 0,32,32,32 half. This function would return the
// world coordinate of where their bounding box begins.
// //
// The W,H of the rect is the W,H of their declared hitbox. // The W,H of the rect is the W,H of their declared hitbox.
// //

View File

@ -117,7 +117,7 @@ func BoxCollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Co
// Adjust the actor's bounding rect by its stated Hitbox from its script. // Adjust the actor's bounding rect by its stated Hitbox from its script.
// e.g.: Boy's Canvas size is 56x56 but he is a narrower character with a // e.g.: Boy's Canvas size is 56x56 but he is a narrower character with a
// hitbox width smaller than its Canvas size. // hitbox width smaller than its Canvas size.
S = SizePlusHitbox(GetBoundingRect(d), hitbox) S = GetBoundingRectHitbox(d, hitbox)
actorHeight := P.Y + S.H actorHeight := P.Y + S.H
// Test if we are ALREADY colliding with level geometry and try and wiggle // Test if we are ALREADY colliding with level geometry and try and wiggle

View File

@ -158,13 +158,12 @@ func (d *Doodle) DrawCollisionBox(canvas *uix.Canvas, actor *uix.Actor) {
} }
var ( var (
rect = collision.GetBoundingRect(actor) rect = collision.GetBoundingRect(actor)
box = collision.GetCollisionBox(rect) box = collision.GetCollisionBox(rect)
hitbox = actor.Hitbox()
) )
// Adjust the actor's bounding rect by its stated Hitbox from its script. // Adjust the actor's bounding rect by its stated Hitbox from its script.
rect = collision.SizePlusHitbox(rect, hitbox) rect = collision.GetBoundingRectHitbox(actor, actor.Hitbox())
box = collision.GetCollisionBox(rect) box = collision.GetCollisionBox(rect)

View File

@ -31,6 +31,7 @@ func (w *Canvas) loopActorCollision() error {
// collision later, store each actor's original position before the move. // collision later, store each actor's original position before the move.
boxes = make([]render.Rect, len(w.actors)) boxes = make([]render.Rect, len(w.actors))
originalPositions = map[string]render.Point{} originalPositions = map[string]render.Point{}
originalHitboxes = map[string]render.Rect{} // original world hitboxes
) )
// Loop over all the actors in parallel, processing their movement and // Loop over all the actors in parallel, processing their movement and
@ -48,6 +49,7 @@ func (w *Canvas) loopActorCollision() error {
func(i int, a *Actor) { func(i int, a *Actor) {
// defer wg.Done() // defer wg.Done()
originalPositions[a.ID()] = a.Position() originalPositions[a.ID()] = a.Position()
originalHitboxes[a.ID()] = collision.GetBoundingRectHitbox(a, a.Hitbox())
// Advance any animations for this actor. // Advance any animations for this actor.
if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) { if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) {
@ -130,11 +132,13 @@ func (w *Canvas) loopActorCollision() error {
w.loopContainActorsInsideLevel(a) w.loopContainActorsInsideLevel(a)
// Store this actor's bounding box after they've moved. // Store this actor's bounding box after they've moved.
boxes[i] = collision.SizePlusHitbox(collision.GetBoundingRect(a), a.Hitbox()) boxes[i] = collision.GetBoundingRect(a)
}(i, a) }(i, a)
// wg.Wait() // wg.Wait()
} }
// log.Warn("== BEGIN BetweenBoxes")
var collidingActors = map[*Actor]*Actor{} var collidingActors = map[*Actor]*Actor{}
for tuple := range collision.BetweenBoxes(boxes) { for tuple := range collision.BetweenBoxes(boxes) {
a, b := w.actors[tuple.A], w.actors[tuple.B] a, b := w.actors[tuple.A], w.actors[tuple.B]
@ -146,13 +150,16 @@ func (w *Canvas) loopActorCollision() error {
collidingActors[a] = b collidingActors[a] = b
// log.Error("between boxes: %+v <%s> <%s>", tuple, a.ID(), b.ID()) log.Error("between boxes: %+v A=<%s> B=<%s>", tuple, a.ID(), b.ID())
// Call the OnCollide handler for A informing them of B's intersection. // Call the OnCollide handler for A informing them of B's intersection.
if w.scripting != nil { if w.scripting != nil {
var ( var (
rect = collision.SizePlusHitbox(collision.GetBoundingRect(b), b.Hitbox()) rect = collision.GetBoundingRectHitbox(b, b.Hitbox())
// lastGoodBox = rect
lastGoodBox = render.Rect{ lastGoodBox = render.Rect{
// Level Positions of the doodad is based on the top left
// of its graphical sprite, not its (possibly offset) hitbox.
X: originalPositions[b.ID()].X, X: originalPositions[b.ID()].X,
Y: originalPositions[b.ID()].Y, Y: originalPositions[b.ID()].Y,
W: boxes[tuple.B].W, W: boxes[tuple.B].W,
@ -180,7 +187,7 @@ func (w *Canvas) loopActorCollision() error {
// only return false if it protests the movement, but not trigger // only return false if it protests the movement, but not trigger
// any actions (such as emit messages to linked doodads) until // any actions (such as emit messages to linked doodads) until
// Settled=true. // Settled=true.
if origPoint, ok := originalPositions[b.ID()]; ok { if origHitbox, ok := originalHitboxes[b.ID()]; ok {
// Trace a vector back from the actor's current position // Trace a vector back from the actor's current position
// to where they originated from. If A protests B's position at // to where they originated from. If A protests B's position at
// ANY time, we mark didProtest=true and continue backscanning // ANY time, we mark didProtest=true and continue backscanning
@ -192,15 +199,26 @@ func (w *Canvas) loopActorCollision() error {
// horizontal movement on the X axis. // horizontal movement on the X axis.
// Touching the solid actor from the side is already fine. // Touching the solid actor from the side is already fine.
var onTop = false var onTop = false
var onBottom = false // they hit the bottom instead
var ( var (
lockX int lockX int
lockY int lockY int
) )
// If their original hitbox is offset from their sprite corner,
// gather the offset now.
var (
origPosition = originalPositions[b.ID()]
hitboxPadding = render.Point{
X: render.AbsInt(origHitbox.X - origPosition.X),
Y: render.AbsInt(origHitbox.Y - origPosition.Y),
}
)
for point := range render.IterLine( for point := range render.IterLine(
origPoint, origHitbox.Point(),
b.Position(), b.Position(), // TODO: verify non 0,0 hitbox doodads work
) { ) {
point := point point := point
test := render.Rect{ test := render.Rect{
@ -211,45 +229,84 @@ func (w *Canvas) loopActorCollision() error {
} }
if info, err := collision.CompareBoxes(boxes[tuple.A], test); err == nil { if info, err := collision.CompareBoxes(boxes[tuple.A], test); err == nil {
// A and B have their drawings overlapping on the page. Get each
// of their declared hitboxes (if smaller) to see if their hitboxes
// intersect as well.
var (
aHitbox = collision.GetBoundingRectHitbox(a, a.Hitbox())
bHitbox = collision.GetBoundingRectHitbox(b, b.Hitbox())
)
// B is overlapping A's box, call its OnCollide handler // B is overlapping A's box, call its OnCollide handler
// with Settled=false and see if it protests the overlap. // with Settled=false and see if it protests the overlap.
err := w.scripting.To(a.ID()).Events.RunCollide(&CollideEvent{ err := w.scripting.To(a.ID()).Events.RunCollide(&CollideEvent{
Actor: b, Actor: b,
Overlap: info.Overlap, Overlap: info.Overlap,
InHitbox: info.Overlap.Intersects(a.Hitbox()), InHitbox: aHitbox.Intersects(bHitbox),
Settled: false, Settled: false,
}) })
// log.Warn("ActorCollision: CompareBoxes info was %+v", info)
// Did A protest? // Did A protest?
if err == scripting.ErrReturnFalse { if err == scripting.ErrReturnFalse {
// Are they on top? // Are they on top?
aHitbox := collision.SizePlusHitbox(collision.GetBoundingRect(a), a.Hitbox()) var (
if render.AbsInt(test.Y+test.H-aHitbox.Y) == 0 { aHitbox = collision.GetBoundingRectHitbox(a, a.Hitbox())
// log.Error("ActorCollision: onTop=true at Y=%s", test.Y) bBottom = test.Y + test.H // bottom of falling actor
aTop = aHitbox.Y
aBottom = aHitbox.Y + aHitbox.H
bTop = test.Y
)
// Is the colliding actor on top? (B=player character)
if render.AbsInt(bBottom-aTop) < 4 {
log.Error("ActorCollision: onTop=true at Y=%d", test.Y)
onTop = true onTop = true
onTopY = test.Y onTopY = aHitbox.Y
}
// Or are they hitting from below?
if render.AbsInt(aBottom-bTop) < 4 {
log.Info("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")
log.Info("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")
log.Info("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")
log.Error("ActorCollision: hit the bottom at Y=%d", test.Y)
onBottom = true
} }
// What direction were we moving? // What direction were we moving?
if test.Y != lastGoodBox.Y { if test.Y != lastGoodBox.Y {
if lockY == 0 { if lockY == 0 {
lockY = lastGoodBox.Y lockY = lastGoodBox.Y
if onBottom {
lockY = lastGoodBox.Y - hitboxPadding.Y
}
log.Error("### Set LockY = %d", lockY)
} }
if onTop { if onTop {
// log.Error("ActorCollision: setGrounded(true)", test.Y) log.Error("ActorCollision: setGrounded(true) at Y=%d", test.Y)
b.SetGrounded(true) b.SetGrounded(true)
} }
} }
if test.X != lastGoodBox.X { if test.X != lastGoodBox.X {
if !onTop { if lockX == 0 && !(onTop || onBottom) {
// lockY = lastGoodBox.Y - (hitboxPadding.Y / 2)
lockX = lastGoodBox.X lockX = lastGoodBox.X
} }
} }
// Move them back to the last good box. // Move them back to the last good box.
lastGoodBox = test lastGoodBox = render.Rect{
X: test.X - hitboxPadding.X,
Y: test.Y - hitboxPadding.Y,
W: test.W,
H: test.H,
}
if lockX != 0 { if lockX != 0 {
lastGoodBox.X = lockX // lockY = lastGoodBox.Y + hitboxPadding.Y
lastGoodBox.X = lockX - hitboxPadding.X
} }
} else { } else {
if err != nil { if err != nil {
@ -273,6 +330,9 @@ func (w *Canvas) loopActorCollision() error {
} }
if !b.noclip { if !b.noclip {
log.Error("Move B to: %s", lastGoodBox.Point())
// The stationary doodad should move the moving one only.
b.MoveTo(lastGoodBox.Point()) b.MoveTo(lastGoodBox.Point())
} }
} else { } else {
@ -313,6 +373,8 @@ func (w *Canvas) loopActorCollision() error {
} }
} }
log.Warn("-- END BetweenBoxes")
// Check for lacks of collisions since last frame. // Check for lacks of collisions since last frame.
for sourceActor, targetActor := range w.collidingActors { for sourceActor, targetActor := range w.collidingActors {
if _, ok := collidingActors[sourceActor]; !ok { if _, ok := collidingActors[sourceActor]; !ok {