Christian Cerri
Christian Cerri

Reputation: 1273

SKLightNode is not zooming with SKCameraNode

In Sprite Kit, I'm using an SKCameraNode to zoom in and out of an SKScene. Any SKLightNodes in the scene are not zoomed at all. They are not children of the Camera so should not be invariant. Can't find anything on this - Search "SKLightNode SKCameraNode" on SO yields 0 results.

Using Xcode 8.3.3 and starting from a basic Game/Swift project, I have replaced sceneDidLoad() in GameScene.swift with:

override func sceneDidLoad() {

    let cameraNode = SKCameraNode()
    cameraNode.position = CGPoint(x:0.0, y:0.0)
    self.addChild(cameraNode)
    self.camera = cameraNode

    let bg = SKSpriteNode(color:.red, size:self.size)
    bg.lightingBitMask = 0b0001
    self.addChild(bg)

    let lightNode = SKLightNode()
    lightNode.position = CGPoint(x:0.0, y:0.0)
    lightNode.categoryBitMask = 0b0001
    lightNode.lightColor = .white
    lightNode.falloff = 1.0
    self.addChild(lightNode)

    let zoomDuration:TimeInterval = 10.0
    let zoomAction = SKAction.sequence([
        SKAction.scale(to:0.25, duration:zoomDuration),
        SKAction.scale(to:1.0, duration:zoomDuration)
        ])
    self.camera?.run(zoomAction)

}

As you can see, the light stays the same during the zooms.

In an attempt to fix this problem, I've tried the following custom action to modulate the falloff property of the light. It's sort of ok but it's not a faithful zoom.

    let lightAction1 = SKAction.customAction(withDuration: zoomDuration) {
        (node, time) -> Void in
        let lightNode = node as! SKLightNode
        let ratio:CGFloat = time / CGFloat(zoomDuration)
        let startFalloff:CGFloat = 1.0
        let endFalloff:CGFloat = 0.25
        let falloff:CGFloat = startFalloff*(1.0-ratio) + endFalloff*ratio
        lightNode.falloff = falloff
    }
    let lightAction2 = SKAction.customAction(withDuration: zoomDuration) {
        (node, time) -> Void in
        let lightNode = node as! SKLightNode
        let ratio:CGFloat = time / CGFloat(zoomDuration)
        let startFalloff:CGFloat = 0.25
        let endFalloff:CGFloat = 1.0
        let falloff:CGFloat = startFalloff*(1.0-ratio) + endFalloff*ratio
        lightNode.falloff = falloff
    }
    let lightSequence = SKAction.sequence([lightAction1, lightAction2])
    lightNode.run(lightSequence)

Surely the camera should zoom on the light? Am I missing something?

EDIT: following suggestions below here is some code that scales the SKView:

    let originalWidth:CGFloat = UIScreen.main.bounds.width
    let originalHeight:CGFloat = UIScreen.main.bounds.height

    let lightAction1 = SKAction.customAction(withDuration: zoomDuration) {
        (node, time) -> Void in
        let ratio:CGFloat = time / CGFloat(zoomDuration)
        let startFalloff:CGFloat = 1.0
        let endFalloff:CGFloat = 1.5
        let falloff:CGFloat = startFalloff*(1.0-ratio) + endFalloff*ratio
        self.view?.frame = CGRect(x: (originalWidth-originalWidth*falloff)/2.0, y: (originalHeight-originalHeight*falloff)/2.0, width: originalWidth*falloff, height: originalHeight*falloff)
    }
    let lightAction2 = SKAction.customAction(withDuration: zoomDuration) {
        (node, time) -> Void in
        let ratio:CGFloat = time / CGFloat(zoomDuration)
        let startFalloff:CGFloat = 1.5
        let endFalloff:CGFloat = 1.0
        let falloff:CGFloat = startFalloff*(1.0-ratio) + endFalloff*ratio
        self.view?.frame = CGRect(x: (originalWidth-originalWidth*falloff)/2.0, y: (originalHeight-originalHeight*falloff)/2.0, width: originalWidth*falloff, height: originalHeight*falloff)
    }
    let lightSequence = SKAction.sequence([lightAction1, lightAction2])
    lightNode.run(lightSequence)

You will also need to halve the Camera zoom. The only problem with this is that everything is scaled (even nodes added as children to the CameraNode like scores/buttons).

Upvotes: 4

Views: 293

Answers (2)

Charlie Dancey
Charlie Dancey

Reputation: 43

The solution is that you need to dynamically adjust two parameters as the cameraNode scales: the falloff, and the brightness.

The falloff needs to be multiplied by the square root of the scale.

The brightness needs to be divided by the square root of the scale.

We do this by subclassing SKLightNode, in this example to DXScalableLightNode, and setting up the new class to respond to messages sent when the camera scale changes.

class DXScalableLightNode: SKLightNode {
    
    var baseFalloff     : CGFloat
    var baseHue         : CGFloat = 0.0
    var baseSaturation  : CGFloat = 1.0
    var baseBrightness  : CGFloat = 0.0
    var baseAlpha       : CGFloat = 0.0

    init(falloff: CGFloat, color: UIColor) {
        color.getHue(&baseHue, saturation: &baseSaturation, brightness: &baseBrightness, alpha: &baseAlpha)
        baseFalloff = falloff
        super.init()
        self.lightColor = color
        NotificationCenter.default.addObserver(self, selector: #selector(scaleDidChange), name: NSNotification.Name.cameraScaleDidChange, object: nil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc func scaleDidChange(notification: Notification) {
        guard let scale = notification.userInfo?["scale"] as? CGFloat else {
            fatalError("Notification.cameraScaleDidChange received with no scale")
        }
        falloff     = baseFalloff * sqrt(scale)
        lightColor  = UIColor(hue: baseHue, saturation: baseSaturation, brightness: baseBrightness / sqrt(scale), alpha: baseAlpha)
    }
}

Walking through that we first convert the original color to its base HSBA values and store them.

When the scale changes we adjust the falloff and brightness appropriately.

So we our new class and we initialise a light with something like:

let myLight = DXScalableLightNode(falloff: 0.75, color: .white)

The next step is to make sure your scalable light nodes get informed of scale changes.

In your code, whenever you change the camera scale (say as the result of a pinch gesture), you do this:

cameraNode.setScale(newScale)
NotificationCenter.default.post(name: .cameraScaleDidChange, object: self, userInfo: ["scale": newScale])

...which sends a message to every DXScalableLightNode in your scene and adjusts them so that their lighting effect is properly scaled.

Do note that the Apple docs say that SKLightNode.falloff should be in the range 0.0...1.0 and it does appear that using falloffs greater than 1.0 does slightly break things.

Upvotes: 1

Knight0fDragon
Knight0fDragon

Reputation: 16837

I would like to thank you for providing an example we can test. By playing around with it, I can definitely see the light growing and shrinking but it is not by much, (it is more noticeable if you move the camera off position (0,0) Perhaps theres a math problem on apples end, or a drawing order priority problem.

I have even attempted to add the SKLightNode to a separate SKNode, and scale the node, but that left the same result.

I also attempted to resize the scene to a larger size, and still the same result.

I then decided to play with the view size, low and behold I was able to get different results when I adjusted this. This means that SKLightNode goes off of the SKView size, and not the SKScene size (Really apple?). Either that or it goes off of the context that the SKView provides. Either way, looks like you are not zooming with SKLightNode. (Btw, the shadow still zooms, so perhaps you just need to work in a better lighting affect?)

Upvotes: 3

Related Questions