Swift - SpriteKit - Update SKAction.waitForDuration while running forever - swift

I am trying to update the waitForDuration action in a sequence that is running forever.
override func didMoveToView(view: SKView) {
//code
runAction(SKAction.repeatActionForever(runSeq()))
}
func runSeq() -> SKAction{
var difficulty: CGFloat = 0.75
let updateAction = SKAction.runBlock({
self.runCount++
self.runCount %= 4
println(self.runCount)
if self.runCount == 0 {
difficulty -= 0.1
}
if self.children.count > 51{
println("You loose")
}else{
self.scoreLabel.text = String(format: "Score: %i", self.score)
}
})
let createAntAction = SKAction.runBlock({self.createAnt()})
var wait = SKAction.waitForDuration(NSTimeInterval(difficulty))
let seq = SKAction.sequence([createAntAction, wait, updateAction])
return seq
}
difficulty is the value that I want, and every 4 loops I want it to decrease by a certain amount. Currently, the starting value remains the same, and doesn't change, even though I am changing the difficulty value, and recalling the sequence of actions every time.

This is happening because wait is already established as .75, and nothing is changing that. SKAction.waitForDuration(NSTimeInterval(difficulty)) is not looking for a reference to difficulty, only a value, so you need to rethink how you want to code this.

Related

Step Counter Sprite Kit

I am trying to implement a step counter in my sprite Kit game.
And it should work like this:
The counter adds 1 to a value each second.
Every fifth second the duration (in this case 1) gets divided by 1.1
But if I create a func that returns the new duration, the repeat forever SKAction only uses this value for one time and then the duration never changes again.
you should make an action that calls itself, rather than using SKAction.repeatForever(...). you can recalculate values that way. not sure i entirely understand your use case, but here is an example that fires an event after a duration, and modifies that duration every fifth cycle.
var isLoopEnabled:Bool = true
var counter:Int = 0
var duration:TimeInterval = 1.0
func updateDuration() {
duration /= 1.1
}
/*
creates an event loop. the action waits, fires, then calls itself again (before exiting)
turn the loop off using the isLoopEnabled flag
*/
func loop() {
let wait = SKAction.wait(forDuration: duration)
let run = SKAction.run {
self.counter += 1 //increment counter
//update duration every fifth count
if self.counter % 5 == 0 {
self.updateDuration()
}
}
let end = SKAction.run{
print("\(self.counter) -- duration: \(self.duration)")
guard self.isLoopEnabled else { return } //flag allows you to exit loop
self.loop() //repeats by calling itself
}
let sequence = SKAction.sequence([ wait, run, end ])
self.run(sequence, withKey:"loop action")
}
override func didMove(to view: SKView) {
loop()
}

How to prevent Timer slowing down in background

