Swift Countdown Function - swift

I am attempting to create a countdown timer for a game using SpriteKit, but whenever I try to run countDown(), my game freezes. I am pretty sure my logic is correct here. I do not know what is going on.
func countDown(){
let countDownWait = SKAction.wait(forDuration: 1.0)
repeat {
self.run(countDownWait){
self.countDownTime -= 1
}
} while (self.countDownTime > 0)
if self.countDownTime == 0{
self.runGameOver()
}
}

you can do some checking in the update func for time passed or use a SKAction to track time similar to what you were doing in your code
let someLabel = SKLabelNode()
func countdown() {
var offset: Double = 0
for x in (0...10).reversed() {
run(SKAction.wait(forDuration: offset)) {
someLabel.text = "\(x)"
if x == 0 {
//do something when counter hits 0
//self.runGameOver()
}
else {
//maybe play some sound tick file here
}
}
offset += 1.0
}
}

Here's how I solved this problem for my Swift/SpriteKit 'Breakout' application. I wanted a countdown from 5 to 1 onthe main game screen but before the ball started to move. I Added these functions and then a call to countdown(5) at the end of didMoveToView: Notice the ball.physicsBody!.applyImpulse(CGVectorMake(20, 20)) as the last step of endCountdownwhich starts the ball and effectively starts the game.
func countdown(count: Int) {
countdownLabel.horizontalAlignmentMode = .Center
countdownLabel.verticalAlignmentMode = .Baseline
countdownLabel.position = CGPoint(x: size.width/2, y: size.height*(1/3))
countdownLabel.fontColor = SKColor.whiteColor()
countdownLabel.fontSize = size.height / 30
countdownLabel.zPosition = 100
countdownLabel.text = "Launching ball in \(count)..."
addChild(countdownLabel)
let counterDecrement = SKAction.sequence([SKAction.waitForDuration(1.0),
SKAction.runBlock(countdownAction)])
runAction(SKAction.sequence([SKAction.repeatAction(counterDecrement, count: 5),
SKAction.runBlock(endCountdown)]))
}
func countdownAction() {
count--
countdownLabel.text = "Launching ball in \(count)..."
}
func endCountdown() {
countdownLabel.removeFromParent()
ball.physicsBody!.applyImpulse(CGVectorMake(20, 20))
}

Try this solution to run countdown...
var seconds = 7200
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
runTimer()
}
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(self.updateTimer)), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
if seconds < 1 {
timer.invalidate()
//Send alert to indicate time's up.
} else {
seconds -= 1
timerLabel.text = timeString(time: TimeInterval(seconds))
}
}
func timeString(time:TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}

Have you considered using a timer? Here is a good tutorial. It might take you 20 minutes to go through it or so, but it goes into detail about starting, stopping, pausing, and more for timers.

Related

Why is my animation quicker than the timer?

