What is off with my math for zoom delta and tweens?

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:

  1. 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)?
    problem

  2. 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

Should that not be:

func zoom_in() -> void:
    [...]
    var delta = (mouse_pos - camera.position) * (new_zoom / current_zoom)
    [...]

That is, scale the mouse distance from the camera by the ratio between the new and old zoom…

This one overshoots a lot. The original version already being so close is whats confusing me.
Godot_v4.4.1-stable_win64_A9B1Y2HpSL

I’d also change delta to something else as it’s nearly a keyword in Godot

1 Like

Well, delta is an argument to _process() and the like, so it could be misleading, but it’s not shadowing anything there.

It seems strange that it would overshoot that much. Have you tried printing the numbers it’s using/calculating?

I know, I just like to keep things KISS and as clear as possible: clarity in code is a good thing :smiling_face_with_sunglasses:

1 Like

Godot_v4.4.1-stable_win64_2HLkGWCdZl

	# calculate camera position delta
	var mouse_pos = get_global_mouse_position()
	var offset = (mouse_pos - camera.position) * (Vector2.ONE - current_zoom / new_zoom)
	var offset2 = (mouse_pos - camera.position) * (new_zoom / current_zoom)
	
	print("mouse_pos", mouse_pos)
	
	print("offset", offset)
	print("offset2", offset2)

	print("camera.position + offset", camera.position + offset)
	print("camera.position + offset2", camera.position + offset2)

This input lead to that output

mouse_pos(-10.0, -10.0)
offset(-129.5, -106.5)
offset2(-518.0, -426.0)
camera.position + offset(119.5, 96.5)
camera.position + offset2(-269.0, -223.0)

Could it also be because of some scaling issues? I got a little bit confused before all that with viewport size, stretch, scale and so on while using pixel art. I am currently using a Viewport size of 640x360, Window override of 1280x720, stretch mode viewport, aspect keep and scale 0.5

Okay, I think I just found an answer to the first question. I changed the default cursor to an own created cursor (the one in the gif) but did not change the hotspot to the middle (its still top left). Thats why it always moves a little bit to the top left. But I stillt didnt solve the second problem, so if someone has a solution I would really appreciate it :slight_smile:

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.