Swift: Random SKSpriteNode - ios

I have a simple question about SpriteKit.
I created two SKSpriteNodes:
let player1 = SKSpriteNode(imageNamed: "Player1")
// let player2 = SKSpriteNode(imageNamed: "Player2")
If i start the app, the app of course shows Player1.
Is there a way to do it randomly?
So if i open the app, it should show randomly Player 1 or Player 2
I know that there is a way to do this with integers, like this:
let randomNumber = Int(arc4random_uniform(7))
but how would i do it, with SKSpriteNodes?

I guess you want something trivial like this?!
let randomPlayerNumber = Int(arc4random_uniform(2)) + 1
let player = SKSpriteNode(imageNamed: "Player\(randomPlayerNumber)")
If there are two different image names:
let randomPlayerNameIndex = Int(arc4random_uniform(2))
let playerNames = ["playerRed", "playerBlue"]
let player = SKSpriteNode(imageNamed: playerNames[randomPlayerNameIndex])

Related

How to synchronize textures animation and sound in SpriteKit

I'm creating a RPGGameKit using SpriteKit to help me develop my iOS games. Now that my player can move, I added animations and an audio system.
I ran on a problem to synchronize textures and sounds. Like a step when my player walk.
let atlas = SKTextureAtlas(named: "Walk")
let textures = atlas.getTextures() // I created an extension that returns textures of atlas
let walkingAnimation = SKAction.animate(with: textures, timePerFrame: 1)
So, walkingAnimation will loop through textures and change it every 1 second.
Now, I want to play a walking sound when the texture changes.
I have look at SKAction and SpriteKit documentation but there is no callback for this SKAction.
If you want to try to get this done with me or you have ideas of how to do it, please leave a comment.
Thanks :)
Try this:
let frame1 = SKAction.setTexture(yourTexture1)
let frame2 = SKAction.setTexture(yourTexture2)
let frame3 = SKAction.setTexture(yourTexture3)
//etc
let sound = SKAction.playSoundFileNamed("soundName", waitForCompletion: false)
let oneSecond = SKAction.wait(forDuration: 1)
let sequence = SKAction.sequence([frame1,sound,oneSecond,frame2,sound,oneSecond,frame3,sound,oneSecond])
node.run(sequence)
So, for now I'm going to do it like this :
let textures = SKTextureAtlas(named: "LeftStep").getTextures()
var actions = [SKAction]()
for texture in textures {
let group = SKAction.group([
SKAction.setTexture(texture),
SKAction.playSoundFileNamed("Step.mp3", waitForCompletion: false)
])
let sequence = SKAction.sequence([
group,
SKAction.wait(forDuration: 0.5)
])
actions.append(sequence)
}
self.node.run(SKAction.repeatForever(SKAction.sequence(actions)))
Thanks #StefanOvomate
I've found myself in the same situation, currently I'm doing the below. From what I have read in the documentation and seen online, the only way to do it is to create the audio file length to match one rotation of the texture animation.
let walkAtlas = global.playerWalkAtlas
var walkFrames: [SKTexture] = []
let numImages = walkAtlas.textureNames.count
for i in 1...numImages {
let texture = "walk\(i)"
walkFrames.append(walkAtlas.textureNamed(texture))
}
walking = walkFrames
isWalking = true
animateMove()
}
func animateMove(){
let animateWalk = SKAction.animate(with: walking, timePerFrame: 0.05)
let soundWalk = global.playSound(sound: .walkSound)
let sequence = SKAction.sequence([soundWalk, animateWalk])
self.run(SKAction.repeatForever(sequence),withKey: "isMoving")
}
func stopMoving(){
self.removeAction(forKey: "isMoving")
isWalking = false
}

Find a plane position in ARKit

I was trying to find the position of the closest Plane in my ARKit app. I wrote some code to help find it, but for some reason, when I run my app, it keeps crashing when I try to add an AR object to the plane. Is there something wrong with my code?
struct myPlaneCoords {
var x = Float()
var y = Float()
var z = Float()
}
func getPlaneCoordinates(sceneView: ARSCNView) -> myPlaneCoords{//coordinates where an AR node will be added
let cameraTransform = sceneView.session.currentFrame?.camera.transform
let cameraCoordinates = MDLTransform(matrix: cameraTransform!)
let camX = CGFloat(cameraCoordinates.translation.x)
let camY = CGFloat(cameraCoordinates.translation.y)
let cameraPosition = CGPoint(x: camX, y: camY)
let anchors = sceneView.hitTest(cameraPosition, types: ARHitTestResult.ResultType.existingPlane)
let spefAnchor = MDLTransform(matrix: anchors[0].localTransform)//finds closest plane
var cc = myPlaneCoords()
cc.x = spefAnchor.translation.x
cc.y = spefAnchor.translation.y
cc.z = spefAnchor.translation.z
return cc
}
Difficult to judge w/o exception description.
I can assume that hitTest doesn't detect any anchor.
In such case your anchors is empty.
let spefAnchor = MDLTransform(matrix: anchors[0].localTransform)//finds closest plane
And here you should get a crash.

