Search code examples
swiftsprite-kittouchesbegan

How to make touch.locationInNode() recognize the difference between a node and its child?


I started out by declaring two SKSpriteNodes, handle and blade, and adding handle as a child of self, and blade as a child of handle

var handle = SKSpriteNode(imageNamed: "Handle.png")
var blade = SKSpriteNode(imageNamed: "Blade.png")

override func didMoveToView(view: SKView) {

    handle.position = CGPointMake(self.size.width / 2, self.size.height / 14)
    blade.position = CGPointMake(0, 124)

    self.addChild(Handle)
    Handle.addChild(Blade)
}

When I click on the handle, it prints to the console "Handle was clicked", however when I click on the Blade, it also prints "Handle was clicked". It is clearly recognizing that the blade is a child of handle, but how can I make it so when I click on blade, it prints "Blade was clicked"?

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
    for touch in (touches as! Set<UITouch>) {
        let location = touch.locationInNode(self)
        if (Handle.containsPoint(location)){
            NSLog("Handle was clicked")
        }
        else if (Blade.containsPoint(location)){
            NSLog("Blade was clicked")
        }

    }
}

Solution

  • Determining whether the user touched the sword's handle or the blade is fairly straightforward with some caveats. The following assumes that 1. the sword image is facing to the right when zRotation = 0, 2. the anchorPoint of the sword is (0, 0.5), and 3. the sword (blade and handle) is a single sprite node. When you add a sprite to another sprite, the size of the parent's frame expands to include the child node. That's why your test of Handle.containsPoint is true no matter where you click on the sword.

    The figure below shows a sword sprite with a dark gray handle (on the left) and lighter gray blade. The black rectangle surrounding the sword represents the sprite's frame and the circle represents the location of the user's touch. The length of the line labeled a is the distance from the touch point to the bottom of the sword. We can test this distance to see if the user touched the handle (if a <= handleLength) or the blade (if a > handleLength). When zRotation = 0, a = x so the test is x <= handleLength, where the bottom of the sword is x = 0.

    enter image description here

    In the below figure, the sword is rotated by 90 degree (i.e., zRotation = M_PI_2). Similarly, if a <= handleLength, the user touched the handle, else the user touched the blade. The only difference is a is now the y value instead of x due to the sword's rotation. In both cases, the frame's bounding box can be used, as is, to detect if the user touched the sword.

    enter image description here

    When the sprite is rotated by 45 degree, however, its frame automatically expands to enclose the sprite as shown by the black rectangle in the figure below. Consequently, when the user touches anywhere in the rectangle, the test if sprite.frame.contains(location) will be true. This may result in the user picking up the sword when the location of the touch is relatively far from the sword (i.e., when the distance b is large). If we want the maximum touch distance to be the same across all rotation angles, additional testing is required.

    enter image description here

    The good news is Sprite Kit provides a way to convert from one coordinate system to another. In this case, we need to convert from scene coordinates to the sword coordinates. This greatly simplifies the problem because it also rotates the point to the new coordinate system. After converting from scene to sword coordinates, the converted touch location's x and y values are the same as the distances a and b over all rotation angles! Now that we know a and b, we can determine how close the touch was to the sword and whether the user touched the handle or the blade.

    From the above, we can implement the following code:

        let location = touch.locationInNode(self)
        // Check if the user touched inside of the sword's frame (see Figure 1-3)
        if (sword.frame.contains(location)) {
            // Convert the touch location from scene to sword coordinates
            let point = sword.convertPoint(location, fromNode: self)
            // Check if the user touched any part of the sword. Note that a = point.x and b = point.y
             if (fabs(point.y) < sword.size.height/2 + touchTolerance) {
                 // Check if the user touched the handle
                 if (point.x <= handleLength) {
                     println("touched handle")
                 }
                 else {
                     println("touched blade")
                 }
            }
        }