Mazer
Mazer

Reputation: 21

How do I scale SKNodes when you zoom in or out?

I've implemented a zoom feature, but the nodes I use to move the player, arrows pointing in the direction the player moves, stay the same size and I can't figure out how to scale them proportionally, so if I zoom in, the buttons get smaller and move closer to the player to stay on screen, and vice versa when you zoom out.

Here's the code I wrote to allow zooming:

@objc func handlePinch(sender: UIPinchGestureRecognizer) {
    guard sender.view != nil else { return }

    if sender.state == .began || sender.state == .changed {
        sender.view?.transform = (sender.view?.transform.scaledBy(x: sender.scale, y: sender.scale))!
        sender.scale = 1.0
    }
}
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(sender:)))
view.addGestureRecognizer(pinch)

Upvotes: 1

Views: 217

Answers (2)

michael.yql
michael.yql

Reputation: 33

Working solution:

@objc func handlePinch(_ sender: UIPinchGestureRecognizer) {
        guard let camera = self.camera else {
            return
        }
        if sender.state == .began {
            sender.scale = camera.xScale
        }
        if sender.state == .changed {
            camera.setScale(sender.scale)
        }
}

For clarity, self.camera is the weak var property defined on SKScene.

When the pinch gesture is initiated, the sender's scale always starts from 1.0. So if you want your camera to start from its previous scale, you need to set the sender's scale to your previous camera scale.

For example, if your camera's scale was 1.5 when the gesture ended, the next time a pinch gesture begins, the sender's scale is set to 1.5. Then when the fingers are moved (i.e. sender.state == .changed), the sender's scale increases/decreases starting from 1.5.

Otherwise, the camera's scale gets set back to 1.0, causing a clipping effect. You will not be able to zoom in/out more than a certain amount, since the scale always starts back from 1.0 instead of picking up where it left off.

According to Apple's documentation,

Because your action method may be called many times, you can’t simply apply the current scale factor to your content. (...) Instead, cache the original value of your content, apply the scale factor to that original value, and apply the new value back to your content.

This means that the action method attached to the gesture recognizer is called continuously (multiple times a second) whenever the fingers move (i.e. sender.state == .changed). If you apply any kind of scaling directly to the camera, be it addition, multiplication, division e.g. camera.setScale(sender.scale + 0.1), the camera's scale will change exponentially.

You can read more here - https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/handling_uikit_gestures/handling_pinch_gestures

EDIT:

My original answer does not account for the fact that SKCameraNode applies its inverse scaling on to the scene's elements. That is to say, for a pinch gesture expecting to zoom the camera out, the camera is actually zooming in. If you want the camera to zoom in and out as expected, you can use the following code:

In GameScene.swift, add a variable to store the previous camera scale:

class GameScene: SKScene {
    var previousCameraScale: CGFloat = CGFloat.zero
}

Change the handlePinch function as follows:

@objc func handlePinch(_ sender: UIPinchGestureRecognizer) {
        guard let camera = self.camera else {
            return
        }
        if sender.state == .began {
            previousCameraScale = camera.xScale
            sender.scale = previousCameraScale
        }
        if sender.state == .changed {
            let change = sender.scale - previousCameraScale
            let newScale = max(min((previousCameraScale - change), maxCameraScale), minCameraScale)
            camera.setScale(newScale)
        }
    }

For sender.state == .began, there is no change. The sender's scale is still set to the previous camera scale so that there is no clipping effect, only now we're storing the variable in the previousCameraScale property so that we can use it in repeated calls of the handlePinch action method. For sender.state == .changed, we first calculate how much the sender's scale has changed. Then we subtract this change from the previous camera scale, and lastly set the camera's scale to this new scale.

For example, if previousCameraScale = 1.5 and the user zooms in (fingers spread apart, causing sender.scale to increase, to, let's say, 1.7), the change will be 0.2. The camera's scale needs to decrease as the sender's scale increases. So if sender.scale is increased to 1.7, camera.scale needs to decrease to 1.3. This is essentially what previousCameraScale - change means.

I also clamped newScale between a lower and upper bound so that (1) the scale doesn't go negative, which would flip everything, and (2) the camera's scale doesn't get too large, in which case everything would become super small.

Upvotes: 0

Magiguigui
Magiguigui

Reputation: 161

Are you using SpriteKit ?

If so you can probably work on camera node scale to implement "zoom" feature: https://developer.apple.com/documentation/spritekit/skcameranode/getting_started_with_a_camera

@objc func handlePinch(sender: UIPinchGestureRecognizer) {
    guard sender.view != nil else { 
        return 
    }

    if sender.state == .began || sender.state == .changed {
        self.camera.xScale = sender.scale
        self.camera.yScale = sender.scale
    }
}

Apple's doc saying :

Because the camera is a node, you define its position within the scene just like any other node. Actions, physics, and GameplayKit behaviors can also be applied to the camera node. When a scene is rendered using a camera node, the following occur: The scene is rendered so that the camera node’s origin is placed in the middle of the scene. The inverse of the camera node’s xScale, yScale, and zRotation properties are applied to all nodes in the scene.

you should probably invert the sender scale value : self.camera.xScale = -sender.scale

Upvotes: 1

Related Questions