Search code examples
swiftcutskphysicsbody

Cut a hole on SKNode and update its physicsBody in Swift


I'm creating an app which will contains some holes on the image. Since the code is very big I've created this simple code which has the same idea. This code has an image with it's physicsBody set already.

What I would like is in the touchesBegan function to draw some transparent circles at the touched location and update the image physicsBody (making a hole on the image).

I've found several codes in objective C and UIImage, can someone help with Swift and SKSpriteNode?

import SpriteKit

class GameScene: SKScene {
    override func didMoveToView(view: SKView) {
        let texture = SKTexture(imageNamed: "Icon.png")
        let node = SKSpriteNode(texture: texture)
        node.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
        addChild(node)

        node.physicsBody = SKPhysicsBody(rectangleOfSize: texture.size())
        node.physicsBody?.dynamic = false

    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {



    }

}

Solution

  • One option is to apply a mask and render that to an image and update your SKSpriteNode's texture. Then use that texture to determine the physics body. This process however will not be very performant.

    For example in touchesBegan you can say something like:

        override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
            guard let touch = touches.first else { return }
            if let node = self.nodeAtPoint(touch.locationInNode(self.scene!)) as? SKSpriteNode{
    
                let layer = self.layerFor(touch, node: node) // We'll use a helper function to create the layer
    
                let image = self.snapShotLayer(layer) // Helper function to snapshot the layer
                let texture = SKTexture(image:image)
                node.texture = texture
    
                // This will map the physical bounds of the body to alpha channel of texture. Hit in performance
                node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.size) 
            }
        }
    

    Here we just get the node and look at the first touch, you could generalize to allow multi touch, but we then create a layer with the new image with transparency, then create a texture out of it, then update the node's physics body.

    For creating the layer you can say something like:

        func layerFor(touch: UITouch, node: SKSpriteNode) -> CALayer
        {
            let touchDiameter:CGFloat = 20.0
    
            let layer = CALayer()
            layer.frame = CGRect(origin: CGPointZero, size: node.size)
            layer.contents = node.texture?.CGImage()
    
            let locationInNode = touch.locationInNode(node)
    
            // Convert touch to layer coordinate system from node coordinates
            let touchInLayerX = locationInNode.x + node.size.width * 0.5 - touchDiameter * 0.5 
            let touchInLayerY = node.size.height - (locationInNode.y + node.size.height * 0.5) - touchDiameter * 0.5
    
            let circleRect = CGRect(x: touchInLayerX, y: touchInLayerY, width: touchDiameter, height: touchDiameter)
            let circle = UIBezierPath(ovalInRect: circleRect)
    
            let shapeLayer = CAShapeLayer()
            shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: node.size.width, height: node.size.height)
            let path = UIBezierPath(rect: shapeLayer.frame)
            path.appendPath(circle)
            shapeLayer.path = path.CGPath
            shapeLayer.fillRule = kCAFillRuleEvenOdd
    
            layer.mask = shapeLayer
            return layer
        }
    

    Here we just set the current texture to the contents of a CALayer then we create a CAShapeLayer for the mask we want to create. We want the mask to be opaque for most of the layer but we want a transparent circle, so we create a path of a rectangle then add a circle to it. We set the fillRule to kCAFillRuleEvenOdd to fill everything but our circle.

    Lastly, we render that layer to a UIImage that we can use to update our texture.

        func snapShotLayer(layer: CALayer) -> UIImage
        {
            UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0.0)
            let context = UIGraphicsGetCurrentContext()
            layer.renderInContext(context!)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            return image
        }
    

    Maybe someone will provide a more performant way of accomplishing this, but I think this will work for a lot of cases.