为什么didBeginContact会被多次调用?

19
在一个使用Sprite Kit与Sprite Kit内置物理引擎中的接触检测的iOS游戏中,每当英雄与敌人接触时,我会从didBeginContact方法中减少英雄的生命数。 但是,似乎该方法不仅在接触开始时调用一次,而且在英雄和敌人重叠期间连续调用:当我在该方法中设置断点时,我可以看到作为contact.bodyAcontact.bodyB 的确切相同物理体实例。结果是,即使英雄只通过一个单独的敌人,他也会失去多个生命。
如果英雄稍后再次遇到同样的敌人,应该再减少一个生命,因此我不能仅维护一个seenEnemies哈希集来处理上述问题。
现在的问题是:您如何确保每次英雄/敌人接触只减少一个生命?

我会假设也有一个 didEndContact - dandan78
寻求调试帮助的问题(“为什么这段代码不起作用?”)必须在问题本身中包含所需的行为、具体问题或错误以及重现它所需的最短代码。没有明确问题陈述的问题对其他读者没有用处。 - CodeSmile
参考文档声称该方法仅在接触开始时调用。我理解为“每个接触只调用一次”。显然,事实并非如此。在这一点上,参考文档真的错了吗? - someName
我曾经遇到过同样的问题,只有在调试了10分钟后才明白,在我的情况下didBeginContact被调用了两次。 - WebOrCode
6个回答

21
由于您的凹形形状上发生了多个接触点,因此didBeginContact被触发多次。
如果您看下面的图片,您会看到我有两个精灵,一个黑色星星和一个红色矩形。当黑色星星撞击红色矩形时,它会在多个点上撞击,用蓝色圈圈标出。Sprite Kit将为每个线交点调用一次,以便开发人员可以针对每个接触使用contactPoint变量。

enter image description here


1
好的观点,这甚至可能发生在简单的形状上。例如,当一个圆与一条直线相交时,在大多数情况下会有两个接触点。 - salocinx
我并没有。我仍然非常喜欢你的插图 :) 尽管我认为你的插图中至少有4个接触点,而不仅仅是2个 ;-p - salocinx
是的哈哈,我注意到这一点很久了,但我从未费心去改变它,只是在演示一个想法。而且,我并不认为那是你,有人随机进来投了反对票,却没有给出任何理由。现在我无法改进答案了。 - Knight0fDragon
是的,您的答案在我的帐户中仍然被点赞。当有人对其进行了踩票后,您无法编辑您的答案吗? - salocinx
你可以这样做,但我不知道为什么他们会对此进行负面评价,所以我无法改进它,哈哈。 - Knight0fDragon
也许那个点踩的人只是有些沮丧,然后看到了你可爱的照片 ;D - salocinx

19

我遇到了同样的问题(单个敌人被摧毁时得分多次、单个伤害实例失去多个生命点数)。苹果论坛上的一个用户认为这是[SKPhysicsBody bodyWithTexture:size:]中的错误,但我不认为这是正确的,因为其他构造器也出现了这种情况。

首先,显然,categoryBitMaskcontactTestBitMask非常重要。可以查看苹果的SpriteKit Physics Collisions示例代码

// 联系通常是双重调度问题;您想要的效果基于联系中两个物体的类型。该示例以一种粗暴的方式来解决此问题,通过检查每个的类型来实现。更复杂的示例可能使用对象上的方法来执行类型检查。

//联系人可能以任何顺序出现,因此通常需要相互检查。在此示例中,类别类型是完全有序的,因此 如果它们的顺序不对,代码会交换两个主体。这允许代码 只进行碰撞测试一次。

我解决问题的方法是在处理每个条件后设置一个标志。在我的情况下,我在didBeginContact中检查了bodyA.node.parent是否为nil,因为我调用了removeFromParent()来摧毁导弹/敌人节点。

我认为您应该期望事件多次触发,而您在其中编写的代码必须确保它仅被处理一次。


1
使用SKPhysicsBody(rectangleOfSize:)构造函数“解决”了我的问题,谢谢!希望他们能修复这个bug。 - YourMJK
没错!SKPhysicsBody(texture:size:)有一个bug。 - undefined

