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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With