I have an animation that I want to coincide with the timer but right now it ends with 6seconds left of the timer. How do I get the animation to match? Also, how would i go about repeating the animation for the countdown of the iteration, i?
The code has the animation, in a circle, and then preset timer of 30s (which will eventually be a slider input). I will also eventually want to include a pause, and stop button for the timer which will need to coincide with the animation
import UIKit
var timer = Timer()
var time = 30
var i = 5
class ViewController: UIViewController {
#IBOutlet weak var displayTime: UILabel!
let shape = CAShapeLayer()
private let label: UILabel = {
let label = UILabel()
label.text = String(i)
// change label to update i
label.font = .systemFont(ofSize: 36, weight: .light)
return label
}()
func countdown() {
displayTime.text = String(time)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector:
#selector(doCountdown), userInfo: nil, repeats: true)
}
override func viewDidLoad() {
super.viewDidLoad()
label.sizeToFit()
view.addSubview(label)
label.center = view.center
let circlePath = UIBezierPath(arcCenter: view.center, radius: 150, startAngle: -
(.pi / 2), endAngle: .pi * 2, clockwise: true)
let trackShape = CAShapeLayer()
trackShape.path = circlePath.cgPath
trackShape.fillColor = UIColor.clear.cgColor
trackShape.lineWidth = 15
trackShape.strokeColor = UIColor.lightGray.cgColor
view.layer.addSublayer(trackShape)
shape.path = circlePath.cgPath
shape.lineWidth = 15
shape.strokeColor = UIColor.blue.cgColor
shape.fillColor = UIColor.clear.cgColor
shape.strokeEnd = 0
// cg = core graphics
view.layer.addSublayer(shape)
let button = UIButton(frame: CGRect(x: 20, y: view.frame.size.height-70, width:
view.frame.size.width-40, height: 50))
view.addSubview(button)
button.setTitle("Animate", for: .normal)
button.backgroundColor = .systemGreen
button.addTarget(self, action:#selector(didTapButton), for: .touchUpInside)
}
#objc func didTapButton() {
countdown()
// Animate
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.toValue = 1
animation.duration = Double(time)
// duration of animation
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
shape.add(animation, forKey: "animation")
}
#objc func doCountdown() {
time = time - 1
displayTime.text = String(time)
if time == 0 {
i = i - 1
time = 30
}
if i == 0 {
label.text = "0"
timer.invalidate()
}
}
}
Your implementation does not work because you are using a naive implementation of countdown.
A timer is not guaranteed to fire exactly after the given amount of time. It won't fire exactly after one second. The accuracy of Timer is 50-100 milliseconds. Therefore the total possible error can add up to 30*100 milliseconds, that is 3 entire seconds.
Instead, you have to use a Timer that will update your UI more often:
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(doCountdown), userInfo: nil, repeats: true)
And that also means you have to calculate your time differently. First of all, store the expected time of animation end:
// declare instance properties
private var animationEnd = Date()
private var timer: Timer? {
didSet {
// invalidate when nil is assigned
oldValue?.invalidate()
}
}
func startCountdown() {
// store the start time - 30 seconds in the future
animationEnd = Date().addingTimeInterval(TimerInterval(time))
// start the timer
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
guard let self = self else { return }
let remainingTime = max(0, self.animationEnd.timeIntervalSinceNow)
if remainingTime == 0 {
// invalidate the timer
self.timer = nil
}
// convert time to seconds
let remaininingSeconds = Int(remainingTime) ?? 0
self.displayTime.text = "\(remaininingSeconds)"
}
}
//
That's all.
If you want to pause & resume the timer, the process is the same. Invalidate the timer, store the current time (e.g. timePaused = Date) and when resumed, just add the difference between current time and timePaused to animationEnd and restart the timer.
Also, please, don't put variables on file level. Always put them to the scope of classes. Otherwise you will soon have problems.
I think these two variables are the source of your problem -
var time = 30
var i = 5
Can you try deleting the i variable and use this updated implementation -
#objc func doCountdown() {
time -= 1
displayTime.text = String(time)
if time == 0 {
timer.invalidate()
}
}

Fade out AVAudioEngine AVAudioPlayerNode

I´m trying to fade out an mp3 file using AVAudioEngine and AVAudioPlayerNode. I cannot figure it out. Can anybody please help in Swift?
I tried some things already. Everything works quite good but not as smooth as I would like it to be.
var fadeOutPlayer: Float = playerVolume
var fadeOutQuinte: Float = playerQuinteVolume
while fadeOutPlayer > 0.0 {
player.volume = fadeOutPlayer
playerQuinte.volume = fadeOutQuinte
fadeOutPlayer -= 0.001
fadeOutQuinte -= 0.001
usleep(100)
}
I also tried a timer (which doesn't do a fade):
if !isTimerFadeOutRunning {
player.volume = 0
playerQuinte.volume = 0
counter = 0
timerFadeOut = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(runTimerFadeOut), userInfo: nil, repeats: true)
isTimerFadeOutRunning = true
}
}
#objc func runTimerFadeOut() {
counter += 0.1
if counter == 0.2 {
player.pause()
playerQuinte.pause()
}
}
I've attached an AVAudioUnitEQ to my flow before mainMixerNode and then controlled the globalGain. The following code shows the idea:
private func manageFadeOut(volume: Double, fadeOutSecs: Double) {
let timeSecs = 0.001
DispatchQueue.main.asyncAfter(deadline: .now() + timeSecs) {
if !self.isFadingOut { return }
for fx in self.fadeOutEffects { fx.globalGain = Float(-96 * (1.0 - volume)) }
if volume <= 0 {
for player in self.players { player.stop() }
self.isFadingOut = false
return
}
let fade = timeSecs / fadeOutSecs
self.manageFadeOut(volume: volume - fade, fadeOutSecs: fadeOutSecs)
}
}

Another SKSpriteNode is removed when current one is touched

