SpriteKit game crashes when two SKPhysicsContacts are detected - swift

I have a SpriteKit game where you must shoot down asteroids before they hit the base. But if you shoot and hit the asteroid and/or the base when the asteroid is hitting the base, the program crashes.
Here's my physics contact code.
public func didBegin(_ contact: SKPhysicsContact) {
//Asteroid is object 1, other item is object 2
var object1 = SKSpriteNode()
var object2 = SKSpriteNode()
//Test for asteroid/projectile contact, then remove appropriate sprites, change game values and play sounds
if contact.bodyA.contactTestBitMask == ColliderType.Asteroid.rawValue && contact.bodyB.contactTestBitMask == ColliderType.Asteroid.rawValue{
if contact.bodyA.categoryBitMask == ColliderType.Asteroid.rawValue{
}else if contact.bodyB.categoryBitMask == ColliderType.Asteroid.rawValue{
object2 = contact.bodyA.node as! SKSpriteNode
object1 = contact.bodyB.node as! SKSpriteNode
let explosionPath = URL(fileURLWithPath: Bundle.main.path(forResource: "astd", ofType: "m4a")!)
do {
audioPlayer = try AVAudioPlayer(contentsOf: explosionPath)
} catch {
print("error")
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}
if object2.physicsBody?.categoryBitMask == ColliderType.Object.rawValue{
object1.removeAllActions()
object1.removeFromParent()
let hitPath = URL(fileURLWithPath: Bundle.main.path(forResource: "craftHit", ofType: "mp3")!)
do {
audioPlayer = try AVAudioPlayer(contentsOf: hitPath)
} catch {
print("error")
}
audioPlayer.prepareToPlay()
audioPlayer.play()
reduceHealthBy(num: 0.075)
}else if object2.physicsBody?.categoryBitMask == ColliderType.Projectile.rawValue{
object1.removeAllActions()
object1.removeFromParent()
object2.removeAllActions()
object2.removeFromParent()
score += 1
}
}
}

Sounds like a duplicate of Optional unwrapping SKPhysics error
SK is generating multiple contact events for one actual contact and so didBegin() is being called multiple times.
On the first call to didBegin you handle the contact and remove one or mode nodes etc, and on the second call to didBegin (with the same contact: argument) the nodes you removed are now nil, so when you try to remove or reference them you get a crash.
There are several ways to handle this such as adding the nodes to be removed to a set and then removing them in didFinishUpdate() or checking if a node is nil and then returning if it is etc.
Trying to get SK to NOT generate the multiple contacts currently seems unachievable.

Related

Prevent memory leak, high memory usage using video(AVPlayer)?

I made an app in which there is a View Controller which which has a view and some buttons in it. On the view I show a 2sec long video, which repeats continuously.
I used instruments to check my memory usage. It showed me that my app's memory usage continuously increases at the View Controller where the video is. After 30 sec my app gets up to 1gb memory usage.
In the code I made a weak variable which breaks the retain cycle, so when I go from the View Controller which shows the video to an another View Controller, then my memory usage drops.
BUT: My purpose is to get dropped the memory usage each time the video starts repeating, or is there something else what I should consider to do?
Thank you in advance!
backView is the view I am using for showing the video.
// Set up the video player.
var startVideo = true
private func playVideo(exercise : String, type : String) {
guard let path = Bundle.main.path(forResource: exercise, ofType: type) else {
debugPrint("video.mp4 not found")
return
}
weak var player = AVPlayer(url: URL(fileURLWithPath: path))
let playerController = AVPlayerViewController()
playerController.player = player
let playerLayerAV = AVPlayerLayer(player: player)
//now we set the size of frame to be like the view ("backview")
playerLayerAV.frame = backView.bounds
// the backview is the view i'm using it to insert a video
backView.layer.addSublayer(playerLayerAV)
player!.play()
if startVideo == true {
player!.play()
}else if startVideo == false {
player?.pause()
}
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player!.currentItem, queue: .main) { _ in
player!.seek(to: kCMTimeZero)
player!.play()
}
}
Try the following to continually loop a video
let playerItem = AVPlayerItem(url: selectedItem)
let player = AVQueuePlayer(playerItem: playerItem)
let playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
let playerLayer = AVPlayerLayer(player: player)
player.play()

How to loop background music without lag/delay when song repeats (seamless)

