Okay so I'm trying to add a 2 seconds countdown timer in a label that activates whenever the user double taps the screen.
So upon doubletapping, the user would find a new label created with the number "2" then a second later "1" then "0".
I can do this perfectly for just 1 timer. When the user double taps to create a second timer, or more, the app crashes.
I found that this is due to the declaration of the variables being made in the class instead of in the function to allow the timer function to retrieve such declarations as the label.
If I found a way to make the timer function inside another function instead of under class it will work fine.
So here's the code that works only when 1 timer is being deployed.
Under Class:
var BTTimer = NSTimer ()
var BTCounter = 2
let BT = SKLabelNode
Under Function touchesBegan:
let B = SKSpriteNode(imageNamed: "B 110.png")
let Touch : UITouch! = touches.first
let TouchLocation = Touch.locationInNode(self)
let PreviousTouchLocation = Touch.previousLocationInNode(self)
let Player = childNodeWithName("Player") as! SKSpriteNode
let xPos = Player.position.x + (TouchLocation.x - PreviousTouchLocation.x)
let yPos = Player.position.y + (TouchLocation.y - PreviousTouchLocation.y)
B.position = CGPointMake(xPos, yPos)
B.zPosition = -2
self.addChild(B)
BT.fontSize = 27;
BT.fontColor = UIColor.redColor()
BT.position = CGPointMake(xPos - 9, yPos - 30)
BT.zPosition = -1
self.addChild(BT)
BT.text = String(BTCounter);
BTTimer = NSTimer.scheduledTimerWithTimeInterval(1, target:self, selector: #selector(GameScene.updateBCounter), userInfo: nil, repeats: true)
}
}
Now under another function of the same class:
func updateBCounter() {
if(BTCounter > 0) {
BTCounter-=1
BT.text = String(BTCounter)
}
else if(BTCounter == 0) {
//Remove B
//Place C
}
I wish to change this so that the user is capable of producing as many timers as he wishes to deploy.
You can update updateBCounter like this:
func updateBCounter(timer: NSTimer)
Your selector is changed to #selector(GameScene.updateBCounter(_:)
When you schedule the timer, you can set userInfo to any dictionary and the timer sent to updateBCounter will have it in its userInfo property.
I don't know exactly what you are trying to do, but those are the tools to do multiple timers at once calling the same selector, but knowing which one you are dealing with.
Get rid of the properties keeping track of things and make keys in the userInfo dictionary instead.
You could use any object as userInfo (not just a dictionary), so you could also do:
class TimerData {
var counter: Int
var l: SKLabelNode
}
Related
I have the below function that works properly when a button is switched to activate it. I want to add a variable that gives each message it's current message number starting from 1. I've tried different methods of setting / updating the value but none of it helped with updating.
I'm very new to Swift / iOS development so I'm sure there is something I'm missing. What I do know is that the message prints to console repeatedly till the button is switched off and the Timer is what enables it to continuously run.
#IBOutlet weak var stateLabel: UILabel!
//Starts / Stops recording of sensor data via a switch
#IBAction func stateChange(_ sender: UISwitch) {
if sender.isOn == true {
startSensorData()
stateLabel.text = "Stop"
} else {
stopSensorData()
stateLabel.text = "Start"
}
}
func startSensorData() {
print("Start Capturing Sensor Data")
// Making sure sensors are available
if self.motionManager.isAccelerometerAvailable, self.motionManager.isGyroAvailable {
// Setting the frequency required for data session
self.motionManager.accelerometerUpdateInterval = 1.0 / 3.0
self.motionManager.gyroUpdateInterval = 1.0 / 3.0
// Start sensor updates
self.motionManager.startAccelerometerUpdates()
self.motionManager.startGyroUpdates()
// Configure a timer to fetch the data.
self.motionUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0/3.0, repeats: true, block: { (timer1) in
// Get the motion data.
var loggingSample = 1
if let accelData = self.motionManager.accelerometerData, let gyroData = self.motionManager.gyroData {
let accelX = accelData.acceleration.x
let accelY = accelData.acceleration.y
let accelZ = accelData.acceleration.z
let gyroX = gyroData.rotationRate.x
let gyroY = gyroData.rotationRate.y
let gyroZ = gyroData.rotationRate.z
let message = "\(Date().timeIntervalSince1970),\(self.device_id),\(loggingSample),\(accelX),\(accelY),\(accelZ),\(gyroX),\(gyroY),\(gyroZ),Processing"
print(message)
loggingSample += 1
}
}
)}
}
You keep getting a value of 1 for loggingSample because you are using a local variable that gets created as 1 each time.
All you need to do is move the declaration of loggingSample to be outside the function so it is a class property.
Move the line:
var loggingSample = 1
outside the function so it is next to your outlets and other properties.
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.
Im trying to keep track of how many times the user loses in my game. So for every loss it goes up by 1. I also want to save it too so the user could see how many total times they lost. Right now the code I have it works the first time and goes to one but if I lose in the game after that it just stays at 1. What am I doing wrong? Thanks!
class level1: SKScene, SKPhysicsContactDelegate, GKGameCenterControllerDelegate {
var deathScore = 0
override func didMoveToView(view: SKView) {
var deathLabel = SKLabelNode()
deathLabel = SKLabelNode(fontNamed: "LadyIce-3D")
deathLabel.text = "100"
deathLabel.zPosition = 14
deathLabel.fontSize = 100
deathLabel.fontColor = SKColor.darkTextColor()
deathLabel.position = CGPointMake(self.size.width / 1.1, self.size.height / 1.4)
deathLabel.hidden = true
self.addChild(deathLabel)
}
if firstBody.categoryBitMask == HeroCategory && fourthBody.categoryBitMask == GameOverCategory {
deathScore++
deathLabel.hidden = false
let defaults = NSUserDefaults()
let saveDeaths = NSUserDefaults().integerForKey("saveNumberOfDeaths")
if(deathScore > saveDeaths)
{
NSUserDefaults().setInteger(saveDeaths, forKey: "saveNumberOfDeaths")
}
var showNumberOfDeaths = defaults.integerForKey("saveNumberOfDeaths")
deathLabel.text = String(showNumberOfDeaths)
}
}
You are declaring a new var deathScore everytime, initialized with 0 and incrementing it. It will always be 1.
UserDefaults.standard.set(UserDefaults.standard.integer(forKey: "saveNumberOfDeaths")+1, forKey: "saveNumberOfDeaths")
deathLabel.text = String(UserDefaults.standard.integer(forKey: "saveNumberOfDeaths"))
I am having trouble with my game, I've managed to have my enemySprites all shoot at the hero in a synchronized matter and I've gotten them to play the "killed" animation once they've been hit. Although I've run into a rather small matter which I was really hoping you guys could help me with. The problem I have is that when my badGuy is killed they move off the screen and I don't know how I can program it so that a 'new' badGuy appears forever until the Hero sprite is killed.
This is my function to spawn my enemy:
func spawnEnemy(targetSprite: SKNode) -> SKSpriteNode {
if !gamePaused{
// create a new enemy sprite
let main = GameScene()
newEnemy = SKSpriteNode(imageNamed:"BNG1_1.png")
enemySprite.append(newEnemy)//ADD TO THE LIBRARY OF BADGUYS
newEnemy.xScale = 1.2
newEnemy.yScale = 0.6
newEnemy.physicsBody?.dynamic = true
newEnemy.physicsBody = SKPhysicsBody(texture: newEnemy.texture, size: newEnemy.size)
newEnemy.physicsBody?.affectedByGravity = false
newEnemy.physicsBody?.categoryBitMask = BodyType.badguyCollision.rawValue
newEnemy.physicsBody?.contactTestBitMask = BodyType.beamCollison.rawValue
newEnemy.physicsBody?.collisionBitMask = 0x0
let muv : UInt32 = (200 + (arc4random()%500))
let actualDuration = NSTimeInterval(random(min: CGFloat(3.0), max: CGFloat(4.0)))
let randomNum = CGPoint(x:Int (muv), y:Int (arc4random()%500))
// Create the actions
var actionMove = SKAction.moveTo(randomNum, duration: NSTimeInterval(actualDuration))
newEnemy.runAction(SKAction.sequence([actionMove]))
// position new sprite at a random position on the screen
var posX = arc4random_uniform(UInt32(sizeRect.size.width))
var posY = arc4random_uniform(UInt32(sizeRect.size.height))
newEnemy.position = CGPoint(x: screenSize.width*2 + newEnemy.size.width, y: random(min: newEnemy.size.height, max: screenSize.height - newEnemy.size.height))
let atlas = SKTextureAtlas(named: "BG1.atlas")
let anime = SKAction.animateWithTextures([atlas.textureNamed("BG1_1.png"), atlas.textureNamed("BG1_2.png"),
atlas.textureNamed("BG1_3.png"),
atlas.textureNamed("BG1_2.png"),
atlas.textureNamed("BG1_1.png")], timePerFrame: 0.1)
dinoRun = SKAction.repeatActionForever(anime)
newEnemy.runAction(dinoRun)
}
return newEnemy
}
And this is my function for once they are hit (this function is called when the collisionTest between my hero's laser and the badguy is recognized):
func deadBadGuy(){
//animation
var dinoRun:SKAction
var newdes = CGPoint(x: Int(arc4random()%500), y:0)
var actionMoov = SKAction.moveTo(newdes, duration: 3)
var goaway = SKAction.removeFromParent()
let aTlas = SKTextureAtlas(named: "dedBG.atlas")
let anime = SKAction.animateWithTextures([aTlas.textureNamed("deadBG1.png"), aTlas.textureNamed("deadBG2.png"),
aTlas.textureNamed("deadBG3.png"),
aTlas.textureNamed("deadBG2.png"),
aTlas.textureNamed("deadBG1.png")], timePerFrame: 0.1)
dinoRun = SKAction.repeatActionForever(anime)
newEnemy.runAction(dinoRun)
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]))
score++
self.scoreNode.text = String(score)
enemySprites.newPlace(neewEnemy)
dead = true //i created this Boolean because the sprite keeps shooting even if its dead
}
This is the way I called for the collisionTest in my program in case you need it for more information:
func didBeginContact(contact:SKPhysicsContact){
if !gamePaused {
let firstNode = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch(firstNode){
case BodyType.beamCollison.rawValue | BodyType.badguyCollision.rawValue:
deadBadGuy()
//enemySprites.spawnEnemy(sprite)
default:
print("hit")
}
Note: I tried having the func spawnEnemy being called during the collisionTest but that results in the BadGuy staying off screen shooting at my hero.
Update : I found out how to add a new enemy sprite once the other is dead all i had to do was
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]), completion: {
self.addChild(self.enemySprites.spawnEnemy(sprite))
})
in the deadBadGuy function(the spawnEnemy function is in another class which I named enemySprites as a variable). However now I've run into a new issue and that is that it adds 10 enemySprites instead of one. How can I change that?
Update 2 : I figured out that issue too, I just needed to remove to dead Boolean method in the deadBadGuy function.
I found my answer and all I had to do was add this line to my deadBadGuy function and remove the dead boolean methods
newEnemy.runAction(SKAction.sequence([actionMoov,goaway]), completion: {
self.addChild(self.enemySprites.spawnEnemy(sprite))
})
How can I create a timer that fires every two seconds that will increment the score by one on a HUD I have on my screen? This is the code I have for the HUD:
#implementation MyScene
{
int counter;
BOOL updateLabel;
SKLabelNode *counterLabel;
}
-(id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size])
{
counter = 0;
updateLabel = false;
counterLabel = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
counterLabel.name = #"myCounterLabel";
counterLabel.text = #"0";
counterLabel.fontSize = 20;
counterLabel.fontColor = [SKColor yellowColor];
counterLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeCenter;
counterLabel.verticalAlignmentMode = SKLabelVerticalAlignmentModeBottom;
counterLabel.position = CGPointMake(50,50); // change x,y to location you want
counterLabel.zPosition = 900;
[self addChild: counterLabel];
}
}
In Sprite Kit do not use NSTimer, performSelector:afterDelay: or Grand Central Dispatch (GCD, ie any dispatch_... method) because these timing methods ignore a node's, scene's or the view's paused state. Moreover you do not know at which point in the game loop they are executed which can cause a variety of issues depending on what your code actually does.
The only two sanctioned ways to perform something time-based in Sprite Kit is to either use the SKScene update: method and using the passed-in currentTime parameter to keep track of time.
Or more commonly you would just use an action sequence that starts with a wait action:
id wait = [SKAction waitForDuration:2.5];
id run = [SKAction runBlock:^{
// your code here ...
}];
[node runAction:[SKAction sequence:#[wait, run]]];
And to run the code repeatedly:
[node runAction:[SKAction repeatActionForever:[SKAction sequence:#[wait, run]]]];
Alternatively you can also use performSelector:onTarget: instead of runBlock: or perhaps use a customActionWithDuration:actionBlock: if you need to mimick the SKScene update: method and don't know how to forward it to the node or where forwarding would be inconvenient.
See SKAction reference for details.
UPDATE: Code examples using Swift
Swift 5
run(SKAction.repeatForever(SKAction.sequence([
SKAction.run( /*code block or a func name to call*/ ),
SKAction.wait(forDuration: 2.5)
])))
Swift 3
let wait = SKAction.wait(forDuration:2.5)
let action = SKAction.run {
// your code here ...
}
run(SKAction.sequence([wait,action]))
Swift 2
let wait = SKAction.waitForDuration(2.5)
let run = SKAction.runBlock {
// your code here ...
}
runAction(SKAction.sequence([wait, run]))
And to run the code repeatedly:
runAction(SKAction.repeatActionForever(SKAction.sequence([wait, run])))
In Swift usable:
var timescore = Int()
var actionwait = SKAction.waitForDuration(0.5)
var timesecond = Int()
var actionrun = SKAction.runBlock({
timescore++
timesecond++
if timesecond == 60 {timesecond = 0}
scoreLabel.text = "Score Time: \(timescore/60):\(timesecond)"
})
scoreLabel.runAction(SKAction.repeatActionForever(SKAction.sequence([actionwait,actionrun])))
I've taken the swift example above and added in leading zeros for the clock.
func updateClock() {
var leadingZero = ""
var leadingZeroMin = ""
var timeMin = Int()
var actionwait = SKAction.waitForDuration(1.0)
var timesecond = Int()
var actionrun = SKAction.runBlock({
timeMin++
timesecond++
if timesecond == 60 {timesecond = 0}
if timeMin / 60 <= 9 { leadingZeroMin = "0" } else { leadingZeroMin = "" }
if timesecond <= 9 { leadingZero = "0" } else { leadingZero = "" }
self.flyTimeText.text = "Flight Time [ \(leadingZeroMin)\(timeMin/60) : \(leadingZero)\(timesecond) ]"
})
self.flyTimeText.runAction(SKAction.repeatActionForever(SKAction.sequence([actionwait,actionrun])))
}
Here's the full code to build a timer for SpriteKit with Xcode 9.3 and Swift 4.1
In our example the score label will be incrementd by 1 every 2 seconds.
Here's final result
Good, let's start!
1) The score label
First of all we need a label
class GameScene: SKScene {
private let label = SKLabelNode(text: "Score: 0")
}
2) The score label goes into the scene
class GameScene: SKScene {
private let label = SKLabelNode(text: "Score: 0")
override func didMove(to view: SKView) {
self.label.fontSize = 60
self.addChild(label)
}
}
Now the label is at the center of the screen. Let's run the project to see it.
Please note that at this point the label is not being updated!
3) A counter
We also want to build a counter property which will hold the current value displayed by the label. We also want the label to be updated as soon as the counter property is changed so...
class GameScene: SKScene {
private let label = SKLabelNode(text: "Score: 0")
private var counter = 0 {
didSet {
self.label.text = "Score: \(self.counter)"
}
}
override func didMove(to view: SKView) {
self.label.fontSize = 60
self.addChild(label)
// let's test it!
self.counter = 123
}
}
4) The actions
Finally we want to build an action that every 2 seconds will increment counter
class GameScene: SKScene {
private let label = SKLabelNode(text: "Score: 0")
private var counter = 0 {
didSet {
self.label.text = "Score: \(self.counter)"
}
}
override func didMove(to view: SKView) {
self.label.fontSize = 60
self.addChild(label)
// 1 wait action
let wait2Seconds = SKAction.wait(forDuration: 2)
// 2 increment action
let incrementCounter = SKAction.run { [weak self] in
self?.counter += 1
}
// 3. wait + increment
let sequence = SKAction.sequence([wait2Seconds, incrementCounter])
// 4. (wait + increment) forever
let repeatForever = SKAction.repeatForever(sequence)
// run it!
self.run(repeatForever)
}
}
The following code creates a new thread and waits 2 seconds before doing something on the main thread:
BOOL continueIncrementingScore = YES;
dispatch_async(dispatch_queue_create("timer", NULL);, ^{
while(continueIncrementingScore) {
[NSThread sleepForTimeInterval:2];
dispatch_async(dispatch_get_main_queue(), ^{
// this is performed on the main thread - increment score here
});
}
});
Whenever you want to stop it - just set continueIncrementingScore to NO