在Swift中的SpriteKit物理引擎——球体滑向墙壁而不是反弹

20

我一边学习SpriteKit(使用Ray Wenderlich等人的iOS Games by Tutorials),一边基于Breakout创建了一个非常简单的测试游戏,以查看我是否能够应用所学的概念。我决定通过使用.sks文件创建精灵节点并将手动边界检查和碰撞替换为物理体来简化代码。

然而,每当球以陡峭的角度与墙壁/其他矩形碰撞时,它就会沿着它们平行地滑动(即仅向上和向下滑动)。以下是相关代码 - 我已经将物理体属性移入代码中以使其更可见:

import SpriteKit

struct PhysicsCategory {
  static let None:        UInt32 = 0      //  0
  static let Edge:        UInt32 = 0b1    //  1
  static let Paddle:      UInt32 = 0b10   //  2
  static let Ball:        UInt32 = 0b100  //  4
}

var paddle: SKSpriteNode!
var ball: SKSpriteNode!

class GameScene: SKScene, SKPhysicsContactDelegate {

  override func didMoveToView(view: SKView) {

    physicsWorld.gravity = CGVector.zeroVector

    let edge = SKNode()
    edge.physicsBody = SKPhysicsBody(edgeLoopFromRect: frame)
    edge.physicsBody!.usesPreciseCollisionDetection = true
    edge.physicsBody!.categoryBitMask = PhysicsCategory.Edge
    edge.physicsBody!.friction = 0
    edge.physicsBody!.restitution = 1
    edge.physicsBody!.angularDamping = 0
    edge.physicsBody!.linearDamping = 0
    edge.physicsBody!.dynamic = false
    addChild(edge)

    ball = childNodeWithName("ball") as SKSpriteNode
    ball.physicsBody = SKPhysicsBody(rectangleOfSize: ball.size))
    ball.physicsBody!.usesPreciseCollisionDetection = true
    ball.physicsBody!.categoryBitMask = PhysicsCategory.Ball
    ball.physicsBody!.collisionBitMask = PhysicsCategory.Edge | PhysicsCategory.Paddle
    ball.physicsBody!.allowsRotation = false
    ball.physicsBody!.friction = 0
    ball.physicsBody!.restitution = 1
    ball.physicsBody!.angularDamping = 0
    ball.physicsBody!.linearDamping = 0

    physicsWorld.contactDelegate = self
}

之前忘记提到了,但我添加了一个简单的touchesBegan函数来调试弹跳 - 它只是将速度调整为指向触摸点的球:

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
  let touch = touches.anyObject() as UITouch
  let moveToward = touch.locationInNode(self)
  let targetVector = (moveToward - ball.position).normalized() * 300.0
  ball.physicsBody!.velocity = CGVector(point: targetVector)
}

normalized()函数将球/触摸位置差减小到一个单位向量,minus操作符被重写以允许CGPoint相减。

球/边缘碰撞应该总是以完全相反的角度反射球,但由于某些原因,球似乎真的很喜欢直角。我当然可以实现一些解决方案来手动反射球的角度,但关键是我想使用SpriteKit内置的物理功能来完成所有这些操作。有什么明显的问题我忽略了吗?


2
您需要通过对其物理体施加力或冲量,或设置其速度属性来移动精灵。如果您使用动作或直接设置其位置来移动精灵,则该精灵将不参与物理模拟。 - 0x141E
抱歉,我忘了我的速度调整。我刚刚进行了编辑,但基本上它是一个触摸事件,将球的速度设置为朝向SKScene中的触摸点。 - Firstname_Numbers
当触点发生时,您是轻触并释放手指还是按住手指不放? - 0x141E
只需要一个轻触(touchesBegan)就可以更新速度。球向那个点移动得很好,只是弹跳有问题。如果球以45度角碰撞,它完全正常。但是,如果它以接近直角的角度碰撞,它将沿着墙壁上下滑动而不是弹跳。 - Firstname_Numbers
我现在遇到了完全相同的问题。 - Mazyod
@simurg 根据应用的纹理确定球的大小。通过打开物理调试,确认物理体与精灵大小相匹配。 - Mazyod
6个回答

10

这似乎是一种碰撞检测问题。大多数人通过使用didBeginContact并在相反方向重新应用力量来找到解决方案。请注意,他在最初的评论中说了didMoveToView,但在后面的评论中进行了更正,改为说didBeginContact。

请参见Ray Wenderlich教程底部的评论here

我已经解决了如果以一个浅角度击中球时“沿着轨道滑行”的问题(@aziz76和@colinf)。我添加了另一个类别“BorderCategory”,并将其分配给我们在didMoveToView中创建的边框PhysicsBody。

还有一个类似的SO问题,here解释了发生这种情况的原因。