I am in developing an application using Xcode and currently I am facing a issue regarding SKSpriteNodes. When there are more than a single SKSpriteNode and when the node is touched, the touched node is not removed but the other not touched is removed. I have also noticed that when there are multiple nodes on the screen only the latest node coming from the top of the screen is removed whilst others are still moving down, although they are being touched. Can someone help identify why this has occurred and the methods of preventing such mistakes please?
For reference, I am have included the class in which the bug is in.
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var gameCounter: Int = 0
private var currentLevel: Int = 0
private var debug: SKLabelNode?
private var increasedTouchArea:SKSpriteNode?
private var generatedNode:SKSpriteNode?
private var moveAndRemove:SKAction?
private var moveNode:SKAction?
// Here we set initial values of counter and level. Debug label is created here as well.
override func didMove(to view: SKView) {
print(action(forKey: "counting") == nil)
gameCounter = 0
currentLevel = 1
//backgroundColor = SKColor.gray
debug = SKLabelNode(fontNamed: "ArialMT")
debug?.fontColor = SKColor.red
debug?.fontSize = 30.0
debug?.position = CGPoint(x: frame.midX, y: frame.midY)
debug?.text = "Counter : [ \(gameCounter) ], Level [ \(currentLevel) ]"
if let aDebug = debug {
addChild(aDebug)
}
}
//Method to start a timer. SKAction is used here to track a time passed and to maintain current level
func startTimer() {
print("Timer Started...")
weak var weakSelf: GameScene? = self
//make a weak reference to scene to avoid retain cycle
let block = SKAction.run({
weakSelf?.gameCounter = (weakSelf?.gameCounter ?? 0) + 1
//Maintaining level
if (weakSelf?.gameCounter ?? 0) < 5 {
//level 1
weakSelf?.currentLevel = 1
} else if (weakSelf?.gameCounter ?? 0) >= 5 && (weakSelf?.gameCounter ?? 0) < 10 {
//level 2
weakSelf?.currentLevel = 2
} else {
//level 3
weakSelf?.currentLevel = 3
}
weakSelf?.debug?.text = "Counter : [ \(Int(weakSelf?.gameCounter ?? 0)) ], Level [ \(Int(weakSelf?.currentLevel ?? 0)) ]"
})
run(SKAction.repeatForever(SKAction.sequence([SKAction.wait(forDuration: 1), block])), withKey: "counting")
}
//Method for stopping timer and reseting everything to default state.
func stopTimer() {
print("Timer Stopped...")
if action(forKey: "counting") != nil {
removeAction(forKey: "counting")
}
gameCounter = Int(0.0)
currentLevel = 1
debug?.text = "Counter : [ \(gameCounter) ], Level [ \(currentLevel) ]"
}
//Get current speed based on time passed (based on counter variable)
func getCurrentSpeed() -> CGFloat {
if gameCounter < 30 {
//level 1
return 1.0
} else if gameCounter >= 31 && gameCounter < 49 {
//level 2
return 2.0
} else {
//level 3
return 3.0
}
}
//Method which stop generating stones, called in touchesBegan
func stopGeneratingStones() {
print("STOPPED GENERATING STONES...")
if action(forKey: "spawning") != nil {
removeAction(forKey: "spawning")
}
}
func randomFloatBetween(_ smallNumber: CGFloat, and bigNumber: CGFloat) -> CGFloat {
let diff: CGFloat = bigNumber - smallNumber
return CGFloat(arc4random() % (UInt32(RAND_MAX) + 1)) / CGFloat(RAND_MAX) * diff + smallNumber
}
//Method for generating stones, you run this method when you want to start spawning nodes (eg. didMoveToView or when some button is clicked)
func generateStones() {
print("Generating Stones...")
let delay = SKAction.wait(forDuration: 1, withRange: 0.5) //Change forDuration: delay decreases as game progresses.
//randomizing delay time
weak var weakSelf: GameScene? = self
//make a weak reference to scene to avoid retain cycle
let block = SKAction.run({
let stone: SKSpriteNode? = weakSelf?.spawnNodes(withSpeed: weakSelf?.getCurrentSpeed() ?? 0.0)
stone?.zPosition = 20
if let aStone = stone {
weakSelf?.addChild(aStone)
}
})
run(SKAction.repeatForever(SKAction.sequence([delay, block])), withKey: "spawning")
}
func spawnNodes(withSpeed stoneSpeed: CGFloat) -> SKSpriteNode? {
let nodeSize = CGSize(width: 60, height: 60) //size of shape.
let initalNodePosition = CGPoint(x: randomFloatBetween(0.0, and: (self.view?.bounds.size.width)!) - 110, y: frame.maxY)
generatedNode = SKSpriteNode(color: SKColor.green, size: nodeSize)
generatedNode?.position = initalNodePosition
moveNode = SKAction.moveBy(x: 0, y: self.view!.scene!.frame.minY + self.view!.scene!.frame.minY , duration: 5.25)
generatedNode?.isUserInteractionEnabled = false //Allows users to touch shape.
increasedTouchArea = SKSpriteNode(color: UIColor.clear, size: CGSize(width: nodeSize.width * 1.35, height: nodeSize.height * 1.35))
increasedTouchArea?.name = "generatedNode"
increasedTouchArea?.isUserInteractionEnabled = false
generatedNode?.addChild(increasedTouchArea!)
moveNode?.speed = stoneSpeed
moveAndRemove = SKAction.sequence([moveNode!, SKAction.removeFromParent()])
generatedNode?.run(moveAndRemove!, withKey: "moving")
generatedNode?.name = "generatedNode"
return generatedNode
}
func deleteNode(){
moveAndRemove = SKAction.sequence([SKAction.removeFromParent()])
generatedNode?.run(moveAndRemove!, withKey: "moving")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent? {
for touch in touches {
let locationTocuhed = touch.location(in: self)
let touchedNode : SKNode = self.atPoint(locationTocuhed)
if touchedNode.name == generatedNode?.name {
print("Node touched")
deleteNode()
}
}
if action(forKey: "counting") == nil {
print("Game started...")
startTimer()
generateStones()
} else {
print("Game paused...")
}
}
}
Your method deleteNode() runs the deletion animation on the node pointed to by generatedNode, not the node last touched:
func deleteNode(){
moveAndRemove = SKAction.sequence([SKAction.removeFromParent()])
generatedNode?.run(moveAndRemove!, withKey: "moving")
}
If you want to delete the node last touched, pass a reference to this node to the method deleteNode and then run your deletion animation in that, not generatedNode.

