1
votes

I have a physics-based 2D game made with SpriteKit. There are an increasing number of little balls colliding with a number of blocks. Each time a ball hits a block I want to play a sound.

Currently I play sounds using playSoundFileNamed(_:waitForCompletion:) with its waitForCompletion parameter always set to false (so that it can play multiple sounds in quick sequence).

This works very well in terms of performance and frame-rate, but I'm getting an increasing number of crash reports regarding a failing deallocation.

This only happens when the GameScene is deallocated, never during the game itself.

I tried to substitute the SKAction with a SKAudioNode but that is not able to play multiple sounds in rapid succession, so the game does not feel right.

I tried to incorporate GameAudioPlayer but the frame rate drops significantly when playing a high number of sounds.

This is currently how I play sounds:

class GameScene: SKScene {
    private var popSound = SKAction.playSoundFileNamed("Pop.wav", waitForCompletion: false)
    private var starSound = SKAction.playSoundFileNamed("Star.wav", waitForCompletion: false)
    private var whooshSound = SKAction.playSoundFileNamed("Whoosh.wav", waitForCompletion: false)

    // ...

    func didEnd(_ contact: SKPhysicsContact) {
        // ball-to-block collision
        self.run(popSound)
    }
}

And this is the stack trace coming from Crashlytics

Fatal Exception: NSRangeException
0  CoreFoundation                 0x1ec81527c __exceptionPreprocess
1  libobjc.A.dylib                0x1eb9ef9f8 objc_exception_throw
2  CoreFoundation                 0x1ec78ece8 _CFArgv
3  CoreFoundation                 0x1ec700298 -[__NSArrayM removeObjectsInRange:]
4  SpriteKit                      0x20354ee3c -[SKSoundSource purgeCompletedBuffers]
5  SpriteKit                      0x20354f0d4 -[SKSoundSource dealloc]
6  SpriteKit                      0x203523b14 -[SKPlaySound .cxx_destruct]
7  libobjc.A.dylib                0x1eb9ee7cc object_cxxDestructFromClass(objc_object*, objc_class*)
8  libobjc.A.dylib                0x1eb9fe6b8 objc_destructInstance
9  libobjc.A.dylib                0x1eb9fe720 object_dispose
10 SpriteKit                      0x203485c08 -[SKAction dealloc]
11 SpriteShot                     0x1005f8418 @objc GameScene.__ivar_destroyer (GameScene.swift)
12 libobjc.A.dylib                0x1eb9ee7cc object_cxxDestructFromClass(objc_object*, objc_class*)
13 libobjc.A.dylib                0x1eb9fe6b8 objc_destructInstance
14 libobjc.A.dylib                0x1eb9fe720 object_dispose
15 UIKitCore                      0x219145b28 -[UIResponder dealloc]
16 SpriteKit                      0x203504be4 -[SKNode dealloc]
17 SpriteKit                      0x2034bade0 -[SKScene dealloc]
18 SpriteShot                     0x1005f83d4 @objc GameScene.__deallocating_deinit (<compiler-generated>)
19 SpriteKit                      0x2034e2004 -[SKView _update:]
20 SpriteKit                      0x2034dd7f4 __51-[SKView _vsyncRenderForTime:preRender:postRender:]_block_invoke.351
21 SpriteKit                      0x2034dcbf8 -[SKView _vsyncRenderForTime:preRender:postRender:]
22 SpriteKit                      0x2034dfc54 __29-[SKView setUpRenderCallback]_block_invoke
23 SpriteKit                      0x203522c18 -[SKDisplayLink _callbackForNextFrame:]
24 QuartzCore                     0x1f0b9cf90 CA::Display::DisplayLink::dispatch_items(unsigned long long, unsigned long long, unsigned long long)
25 QuartzCore                     0x1f0c66b10 display_timer_callback(__CFMachPort*, void*, long, void*)
26 CoreFoundation                 0x1ec780a8c __CFMachPortPerform
27 CoreFoundation                 0x1ec7a7690 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
28 CoreFoundation                 0x1ec7a6ddc __CFRunLoopDoSource1
29 CoreFoundation                 0x1ec7a1c00 __CFRunLoopRun
30 CoreFoundation                 0x1ec7a10b0 CFRunLoopRunSpecific
31 GraphicsServices               0x1ee9a179c GSEventRunModal
32 UIKitCore                      0x21911b978 UIApplicationMain
33 SpriteShot                     0x1005f1910 main + 16 (Ball.swift:16)
34 libdyld.dylib                  0x1ec2668e0 start
1

1 Answers

1
votes

I'd highly recommend you read my answer here: https://stackoverflow.com/a/56615200/5277976.

My final conclusion was to use AVAudioPlayer for everything: music and sound. Create AVAudioPlayer instances, allowing a maximum number of each sound as best fits your case. An example is given here: https://www.oreilly.com/library/view/ios-swift-game/9781491920794/ch04.html.

Note that what O'Reilly does is not precisely what I am saying or ended up doing: in their case they allow you to generate more AVAudioPlayers but I found this had a big impact on performance given how many overlapping sounds we would have at once--at some point you should just call it if you have many overlapping sounds of the same type, but I leave that to you to decide.

IN OUR CASE, if we have a sound that plays frequently and overlaps, we keep several players (but not infinity, like in the O'Reilly example) available to play that sound. Note that the more sounds you play overlapping the worse your performance will be. There is no way around this as far as I know. Keep as many sounds to 1 player as possible. We have a separate enum for this.

PlaySoundFileNamed is lighter-weight and seems to be better for performance; unfortunately it is in effect BROKEN--it was crashing our app during IAP purchases and that is just not OK. Given how long it has been broken I doubt it will ever be fixed. SKAudioNode has the limitation you described and my efforts to fit the square peg into the round hole with it failed for a variety of reasons.

As I explain there, having many AVAudioPlayer instances doesn't feel like the ideal solution, but it was the best of the worst when it came down to it.

I would also note that problems like this put us off of SpriteKit entirely and we now use Unity.