+4 votes

Hi folks!
I want the Camera2D to zoom towards the mouse position and although I set the Anchor Mode to Drag Center, when I zoom is the camera zoomes towards the top left corner of the screen. How can I fix this?

asked Mar 20, 2018 in Engine by DodoIta (471 points)

1 Answer

+7 votes
Best answer

First using only zoom property to zoom at point will not be sufficient(read below). Second it looks like either some script modifies cameras anchor_mode or your camera isn't active-one after all. Try creating simple scene and make zooming at center work, then after you resolve that issue read next part how zoom at specific point.

If we would do manually zoom-at-point transformation we could split it into 3 parts:

  1. translating to zooming point - to set zoom center
  2. zooming by desire amount - actual zooming
  3. applying reversed translation - returning camera to its appropriate position

Because Zooming part is doing for us by Godot as a first transformation we need to combine 1st and 3rd step together and do camera positioning ourselves and it will involves math.

BEWARE MATH BELOW!!!

To tackle this problem first we must get our facts strait.
* Looking for: new camera position
* Variable: zoom
* Invariant: screen size, mouse global position, mouse position on screen

Mouse positions looks like most appropriate points of building an equation to compute new position of camera.

Lets start with defining vars:

  • M global mouse position
  • m mouse position on screen
  • C global camera position
  • s screen size
  • z zoom level

now lets build equation:

  1. mouse global position depends on position on screen

    M ~ m
    
  2. we set camera anchor to drag center so its reference point is in center of the screen and mouse position on screen is relative to upper left corner which is half screen size to upper left direction

    M ~ (m - 0.5 * s)
    
  3. screen can be zoomed so we need to apply that to screen size and mouse position on screen

    M ~ (m - 0.5 * s)*z
    
  4. everything is relative to camera position

    M ~ (m - 0.5 * s)*z + C
    
  5. that is everything need to compute global mouse position

    M = (m - 0.5 * s)*z + C
    
  6. global mouse positions before and after zooming will be

    M0 = (m - 0.5 * s)*z0 + C0
    M1 = (m - 0.5 * s)*z1 + C1
    
  7. We know that global mouse position and mouse position on screen before and after zooming will be same

    M0 = M1 
    
  8. replacing M1 and M2 with equations

    (m - 0.5 * s)*z0 + C0 = (m - 0.5 * s)*z1 + C1
    
  9. After transforming(skipping some steps) we will get this:

    C1 = C0 + (-0.5*s + m)*(z0 - z1)
    

That is new camera position that we were searching for.

And here some code to attach to camera script:

    var zoom_step = 1.1

    func _input(event):
        if event is InputEventMouse:
            if event.is_pressed() and not event.is_echo():
                var mouse_position = event.position
                if event.button_index == BUTTON_WHEEL_UP:
                    zoom_at_point(zoom_step,mouse_position)
                else : if event.button_index == BUTTON_WHEEL_DOWN:
                    zoom_at_point(1/zoom_step,mouse_position)

    func zoom_at_point(zoom_change, point):
        var c0 = global_position # camera position
        var v0 = get_viewport().size # vieport size
        var c1 # next camera position
        var z0 = zoom # current zoom value
        var z1 = z0 * zoom_change # next zoom value

        c1 = c0 + (-0.5*v0 + point)*(z0 - z1)
        zoom = z1
        global_position = c1

best practice is to write new code by yourself - you will remember it better that way - so try avoid copy-pasting

answered Mar 22, 2018 by Bartosz (1,004 points)
selected Mar 23, 2018 by DodoIta

Thank you so much for replying.
I have a few doubts about what you typed:

  1. I don't undestand the difference between M and m.
  2. Why M0 and M1 would be the same after zooming?
  3. I implemented your algorithm and zooming works, but camera positioning behaves still the same as before (it zooms towards the upper left corner of the screen.

I'll try investigating in the meantime

  1. Imaging you are looking at google maps, at some house. Its geographic location is M and it m is its location on your screen. You can zoom and pan view m will change but M does not.

  2. We decided that. We want to have zoom-at-point meaning that we point on something, we zoom ,and we still pointing at the same thing e.g you point on sprite of small insect and use mouse wheel, you expected that when zoom is finished you will be pointing on enlarge sprite of insect

  3. I do not have enough information to help you with that. Maybe if you shared minimal scene that causes problem I could find whats wrong

Ok, now I understand it.
After some thought I discovered that the problem was setting strict limits on the camera (Limit property), so setting very large limits fixed it. That's probably what messed up my code, too.
Thank you a lot, very helpful.

I just spent 3 days trying to fix my problem and I thought it would help others to leave a comment here.

I used your script to implement zooming in my top-down 2D game. Your script seemed to work as intended, initially. However, I soon discovered that all of my A Star pathing logic in my TileMap broke after I zoomed.

The game seemed to register clicks in a different position than where my tiles actually were. But it was only off after I zoomed in or out, even if I returned (or tried to) to the default zoom level and position. Your script even helped me write my own simple camera panning script, but that too altered how the game was registering clicks.

Anyway the fix was to not use the Camera2D.global_position or Camera2D.position. I edited your zoom script and my simple panning script to use the Camera2D.offset instead. The panning and zooming now works as before, and the game is registering mouse clicks where it should.

For anyone in the future who needs to zoom in on something without using a camera, this is what I'm using and it works well.

var zoom_factor = 1.1

func _input(event):
    if event is InputEventMouse:
        if event.is_pressed() and not event.is_echo():
            var mouse_position = event.position
            if event.button_index == BUTTON_WHEEL_UP:
                _zoom_at_point(zoom_factor, mouse_position)
            elif event.button_index == BUTTON_WHEEL_DOWN:
                _zoom_at_point(1 / zoom_factor, mouse_position)


func _zoom_at_point(zoom_change, mouse_position):
    scale = scale * zoom_change
    var delta_x = (mouse_position.x - global_position.x) * (zoom_change - 1)
    var delta_y = (mouse_position.y - global_position.y) * (zoom_change - 1)
    global_position.x = global_position.x - delta_x
    global_position.y = global_position.y - delta_y

thanks! epic answer :)

In case anyone lands here looking for how to scale a tilemap, using the mouse wheel, centred on the mouse pointer I have a piece of code as such:

            if event.pressed:
                if (event.button_index == BUTTON_WHEEL_UP) or (event.button_index == BUTTON_WHEEL_DOWN):
                    var old_scale := self.scale.x
                    if (event.button_index == BUTTON_WHEEL_UP): self.scale += Vector2(1, 1) 
                    elif (event.button_index == BUTTON_WHEEL_DOWN): self.scale -= Vector2(1, 1) 
                    var scale_amount := self.scale.x / old_scale
                    # Reset position to Centre on pointer
                    self.position = (self.position * scale_amount) - (event.position * (scale_amount - 1))

It's a bit raw currently (using the actual scale on the node rather than having a global scale for instance) but shows how to do the math.

Thank you all for sharing!

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.