Swift中的SpriteKit物理 - Ball滑向墙壁而不是反射

时间:2023-01-23 07:34:49

I have been creating my own very simple test game based on Breakout while learning SpriteKit (using iOS Games by Tutorials by Ray Wenderlich et al.) to see if I can apply concepts that I have learned. I have decided to simplify my code by using an .sks file to create the sprite nodes and replacing my manual bounds checking and collision with physics bodies.

我一直在创建自己非常简单的基于Breakout的测试游戏,同时学习SpriteKit(使用Ray Wenderlich等教授的iOS游戏),看看我是否可以应用我学到的概念。我决定通过使用.sks文件来创建精灵节点并用物理主体替换我的手动边界检查和碰撞来简化我的代码。

However, my ball keeps running parallel to walls/other rectangles (as in, simply sliding up and down them) any time it collides with them at a steep angle. Here is the relevant code--I have moved the physics body properties into code to make them more visible:

然而,只要它以陡峭的角度与它们碰撞,我的球就会保持平行于墙壁/其他矩形(例如,只是向上和向下滑动)。这是相关的代码 - 我已将物理主体属性移动到代码中以使它们更加可见:

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
}

Forgot to mention this before, but I added a simple touchesBegan function to debug the bounces - it just adjusts the velocity to point the ball at the touch point:

之前忘记提到这一点,但我添加了一个简单的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)
}

The normalized() function just reduces the ball/touch position delta to a unit vector, and there is an override of the minus operator that allows for CGPoint subtraction.

normalized()函数只是将球/触摸位置增量减少为单位矢量,并且有一个允许CGPoint减法的减号运算符的覆盖。

The ball/edge collisions should always reflect the ball at a precisely opposite angle but for some reason the ball really seems to have a thing for right angles. I can of course implement some workaround to reflect the ball's angle manually, but the point is that I want to do this all using the built in physics functionality in SpriteKit. Is there something obvious that I am missing?

球/边缘碰撞应始终以恰好相反的角度反射球,但由于某种原因,球似乎确实有一个直角的东西。我当然可以实现一些解决方法来手动反映球的角度,但重点是我想使用SpriteKit中的内置物理功能来完成所有这些操作。有什么明显的东西让我失踪吗?

4 个解决方案

#1


8  

This appears to be an issue with collision detection. Most have found solutions by using the didBeginContact and reapplying the force at an opposite direction. Note he says didMoveToView but corrects himself in a later comment to didBeginContact.

这似乎是碰撞检测的问题。大多数人通过使用didBeginContact并在相反方向上重新施加力来找到解决方案。注意他说didMoveToView但在稍后对didBeginContact的评论中纠正了自己。

See comments at the bottom of the Ray Wenderlich tutorial here

请参阅Ray Wenderlich教程底部的注释

I have a fix for the problem with the ball "riding the rail" if it strikes at a shallow angle (@aziz76 and @colinf). I added another category, "BorderCategory" and assigned it to the border PhysicsBody we create in didMoveToView.

如果它以一个较小的角度撞击(@ aziz76和@colinf),我可以解决球“骑在轨道上”的问题。我添加了另一个类别“BorderCategory”并将其分配给我们在didMoveToView中创建的边界PhysicsBody。

and a similar SO question here explaining why it is happening.

和类似的问题在这里解释它为什么会发生。

Even if you do that, though, many physics engines (including SpriteKit's) have trouble with situations like this because of floating point rounding errors. I've found that when I want a body to keep a constant speed after a collision, it's best to force it to -- use a didEndContact: or didSimulatePhysics handler to reset the moving body's velocity so it's going the same speed it was before the collision (but in the opposite direction).

但即使你这样做,许多物理引擎(包括SpriteKit)也因为浮点舍入错误而遇到这种情况。我发现当我想要一个身体在碰撞后保持恒定速度时,最好强制它 - 使用didEndContact:或didSimulatePhysics处理程序来重置移动体的速度,使其速度与之前相同。碰撞(但方向相反)。

Also another thing I noticed is you are using a square instead of a circle for your ball and you may want to consider using...

我注意到的另一件事是你使用方形而不是圆形球,你可能想考虑使用......

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

So turns out you aren't crazy which is always good to hear from someone else and hopefully this will help you find a solution that works best for your application.

事实证明,你并不疯狂,总是很高兴听到别人的声音,希望这能帮助你找到最适合你应用的解决方案。

#2


7  

I came up with a temporary solution that is working surprisingly well. Simply apply a very small impulse opposite of the border. You may need to change the strength based on the masses in your system.

我提出了一个令人惊讶的临时解决方案。只需在边界对面施加一个非常小的冲动。您可能需要根据系统中的质量来更改强度。

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))
    }
}

