So I'm doing a simple game on swift 5 and I basically have a 3..2..1.. go timer to start the game and then a 3..2..1..stop timer to stop the game. And finally a function that displays the score. I need a way for each function call to wait for the timer to be done before the next one begins, any suggestions? Here's my code so far. (Also if you have any other suggestions on the app let me know as well, the end goal is to register how many taps of a button you can do in 3 seconds)
var seconds = 3 //Starting seconds
var countDownTimer = Timer()
var gameTimer = Timer()
var numberOfTaps = 0
override func viewDidLoad() {
super.viewDidLoad()
self.startCountdown(seconds: seconds)
self.gameCountdown(seconds: seconds)
self.displayFinalScore()
}
func startCountdown(seconds: Int) {
countDownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
self?.seconds -= 1
if self?.seconds == 0 {
self?.countdownLabel.text = "Go!"
timer.invalidate()
} else if let seconds = self?.seconds {
self?.countdownLabel.text = "\(seconds)"
}
}
}
func gameCountdown(seconds: Int) {
gameTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
self?.seconds -= 1
if self?.seconds == 0 {
self?.countdownLabel.text = "Stop!"
timer.invalidate()
} else if let seconds = self?.seconds {
self?.countdownLabel.text = "\(seconds)"
}
}
}
deinit {
// ViewController going away. Kill the timer.
countDownTimer.invalidate()
}
#IBOutlet weak var countdownLabel: UILabel!
#IBAction func tapMeButtonPressed(_ sender: UIButton) {
if gameTimer.isValid {
numberOfTaps += 1
}
}
func displayFinalScore() {
if !gameTimer.isValid && !countDownTimer.isValid {
countdownLabel.text = "\(numberOfTaps)"
}
}
You should think about the states your game could be in. It could be -
setup - Establish the game
starting - The first three seconds
running - After the first three seconds but before the end
ending - In the final three seconds
ended - Time is up.
Each time your timer ticks you need to consider what action do you need to take and what state do you need to move to. You haven't said how long you want the game to last, but let's say it is 30 seconds.
When a new game is started, you are in the setup state; The button is disabled (ie. it doesn't react to taps) and you set the score to 0. You move to the starting state.
In the starting you show the countdown. After three seconds you enable the button and move into the running state.
Once you reach 27 seconds, you move into the ending state and show the end count down
Finally time is up and you move into the ended state, disable the button and show the score.
You could code it something like this
enum GameState {
case setup
case starting
case running
case ending
case ended
}
class ViewController: UIViewController {
#IBOutlet weak var startButton: UIButton!
#IBOutlet weak var tapButton: UIButton!
#IBOutlet weak var countdownLabel: UILabel!
var gameState = GameState.ended
var gameTimer:Timer?
var numberOfTaps = 0
var gameStartTime = Date.distantPast
let GAMEDURATION: TimeInterval = 30
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func startButtonTapped(_ sender: UIButton) {
self.startGame()
}
#IBAction func tapMeButtonPressed(_ sender: UIButton) {
self.numberOfTaps += 1
}
func startGame() {
self.gameState = .setup
self.gameTimer = Timer.scheduledTimer(withTimeInterval:0.1, repeats: true) { timer in
let elapsedTime = -self.gameStartTime.timeIntervalSinceNow
let timeRemaining = self.GAMEDURATION-elapsedTime
switch self.gameState {
case .setup:
self.gameStartTime = Date()
self.tapButton.isEnabled = false
self.startButton.isEnabled = false
self.numberOfTaps = 0
self.gameState = .starting
case .starting:
if elapsedTime > 2.5 {
self.gameState = .running
self.tapButton.isEnabled = true
self.countdownLabel.text = "Go!"
} else {
let countdown = Int(3-round(elapsedTime))
self.countdownLabel.text = "\(countdown)"
}
case .running:
if timeRemaining < 4 {
self.gameState = .ending
}
case .ending:
let countdown = Int(timeRemaining)
self.countdownLabel.text = "\(countdown)"
if timeRemaining < 1 {
self.countdownLabel.text = "Stop"
self.gameState = .ended
self.tapButton.isEnabled = false
}
case .ended:
if timeRemaining <= 0 {
self.countdownLabel.text = "You tapped the button \(self.numberOfTaps) times"
self.startButton.isEnabled = true
self.gameTimer?.invalidate()
self.gameTimer = nil
}
}
}
}
}
The approach shouldn't be that the function calls wait for the timer to get finished, rather the timers should call the functions when they finish.
So, you need to move below function calls out of viewDidLoad and put them inside the Timer blocks.
self.gameCountdown(seconds: seconds)
self.displayFinalScore()
I.e. the function call self.gameCountdown(seconds: seconds) will go inside the timer block started in startCountdown. In that, when you are invalidating the timer when the seconds become 0, you call gameCountdown.
Similarly, in the timer started in gameCountdown, you call the self.displayFinalScore when the seconds become 0.
Few other suggestions. You should avoid checking properties in tapMeButtonPressed.
You should rather disable and enable the tap me button instead. I.e. enable it when you start the gameCountdown and disable it when it ends.
Similarly, you shouldn't need to check the state of the timers in displayFinalScore. It should just do one thing i.e. display the final score.
Will save you a lot of headaches later :). My 2 cents.
Related
so I have tried 2 different ways to display the timer count down on the screen.
the code will print to the console but not to the UITextView (in both loop cases) in the repeat the UITextView ends with a 0 and that is the only thing it displays other than original txt "time count".... in the case where the commented loop is implemented the UITextView only displays the 1 (end of count down)... why is it printing to the console though these commands are in the same brackets as UITextView and they repeat
the image is after running code and clicking Soft (this is spin off of app brewery egg timer)
//
// ViewController.swift
// EggTimer
//
// Created by Angela Yu on 08/07/2019.
// Copyright © 2019 The App Brewery. All rights reserved.
//
import UIKit
let softTime = 5
let medTime = 7
let hardTime = 12
class ViewController: UIViewController {
#IBOutlet weak var timerTextView: UITextView!
#IBAction func eggHardnessButton(_ sender: UIButton) {
let hardness = sender.currentTitle
func timeCountDownSoft() {
var time = 3
repeat {
time = time - 1 //repeats
timerTextView.selectAll("") // does not repeat
timerTextView.insertText("") // does not repeat
let timeWord = String(time) // repeats
timerTextView.insertText(timeWord)//does not repeat
print(time) //repeats
sleep(1) //repeats
} while time >= 1
/*for timer in stride(from: 2, to: 0 , by: -1){
let time = String(timer)
timerTextView.selectAll("")
timerTextView.insertText("")
timerTextView.insertText(time)
print(time)
sleep(1)
}*/
}
func timeCountDownMed() {
for timer in stride(from: 420, to: 0 , by: -1) {
print(timer)
sleep(1)
}
}
func timeCountDownHard() {
for timer in stride(from: 720, to: 0 , by: -1) {
print(timer)
sleep(1)
}
}
if hardness == "Soft" {
print(softTime) // does not repeat
timeCountDownSoft() // does not repeat
} else if hardness == "Medium" {
print(medTime)
timeCountDownMed()
} else {
print(hardTime)
timeCountDownHard()
}
}
}
You never (well, almost never) want to use sleep().
The reason your text is not updating is because you are running closed-loops that never allow UIKit to update the view.
What you want to do instead is to use a repeating Timer with a one-second interval. Each time the timer fires, decrement your counter and update the UI. When the counter reaches Zero, stop the timer.
Here's a simple example:
import UIKit
let softTime = 5
let medTime = 7
let hardTime = 12
class ViewController: UIViewController {
#IBOutlet weak var timerTextView: UITextView!
var theTimer: Timer?
#IBAction func eggHardnessButton(_ sender: UIButton) {
let hardness = sender.currentTitle
var numSeconds = 0
// set number of seconds based on button title
switch hardness {
case "Soft":
numSeconds = softTime * 60
case "Medium":
numSeconds = medTime * 60
case "Hard":
numSeconds = hardTime * 60
default:
// some other button called this func, so just return
return()
}
// stop current timer if it's running
if let t = theTimer {
if t.isValid {
t.invalidate()
}
}
// update text field with selected time
let timeWord = String(numSeconds)
self.timerTextView.text = timeWord
// start a timer with 1-second interval
theTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
numSeconds -= 1
let timeWord = String(numSeconds)
self.timerTextView.text = timeWord
// if we've reached Zero seconds remaining
if numSeconds == 0 {
// stop the timer
timer.invalidate()
}
}
}
}
Several points:
When your app changes the UI, those changes do not actually get
rendered to the screen until your code returns and visits the app's
event loop. Thus, with your repeat loop, nothing will show up on in
the UI until the loop completes
Do not use sleep() on the main thread. That blocks everything, and
your app comes to a screeching halt.
If you want to update the UI once a second, set up a repeating timer
that fires once a second and do your UI updates in the timer code.
Any idea why my label.text is only updating when the count finishes?
didSet is called. But the label.text = String(counter) appears to do nothing.
Swift 5
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
var counter:Int = 0 {
didSet {
print("old value \(oldValue) and new value: \(counter)")
label.text = String(counter)
sleep(1). // just added to show the label.text is not updating
}
}
#IBAction func start_btn(_ sender: Any) {
for _ in 1...3 {
counter += 1
}
}
}
didSet code is called from the Main Thread. It is all wired correctly with Storyboards ( not SwiftUI).
You can see the didSet code is called.
old value 0 and new value: 1. Main thread: true
old value 1 and new value: 2. Main thread: true
old value 2 and new value: 3. Main thread: true
It looks like you're trying to make some kind of counter which starts at 0 and stops at 3. If that is the case you should not call sleep (which blocks the main thread).
edit: apparently the sleep call was added for demonstration purposes?
In any case the reason why your label seems like it is only updating when the count finishes is because the for loop runs too quickly for the UI to update on each counter increment.
Rather use Timer:
counter = 0
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.counter += 1
if self.counter >= 3 {
timer.invalidate()
}
}
This is based on my rough understanding on what you're aiming to achieve.
You could also DispatchQueue.main.asyncAfter:
func countUp() {
guard counter < 3 else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.counter += 1
fire()
}
}
For short time intervals, the difference between the two approaches is going to be pretty insignificant. For really accurate time counting, one shouldn't rely on either though, but rather use Date with a Timer that fires say every tenth of a second, and updates counter by rounding to the nearest second (for example).
You can achieve it like following
#IBAction func start_btn(_ sender: Any) {
updateCounter()
}
func updateCounter() {
if counter == 3 {
return
} else {
counter += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.updateCounter()
})
}
}
Never, ever call sleep in an iOS app. That will block the main thread, which means your app will be frozen for a whole second with sleep(1).
This means that the main thread will be blocked while the loop in start_btn finishes and hence the UI can only be updated after the loop has already finished.
If you want to make the text change every second, modify the button action to
#IBAction func start_btn(_ sender: Any) {
for i in 1...3 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i), execute: {
self.counter += 1
})
}
}
and remove sleep(1) from didSet.
So I'm making a game for my computer science class and I made a class named Bomb for the sprites in the game. In the Bomb class, I have a function named blowUp() that is called when a countdown timer for the bomb goes off. When time runs out, the bomb's texture changes among other things, but it should also trigger a game over.
I have a gameOver() function in my GameScene that I want to call up in the blowUp() method but when I do that I get the error message "Instance member 'gameOver' cannot be used on type 'GameScene'; did you mean to use a value of this type instead?"
Is there any way around this? Thanks in advance
class Bomb{
var sprite = SKSpriteNode()
var timer = Timer()
var secondsLeft = 20
func countDown(){
secondsLeft -= 1
if secondsLeft == 0{
blowUp()
}
}
init {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in self.countDown()})
}
func blowUp(){
self.timer.invalidate()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sprite.removeFromParent()
}
// gameOver() gives me an error
}
}
Here are the changes I would make to your code.
Extend SKSpriteNode and remove the sprite variable, because you want your bomb to be an actual sprite.
class Bomb : SKSpriteNode{
var sprite = SKSpriteNode()
Eliminate your Timer. Timer goes off of real world, not in game world, so if any external event happens (like a phone call) your time will be off. Use SKActions instead.
var timer = Timer()
var secondsLeft = SKAction.wait(forDuration:20)
Remove your countdown function, blowup function and your init, and replace it with an ignite function
func countDown(){
secondsLeft -= 1
if secondsLeft == 0{
blowUp()
}
}
init {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in self.countDown()})
}
func blowUp(){
self.timer.invalidate()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sprite.removeFromParent()
}
// gameOver() gives me an error
}
func ignite() {
var explosionAnimation = SKAction.animate(with:arrayOfExplosionTextures,timePerFrame:0.1666)
var blowup = SKAction.run{
[unowned self] in
(self.scene as GameScene).gameOver()
}
//var seq = SKAction.sequence([secondsLeft,explosionAnimation,blowup,SKAction.removeFromParent])
var seq = SKAction.sequence([secondsLeft,blowup,SKAction.removeFromParent])
}
What ignite does is start your bomb when you are ready to start it, waits 30 seconds, then explodes. This allows you to place bombs on the screen that are delayed or duds.
Here is what your class should look like:
class Bomb : SKSpriteNode{
var secondsLeft = SKAction.wait(forDuration:20)
func ignite() {
//var explosionAnimation = SKAction.animate(with:arrayOfExplosionTextures,timePerFrame:0.1666)
var blowup = SKAction.run{
[unowned self] in
(self.scene as GameScene).gameOver()
}
//var seq = SKAction.sequence([secondsLeft,explosionAnimation,blowup,SKAction.removeFromParent])
var seq = SKAction.sequence([secondsLeft,blowup,SKAction.removeFromParent])
}
}
Here is how you use it in game scene:
func placeBomb(x:CGFloat,y:CGFloat){
let bomb = Bomb(imageNamed:"bomb")
addChild(bomb)
bomb.position = CGPoint(x:x,y:y)
bomb.ignite()
}
There are 2 Arrays. First one contains Strings that i want to show on UILabel. Second one contains their waiting durations on UILabel.
let items = ["stone","spoon","brush","ball","car"]
let durations = [3,4,1,3,2]
And two variables for specifying which one is on the go.
var currentItem = 0
var currentDuration = 0
This one is the timer system:
var timer = NSTimer()
var seconds = 0
func addSeconds () {seconds++}
func setup () {
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "addSeconds", userInfo: nil, repeats: true)
}
Finally, that's the loop. Answer of: Which Array item stays how many seconds on the UILabel question.
func flow () {
while seconds <= durations[currentDuration] {
myScreen.text = items[currentItem]
if seconds == durations[currentDuration]{
seconds == 0
currentItem++
currentDuration++
}
}
Label and Button:
#IBOutlet weak var myScreen: UILabel!
#IBAction func startButton(sender: UIButton) {
setup()
}
}
If i change this:
func addSeconds () {seconds++}
To that:
func addSeconds () {seconds++ ; flow () }
For setting the loop, nothing happens. Even NSTimer, it stops at 1st second.
Because your flow method has a while loop that never exits and blocks the main thread, so the timer can never fire.
Don't use a while loop. Us the method triggered by the timer to update the UI.
So:
func addSeconds () {
seconds++
myScreen.text = items[currentItem]
if seconds == durations[currentDuration] {
seconds == 0
currentItem++
currentDuration++
}
}
I created a timer in one class, and tried to do something else in another class while timer works, and do other thing while timer stops. For example, show every second when timer works. I simplified the code as below. How to realize that?
import Foundation
import UIView
class TimerCount {
var timer: NSTimer!
var time: Int!
init(){
time = 5
timer = NSTimer.scheduledTimerWithTimeInterval( 1.0 , target: self, selector: Selector("update"), userInfo: nil, repeats: true)
}
func update(){
if(time > 0) {
time = time - 1
// do something while timer works
}
else{
timer.invalidate()
timer = nil
time = 5
}
}
}
class Main: UIView {
var Clock: TimerCount!
override func viewDidLoad() {
Clock = TimerCount()
//? do something else while clock works
// ? do other things while clock stops
// FOR EXAMPLE: show every second when timer works
if(Clock.time > 0){
println(Clock.time)
}else{
println("clocker stops")
}
}
}
viewDidLoad is most likely only going to be called once. You could simply make the update method be in your Main object and then pass that instance of main and that update method into the scheduledTimerWithTimerInterval call. Otherwise you need a new method in the Main class to call from the timerCount class and pass in the int for the current time.
This is in your Main class:
func updateMethodInMain(timeAsInt: Int){
//do stuff in Main instance based on timeAsInt
}
This is what you have in your timer class:
func update(){
if(time > 0) {
time = time - 1
instanceNameForMain.updateMethodInMain(time)
}
else{
timer.invalidate()
timer = nil
time = 5
}
}
}