How to transport SKAction between devices? - swift

I'm trying to send a SKAction between devices but I've got some problem... could you help me please? This is my code to send it:
let movment1 = SKAction.moveTo(x: 1600, duration: 0.25)
car.run(movment1)
var positionToSend = movment1
let dataSend = Data(bytes: &positionToSend, count: MemoryLayout.size(ofValue: positionToSend))
try match?.sendData(toAllPlayers: dataSend, with: GKMatchSendDataMode.reliable)
and this is how I receive it:
func receiveDataP(position: Data, player: GKPlayer) {
let _ : SKAction = position.withUnsafeBytes { $0.pointee }
}
This is not working, in fact I was just trying...can someone help me rewriting the correct code? I'm getting mad...

Instead of sending the whole SKAction, you can just send the necessary information needed to recreate the SKAction in the other device.
Here, I suppose an SKAction is described an x value and a duration. If duration is always 0.25 then you don't need to send the duration.
Create something like this:
struct SKActionDescriptor : Codable {
let x: CGFloat
let duration: Double
}
To create the data to send, use a JSONEncoder:
let encoder = JSONEncoder()
let data = try! encoder.encode(SKActionDescriptor(x: 1600, duration: 0.25))
And you can use a JSONDecoder at the other end:
let decoder = JSONDecoder()
let descriptor = try! decoder.decode(SKActionDescriptor.self, from: receivedData)
And then create an SKAction:
let action = SKAction.moveTo(x: descriptor.x, duration: descriptor.duration)

if you want it to hit some thing or make it more movable you could make it physically
like that and try to use
Player.physicsBody?.velocity = CGVector(dx: 1600, dy: 0)
Player.physicsBody?.angularVelocity = 0.25
enter image description here
am not sure but I hope it work;

Related

How to know when an SKAction.repeat(action, count) is finished