I am writing a Mac OS app in Swift and want to repeat a task every 0.5s (more or less, high precision is not required). This app should run in the background while I use other applications.
I'm currently using a Timer:
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true)
It starts fine with updates roughly every 0.5s but after some time in the background, the Timer slows down considerably to intervals of roughly 1s or 2s (it's very close to these values to it seems that the timer skips ticks or has a slowdown of a factor 2 or 4).
I suspect it's because the app is given a low priority after a few seconds in the background. Is there a way to avoid this? It can be either in the app settings in XCode by asking to stay active all the time, or possibly from the system when the app is run (or even but doing things differently without Timer but I'd rather keep it simple if possible).
Here is a minimal working example: the app only has a ViewController with this code
import Cocoa
class ViewController: NSViewController {
var lastUpdate = Date().timeIntervalSince1970
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) {
timer in
let now = Date().timeIntervalSince1970
print(now - self.lastUpdate)
self.lastUpdate = now
}
RunLoop.current.add(timer, forMode: .common)
}
}
Output at start is
0.5277011394500732
0.5008649826049805
0.5000109672546387
0.49898695945739746
0.5005381107330322
0.5005340576171875
0.5000457763671875
...
But after a few seconds in the background it becomes
0.49993896484375
0.49997520446777344
0.5000619888305664
1.5194149017333984
1.0009620189666748
0.9984869956970215
2.0002501010894775
2.001321792602539
1.9989290237426758
...
If I bring the app back to the foreground, the timer goes back to 0.5s increments.
Note: I'm running Mac OSX 10.15.5 (Catalina) on an iMac
This is because of the App Nap. You can disable App Nap but it is not recommended.
var activity: NSObjectProtocol?
activity = ProcessInfo().beginActivity(options: .userInitiatedAllowingIdleSystemSleep, reason: "Timer delay")
The default tolerance value of timer is zero but The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of tolerance property.
As I stated in my comment below, if you want granularities lower than 1.0 s, you should not use Timer objects, but rather GCD. I wrote a class MilliTimer you can use where you have improved granularity down to a few milliseconds. Please try this in a Playground and then in your app. In this example, I set the granularity of the timer based on GCD to 50 milliseconds. To adjust the delay pass the delay you want in milliseconds in the respective parameter of the initializer. In your case, you might be interested in 500 ms = 0.5 s.
import Cocoa
public class MilliTimer
{
static let µseconds = 1000000.0
static var lastUpdate = DispatchTime.now()
var delay = 0
var doStop = false
var runs = 0
let maxRuns = 50
private class func timer(_ milliseconds:Int, closure:#escaping ()->())
{
let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(milliseconds)
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
init(delay:Int) {
self.delay = delay
}
func delta() -> Double {
let now = DispatchTime.now()
let nowInMilliseconds = Double(now.uptimeNanoseconds) / MilliTimer.µseconds
let lastUpdateInMilliseconds = Double(MilliTimer.lastUpdate.uptimeNanoseconds) / MilliTimer.µseconds
let delta = nowInMilliseconds - lastUpdateInMilliseconds
MilliTimer.lastUpdate = now
return delta
}
func scheduleTimer()
{
MilliTimer.timer(delay) {
print(self.delta())
if self.doStop == false {
self.scheduleTimer()
self.runs += 1
if self.runs > self.maxRuns {
self.stop()
}
}
}
}
func stop() {
doStop = true
}
}
MilliTimer(delay: 50).scheduleTimer()
CFRunLoopRun()

Swift for iOS - 2 for loops run at the same time?

I have two objects where I need to update their UI at the same time. I have a for loop for one, and after that another for loop. Each iteration in the for loop I have a short delay so that for elements in the object I am making a UI change... one after the other - not seemingly all at once.
func update(value: Int){
var delay: Double = 0.05
// first loop
for i in 0...value {
delayWithSeconds(delay) {
//do something with object 1
}
delay = delay + 0.05
}
var delay2: Double = 0.05
// second loop
for i in 0...value {
delayWithSeconds(delay2) {
//do something with object 2
}
delay2 = delay2 + 0.05
}
}
// Utility
func delayWithSeconds(_ seconds: Double, completion: #escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
I have tried wrapping each for loop with DispatchQueue.main.async and it didn't make a difference. In short - I would like to run both for loops at the same time (or perceived as such). These are on the UI thread.
I tried this and it seemed to work out quite well. It does exactly what I want it to do (at least visually they seem to run at the same time).
let concurrentQueue = DispatchQueue(label: "net.ericd.hello", attributes: .concurrent)
concurrentQueue.async {
//my loop with delay here for object 1.
}
concurrentQueue.async {
//my separate loop with delay here for object 2.
}
We can use it when we want execute different arrays at the same time:
using this Generic Function
zip(_:_:)
Here i took 2 array:
var arrOfInt = ["1","2","3"]
var arrOfIntString = ["one","two","three"]
for (intNum, intString) in zip(arrOfInt, arrOfIntString) {
print("Int:\(intNum), String:\(intString)")
}

How to get different random delays in a SpriteKit sequence?

I have a sequence where i spawn a obstacle and then wait for a random amount of time, but if I run the game and for example the first random delay 1.4 seconds, but its not just for the first delay it's just all the time 1.4 and it doesn't change (it doesn't have to be 1.4 it's just an example). I have tried to make a function which has a random return value but its doesn't work. I have no idea how i could solve this. Here's my Code for the function with the random return value. If it helps obstSwitch() is the function that creates the Obstacle:
func getRandomDelay() ->Double {
let randomNumber = arc4random_uniform(20) + 5
let randomDelay: Double = Double(randomNumber) / 10
print(randomDelay)
return randomDelay
}
and heres the function that get's called when the game started:
func gameStarted() {
gameAbleToStart = false
moveLine()
scoreTimer()
let runObstSwitch = SKAction.run {
self.obstSwitch()
}
let wait = SKAction.wait(forDuration: getRandomDelay())
let sequence = SKAction.sequence([runObstSwitch, wait])
self.run(SKAction.repeatForever(sequence))
}
let wait = SKAction.wait(forDuration: getRandomDelay())
let sequence = SKAction.sequence([runObstSwitch, wait])
creates the wait action once, which is then used in the sequence,
so the same amount of idle time is spent between the runObstSwitch
actions.
If you want the idle time to be variable, use
wait(forDuration:withRange:) instead. For example with
let wait = SKAction.wait(forDuration: 1.5, withRange: 2.0)
let sequence = SKAction.sequence([runObstSwitch, wait])
the delay will be a random number between 1.5-2.0/2 = 0.5 and 1.5+2.0/2 = 2.5 seconds, varying for each execution.

How to gradually speed up falling nodes over time?

I have two dots that fall parallel to each other from the top of my screen that are to be matched with two circles on the bottom that can be rotated with touch. I have them generated like this:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(generateDots),
SKAction.waitForDuration(1.0)])))
}
func generateDots() {
let count = 2
let index=Int(arc4random_uniform(UInt32(count)))
let dots = SKSpriteNode(imageNamed: "Color\(index+1)")
dots.physicsBody = SKPhysicsBody(circleOfRadius: 10)
dots.physicsBody?.dynamic = true
dots.physicsBody?.affectedByGravity = false
for i in 0..<2 {
dots.physicsBody?.categoryBitMask = UInt32(0x1 << index)
dots.physicsBody?.contactTestBitMask = UInt32(0x1 << index)
}
addChild(dots)
dots.size = CGSizeMake(45, 45)
dots.position = CGPointMake(150, 400)
dots.runAction(
SKAction.moveByX(0, y: -900,
duration: NSTimeInterval(11.5)))
}
}
Is there any way to gradually speed up either how fast they're falling or gradually change the waitForDuration so that over time it will produce a node every 3 sec, then 2 sec, then 1 sec and so forth?
This is completely doable! You just need to add some variables.
If you want to change how fast they fall then you need to make a variable like
Var droptime:NSTimeInterval = 11.5
Then in your "dropdot()" method you need to do two things.
At the beginning subtract or devide your droptime variable like...
Droptime -= 1
Then at the end when you generate the falling action make it
Duration: droptime
Instead of what it was before.
If you want to make the generation time be shorter then you need to make a function that you can trigger each time you want to make your action that the scene runs (like you did in the viewdidload) and edit it so that it has variable wait and triggers itself. Also you will need to self trigger it once in your didMoveToView method.
func controlMethod() {
waitdur -= 1
runAction(SKAction.repeatActionForever( SKAction.sequence([
SKAction.runBlock(generateDots),
SKAction.waitForDuration(waitdur),
SKAction.runBlock(controlMethod)
])))
}
Good luck!
So sorry for the formatting! I'm on mobile... Hopefully someone can fix it.