Why is my SpriteKit update() function creating multiple NSTimers? - ios

I'm creating a SpriteKit game that updates based on the amount of time passed. The game spawns enemies using an NSTimer and its scheduledTimerWithTimeInterval method, calling the spawnEnemy function every 2.0 seconds.
When 5 seconds have passed there should be a very brief intermission, preventing new enemies from spawning in order to show a level change animation.
When the initial 5 seconds has been reached, everything works well up until the conditional where self.nextLevelDelayTicker == 100. Once this conditional is met, the "YOLO" string is only fired once in the console. However, I'm assuming multiple instances of NSTimer are being created and stored within self.timer since a massive amount of enemies are spawned after self.resumeGame() is called to create a new scheduled timer.
Any ideas on why this is happening even though I have flags set up within my conditional to only call the self.resumeGame() function once?
func resumeGame() {
// Start game timer
// Need a way to access ib action of pause button
self.timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: "spawnEnemy", userInfo: nil, repeats: true)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if gameTicker.isActive == true {
gameTicker.increment()
}
// If gameTicker is equal to 5 seconds, increase enemy amount
if gameTicker.getTimePassed() % 500 == 0 {
self.enemyAmount += 1
self.timer?.invalidate()
levelCount += 1
gameTicker.isActive = false
}
// If level has been completed and last ghost has been killed, activate next level scene
if gameTicker.isActive == false && enemyArray.count == 0 {
self.nextLevelDelayTicker.increment()
if self.nextLevelDelayTicker.getTimePassed() == 100 {
print("YOLO")
self.gameTicker.isActive = true
self.nextLevelDelayTicker.reset()
self.resumeGame()
}
}
}

Trying to follow your code.. but I think your approach here isn't great for spritekit. It's probably making things way more complicated than it needs to be.
You can keep track of time using your update method directly. It would probably be worth rewriting this part of your code. Would work better within spritekit and be less prone to bugs.
All you really need is delta time.
scene properties
// time values
var delta = NSTimeInterval(0)
var last_update_time = NSTimeInterval(0)
// some time youre trying to keep track of
var timeLimit = NSTimeInterval(5)
var timeLimitMax = NSTimeInterval(5)
your scene's update method
func update(currentTime: NSTimeInterval) {
if last_update_time == 0.0 {
delta = 0
} else {
delta = currentTime - last_update_time
}
last_update_date = currentTime
// now we can keep track of time
timeLimit -= self.delta
if timeLimit <= 0 {
// do something and reset timer
timeLimit = timeLimitMax
}
}
Now if you're going to be consistently spawning something every number of seconds then we dont even need to bother with update to do this. Just put this in your viewDidLoad
Now we're running this code every two seconds forever. The best part is this will pause and resume with your game automatically. You don't have to manage SKAction too much. spritekit does it for you :)
let spawnAction = SKAction.repeatActionForever(
SKAction.sequence([
SKAction.waitForDuration(2),
SKAction.runBlock({
[unowned self] in
self.spawnEnemy()
})
])
)
runAction(spawnAction)

Related

Using updates current time to keep track of game time

I am curious if anybody has been able to figure out a way to keep how long a scene has been active for. This is critical for when I need to fire certain events, like say "spawn ship 5 minutes into game play".
The issue with the update cycle, is that the current time being passed is not active game time only, so if you get a call or switch apps, upon return you will get that X minute jump in time.
Currently, I use an SKAction.customAction to keep track of my time, and it works, but I cannot guarantee the order in which actions fire, so my events could end up being 1 frame off.
Here is what I am doing now, please let me know what you have done to keep time consistent.
//Note: NodeComponent is a protocol extension to quickly access the GKSKNodeComponent's node
import GameplayKit
class ElapsedTimeComponent:GKComponent,NodeComponent
{
var elapsedTime : TimeInterval = 0.0
override func didAddToEntity() {
node.scene?.addComponentToComponentSystem(self)
self.node.run(SKAction.customAction(withDuration:330000000000000000000000000000000000000){node,seconds in
elapsedTime = seconds
(node as! SKLabelNode).text = "\(seconds)"
})
}
override func update(deltaTime seconds: TimeInterval) {
}
}
So it turns out, SKView has a delegate protocol you can attach anywhere. I have set up my latest test to look like this:
public class GameScene: SKScene,SKViewDelegate {
var previousTime : TimeInterval = 0
var gameTime : TimeInterval = 0
public func view(_ view: SKView, shouldRenderAtTime time: TimeInterval) -> Bool
{
if !self.isPaused{
gameTime += time - previousTime
}
print("GameTime: \(gameTime)")
componentSystems.forEach({$0.update(deltaTime: gameTime)})
self.previousTime = time
return !self.isPaused
}
public override func didMove(to view: SKView) {
view.delegate = self
}
}
https://developer.apple.com/documentation/spritekit/skviewdelegate
If I am understanding this correctly, then the only thing I need to look out for outside of this is ensuring that when returning from game, that the view.isPaused does not unpause my scene at the same exact time (ughhh why do you do this to me Apple) to allow for 1 render loop to occur.
Of course, the downside with this method, is any pause screen I create will have to be outside of this scene since rendering is disabled.

