Search code examples
unity-game-enginecamera

Moving camera to proper position in Zoom function in Unity


Hi I have a question that I'm hoping someone can help me work through. I've asked elsewhere to no avail but it seems like a standard problem so I'm not sure why I haven't been getting answers.

Its basically setting up a zoom function that mirrors Google Maps zoom. Like, the camera zooms in/out onto where your mouse is. I know this probably gets asked a lot but I think Unity's new Input System changed things up a bit since the 4-6 year old questions that I've found in my own research.

In any case, I've set up an parent GameObject that holds all 2D sprites that will be in my scene and an orthographic camera. I can set the orthographic size through code to change to zoom, but its moving the camera to the proper place that I am having trouble with.

This was my 1st attempt:

public Zoom(float direction, Vector2 mousePosition) {
    // zoom calcs
    float rate 1 + direction * Time.deltaTime;
    float targetOrtho = Mathf.MoveTowards(mainCam.orthographicSize, mainCam.orthoGraphicSize/rate, 0.1f);

    // move calcs
    mousePosition = mainCam.ScreenToWorldPoint(mousePosition);
    Vector2 deltaPosition = previousPosition - mousePosition;
    
    // move and zoom    
    transform.position += new Vector3(deltaPosition.x, deltaPosition.y, 0);
    // zoomLevels are a generic struct that holds the max/min values.
    SetZoomLevel(Mathf.Clamp(targetOrthoSize, zoomLevels.min, zoomLevels.max));

    previousPosition = mousePosition;
}

This function gets called through my input controller, activated through Unity's Input System events. When the mouse wheel scrolls, the Zoom function is given a normalized value as direction (1 or -1) and the current mousePosition. When its finished its calculation, the mousePosition is stored in previousPosition.

The code actually works -- except it is extremely jittery. This, of course happens because there is no Time.deltaTime applied to the camera movement, nor is this in LateUpdate; both of which helps to smooth the movements. Except, in the former case, multiplying Time.deltaTime to new Vector3(deltaPosition.x, deltaPosition.y, 0) seems to cause the zoom occur at the camera's centre rather than the mouse position. When i put zoom into LateUpdate, it creates a cool but unwanted vibration effect when the camera moves.

So, after doing some thinking and reading, I thought it may be best to calculate the difference between the mouse position and the camera's center point, then multiply it by a scale factor, which is the camera's orthographic size * 2 (maybe...??). Hence my updated code here:


    public void Zoom(float direction, Vector2 mousePosition)
    {
        // zoom
        float rate = 1 + direction * Time.unscaledDeltaTime * zoomSpeed;
        float orthoTarget = Mathf.MoveTowards(mainCam.orthographicSize, mainCam.orthographicSize * rate, maxZoomDelta);
    
        SetZoomLevel(Mathf.Clamp(orthoTarget, zoomLevels.min, zoomLevels.max));
    
        // movement
        if (mainCam.orthographicSize < zoomLevels.max && mainCam.orthographicSize > zoomLevels.min)
        {
            mousePosition = mainCam.ScreenToWorldPoint(mousePosition);
            Vector2 offset = (mousePosition - new Vector2(transform.position.x, transform.position.y)) / (mainCam.orthographicSize * 2);
    
            // panPositions are the same generic struct holding min/max values
            offset.x = Mathf.Clamp(offset.x, panPositions.min.x, panPositions.max.x);
            offset.y = Mathf.Clamp(offset.y, panPositions.min.y, panPositions.max.y);
    
            transform.position += new Vector3(offset.x, offset.y, 0) * Time.deltaTime;
        }
     }

This seems a little closer to what I'm trying to achieve but the camera still zooms in near its center point and zooms out on some point... I'm a bit lost as to what I am missing out here.

Is anyone able to help guide my thinking about what I need to do to create a smooth zoom in/out on the point where the mouse currently is? Much appreciated & thanks for reading through this.


Solution

  • Ok I figured it out for if anyone ever comes across the same problem. it is a standard problem that is easily solved once you know the math.

    Basically, its a matter of scaling and translating the camera. You can do one or the other first - it does not matter; the outcome is the same. Imagine your screen looks like this:

    A map with a square box representing a camera viewport and a cursor

    The green box is your camera viewport, the arrow is your cursor. When you zoom in, the orthographic size gets smaller and shrinks around its anchor point (usually P1(0,0)). This is the scaling aspect of the problem and the following image explains it well:

    enter image description here

    So, now we want to move the camera position to the new position:

    enter image description here

    So how do we do this? Its just a matter of getting distance from the old camera position (P1(0, 0)) to the new camera position (P2(x,y)). Basically, we only want this: enter image description here

    My solution to find the length of the arrow in the picture above was to basically subtract the length of the cursor position from the old camera position (oldLength) from the length of the cursor position to the new camera position (newLength).

    But how do you find newLength? Well, since we know the length will be scaled accordingly to the size of the camera viewport, newLength will be either oldLength / scaleFactor or oldLength * scaleFactor, depending on whether you want to zoom in or out, respectively. The scale factor can be whatever you want (zoom in/out by 2, 4, 1.4... whatever).

    From there, its just a matter of subtracting newLength from oldLength and adding that difference from the current camera position. The psuedo code is below:

    (Note that i changed 'newLength' to 'length' and 'oldLength' to 'scaledLength')

    // make sure you're working in world space
    mousePosition = camera.ScreenToWorldPoint(mousePosition);
    
    length = mousePosition - currentCameraPosition;
    scaledLength = length / scaleFactor // to zoom in, otherwise its length * scaleFactor
    deltaLength = length - scaledLength;
    
    // change position
    cameraPosition = currentCameraPosition - deltaLength;
    // do zoom
    camera.orthographicSize /= scaleFactor  // to zoom in, otherwise orthographic size *= scaleFactor
    

    Works perfectly for me. Thanks to those who helped me in a discord coding community!