I am trying to make a game with background music looping in it. I made the song file in Adobe Audition (which is similar to audacity) and when I play it in a loop in Adobe Audition it loops how I want it.
When I play it in Xcode however, it has a lag in between the loops. I am using AVFoundations for the sound playing.
I have searched everywhere but I can't find the solution for the problem.
Is there any way you can loop audio files without there being any lags in between ? (I believe its called "seamless looping" )
here is the code:
class GameScene: SKScene {
...// Other Code
var ButtonAudio = URL(fileURLWithPath: Bundle.main.path(forResource: "Gamescene(new)", ofType: "mp3")!)
var ButtonAudioPlayer = AVAudioPlayer()
... //Other Code
}
And When I Call it:
override func didMove(to view: SKView) {
...//Code
ButtonAudioPlayer = try! AVAudioPlayer(contentsOf: ButtonAudio, fileTypeHint: nil)
ButtonAudioPlayer.numberOfLoops = -1
ButtonAudioPlayer.prepareToPlay()
ButtonAudioPlayer.play()
...//More Code
}
Can someone help me with this issue ?
Thank you in advance!
You can use AVPlayerLooper and AVQueuePlayer to do this.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var queuePlayer = AVQueuePlayer()
var playerLooper: AVPlayerLooper?
override func viewDidLoad() {
super.viewDidLoad()
guard let url = Bundle.main.url(forResource: "Gamescene(new)", withExtension: "mp3") else { return }
let playerItem = AVPlayerItem(asset: AVAsset(url: url))
playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: playerItem)
queuePlayer.play()
}
}
The solution proposed by #dave234 only works in iOS > 10. Since I needed to make seamless playback in iOS > 9, I did something different:
Instead of AVPlayer, I created AVQueuePlayer and immediately added two identical melodies to the queue.
Next, I made a listener to the penultimate melody.
When the listener was triggered, I added another similar record to the queue after the last one.
In fact, in order to avoid delay, I always play the penultimate record.
My code:
var player: AVQueuePlayer?
override func viewDidLoad() {
super.viewDidLoad()
if let path = Bundle.main.path(forResource: "music_file", ofType: "mp3") {
player = createPlayer(url: URL(fileURLWithPath: path))
}
}
func createPlayer(url: URL) -> AVQueuePlayer {
let player = AVQueuePlayer(items: [AVPlayerItem(url: url), AVPlayerItem(url: url)])
loopPlayer(playerItem: player.items()[player.items().count - 2])
return player
}
func loopPlayer(playerItem: AVPlayerItem) {
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { _ in
if let player = self.player, let url = (playerItem.asset as? AVURLAsset)?.url {
player.insert(AVPlayerItem(url: url), after: player.items()[player.items().count - 1])
self.loopPlayer(playerItem: player.items()[player.items().count - 2])
}
}
}

Sound causing game to lag in swift sprite kit game?

New code
class SceneTwo: SKScene, SKPhysicsContactDelegate {
let flap = SKAction.playSoundFileNamed("flap.caf", waitForCompletion: false)
let whack = SKAction.playSoundFileNamed("whack.caf", waitForCompletion: false)
let tap = SKAction.playSoundFileNamed("tap.caf", waitForCompletion: false)
Then I simply have put
run(tap)
run(flap) etc
where necessary..
Hi just wondering if I am using the correct coding to play sounds in my game. For some context my game is similar to Flappy bird. One sound is played each time the screen is touched (when the bird has impulse upwards) the second sound is when the bird collects a coin in between each wall.
I have noticed that both of these sounds are causing my game to lag.
Below is my relative sound code for the game.
import AVFoundation
var flap: AVAudioPlayer?
var tap: AVAudioPlayer?
override func didMove(to view: SKView) {
tap?.prepareToPlay()
flap?.prepareToPlay()
func playFlap() {
let url = Bundle.main.url(forResource: "flap", withExtension: "caf")!
do {
flap = try AVAudioPlayer(contentsOf: url)
guard let flap = flap else { return }
flap.play()
} catch let error {
print(error.localizedDescription)
}
}
func playTap() {
let url = Bundle.main.url(forResource: "tap", withExtension: "caf")!
do {
tap = try AVAudioPlayer(contentsOf: url)
guard let tap = tap else { return }
tap.play()
} catch let error {
print(error.localizedDescription)
}
}
After this I have simply
playTap()
playFlap()
to where they are needed.
The sound is clear it just seems to make my spawning walls jump a little bit when the sound is made.
Is there something I am doing that is wrong?
You are getting lag because you are not preloading the sound files. You can preload them at App Launch, and then when you need just play them. For reference look into this stackoverflow's post
And if you still face the same issue then you can add sound in background queue, as demostrated here
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
audioPlayer.play()
})

How to make audioplayer in swift?

