Last active
February 8, 2025 13:21
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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