Refactored use of SAT collision checking.

This commit is contained in:
genxium 2022-10-19 17:32:18 +08:00
parent 3baaf1d52c
commit 1959a7fd9a
4 changed files with 211 additions and 102 deletions

View File

@ -4,6 +4,7 @@ import (
. "dnmshared" . "dnmshared"
"fmt" "fmt"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/kvartborg/vector"
"github.com/solarlune/resolv" "github.com/solarlune/resolv"
"go.uber.org/zap" "go.uber.org/zap"
"image/color" "image/color"
@ -56,7 +57,7 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi
if moveToCollide { if moveToCollide {
toTestPlayerCollider := playerColliders[0] toTestPlayerCollider := playerColliders[0]
oldDx := 0.0 oldDx := 0.0
oldDy := 180.0 oldDy := 135.0
dx := oldDx dx := oldDx
dy := oldDy dy := oldDy
if collision := toTestPlayerCollider.Check(oldDx, oldDy, "Barrier"); collision != nil { if collision := toTestPlayerCollider.Check(oldDx, oldDy, "Barrier"); collision != nil {
@ -64,11 +65,26 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi
barrierShape := collision.Objects[0].Shape.(*resolv.ConvexPolygon) barrierShape := collision.Objects[0].Shape.(*resolv.ConvexPolygon)
origX, origY := playerShape.Position() origX, origY := playerShape.Position()
playerShape.SetPosition(origX+oldDx, origY+oldDy) playerShape.SetPosition(origX+oldDx, origY+oldDy)
if mtv := CalculateMTVForConvexPolygon(playerShape, barrierShape); mtv != nil { if colliding := IsPolygonPairColliding(playerShape, barrierShape, nil); colliding {
Logger.Info(fmt.Sprintf("Collided: shape=%v, oldDx=%v, oldDy=%v, MTV=%v", toTestPlayerCollider.Shape, oldDx, oldDy, mtv)) Logger.Info(fmt.Sprintf("Collided: playerShape=%v, oldDx=%v, oldDy=%v", playerShape, oldDx, oldDy))
//dx, dy = mtv[0], mtv[1] overlapResult := &SatResult{
Overlap: 0,
OverlapX: 0,
OverlapY: 0,
AContainedInB: true,
BContainedInA: true,
Axis: vector.Vector{0, 0},
}
e := vector.Vector{oldDx, oldDy}.Unit()
if separatableAlongMovement := IsPolygonPairSeparatedByDir(playerShape, barrierShape, e, overlapResult); !separatableAlongMovement {
pushbackX, pushbackY := overlapResult.Overlap*overlapResult.OverlapX, overlapResult.Overlap*overlapResult.OverlapY
Logger.Info(fmt.Sprintf("Collided: playerShape=%v, oldDx=%v, oldDy=%v, toCheckBarrier=%v, pushbackX=%v, pushbackY=%v", playerShape, oldDx, oldDy, barrierShape, pushbackX, pushbackY))
dx, dy = oldDx-pushbackX, oldDy-pushbackY
} else {
Logger.Info(fmt.Sprintf("Not Collided: playerShape=%v, oldDx=%v, oldDy=%v, toCheckBarrier=%v, e=%v", playerShape, oldDx, oldDy, barrierShape, e))
}
} else { } else {
Logger.Info(fmt.Sprintf("Collided: shape=%v, oldDx=%v, oldDy=%v, toCheckBarrier=%v, not intersecting", toTestPlayerCollider.Shape, oldDx, oldDy, barrierShape)) Logger.Info(fmt.Sprintf("Not collided: playerShape=%v, oldDx=%v, oldDy=%v, toCheckBarrier=%v", playerShape, oldDx, oldDy, barrierShape))
} }
playerShape.SetPosition(origX, origY) playerShape.SetPosition(origX, origY)

View File

@ -15,6 +15,10 @@ type Vec2D struct {
Y float64 `json:"y,omitempty"` Y float64 `json:"y,omitempty"`
} }
func NormVec2D(dx, dy float64) Vec2D {
return Vec2D{dy, -dx}
}
type Polygon2D struct { type Polygon2D struct {
Anchor *Vec2D `json:"-"` // This "Polygon2D.Anchor" is used to be assigned to "B2BodyDef.Position", which in turn is used as the position of the FIRST POINT of the polygon. Anchor *Vec2D `json:"-"` // This "Polygon2D.Anchor" is used to be assigned to "B2BodyDef.Position", which in turn is used as the position of the FIRST POINT of the polygon.
Points []*Vec2D `json:"-"` Points []*Vec2D `json:"-"`

View File

@ -3,80 +3,169 @@ package dnmshared
import ( import (
"github.com/kvartborg/vector" "github.com/kvartborg/vector"
"github.com/solarlune/resolv" "github.com/solarlune/resolv"
"math" "math"
) )
func GenerateRectCollider(origX, origY, w, h, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object { func GenerateRectCollider(origX, origY, w, h, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object {
collider := resolv.NewObject(origX-w*0.5+spaceOffsetX, origY-h*0.5+spaceOffsetY, w, h, tag) collider := resolv.NewObject(origX-w*0.5+spaceOffsetX, origY-h*0.5+spaceOffsetY, w, h, tag)
shape := resolv.NewRectangle(0, 0, w, h) shape := resolv.NewRectangle(0, 0, w, h)
collider.SetShape(shape) collider.SetShape(shape)
return collider return collider
} }
func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object { func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object {
aligned := AlignPolygon2DToBoundingBox(unalignedSrc) aligned := AlignPolygon2DToBoundingBox(unalignedSrc)
var w, h float64 = 0, 0 var w, h float64 = 0, 0
shape := resolv.NewConvexPolygon() shape := resolv.NewConvexPolygon()
for i, pi := range aligned.Points { for i, pi := range aligned.Points {
for j, pj := range aligned.Points { for j, pj := range aligned.Points {
if i == j { if i == j {
continue continue
} }
if math.Abs(pj.X-pi.X) > w { if math.Abs(pj.X-pi.X) > w {
w = math.Abs(pj.X - pi.X) w = math.Abs(pj.X - pi.X)
} }
if math.Abs(pj.Y-pi.Y) > h { if math.Abs(pj.Y-pi.Y) > h {
h = math.Abs(pj.Y - pi.Y) h = math.Abs(pj.Y - pi.Y)
} }
} }
} }
for i := 0; i < len(aligned.Points); i++ { for i := 0; i < len(aligned.Points); i++ {
p := aligned.Points[i] p := aligned.Points[i]
shape.AddPoints(p.X, p.Y) shape.AddPoints(p.X, p.Y)
} }
collider := resolv.NewObject(aligned.Anchor.X+spaceOffsetX, aligned.Anchor.Y+spaceOffsetY, w, h, tag) collider := resolv.NewObject(aligned.Anchor.X+spaceOffsetX, aligned.Anchor.Y+spaceOffsetY, w, h, tag)
collider.SetShape(shape) collider.SetShape(shape)
return collider return collider
} }
func CalculateMTVForConvexPolygon(cp *resolv.ConvexPolygon, other *resolv.ConvexPolygon) vector.Vector { type SatResult struct {
delta := vector.Vector{0, 0} Overlap float64
OverlapX float64
smallest := vector.Vector{math.MaxFloat64, 0} OverlapY float64
AContainedInB bool
for _, axis := range cp.SATAxes() { BContainedInA bool
if !cp.Project(axis).Overlapping(other.Project(axis)) { Axis vector.Vector
return nil }
}
func IsPolygonPairColliding(a, b *resolv.ConvexPolygon, result *SatResult) bool {
overlap := cp.Project(axis).Overlap(other.Project(axis)) aCnt, bCnt := len(a.Points), len(b.Points)
// Single point case
if smallest.Magnitude() > overlap { if 1 == aCnt && 1 == bCnt {
smallest = axis.Scale(overlap) if nil != result {
} result.Overlap = 0
}
} return a.Points[0].X() == b.Points[0].X() && a.Points[0].Y() == b.Points[0].Y()
}
for _, axis := range other.SATAxes() {
if 1 < aCnt {
if !cp.Project(axis).Overlapping(other.Project(axis)) { for _, axis := range a.SATAxes() {
return nil if IsPolygonPairSeparatedByDir(a, b, axis.Unit(), result) {
} return false
}
overlap := cp.Project(axis).Overlap(other.Project(axis)) }
}
if smallest.Magnitude() > overlap {
smallest = axis.Scale(overlap) if 1 < bCnt {
} for _, axis := range b.SATAxes() {
if IsPolygonPairSeparatedByDir(a, b, axis.Unit(), result) {
} return false
}
delta[0] = smallest[0] }
delta[1] = smallest[1] }
return delta return true
}
func IsPolygonPairSeparatedByDir(a, b *resolv.ConvexPolygon, e vector.Vector, result *SatResult) bool {
var aStart, aEnd, bStart, bEnd float64 = math.MaxFloat64, -math.MaxFloat64, math.MaxFloat64, -math.MaxFloat64
for _, p := range a.Points {
dot := p.X()*e.X() + p.Y()*e.Y()
if aStart > dot {
aStart = dot
}
if aEnd < dot {
aEnd = dot
}
}
for _, p := range b.Points {
dot := p.X()*e.X() + p.Y()*e.Y()
if bStart > dot {
bStart = dot
}
if bEnd < dot {
bEnd = dot
}
}
if aStart > bEnd || aEnd < bStart {
// Separated by unit vector "e"
return true
}
if nil != result {
result.Axis = e
overlap := float64(0)
if aStart < bStart {
result.AContainedInB = false
if aEnd < bEnd {
overlap = aEnd - bStart
result.BContainedInA = false
} else {
option1 := aEnd - bStart
option2 := bEnd - aStart
if option1 < option2 {
overlap = option1
} else {
overlap = -option2
}
}
} else {
result.BContainedInA = false
if aEnd > bEnd {
overlap = aStart - bEnd
result.AContainedInB = false
} else {
option1 := aEnd - bStart
option2 := bEnd - aStart
if option1 < option2 {
overlap = option1
} else {
overlap = -option2
}
}
}
currentOverlap := result.Overlap
absoluteOverlap := overlap
if overlap < 0 {
absoluteOverlap = -overlap
}
if 0 == currentOverlap || currentOverlap > absoluteOverlap {
var sign float64 = 1
if overlap < 0 {
sign = -1
}
result.Overlap = absoluteOverlap
result.OverlapX = e.X() * sign
result.OverlapY = e.Y() * sign
}
}
// the specified unit vector "e" doesn't separate "a" and "b", overlap result is generated
return false
} }

View File

@ -444,38 +444,38 @@ func (pTmxMapIns *TmxMap) continuousObjLayerOffsetToContinuousMapNodePos(continu
} }
func AlignPolygon2DToBoundingBox(input *Polygon2D) *Polygon2D { func AlignPolygon2DToBoundingBox(input *Polygon2D) *Polygon2D {
// Transform again to put "anchor" at the top-left point of the bounding box for "resolv" // Transform again to put "anchor" at the top-left point of the bounding box for "resolv"
float64Max := float64(99999999999999.9) float64Max := float64(99999999999999.9)
boundingBoxTL := &Vec2D{ boundingBoxTL := &Vec2D{
X: float64Max, X: float64Max,
Y: float64Max, Y: float64Max,
} }
for _, p := range input.Points { for _, p := range input.Points {
if p.X < boundingBoxTL.X { if p.X < boundingBoxTL.X {
boundingBoxTL.X = p.X boundingBoxTL.X = p.X
} }
if p.Y < boundingBoxTL.Y { if p.Y < boundingBoxTL.Y {
boundingBoxTL.Y = p.Y boundingBoxTL.Y = p.Y
} }
} }
// Now "input.Anchor" should move to "input.Anchor+boundingBoxTL", thus "boundingBoxTL" is also the value of the negative diff for all "input.Points" // Now "input.Anchor" should move to "input.Anchor+boundingBoxTL", thus "boundingBoxTL" is also the value of the negative diff for all "input.Points"
output := &Polygon2D{ output := &Polygon2D{
Anchor: &Vec2D{ Anchor: &Vec2D{
X: input.Anchor.X+boundingBoxTL.X, X: input.Anchor.X + boundingBoxTL.X,
Y: input.Anchor.Y+boundingBoxTL.Y, Y: input.Anchor.Y + boundingBoxTL.Y,
}, },
Points: make([]*Vec2D, len(input.Points)), Points: make([]*Vec2D, len(input.Points)),
TileWidth: input.TileWidth, TileWidth: input.TileWidth,
TileHeight: input.TileHeight, TileHeight: input.TileHeight,
} }
for i, p := range input.Points { for i, p := range input.Points {
output.Points[i] = &Vec2D{ output.Points[i] = &Vec2D{
X: p.X-boundingBoxTL.X, X: p.X - boundingBoxTL.X,
Y: p.Y-boundingBoxTL.Y, Y: p.Y - boundingBoxTL.Y,
} }
} }
return output return output
} }