I'm trying to change the timeInterval in a scheduledTimer. I'm trying to do this by changing a variable to the interval and than setting the timeInterval to this variable. I don't get any errors but the timeInterval won't change. Can someone help me?
var enemyTimer = Timer()
var playTime = 0
var enemySpawnTime: Double = 3
enemyTimer = Timer.scheduledTimer(timeInterval: Double(enemySpawnTime), target: self, selector: #selector(GameScene.enemySpawn), userInfo: nil, repeats: true)
playTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(GameScene.ingameTimer), userInfo: nil, repeats: true)
func enemySpawn() {
let enemy = SKSpriteNode(imageNamed: "Enemy")
let minValue = self.size.width / 8
let maxValue = self.size.width - 20
let spawnPoint = UInt32(maxValue - minValue)
enemy.position = CGPoint(x: CGFloat(arc4random_uniform(spawnPoint)), y: self.size.height)
let action = SKAction.moveTo(y: -70, duration: 5)
enemy.run(action)
self.addChild(enemy)
}
func ingameTimer() {
playTime += 1
if(playTime >= 10 && playTime < 30){
enemySpawnTime = 2
print(enemySpawnTime)
}else
if(playTime >= 30 && playTime < 60){
enemySpawnTime = 1
print(enemySpawnTime)
}else
if(playTime >= 60 && playTime < 120){
enemySpawnTime = 0.75
print(enemySpawnTime)
}else
if(playTime >= 120 && playTime < 180){
enemySpawnTime = 0.5
print(enemySpawnTime)
}else
if(playTime >= 180 && playTime < 240){
enemySpawnTime = 0.25
print(enemySpawnTime)
}
}
I hope someone can help me!
Thanks!
The reason why your code doesn't work is because the Timer object doesn't know that its interval needs to be in sync with your enemySpawnTime. The solution is simple, just recreate the timer when you change the enemy spawn time.
But...
You should NEVER use Timer (NSTimer prior to Swift 3) or GCD to delay stuff when you're using SpriteKit. See this for more details.
The correct way to do this is to create a sequence of SKActions.
Assuming self is a subclass of SKScene, you can do this:
override func didMove(to view: SKView) {
let waitAction = SKAction.wait(forDuration: enemySpawnTime)
let enemySpawnAction = SKAction.run { self.enemySpawn() }
let sequence = SKAction.sequence([waitAction, enemySpawnAction])
somePlaceholderNode.run(SKAction.repeatForever(sequence))
}
where somePlaceholderNode is just a node that does nothing but run the action. I'll explain this later.
And you should do this for the other timer as well.
Now whenever you change the timer interval, also do this:
somePlaceholderNode.removeAllActions()
let waitAction = SKAction.wait(forDuration: enemySpawnTime)
let enemySpawnAction = SKAction.run { self.enemySpawn() }
let sequence = SKAction.sequence([waitAction, enemySpawnAction])
somePlaceholderNode.run(SKAction.repeatForever(sequence))
Here I first remove the action that the node was running, and tell it to run almost the same action, but with a different time interval. You can add this block of code to the didSet block of enemySpawnTime:
var enemySpawnTime: Double = 3 {
didSet {
let waitAction = SKAction.wait(forDuration: enemySpawnTime)
let enemySpawnAction = SKAction.run { self.enemySpawn() }
let sequence = SKAction.sequence([waitAction, enemySpawnAction])
somePlaceholderNode.run(SKAction.repeatForever(sequence))
}
}
Now your code should work!
The reason why we want a placeholder node here is because when we remove the action by calling removeAllActions, we don't want to remove all the actions that is running.
Related
I have 2 counting labels
i.e. label1 and label 2
I want to start count from 0 to label count value in fixed duration.
I have tried using timer, but labels are not set in same time.
Code I have tried
let label1Value = 20
let label2Value = 30
var label1Count = 0
var label2Count = 0
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
if label1Count > label1Value && label2Count > label2Value {
timer.invalidate()
}
if label1Count <= label1Value {
label1.text = label1Count
label1Count += 1
}
if label2Count <= label2Value {
label2.text = label2Count
label2Count += 1
}
}
Help me if anything present to make counting labels same time in fixed duration
I don't see a way to do this with one Timer. Use a separate scheduledTimer object for each label.
let label1Value = 20.0
let label2Value = 30.0
var label1Count = 0.0
var label2Count = 0.0
let duration = 10.0 //This is the duration u want to finish within
let timer1 = Timer.scheduledTimer(withTimeInterval: duration/label1Value, repeats: true) {t in
if self.label1Count <= self.label1Value {
self.l1.text = "\(Int(self.label1Count))"
self.label1Count += 1
}
}
let timer2 = Timer.scheduledTimer(withTimeInterval: duration/label2Value, repeats: true) {t in
if self.label2Count <= self.label2Value {
self.l2.text = "\(Int(self.label2Count))"
self.label2Count += 1
}
}
I am working on a stopwatch app that begins flashing when approaching limit and again when overtime. Everything works great except for when the app is pushed to the background and then brought to the foreground again. At that time the animation will not start.
I have tried moving the code to applicationWillEnterForeground and applicationDidBecomeActive but it produces a fatal error:
unexpectedly found nil while unwrapping an Optional value
Am I missing something basic here?
There is more to the code of course (calculations, etc.), but this is relevant section. (Edit: I've expanded this to provide a better view of what I'm doing.)
#IBAction func start(_ sender: AnyObject) {
if timerOn == 0 {
myTimer = Timer.scheduledTimer(timeInterval: 0.1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate
timerOn = 1
redOrYellow = "white"
startButton.superview?.sendSubview(toBack: startButton)
background.superview?.sendSubview(toBack: background)
pickerMask.isHidden = false
}
}
....
func animateLight() {
if redOrYellow == "red" {
print("red")
//background.superview?.sendSubview(toBack: background)
UIView.animate(withDuration: 0.3, delay: 0.01, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.allowUserInteraction], animations: {
self.background.backgroundColor = UIColor.red
self.background.backgroundColor = UIColor.white
}, completion: nil)
} else if redOrYellow == "yellow" {
print("yellow")
//background.superview?.sendSubview(toBack: background)
UIView.animate(withDuration: 1, delay: 0.1, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.allowUserInteraction], animations: {
self.background.backgroundColor = UIColor.yellow
self.background.backgroundColor = UIColor.white
}, completion: nil)
}
}
func updateCounter() {
let currentTime = NSDate.timeIntervalSinceReferenceDate
var elapsedTime: TimeInterval = currentTime - startTime
totalTime = elapsedTime
if totalTime >= partBaseTimes[numberOfSelectedPart] {
if redOrYellow != "red" {
redOrYellow = "red"
animateLight()
}
} else if totalTime >= (partBaseTimes[numberOfSelectedPart] - 30) {
if redOrYellow != "yellow" {
redOrYellow = "yellow"
animateLight()
}
}
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (TimeInterval(minutes) * 60)
let seconds = UInt8(elapsedTime)
elapsedTime -= TimeInterval(seconds)
let fractionOfSecond = UInt8(elapsedTime * 10)
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
let strFractionOfSecond = String(format: "%01d", fractionOfSecond)
talkTimer.text = "\(strMinutes):\(strSeconds).\(strFractionOfSecond)"
}
The error that I keep getting is this one: Attemped to add a SKNode which already has a parent. I have tried to take out the spawn function and it works without it. Can someone please help me ?
func spawnTrolls(){
let minValue = self.size.width / 10
let maxValue = self.size.width - 30
let spawnpoint = UInt32(maxValue - minValue)
Troll.position = CGPointMake(CGFloat(arc4random_uniform(spawnpoint)), self.size.height)
self.addChild(Troll)
let action = SKAction.moveToY(-70, duration: 2)
let actionDone = SKAction.removeFromParent()
Troll.runAction(SKAction.sequence([action, actionDone]))
Troll.physicsBody? = SKPhysicsBody(rectangleOfSize: Troll.size)
Troll.physicsBody?.categoryBitMask = physicsCategory.Troll
Troll.physicsBody?.contactTestBitMask = physicsCategory.Rocket
Troll.physicsBody?.affectedByGravity = false
Troll.physicsBody?.dynamic = true
}
_ = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("spawnTrolls"), userInfo: nil, repeats: true)
I have a game that I am making where I have a player and an unlimited number of enemies that's spawn at a given position in a certain amount of time. These enemies are shooting bullets at the player. I need to be able to access the Enemy variable outside the SpawnEnemies() function so that I can use it in my SpawnBullets() function. I tried declaring the Enemy variable outside the SpawnEnemies() function but it returned Sigabrt and I don't know how to access the Enemy variable outside the function without getting this error.
Enemy declaration:
var Enemy = SKSpriteNode(imageNamed: "Enemy.png")
SpawnEnemies function:
func SpawnEnemies() {
let MinValue = self.size.width/8
let MaxValue = self.size.width-20
let SpawnPoint = UInt32(MaxValue-MinValue)
self.Enemy.position = CGPoint(x: CGFloat(arc4random_uniform(SpawnPoint)), y: self.size.height)
let action = SKAction.moveToY(-70, duration: 3.0)
self.Enemy.runAction(SKAction.repeatActionForever(action))
self.addChild(Enemy)
}
SpawnBullets function:
func SpawnBullets(){
let Bullet = SKSpriteNode(imageNamed: "bullet.png")
Bullet.zPosition = -5
Bullet.position = CGPointMake(Enemy.position.x, Enemy.position.y)
let action = SKAction.moveToY(self.size.height + 30, duration: 1.0)
Bullet.runAction(SKAction.repeatActionForever(action))
self.addChild(Bullet)
}
Call SpawnEnemies function in didMoveToView():
var EnemyTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("SpawnEnemies"),userInfo: nil, repeats: true)
Call SpawnBullets function in didMoveToView():
var BulletTimer = NSTimer.scheduledTimerWithTimeInterval(0.2, target: self, selector: Selector("SpawnBullets"),userInfo: nil, repeats: true)
I get this error which is a sigabrt but I used a breakpoint to figure out exactly where the error was and what it was:
Attemped to add a SKNode which already has a parent: <SKSpriteNode> name:'(null)' texture:[<SKTexture> 'Enemy.png' (60 x 80)] position:{132, 1024} scale:{1.00, 1.00} size:{60, 80} anchor:{0.5, 0.5} rotation:0.00
Adding children to a scene already is "infinite", you just need to use it to your advantage.
First rework your bullet function like this so that you are passing in an enemy.
func SpawnBullets(enemy : SKSpriteNode){
let Bullet = SKSpriteNode(imageNamed: "bullet.png")
Bullet.zPosition = -5
Bullet.position = CGPointMake(enemy.position.x, enemy.position.y)
let action = SKAction.moveToY(-self.size.height - 70, duration: 1.0)
Bullet.runAction(SKAction.repeatActionForever(action))
self.addChild(Bullet)
}
Then rework your enemy function so that overtime you call it you spawn a new enemy.
func SpawnEnemies() {
var Enemy = SKSpriteNode(imageNamed: "Enemy.png")
Enemy.name = "enemy";
let MinValue = self.size.width/8
let MaxValue = self.size.width-20
let SpawnPoint = UInt32(MaxValue-MinValue)
Enemy.position = CGPoint(x: CGFloat(arc4random_uniform(SpawnPoint)), y: self.size.height)
let action = SKAction.moveToY(-70, duration: 3.0)
Enemy.runAction(SKAction.repeatActionForever(action))
self.addChild(Enemy)
}
In the function didMoveToView, lets add actions to the scene that handles our timing so that we do not use NSTimer
{
...
let spawnEnemy = SKAction.sequence([SKAction.runBlock(
{
[unowned self] in
self.SpawnEnemies();
}),SKAction.waitForDuration(1)]);
let spawnBullet = SKAction.sequence([SKAction.runBlock(
{
[unowned self] in
self.enumerateChildNodesWithName("enemy", usingBlock:
{
(enemy : SKNode,stop: UnsafeMutablePointer <ObjCBool>) in
self.SpawnBullets(enemy as! SKSpriteNode);
});
}),SKAction.waitForDuration(0.2)]);
let group = SKAction.group([spawnEnemy, spawnBullet])
self.runAction(SKAction.repeatActionForever(group))
}
This code is not tested so let me know if I need to update anything.
Is it possible to animate the screen brightness change on iOS 5.1+? I am using [UIScreen mainScreen] setBrightness:(float)] but I think that the abrupt change is ugly.
I ran into issues with the accepted answer when attempting to animate to another value with a previous animation in progress. This solution cancels an in-progress animation and animates to the new value:
extension UIScreen {
func setBrightness(_ value: CGFloat, animated: Bool) {
if animated {
_brightnessQueue.cancelAllOperations()
let step: CGFloat = 0.04 * ((value > brightness) ? 1 : -1)
_brightnessQueue.add(operations: stride(from: brightness, through: value, by: step).map({ [weak self] value -> Operation in
let blockOperation = BlockOperation()
unowned let _unownedOperation = blockOperation
blockOperation.addExecutionBlock({
if !_unownedOperation.isCancelled {
Thread.sleep(forTimeInterval: 1 / 60.0)
self?.brightness = value
}
})
return blockOperation
}))
} else {
brightness = value
}
}
}
private let _brightnessQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
Swift 5
import UIKit
extension UIScreen {
public func setBrightness(to value: CGFloat, duration: TimeInterval = 0.3, ticksPerSecond: Double = 120) {
let startingBrightness = UIScreen.main.brightness
let delta = value - startingBrightness
let totalTicks = Int(ticksPerSecond * duration)
let changePerTick = delta / CGFloat(totalTicks)
let delayBetweenTicks = 1 / ticksPerSecond
let time = DispatchTime.now()
for i in 1...totalTicks {
DispatchQueue.main.asyncAfter(deadline: time + delayBetweenTicks * Double(i)) {
UIScreen.main.brightness = max(min(startingBrightness + (changePerTick * CGFloat(i)),1),0)
}
}
}
}
I don't know if this is "animatable" in some other way, but you could do it yourself.
For instance the following example code was hooked up to "Full Bright" and "Half Bright" buttons in the UI. It uses performSelector...afterDelay to change the brightness by 1% every 10ms till the target brightness is reached. You would pick an appropriate change rate based on some experimenting. Actually the refresh rate is, I think, 60 hz so there is probably no point in doing a change at an interval smaller than 1/60th of a second (My example rate was chosen to have nice math). Although you might want to do this on a non-UI thread, it doesn't block the UI.
- (IBAction)fullBright:(id)sender {
CGFloat brightness = [UIScreen mainScreen].brightness;
if (brightness < 1) {
[UIScreen mainScreen].brightness += 0.01;
[self performSelector:#selector(fullBright:) withObject:nil afterDelay:.01];
}
}
- (IBAction)halfBright:(id)sender {
CGFloat brightness = [UIScreen mainScreen].brightness;
if (brightness > 0.5) {
[UIScreen mainScreen].brightness -= 0.01;
[self performSelector:#selector(halfBright:) withObject:nil afterDelay:.01];
}
}
A Swift extension:
extension UIScreen {
private static let step: CGFloat = 0.1
static func animateBrightness(to value: CGFloat) {
guard fabs(UIScreen.main.brightness - value) > step else { return }
let delta = UIScreen.main.brightness > value ? -step : step
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
UIScreen.main.brightness += delta
animateBrightness(to: value)
}
}
}
Based on Charlie Price's great answer, here's a method for "animating" a change in screen brightness to any value desired.
- (void)graduallyAdjustBrightnessToValue:(CGFloat)endValue
{
CGFloat startValue = [[UIScreen mainScreen] brightness];
CGFloat fadeInterval = 0.01;
double delayInSeconds = 0.01;
if (endValue < startValue)
fadeInterval = -fadeInterval;
CGFloat brightness = startValue;
while (fabsf(brightness-endValue)>0) {
brightness += fadeInterval;
if (fabsf(brightness-endValue) < fabsf(fadeInterval))
brightness = endValue;
dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(dispatchTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[UIScreen mainScreen] setBrightness:brightness];
});
}
}
Or you can use NSTimer instead of while loops and performSelector.
finalValue - is value you want to achieve.
Timer fires 30 times with duration 0.02 second for each (you can choose something different but smoothly) and changes brightness value.
weak var timer: NSTimer?
var count = 1
let maxCount = 30
let interval = 0.02
timer = NSTimer
.scheduledTimerWithTimeInterval(interval,
target: self,
selector: #selector(changeBrightness),
userInfo: nil,
repeats: true)
func changeBrightness()
{
guard count < maxCount else { return }
let currentValue = UIScreen.mainScreen().brightness
let restCount = maxCount - count
let diff = (finalValue - currentValue) / CGFloat(restCount)
let newValue = currentValue + diff
UIScreen.mainScreen().brightness = newValue
count += 1
}
You can use this helper if you need to change brightness of your specific ViewController
import Foundation
import UIKit
final class ScreenBrightness {
private var timer: Timer?
private var brightness: CGFloat?
private var isBrighteningScreen = false
private var isDarkeningScreen = false
private init() { }
static let shared = ScreenBrightnessHelper()
//Увеличение яркости экрана до максимального уровня
func brightenDisplay() {
resetTimer()
isBrighteningScreen = true
if #available(iOS 10.0, *), timer == nil {
brightness = UIScreen.main.brightness
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { (timer) in
UIScreen.main.brightness = UIScreen.main.brightness + 0.01
if UIScreen.main.brightness > 0.99 || !self.isBrighteningScreen {
self.resetTimer()
}
}
}
timer?.fire()
}
//Затемнение экрана до предыдущего уровня
func darkenDisplay() {
resetTimer()
isDarkeningScreen = true
guard let brightness = brightness else {
return
}
if #available(iOS 10.0, *), timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { (timer) in
UIScreen.main.brightness = UIScreen.main.brightness - 0.01
if UIScreen.main.brightness <= brightness || !self.isDarkeningScreen {
self.resetTimer()
self.brightness = nil
}
}
timer?.fire()
}
}
private func resetTimer() {
timer?.invalidate()
timer = nil
isBrighteningScreen = false
isDarkeningScreen = false
}
}
Call ScreenBrightness.shared.brightenDisplay() in viewWillAppear and if you wanna change it back call method ScreenBrightness.shared.darkenDisplay() that will change brightness back