即使您这样做,许多物理引擎(包括SpriteKit的)也会遇到此类情况的困难,因为会出现浮点舍入误差。我发现,当我希望身体在碰撞后保持恒定速度时,最好强制执行它 - 使用didEndContact:或didSimulatePhysics处理程序重置移动体的速度,以使其以相同的速度运动与碰撞之前一样(但是方向相反)。

还有另一件事我注意到您使用的是正方形而不是圆形的球,您可能需要考虑使用...

ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.width/2)

原来你并不是疯了,这总是从别人那里听到的好消息,希望这可以帮助你找到最适合你应用的解决方案。


问题的解释非常好。+1 - sangony
就个人而言,我已经找到了冲量解决方案,这是最直观的方法,但不应该是必要的。我知道SpriteKit没有确定性物理引擎,但对于所有情况都有更精细的解决方案很感兴趣。最后,我的球体具有圆形身体。 - Mazyod
@Mazyod 我同意。这不应该是一个问题。如果你不能信任物理引擎,那么你的应用程序可能无法按照预期工作,甚至可能不是你的错。对于像打砖块游戏这样的东西,最好在更新循环中处理自己的位置/速度,但让SK在接触开始时通知你。这样,您始终具有一致的速度,因为您完全控制它。类似的事情已经通过使用屏幕上的摇杆来移动角色而完成。希望苹果在下一个更新中改进这一点。 - Skyler Lauren
@SkylerLauren 我也曾经考虑过这种方法,但我感觉好像会重新发明轮子,因为我将处理各种碰撞,例如与方形边缘、圆形物体以及可能的多边形相撞。我已经在下面更新了我的答案,采用的方法是“使用Box2D”。 - Mazyod
为了完美的回答,我们应该在这个答案中添加一个冲击解决方案。或者将你的两个答案合并成一个 ;) - Jurik

8
我想到了一个临时解决方案,效果出奇地好。只需施加一个非常小的脉冲,与边界相反的方向即可。根据系统中的质量,您可能需要更改 strength 的值。
func didBeginContact(contact: SKPhysicsContact) {

    let otherNode = contact.bodyA.node == ball.sprite ? contact.bodyB.node : contact.bodyA.node

    if let obstacle = otherNode as? Obstacle {
        ball.onCollision(obstacle)
    }
    else if let border = otherNode as? SKSpriteNode {

        assert(border.name == "border", "Bad assumption")

        let strength = 1.0 * (ball.sprite.position.x < frame.width / 2 ? 1 : -1)
        let body = ball.sprite.physicsBody!
        body.applyImpulse(CGVector(dx: strength, dy: 0))
    }
}

实际上,这不应该是必要的,因为如问题所述,无摩擦、完全弹性碰撞规定球应该通过反转x速度(假设有侧边框)来反弹,无论碰撞角度多小。

相反,在游戏中发生的情况是,如果X速度小于某个值,sprite kit会忽略它,使球滑动到墙壁上而没有反弹。

最后说明

阅读了thisthis之后,对我来说显然真正的答案是,对于任何严肃的物理游戏,你应该使用Box2D。从迁移中获得太多好处了。


Box2D工作良好,并且没有像粘在边缘上一样的问题吗? - Jurik
@Jurik 这就是我所引用的文章所证明的。 - Mazyod
好的 - 谢谢 - 因为我浏览了两篇文章,没有找到任何关于这个错误或者Box2D没有浮点数舍入问题的信息。 - Jurik
你能回答一下这个问题吗?我在上面设置了悬赏并且它将自动奖励给唯一的答案,但那不是正确的答案。所以我想奖励你的答案。问题链接:https://dev59.com/NmAf5IYBdhLWcg3wlDcD - Jurik

2

这个问题似乎只会在速度较小的情况下发生,无论是向左还是向右。然而,为了减少其影响,可以降低physicsWorld的速度,例如:

physicsWorld.speed = 0.1

然后增加物理体的速度,例如:

let targetVector = (moveToward - ball.position).normalized() * 300.0 * 10
ball.physicsBody!.velocity = CGVector(point: targetVector)

好问题。我不是很确定,但我猜这是由于物理学原因:F = ma。假设m = 1,数值上a = dv / dt,一个猜测是设置physicWorld速度与设置dt相同,即如果我们将pW速度设置为0.1,则有F = dv /(0.1 * dt)。因此,如果滑动发生在F < tol的情况下,我们可以允许v比正常情况下(pW速度= 1)大10倍。这可以通过找到不同质量的滑动速度极限来验证。 - CrazyChris
“粘滞”的物体以恒定速度移动,即a=dv/dt=0…因此,尽管存在某些变量<公差导致“粘滞”,但它可能不是F…也许是动量或仅仅是V。 - greg

2

Add code below:

let border = SKPhysicsBody(edgeLoopFrom: self.frame)
border.friction = 0
border.restitution = 1
self.physicsBody = border

当球体与墙壁碰撞时,你需要让它反弹回来。这就是通过设置物理属性中的恢复系数为1来实现的。

恢复系数指的是物理体的弹性,将其设置为1即可使球体反弹回来。

注:Restitution即恢复系数。


1
我遇到了完全相同的问题,但对我来说解决方法与其他答案中提到的碰撞检测问题无关。结果发现,我是通过使用一个永久重复的SKAction来使球开始运动的。最终我发现,这与SpriteKit的物理模拟发生冲突,导致节点/球沿着墙壁行进而不是反弹。
我假设重复的SKAction会继续应用并覆盖/冲突于物理模拟自动调整球的physicsBody.velocity属性。
解决方法是通过设置球的physicsBody属性上的velocity来使球开始运动。一旦我这样做了,球就开始正确地反弹了。我猜想,通过使用力和冲量通过physicsBody操纵其位置也将奏效,因为它们是物理模拟的一部分。

我花了很长时间才意识到这个问题,感到很尴尬,所以我在这里发布它,以防我能够让其他人节省一些时间。非常感谢0x141e!你的评论让我(和我的球)走上了正确的道路。


0

问题是双重的,因为1)通过改变物理体的摩擦/恢复系数无法解决,并且2)由于接触发生在物体已经开始减速之后,在renderer()循环中应用返回脉冲也不会得到可靠的解决。

问题1:调整物理属性没有效果 - 因为碰撞的角度分量低于某个预定的阈值,物理引擎将不会将其注册为物理碰撞,因此,身体不会根据您设置的物理属性反应。在这种情况下,恢复不会被考虑,无论设置如何。

问题2:在检测到碰撞时施加冲量力将无法产生一致的结果 - 这是因为为了模拟恢复,需要对象在碰撞之前的速度。

例如,如果一个物体以-10m / s的速度撞击地面,您想要模拟0.8的恢复,那么您希望该物体向相反方向推进8m / s。

不幸的是,由于渲染循环,当碰撞发生时注册的速度要低得多,因为物体已经已经减速

-->例如,在我运行的模拟中,以低角度撞击地板的球体到达-9m/s,但在检测到碰撞时注册的速度为-2m/s。

这很重要,因为为了创建一致的恢复表示,我们必须知道预碰撞速度才能到达所需的后碰撞速度...你无法在Swift碰撞回调委托中确定这一点。

解决方案: 第1步。 在渲染周期内记录对象的速度。

//Prior to the extension define two variables:
var objectNode : SCNNode!
var objectVelocity : SCNVector3!

//Then, in the renderer delegate, capture the velocity of the object
extension GameViewController: SCNSceneRendererDelegate 
{
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval)
    {
    if objectNode != nil {
        //Capture the object's velocity here, which will be saved prior to the collision
        if objectNode.physicsBody != nil {          
           objectVelocity = objectNode.physicsBody!.velocity
        }
     }
    }
}

步骤2:在物体发生碰撞时,使用先前保存的速度施加返回冲量。在此示例中,我仅使用y分量,因为我在该轴上模拟恢复。

extension GameViewController: SCNPhysicsContactDelegate {
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {

  let contactNode: SCNNode!

  //Bounceback factor is in essence restitution.  It is negative signifying the direction of the vector will be opposite the impact
  let bounceBackFactor : Float! = -0.8

  //This is the slowest impact registered before the restitution will no longer take place
  let minYVelocity : Float! = -2.5

  // This is the smallest return force that can be applied (optional)
  let minBounceBack : Float! = 2.5

  if contact.nodeA.name == "YourMovingObjectName" && contact.nodeB.name == "Border" {
    //Using the velocity saved during the render loop
    let yVel = objectVelocity.y
    let vel = contact.nodeA.physicsBody?.velocity
    let bounceBack : Float! = yVel * bounceBackFactor

    if yVel < minYVelocity
    {
      // Here, the opposite force is applied (in the y axis in this example)
      contact.nodeA.physicsBody?.velocity = SCNVector3(x: vel!.x, y: bounceBack, z: vel!.z)
    }
  }

  if contact.nodeB.name == "YourMovingObjectName" && contact.nodeA.name == "Border" {
    //Using the velocity saved during the render loop
    let yVel = objectVelocity.y
    let vel = contact.nodeB.physicsBody?.velocity
    let bounceBack : Float! = yVel * bounceBackFactor

    if yVel < minYVelocity
    {
      // Here, the opposite force is applied (in the y axis in this example)
      contact.nodeB.physicsBody?.velocity = SCNVector3(x: vel!.x, y: bounceBack, z: vel!.z)
    }
  }
  }
}

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