Multiple timers at once ios

I am making an app where the user can have multiple timers going at once and see them in a list view.
I am aware that there are 2 main options for working out time:
Subtract the date started from current date (current date-start date)
OR
Use an NSTimer and take away 1 second every second from each active timer.
I have previously been using the latter, but having looked around the internet I am starting to think that the data one may be better.
Please could you let me know which you think is best to use, and if you chose the first one (dates), please could you provide some sample code on how to use it.
You can Use an NSTimer and take away 1 second every second from each active timer. You can use this class.
class CustomTimer {
typealias Update = (Int)->Void
var timer:Timer?
var count: Int = 0
var update: Update?
init(update:#escaping Update){
self.update = update
}
func start(){
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerUpdate), userInfo: nil, repeats: true)
}
func stop(){
if let timer = timer {
timer.invalidate()
}
}
/**
* This method must be in the public or scope
*/
#objc func timerUpdate() {
count += 1;
if let update = update {
update(count)
}
}
}
To use multiple timer you can create multiple instance of CustomTimer, Example Code:
let timer1 = CustomTimer { (seconds) in
// do whatever you want
}
timer1.start()
let timer2 = CustomTimer { (seconds) in
// do whatever you want
}
timer2.start()
NOTE:
timerUpdate method will be called exactly at 1 second interval. to keep some space for function execution we can set interval to 0.9 or 0.95 according to time taken by execution.
You use both. You have one Timer that repeats every second. The handler for the Timer then iterates through your list of start dates for each of the user's timers and you update the display for each based on the current date.

iOS SpriteKit - countdown before game starts?

Just getting into SpriteKit on iOS using Swift. I have a 'Breakout' game from a tutorial and I want to implement a countdown before every ball launches, by putting an SKLabel in the middle of the screen which counts down from 5 to 1 before removing itself and the game starting. Whilst the countdown is going on, you can see the full games screen with the wall, stationary ball etc.
I can't work out where in the game loop to do this. If I do the countdown in didMoveToView (where I create the wall and initialise the ball and paddle), I never see it, but I see my debugging messages in the log. I guess that didMoveToView is called before the SKScene is presented.
I tried to use a flag to call the countdown function the first time 'Update' is called, but again I saw the countdown executed before anything appeared on screen - i think 'update' is called initially before the scene is rendered.
I could implement a 'Tap screen to start' screen in a another SKScene, but I really wanted a countdown on the screen with the wall and the (stationary) ball ready to go. I could create this countdown scene using a background image of the game screen, but this seems awkward.
Any suggestions gratefully received,
Steve
Added these functions and a call to countdown(5) at the end of didMoveToView
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))
}
So I create and set up the text of the countdown and then create and run an SKAction that waits for 1 second before decrementing the countdown and updating the label. It repeats this 5 times and then removes the countdown label before finally giving the ball an impulse to start it moving and so the game proper can start.
Seems to work ok...
This is where NSTimers really come in handy. An NSTimer basically activates a function every so often (you specify how often).
Update: Sometimes it's better not to use an NSTimer; see here: https://stackoverflow.com/a/24950982/5700898
Here's a code sample, using NSTimer:
class ViewController: UIViewController {
var countdownTime = 5
var countdownTimer = NSTimer()
// above, setting your timer and countdown time as global variables so they can be accessed in multiple functions.
func PlayerPressesStart() {
countdownTime = 5
countdownTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "Countdown", userInfo: nil, repeats: true) // calls the function "Countdown" every second.
}
func Countdown() { // this is the function called by the timer every second, which causes your "countdownTime" to go down by 1. When it reaches 0, it starts the game.
countdownTime--
if countdownTime > 0 {
countdownTimer.invalidate()
placewhereyoudisplaycountdown.text = String(countdownTime)
}
if countdownTime == 0 {
// call the function where game action begins here, or call the function that makes the game begin here.
}
} // closing bracket for the View Controller, don't include this if you're copying and pasting to your already existing View Controller.

