From 784045b16adfe457a1d324c7f389deaf07e3013d Mon Sep 17 00:00:00 2001 From: MoltenWolfCub Date: Fri, 7 Jul 2023 20:36:26 +0100 Subject: [PATCH 1/3] implemented river priorities when jumping a river it will now prioritise the shortest jump that can be made rather than the first one found in the internal representation. This makes the gameplay more predictable as it behaves based on things the user can manipulate. Also made river jumping work on the individual segments instead of only the whole river. --- game/player.go | 93 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/game/player.go b/game/player.go index 177142c..7ace12f 100644 --- a/game/player.go +++ b/game/player.go @@ -5,6 +5,7 @@ import ( _ "embed" "image" _ "image/png" + "math" "github.com/hajimehoshi/ebiten/v2" "github.com/moltenwolfcub/Forest-Game/assets" @@ -57,15 +58,17 @@ func (p Player) DrawAt(screen *ebiten.Image, pos image.Point) { func (p Player) Overlaps(layer GameContext, other []image.Rectangle) bool { return DefaultHitboxOverlaps(layer, p, other) } -func (p Player) Origin(GameContext) image.Point { - return p.hitbox.Min +func (p Player) Origin(layer GameContext) image.Point { + bounds := p.findBounds(layer) + return bounds.Min } -func (p Player) Size(GameContext) image.Point { - return p.hitbox.Size() +func (p Player) Size(layer GameContext) image.Point { + bounds := p.findBounds(layer) + return bounds.Size() } func (p Player) GetHitbox(layer GameContext) []image.Rectangle { switch layer { - case Collision: + case Collision, Interaction: baseSize := p.hitbox.Size().Y / 2 playerRect := image.Rectangle{ @@ -82,6 +85,19 @@ func (p Player) GetHitbox(layer GameContext) []image.Rectangle { } } +func (p Player) findBounds(layer GameContext) image.Rectangle { + minX, minY := math.MaxFloat64, math.MaxFloat64 + maxX, maxY := -math.MaxFloat64, -math.MaxFloat64 + for _, seg := range p.GetHitbox(layer) { + minX = math.Min(float64(seg.Min.X), minX) + minY = math.Min(float64(seg.Min.Y), minY) + maxX = math.Max(float64(seg.Max.X), maxX) + maxY = math.Max(float64(seg.Max.Y), maxY) + } + bounds := image.Rect(int(minX), int(minY), int(maxX), int(maxY)) + return bounds +} + func (p Player) GetZ() int { return 0 } @@ -97,23 +113,68 @@ func (p *Player) Update(collidables []HasHitbox, climbables []Climbable, rivers func (p *Player) handleInteractions(interactables []HasHitbox) { if p.RiverJumping { - var objectToJump HasHitbox = nil - for _, c := range interactables { - if p.Overlaps(Interaction, c.GetHitbox(Interaction)) { - objectToJump = c - break - } + newPos, found := p.GetSmallestJump(interactables) + + if found { + p.hitbox = p.hitbox.Sub(p.hitbox.Min).Add(newPos) + offset := p.Origin(Collision).Y - p.hitbox.Min.Y + p.hitbox = p.hitbox.Sub(image.Pt(0, offset)) } - if objectToJump == nil { - return + } +} + +func (p Player) GetSmallestJump(jumpables []HasHitbox) (point image.Point, found bool) { + origin := p.Origin(Collision) + size := p.Size(Collision) + hitbox := p.GetHitbox(Collision) + + smallestJumpDist := math.MaxFloat64 + smallestJump := image.Point{} + for _, jumpable := range jumpables { + if !jumpable.Overlaps(Interaction, hitbox) { + continue } - //for now jump to top corner will need to properly re-implement at some point - newPos := objectToJump.Origin(Collision).Sub(image.Point{p.hitbox.Dx(), p.hitbox.Dy()}) + for id, seg := range jumpable.GetHitbox(Interaction) { + if !p.Overlaps(Collision, []image.Rectangle{seg}) { + continue + } + segHitbox := jumpable.GetHitbox(Collision)[id] + + if origin.X >= segHitbox.Max.X { //right + newPoint := image.Pt(segHitbox.Min.X, origin.Y) + newPoint = newPoint.Sub(image.Pt(size.X, 0)) - p.hitbox = p.hitbox.Sub(p.hitbox.Min).Add(newPos) + updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) + } else if origin.X <= segHitbox.Min.X { //left + newPoint := image.Pt(segHitbox.Max.X, origin.Y) + + updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) + } + + if origin.Y >= segHitbox.Max.Y { //bottom + newPoint := image.Pt(origin.X, segHitbox.Min.Y) + newPoint = newPoint.Sub(image.Pt(0, size.Y)) + + updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) + } else if origin.Y <= segHitbox.Min.Y { //top + newPoint := image.Pt(origin.X, segHitbox.Max.Y) + + updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) + } + } } + return smallestJump, smallestJumpDist != math.MaxFloat64 +} + +func updateJumpIfSmaller(before image.Point, new image.Point, dist *float64, point *image.Point) { + delta := math.Hypot(float64(before.X-new.X), float64(before.Y-new.Y)) + if delta < *dist { + *point = new + *dist = delta + } + } func (p Player) calculateMovementSpeed(currentClimable Climbable) (speed float64) { From bd1886025a2834bb3327ba2a6eab6618bab679c8 Mon Sep 17 00:00:00 2001 From: MoltenWolfCub Date: Fri, 7 Jul 2023 21:09:36 +0100 Subject: [PATCH 2/3] Fixed river Clipping when crossing a river if there are multiple segments that overlap and create a longer compound segment the player jumps across the full length rather than getting stuck in the middle of a river and then having weird jump behaviour (bug #1). --- game/player.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/game/player.go b/game/player.go index 7ace12f..fabe189 100644 --- a/game/player.go +++ b/game/player.go @@ -143,24 +143,94 @@ func (p Player) GetSmallestJump(jumpables []HasHitbox) (point image.Point, found segHitbox := jumpable.GetHitbox(Collision)[id] if origin.X >= segHitbox.Max.X { //right - newPoint := image.Pt(segHitbox.Min.X, origin.Y) - newPoint = newPoint.Sub(image.Pt(size.X, 0)) + newPoint := image.Pt(segHitbox.Min.X, origin.Y).Sub(image.Pt(size.X, 0)) + + newRect := image.Rectangle{ + Min: newPoint, + Max: newPoint.Add(size), + } + for jumpable.Overlaps(Collision, []image.Rectangle{newRect}) { + for _, segInner := range jumpable.GetHitbox(Collision) { + if !newRect.Overlaps(segInner) { + continue + } + newPoint = image.Pt(segInner.Min.X, origin.Y).Sub(image.Pt(size.X, 0)) + break + } + newRect = image.Rectangle{ + Min: newPoint, + Max: newPoint.Add(size), + } + } updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) } else if origin.X <= segHitbox.Min.X { //left newPoint := image.Pt(segHitbox.Max.X, origin.Y) + newRect := image.Rectangle{ + Min: newPoint, + Max: newPoint.Add(size), + } + for jumpable.Overlaps(Collision, []image.Rectangle{newRect}) { + for _, segInner := range jumpable.GetHitbox(Collision) { + if !newRect.Overlaps(segInner) { + continue + } + newPoint = image.Pt(segInner.Max.X, origin.Y) + break + } + newRect = image.Rectangle{ + Min: newPoint, + Max: newPoint.Add(size), + } + } + updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) } if origin.Y >= segHitbox.Max.Y { //bottom - newPoint := image.Pt(origin.X, segHitbox.Min.Y) - newPoint = newPoint.Sub(image.Pt(0, size.Y)) + newPoint := image.Pt(origin.X, segHitbox.Min.Y).Sub(image.Pt(0, size.Y)) + + newRect := image.Rectangle{ + Min: newPoint, + Max: newPoint.Add(size), + } + for jumpable.Overlaps(Collision, []image.Rectangle{newRect}) { + for _, segInner := range jumpable.GetHitbox(Collision) { + if !newRect.Overlaps(segInner) { + continue + } + newPoint = image.Pt(origin.X, segInner.Min.Y).Sub(image.Pt(0, size.Y)) + break + } + newRect = image.Rectangle{ + Min: newPoint, + Max: newPoint.Add(size), + } + } updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) } else if origin.Y <= segHitbox.Min.Y { //top newPoint := image.Pt(origin.X, segHitbox.Max.Y) + newRect := image.Rectangle{ + Min: newPoint, + Max: newPoint.Add(size), + } + for jumpable.Overlaps(Collision, []image.Rectangle{newRect}) { + for _, segInner := range jumpable.GetHitbox(Collision) { + if !newRect.Overlaps(segInner) { + continue + } + newPoint = image.Pt(origin.X, segInner.Max.Y) + break + } + newRect = image.Rectangle{ + Min: newPoint, + Max: newPoint.Add(size), + } + } + updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) } } @@ -174,7 +244,6 @@ func updateJumpIfSmaller(before image.Point, new image.Point, dist *float64, poi *point = new *dist = delta } - } func (p Player) calculateMovementSpeed(currentClimable Climbable) (speed float64) { From c9e42e9f4704ef20f84788817ce217b638f28d4c Mon Sep 17 00:00:00 2001 From: MoltenWolfCub Date: Sat, 8 Jul 2023 09:07:13 +0100 Subject: [PATCH 3/3] Refactored `player.GetSmallestJump()` Similar code was being used to test for each direction of jump so refactored out the similar code into a function that could be used for all 4 directions. Works by passing functions that calculate how to jump an object to a generic jump test function. This means each direction can jump in its own way while using the same jump code --- game/player.go | 143 ++++++++++++++++++++++--------------------------- 1 file changed, 65 insertions(+), 78 deletions(-) diff --git a/game/player.go b/game/player.go index fabe189..49b9565 100644 --- a/game/player.go +++ b/game/player.go @@ -127,12 +127,11 @@ func (p *Player) handleInteractions(interactables []HasHitbox) { func (p Player) GetSmallestJump(jumpables []HasHitbox) (point image.Point, found bool) { origin := p.Origin(Collision) size := p.Size(Collision) - hitbox := p.GetHitbox(Collision) smallestJumpDist := math.MaxFloat64 smallestJump := image.Point{} for _, jumpable := range jumpables { - if !jumpable.Overlaps(Interaction, hitbox) { + if !jumpable.Overlaps(Interaction, p.GetHitbox(Collision)) { continue } @@ -143,93 +142,49 @@ func (p Player) GetSmallestJump(jumpables []HasHitbox) (point image.Point, found segHitbox := jumpable.GetHitbox(Collision)[id] if origin.X >= segHitbox.Max.X { //right - newPoint := image.Pt(segHitbox.Min.X, origin.Y).Sub(image.Pt(size.X, 0)) - - newRect := image.Rectangle{ - Min: newPoint, - Max: newPoint.Add(size), - } - for jumpable.Overlaps(Collision, []image.Rectangle{newRect}) { - for _, segInner := range jumpable.GetHitbox(Collision) { - if !newRect.Overlaps(segInner) { - continue - } - newPoint = image.Pt(segInner.Min.X, origin.Y).Sub(image.Pt(size.X, 0)) - break - } - newRect = image.Rectangle{ - Min: newPoint, - Max: newPoint.Add(size), - } - } + newPoint := testJump(jumpable, segHitbox, + func(segment image.Rectangle) image.Point { + return image.Pt(segment.Min.X, origin.Y).Sub(image.Pt(size.X, 0)) + }, + func(origin image.Point) image.Rectangle { + return image.Rectangle{origin, origin.Add(size)} + }, + ) updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) } else if origin.X <= segHitbox.Min.X { //left - newPoint := image.Pt(segHitbox.Max.X, origin.Y) - - newRect := image.Rectangle{ - Min: newPoint, - Max: newPoint.Add(size), - } - for jumpable.Overlaps(Collision, []image.Rectangle{newRect}) { - for _, segInner := range jumpable.GetHitbox(Collision) { - if !newRect.Overlaps(segInner) { - continue - } - newPoint = image.Pt(segInner.Max.X, origin.Y) - break - } - newRect = image.Rectangle{ - Min: newPoint, - Max: newPoint.Add(size), - } - } + newPoint := testJump(jumpable, segHitbox, + func(segment image.Rectangle) image.Point { + return image.Pt(segment.Max.X, origin.Y) + }, + func(origin image.Point) image.Rectangle { + return image.Rectangle{origin, origin.Add(size)} + }, + ) updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) } if origin.Y >= segHitbox.Max.Y { //bottom - newPoint := image.Pt(origin.X, segHitbox.Min.Y).Sub(image.Pt(0, size.Y)) - - newRect := image.Rectangle{ - Min: newPoint, - Max: newPoint.Add(size), - } - for jumpable.Overlaps(Collision, []image.Rectangle{newRect}) { - for _, segInner := range jumpable.GetHitbox(Collision) { - if !newRect.Overlaps(segInner) { - continue - } - newPoint = image.Pt(origin.X, segInner.Min.Y).Sub(image.Pt(0, size.Y)) - break - } - newRect = image.Rectangle{ - Min: newPoint, - Max: newPoint.Add(size), - } - } + newPoint := testJump(jumpable, segHitbox, + func(segment image.Rectangle) image.Point { + return image.Pt(origin.X, segment.Min.Y).Sub(image.Pt(0, size.Y)) + }, + func(origin image.Point) image.Rectangle { + return image.Rectangle{origin, origin.Add(size)} + }, + ) updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) } else if origin.Y <= segHitbox.Min.Y { //top - newPoint := image.Pt(origin.X, segHitbox.Max.Y) - - newRect := image.Rectangle{ - Min: newPoint, - Max: newPoint.Add(size), - } - for jumpable.Overlaps(Collision, []image.Rectangle{newRect}) { - for _, segInner := range jumpable.GetHitbox(Collision) { - if !newRect.Overlaps(segInner) { - continue - } - newPoint = image.Pt(origin.X, segInner.Max.Y) - break - } - newRect = image.Rectangle{ - Min: newPoint, - Max: newPoint.Add(size), - } - } + newPoint := testJump(jumpable, segHitbox, + func(segment image.Rectangle) image.Point { + return image.Pt(origin.X, segment.Max.Y) + }, + func(origin image.Point) image.Rectangle { + return image.Rectangle{origin, origin.Add(size)} + }, + ) updateJumpIfSmaller(origin, newPoint, &smallestJumpDist, &smallestJump) } @@ -238,6 +193,38 @@ func (p Player) GetSmallestJump(jumpables []HasHitbox) (point image.Point, found return smallestJump, smallestJumpDist != math.MaxFloat64 } +// Finds the location the player would jump to taking into account multiple +// segments that might need to be jumped. If the player would land in another +// segment it continues to test further jumps on that new segment. +// +// Once that new land location is found it gets returned. +// +// makeJump returns the origin of the new player hitbox after jumping the given +// segment. This function handles how the jump should be made (what direction) +// +// pRect generates a version of the player's hitbox to test for collisions after +// each jump without actually moving the player yet. It should just return a hitbox +// of the player's size with it's origin at the provided point. +func testJump(fullObj HasHitbox, jumpSeg image.Rectangle, makeJump func(image.Rectangle) image.Point, pRect func(image.Point) image.Rectangle) image.Point { + newPoint := makeJump(jumpSeg) + + newRect := pRect(newPoint) + for fullObj.Overlaps(Collision, []image.Rectangle{newRect}) { + for _, newSegTest := range fullObj.GetHitbox(Collision) { + if !newRect.Overlaps(newSegTest) { + continue + } + newPoint = makeJump(newSegTest) + break + } + newRect = pRect(newPoint) + } + return newPoint +} + +// Calculates jump distance between points before and new and updates the +// pointers with the new distance and point respectively if the new distance +// is smaller than the previous smallest. func updateJumpIfSmaller(before image.Point, new image.Point, dist *float64, point *image.Point) { delta := math.Hypot(float64(before.X-new.X), float64(before.Y-new.Y)) if delta < *dist {