I'm trying to set up a group of 16 nodes to shoot particles in sequences to form patterns.
I'm able to activate them all at once, with different particle systems on different nodes, but I can't get the nodes to activate in a sequential fashion.
The particle system itself is already looping active and inactive.
Maybe this is the wrong approach and I should use Actions, but I don't know.
There's a while (self.fWorksequence == 2) calling the Sequence2(), that does activate the particles on the nodes that I need, but it's all at the same time. I need to have them lag: Node f1 fires, wait 1 second, nodes f2 and f16 fire simultaneously, wait another second, nodes f3 and f15 fire simultaneously.
var fWCycle = 1
func Sequence2() {
if self.fWCycle == 1 {
sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
if node.name == "f1" {
let fWorkBoom = self.launchFWork1(color: UIColor.red)
node.addParticleSystem(fWorkBoom)
}
}
} else if self.fWCycle == 31 {
sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
if node.name == "f2" {
let fWorkBoom = self.launchFWork1(color: UIColor.red)
node.addParticleSystem(fWorkBoom)
}
if node.name == "f16" {
let fWorkBoom = self.launchFWork1(color: UIColor.red)
node.addParticleSystem(fWorkBoom)
}
}
}
...
//check the cycle stage
if self.fWCycle <= 100 {
self.fWCycle += 1
} else if self.fWCycle > 600 {
self.fWorkSequence = 1
}
}
*Edit
tried with
func Sequence2() {
timer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
}
and then
var timer = Timer()
var nodeCounter = 0
#objc func timerAction() {
self.nodeCounter += 1
if self.nodeCounter == 1 {
self.sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
if node.name == "f1" {
let fWorkBoom = self.launchFWork1(color: UIColor.red)
node.addParticleSystem(fWorkBoom)
}
}
}...
}
And now the memory crashes the app...
I couldn't figure out how to do it with code, tried Timers and Loops and either got nothing to work, or constant crashes due to memory issues.
So I ended up solving by animating the whole thing in Blender and then importing that file with all the correct times to xcode. Worked perfectly.
If anyone finds this in the future, I would still like to know how to do it by code.
Thank you!
Related
I need to set a specific timer asynchronously after executing an action like this:
calling my function (sending http request)
10 seconds after, sending another request
20 seconds after 2), sending another one
40 seconds after 3), another one
then send every 60 seconds another one
At any moment, I must be able to cancel my timer. Firstable I thought using DispatchQueue, but I see several post saying that it's not possible to cancel it.
Some post suggest to use DispatchWorkItem ( how to stop a dispatchQueue in swift ) but I'm not sur it fit my need (unless adding a sleep(10,20,40,60...) in each loop but will it not impact asynchronous part?).
Another answer from this post suggest to use Timer instead ( scheduledTimerWithTimeInterval ) with repeats:false, and invalidate it after each loop, but I didn't undertand how to do the loop in this case. Actually, here's my code, that just send a request after 10 seconds:
private func start() {
timer?.invalidate()
if(self.PCount > self.Intervals.count){
self.value = self.pollingIntervals.count-1
} else {
self.Value = self.Intervals[self.pCount]
}
print("set timer with \(pollingValue) as interval")
timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(pollingValue), repeats: false, block: { timer in
self.sessionManager.sendHit()
self.pollingCount+=1
})
}
The current goal is to do something like coroutine in Kotlin, like it work with this code :
private val Intervals = longArrayOf(10000,20000,40000,60000)
private var Count = 0
private fun start() {
currentJob = GlobalScope.launch {
while (true) {
delay(Intervals[if (Count > Intervals.size) Intervals.size - 1 else Count]) // 10,20,40 then every 60
session.sendHit()
pollingCount++
}
}
}
I'm not sure what solution is the most appropriate to my project
Here is a basic idea on how to approach the problem
struct RequestMananger {
var timers: [Timer] = []
mutating func startSequence() {
var delay = 10.0
sendRequest()
timers.append(scheduleTimer(delay))
delay += 20
timers.append(scheduleTimer(delay))
delay += 40
timers.append(scheduleTimer(delay))
delay += 60
timers.append(scheduleTimer(delay, repeats: true))
}
private func scheduleTimer(_ delay: TimeInterval, repeats: Bool = false) -> Timer {
return Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { timer in
self.sendRequest()
})
}
func sendRequest() {
}
func cancelTimers() {
timers.forEach { $0.invalidate() }
}
}
I'm still pretty new to coding and Swift. So bear with me.
Problem Statement : I've got a stopwatch style app that has two concurrent timers start at the same time and display in a mm:ss.SS format, but one is designed to reset to 0 at specific intervals automatically while the other keeps going and tracks total time.
Similar to a "lap" function but it does it automatically. The problem I've encountered is that occasionally the timers aren't perfectly synced up when the user pauses the timers. Since the reset happens at an exact second, both timers should have identical hundredths of a second, while the seconds and minutes will obviously be different. But sometimes the hundredths will be off by .01 or more.
Now, I know Timer isn't designed to be perfectly accurate, and in practice on my app this isn't even a huge deal. My timer doesn't even need to be accurate to the hundredth of a second, and while running it's not noticeably off at all, only while paused. I could display fewer decimal places or none at all, but I prefer the style of showing the hundredths since it fits in well with the stock timer app style.
So if there's a way to make this work, I'd like to keep it.
Screenshot : screenshot
What I tried :
#IBAction func playPauseTapped(_ sender: Any) {
if timerState == .new {
//start new timer
startCurrentTimer()
startTotalTimer()
currentStartTime = Date.timeIntervalSinceReferenceDate
totalStartTime = Date.timeIntervalSinceReferenceDate
timerState = .running
//some ui updates
} else if timerState == .running {
//pause timer
totalTimer.invalidate()
currentTimer.invalidate()
timerState = .paused
pausedTime = Date()
//other ui updates
} else if timerState == .paused {
//resume paused timer
let pausedInterval = Date().timeIntervalSince(pausedTime!)
pausedIntervals.append(pausedInterval)
pausedIntervalsCurrent.append(pausedInterval)
pausedTime = nil
startCurrentTimer()
startTotalTimer()
timerState = .running
//other ui updates
}
}
func startTotalTimer() {
totalTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(runTotalTimer), userInfo: nil, repeats: true)
}
func startCurrentTimer() {
currentTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(runCurrentTimer), userInfo: nil, repeats: true)
}
func resetCurrentTimer() {
currentTimer.invalidate()
currentStartTime = Date.timeIntervalSinceReferenceDate
pausedIntervalsCurrent.removeAll()
startCurrentTimer()
}
#objc func runCurrentTimer() {
let currentTime = Date.timeIntervalSinceReferenceDate
//calculate total paused time
var pausedSeconds = pausedIntervalsCurrent.reduce(0) { $0 + $1 }
if let pausedTime = pausedTime {
pausedSeconds += Date().timeIntervalSince(pausedTime)
}
let currentElapsedTime: TimeInterval = currentTime - currentStartTime - pausedSeconds
currentStepTimeLabel.text = format(time: currentElapsedTime)
if currentElapsedTime >= recipeInterval {
if recipeIndex < recipeTime.count - 1 {
recipeIndex += 1
//ui updates
//reset timer to 0
resetCurrentTimer()
} else {
//last step
currentTimer.invalidate()
}
}
}
#objc func runTotalTimer() {
let currentTime = Date.timeIntervalSinceReferenceDate
//calculate total paused time
var pausedSeconds = pausedIntervals.reduce(0) { $0 + $1 }
if let pausedTime = pausedTime {
pausedSeconds += Date().timeIntervalSince(pausedTime)
}
let totalElapsedTime: TimeInterval = currentTime - totalStartTime - pausedSeconds
totalTimeLabel.text = format(time: totalElapsedTime)
if totalElapsedTime >= recipeTotalTime {
totalTimer.invalidate()
currentTimer.invalidate()
//ui updates
}
}
func format(time: TimeInterval) -> String {
//formats TimeInterval into mm:ss.SS
let formater = DateFormatter()
formater.dateFormat = "mm:ss.SS"
let date = Date(timeIntervalSinceReferenceDate: time)
return formater.string(from: date)
}
You should use a single timer. And when you need a reset to zero, save the current time to a variable.
When presenting the time in the UI, calculate the difference between the running total timer, and the time you saved previously.
I have an app that does a countdown with a Timer. The countdown tracks multiple steps (all at the same intervals) as well as the total time left, and updates 2 separate UILabels accordingly. Occasionally, the labels will be out of sync.
I can't say for sure, but I think it might be only happening when I pause the countdown sometimes, and usually on steps later than the first step. It's most apparent on the last step when the two labels should be displaying the same exact thing, but will sometimes be 1 second off.
The other tricky thing is that sometimes pausing and resuming after the time has gone out of sync will get it back in sync.
My guess is I'm getting something weird happening in the pause code and/or the moving between steps, or maybe the calculating and formatting of TimeIntervals. Also I'm using rounded() on the calculated TimeIntervals because I noticed only updating the timer every 1s the labels would freeze and skip seconds a lot. But I'm unsure if that's the best way to solve this problem.
Here's the relevant code. (still need to work on refactoring but hopefully it's easy to follow, I'm still a beginner)
#IBAction func playPauseTapped(_ sender: Any) {
if timerState == .running {
//pause timer
pauseAnimation()
timer.invalidate()
timerState = .paused
pausedTime = Date()
playPauseButton.setImage(UIImage(systemName: "play.circle"), for: .normal)
} else if timerState == .paused {
//resume paused timer
guard let pause = pausedTime else { return }
let pausedInterval = Date().timeIntervalSince(pause)
startTime = startTime?.addingTimeInterval(pausedInterval)
endTime = endTime?.addingTimeInterval(pausedInterval)
currentStepEndTime = currentStepEndTime?.addingTimeInterval(pausedInterval)
pausedTime = nil
startTimer()
resumeAnimation()
timerState = .running
playPauseButton.setImage(UIImage(systemName: "pause.circle"), for: .normal)
} else {
//first run of brand new timer
startTimer()
startProgressBar()
startTime = Date()
if let totalTime = totalTime {
endTime = startTime?.addingTimeInterval(totalTime)
}
currentStepEndTime = Date().addingTimeInterval(recipeInterval)
timerState = .running
playPauseButton.setImage(UIImage(systemName: "pause.circle"), for: .normal)
currentWater += recipeWater[recipeIndex]
currentWeightLabel.text = "\(currentWater)g"
}
}
func startTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
}
#objc func runTimer() {
let currentTime = Date()
guard let totalTimeLeft = endTime?.timeIntervalSince(currentTime).rounded() else { return }
guard let currentInterval = currentStepEndTime?.timeIntervalSince(currentTime).rounded() else { return }
//end of current step
if currentInterval <= 0 {
//check if end of recipe
if recipeIndex < recipeWater.count - 1 {
//move to next step
totalTimeLabel.text = totalTimeLeft.stringFromTimeInterval()
currentStepEndTime = Date().addingTimeInterval(recipeInterval)
startProgressBar()
currentStepTimeLabel.text = recipeInterval.stringFromTimeInterval()
stepsTime += recipeInterval
recipeIndex += 1
//update some ui
} else {
//last step
currentStepTimeLabel.text = "00:00"
totalTimeLabel.text = "00:00"
timer.invalidate()
//alert controller saying finished
}
} else {
//update time labels
currentStepTimeLabel.text = currentInterval.stringFromTimeInterval()
totalTimeLabel.text = totalTimeLeft.stringFromTimeInterval()
}
}
extension TimeInterval {
func stringFromTimeInterval() -> String {
let time = NSInteger(self)
let seconds = time % 60
let minutes = (time / 60) % 60
return String(format: "%0.2d:%0.2d",minutes,seconds)
}
}
EDIT UPDATE: I tried a few different things but still kept having the same issue. I started testing with printing the TimeInterval and the formatted string to compare and see what's off. It's definitely some sort of rounding error.
Total - 173.50678288936615 / 02:54
Step - 39.00026595592499 / 00:39
Total - 172.5073879957199 / 02:53
Step - 38.00087106227875 / 00:38
Total - 171.1903439760208 / 02:51
Step - 36.68382704257965 / 00:37
Total - 170.19031596183777 / 02:50
Step - 35.683799028396606 / 00:36
As you can see, the total time skips from 2:53 to 2:51, but the step timer remains consistent. The reason is the TimeInterval for total goes from 172.5 which gets rounded up, to 171.19 which gets rounded down.
I've also watched the timer count down without touching pause, and it remains in sync reliably. So I've narrowed it down to my pause code.
Fixed my issue and posting here for posterity. I ended up making my totalTimeLeft and currentInterval global properties. Then, on pause and resume, instead of tracking the paused time and adding it to endTime, I just used the totalTimeLeft and currentInterval values that are still stored from the last Timer firing and doing endTime = Date().addingTimeInterval(totalTimeLeft) and the same with the interval time. This got rid of the paused time adding weird amounts that would mess up the rounding.
So I've got an NSTimer running a function every 0.1 seconds.
No matter how many conditions & caveats I put in place, this sound (and others on the timer) play twice. The same sound triggered by a touch plays only once.
myTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("flip:"), userInfo: nil, repeats: true)
Here's the function it triggers.
func flip(timer: NSTimer) {
if current_timer == -3.0 && rang_3 == false {
if rang_3 == false {
rang_3 = true
dialed_3()
NSLog("DIALED 3")
}
}
}
And here's the sound function I need triggered only ONCE.
func dialed_3() {
if sound_mute == false {
do {
dial_3 = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("3", ofType: "mp3")!))
dial_3.volume = 2
dial_3.numberOfLoops = 0
dial_3.prepareToPlay()
dial_3.play()
} catch {
print("Error")
}
}
}
I don't know about your other code, but with what you show, it runs 1 time.
A small suggestion for you: move initialization to global without run it everytime timer run.
Alright! So I moved the !rang_3 check to the fixed !sound_mute check & it worked. As a failsafe I replicated the sounds that are used during countdown & muted them after 1 play. So if they play twice, the user won't notice.
if !sound_mute && !rang_3 {
do {
//soundstuff
}
}
func updateTimer() {
if Score < 10 {
runAction(SKAction.repeatActionForever(SKAction.sequence([SKAction.runBlock(addEnemy), SKAction.waitForDuration(1.0)])))
}else if Score == 10 {
runAction(SKAction.repeatActionForever(SKAction.sequence([SKAction.runBlock(addEnemy), SKAction.waitForDuration(0.5)])))
}else if Score == 20 {
runAction(SKAction.repeatActionForever(SKAction.sequence([SKAction.runBlock(addEnemy), SKAction.waitForDuration(0.1)])))
}
}
i was trying to make the spawn faster when a player reach a certain score but this code has its error can anyone help me with this please
thank you very much to whoever answers my question
i wanted to make it like this one
func updateTimer() {
if score < 3 {
timerDots = NSTimer.scheduledTimerWithTimeInterval(1.5, target: self, selector: "fireDot", userInfo: nil, repeats: true)
// Dots get generated a bit faster than before after score reaches 3
} else if score == 3 {
timerDots?.invalidate()
timerDots = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "fireDot", userInfo: nil, repeats: true)
// Dots get generated even faster than before after score reaches 12
} else if score == 12 {
timerDots?.invalidate()
timerDots = NSTimer.scheduledTimerWithTimeInterval(0.9, target: self, selector: "fireDot", userInfo: nil, repeats: true)
}
}
It's simpler to do this using an instance variable and the SKScene update: method:
private var score: Int = 0
private var priorEnemyAddTime: CFTimeInterval = 0
private var nextEnemyAddTime: CFTimeInterval {
var waitTime = score < 10 ? 1.0
: score < 20 ? 0.5
: 0.1
return priorEnemyAddTime + waitTime
}
override func update(currentTime: CFTimeInterval) {
if nextEnemyAddTime <= currentTime {
priorEnemyAddTime = currentTime
addEnemy()
}
}
On every frame, you check whether it's been long enough since the last time you added an enemy. If so, you save the current time as the time of last add, and add an enemy.