当用户交互设置为YES的Sprite被普通Sprite覆盖时,它无法接收到触摸事件。

9
我放置了一个精灵A(子类化以接收点击(userInteractionEnabled设置为YES)),然后放置了一个普通的精灵B,它不会接收点击(userInteractionEnabled默认为NO),完全覆盖了精灵A。
在精灵B上轻击,我认为精灵A会受到点击,但什么也没有发生。有关此事的文档部分如下。
我觉得这里有些不清楚,因为似乎精灵B仍然接收到了点击,但是被丢弃了。或者,精灵A由于不可见而被从可能的点击接收器中删除。 来自文档的内容: https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Nodes/Nodes.html#//apple_ref/doc/uid/TP40013043-CH3-SW7 为了在命中测试期间考虑节点,必须将其userInteractionEnabled属性设置为YES。对于除场景节点之外的任何节点,默认值为NO。想要接收事件的节点需要从其父类(在iOS上是UIResponder,在OS X上是NSResponder)实现适当的响应方法。这是Sprite Kit中必须实现平台特定代码的少数几个地方之一。
无论如何解决? 只要某物的userInteractionEnabled是NO,它就不应干扰其他触摸接收者。
更新:即使将精灵B的alpha设置为0.2,使精灵A非常可见,也无法使精灵A可触摸。精灵B完全“吞噬”了触摸,尽管未启用交互。

3
我也遇到了同样的问题。即使覆盖的精灵(sprite)隐藏了,问题仍然存在。这一定是一个 bug。如果我在场景中创建了一个可触摸的 SKSpriteNode,然后将其隐藏,它仍然会接收到点击事件。我正在考虑向苹果提交一个错误报告,但我的上一个报告甚至没有得到任何回应,所以我对此并不太激动。 - bobmoff
我认为你应该提交错误报告。在我的职业生涯中,我只提交了几个错误报告。其中大部分都被关闭为重复项,但是值得一提的是,他们修复了其中一个问题(iPad方向问题,自iOS5以来应该已经解决;^))。当然,对错误报告的响应可能需要数月甚至更长时间,但这仍然值得,因为这不会自己改变。 - Jonny
没错,我会处理的。感谢你的启发!让人烦恼的是,在SpriteKit中出现错误与使用Cocos2D相比,修复新版本中的某些问题会要求用户拥有最新的操作系统版本...而不是将其包含在应用程序的新版本中。 - bobmoff
是的。而且使用cocos2d,您可以随时编辑源代码以适应您的需求。开源的优势。您无法使用Sprite Kit做到这一点。您只能提交错误报告或进行丑陋的解决方法。这就是我认为cocos2d此时应该强调的地方:开放性和快速修复错误,平台兼容性(超出Apple)。因为确实,Sprite Kit从现在开始将变得很大。 - Jonny
1
仍未在iOS 8中修复 - Epic Byte
显示剩余4条评论
5个回答

10
直到苹果更新SpriteKit以修正错误行为,或有人真的找到了如何按照我们想要的方式使用它的方法之前,这是我的解决方案。
将文件添加到您的项目中,并在前缀标头中导入它。现在,触摸应该按预期工作。
链接:https://gist.github.com/bobmoff/7110052

很酷,我一定会稍后去看看的。我正在移植一个cocos2d游戏,在这个过程中我做了一个丑陋的解决方法,放置了新的临时按钮(如果您愿意,请称它们为sprite-button Cs),看起来像是覆盖在sprite B上面的。如果我没记错的话,在UIView中不可交互的视图不会阻挡下方的按钮,所以这里SpriteKit的行为是不正确的,因此存在不一致性。无论如何,这应该提交到错误报告中... - Jonny
我看了一下你的解决方案,不太确定如何使用它。你能提供几句话的使用说明吗?谢谢 :) - zeeple
该解决方案扩展了SKScene和SKNode,因此您无需进行其他操作即可使其正常工作。只需确保将文件导入场景头文件的顶部,或者如果您想在所有场景中使用它,请在前缀头文件中#import它,该文件名为ProjectName-Prefix.pch,并包含在“Supporting Files”文件夹中。 - bobmoff
我遇到了完全相同的问题。一个SKLabelNode覆盖了一个按钮(尽管文本并没有真正覆盖按钮,我仍然不确定SKLabelNode的框架是如何确定的)。将TouchPriority.m和TouchPriority.h添加到项目中就足够了,不需要将它们添加到pch文件中。感谢bobmoff! - Joris Weimar
我想详细说明一下这个机制如何工作。由于重叠的精灵会阻止目标精灵接收触摸事件(但并不会接收触摸事件),因此触摸事件会被转发到最低的父节点,该节点接收触摸事件。如果该节点是SKScene,则按照此答案中描述的类别方法遍历节点树,并根据其自身的选择过程找到相应的节点,这正是你想要的。很好的解决方案,帮助了我(在iOS 9.0公共测试版1中测试通过)。 - CloakedEddy
显示剩余3条评论

2

我的解决方案不需要使用isKindOfClass之类的东西...

在你的SKScene接口中:

@property (nonatomic, strong) SKNode*   touchTargetNode;
// this will be the target node for touchesCancelled, touchesMoved, touchesEnded

在您的SKScene实现中
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch*    touch   = [touches anyObject];
    NSArray*    nodes   = [self nodesAtPoint:[touch locationInNode:self]];

    self.touchTargetNode = nil;

    for( SKNode* node in [nodes reverseObjectEnumerator] )
    {
        if( [node conformsToProtocol:@protocol(CSTouchableNode)] )
        {
            SKNode<CSTouchableNode>*    touchable   = (SKNode<CSTouchableNode>*)node;

            if( touchable.wantsTouchEvents )
            {
                self.touchTargetNode = node;
                [node touchesBegan:touches
                         withEvent:event];
                break;
            }
        }
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.touchTargetNode touchesCancelled:touches
                                 withEvent:event];
    self.touchTargetNode = nil;
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.touchTargetNode touchesMoved:touches
                             withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.touchTargetNode touchesEnded:touches
                                 withEvent:event];
    self.touchTargetNode = nil;
}