Swift - Animate Image sequence

I have a list of images that I want to animate in Swift. I have tried to find the best possible way to do that - but I still have some problems.
The way I animate my image-list right now is by:
var animatedImage = UIImageView()
var velkomstImgList = [UIImage]()
override func viewDidAppear(_ animated: Bool) {
animatedImage.frame = CGRect(x: 0, y: 0, width: self.scrollView.frame.width, height: 200)
animatedImage.contentMode = .scaleAspectFit
scrollView.addSubview(animatedImage)
animateVelkomst()
}
func animateVelkomst() {
for i in 0...150 {
velkomstImgList.append(UIImage(named:"Velkomst_\(i).png")!)
if i == 150 {
self.animatedImage.animationImages = self.velkomstImgList
self.animatedImage.animationDuration = 5.0
self.animatedImage.startAnimating()
}
}
}
It works and the animation appears as expected. But the for loop in the beginning takes pretty long time and I dont think this is the right way to show the animation. Any suggestions on how I should animate the image sequence?
loading is slow because you are attempt to load 150 images same time. you can use timer and load image when you need it.
let imageCount = 150
var frameIndex = 0
var timer: Timer?
func startAnimation() {
frameIndex = 0
let deltaTime = 5.0 / Double(imageCount)
timer = Timer(timeInterval: deltaTime, target: self, selector: #selector(updateFrame), userInfo: nil, repeats: true)
}
func stopAnimation() {
timer?.invalidate()
timer = nil
}
#objc func updateFrame(_ timer: Timer) {
self.animatedImage.image = UIImage(named:"Velkomst_\(frameIndex).png")
frameIndex += 1
if frameIndex > imageCount {
frameIndex = 0
}
}

Changing score with a delay

I want to create this effect, that the score is changing with a delay between every number, like the score on the game over screen in Flappy Bird.
In this example, it should start counting when I touch the screen.
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
// Global declaration of objects
var scoreLabel = SKLabelNode()
var score:Int = 15
override func didMoveToView(view: SKView) {
/* Setup your scene here */
scoreLabel = SKLabelNode(fontNamed: "RubberBiscuitBold")
scoreLabel.fontSize = 50
scoreLabel.fontColor = SKColor.blackColor()
scoreLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
scoreLabel.zPosition = 1000
self.addChild(scoreLabel)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for var i = 0; i <= score; i++ {
scoreLabel.runAction(SKAction.waitForDuration(1))
scoreLabel.text = "\(i)"
print("\(i)")}
}
try something like this:
for var i = 0; i <= 10; i++ {
let seconds: Double = 0.5
let count = i
let delay = seconds * Double(NSEC_PER_SEC) // nanoseconds per seconds
let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(i)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
print("\(count)")
scoreLabel.text = "\(count)" })
}
Alternatively, with a recursive function definition it gets a bit more compact (and hopefully usable)
func countUp(start: Int, end: Int, delay: Double) {
if start <= end {
let del = delay * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(del))
dispatch_after(time, dispatch_get_main_queue()) {
print("\(start)") // Your rendering code here
countUp(start + 1, end: end, delay: delay)
}
} else {
let del = delay * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(del))
dispatch_after(time, dispatch_get_main_queue()) {
print("I'm done!") // Your continuation code here
// Please not that you should, for clarity and maintenance, wrap your delayed execution code inside a function.....
}
}
}
countUp(10, end: 20, delay: 1)
Just an idea here.
What if you create an additional node between the pipes that are a bit smaller.
Make it so that once they collide it ups your score.
That would also bring you the delay you want.