I'm making a video games with Swift & SpriteKit. I'm trying to make the level system of my game. Each levels has his own specifications (but it's not in the code right now).
However, I would like that when my SKaction.repeat is done, to move to an other scene (such as "Level completed" scene).
Do you know how can I do it ?
Here's my code :
func parametersLevel(){
let spawn = SKAction.run(asteroids)
let waitSpawn = SKAction.wait(forDuration: 0.8)
let sequence = SKAction.sequence([waitSpawn,spawn])
let spawnCount = SKAction.repeat(sequence, count: 750)
self.run(spawnCount)
}
Thanks for you help.
From run(_:completion:) instead of self.run(spawnCount) try with:
self.run(spawnCount, completion: {() -> Void in
println("completed")
})
If you need a key with your action, you can also do:
func parametersLevel(){
let spawn = SKAction.run(asteroids)
let waitSpawn = SKAction.wait(forDuration: 0.8)
let sequence1 = SKAction.sequence([waitSpawn,spawn])
let spawnCount = SKAction.repeat(sequence, count: 750)
let endAction = SKAction.run{} //whatever you need your ending to be
let sequence2 = SKAction.sequence([spawnCount ,endAction])
self.run(sequence2,withKey:”spawn” )
}

Spawn SKSpriteNode objects at random times

I am making a game where I would like to spawn obstacles at random times. The obstacles are going to make contact with the player.
Here the code that I have:
var obstacle : SKSpriteNode?
func createObstacles() {
let obstaclesArray = ["obstacle_1", "obstacle_2", "obstacle_3", "obstacle_4", "obstacle_5"]
let randomObs = obstaclesArray.randomElement()
let selectedTexture = SKTexture(imageNamed: randomObs!)
obstacle = SKSpriteNode(imageNamed: randomObs!)
obstacle?.physicsBody = SKPhysicsBody(texture: selectedTexture, size: selectedTexture.size())
obstacle?.position = CGPoint(x: scene!.size.width/2 + selectedTexture.size().width, y: -120)
obstacle?.zPosition = -15
if let obs = obstacle?.physicsBody {
obs.affectedByGravity = true
obs.allowsRotation = false
obs.isDynamic = true
obs.restitution = 0
obs.categoryBitMask = obstacleCategory
obs.collisionBitMask = floorCategory
obs.contactTestBitMask = heroCategory
}
addChild(obstacle!)
}
func spawnObstacles() {
let wait = SKAction.wait(forDuration: 1, withRange: 0.4)
let spawn = SKAction.run {
createObstacles()
}
let moveLeft = SKAction.moveBy(x: -scene!.size.width - obstacle.size.width - 10, y: 0, duration: 2)
let sequence = SKAction.sequence([wait, spawn, moveLeft, SKAction.removeFromParent()])
self.run(SKAction.repeatForever(sequence))
}
I read some a similar questions but the response is my same code but it is not working.
Spawning a Spritekit node at a random time
I also tried other way:
var randDelay = Double.random(in: 0.7 ..< 1.4)
DispatchQueue.main.asyncAfter(deadline: randDelay, execute: {
if self.canCreateObstacle == true {
self.spawnObstacle()
}})
But it is not working and everytime I restart the game it seems like the function is being called two times, if I restart the game a third time it is called 3 times and so on.
Anyone with a good and clean solution to spawn objects at random times?
Do not use DispatchQueue Set up a repeating sequential action using wait(forDuration:withRange:) like you previously had.
https://developer.apple.com/documentation/spritekit/skaction/1417760-wait
First, create a generic node used to spawn obstacles, then attach this generic node to the scene.
Finally assign the repeating sequential action to this node.
Boom, you are done.
The reason why you want to assign it to a random node is because you want to be able to give your game the opportunity to stop generating obstacles, plus you alter the speed property to make the node generate nodes faster or slower.
You also want to detach the spawning/waiting from the moving/destroying, because as of right now, your code is confused. You are saying move the scene left for 2 seconds, then wait a random amount of time to spawn the next enemy, but I think you are trying to just spawn enemies on a time interval and move the enemy to the left.
Your scene code should look something like this
class GameScene : SKScene{
let obstacleGenerator = SKNode()
func didMove(to: view){
let wait = SKAction.wait(forDuration: 1, withRange: 0.4)
let spawn = SKAction.run({createObstacles()})
let sequence = SKAction.sequence([wait, spawn])
obstacleGenerator.run(SKAction.repeatForever(sequence))
addChild(obstacleGenerator)
}
func createObstacles() {
let obstaclesArray = ["obstacle_1", "obstacle_2", "obstacle_3", "obstacle_4", "obstacle_5"]
let randomObs = obstaclesArray.randomElement()
let selectedTexture = SKTexture(imageNamed: randomObs!)
obstacle = SKSpriteNode(imageNamed: randomObs!)
obstacle.position = CGPoint(x: scene!.size.width/2 + selectedTexture.size().width, y: -120)
obstacle.zPosition = -15
let body = SKPhysicsBody(texture: selectedTexture, size: selectedTexture.size())
body.affectedByGravity = true
body.allowsRotation = false
body.isDynamic = true
body.restitution = 0
body.categoryBitMask = obstacleCategory
body.collisionBitMask = floorCategory
body.contactTestBitMask = heroCategory
obstacle.physicsBody = body
addChild(obstacle!)
let moveLeft = SKAction.moveBy(x: -scene!.size.width - obstacle.size.width - 10, y: 0, duration: 2)
let seq = SKAction.sequence([moveLeft,SKAction.removeFromParent()])
obstacle.run(seq)
}
}
Now as for your spawning increasing with each reset, you never post how you are resetting. I am going to assume you never remove the previous action upon reset, and this is why your rate increases. It is always better to just create a new GameScene instance when doing a reset.

AVPlayer Seek not accurate even with toleranceBefore: kCMTimeZero

We have an app which plays a long mp3 file (1 hour long). We want to be able to play from set points within the file. But, when we do it, it is inaccurate by up to 10 seconds.
Here's the code:
let trackStart = arrTracks![MediaPlayer.shared.currentSongNo].samples
let frameRate : Int32 = (MediaPlayer.shared.player?.currentItem?.asset.duration.timescale)!
MediaPlayer.shared.player?.seek(to: CMTimeMakeWithSeconds(Double(trackStart), frameRate),
toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
We have to use AVPlayer because we need the better quality "spectral:" AVAudioTimePitchAlgorithm.
We didn't have the problem with AVAudioPlayer, but (AFAIK) we have to use AVPlayer because we need the better quality "spectral:" AVAudioTimePitchAlgorithm.
[Edit:] - The error is consistent - it always plays from the same (wrong) place for a given requested position. This is also true after restarting.
Any help very much appreciated! Thanks
[Edit:]
We have already tried preferredTimescale: playerTimescale
Also tried kCMTimeIndefinite instead of kCMTimeZero
I have done something similar but with a slider to change seconds of playing and worked perfectly.
#objc func handleSliderChange(sender: UISlider?){
if let duration = player?.currentItem?.duration{
let totalSeconds = CMTimeGetSeconds(duration)
let value = Float64(videoSlider.value) * totalSeconds
let seekTime = CMTime(value: CMTimeValue(value), timescale: 1)
player?.seek(to: seekTime , completionHandler: { (completedSeek) in
//do smthg later
})
}
}
in you case this will be like this:
let trackStart = arrTracks![MediaPlayer.shared.currentSongNo].samples
let value = Float64(trackStart)
let seekTime = CMTime(value: CMTimeValue(value), timescale: 1)
MediaPlayer.shared.player?.seek(to: seekTime , completionHandler: { (completedSeek) in
//do smthg later
})
This is what AVURLAsset’s AVURLAssetPreferPreciseDurationAndTimingKey is for.
Apple's documentation.
Beware that this should increase the loading time.
Try this, It's working perfectly for me
#IBAction func playbackSliderValueChanged(_ playbackSlider: UISlider) {
let seconds : Int64 = Int64(playbackSlider.value)
let targetTime: CMTime = CMTimeMake(value: seconds, timescale: 1)
DispatchQueue.main.async {
self.player!.seek(to: targetTime)
if self.player!.rate == 0 { // if the player is not yet started playing
self.player?.play()
}
}
}