10

我想出了一个简单的解决方案:

只需要在检测到接触后将其中一个物体的categoryBitMask值更改为0或未使用的值即可。

例如:

if (firstBody.categoryBitMask == padCategory && secondBody.categoryBitMask == colorBallCategory) {

      secondBody.categoryBitMask = 0;

      // DO OTHER THING HERE

}

6

我遇到了同样的问题。在我的情况下,当一颗子弹与敌人接触时,didBeginContact()会被调用多次(我数了最多5次)。由于子弹只是一个简单的圆形格式,我同意@SFX的看法,这不可能是Texture-Bodies中的一个bug。测试表明,在didBeginContact()调用之间没有调用update()。因此,解决方案很简单(Swift):

var updatesCalled = 0
...
internal update() {
  updatesCalled ++
}
...
internal func didBeginContact(contact: SKPhysicsContact) {
    NSLog("didBeginContact: (\(contact.contactPoint.x), \(contact.contactPoint.y)), \(updatesCalled)")
    if(updatesCalled == 0) {return} // No real change since last call
    updatesCalled = 0
    ... your code here ...
}

我尝试使用didEndContact(),但它根本没有被调用。我没有进一步调查这个问题。
顺便说一下:我刚从安卓系统转过来,对这个系统的易用性和稳定性印象深刻 :-)

1

这里有一个选项,可以让玩家在被攻击后一段时间内无敌:

A. 创建一个变量,在玩家被攻击后的几秒钟内使其无敌,以免失去生命。

  1. 创建一个名为isInvuln的全局布尔变量(设置为FALSE),以及一个名为invulnTime的NSTimeInterval。
  2. 在处理玩家和敌人接触的方法中,在扣除生命之前检查isInvuln是否为False。(如果isInvuln为true……则什么都不做)
  3. 如果isInvuln为false,则扣除一条生命,然后将isInvuln设置为true。

     if(self.isInvuln == FALSE){
          self.player.lives-=1;
          self.isInvuln = True;}
    
  4. 在updateWithCurrentTime中添加:

     if(self.isInvuln==True){
     self.invulnTime += timeSinceLast;}
    
     if (self.invulnTime > 3) {             
         self.isInvuln = FALSE:}
         self.invulnTime= 0;
    
当敌人和玩家碰撞时,这将导致玩家失去一条生命并在3秒内变得无敌。 之后的3秒钟,玩家可以再次受到伤害。 如果敌人在3秒的无敌时间内接触玩家,则接触方法不起作用。 希望这能激发您解决问题的想法。

2
我最终让英雄在第一次接触时免疫一段时间,从而规避了问题。然而,原始问题仍未得到回答:didBeginContact是否会在持续接触的情况下被故意不断触发,如果是这种情况,那么有什么好的策略可以仅注册第一个接触(并在英雄走出接触区域后重新启用接触动作)? - someName
尝试使用didEndContact重新启用联系人(而不是使用时间),这将完全符合您的要求。我不能对didEndContact发表评论,因为我并没有真正使用过它。 - meisenman
你还可以将与接触时会夺取生命的对象设为SKSpriteNode的子类,并添加一个布尔值属性。在第一次接触后,你可以将该布尔值设为false。当发生接触时,如果该布尔值为true,你将扣除一条生命并将该布尔值设为false。如果该布尔值为false...则不做任何操作。 - meisenman

0
在我的经验中,当对象重叠时,didEndContact和didBeginContact都会被多次调用。这在使用iOS 9的SceneKit中也发生了,因此我必须假设这是一种预期行为。

这种情况对我来说也是发生了,有点让人沮丧。在我的情况下,我只是进行接触测试而没有发生碰撞。我想简单地检测玩家是否在某物体上方、里面。让我烦恼的是当我的角色完全处于contactTest对象内部且从未离开其边界时,didEnd会被不断地触发。对于我的角色的任何像素来说,接触肯定还没有结束,但它仍然会被调用。 - DiggyJohn

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