Godot Version
4.4.1
Question
Hello, I am currently working on my first game ever and trying to understand how to zoom the camera properly. I work with two zoom levels because I already learned that pixel art could otherwise look bad. When I zoom out, the zoom should not change the position (already working). When I zoom in, the cursor should stay over the same pixel as it was before zooming in (this should give the best feeling I think?). I have two problems:
-
Why does my mouse cursor is not exactly at the same position as it was before zooming in (see gif)? Is there something off with my math or is it just because of some rounding errors (even though it is off multiple pixels)?
-
Is there some way to let the two tweens (zoom and position change) work parallel and have the mouse always at the exact same position? Because currently the zoom is a little bit faster at the start and later on the position catches up. I tried setting both to TRANS_LINEAR but it didnt work.
Here is my code of the camera_manager
class_name CameraManager
extends Node2D
@onready var camera: Camera2D = $Camera2D
# panning
const VELOCITY: float = 800.0
const EDGE_MARGIN: float = 1.0
# zooming
@onready var current_zoom: Vector2 = camera.zoom
const ZOOM_LEVEL: Array[float] = [1.0, 2.0]
const ZOOM_RATE: float = 5.0
# dragging
var drag_active: bool = false
func setup(pos: Vector2i) -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_CONFINED)
camera.position = pos
func _unhandled_input(event: InputEvent) -> void:
# drag mouse
if event is InputEventMouseMotion:
if event.button_mask == MOUSE_BUTTON_MASK_MIDDLE:
camera.position -= event.relative / current_zoom
# activate zooming and dragging
if event is InputEventMouseButton:
if event.is_pressed():
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
_zoom_in()
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_zoom_out()
if event.button_index == MOUSE_BUTTON_MIDDLE:
drag_active = true
if event.is_released():
if event.button_index == MOUSE_BUTTON_MIDDLE:
drag_active = false
func _process(delta: float) -> void:
var mouse_pos: Vector2 = get_viewport().get_mouse_position()
var limits: Vector2 = get_viewport_rect().size - Vector2(1.0, 1.0)
var direction := Vector2.ZERO
# allow no panning while dragging is active
if drag_active:
return
# keyboard panning
if Input.is_action_pressed("move_camera_left"):
direction.x += -1
if Input.is_action_pressed("move_camera_right"):
direction.x += 1
if Input.is_action_pressed("move_camera_up"):
direction.y += -1
if Input.is_action_pressed("move_camera_down"):
direction.y += 1
# edge panning
if mouse_pos.x < EDGE_MARGIN:
direction.x += -1
if mouse_pos.x > limits.x - EDGE_MARGIN:
direction.x += 1
if mouse_pos.y < EDGE_MARGIN:
direction.y += -1
if mouse_pos.y > limits.y - EDGE_MARGIN:
direction.y += 1
camera.position += VELOCITY * direction.normalized() * delta / camera.zoom
func _zoom_in() -> void:
# allow no zooming while dragging is active or zoom at max
if drag_active or current_zoom.x == ZOOM_LEVEL[-1]:
return
# get new zoom value
var new_zoom = ZOOM_LEVEL[ZOOM_LEVEL.find(current_zoom.x) + 1] * Vector2.ONE
# calculate camera position delta
var mouse_pos = get_global_mouse_position()
var delta = (mouse_pos - camera.position) * (Vector2.ONE - current_zoom / new_zoom)
# animate zoom + movement
var tween = get_tree().create_tween()
tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
tween.tween_property(camera, "position", camera.position + delta, 1.0/ZOOM_RATE)
tween.parallel().tween_property(camera, "zoom", new_zoom, 1.0/ZOOM_RATE)
# update zoom value
current_zoom = new_zoom
func _zoom_out() -> void:
# allow no zooming while dragging is active or zoom at min
if drag_active or current_zoom.x == ZOOM_LEVEL[0]:
return
# get new zoom value
var new_zoom = ZOOM_LEVEL[ZOOM_LEVEL.find(current_zoom.x) - 1] * Vector2.ONE
# animate zoom
var tween = get_tree().create_tween()
tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
tween.tween_property(camera, "zoom", new_zoom, 1.0/ZOOM_RATE)
# update zoom value
current_zoom = new_zoom