Skip to content

Instantly share code, notes, and snippets.

@Tam
Last active May 9, 2024 17:48
Show Gist options
  • Save Tam/202ae5fd472f55c69ba0ad3da2ff8e77 to your computer and use it in GitHub Desktop.
Save Tam/202ae5fd472f55c69ba0ad3da2ff8e77 to your computer and use it in GitHub Desktop.
A smoothly panning and zooming camera for Godot 4 (now with mouse edge panning)
extends Camera2D
# Note: Limit smoothing and position smoothing must be disabled
const V2_2: Vector2 = Vector2.ONE * 2
const PAN_SPEED: int = 2000
const PAN_SMOOTHING: int = 8
# How far from the edge of the screen panning starts (% of screen size)
const EDGE_PAN_THRESHOLD_MIN: Vector2 = Vector2(0.06, 0.12)
# How close to the edge of the screen panning reaches max speed
const EDGE_PAN_THRESHOLD_MAX: Vector2 = Vector2(0.01, 0.03)
const ZOOM_SPEED_MOUSE: float = 0.1
const ZOOM_SPEED_KEYBOARD: float = 2
const ZOOM_MIN: float = 0.7
const ZOOM_MAX: float = 1.2
const ZOOM_SMOOTHING: int = 10
var target_position: Vector2
var target_zoom: Vector2
var is_mouse_in_window: bool
func _notification(what: int) -> void:
match what:
NOTIFICATION_WM_MOUSE_ENTER:
is_mouse_in_window = true
NOTIFICATION_WM_MOUSE_EXIT:
is_mouse_in_window = false
func _ready() -> void:
target_position = position
target_zoom = zoom
func _process(delta: float) -> void:
_zoom(delta) # Zoom must be handled before panning (since zoom updates the target position)
_pan(delta)
func _zoom (delta: float) -> void:
# Handle zoom input (i.e. Mouse wheel, +/- keys)
if Input.is_action_pressed('zoom_in'):
_zoom_from_delta(+(ZOOM_SPEED_KEYBOARD * delta))
elif Input.is_action_pressed('zoom_out'):
_zoom_from_delta(-(ZOOM_SPEED_KEYBOARD * delta))
# Zoom smoothly to the target zoom level, towards the mouse
var mouse_pos: Vector2 = get_global_mouse_position()
zoom = zoom.lerp(target_zoom, ZOOM_SMOOTHING * delta)
target_position += mouse_pos - get_global_mouse_position()
func _input(event):
# Handle zooming on trackpads
if event is InputEventPanGesture:
_zoom_from_delta(ZOOM_SPEED_MOUSE * event.delta.y)
func _zoom_from_delta (zoom_delta: float) -> void:
# Zoom by delta and clamp
target_zoom += Vector2(zoom_delta, zoom_delta)
target_zoom.x = clamp(target_zoom.x, ZOOM_MIN, ZOOM_MAX)
target_zoom.y = clamp(target_zoom.y, ZOOM_MIN, ZOOM_MAX)
func _pan (delta: float) -> void:
# Get the camera movement vector
var motion: Vector2 = Input.get_vector('camera_left', 'camera_right', 'camera_up', 'camera_down')
# Calculate the viewport half sizes
var vs: Vector2 = get_viewport_rect().size
var vr: Vector2 = vs * (V2_2 - zoom)
var vw: float = vr.x / 2;
var vh: float = vr.y / 2;
# Calculate edge pan distances (only when the mouse is inside the window
if is_mouse_in_window:
var mouse_pos: Vector2 = get_viewport().get_mouse_position()
if mouse_pos.x < vs.x * EDGE_PAN_THRESHOLD_MIN.x:
motion.x = -_compute_mouse_to_edge(mouse_pos, vs.x, 0, 'x')
elif mouse_pos.x > vs.x - vs.x * EDGE_PAN_THRESHOLD_MIN.x:
motion.x = _compute_mouse_to_edge(mouse_pos, vs.x, vs.x, 'x')
if mouse_pos.y < vs.y * EDGE_PAN_THRESHOLD_MIN.y:
motion.y = -_compute_mouse_to_edge(mouse_pos, vs.y, 0, 'y')
elif mouse_pos.y > vs.y - vs.y * EDGE_PAN_THRESHOLD_MIN.y:
motion.y = _compute_mouse_to_edge(mouse_pos, vs.y, vs.y, 'y')
# Pan to the target position, clamping within the bounds
target_position += motion * PAN_SPEED * delta
target_position.x = clamp(target_position.x, limit_left + vw, limit_right - vw)
target_position.y = clamp(target_position.y, limit_top + vh, limit_bottom - vh)
# Smoothly pan to target
position = position.lerp(target_position, PAN_SMOOTHING * delta)
# Convert the mouse position to a percentage from the inner bound (min) to the outer bound (max)
func _compute_mouse_to_edge (
mouse_pos: Vector2,
origin: float,
_offset: float,
axis: String = 'x'
) -> float:
var lower
var upper
if _offset > 0:
lower = origin - _offset * EDGE_PAN_THRESHOLD_MAX[axis]
upper = origin - _offset * EDGE_PAN_THRESHOLD_MIN[axis] - lower
else:
lower = origin * EDGE_PAN_THRESHOLD_MAX[axis]
upper = origin * EDGE_PAN_THRESHOLD_MIN[axis] - lower
var pos = mouse_pos[axis] - lower
return 1 - max(0, pos / upper)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment