SpriteKit physics in Swift - Ball slides against wall instead of reflecting
Asked Answered
A

6

20

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.

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:

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.

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?

Alphard answered 27/12, 2014 at 20:56 Comment(6)
You will need to move the sprite by applying a force or impulse to its physics body or by setting its velocity property. The sprite will not participate in the physics simulation if you move it with an action or by setting its position directly.Mews
Sorry, I had left out my velocity adjustment. I just edited it in, but basically it is a touch event that sets the ball velocity toward the touch point in the SKScene.Alphard
Are you tapping and releasing your finger or holding it down when the contact occurs?Mews
It is just a tap (touchesBegan) that updates the velocity.The ball moves toward that point fine, it is just the bouncing that has the issue. If the ball collides at say, a 45 degree angle, it works completely fine. However if it collides at something closer to a right angle, it will slide up and down the wall instead of bouncing off.Alphard
I am having the exact same issue nowMisplay
@simurg the ball's size based on the texture applied. By turning on the physics debug, it's confirmed that the physics body matches the sprite size.Misplay
G
10

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.

See comments at the bottom of the Ray Wenderlich tutorial here

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.

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

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.

Godart answered 4/4, 2015 at 15:2 Comment(5)
Excellent explanation on what the problem is. +1Reglet
Personally, I have already found the impulse solution, was the most intuitive way to go, but again, it shouldn't be necessary. I understand that SpriteKit does not have a deterministic physics engine, but was interested in a more elaborate solution for all cases. finally, by ball has a circular body.Misplay
@Misplay I agree. It shouldn't be an issue. If you can't trust the physics engine then you run the fear of your app not working as intended and it may not even be your fault. For something like a breakout game it might be better to handle your own positioning/velocity in the update loop, but let SK notify you when contact starts. That way you always have a consistent velocity because you have complete control over it. Similar things have been done with moving a character around with an onscreen joystick. Hopefully Apple improves this with the next update.Godart
@SkylerLauren I actually went down that train of thought as well, but it seemed to me like I'd be reinventing the wheel, since I'll be handling all kinds of collisions, such as colliding with square edges, circular bodies, and possibly polygons. I updated my answer below with the approach I'm left with, that is "use Box2D".Misplay
In spirit of a perfect answer we should add an impulse solution to this answer as well. Or merge your two answers into one ;)Baten
M
8

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.

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.

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.

Misplay answered 3/4, 2015 at 14:8 Comment(4)
Box2D works fine and has no problems like this stickiness to the edge?Baten
@Baten That's what the articles I referenced proveMisplay
Ah okay - thanks - because I glanced through both articles and I didn't find any information that belongs to this bug or that Box2D has no float rounding problems.Baten
Could you answer this question please? I set a bounty on it and it will be auto rewarded to the only answer there and that's not the correct answer. So I'd like to reward your answer.Baten
N
2

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.,

let targetVector = (moveToward - ball.position).normalized() * 300.0 * 10
ball.physicsBody!.velocity = CGVector(point: targetVector)
Nae answered 3/3, 2016 at 15:15 Comment(2)
Good question. I don’t really know but I guess that it’s due to the physics: F = ma. Assume that m = 1 and numerically a = dv / dt, a guess is that setting the physicWorld speed is the same as setting dt, i.e., if we set the pW-speed to 0.1 we have that F = dv / (0.1*dt). And so if the sliding occurs for F<tol, then we can allow v to be 10 times as large as in the normal case (pW-speed = 1). This might be verified by finding the speed limit for sliding for different masses.Nae
The body which "sticks" is moving at constant speed, ie a=dv/dt=0...so although there is some variable < tolerance which is causing "sticking", it's probably not F...maybe Momentum or just VWedurn
A
2

Add code below:

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

which will make your ball bounce back when it collides with wall. Restitution is the bounciness of the physics body so setting it to 1 will bounce ball back.

Ascription answered 7/6, 2019 at 11:20 Comment(0)
R
1

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.

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.

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.

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.

Respite answered 23/8, 2015 at 20:48 Comment(0)
O
0

The problem is twofold in that 1) it will not be solved by altering friction/restitution of the physics bodies and 2) will not be reliably addressed by a return impulse in the renderer() loop due to the contact occurring after the body has already begun decelerating.

Issue 1: Adjusting physics properties has no effect -- Because the angular component of the collision is below some predetermined threshold, the physics engine will not register it as a physical collision and therefore, the bodies will not react per the physics properties you've set. In this case, restitution will not be considered, regardless of the setting.

Issue 2: Applying an impulse force when the collision is detected will not produce consistent results -- This is due to the fact that in order to simulate restitution, one needs the velocity of the object just prior to impact.

-->For instance, if an object hits the floor at -10m/s and you want to simulate 0.8 restitution, you would want that object to be propelled 8m/s in the oppostie direction.

Unfortunately, due to the render loop, the velocity registered when the collision occurs is much lower since the object has already decelerated.

-->For example, in the simulations I was running, a ball hitting a floor at a low angle was arriving at -9m/s, but the velocity registered when the collision was detected was -2m/s.

This is important since in order to create a consistent representation of restitution, we must know the pre-collision velocity in order to arrive at our desired post-collision velocity...you can't ascertain this in the Swift collision callback delegate.

Solution: Step 1. During the render cycle, record the velocity of the object.

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

Step 2: Apply a return impulse when the object collides, using the velocity saved prior to the collision. In this example, I am only using the y-component since I am simulating restitution in that axis.

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)
    }
  }
  }
}
Ovi answered 12/4, 2020 at 7:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.