AVAudioPlayer causing SpriteNodes to jerk when animating - swift

I am working on app atm where the user sees nodes animating across the screen. When the node passes a certain target area, the user must press a button and then a avplayer sound will play.
The problem is, anytime the user presses the button and the sound plays, the nodes all jerk and I have no idea why. To replicate the problem I am having, here is an example project I put together in few mins.
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let skView = self.view as! SKView
let scene = GameScene(size: CGSizeMake(self.view.frame.size.width, self.view.frame.size.height))
skView.presentScene(scene)
}
}
import SpriteKit
import AVFoundation
class GameScene: SKScene {
var buttonNode : SKSpriteNode?
override init(size: CGSize)
{
super.init(size: size)
self.backgroundColor = SKColor.whiteColor()
self.view?.backgroundColor = UIColor.whiteColor()
}
override func didMoveToView(view: SKView)
{
buttonNode = SKSpriteNode(imageNamed: "button")
buttonNode?.name = "button"
buttonNode?.position = CGPointMake(200, 200)
buttonNode?.size = CGSizeMake(150, 150)
self.addChild(buttonNode!)
NSTimer.scheduledTimerWithTimeInterval(1.5, target: self, selector: #selector(self.animateNodes), userInfo: nil, repeats: true)
}
var avPlayer : AVAudioPlayer?
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
{
for touch in touches
{
let viewTouchLocation = touch.locationInView(self.view)
let sceneTouchPoint = self.convertPointFromView(viewTouchLocation)
let touchedNode = self.nodeAtPoint(sceneTouchPoint)
if(touchedNode.name == "button")
{
let avURL = NSURL.fileURLWithPath(NSBundle.mainBundle().pathForResource("exampleSound", ofType: "wav")!)
avPlayer = try? AVAudioPlayer(contentsOfURL: avURL)
avPlayer?.volume = 0.2
avPlayer?.play()
}
}
}
func animateNodes()
{
let sprite = SKSpriteNode(imageNamed: "shortMetalWhite")
sprite.color = UIColor.blackColor()
sprite.colorBlendFactor = 1.0
sprite.position = CGPointMake(self.view!.frame.size.width, 400)
self.addChild(sprite)
let noteDuration = 4.0
let actionMove = SKAction.moveTo(CGPointMake(-5, 400), duration: noteDuration)
let actionMoveDone = SKAction.removeFromParent()
sprite.runAction(SKAction.sequence([actionMove, actionMoveDone]))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Does anyone have any idea what causes this? Is there something wrong with sprite nodes and avplayer that causes this? I have replicated this same code in a non game app, where you have a series of UIViews animating across the screen and a sound plays everytime you press a button and there is no issue with any lag or jerkiness. So what is it about sprite nodes and AVPlayer that causes this issue?
(Btw the issue is not as noticeable on the simulator, but when you run it on any iPhone or iPad, it is very noticeable)
Any help would be much appreciated
Edit:
If I swap to using AudioServices instead like this
if let soundURL = NSBundle.mainBundle().URLForResource("OHH10 Drums1DOTcom", withExtension: "wav") {
var mySound: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundURL, &mySound)
// Play
AudioServicesPlaySystemSound(mySound);
}
there is no lag, but then I have no control over the volume. So not an ideal solution

After doing more research, it seems its an issue with AVAudioPlayer, it is not really suited for playing repeating sounds effects in short succession.
So instead I swapped to using
SKAction.playSoundFileNamed("example.wav", waitForCompletion: false)
Again issue with this is that it does not allow you to control the volume. This means I had to open up audacity and modify the volume of each sound file to get it just right.
I would have preferred a better solution, but sadly this is the best I can find if I want to avoid any game lag

Related

Trying to get a bunch of images to show in succession when touching screen on Swift

I put the different images to show in succession in the assets.xcassets folder as shooter, shooter1, shooter2 and so on but whenever i touch the screen the animation/image shown in the view doesn't change?
Here is my code:
import UIKit
import SpriteKit
class ShooterScene: SKScene
{
var score = 0
var enemyCount = 10
var shooterAnimation = [SKTexture]()
override func didMove(to view: SKView)
{
self.initShooterScene()
}
func initShooterScene()
{
let shooterAtlas = SKTextureAtlas(named: "shooter") // referencing shooter.atlas
for index in 1...shooterAtlas.textureNames.count
{
let imgName = "shooter\(index)"
shooterAnimation += [shooterAtlas.textureNamed(imgName)]
}
}
//Animate the shooter
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let shooterNode = self.childNode(withName: "shooterNode") as? SKSpriteNode
{
let animation = SKAction.animate(with: shooterAnimation, timePerFrame: 0.1)
shooterNode.run(animation)
}
}
}
The sprite node that I use in the view has the name shooterNode but the image doesn't seem to change at all. Any help would be great
edit:
console output:
2020-01-23 17:47:59.470204-0500 Tutorial 31 - Introduction to Sprike
Kit[5666:203542] Metal API Validation Enabled
2020-01-23 17:47:59.715838-0500 Tutorial 31 - Introduction to Sprike
Kit[5666:203542] SKView: ignoreRenderSyncInLayoutSubviews is NO. Call
_renderSynchronouslyForTime without handler
2020-01-23 17:48:11.764763-0500 Tutorial 31 - Introduction to Sprike
Kit[5666:203964] XPC connection interrupted
2020-01-23 17:48:11.765810-0500 Tutorial 31 - Introduction to Sprike
Kit[5666:203968] [connection] Connection interrupted: will attempt to
reconnect
2020-01-23 17:48:11.765887-0500 Tutorial 31 - Introduction to Sprike
Kit[5666:205206] [ServicesDaemonManager] interruptionHandler is
called. -[FontServicesDaemonManager connection]_block_invoke
Message from debugger: Terminated due to signal 15
Here's your code modified. It works for me. Without more code, I don't know how you are setting up your shooter node so I can't replicate your issue.
class ShooterScene: SKScene {
var textures = [SKTexture]()
//Also serves as the name of the texture the node uses.
let NODE_NAME:String = "shooter"
override init(size: CGSize) {
super.init(size: size)
//Better the setup scene in intializer, because didMove(to view) is called multiple times.
//You only want to call the setup method once.
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
//Node setup
let shooterNode = SKSpriteNode(imageNamed: NODE_NAME)
shooterNode.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
shooterNode.name = NODE_NAME
addChild(shooterNode)
//Create animation collection
let atlas = SKTextureAtlas(named: NODE_NAME)
//Arrays start at index zero so we need to subtract 1 from the total textures count
let upperBound = atlas.textureNames.count - 1
for index in 1...upperBound {
let textureName = "\(NODE_NAME)\(index)"
textures += [atlas.textureNamed(textureName)]
}
}
//Animate the shooter when screen pressed.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let shooterNode = self.childNode(withName: NODE_NAME) as? SKSpriteNode else { return }
//For animations that are around 60 Frames per second
let FPS:Double = 1/60
let animation = SKAction.animate(with: self.textures, timePerFrame: FPS)
shooterNode.run(animation)
}
}

