swift - Delay a spawn function using NSTimer - swift

I'm looking to integrate some delayed functions into my game, currently I'm working on putting a 5 second delay on calling my first function as in this time period the game goes through a countdown to get the player ready.
Currently my countdown is displayed but the game is in full swing in the background. I've got one function I'd like to delay for 5 seconds but I'm also thinking of using the same method to incorporate other objects later in the game to boost difficulty.
The call I'm looking to delay is moving.addChild(crows)
If anyone can show me how to build that would be great as I've been unable to find on site (unless I've missed it).

Don't use NSTimer in SpriteKit. Use this instead:
let delay = SKAction.waitForDuration(5.0)
someNode.runAction(delay) {
//run code here after 5 secs
}

let delay = 5.0 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
moving.addChild(crows)
}

Related

Alternative for Timers

I am having a simple game with a player, a moving background and moving walls, kinda like flappy bird. My player is able to collect a powerUp. It is transforming after and put into a special level.
I want this level to run for like 10 seconds and then call a backtransformation.
Currently I am using a timer which just calls the backtransformation after 10 seconds. I am having trouble now when the player pauses the game, the timer is still running.
--> what I found on stackoverflow is that you can't resume a timer, you can just invalidate and restart it. This would miss my target, otherwise the player pauses the game every 9 seconds and can stay in the super Level forever.
Do you guys have any idea how I can solve my problem or like an alternative to use Timers in my code?
I appreciate any help
Edit: Here is how I simply used the timer
// transform and go into super level added here
self.transform()
timer = Timer.scheduledTimer(withTimeInterval:10, repeats: false) {
timer in
self.backtransform()
}
When you start the timer record the current time as a Double using
let start = Date().timeIntervalSinceReferenceDate
var remaining = 10.0
When the user pauses the timer, calculate the amount of time that has passed with:
let elapsed = Date().timeIntervalSinceReferenceDate - start
And the amount of remaining time for your timer:
remaining -= elapsed
If the user resumes the timer, set it to remaining, not 10 seconds.

Change vibrate duration on iPhone's without haptic feedback in Swift

I'm creating a simple game in Swift. If the time is almost over (5 seconds) I would like to vibrate the phone.
When 5 or 4 seconds left: normal vibration with AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) once per second.
Question 1:
When 3 or 2 seconds left: short vibration, twice per second.
When 1 second left, short vibration, thrice per second.
Question 2:
When 0 seconds left, vibrate for 2 or 3 seconds.
Is this possible? How can I change the duration of the vibration? I read online that increasing the duration of the vibration is not allowed, thats fine for me, but can I decrease the duration?
I have already checked How to make iPhone vibrate using Swift? but this question from 2014 did not cover Swift 3 or 4. If I try to play the sound 1519, 1520 or 1521 it just does nothing. The answer posted by Malmer only targets iPhone's with a haptic feedback. I want to find a solution for the iPhone 6s and lower, since they still run the newest iOS (11) and are able to run short vibrations when you're browsing through the iOS Settings for notification sounds.
In terms of vibrating the device, possibilities are very limited. As far as I know, this is the only method available (if you need to support all iPhones):
AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
From the docs: "function to invoke a brief vibration. On the iPod touch, does nothing.".
You would have to create som logic in order for this to repeat. One solution could be to use the NSTimer API. E.g. scheduledTimerWithTimeInterval.
Example implementation:
var gameTimer: Timer!
// Execute doTimedStuff method every 2 seconds
gameTimer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(doTimedStuff), userInfo: nil, repeats: true)
// Invalidate timer when done
gameTimer.invalidate()
One option is to use UIFeedbackGenerator to produce haptic feedback. Documentation Link
This allows you to choose from a few pre-set haptic vibrations, which you can fire at your desired time intervals.
Here is a quick example, although make sure you visit the documentation linked above for all the haptic feedback options and best practices.
let feedbackGenerator = UISelectionFeedbackGenerator()
feedbackGenerator.prepare()
feedbackGenerator.selectionChanged()
(Note that this only works on iPhone 7 and newer devices.)
Use .hapticContinuous event type with CHHapticEvent. You can customize the the time of vibration and intensity of vibrations.
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
var events = [CHHapticEvent]()
var relativeTimer = 0.0
let event1 = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness], relativeTime: relativeTimer, duration: longDuration)
relativeTimer += event1.duration
events.append(event1)
do {
let pattern = try CHHapticPattern(events: events, parameters: [])
let player = try engine.makePlayer(with: pattern)
try engine.start()
try player.start(atTime: 0)
} catch {
print("Haptic Error: \(error.localizedDescription).")
}

How to properly measure elapsed time in background in Swift