In reality, this should not be necessary, since as described in the question, frictionless, fully elastic collision dictates that the ball should rebound by inverting the x velocity (assuming side borders) no matter how small the collision angle is.

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

Instead, what is happening in the game is as if sprite kit ignores the X velocity if it is smaller than a certain value, making the ball slide against the wall without rebound.

相反,游戏中发生的事情就好像精灵套件忽略了X速度,如果它小于某个值,使得球在没有反弹的情况下滑向墙壁。

Final Note

最后的说明

After reading this and this, it's obvious to me that the real answer is for any serious physics game you have, you should be using Box2D instead. You get way too many perks from the migration.

阅读本文后,对我来说很明显,真正的答案是你所拥有的任何严肃的物理游戏,你应该使用Box2D。你从迁移中得到了太多的好处。

#3


1  

This problem only seems to occur when the velocity is small in either direction. However to reduce the effect it is possible to decrease the speed of the physicsWorld, e.g.,

只有当速度在任一方向上都很小时才会出现这个问题。然而,为了减少效果,可以降低物理世界的速度,例如,

physicsWorld.speed = 0.1

and then increase the velocity of the physicsBody, e.g.,

然后增加physicsBody的速度,例如,

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

#4


0  

I was seeing exactly the same issue, but the fix for me was not related to the collision detection issues mentioned in the other answers. Turns out I was setting the ball into motion by using an SKAction that repeats forever. I eventually discovered that this conflicts with SpriteKit's physics simulation leading to the node/ball travelling along the wall instead of bouncing off it.

我看到完全相同的问题,但对我的修复与其他答案中提到的碰撞检测问题无关。事实证明,我通过使用永远重复的SKAction让球进入运动状态。我最终发现这与SpriteKit的物理模拟冲突导致节点/球沿着墙壁行进而不是从它反弹。

I'm assuming that the repeating SKAction continues to be applied and overrides/conflicts with the physics simulation's auto-adjustment of the the ball's physicsBody.velocity property.

我假设重复的SKAction继续被应用并覆盖/与物理模拟的球的physicsBody.velocity属性的自动调整冲突。

The fix for this was to set the ball into motion by setting the velocity on its physicsBody property. Once I'd done this the ball began bouncing correctly. I'm guessing that manipulating its position via physicsBody by using forces and impulses will also work given that they are a part of the physics simulation.

解决这个问题的方法是通过在physicsBody属性上设置速度来将球设置为运动。一旦我这样做,球开始正确弹跳。我猜测,通过使用力和冲动来操纵物理的位置也会有效,因为它们是物理模拟的一部分。

It took me an embarrassing amount of time to realise this issue, so I'm posting this here in case I can save anyone else some time. Thank you to 0x141e! Your comment put me (and my ball) on the right path.

我花了很多时间来实现这个问题,所以我在这里发布这个以防万一我可以节省其他人的时间。谢谢0x141e!你的评论让我(和我的球)走在了正确的道路上。

#1


8  

This appears to be an issue with collision detection. Most have found solutions by using the didBeginContact and reapplying the force at an opposite direction. Note he says didMoveToView but corrects himself in a later comment to didBeginContact.

这似乎是碰撞检测的问题。大多数人通过使用didBeginContact并在相反方向上重新施加力来找到解决方案。注意他说didMoveToView但在稍后对didBeginContact的评论中纠正了自己。

See comments at the bottom of the Ray Wenderlich tutorial here

请参阅Ray Wenderlich教程底部的注释

I have a fix for the problem with the ball "riding the rail" if it strikes at a shallow angle (@aziz76 and @colinf). I added another category, "BorderCategory" and assigned it to the border PhysicsBody we create in didMoveToView.

如果它以一个较小的角度撞击(@ aziz76和@colinf),我可以解决球“骑在轨道上”的问题。我添加了另一个类别“BorderCategory”并将其分配给我们在didMoveToView中创建的边界PhysicsBody。

and a similar SO question here explaining why it is happening.

和类似的问题在这里解释它为什么会发生。

