I'm making a 2D scrolling shooter in Swift
with SpriteKit
. I've set up SKPhysicsBody
and using bitmasks for collisions. I keep getting intermittent errors, where the collisions will work fine and then stop working. The error I get is Fatal error: Unexpectedly found nil while unwrapping an Optional value. I don't understand why I get nil value sometimes, when it gets a value other times. I have a few different sprites in the game and after testing a lot to see if there is any difference in the collisions, I can't seem to find the problem. For example, a few play throughs and I shoot an asteroid with the laser and it will work fine. The next day the exact same thing crashes the game. Another example asteroid hits player head on and works fine, asteroid hits player from the side crashes game but next day could work fine. I don't know if the problem is with the way I've set the PhysicsBody for each sprite, as I've tried changing that and still had problems, or have I got the SKPhysicsContact
set up all wrong. Any help would be mostly appreciated, Thank you.
Striped down version of my code
import SpriteKit
import GameplayKit
import CoreMotion
@objcMembers
class GameScene: SKScene, SKPhysicsContactDelegate {
//Player Image
let player = SKSpriteNode(imageNamed: "Player.png")
//Timer to spawn enemies
var gameTimer:Timer!
//Array for different astroids
var astroidArray = ["astroid1", "astroid2"]
//Array for differnet enemy ships
var enemyArray = ["Enemy1"]
//For collision
let playerCategory:UInt32 = 0x1 << 1
let playerLaserCategory:UInt32 = 0x1 << 2
let astroidCategory:UInt32 = 0x1 << 3
let enemyCategory:UInt32 = 0x1 << 4
let bossCategory:UInt32 = 0x1 << 5
override func didMove(to view: SKView) {
//Position Player
player.position.y = -400
player.zPosition = 1
addChild(player)
//Player Physics for collision
//player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.physicsBody?.isDynamic = true
player.physicsBody?.categoryBitMask = playerCategory
player.physicsBody?.contactTestBitMask = astroidCategory | enemyCategory | bossCategory
//avoid any unwanted collisions
//player.physicsBody?.collisionBitMask = 0
//Physics for World
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
physicsWorld.contactDelegate = self
//Timer to spawn astroids
gameTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(addAstroid), userInfo: nil, repeats: true)
//Timer to spawn enemy
gameTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(addEnemy), userInfo: nil, repeats: true)
}
func addAstroid() {
astroidArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: astroidArray) as! [String]
//Select astroid from array
let astroid = SKSpriteNode(imageNamed: astroidArray[0])
//GameplayKit randomization services to spawn different astroids
let randomAstroidPosition = GKRandomDistribution(lowestValue: -350, highestValue: 350)
//Randomly spawn astroid in different positions
let position = CGFloat(randomAstroidPosition.nextInt())
astroid.position = CGPoint(x: position, y: self.frame.size.height + astroid.size.height)
astroid.zPosition = 1
//Astroid Physics for collision
astroid.physicsBody = SKPhysicsBody(circleOfRadius: astroid.size.width / 2)
astroid.physicsBody?.isDynamic = true
astroid.physicsBody?.categoryBitMask = astroidCategory
astroid.physicsBody?.contactTestBitMask = playerLaserCategory | playerCategory
//avoid any unwanted collisions
//astroid.physicsBody?.collisionBitMask = 0
addChild(astroid)
//Astroid speed
let animationDuration:TimeInterval = 6
//Clean up, remove astroids once reached a certain distance
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: position, y: -700), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
astroid.run(SKAction.sequence(actionArray))
}
func addEnemy() {
enemyArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: enemyArray) as! [String]
//Select enemy from array
let enemy = SKSpriteNode(imageNamed: enemyArray[0])
//GameplayKit randomization services to spawn different enemies
let randomEnemyPosition = GKRandomDistribution(lowestValue: -350, highestValue: 350)
//Randomly spawn enemy in different positions
let position = CGFloat(randomEnemyPosition.nextInt())
enemy.position = CGPoint(x: position, y: self.frame.size.height + enemy.size.height)
enemy.zPosition = 1
//Enemy Physics for collision
enemy.physicsBody = SKPhysicsBody(circleOfRadius: enemy.size.width / 2)
enemy.physicsBody?.isDynamic = true
enemy.physicsBody?.categoryBitMask = enemyCategory
enemy.physicsBody?.contactTestBitMask = playerLaserCategory | playerCategory
//avoid any unwanted collisions
//enemy.physicsBody?.collisionBitMask = 0
if score >= 20 {
addChild(enemy)
}
//Enemy speed
let animationDuration:TimeInterval = 6
//Clean up, remove enemy once reached a certain distance
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: position, y: -700), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
enemy.run(SKAction.sequence(actionArray))
}
func fireLaser() {
//Sound effect
self.run(SKAction.playSoundFileNamed("laser.wav", waitForCompletion: false))
//Create and position laser
let playerLaser = SKSpriteNode(imageNamed: "laser")
playerLaser.position = player.position
playerLaser.position.y += 65
//Laser Physics
playerLaser.physicsBody = SKPhysicsBody(circleOfRadius: playerLaser.size.width / 2)
playerLaser.physicsBody?.isDynamic = true
playerLaser.physicsBody?.categoryBitMask = playerLaserCategory
playerLaser.physicsBody?.contactTestBitMask = astroidCategory | enemyCategory
//avoid any unwanted collisions
//playerLaser.physicsBody?.collisionBitMask = 0
playerLaser.physicsBody?.usesPreciseCollisionDetection = true
addChild(playerLaser)
//Animation for laser firing
let animationDuration:TimeInterval = 0.3
//Clean up, removes laser blast from game
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: player.position.x, y: self.frame.size.height), duration: animationDuration))
actionArray.append(SKAction.removeFromParent())
playerLaser.run(SKAction.sequence(actionArray))
}
//Function for physics to know what object hit what
func didBegin(_ contact: SKPhysicsContact) {
var A:SKPhysicsBody
var B:SKPhysicsBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
A = contact.bodyA
B = contact.bodyB
} else {
A = contact.bodyB
B = contact.bodyA
}
//PlayerLaser is A and Astroid is B
if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerLaserHitAstroid(laserNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//PlayerLaser is A and Enemy is B
else if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerLaserHitEnemy(laserNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
//Player is A and Astroid is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
playerHitAstroid(playerNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
}
//Player is A and Enemy is B
else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
playerHitEnemy(playerNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
}
}
//Function for playerLaser to destroy Astroid
func playerLaserHitAstroid (laserNode:SKSpriteNode, astroidNode:SKSpriteNode) {
//Create explosion effect
let explosion = SKEmitterNode(fileNamed: "Explosion")!
explosion.position = astroidNode.position
addChild(explosion)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
laserNode.removeFromParent()
astroidNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosion.removeFromParent()
}
print("laser hit astroid")
//Add score
score += 5
}
//Function for playerLaser to destroy Enemy
func playerLaserHitEnemy (laserNode:SKSpriteNode, enemyNode:SKSpriteNode) {
//Create explosion effect
let explosion = SKEmitterNode(fileNamed: "Explosion")!
explosion.position = enemyNode.position
addChild(explosion)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
laserNode.removeFromParent()
enemyNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosion.removeFromParent()
}
print("laser hit enemy")
//Add score
score += 10
}
//Function for when player and astroid collide
func playerHitAstroid(playerNode:SKSpriteNode, astroidNode:SKSpriteNode) {
let explosionA = SKEmitterNode(fileNamed: "Explosion")!
explosionA.position = astroidNode.position
explosionA.zPosition = 3
addChild(explosionA)
print("Player hit astroid")
// let explosionB = SKEmitterNode(fileNamed: "Explosion")!
// explosionB.position = playerNode.position
// explosionB.zPosition = 3
// addChild(explosionB)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
//playerNode.removeFromParent()
astroidNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosionA.removeFromParent()
//explosionB.removeFromParent()
}
//Removes a life when hit
if livesArray.count > 0 {
let lifeNode = livesArray.first
lifeNode?.removeFromParent()
livesArray.removeFirst()
}
//Remove player when all lives are gone
if livesArray.count == 0 {
playerNode.removeFromParent()
let transition = SKTransition.flipHorizontal(withDuration: 0.5)
let gameOver = GameOverScene(fileNamed: "GameOverScene")!
gameOver.score = self.score
gameOver.scaleMode = scaleMode
self.view?.presentScene(gameOver, transition: transition)
}
}
//Function for when player and enemy collide
func playerHitEnemy(playerNode:SKSpriteNode, enemyNode:SKSpriteNode) {
let explosionA = SKEmitterNode(fileNamed: "Explosion")!
explosionA.position = enemyNode.position
explosionA.zPosition = 3
addChild(explosionA)
print("Player hit enemy")
// let explosionB = SKEmitterNode(fileNamed: "Explosion")!
// explosionB.position = playerNode.position
// explosionB.zPosition = 3
// addChild(explosionB)
//Play explosion sound effect
self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
//remove sprites
//playerNode.removeFromParent()
enemyNode.removeFromParent()
//Remove explosion effect after a delay
self.run(SKAction.wait(forDuration: 2)) {
explosionA.removeFromParent()
//explosionB.removeFromParent()
}
//Removes a life when hit
if livesArray.count > 0 {
let lifeNode = livesArray.first
lifeNode?.removeFromParent()
livesArray.removeFirst()
}
//Remove player when all lives are gone
if livesArray.count == 0 {
playerNode.removeFromParent()
let transition = SKTransition.flipHorizontal(withDuration: 0.5)
let gameOver = GameOverScene(fileNamed: "GameOverScene")!
gameOver.score = self.score
gameOver.scaleMode = scaleMode
self.view?.presentScene(gameOver, transition: transition)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
fireLaser()
}
}