I have this two functions that measure the elapsed time when the phone is locked or the app is in background:
func saveTimeInBackground(){
startMeasureTime = Int(Date.timeIntervalSinceReferenceDate)
}
func timeOnAppActivated(){
stopMeasureTime = Int(Date.timeIntervalSinceReferenceDate)
elapsedTime = stopMeasureTime - startMeasureTime
seconds = seconds - elapsedTime + 2
if seconds > 0 {
timerLbl.text = "time: \(seconds)"
} else {
seconds = 0
timerLbl.text = "time: \(seconds)"
}
}
and then in the viewDidLoad() i have observers that are trigger the functions when the app becomes active/inactive:
NotificationCenter.default.addObserver(self, selector: #selector(saveTimeInBackground), name: Notification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(timeOnAppActivated), name: Notification.Name.UIApplicationDidBecomeActive, object: nil)
The problem is that when the app becomes active there are 2 seconds (approximately) of difference so i've added 2 seconds and it seems to work fine, but only if the elapsed time is > 15 seconds.
If i lock the phone and immediately unlock it the there are like 5 or more seconds that are missing. For example, if there are 50 seconds left, when i lock and immediately unlock it there are like 42 seconds left.
Can anyone please explain, what i am doing wrong?
Edit: The logic of the app is this:
It starts a match between 2 players with 60 seconds for a game. The problem is that when one of the players locks the phone the app stop to measure the time. This way if the player1 has 10 seconds left to make a move, the player2 still has 50 seconds left. I'm looking for a reliable way to calculate the time even if the player locks the phone or put the app in background.
Edit 2: I think i figured out what the problem is: I think the issue has to do with the fact that the “seconds” are Int, and the Date not and when it gets converted it’s rounded up. I didn't tested it, but when i ahve the solution i'll post the answer. Thanks all for your time!
You're relying on exact timing of notifications that aren't guaranteed to have any exact timing. There's no guarantee about when, exactly, either of those notifications will arrive, and there's nothing you can do about that. Even your two-second fix is, as you say, approximate. It'll probably be different on different models of iPhone or even at different times on the same iPhone, depending how busy iOS is when you check.
What's more, when you go into the background, you can't be certain that you'll stay there. Once in the background, iOS might decide to terminate your app at any time.
I'm not sure what the goal is here but I think you'll need to reconsider what you want to do and see if there's some other approach. Your current two-second hack will, at best, spawn a bunch of other hacks (like the 15 second threshold you mention) without ever being especially accurate. And then it'll probably all break in the next iOS update when some iOS change causes the timing to change.
I would use Date object to track game time.
func gameStart() {
gameStartDate = Date()
}
func timeOnAppActivated() {
let secondsLeft = 60 - abs(gameStartDate?.timeIntervalSinceNow ?? 0)
if secondsLeft > 0 {
timerLbl.text = "time: \(secondsLeft)"
} else {
timerLbl.text = "time: 0"
}
}
Ok, like I mention in the edit 2 of the question:
The first issue was because "seconds" is a Int and then it almost always gains or lose when converting it from Double.
But the main problem was that i had to invalidate the timer when the app enter in background and i didn't.
So now with invalidating the timer when the app gets the notification that will enter background and then starting it when it enter foreground everything works fine.
To test this properly call those methods on button click. It may be coz of delay in releasing some resources in background.

dispatch_after time triggers immediately

This is the first time I've used GCD, I'll admit, so sorry if I've been stupid. I have a dispatch_after command which acts as a handy delay for me.
My problem is that when I send
dispatch_after(500000000000, dispatch_get_main_queue()){
println("triggered") //or any other code
}
the closure is triggered immediately (e.g. I have tested this and "triggered" prints immediately). It should take longer right? Like 500 seconds longer.
Thanks :)
The first parameter of dispatch_after(_:_:_:) is not a delay, but a point in time. From the docs:
when: The temporal milestone returned by dispatch_time or dispatch_walltime.
Discussion
This function waits until the specified time and then asynchronously
adds block to the specified queue.
You need to construct a delay relative to the current time, using dispatch_time(_:_:):
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(500 * NSEC_PER_SEC))
dispatch_after(delayTime, dispatch_get_main_queue()) { ... }

Synchronize the playback of two or more AVAudioPlayer in Iphone

I need to play 2 sounds with 2 AVAudioPlayer objects at the same exact time... so I found this example on Apple AVAudioPlayer Class Reference (https://developer.apple.com/library/mac/#documentation/AVFoundation/Reference/AVAudioPlayerClassReference/Reference/Reference.html):
- (void) startSynchronizedPlayback {
NSTimeInterval shortStartDelay = 0.01; // seconds
NSTimeInterval now = player.deviceCurrentTime;
[player playAtTime: now + shortStartDelay];
[secondPlayer playAtTime: now + shortStartDelay];
// Here, update state and user interface for each player, as appropriate
}
What I don't understand is: why also the secondPlayer has the shorStartDelay?
Shouldn't it be without? I thought the first Player needed a 0.1 sec delay as it is called before the second Player... but in this code the 2 players have the delay...
Anyone can explain me if that is right and why?
Thanks a lot
Massy
If you only use the play method ([firstPlayer play];), firstPlayer will start before the second one as it will receive the call before.
If you set no delay ([firstPlayer playAtTime:now];), the firstPlayer will also start before de second one because firstPlayer will check the time at which it is supposed to start, and will see that it's already passed. Thus, it will have the same behaviour as when you use only the play method.
The delay is here to ensure that the two players start at the same time. It is supposed to be long enough to ensure that the two players receive the call before the 'now+delay' time has passed.
I don't know if I'm clear (English is not my native langage). I can try to be more clear if you have questions
Yeah what he said ^ The play at time will schedule both players to start at that time (sometime in the future).
To make it obvious, you can set "shortStartDelay" to 2 seconds and you will see there will be a two second pause before both items start playing.
Another tip to keep in mind here are that when you play/pause AVAudioPlayer they dont actually STOP at exactly the same time. So when you want to resume, you should also sync the audio tracks.
Swift example:
let currentDeviceTime = firstPlayer.deviceCurrentTime
let trackTime = firstPlayer.currentTime
players.forEach {
$0.currentTime = trackTime
$0.play(atTime: currentDeviceTime + 0.1)
}
Where players is a list of AVAudioPlayers and firstPlayer is the first item in the array.
Notice how I am also resetting the "currentTime" which is how many seconds into the audio track you want to keep playing. Otherwise every time the user plays/pauses the track they drift out of sync!