SKScene returns nil when transition method is called

In my little Xcode Project I am trying to transition scenes by when my SKLabelNode is touched it presents the next scene. For some reason in my scene transition method it makes me store it as an optional. And the optional returns false. My file names are correct and no grammatical errors in the references would be causing the problem here is my code. When I click my SKLabelNode my ios platform that is running it on recognizes the touch then the app crashes, in console saying that an optional value returned nil. How could I resolve this problem? Thanks
import SpriteKit
class GameScene: SKScene {
let next = SKScene(fileNamed: "nextScene")
override func didMoveToView(view: SKView) {
let backgroundimage = SKSpriteNode(imageNamed: "ipbg")
backgroundimage.size = CGSize(width: self.frame.size.width, height: self.frame.size.height)
backgroundimage.position = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2)
addChild(backgroundimage)
let playButton = SKLabelNode(fontNamed: "")
playButton.name = "play"
playButton.position = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2 + 100)
playButton.text = "Play"
let wait = SKAction.waitForDuration(2)
let run = SKAction.runBlock({
let randomNumber = Int(arc4random_uniform(UInt32(4)))
switch(randomNumber){
case (0):
playButton.fontColor = UIColor.blueColor()
case 1:
playButton.fontColor = UIColor.yellowColor()
case 2:
playButton.fontColor = UIColor.purpleColor()
case 3:
playButton.fontColor = UIColor.orangeColor()
default: print("default")
}
})
addChild(playButton)
var repeatActionForever = SKAction.repeatActionForever(SKAction.sequence([wait, run]))
runAction(repeatActionForever)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let trans = SKTransition.crossFadeWithDuration(1)
let touch = touches.first! as UITouch
let touchLocation = touch.locationInNode(self)
let touchedNode = nodeAtPoint(touchLocation)
if touchedNode.name == "play"{
scene!.view?.presentScene(next!, transition: trans)
}
}
}
func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
The code you posted does not compile so it's hard to find the error.
However I would try replacing the touchesBegan method with the following
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let trans = SKTransition.crossFadeWithDuration(1)
let touch = touches.first! as UITouch
let touchLocation = touch.locationInNode(self)
let touchedNode = nodeAtPoint(touchLocation)
if touchedNode.name == "play"{
guard let nextScene = SKScene(fileNamed: "nextScene")
else { fatalError("Could not load nextScene") }
self.view?.presentScene(nextScene, transition: trans)
}
}
There is no need to store the scene as a property just add it in the touches began method.
Also you should create the corresponding swift file and class for each scene instead of trying to initialising a .sks file with a generic SKScene object. The line ...(fileNamed: "nextScene") looks for an .sks file (Xcode visual level editor file), if you haven't created one your property will be nil.
To make it work do this:
First make sure you created a .sks file called NextScene (right click a file in your project -> New -> Resource -> SpriteKit Scene)
Secondly create a new swift file and create a new class called NextScene
class NextScene: SKScene {
}
Than the loading code in your touches label function would look like this.
override func touchesBegan... {
...
if let nextScene = NextScene(fileNamed: "NextScene") //actually init the corresponding class with the corresponding .sks file
/// scene presenting code
}
}
As you mentioned your scene property returns an optional because the fileNamed (.sks file) you specified might not exists, so check it is there. In general its better practice to safely unwrap like I do above or the default loading code in GameViewController does, instead of just force unwrapping with a ! .
If you don't use the visual level editor than just create your scene class and ignore creating the .sks file and load your scene like so
let nextScene = NextScene(size: self.size) // use current scene size
/// scene presenting code
Note: Your file names should start with capital letters