Even if you do that, though, many physics engines (including SpriteKit's) have trouble with situations like this because of floating point rounding errors. I've found that when I want a body to keep a constant speed after a collision, it's best to force it to -- use a didEndContact: or didSimulatePhysics handler to reset the moving body's velocity so it's going the same speed it was before the collision (but in the opposite direction).

但即使你这样做,许多物理引擎(包括SpriteKit)也因为浮点舍入错误而遇到这种情况。我发现当我想要一个身体在碰撞后保持恒定速度时,最好强制它 - 使用didEndContact:或didSimulatePhysics处理程序来重置移动体的速度,使其速度与之前相同。碰撞(但方向相反)。

Also another thing I noticed is you are using a square instead of a circle for your ball and you may want to consider using...

我注意到的另一件事是你使用方形而不是圆形球,你可能想考虑使用......

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

So turns out you aren't crazy which is always good to hear from someone else and hopefully this will help you find a solution that works best for your application.

事实证明,你并不疯狂,总是很高兴听到别人的声音,希望这能帮助你找到最适合你应用的解决方案。

#2


7  

I came up with a temporary solution that is working surprisingly well. Simply apply a very small impulse opposite of the border. You may need to change the strength based on the masses in your system.

我提出了一个令人惊讶的临时解决方案。只需在边界对面施加一个非常小的冲动。您可能需要根据系统中的质量来更改强度。

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))
    }
}

In reality, this should not be necessary, since as described in the question, frictionless, fully elastic collision dictates that the ball should rebound by inverting the x velocity (assuming side borders) no matter how small the collision angle is.

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

Instead, what is happening in the game is as if sprite kit ignores the X velocity if it is smaller than a certain value, making the ball slide against the wall without rebound.

相反,游戏中发生的事情就好像精灵套件忽略了X速度,如果它小于某个值,使得球在没有反弹的情况下滑向墙壁。

Final Note

最后的说明

After reading this and this, it's obvious to me that the real answer is for any serious physics game you have, you should be using Box2D instead. You get way too many perks from the migration.

阅读本文后,对我来说很明显,真正的答案是你所拥有的任何严肃的物理游戏,你应该使用Box2D。你从迁移中得到了太多的好处。

#3


1  

This problem only seems to occur when the velocity is small in either direction. However to reduce the effect it is possible to decrease the speed of the physicsWorld, e.g.,

只有当速度在任一方向上都很小时才会出现这个问题。然而,为了减少效果,可以降低物理世界的速度,例如,

physicsWorld.speed = 0.1

and then increase the velocity of the physicsBody, e.g.,

然后增加physicsBody的速度,例如,

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

#4


0  

I was seeing exactly the same issue, but the fix for me was not related to the collision detection issues mentioned in the other answers. Turns out I was setting the ball into motion by using an SKAction that repeats forever. I eventually discovered that this conflicts with SpriteKit's physics simulation leading to the node/ball travelling along the wall instead of bouncing off it.

我看到完全相同的问题,但对我的修复与其他答案中提到的碰撞检测问题无关。事实证明,我通过使用永远重复的SKAction让球进入运动状态。我最终发现这与SpriteKit的物理模拟冲突导致节点/球沿着墙壁行进而不是从它反弹。

I'm assuming that the repeating SKAction continues to be applied and overrides/conflicts with the physics simulation's auto-adjustment of the the ball's physicsBody.velocity property.

我假设重复的SKAction继续被应用并覆盖/与物理模拟的球的physicsBody.velocity属性的自动调整冲突。

The fix for this was to set the ball into motion by setting the velocity on its physicsBody property. Once I'd done this the ball began bouncing correctly. I'm guessing that manipulating its position via physicsBody by using forces and impulses will also work given that they are a part of the physics simulation.

解决这个问题的方法是通过在physicsBody属性上设置速度来将球设置为运动。一旦我这样做,球开始正确弹跳。我猜测,通过使用力和冲动来操纵物理的位置也会有效,因为它们是物理模拟的一部分。

It took me an embarrassing amount of time to realise this issue, so I'm posting this here in case I can save anyone else some time. Thank you to 0x141e! Your comment put me (and my ball) on the right path.

我花了很多时间来实现这个问题,所以我在这里发布这个以防万一我可以节省其他人的时间。谢谢0x141e!你的评论让我(和我的球)走在了正确的道路上。