From 1959a7fd9a544752966c3c4dae1ab831ef3f1654 Mon Sep 17 00:00:00 2001 From: genxium Date: Wed, 19 Oct 2022 17:32:18 +0800 Subject: [PATCH] Refactored use of SAT collision checking. --- collider_visualizer/worldColliderDisplay.go | 26 ++- dnmshared/geometry.go | 4 + dnmshared/resolv_helper.go | 217 ++++++++++++++------ dnmshared/tmx_parser.go | 66 +++--- 4 files changed, 211 insertions(+), 102 deletions(-) diff --git a/collider_visualizer/worldColliderDisplay.go b/collider_visualizer/worldColliderDisplay.go index bd4df2e..e345e70 100644 --- a/collider_visualizer/worldColliderDisplay.go +++ b/collider_visualizer/worldColliderDisplay.go @@ -4,6 +4,7 @@ import ( . "dnmshared" "fmt" "github.com/hajimehoshi/ebiten/v2" + "github.com/kvartborg/vector" "github.com/solarlune/resolv" "go.uber.org/zap" "image/color" @@ -56,7 +57,7 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi if moveToCollide { toTestPlayerCollider := playerColliders[0] oldDx := 0.0 - oldDy := 180.0 + oldDy := 135.0 dx := oldDx dy := oldDy 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) origX, origY := playerShape.Position() playerShape.SetPosition(origX+oldDx, origY+oldDy) - if mtv := CalculateMTVForConvexPolygon(playerShape, barrierShape); mtv != nil { - Logger.Info(fmt.Sprintf("Collided: shape=%v, oldDx=%v, oldDy=%v, MTV=%v", toTestPlayerCollider.Shape, oldDx, oldDy, mtv)) - //dx, dy = mtv[0], mtv[1] + if colliding := IsPolygonPairColliding(playerShape, barrierShape, nil); colliding { + Logger.Info(fmt.Sprintf("Collided: playerShape=%v, oldDx=%v, oldDy=%v", playerShape, oldDx, oldDy)) + 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 { - 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) diff --git a/dnmshared/geometry.go b/dnmshared/geometry.go index c1c4edc..575d5f8 100644 --- a/dnmshared/geometry.go +++ b/dnmshared/geometry.go @@ -15,6 +15,10 @@ type Vec2D struct { Y float64 `json:"y,omitempty"` } +func NormVec2D(dx, dy float64) Vec2D { + return Vec2D{dy, -dx} +} + 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. Points []*Vec2D `json:"-"` diff --git a/dnmshared/resolv_helper.go b/dnmshared/resolv_helper.go index a1a1698..a99a9e0 100644 --- a/dnmshared/resolv_helper.go +++ b/dnmshared/resolv_helper.go @@ -3,80 +3,169 @@ package dnmshared import ( "github.com/kvartborg/vector" "github.com/solarlune/resolv" - "math" + "math" ) 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) - shape := resolv.NewRectangle(0, 0, w, h) - collider.SetShape(shape) - return collider + collider := resolv.NewObject(origX-w*0.5+spaceOffsetX, origY-h*0.5+spaceOffsetY, w, h, tag) + shape := resolv.NewRectangle(0, 0, w, h) + collider.SetShape(shape) + return collider } func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object { - aligned := AlignPolygon2DToBoundingBox(unalignedSrc) - var w, h float64 = 0, 0 + aligned := AlignPolygon2DToBoundingBox(unalignedSrc) + var w, h float64 = 0, 0 - shape := resolv.NewConvexPolygon() - for i, pi := range aligned.Points { - for j, pj := range aligned.Points { - if i == j { - continue - } - if math.Abs(pj.X-pi.X) > w { - w = math.Abs(pj.X - pi.X) - } - if math.Abs(pj.Y-pi.Y) > h { - h = math.Abs(pj.Y - pi.Y) - } - } - } + shape := resolv.NewConvexPolygon() + for i, pi := range aligned.Points { + for j, pj := range aligned.Points { + if i == j { + continue + } + if math.Abs(pj.X-pi.X) > w { + w = math.Abs(pj.X - pi.X) + } + if math.Abs(pj.Y-pi.Y) > h { + h = math.Abs(pj.Y - pi.Y) + } + } + } - for i := 0; i < len(aligned.Points); i++ { - p := aligned.Points[i] - shape.AddPoints(p.X, p.Y) - } + for i := 0; i < len(aligned.Points); i++ { + p := aligned.Points[i] + shape.AddPoints(p.X, p.Y) + } - collider := resolv.NewObject(aligned.Anchor.X+spaceOffsetX, aligned.Anchor.Y+spaceOffsetY, w, h, tag) - collider.SetShape(shape) + collider := resolv.NewObject(aligned.Anchor.X+spaceOffsetX, aligned.Anchor.Y+spaceOffsetY, w, h, tag) + collider.SetShape(shape) - return collider + return collider } -func CalculateMTVForConvexPolygon(cp *resolv.ConvexPolygon, other *resolv.ConvexPolygon) vector.Vector { - delta := vector.Vector{0, 0} - - smallest := vector.Vector{math.MaxFloat64, 0} - - for _, axis := range cp.SATAxes() { - if !cp.Project(axis).Overlapping(other.Project(axis)) { - return nil - } - - overlap := cp.Project(axis).Overlap(other.Project(axis)) - - if smallest.Magnitude() > overlap { - smallest = axis.Scale(overlap) - } - - } - - for _, axis := range other.SATAxes() { - - if !cp.Project(axis).Overlapping(other.Project(axis)) { - return nil - } - - overlap := cp.Project(axis).Overlap(other.Project(axis)) - - if smallest.Magnitude() > overlap { - smallest = axis.Scale(overlap) - } - - } - - delta[0] = smallest[0] - delta[1] = smallest[1] - - return delta +type SatResult struct { + Overlap float64 + OverlapX float64 + OverlapY float64 + AContainedInB bool + BContainedInA bool + Axis vector.Vector +} + +func IsPolygonPairColliding(a, b *resolv.ConvexPolygon, result *SatResult) bool { + aCnt, bCnt := len(a.Points), len(b.Points) + // Single point case + if 1 == aCnt && 1 == bCnt { + if nil != result { + result.Overlap = 0 + } + return a.Points[0].X() == b.Points[0].X() && a.Points[0].Y() == b.Points[0].Y() + } + + if 1 < aCnt { + for _, axis := range a.SATAxes() { + if IsPolygonPairSeparatedByDir(a, b, axis.Unit(), result) { + return false + } + } + } + + if 1 < bCnt { + for _, axis := range b.SATAxes() { + if IsPolygonPairSeparatedByDir(a, b, axis.Unit(), result) { + return false + } + } + } + + 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 } diff --git a/dnmshared/tmx_parser.go b/dnmshared/tmx_parser.go index 31c4d50..dccb287 100644 --- a/dnmshared/tmx_parser.go +++ b/dnmshared/tmx_parser.go @@ -444,38 +444,38 @@ func (pTmxMapIns *TmxMap) continuousObjLayerOffsetToContinuousMapNodePos(continu } func AlignPolygon2DToBoundingBox(input *Polygon2D) *Polygon2D { - // Transform again to put "anchor" at the top-left point of the bounding box for "resolv" - float64Max := float64(99999999999999.9) - boundingBoxTL := &Vec2D{ - X: float64Max, - Y: float64Max, - } - for _, p := range input.Points { - if p.X < boundingBoxTL.X { - boundingBoxTL.X = p.X - } - if p.Y < boundingBoxTL.Y { - boundingBoxTL.Y = p.Y - } - } + // Transform again to put "anchor" at the top-left point of the bounding box for "resolv" + float64Max := float64(99999999999999.9) + boundingBoxTL := &Vec2D{ + X: float64Max, + Y: float64Max, + } + for _, p := range input.Points { + if p.X < boundingBoxTL.X { + boundingBoxTL.X = p.X + } + if p.Y < boundingBoxTL.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" - output := &Polygon2D{ - Anchor: &Vec2D{ - X: input.Anchor.X+boundingBoxTL.X, - Y: input.Anchor.Y+boundingBoxTL.Y, - }, - Points: make([]*Vec2D, len(input.Points)), - TileWidth: input.TileWidth, - TileHeight: input.TileHeight, - } + // 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{ + Anchor: &Vec2D{ + X: input.Anchor.X + boundingBoxTL.X, + Y: input.Anchor.Y + boundingBoxTL.Y, + }, + Points: make([]*Vec2D, len(input.Points)), + TileWidth: input.TileWidth, + TileHeight: input.TileHeight, + } - for i, p := range input.Points { - output.Points[i] = &Vec2D{ - X: p.X-boundingBoxTL.X, - Y: p.Y-boundingBoxTL.Y, - } - } - - return output -} + for i, p := range input.Points { + output.Points[i] = &Vec2D{ + X: p.X - boundingBoxTL.X, + Y: p.Y - boundingBoxTL.Y, + } + } + + return output +}