How can I put a mute button for background music in my Sprite Kit Game with Swift 2(Xcode 7)?

I am making a game with Sprite kit, I recently added background music to the game and it works, but i want to put a mute button that can allow the player to stop and play the background music while the game is running in case he don't like it. Thanks.
import AVFoundation
class GameScene: SKScene, SKPhysicsContactDelegate {
var backgroundMusic = SKAudioNode()
func restartScene(){
self.removeAllChildren()
self.removeAllActions()
died = false
gameStarted = false
score = 0
createScene()
}
func createScene(){
backgroundMusic = SKAudioNode(fileNamed: "Musicnamefile.mp3")
addChild(backgroundMusic)
}
override func didMoveToView(view: SKView) {
/* Setup your scene here */
createScene()
}
To create a button add this in didMoveToView:
// pause button
let pauseButton = SKLabelNode()
let pauseContainer = SKSpriteNode()
pauseContainer.position = CGPointMake(hud.size.width/1.5, 1)
pauseContainer.size = CGSizeMake(hud.size.height*3, hud.size.height*2)
pauseContainer.name = "PauseButtonContainer" // pause button
let pauseButton = SKLabelNode()
let pauseContainer = SKSpriteNode()
pauseContainer.position = CGPointMake(hud.size.width/1.5, 1)
pauseContainer.size = CGSizeMake(hud.size.height*3, hud.size.height*2)
pauseContainer.name = "PauseButtonContainer"
hud.addChild(pauseContainer)
pauseButton.position = CGPointMake(hud.size.width/2, 1)
pauseButton.text="I I"
pauseButton.fontSize=hud.size.height
//pauseButton.fontColor = UIColor.blackColor()
pauseButton.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center
pauseButton.name="PauseButton"
hud.addChild(pauseButton)
I'm using the SKLabel to show the pause symbol and a container to increase the touch area. HUD is an rectangle of type SKNode at the top of my game. You have to add the nodes to an element in your game and change the size and position.
To react on the touch:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if (node.name == "PauseButton" || node.name == "PauseButtonContainer") {
Insert your code here ....
}
}
}

Change SKScene using presentScene()