Small lag/jitter when tapping the screen

So my game is almost complete... but there's this little glitch or jitter that occurs when I press and hold my finger on the screen which, now I've noticed, I can't un-notice...
It happens really fast, and only happens when a function is called to handle tap&holds (long press). This happens after 0.2seconds have passed using a timer.
I've tried breakpointing it to pin down where exactly the jitter happens in the code, but it seems I can not fine tune it enough to locate it.
My update method is typical:
override func update(currentTime: CFTimeInterval) {
//calc delta time
if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
} else {
dt = 0
}
lastUpdateTime = currentTime
//timer for handleLongPress
if touched {
longTouchTimer += dt
}
if longTouchTimer >= 0.2 && !canLongPressNow {
canLongPressNow = true
handleLongPress()
} else {
canLongPressNow = false
}
...
//switch GameSate
//switch CharacterState
}
My function to handleLongPress is this:
func handleLongPress() {
//switch gameState
//if in gameplay gamestate
if canLongPressNow {
//rotate player
//change character state
startPlayerAnimation_Sliding()
}
touched = false
longTouchTimer = 0
}
The startPlayerAnimation_Sliding() just iterates a texture array of the playerNode.
func startPlayerAnimation_Sliding() {
var textures: Array<SKTexture> = []
for i in 0..<KNumSlideFrames{
textures.append(SKTexture(imageNamed: "slide\(i)"))
}
let playerAnimation = SKAction.animateWithTextures(textures, timePerFrame: 0.3)
player.runAction(SKAction.repeatActionForever(playerAnimation), withKey: "sliding")
}
Is there anything noticeable that may be causing this?
update
I've removed this from my update(..) method, and it seems smooth again... and I have no idea why...? Maybe because it's removing a key (explosion) that hasn't been created yet? or the fact it's removing these keys every frame... Doesn't make sense though... But I'm calling it a night, and looking at this again tomorrow. Thanks for your help so far. Have a good evening. (will update tomorrow)
//for animations
switch characterState {
case .Running:
player.removeActionForKey("exploding")
player.removeActionForKey("sliding")
break
case .Sliding:
player.removeActionForKey("running")
player.removeActionForKey("exploding")
break
case .Exploding:
player.removeActionForKey("running")
player.removeActionForKey("sliding")
break
}
Yikes, how you create textures is what is slowing you down a lot, you are creating new textures every time a touch happens, this is not needed. Instead do:
var textures: Array<SKTexture> = []
var playerAnimation : SKAction?
func loadingPhase() //however this is defined for you
{
for i in 0..<KNumSlideFrames{
textures.append(SKTexture(imageNamed: "slide\(i)"))
}
playerAnimation = SKAction.repeatActionForever(SKAction.animateWithTextures(textures, timePerFrame: 0.3))
}
func startPlayerAnimation_Sliding() {
player.runAction(playerAnimation!, withKey: "sliding")
}

Swift - slowing down "too fast" animation (UIImage updates) -- aka is NSTimer the only option here?

