From 3afea0f46c677d5b957e5ba0e88d469168963ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F?= Date: Tue, 10 Dec 2024 02:09:54 +0300 Subject: [PATCH] Ability to control X and Y axes separately for smoothing settings. --- camera.go | 137 +++++++++++++++++++++++++++++------- examples/director/main.go | 45 ++++++++---- examples/platformer/main.go | 54 ++++++++------ util.go | 5 ++ vec2.go | 39 ---------- 5 files changed, 184 insertions(+), 96 deletions(-) create mode 100644 util.go diff --git a/camera.go b/camera.go index 17e3a26..821bdfe 100644 --- a/camera.go +++ b/camera.go @@ -59,7 +59,6 @@ func NewCamera(lookAtX, lookAtY, w, h float64) *Camera { tempTarget: vec2{}, tickSpeed: 1.0 / 60.0, tick: 0, - currentVelocity: vec2{}, } c.LookAt(lookAtX, lookAtY) @@ -90,12 +89,23 @@ func (cam *Camera) LookAt(targetX, targetY float64) { switch cam.Smoothing { case SmoothDamp: - cam.tempTarget = smoothDamp(cam.tempTarget, target, &cam.currentVelocity, - cam.SmoothingOptions.SmoothDampTime, cam.SmoothingOptions.SmoothDampMaxSpeed) + cam.tempTarget = smoothDamp( + cam.tempTarget, + target, + &cam.currentVelocity, + cam.SmoothingOptions.SmoothDampTimeX, + cam.SmoothingOptions.SmoothDampTimeY, + cam.SmoothingOptions.SmoothDampMaxSpeedX, + cam.SmoothingOptions.SmoothDampMaxSpeedY, + ) + // cam.tempTarget.Y = smoothDamp2(cam.tempTarget.Y, targetX, &cam.velY, cam.SmoothingOptions.SmoothDampTimeY, cam.SmoothingOptions.SmoothDampMaxSpeedY) cam.topLeft = cam.tempTarget case Lerp: - cam.tempTarget = cam.tempTarget.Lerp(target, cam.SmoothingOptions.LerpSpeed) - cam.topLeft = cam.tempTarget + // cam.tempTarget = cam.tempTarget.Lerp(target, cam.SmoothingOptions.LerpSpeed) + cam.tempTarget.X = lerp(cam.tempTarget.X, targetX, cam.SmoothingOptions.LerpSpeedX) + cam.tempTarget.Y = lerp(cam.tempTarget.Y, targetY, cam.SmoothingOptions.LerpSpeedY) + cam.topLeft.X = cam.tempTarget.X + cam.topLeft.Y = cam.tempTarget.Y default: // None cam.topLeft = target } @@ -134,11 +144,13 @@ func (cam *Camera) LookAt(targetX, targetY float64) { cam.zoomFactorShake *= cam.ZoomFactor cam.zoomFactorShake += cam.ZoomFactor - cam.trauma = clamp( - cam.trauma-(cam.tickSpeed*cam.ShakeOptions.Decay), - 0, - 1, - ) + cam.trauma = min(max(cam.trauma-(cam.tickSpeed*cam.ShakeOptions.Decay), 0), 1) + + // cam.trauma = clamp( + // cam.trauma-(cam.tickSpeed*cam.ShakeOptions.Decay), + // 0, + // 1, + // ) } else { cam.actualAngle = 0.0 @@ -168,7 +180,9 @@ func (cam *Camera) LookAt(targetX, targetY float64) { // AddTrauma adds trauma. Factor is in the range [0-1] func (cam *Camera) AddTrauma(factor float64) { if cam.ShakeEnabled { - cam.trauma = clamp(cam.trauma+factor, 0, 1) + + cam.trauma = min(max(cam.trauma+factor, 0), 1) + // cam.trauma = clamp(cam.trauma+factor, 0, 1) } } @@ -237,13 +251,16 @@ Cam Rotation: %.2f Zoom factor: %.2f ShakeEnabled: %v Smoothing Function: %s -LerpSpeed: %.4f -SmoothDampTime: %.4f -SmoothDampMaxSpeed: %.2f` +LerpSpeedX: %.4f +LerpSpeedY: %.4f +SmoothDampTimeX: %.4f +SmoothDampTimeY: %.4f +SmoothDampMaxSpeedX: %.2f +SmoothDampMaxSpeedY: %.2f` // String returns camera values as string func (cam *Camera) String() string { - var smoothTypeStr string + smoothTypeStr := "" switch cam.Smoothing { case None: smoothTypeStr = "None" @@ -261,9 +278,12 @@ func (cam *Camera) String() string { cam.zoomFactorShake, cam.ShakeEnabled, smoothTypeStr, - cam.SmoothingOptions.LerpSpeed, - cam.SmoothingOptions.SmoothDampTime, - cam.SmoothingOptions.SmoothDampMaxSpeed, + cam.SmoothingOptions.LerpSpeedX, + cam.SmoothingOptions.LerpSpeedY, + cam.SmoothingOptions.SmoothDampTimeX, + cam.SmoothingOptions.SmoothDampTimeY, + cam.SmoothingOptions.SmoothDampMaxSpeedX, + cam.SmoothingOptions.SmoothDampMaxSpeedY, ) } @@ -321,15 +341,18 @@ type SmoothOptions struct { // LerpSpeed is the linear interpolation speed every frame. Value is in the range [0-1]. // // A smaller value will reach the target slower. - LerpSpeed float64 + LerpSpeedX float64 + LerpSpeedY float64 // SmoothDampTime is the approximate time it will take to reach the target. // // A smaller value will reach the target faster. - SmoothDampTime float64 + SmoothDampTimeX float64 + SmoothDampTimeY float64 // SmoothDampMaxSpeed is the maximum speed the camera can move while smooth damping - SmoothDampMaxSpeed float64 + SmoothDampMaxSpeedX float64 + SmoothDampMaxSpeedY float64 } // SmoothingType is the camera movement smoothing type. @@ -346,8 +369,74 @@ const ( func DefaultSmoothOptions() *SmoothOptions { return &SmoothOptions{ - LerpSpeed: 0.09, - SmoothDampTime: 0.2, - SmoothDampMaxSpeed: 1000.0, + LerpSpeedX: 0.09, + LerpSpeedY: 0.09, + SmoothDampTimeX: 0.2, + SmoothDampTimeY: 0.2, + SmoothDampMaxSpeedX: 1000.0, + SmoothDampMaxSpeedY: 1000.0, + } +} + +// smoothDamp gradually changes a value towards a desired goal over time, +// with independent smoothing for X and Y axes. +func smoothDamp(current, target vec2, currentVelocity *vec2, smoothTimeX, smoothTimeY, maxSpeedX, maxSpeedY float64) vec2 { + // Ensure smooth times are not too small to avoid division by zero + smoothTimeX = math.Max(0.0001, smoothTimeX) + smoothTimeY = math.Max(0.0001, smoothTimeY) + + // Calculate exponential decay factors for X and Y + omegaX := 2.0 / smoothTimeX + omegaY := 2.0 / smoothTimeY + + xX := omegaX * 0.016666666666666666 + xY := omegaY * 0.016666666666666666 + + expX := 1.0 / (1.0 + xX + 0.48*xX*xX + 0.235*xX*xX*xX) + expY := 1.0 / (1.0 + xY + 0.48*xY*xY + 0.235*xY*xY*xY) + + // Calculate change with independent max speeds + change := current.Sub(target) + originalTo := target + + maxChangeX := maxSpeedX * smoothTimeX + maxChangeY := maxSpeedY * smoothTimeY + + maxChangeXSq := maxChangeX * maxChangeX + maxChangeYSq := maxChangeY * maxChangeY + + // Limit change independently for X and Y + if change.X*change.X > maxChangeXSq { + change.X = math.Copysign(maxChangeX, change.X) + } + + if change.Y*change.Y > maxChangeYSq { + change.Y = math.Copysign(maxChangeY, change.Y) } + + target = current.Sub(change) + + // Calculate velocity and output with independent exponential decay + tempX := (currentVelocity.X + change.X*omegaX) * 0.016666666666666666 + tempY := (currentVelocity.Y + change.Y*omegaY) * 0.016666666666666666 + + currentVelocity.X = (currentVelocity.X - tempX*omegaX) * expX + currentVelocity.Y = (currentVelocity.Y - tempY*omegaY) * expY + + outputX := target.X + (change.X+tempX)*expX + outputY := target.Y + (change.Y+tempY)*expY + + output := vec2{outputX, outputY} + + // Ensure we don't overshoot the target + origMinusCurrent := originalTo.Sub(current) + outMinusOrig := output.Sub(originalTo) + + if origMinusCurrent.Dot(outMinusOrig) > 0 { + output = originalTo + currentVelocity.X = (output.X - originalTo.X) / 0.016666666666666666 + currentVelocity.Y = (output.Y - originalTo.Y) / 0.016666666666666666 + } + + return output } diff --git a/examples/director/main.go b/examples/director/main.go index 8f25b2b..3b201bd 100644 --- a/examples/director/main.go +++ b/examples/director/main.go @@ -8,6 +8,7 @@ import ( "image/color" _ "image/jpeg" "log" + "math" "math/rand/v2" "github.com/hajimehoshi/ebiten/v2" @@ -42,6 +43,12 @@ R Rotate type Game struct{} func (g *Game) Update() error { + + aX, aY := Normalize(Axis()) + + targetX += aX * camSpeed + targetY += aY * camSpeed + cam.LookAt(targetX, targetY) if inpututil.IsKeyJustPressed(ebiten.KeyT) { @@ -70,19 +77,6 @@ func (g *Game) Update() error { } } - if ebiten.IsKeyPressed(ebiten.KeyA) { - targetX -= camSpeed / cam.ZoomFactor - } - if ebiten.IsKeyPressed(ebiten.KeyD) { - targetX += camSpeed / cam.ZoomFactor - } - if ebiten.IsKeyPressed(ebiten.KeyW) { - targetY -= camSpeed / cam.ZoomFactor - } - if ebiten.IsKeyPressed(ebiten.KeyS) { - targetY += camSpeed / cam.ZoomFactor - } - if ebiten.IsKeyPressed(ebiten.KeyQ) { // zoom out cam.ZoomFactor /= zoomSpeedFactor } @@ -101,6 +95,7 @@ func (g *Game) Update() error { targetX, targetY = w/2, h/2 cam.Reset() } + return nil } @@ -143,3 +138,27 @@ func main() { log.Fatal(err) } } + +func Normalize(x, y float64) (float64, float64) { + magnitude := math.Sqrt(x*x + y*y) + if magnitude == 0 { + return 0, 0 + } + return x / magnitude, y / magnitude +} + +func Axis() (axisX, axisY float64) { + if ebiten.IsKeyPressed(ebiten.KeyW) { + axisY -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyS) { + axisY += 1 + } + if ebiten.IsKeyPressed(ebiten.KeyA) { + axisX -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyD) { + axisX += 1 + } + return axisX, axisY +} diff --git a/examples/platformer/main.go b/examples/platformer/main.go index 99ae19a..1104aea 100644 --- a/examples/platformer/main.go +++ b/examples/platformer/main.go @@ -18,7 +18,7 @@ WASD -------- Move player Space ------- Jump Shift ------- Run Tab --------- Change camera smoothing type -Left/Right -- Decrease/Increase camera smoothing speed +Arrow Keys -- Decrease/Increase camera smoothing speed LerpSpeed: Smaller value will reach the target slower. SmoothDampTime: Smaller value will reach the target faster. ` @@ -47,6 +47,7 @@ var TileMap = [][]uint8{ func init() { Controller.SetPhyicsScale(2.2) cam.Smoothing = kamera.SmoothDamp + cam.SmoothingOptions.SmoothDampTimeY = 1 } func Translate(box *[4]float64, x, y float64) { @@ -68,24 +69,48 @@ var collider = tilecollider.NewCollider(TileMap, TileSize[0], TileSize[1]) func (g *Game) Update() error { + if ebiten.IsKeyPressed(ebiten.KeyDown) { + switch cam.Smoothing { + case kamera.Lerp: + cam.SmoothingOptions.LerpSpeedY -= 0.01 + cam.SmoothingOptions.LerpSpeedY = max(0, min(cam.SmoothingOptions.LerpSpeedY, 1)) + + case kamera.SmoothDamp: + cam.SmoothingOptions.SmoothDampTimeY += 0.01 + cam.SmoothingOptions.SmoothDampTimeY = max(0, min(cam.SmoothingOptions.SmoothDampTimeY, 10)) + + } + } + if ebiten.IsKeyPressed(ebiten.KeyUp) { + switch cam.Smoothing { + case kamera.Lerp: + cam.SmoothingOptions.LerpSpeedY += 0.01 + cam.SmoothingOptions.LerpSpeedY = max(0, min(cam.SmoothingOptions.LerpSpeedY, 1)) + case kamera.SmoothDamp: + cam.SmoothingOptions.SmoothDampTimeY -= 0.01 + cam.SmoothingOptions.SmoothDampTimeY = max(0, min(cam.SmoothingOptions.SmoothDampTimeY, 10)) + } + } if ebiten.IsKeyPressed(ebiten.KeyLeft) { switch cam.Smoothing { case kamera.Lerp: - cam.SmoothingOptions.LerpSpeed -= 0.0005 - cam.SmoothingOptions.LerpSpeed = clamp(cam.SmoothingOptions.LerpSpeed, 0, 1) + cam.SmoothingOptions.LerpSpeedX -= 0.01 + cam.SmoothingOptions.LerpSpeedX = max(0, min(cam.SmoothingOptions.LerpSpeedX, 1)) + case kamera.SmoothDamp: - cam.SmoothingOptions.SmoothDampTime += 0.0005 - cam.SmoothingOptions.SmoothDampTime = clamp(cam.SmoothingOptions.SmoothDampTime, 0, 10) + cam.SmoothingOptions.SmoothDampTimeX += 0.01 + cam.SmoothingOptions.SmoothDampTimeX = max(0, min(cam.SmoothingOptions.SmoothDampTimeX, 10)) + } } if ebiten.IsKeyPressed(ebiten.KeyRight) { switch cam.Smoothing { case kamera.Lerp: - cam.SmoothingOptions.LerpSpeed += 0.0005 - cam.SmoothingOptions.LerpSpeed = clamp(cam.SmoothingOptions.LerpSpeed, 0, 1) + cam.SmoothingOptions.LerpSpeedX += 0.01 + cam.SmoothingOptions.LerpSpeedX = max(0, min(cam.SmoothingOptions.LerpSpeedX, 1)) case kamera.SmoothDamp: - cam.SmoothingOptions.SmoothDampTime -= 0.0005 - cam.SmoothingOptions.SmoothDampTime = clamp(cam.SmoothingOptions.SmoothDampTime, 0, 10) + cam.SmoothingOptions.SmoothDampTimeX -= 0.01 + cam.SmoothingOptions.SmoothDampTimeX = max(0, min(cam.SmoothingOptions.SmoothDampTimeX, 10)) } } @@ -428,14 +453,3 @@ func (pc *PlayerController) ProcessVelocity(vel [2]float64) [2]float64 { return vel } - -// clamp returns f clamped to [low, high] -func clamp(f, low, high float64) float64 { - if f < low { - return low - } - if f > high { - return high - } - return f -} diff --git a/util.go b/util.go new file mode 100644 index 0000000..e6c1d84 --- /dev/null +++ b/util.go @@ -0,0 +1,5 @@ +package kamera + +func lerp(start, end, t float64) float64 { + return start + t*(end-start) +} diff --git a/vec2.go b/vec2.go index 9747b2b..a641481 100644 --- a/vec2.go +++ b/vec2.go @@ -47,42 +47,3 @@ func (v vec2) Mag() float64 { func (v vec2) Dot(other vec2) float64 { return v.X*other.X + v.Y*other.Y } - -// clamp returns f clamped to [low, high] -func clamp(f, low, high float64) float64 { - if f < low { - return low - } - if f > high { - return high - } - return f -} - -// smoothDamp gradually changes a value towards a desired goal over time. -func smoothDamp(current, target vec2, currentVelocity *vec2, smoothTime, maxSpeed float64) vec2 { - smoothTime = math.Max(0.0001, smoothTime) - omega := 2.0 / smoothTime - x := omega * 0.016666666666666666 - exp := 1.0 / (1.0 + x + 0.48*x*x + 0.235*x*x*x) - change := current.Sub(target) - originalTo := target - maxChange := maxSpeed * smoothTime - maxChangeSq := maxChange * maxChange - sqDist := change.Dot(change) - if sqDist > maxChangeSq { - mag := math.Sqrt(sqDist) - change = change.Scale(maxChange / mag) - } - target = current.Sub(change) - temp := (currentVelocity.Add(vec2{change.X * omega, change.Y * omega})).Scale(0.016666666666666666) - *currentVelocity = currentVelocity.Sub(vec2{temp.X * omega, temp.Y * omega}).Scale(exp) - output := target.Add(change.Add(temp).Scale(exp)) - origMinusCurrent := originalTo.Sub(current) - outMinusOrig := output.Sub(originalTo) - if origMinusCurrent.Dot(outMinusOrig) > 0 { - output = originalTo - *currentVelocity = output.Sub(originalTo).Scale(1.0 / 0.016666666666666666) - } - return output -}