您需要定义CSTouchableNode协议...可以随意命名 :)
@protocol CSTouchableNode <NSObject>

- (BOOL) wantsTouchEvents;
- (void) setWantsTouchEvents:(BOOL)wantsTouchEvents;

@end

如果您希望使您的SKNodes可以被触摸,则需要符合CSTouchableNode协议。您需要根据需要向您的类添加以下内容:

@property (nonatomic, assign) BOOL wantsTouchEvents;

这样做,点击节点可以按照我预期的方式工作。而且当苹果修复“userInteractionEnabled”错误时,它不应该出现问题。是的,这是一个错误,一个愚蠢的错误。愚蠢的苹果。
更新
SpriteKit中还有另一个bug……节点的z顺序很奇怪。我曾经见过节点的奇怪排序……因此我倾向于为需要的节点/场景强制设置zPosition。
我已更新我的SKScene实现,根据zPosition对节点进行排序。
nodes = [nodes sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"zPosition" ascending:false]]];
for( SKNode* node in nodes )
{
    ...
}

1

使用更少的代码解决方案。记得在每个想要接收触摸的节点中设置userInteractionEnabled。

- (void)handleTap:(UITapGestureRecognizer *)gestureRecognizer {
    CGPoint touchLocation = self.positionInScene;
    SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];

    NSArray *nodes   = [self nodesAtPoint:touchLocation];
    for (SKSpriteNode *node in [nodes reverseObjectEnumerator]) {
        NSLog(@"Node in touch array: %@. Touch enabled: %hhd", node.name, node.isUserInteractionEnabled);
        if (node.isUserInteractionEnabled == 1)
            touchedNode = node;
    }
    NSLog(@"Newly selected node: %@", touchedNode);
}

1
Here is an example for getting touches etc..
场景描述:
-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        /* Setup your scene here */

        HeroSprite *newHero = [HeroSprite new];
        newHero.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
        [self addChild:newHero];

        SKSpriteNode *overLapSprite = [SKSpriteNode spriteNodeWithColor:[UIColor orangeColor] size:CGSizeMake(70, 70)];
        overLapSprite.position = CGPointMake(CGRectGetMidX(self.frame) + 30, CGRectGetMidY(self.frame));
        overLapSprite.name = @"overlap";
        [self addChild:overLapSprite];
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];
    SKNode *node = [self nodeAtPoint:location];

    if ([node.name isEqualToString:@"heroNode"]) {
        NSLog(@"Scene detect hit on hero");
    }
    if ([node.name isEqualToString:@"overlap"]) {
        NSLog(@"overlap detect hit");
    }

    // go through all the nodes at the point
    NSArray *allNodes = [self nodesAtPoint:location];
    for (SKNode *aNode in allNodes) {
        NSLog(@"Loop; node name = %@", aNode.name);
    }
}

子类化精灵/英雄

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

-(void) setUpHeroDetails
{
    self.name = @"heroNode";
    SKSpriteNode *heroImage = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
    [self addChild:heroImage];
}


-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint locationA = [touch locationInNode:self];
    CGPoint locationB = [touch locationInNode:self.parent];
    NSLog(@"Hero got Hit at, %@ %@", NSStringFromCGPoint(locationA), NSStringFromCGPoint(locationB));
}

@end

尝试了很多方法,但是触摸无法穿过重叠的精灵。我猜可以使用场景类通过循环节点来检测触摸,然后直接调用该节点。
当粒子发射器覆盖按钮时,我遇到了这个问题...

这个解决方案完美地运作。重叠的节点按照它们在zOrder中的位置接收触摸事件。高位置的节点先接收,然后是所有下面的节点。从这里只需要一步,过滤出您想要接收触摸事件的对象,并拒绝其余的触摸事件即可。 - Endre Olah

1
我认为这可能是一种设计选择,但肯定不是非常灵活的。我更喜欢使用nodeAtPoint:结合isKindOfClass:方法在Sprite Kit中处理可以重叠的对象(如游戏对象)。对于通常不重叠并放置在不同层(如覆盖和按钮)中的按钮等对象,我在它们的类内处理它们的用户交互。
我希望我的触摸事件能够像Sparrow Framework中那样“冒泡”到节点树中,但我担心这更多是一个功能请求而不是错误报告。
附言:通过在场景级别处理触摸,即使节点完全被不可移动和不可触摸的节点覆盖,您也可以轻松触摸和移动节点!

我认为这不是一个设计选择,或者文档本身就是错误的。“要使节点在命中测试期间被考虑,其userInteractionEnabled属性必须设置为YES。对于除场景节点以外的任何节点,默认值为NO。”您是否也会以这种方式处理按钮,包括高亮显示? - Jonny
嗯,我想你是对的,我没有仔细阅读它。我基本上将场景组织在两个主要层上,游戏层(首先添加)和覆盖层(在顶部)。考虑到按钮不会重叠在一起,我在按钮类内处理它们的交互。我只是为iOS开发,所以我从来没有使用高亮,但我认为我会以同样的方式处理。 - membersheep
2
我在柏林的iOS技术讲座上与苹果团队交流了这些问题,他们说这可能是一个bug,因为不仅我一个人报告了这个问题,而且行为方式也不符合文档所述。 - bobmoff

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接