Newbie to IOS programming - learning through Swift. I'm writing a simple "slot machine / dice game".
I'm trying to show the user a flashing sequence of rolls before the "actual" roll appears.
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
}
}
Die1, etc., are defined as generic UIImage views.
In any case, I'm not seeing the 100x iterations, just the images at the end of the loop. I'm assuming that either it redraws "too fast" or that IOS is trying to be smart, and only draws the last images so as to conserve resources.
I'll wildly guess that I need to either implement some kind of delay here, or, IOS needs to be told to explicitly draw out my images, and not try to outthink my intent.
For the delay, I've seen something about NSTimer, but nothing I saw seems to simply say something like "pause for .05" second, and the whole construct was unclear as they were ObjC examples/conversions.
(Note: I've simplified things here --- normally, I would store the value of RollOne() so I can use it later. I also would like to make an array (or collection?) like Die[1].image, but that is another question.)
========== Edit =======
OK, so I'm following up with more of my original code, merged in with that of #doctordoder so we can discuss a bit better. Hopefully that is kosher. (If this appended question is too long, please advise me on the best way to post a lengthy follow-up directly.)
import UIKit
class ViewController: UIViewController {
//( stripping out stuff unneeded for discussion )
// refers to same label below - works but kosher??
#IBOutlet var btnRoll_x: UIView
#IBAction func btnRoll(sender: AnyObject) {
triggerRoll()
}
var timer : NSTimer? = nil
var rolls : Int = 0
func triggerRoll() {
//hiding is bad UI, but until i know how to "disable & dim"
btnRoll_x.hidden = true
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "doFancyDiceRoll", userInfo: nil, repeats: true);
}
func doFancyDiceRoll() {
Die1.image = PipsImg[randomInt(6)]
Die2.image = PipsImg[randomInt(6)]
Die3.image = PipsImg[randomInt(6)]
if (++rolls > 10)
{
timer?.invalidate()
timer = nil
rolls = 0 // DCB added this presumed missing line
btnRoll_x.hidden = false //again, need to do this better
}
}
}
Hopefully, my implementation of the code is what would have been intended. I made some minor adjustments for (hopeful) clarity.
Good news is I have working code. I have just enough understanding to get in place, but I'm fuzzy on some details.
Here is what I (think I) know...
We declare an NSTImer object, and a roll counter at the main level of the class. I note that in my original version, I had the roll counter scoped within the rolling function itself. Took me a while to understand why it could not live in the DiceRoll loop itself, but now I do. I'm going to express it poorly, but since the timer is going to call DiceRoll multiple instances, it needs to live outside the function.
The button btnRoll gets touched, and invokes triggerRoll().
To prevent the user from touching the button while we are in progress, which put us into a state where the roll counter never got to zero, I hide the button. (I'll figure how to properly put in in disabled state later.)
The timer is set. It fires every .1 second (within limits), and is set to repeat. (until .invalidate ). And it "calls" the function doFancyDiceRoll via the selector: attribute.
So, the big change as previously noted is that doFancy..Roll() no longer loops. It excites a single instance up updating the images. It checks the counter, and if we reach the limit, we kill the timer, which stops the timer (invalidate). (And I unhide the button, making it available again.)
So, a few things I am concerned/wondering about: I get the value of timers for other things that need to happen periodically (take health away every second, check a GPS position every 10 seconds, etc.). It's seems a odd construct to force a screen refresh.
Frankly, I would have expected to see see something like this:
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()] // and 2 and 3 of course.....
VIewController.forceRedraw <<=== something like this, or maybe
ViewController.wait(.05) <<== this?? I dunno ;-)
}
}
instead we end up with about 20 extra lines or so. I'd be interested in knowing if there other approaches that could work keeping the loop intact.
Anyway, assuming this is the one true way to go, I guess my followup to this is how do I pass parameters, since this is not a "real" function call. Trying
selector: "doFancyDiceRoll(40)"
was not objected to by the IDE, but failed in execution.
I had exactly same problem back in days, entire loop is finished before the view is refreshed as #doctordoder mentioned. Solved with using NSTimer
var rollCount = 0
let arrayOfImages = ["image01", "image02", "image03"]
var timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("doFancyDiceRoll"), userInfo: nil, repeats: true)
func doFancyDiceRoll() {
if rollCount == 100 {
timer.invalidate
rollCount = 0
{
else {
//get images from array arrayOfImages[rollCount]
rollCount++
}
}
there could be typos, since I have no Xcode right now.
I have basically the same answer as above :(, but I thought I'd post it anyway.
var timer : NSTimer? = nil
var rolls : Int = 0
func doFancyDiceRoll() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "roll", userInfo: nil, repeats: true);
}
func roll() {
println("rolling")
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
if (++rolls > 100)
{
timer?.invalidate()
timer = nil
}
}
Rather than NSTimer and invalidating, you can use dispatch_after to do the work for you.
func rollDice(howManyTimes: Int) {
die1.image = PipsImg[RollOne()]
die2.image = PipsImg[RollOne()]
die3.image = PipsImg[RollOne()]
if howManyTimes > 0 {
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) / 10.0))
dispatch_after(delayTime, dispatch_get_main_queue()) {
self.rollDice(howManyTimes - 1)
}
}
}
This will run the code for the number of times specified, delaying each time by 0.1 seconds. It works like this: First it sets the images on each die, then, if there are more iterations, it does a dispatch_after to call itself with rollDice(howManyTimes - 1)
With this, you don't need to maintain a NSTimer and it is pretty self contained.

Resources