In my SpriteKit Game i'm using:
self.scene!.removeFromParent()
let skView = self.view! as SKView
skView.ignoresSiblingOrder = true
var scene: PlayScene!
scene = PlayScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
skView.presentScene(scene, transition: SKTransition.fadeWithColor(SKColor(red: 25.0/255.0, green: 55.0/255.0, blue: 12.0/255.0, alpha: 1), duration: 1.0))
to move from one scene to another. But how can I go back to the original scene? Using the same principle of code always led to a major crash..
I made an example where global structure is used to track the info about previousScene. It can be done with a custom property as well, or by using userData which every node has. The logic is the same. Also, I've removed debugging code (debug label code etc.) because it is not important for everything to work.
Example might be better if I added a few buttons where each links to the certain scene, but I left just one button to keep everything short as possible.
What you need to know about this example (you will change this rules according to your game, but the logic is the same - set the previousScene before an actual transition):
there are three scenes, WelcomeScene (default one), MenuScene and a GameScene.
tapping on the black button takes you to the GameScene. There is an exception to this rule when current scene is a GameScene. In that case, transition will take you to the previousScene.
tapping anywhere around the black button will take you to the previous scene. There is an exception to this rule when WelcomeScene is loaded for the first time (previousScene is not set) and a transition will take you to the MenuScene in that case.
-in your GameViewController you should set up a WelcomeScene to be a default one. Otherwise, you should change a code a bit to handle situations what happening when previousScene is not set (like I did in touchesBegan of WelcomeScene).
So those are rules I've made, just in order to make all those transitions a bit more meaningful...
Here is the code (BaseScene.swift):
import SpriteKit
enum SceneType: Int {
case WelcomeScene = 0
case MenuScene //1
case GameScene //2
}
struct GlobalData
{
static var previousScene:SceneType?
//Other global data...
}
class BaseScene:SKScene {
let button = SKSpriteNode(color: SKColor.blackColor(), size: CGSize(width: 50, height: 50))
override func didMoveToView(view: SKView) {
setupButton()
}
private func setupButton(){
if (button.parent == nil){
//Just setup button properties like position, zPosition and name
button.name = "goToGameScene"
button.zPosition = 1
button.position = CGPoint(x: CGRectGetMidX(frame), y: 100)
addChild(button)
}
}
func goToScene(newScene: SceneType){
var sceneToLoad:SKScene?
switch newScene {
case SceneType.GameScene:
sceneToLoad = GameScene(fileNamed: "GameScene")
case SceneType.MenuScene:
sceneToLoad = MenuScene(fileNamed: "MenuScene")
case SceneType.WelcomeScene:
sceneToLoad = WelcomeScene(fileNamed:"WelcomeScene")
}
if let scene = sceneToLoad {
scene.size = size
scene.scaleMode = scaleMode
let transition = SKTransition.fadeWithDuration(3)
self.view?.presentScene(scene, transition: transition)
}
}
}
Every scene (WelcomeScene, MenuScene, GameScene) inherits from a BaseScene class (which is subclass of a SKScene). I guess, there is no need to explain that, but feel free to ask if something confuses you. The important method here (which is used by every subclass) is goToScene(scene:SceneType) and its parameter (of type SceneType) which tells us what type of scene a method should load.
SceneType is just an enum which holds integers...So actually we are not working with objects here, thus there is no fear of strong reference cycles.
Next, there are other scenes (WelcomeScene.swift):
import SpriteKit
class WelcomeScene:BaseScene {
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
self.backgroundColor = SKColor.darkGrayColor()
}
deinit {print ("WelcomeScene deinited")}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
if let location = touch?.locationInNode(self){
//Give a priority to a button - if button is tapped go to GameScene
let node = nodeAtPoint(location)
if node.name == "goToGameScene"{
GlobalData.previousScene = SceneType.MenuScene
goToScene(SceneType.GameScene)
}else{
//Otherwise, do a transition to the previous scene
//Get the previous scene
if let previousScene = GlobalData.previousScene {
GlobalData.previousScene = SceneType.WelcomeScene
goToScene(previousScene)
}else{
// There is no previousScene set yet? Go to MenuScene then...
GlobalData.previousScene = SceneType.WelcomeScene
goToScene(SceneType.MenuScene)
}
}
}
}
}
To keep short as possible, everything is commented. Next code (MenuScene.swift):
import SpriteKit
class MenuScene: BaseScene {
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
backgroundColor = SKColor.purpleColor()
}
deinit {
print ("MenuScene deinited") //If this method isn't called, you might have problems with strong reference cycles.
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
if let location = touch?.locationInNode(self){
//Give a priority to a button - if button is tapped go to GameScene
let node = nodeAtPoint(location)
if node.name == "goToGameScene"{
GlobalData.previousScene = SceneType.MenuScene
goToScene(SceneType.GameScene)
}else{
//Otherwise, do a transition to the previous scene
//Get the previous scene
if let previousScene = GlobalData.previousScene {
GlobalData.previousScene = SceneType.MenuScene
goToScene(previousScene)
}
}
}
}
}
And for the end (GameScene.swift):
import SpriteKit
class GameScene: BaseScene{
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
self.backgroundColor = SKColor.orangeColor()
}
deinit {print ("GameScene deinited")}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Here, we ignore black button because we don't want to transition to the same scene
if let previousScene = GlobalData.previousScene {
GlobalData.previousScene = SceneType.GameScene
goToScene(previousScene)
}
}
}
Preview:
Just read again the rules from the beginning and you will be fine (eg. in GameScene black button doesn't work, or on first launch previousScene is not set , so you will be transitioned to the MenuScene by default).
That would be it. Hope this helps a bit. You can copy and paste the code to test it and improve it to your needs. Still, not sure that you really need this. It looks that you just need to correctly transition between scenes.
HINT: What is important here is that every scene BaseScene, WelcomeScene... has it own .sks file. You create those from File->New->File->Resource and name it appropriately (like BaseClass.sks, WelcomeScene.sks...) Also, it is your job to maintain the state of GlobalData.previousScene variable (eg. set it before the transition is made).
You would need to create a property in your new scene that stores the previous one, something like previousScene. Then you can set it like this: scene.previousScene = self.scene. In you new scene, you can now go back to the previous scene with skView.presentScene(previousScene)
And I'd advise against naming the new scene you are going to present scene because your current scene is also named scene, so if you accidentally forget the self in self.scene then that may cause a lot of confusion. I'd name it something like newScene or sceneToPresent.
Also, your first line, self.scene!.removeFromParent(), isn't necessary. You don't need to remove the current scene before presenting a new one.

How to transition scenes in Swift

In Objective-C, using Sprite-Kit, I would successfully use something like the following code in Objective-C to bring up a new scene
if ([touchedNode.name isEqual: #"Game Button"]) {
SKTransition *reveal = [SKTransition revealWithDirection:SKTransitionDirectionDown duration:1.0];
GameScene *newGameScene = [[GameScene alloc] initWithSize: self.size];
// Optionally, insert code to configure the new scene.
newGameScene.scaleMode = SKSceneScaleModeAspectFill;
[self.scene.view presentScene: newGameScene transition: reveal];
}
In trying to port my simple game to Swift, so far I have this working...
for touch: AnyObject in touches {
let touchedNode = nodeAtPoint(touch.locationInNode(self))
println("Node touched: " + touchedNode.name);
let touchedNodeName:String = touchedNode.name
switch touchedNodeName {
case "Game Button":
println("Put code here to launch the game scene")
default:
println("No routine established for this")
}
But I do not know what code to write to actually transition to another scene.
Question(s):
Can someone please provide an example of using SKTransition with Swift?
Would you normally create another "file" to put the other scene code in for the other scene, assuming you would have under Objective-C, or is there something about using Swift that means I should approach it differently?
Thank you
To start with your second question, it's kind of up to you. If you wish to, you can continue to follow the Objective-C convention of having one class per file, although this isn't a requirement, and wasn't in Objective-C either. That being said, if you have a couple of classes that are tightly related, but aren't made up of much code, it wouldn't be unreasonable to have them grouped in a single file. Just do what feels right, and don't make a huge blob of code in a single file.
Then for your first questions... Yes, you had a good deal of it already. Basically, from where you got up to, you need to create the instance of GameScene through its size: initializer. From there, you just set the properties and call present.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
guard let location = touches.first?.locationInNode(self),
let scene = scene,
nodeAtPoint(location) == "Game Button" else {
return
}
let transition = SKTransition.reveal(
with: .down,
duration: 1.0
)
let nextScene = GameScene(size: scene.size)
nextScene.scaleMode = .aspectFill
scene.view?.presentScene(nextScene, transition: transition)
}
if you have to work on touch-begain or node action , Then use it:
override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!) {
let touch = touches.anyObject() as UITouch
if CGRectContainsPoint(btncloseJump.frame, touch.locationInNode(self)) {
self.scene.removeFromParent()
btncloseJump.removeFromParent()
let skView = self.view as SKView
skView.ignoresSiblingOrder = true
var scene: HomeScene!
scene = HomeScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
skView.presentScene(scene, transition: SKTransition.fadeWithColor(SKColor(red: 25.0/255.0, green: 55.0/255.0, blue: 12.0/255.0, alpha: 1), duration: 1.0))
}
}