Creating multiple Countdown Timers in Swift

Okay so I'm trying to add a 2 seconds countdown timer in a label that activates whenever the user double taps the screen.
So upon doubletapping, the user would find a new label created with the number "2" then a second later "1" then "0".
I can do this perfectly for just 1 timer. When the user double taps to create a second timer, or more, the app crashes.
I found that this is due to the declaration of the variables being made in the class instead of in the function to allow the timer function to retrieve such declarations as the label.
If I found a way to make the timer function inside another function instead of under class it will work fine.
So here's the code that works only when 1 timer is being deployed.
Under Class:
var BTTimer = NSTimer ()
var BTCounter = 2
let BT = SKLabelNode
Under Function touchesBegan:
let B = SKSpriteNode(imageNamed: "B 110.png")
let Touch : UITouch! = touches.first
let TouchLocation = Touch.locationInNode(self)
let PreviousTouchLocation = Touch.previousLocationInNode(self)
let Player = childNodeWithName("Player") as! SKSpriteNode
let xPos = Player.position.x + (TouchLocation.x - PreviousTouchLocation.x)
let yPos = Player.position.y + (TouchLocation.y - PreviousTouchLocation.y)
B.position = CGPointMake(xPos, yPos)
B.zPosition = -2
self.addChild(B)
BT.fontSize = 27;
BT.fontColor = UIColor.redColor()
BT.position = CGPointMake(xPos - 9, yPos - 30)
BT.zPosition = -1
self.addChild(BT)
BT.text = String(BTCounter);
BTTimer = NSTimer.scheduledTimerWithTimeInterval(1, target:self, selector: #selector(GameScene.updateBCounter), userInfo: nil, repeats: true)
}
}
Now under another function of the same class:
func updateBCounter() {
if(BTCounter > 0) {
BTCounter-=1
BT.text = String(BTCounter)
}
else if(BTCounter == 0) {
//Remove B
//Place C
}
I wish to change this so that the user is capable of producing as many timers as he wishes to deploy.
You can update updateBCounter like this:
func updateBCounter(timer: NSTimer)
Your selector is changed to #selector(GameScene.updateBCounter(_:)
When you schedule the timer, you can set userInfo to any dictionary and the timer sent to updateBCounter will have it in its userInfo property.
I don't know exactly what you are trying to do, but those are the tools to do multiple timers at once calling the same selector, but knowing which one you are dealing with.
Get rid of the properties keeping track of things and make keys in the userInfo dictionary instead.
You could use any object as userInfo (not just a dictionary), so you could also do:
class TimerData {
var counter: Int
var l: SKLabelNode
}

Details on using the AVAudioEngine

Background: I found one of Apple WWDC sessions called "AVAudioEngine in Practice" and am trying to make something similar to the last demo shown at 43:35 (https://youtu.be/FlMaxen2eyw?t=2614). I'm using SpriteKit instead of SceneKit but the principle is the same: I want to generate spheres, throw them around and when they collide the engine plays a sound, unique to each sphere.
Problems:
I want a unique AudioPlayerNode attached to each SpriteKitNode so that I can play a different sound for each sphere. i.e Right now, if I create two spheres and set a different pitch for each of their AudioPlayerNode, only the most recently created AudioPlayerNode seems to be playing, even when the original sphere collides. During the demo, he mentions "I'm tying a player, a dedicated player to each ball". How would I go about doing that?
There are audio clicks/artefacts every time a new collision happens. I'm assuming this has to do with the AVAudioPlayerNodeBufferOptions and/or the fact that I'm trying to create, schedule and consume buffers very quickly each time contact occurs, which is not the most efficient method. What would be a good work around for this?
Code: As mentioned in the video, "...for every ball that's born into this world, a new player node is also created". I have a separate class for the spheres, with a method that returns a SpriteKitNode and also creates an AudioPlayerNode every time it is called :
class Sphere {
var sphere: SKSpriteNode = SKSpriteNode(color: UIColor(), size: CGSize())
var sphereScale: CGFloat = CGFloat(0.01)
var spherePlayer = AVAudioPlayerNode()
let audio = Audio()
let sphereCollision: UInt32 = 0x1 << 0
func createSphere(position: CGPoint, pitch: Float) -> SKSpriteNode {
let texture = SKTexture(imageNamed: "Slice")
let collisionTexture = SKTexture(imageNamed: "Collision")
// Define the node
sphere = SKSpriteNode(texture: texture, size: texture.size())
sphere.position = position
sphere.name = "sphere"
sphere.physicsBody = SKPhysicsBody(texture: collisionTexture, size: sphere.size)
sphere.physicsBody?.dynamic = true
sphere.physicsBody?.mass = 0
sphere.physicsBody?.restitution = 0.5
sphere.physicsBody?.usesPreciseCollisionDetection = true
sphere.physicsBody?.categoryBitMask = sphereCollision
sphere.physicsBody?.contactTestBitMask = sphereCollision
sphere.zPosition = 1
// Create AudioPlayerNode
spherePlayer = audio.createPlayer(pitch)
return sphere
}
Here's my Audio Class with which I create AudioPCMBuffers and AudioPlayerNodes
class Audio {
let engine: AVAudioEngine = AVAudioEngine()
func createBuffer(name: String, type: String) -> AVAudioPCMBuffer {
let audioFilePath = NSBundle.mainBundle().URLForResource(name as String, withExtension: type as String)!
let audioFile = try! AVAudioFile(forReading: audioFilePath)
let buffer = AVAudioPCMBuffer(PCMFormat: audioFile.processingFormat, frameCapacity: UInt32(audioFile.length))
try! audioFile.readIntoBuffer(buffer)
return buffer
}
func createPlayer(pitch: Float) -> AVAudioPlayerNode {
let player = AVAudioPlayerNode()
let buffer = self.createBuffer("PianoC1", type: "wav")
let pitcher = AVAudioUnitTimePitch()
let delay = AVAudioUnitDelay()
pitcher.pitch = pitch
delay.delayTime = 0.2
delay.feedback = 90
delay.wetDryMix = 0
engine.attachNode(pitcher)
engine.attachNode(player)
engine.attachNode(delay)
engine.connect(player, to: pitcher, format: buffer.format)
engine.connect(pitcher, to: delay, format: buffer.format)
engine.connect(delay, to: engine.mainMixerNode, format: buffer.format)
engine.prepare()
try! engine.start()
return player
}
}
In my GameScene class I then test for collision, schedule a buffer and play the AudioPlayerNode if contact has occurred
func didBeginContact(contact: SKPhysicsContact) {
let firstBody: SKPhysicsBody = contact.bodyA
if (firstBody.categoryBitMask & sphere.sphereCollision != 0) {
let buffer1 = audio.createBuffer("PianoC1", type: "wav")
sphere.spherePlayer.scheduleBuffer(buffer1, atTime: nil, options: AVAudioPlayerNodeBufferOptions.Interrupts, completionHandler: nil)
sphere.spherePlayer.play()
}
}
I'm new to Swift and only have basic knowledge of programming so any suggestion/criticism is welcome.
I've been working on AVAudioEngine in scenekit and trying to do something else, but this will be what you are looking for:
https://developer.apple.com/library/mac/samplecode/AVAEGamingExample/Listings/AVAEGamingExample_AudioEngine_m.html
It explains the process of:
1-Instantiating your own AVAudioEngine sub-class
2-Methods to load PCMBuffers for each AVAudioPlayer
3-Changing your Environment node's parameters to accomodate the reverb for the large number of pinball objects
Edit: Converted, tested and added a few features:
1-You create a subclass of AVAudioEngine, name it AudioLayerEngine for example. This is to access the AVAudioUnit effects such as distortion, delay, pitch and many of the other effects available as AudioUnits.
2-Initialise by setting up some configurations for the audio engine, such as rendering algorithm, exposing the AVAudioEnvironmentNode to play with 3D positions of your SCNNode objects or SKNode objects if you are in 2D but want 3D effects
3-Create some helper methods to load presets for each AudioUnit effect you want
4-Create a helper method to create an audio player then add it to whatever node you want, as many times as you want since that SCNNode accepts a .audioPlayers methods which returns [AVAudioPlayer] or [SCNAudioPlayer]
5-Start playing.
I've pasted the entire class for reference so that you can then structure it as you wish, but keep in mind that if you are coupling this with SceneKit or SpriteKit, you use this audioEngine to manage all your sounds instead of SceneKit's internal AVAudioEngine. This means that you instantiate this in your gameView during the AwakeFromNib method
import Foundation
import SceneKit
import AVFoundation
class AudioLayerEngine:AVAudioEngine{
var engine:AVAudioEngine!
var environment:AVAudioEnvironmentNode!
var outputBuffer:AVAudioPCMBuffer!
var voicePlayer:AVAudioPlayerNode!
var multiChannelEnabled:Bool!
//audio effects
let delay = AVAudioUnitDelay()
let distortion = AVAudioUnitDistortion()
let reverb = AVAudioUnitReverb()
override init(){
super.init()
engine = AVAudioEngine()
environment = AVAudioEnvironmentNode()
engine.attachNode(self.environment)
voicePlayer = AVAudioPlayerNode()
engine.attachNode(voicePlayer)
voicePlayer.volume = 1.0
outputBuffer = loadVoice()
wireEngine()
startEngine()
voicePlayer.scheduleBuffer(self.outputBuffer, completionHandler: nil)
voicePlayer.play()
}
func startEngine(){
do{
try engine.start()
}catch{
print("error loading engine")
}
}
func loadVoice()->AVAudioPCMBuffer{
let URL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("art.scnassets/sounds/interface/test", ofType: "aiff")!)
do{
let soundFile = try AVAudioFile(forReading: URL, commonFormat: AVAudioCommonFormat.PCMFormatFloat32, interleaved: false)
outputBuffer = AVAudioPCMBuffer(PCMFormat: soundFile.processingFormat, frameCapacity: AVAudioFrameCount(soundFile.length))
do{
try soundFile.readIntoBuffer(outputBuffer)
}catch{
print("somethign went wrong with loading the buffer into the sound fiel")
}
print("returning buffer")
return outputBuffer
}catch{
}
return outputBuffer
}
func wireEngine(){
loadDistortionPreset(AVAudioUnitDistortionPreset.MultiCellphoneConcert)
engine.attachNode(distortion)
engine.attachNode(delay)
engine.connect(voicePlayer, to: distortion, format: self.outputBuffer.format)
engine.connect(distortion, to: delay, format: self.outputBuffer.format)
engine.connect(delay, to: environment, format: self.outputBuffer.format)
engine.connect(environment, to: engine.outputNode, format: constructOutputFormatForEnvironment())
}
func constructOutputFormatForEnvironment()->AVAudioFormat{
let outputChannelCount = self.engine.outputNode.outputFormatForBus(1).channelCount
let hardwareSampleRate = self.engine.outputNode.outputFormatForBus(1).sampleRate
let environmentOutputConnectionFormat = AVAudioFormat(standardFormatWithSampleRate: hardwareSampleRate, channels: outputChannelCount)
multiChannelEnabled = false
return environmentOutputConnectionFormat
}
func loadDistortionPreset(preset: AVAudioUnitDistortionPreset){
distortion.loadFactoryPreset(preset)
}
func createPlayer(node: SCNNode){
let player = AVAudioPlayerNode()
distortion.loadFactoryPreset(AVAudioUnitDistortionPreset.SpeechCosmicInterference)
engine.attachNode(player)
engine.attachNode(distortion)
engine.connect(player, to: distortion, format: outputBuffer.format)
engine.connect(distortion, to: environment, format: constructOutputFormatForEnvironment())
let algo = AVAudio3DMixingRenderingAlgorithm.HRTF
player.renderingAlgorithm = algo
player.reverbBlend = 0.3
player.renderingAlgorithm = AVAudio3DMixingRenderingAlgorithm.HRTF
}
}

How do I make my enemySprite respawn as new bad guy once its been killed?

I am having trouble with my game, I've managed to have my enemySprites all shoot at the hero in a synchronized matter and I've gotten them to play the "killed" animation once they've been hit. Although I've run into a rather small matter which I was really hoping you guys could help me with. The problem I have is that when my badGuy is killed they move off the screen and I don't know how I can program it so that a 'new' badGuy appears forever until the Hero sprite is killed.
This is my function to spawn my enemy:
func spawnEnemy(targetSprite: SKNode) -> SKSpriteNode {
if !gamePaused{
// create a new enemy sprite
let main = GameScene()
newEnemy = SKSpriteNode(imageNamed:"BNG1_1.png")
enemySprite.append(newEnemy)//ADD TO THE LIBRARY OF BADGUYS
newEnemy.xScale = 1.2
newEnemy.yScale = 0.6
newEnemy.physicsBody?.dynamic = true
newEnemy.physicsBody = SKPhysicsBody(texture: newEnemy.texture, size: newEnemy.size)
newEnemy.physicsBody?.affectedByGravity = false
newEnemy.physicsBody?.categoryBitMask = BodyType.badguyCollision.rawValue
newEnemy.physicsBody?.contactTestBitMask = BodyType.beamCollison.rawValue
newEnemy.physicsBody?.collisionBitMask = 0x0
let muv : UInt32 = (200 + (arc4random()%500))
let actualDuration = NSTimeInterval(random(min: CGFloat(3.0), max: CGFloat(4.0)))
let randomNum = CGPoint(x:Int (muv), y:Int (arc4random()%500))
// Create the actions
var actionMove = SKAction.moveTo(randomNum, duration: NSTimeInterval(actualDuration))
newEnemy.runAction(SKAction.sequence([actionMove]))
// position new sprite at a random position on the screen
var posX = arc4random_uniform(UInt32(sizeRect.size.width))
var posY = arc4random_uniform(UInt32(sizeRect.size.height))
newEnemy.position = CGPoint(x: screenSize.width*2 + newEnemy.size.width, y: random(min: newEnemy.size.height, max: screenSize.height - newEnemy.size.height))
let atlas = SKTextureAtlas(named: "BG1.atlas")
let anime = SKAction.animateWithTextures([atlas.textureNamed("BG1_1.png"), atlas.textureNamed("BG1_2.png"),
atlas.textureNamed("BG1_3.png"),
atlas.textureNamed("BG1_2.png"),
atlas.textureNamed("BG1_1.png")], timePerFrame: 0.1)
dinoRun = SKAction.repeatActionForever(anime)
newEnemy.runAction(dinoRun)
}
return newEnemy
}
And this is my function for once they are hit (this function is called when the collisionTest between my hero's laser and the badguy is recognized):
func deadBadGuy(){
//animation
var dinoRun:SKAction
var newdes = CGPoint(x: Int(arc4random()%500), y:0)
var actionMoov = SKAction.moveTo(newdes, duration: 3)
var goaway = SKAction.removeFromParent()
let aTlas = SKTextureAtlas(named: "dedBG.atlas")
let anime = SKAction.animateWithTextures([aTlas.textureNamed("deadBG1.png"), aTlas.textureNamed("deadBG2.png"),
aTlas.textureNamed("deadBG3.png"),
aTlas.textureNamed("deadBG2.png"),
aTlas.textureNamed("deadBG1.png")], timePerFrame: 0.1)
dinoRun = SKAction.repeatActionForever(anime)
newEnemy.runAction(dinoRun)
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]))
score++
self.scoreNode.text = String(score)
enemySprites.newPlace(neewEnemy)
dead = true //i created this Boolean because the sprite keeps shooting even if its dead
}
This is the way I called for the collisionTest in my program in case you need it for more information:
func didBeginContact(contact:SKPhysicsContact){
if !gamePaused {
let firstNode = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch(firstNode){
case BodyType.beamCollison.rawValue | BodyType.badguyCollision.rawValue:
deadBadGuy()
//enemySprites.spawnEnemy(sprite)
default:
print("hit")
}
Note: I tried having the func spawnEnemy being called during the collisionTest but that results in the BadGuy staying off screen shooting at my hero.
Update : I found out how to add a new enemy sprite once the other is dead all i had to do was
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]), completion: {
self.addChild(self.enemySprites.spawnEnemy(sprite))
})
in the deadBadGuy function(the spawnEnemy function is in another class which I named enemySprites as a variable). However now I've run into a new issue and that is that it adds 10 enemySprites instead of one. How can I change that?
Update 2 : I figured out that issue too, I just needed to remove to dead Boolean method in the deadBadGuy function.
I found my answer and all I had to do was add this line to my deadBadGuy function and remove the dead boolean methods
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]), completion: {
self.addChild(self.enemySprites.spawnEnemy(sprite))
})

Resources