I made audio player. I realize playlist and playing audio background functionality. I need to recognize which audio played in background.
Firts I create audio player object like:
var mp3Player:AVAudioPlayer?=AVAudioPlayer()
var firstLoad=true
var playingType_Index=0
var speedType_Index=0
When I click audio from list I put clicked audio to my selectedAudio
let audios=[
[
"image":UIImage.fontAwesomeIconWithName(.Headphones, textColor: UIColor(colorLiteralRed: 223/255, green: 156/255, blue: 104/255, alpha: 1.0), size: CGSizeMake(35, 35)),
"title": "Audio1",
"desc":"Erkemin",
"time":3,
"src":NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("audio1", ofType: "mp3")!)
]
]
var selectedAudio=[
"id":"",
"status":"",
"image":"",
"title": "",
"desc":"",
"time":0,
"src":""
]
func playAudio(sender:AnyObject){
firstLoad=false
backwardBtn.enabled=true
step_backward.enabled=true
play_pauseBtn.enabled=true
stopBtn.enabled=true
step_forward.enabled=true
forwardBtn.enabled=true
audioSlider.enabled=true
selectedAudio=audios[sender.tag]
selectedAudio["id"]=sender.tag
selectedAudio["status"]="true"
music()
tableView.reloadData()
}
I'm using selectedAUdio to recognize which audio now played. But when go to other view my audio continue playing but my selected audio being nil.
How can I do this?
I think you could have some problem like re-creation of this view: if you launch this view and playAudio(), selectAudio property is filled with your parameters but for something reason seems you re-make this view so your selectedAudio return to default values (empty strings).
If you need to stop your player with his method stop, you could do something like the code below:
func stopMp3Player() {
if let player = mp3Player {
if player.playing {
player.stop()
}
}
}
Warning: when you build this methods (you could also make pause) you should always check if your mp3Player is not nil to avoid crash.
About your play method I would do something like:
func playMp3Player(filename: String) {
let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
if (url == nil) {
print("Could not find file: \(filename)")
return
}
do { mp3Player = try AVAudioPlayer(contentsOfURL: url!, fileTypeHint: nil) }
catch let error as NSError { print(error.description) }
if let player = mp3Player {
player.volume = // set your volume here
player.numberOfLoops = 0 // set the repeating
player.prepareToPlay()
player.play()
}
}

How to play the same sound overlapping with AVAudioPlayer?

This code plays the sound when the button is tapped but cancels the previous if it is pressed again. I do not want this to happen I want the same sound to overlap when repeatedly pressed. I believe it might be due to using the same AVAudioPlayer as I have looked on the internet but I am new to swift and want to know how to create a new AVAudioPlayer everytime the method runs so the sounds overlap.
func playSound(sound:String){
// Set the sound file name & extension
let soundPath = NSURL(fileURLWithPath:NSBundle.mainBundle().pathForResource(sound, ofType: "mp3")!)
do {
//Preperation
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch _{
}
do {
try AVAudioSession.sharedInstance().setActive(true)
}
catch _ {
}
//Play the sound
var error:NSError?
do{
audioPlayer = try AVAudioPlayer(contentsOfURL: soundPath)
}catch let error1 as NSError {
error = error1
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}
To play two sounds simultaneously with AVAudioPlayer you just have to use a different player for each sound.
In my example I've declared two players, playerBoom and playerCrash, in the Viewcontroller, and I'm populating them with a sound to play via a function, then trigger the play at once:
import AVFoundation
class ViewController: UIViewController {
var playerBoom:AVAudioPlayer?
var playerCrash:AVAudioPlayer?
override func viewDidLoad() {
super.viewDidLoad()
playerBoom = preparePlayerForSound(named: "sound1")
playerCrash = preparePlayerForSound(named: "sound2")
playerBoom?.prepareToPlay()
playerCrash?.prepareToPlay()
playerBoom?.play()
playerCrash?.play()
}
func preparePlayerForSound(named sound: String) -> AVAudioPlayer? {
do {
if let soundPath = NSBundle.mainBundle().pathForResource(sound, ofType: "mp3") {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
return try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: soundPath))
} else {
print("The file '\(sound).mp3' is not available")
}
} catch let error as NSError {
print(error)
}
return nil
}
}
It works very well but IMO is not suitable if you have many sounds to play. It's a perfectly valid solution for just a few ones, though.
This example is with two different sounds but of course the idea is exactly the same for two identic sounds.
I could not find a solution using just AVAudioPlayer.
Instead, I have found a solution to this problem with a library that is built on top of AVAudioPlayer.
The library allows same sounds to be played overlapped with each other.
https://github.com/adamcichy/SwiftySound