Search code examples
objective-csprite-kituiresponder

touchesEnded:withEvent: not detecting the top node in SpriteKit


I have a custom class Object derived from SKSpriteNode with an SKLabelNode member variable.

#import <SpriteKit/SpriteKit.h>

@interface Object : SKSpriteNode {
    SKLabelNode* n;
}

@end

In the implementation, I set up the SKLabelNode.

#import "Object.h"

@implementation Object

- (instancetype) initWithImageNamed:(NSString *)name {
    self = [super initWithImageNamed:name];

    n = [[SKLabelNode alloc] initWithFontNamed:@"Courier"];
    n.text = @"Hello";
    n.zPosition = -1;
    //[self addChild:n];

    return self;
}

Note: I have not yet added the SKLabelNode as a child to Object. I have the line commented out.

I have a separate class derived from SKScene. In this class, I add an instance of Object as a child. This is my touchesBegan:withEvent: method in this class:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch* touch = [touches anyObject];
    CGPoint loc = [touch locationInNode:self];
    NSArray* nodes = [self nodesAtPoint:loc]; //to find all nodes at the touch location
    SKNode* n = [self nodeAtPoint:loc]; //to find the top most node at the touch location

    NSLog(@"TOP: %@",[n class]);
    for(SKNode* node in nodes) {
        NSLog(@"%@",[node class]);
    }
}

When I tap on the instance of Object in my SKScene class, it works as I expected. It detects the node is an instance of Object. It logs:

TOP: Object
Object

Now if I go back to my Object class and uncomment [self addChild:n] so that the SKLabelNode is added as a child to Object, this is logged:

TOP: SKLabelNode
Object
SKLabelNode
SKSpriteNode

Why does adding an SKLabelNode as a child to a class derived from SKSpriteNode cause the touch to detect the object itself, along with an SKLabelNode and an SKSpriteNode?

Furthermore, why is the SKLabelNode on top? I have its zPosition as -1 so I would assume that in touchesBegan:withEvent: of another class, the Object would be detected as on top?

Surely, I am not understanding an important concept.


Solution

  • If you use nodeAtPoint to check for the node in -touchesEnded:, it will definitely return the topmost node at the point of the touch, which in this case, is the SKLabelNode. There are a few ways you can go around this:


    Making the subclass detect it's own touches

    The cleanest way would be to make the Object subclass detect it's own touches. This can be done by setting it's userInteractionEnabled property to YES. This can be done in the init method itself.

    -(instanceType) init {
        if (self = [super init]) {
            self.userInteractionEnabled = YES;
        }
        return self;
    }
    

    Now, implement the touch delegate methods in the class itself. Since the LabelNode has its userInteractionEnabled property set by default as NO, the touch delegates will still be triggered when it is tapped.

    You can use either delegation, NSNotificationCenter or a direct message to the parent in the touch delegate.

    -(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
        //Send a message to the delegate, which is the SKScene, should be included in protocol
        [self.delegate handleTouchOnObjectInstance:self];
    
        //or, send a message directly to the SKScene, the method should be included in the interface.
        MyScene *myScene = (MyScene*)self.scene;
        [myScene handleTouchOnObjectInstance:self];
    }
    


    Checking for the node's parent

    In the touch delegate of the SKScene, you can check whether the parent node of the SKLabelNode returned from nodeAtPoint is an instance of the C class.

    //Inside the touch delegate, after extracting touchPoint
    SKNode *node = [self nodeAtPoint: touchPoint];
    
    if ([node isKindOfClass: [SKLabelNode class]]) {
        if ([node.parent isKindOfClass: [Object class]])
            [self performOperationOnCInstance: (Object*)node.parent];
    } else if ([node isKindOfClass: [Object class]])
        [self performOperationOnCInstance: (Object*)node];
    


    Use nodesAtPoint instead of nodeAtPoint

    This method returns an array of all nodes detected at a certain point. So if the SKLabelNode is tapped, the nodesAtPoint method will return the Object node as well.

    //Inside the touch delegate, after extracting touchPoint
    NSArray *nodes = [self nodesAtPoint: touchPoint];
    
    for (SKNode *node in nodes) {
        if ( [node isKindOfClass: [Object class]]) {
            [self performOperationOnObjectInstance: (Object*)node];
            break;
        }
    }
    

    EDIT:

    There is a difference between the zPosition and the node tree.

    The zPosition only tells the scene to render nodes in a certain order, whatever their child order may be. This property was introduced to make it easier to specify the order in which a node's children are drawn. If the same were to be achieved by adding the nodes to the parent in a specific order, it becomes difficult to manage the nodes.

    When the nodesAtPoint method is called, the scene conducts a depth-first search through the node tree to see which nodes intersect the given touch point. This explains the order in which the array is populated.

    The nodeAtPoint method returns the deepest node according to zPosition which the scene could find.

    The following section from Apple's documentation explains it all extensively: Creating The Node Tree