Camera feeling great

This commit is contained in:
2026-06-11 22:01:36 -05:00
parent f1bc43125b
commit 3e40d9f053
2 changed files with 62 additions and 40 deletions
+46 -34
View File
@@ -1,5 +1,7 @@
class_name OverworldCamera extends Camera3D
enum CameraMode { FREE, CENTERED }
const COLLISION_MARGIN:float = 0.3
@export_category("Target")
@@ -18,17 +20,19 @@ const COLLISION_MARGIN:float = 0.3
@export var mouseSensitivity:float = 0.3
@export_category("Auto-center")
@export var autoReturnDelay:float = 2.0
@export var autoReturnSpeed:float = 90.0
@export var autoReturnPitch:float = 30.0
@export var centeredDelay:float = 2.0
@export var centeredFollowRate:float = 0.75
@export var centeredMaxFollowRate:float = 1.0
@export var centeredPitch:float = 30.0
@export_category("Collision")
@export_flags_3d_physics var collisionMask:int = 1
var _mode:CameraMode = CameraMode.CENTERED
var _yaw:float = 0.0
var _pitch:float = 30.0
var _camVelocity:Vector2 = Vector2.ZERO
var _idleTimer:float = 0.0
var _freeTimer:float = 0.0
var _mouseDelta:Vector2 = Vector2.ZERO
var _rightMouseHeld:bool = false
@@ -51,8 +55,15 @@ func _process(delta:float) -> void:
"camera_orbit_up", "camera_orbit_down"
)
# Controller input with speed ramp; instant stop on release
var controllerActive:bool = orbitInput.length() > 0.01
var mouseActive:bool = _mouseDelta.length_squared() > 0.0
# Any manual camera input returns to FREE and resets the centering timer
if controllerActive or mouseActive:
_mode = CameraMode.FREE
_freeTimer = 0.0
# Controller input with speed ramp; instant stop on release
if controllerActive:
_camVelocity = _camVelocity.move_toward(
orbitInput * orbitSensitivity * SETTINGS.cameraSpeedController, orbitAcceleration * delta
@@ -63,33 +74,41 @@ func _process(delta:float) -> void:
_camVelocity = Vector2.ZERO
# Right-click mouse drag
var mouseActive:bool = _mouseDelta.length_squared() > 0.0
if mouseActive:
_yaw += _mouseDelta.x * mouseSensitivity * SETTINGS.cameraSpeedMouse * xMult
_pitch += _mouseDelta.y * mouseSensitivity * SETTINGS.cameraSpeedMouse * yMult
_mouseDelta = Vector2.ZERO
# Auto-center behind player: only when player is moving and camera has been idle
var centerPressed:bool = Input.is_action_just_pressed("center_camera")
if centerPressed:
_idleTimer = autoReturnDelay
elif controllerActive or mouseActive:
_idleTimer = 0.0
else:
# center_camera input → switch to CENTERED immediately
if Input.is_action_just_pressed("center_camera"):
_mode = CameraMode.CENTERED
# In FREE mode, accumulate time toward auto-centering while the player is moving
if _mode == CameraMode.FREE:
var playerBody := targetNode as CharacterBody3D
if playerBody != null and playerBody.velocity.length_squared() > 0.1:
_idleTimer += delta
_freeTimer += delta
if _freeTimer >= centeredDelay:
_mode = CameraMode.CENTERED
_freeTimer = 0.0
else:
_idleTimer = 0.0
_freeTimer = 0.0
if _idleTimer >= autoReturnDelay:
_pitch = move_toward(_pitch, autoReturnPitch, autoReturnSpeed * delta)
var behindYaw:float = rad_to_deg(atan2(
targetNode.global_transform.basis.z.x,
targetNode.global_transform.basis.z.z
))
var yawDiff:float = fposmod(behindYaw - _yaw + 180.0, 360.0) - 180.0
_yaw += move_toward(0.0, yawDiff, autoReturnSpeed * delta)
# In CENTERED mode, smoothly slerp toward behind the player while they move;
# rate scales up with angular distance so large offsets catch up quickly.
if _mode == CameraMode.CENTERED:
var centerBody := targetNode as CharacterBody3D
if centerBody != null and centerBody.velocity.length_squared() > 0.1:
var behindYaw:float = rad_to_deg(atan2(
targetNode.global_transform.basis.z.x,
targetNode.global_transform.basis.z.z
))
var centerYawDiff:float = fposmod(behindYaw - _yaw + 180.0, 360.0) - 180.0
var totalAngle:float = abs(centerYawDiff) + abs(centeredPitch - _pitch)
var dynamicRate:float = minf(centeredFollowRate * (1.0 + totalAngle / 90.0), centeredMaxFollowRate)
var t:float = minf(dynamicRate * delta, 1.0)
_pitch = lerpf(_pitch, centeredPitch, t)
_yaw += centerYawDiff * t
_pitch = clamp(_pitch, pitchMin, pitchMax)
@@ -113,10 +132,9 @@ func _process(delta:float) -> void:
var targetYaw:float = rad_to_deg(atan2(actualDir.x, actualDir.z))
var feedbackRate:float = minf(20.0 * delta, 1.0)
_pitch = lerpf(_pitch, targetPitch, feedbackRate)
var yawDiff:float = fposmod(targetYaw - _yaw + 180.0, 360.0) - 180.0
_yaw += lerpf(0.0, yawDiff, feedbackRate)
var feedbackYawDiff:float = fposmod(targetYaw - _yaw + 180.0, 360.0) - 180.0
_yaw += lerpf(0.0, feedbackYawDiff, feedbackRate)
# Returns the camera world position, respecting both minDistance and terrain.
func _resolvePosition(pivot:Vector3, dir:Vector3) -> Vector3:
var desired:Vector3 = pivot + dir * distance
var space:PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
@@ -130,26 +148,20 @@ func _resolvePosition(pivot:Vector3, dir:Vector3) -> Vector3:
var hitNormal:Vector3 = hit["normal"]
var hitDist:float = (hit["position"] - pivot).length() - COLLISION_MARGIN
# Find the closest point on the circle formed by intersecting:
# • a sphere of radius max(hitDist, minDistance) centred on pivot
# • the terrain surface plane (inset by COLLISION_MARGIN along its normal)
# Using max() makes both cases meet exactly at hitDist == minDistance,
# eliminating the branch discontinuity that causes jitter at the boundary.
# Sphere-plane circle intersection: find the closest valid point that
# satisfies both minDistance from pivot and stays clear of terrain.
var targetDist:float = maxf(hitDist, minDistance)
var d:float = hitNormal.dot(pivot - hit["position"]) - COLLISION_MARGIN
var rSq:float = targetDist * targetDist - d * d
if rSq <= 0.0:
# Pivot is itself nearly on the terrain — sit at the tangent point
return pivot - hitNormal * d
var circleCenter:Vector3 = pivot - hitNormal * d
var circleRadius:float = sqrt(rSq)
# Project desired onto the terrain plane to get the on-plane direction
var toDesired:Vector3 = desired - circleCenter
toDesired -= hitNormal * hitNormal.dot(toDesired)
if toDesired.length_squared() < 0.0001:
# Desired is directly along the normal — any circle point is equally good
return circleCenter
return circleCenter + toDesired.normalized() * circleRadius