SpriteKit multiple SKActions for multiple SKSpriteNode - wait for all to complete

I'm working on a iOs game and I have a problem.
I need to display x sprites (for each one I have a scale SKAction).
I need to be able to wait until all SKAction sprites and then do something else.
Each SKAction run in a separate thread.
How I can wait ?
Here is a piece of code:
for tile in tiles {
let randomNum:UInt32 = arc4random_uniform(20) // range is 0 to 99
let randomTime:TimeInterval = TimeInterval(randomNum/10)
let scale = SKAction.scale(by: 1, duration: 2, delay:randomTime , usingSpringWithDamping: 0.1, initialSpringVelocity: 5)
tile.sprite = SKSpriteNode(imageNamed: tile.type.spriteName)
tile.sprite?.size = CGSize(width: TileWidth, height: TileHeight)
tile.sprite?.position = tile.position!
tile.sprite?.scale(to: CGSize(width: 0, height: 0))
cookiesLayer.addChild(tile.sprite!)
tile.sprite?.run(scale)
}
//TODO code to add to be executed after all SKActions
How can I do my TODO code to be executer after all SKActions ?
I would like to run the SKAction in parallel or one after another.
Thanks.
You could do this very easily using a completion block with your run method.
Just for the sake of this example, say you have a SKSpriteNode named someSpriteNode and want to know when two actions (applyImpulse in that case) have finished running:
// 1) Create your actions:
let action1 = SKAction.applyImpulse(CGVector(dx: 1.0, dy: 0.0), duration: 2.0)
let action2 = SKAction.applyImpulse(CGVector(dx: 6.0, dy: 2.0), duration: 1.0)
// 2) Add them to a sequence:
let actionSequence = SKAction.sequence([action1, action2])
// 3) Run the sequence using a completion block:
someSpriteNode?.run(actionSequence, completion: {
// All your actions are now finished
// Do whatever you want here :)
})
UPDATE: Get notified when a group of actions got executed, where all the actions run on the same node
You might be looking for action groups then:
// Declare an empty array that will store all your actions:
var actions = [SKAction]()
// Iterate through your nodes:
for _ in 0..<6 {
// ...
// Generate your random scale, delay, or whatever you need:
let randomScale = CGFloat(GKRandomDistribution(lowestValue: 0, highestValue: 10).nextInt())
// Create your custom action
let scaleAction = SKAction.scale(by: randomScale, duration: 2.0)
// Append your action to the actions array:
actions.append(scaleAction)
}
// Create an action group using the actions array:
let actionGroup = SKAction.group(actions)
// Run your action group, and do whatever you need inside the completion block:
self.run(actionGroup, completion: {
// All your actions are now finished, no matter what node they were ran on.
})
Also, I would recommend you using GameplayKit to generate random numbers in your game, it will definitely make your life easier :)
UPDATE 2: Get notified when all actions got executed, in the case where all the actions run on different nodes
Using DispatchGroup :
// Create a DispatchGroup:
let dispatchGroup = DispatchGroup()
for _ in 0..<6 {
// ...
let randomWait = Double(GKRandomDistribution(lowestValue: 1, highestValue: 12).nextInt())
let waitAction = SKAction.wait(forDuration: randomWait)
let fadeOutAction = SKAction.fadeOut(withDuration: 2.0)
let fadeInAction = SKAction.fadeIn(withDuration: 2.0)
let sequenceAction = SKAction.sequence([waitAction, fadeOutAction, fadeInAction])
// Enter the DispatchGroup
dispatchGroup.enter()
colorSquares[i].run(sequenceAction, completion: {
// Leave the DispatchGroup
dispatchGroup.leave()
})
}
// Get notified when all your actions left the DispatchGroup:
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
// When this block is executed, all your actions are now finished
})
I think this solution is way more elegant than a counter :)
First of all you should always create a Minimum Verifiable Example. Remove the unneeded things from your question and make sure to include the everything we needed to test your code.
Premise
I assume you have a Tile class similar to this
class Tile {
var sprite: SKSpriteNode?
}
and an array like this
let tiles:[Tile] = ...
Your objective
You want to run an action of random duration on the sprite element of each tile in tiles.
You want the actions to start at the same time
You want to be able to run some code when all the actions are completed
Solution
// 0. create a maxDuration variable
var maxDuration:TimeInterval = 0
// 1. create all the actions
let actions = tiles.map { tile in
return SKAction.run {
let randomNum = arc4random_uniform(100)
let randomTime = TimeInterval(randomNum / 10)
let wait = SKAction.wait(forDuration: randomTime)
let scale = SKAction.scale(by: 1, duration: 2)
tile.sprite?.run(scale)
maxDuration = max(maxDuration, randomTime + 2)
}
}
// 2. create a wait action for the max duration
let wait = SKAction.wait(forDuration: maxDuration)
// 3. write inside this action the code to be executed after all the actions
let completion = SKAction.run {
print("now all the actions are completed")
}
// 4. create a sequence of wait + completion
let sequence = SKAction.sequence([wait, completion])
// 5. create a group to run in parallel actions + sequence
let group = SKAction.group(actions + [sequence])
// 6. run the group on the node you prefer (it doesn't really matter which node since every inner action is tied to a specific node)
self.run(group)
Update (as suggested by #Alex)
var maxDuration:TimeInterval = 0
tiles.forEach { tile in
let randomNum = arc4random_uniform(100)
let randomTime = TimeInterval(randomNum / 10)
let wait = SKAction.wait(forDuration: randomTime)
let scale = SKAction.scale(by: 1, duration: 2)
tile.sprite?.run(scale)
maxDuration = max(maxDuration, randomTime + 2)
}
run(.wait(forDuration: maxDuration)) {
print("now all the actions are completed")
}

Swift SpriteKit unwrapping and wrapping

func bombTowerTurnShoot() {
let zombieGreen = self.childNode(withName: "zombie") as! SKSpriteNode
self.enumerateChildNodes(withName: "bomb tower") {
node, stop in
if let bombTower = node as? SKSpriteNode {
let angle = atan2((zombieGreen.position.x) - bombTower.position.x, (zombieGreen.position.y) - bombTower.position.y)
let actionTurn = SKAction.rotate(toAngle: -(angle - CGFloat(Double.pi/2)), duration: 0.2)
bombTower.run(actionTurn)
}
}
}
My issue is on the let angle line. When I call the function when there is no zombieGreens on the scene I get a Thread 1 problem. How can I change the code to take into account when the zombie is not present?
if there is no ZombiGreens in the scene the error should happen already at the second line:
let zombieGreen = self.childNode(withName: "zombie") as! SKSpriteNode
I think the simplest solution without changing to much of your code would be to use an if let just as you did for the bomb tower. and it would look something like this:
func bombTowerTurnShoot() {
if let zombieGreen = self.childNode(withName: "zombie") as? SKSpriteNode{
self.enumerateChildNodes(withName: "bomb tower") {
node, stop in
if let bombTower = node as? SKSpriteNode {
let angle = atan2((zombieGreen.position.x) - bombTower.position.x, (zombieGreen.position.y) - bombTower.position.y)
let actionTurn = SKAction.rotate(toAngle: -(angle - CGFloat(Double.pi/2)), duration: 0.2)
bombTower.run(actionTurn)
}
}
}
}
But it could be a good idea to review your code when you have more logic to handle. it might be a better way to do things but this should work :)