Sprites Spawning In Straight Away At The Start - swift

I am currently making a game on Xcode 11, I am having a problem where in which my sprite will spawn in straight away at the start, even through their is a wait delay, but after the first spawn the sprites spawn in at their current time. How can I make it so the sprites wont spawn in untill 3 seconds after the scene has started.
Any help would be greatly appreciated.
The photo attached shows what happens at the start of the game.
I have attached part of my code for one of the sprites as their setup is similar to each other.
//Setup Bird
func setupBird() {
bird = SKSpriteNode(imageNamed: "bird-1")
bird.name = "Bird"
bird.zPosition = 20.0
bird.setScale(1.5)
let birdHeight = bird.frame.height
let random = CGFloat.random(min: -birdHeight, max: birdHeight*2.0)
bird.position = CGPoint(x: cameraRect.maxX + bird.frame.width, y: size.height/2.0 + birdHeight + random)
bird.physicsBody = SKPhysicsBody(circleOfRadius: bird.size.width/2.0)
bird.physicsBody!.affectedByGravity = false
bird.physicsBody!.isDynamic = false
bird.physicsBody!.categoryBitMask = PhysicsCategory.Bird
bird.physicsBody!.contactTestBitMask = PhysicsCategory.Player
addChild(bird)
bird.run(.sequence([.wait(forDuration: 15, withRange: 5), .removeFromParent()]))
//Animation For Birds
var textures: [SKTexture] = []
for i in 1...3 {
textures.append(SKTexture(imageNamed: "bird-\(i)"))
}
bird.run(.repeatForever(.animate(with: textures, timePerFrame: 0.15)))
}
func spawnBird() {
let random = CGFloat.random(min: 15.0, max: 30.0)
run(.repeatForever(.sequence([
.wait(forDuration: TimeInterval(random)),
.run { [weak self] in
self?.setupBird()
}
])))
}

I fixed the problem by adding frame.width/2.0 to the position, which moves the inital bird position.
bird.position = CGPoint(x: cameraRect.maxX + frame.width/2.0 + bird.frame.width, y: size.height/2.0 + birdHeight + random)

If I understand correctly, you want to spawn a bird periodically, starting after an initial delay.
You could delay the repeating action, either with another SpriteKit action or with DispatchQueue.
func spawnBirds() {
let spawnBirdAfterDelayAction = SKAction.sequence([
.wait(forDuration: 22.5, withRange: 7.5),
.run { [weak self] in self?.setupBird() }
])
run(
.sequence([
.wait(forDuration: 3), // initial delay
.repeatForever(spawnBirdAfterDelayAction)
])
)
}
/// As Knight0fDragon pointed out, this approach is inferior because it circumvents the SpriteKit runtime.
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.spawnBirds()
}

Related

how to make an SKSpriteNode follow another SKSpriteNode

Running into some trouble with my code. I'm trying to make zombies follow my player around with the following code:
class GameScene: SKScene, SKPhysicsContactDelegate {
func Enemies() {
Enemy = SKSpriteNode(imageNamed: "Spaceship")
Enemy.size = CGSize(width: 50, height: 50)
Enemy.color = UIColor(red: 0.9, green: 0.1, blue: 0.1, alpha: 1.0)
Enemy.colorBlendFactor = 1.0
//physics
Enemy.physicsBody = SKPhysicsBody(rectangleOf: Enemy.size)
Enemy.physicsBody?.isDynamic = true
Enemy.physicsBody?.affectedByGravity = false
Enemy.name = "Enemy"
Enemy.position.y = -frame.size.height/2
let positionX = arc4random_uniform(UInt32(frame.size.width))
Enemy.position.x = CGFloat(positionX)
addChild(Enemy)
}
override func didMove(to view: SKView) {
enemyTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(GameScene.Enemies), userInfo: nil, repeats: true)
override func update(_ currentTime: TimeInterval) {
Enemy.run(SKAction.move(to: ship.position, duration: 3))
}
If I run this code, I can spawn the zombies, but they will not follow my main player, they will just go to the position that he was at when they were spawned (i.e. zombie spawned at time = 0 will go to the ship position at time = 0, zombie spawned at time = 1 will go to the ship position at time = 1, and so on). However, if I run this code while only spawning one zombie like so:
override func didMove(to view: SKView) {
Enemies()
}
The lone zombie will follow my player around. Any idea why the code works for one zombie, but not for multiple zombies?
I would not recommend constantly adding actions on your update cycle, you are going to suffer from performance loss due to all the things that happen behind the scenes. Instead, use an SKAction.customAction that you would only add to your sprite once.
Here is an example of a custom action that will do what you want, remember only assign it once. (Code is not tested, so may need edits)
let customActionBlock =
{
(node,elapsedTime) in
let dx = ship.x - node.position.x
let dy = ship.y - node.position.y
let angle = atan2(dx,dy)
node.position.x += sin(angle) * speedPerFrame
node.position.y += cos(angle) * speedPerFrame
}
let duration = TimeInterval(Int.max) //want the action to run infinitely
let followPlayer = SKAction.customAction(withDuration:duration,actionBlock:customActionBlock)
Enemy.run(action:followPlayer,withKey:"follow")
Maybe you should removeAllActions() on Enemy before readding an action. It seems that you have actions that take 3 seconds, but you add an action every frame, so it has at most 180 actions for a node at once.

How do you trigger actions with NSTimer in Spritekit?

I'm trying to get my "Player" (A circle in the middle) to increase in size once the screen is touched by running a timer.
Once the timer is over 0 seconds, it increases in size. Once the timer is over 3 seconds, it decreases to its original scale size and once the timer is over 7 seconds, it resets and this repeats forever.
What am I doing wrong?
import SpriteKit
class GameScene: SKScene {
var Center = SKSpriteNode()
var Player = SKSpriteNode()
var timer = NSTimer()
var seconds = 0
override func didMoveToView(view: SKView) {
Center = SKSpriteNode(imageNamed: "Center")
Center.size = CGSize(width: 80, height: 80)
Center.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(Center)
Player = SKSpriteNode(imageNamed: "Player")
Player.size = CGSize(width: 80, height: 80)
Player.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
self.addChild(Player)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
timer = NSTimer.scheduledTimerWithTimeInterval(4.0, target: self, selector: #selector(GameScene.playerScaleUp), userInfo: nil, repeats: true)
}
func playerScaleUp(){
if seconds > 0{
Player.runAction(SKAction.scaleBy(4, duration: 2))
}
}
func playerScaleDown(){
if seconds > 3{
Player.runAction(SKAction.scaleBy(-4, duration: 2))
}
}
func resetScale(){
if seconds > 7{
timer.invalidate()
}
}
override func update(currentTime: CFTimeInterval) {
}
}
This can be done in a few different ways:
1) Using SKActions (which my example uses)
2) Using update: method and its passed currentTime parameter
3) Using NSTimer which I wouldn't recommend because it is not affected by scene's, view's or node's paused state, so it can lead you into troubles in the future. Read more in this StackOverflow answer
4) Well, probably some more, but I will stop here.
My example uses SKAction. This means that I don't really check how much time is passed, but rather I organize actions into sequences (where actions are executed sequentially) and into groups (where actions are organized parallel). Means I use SKActions like I am playing with LEGO's :)
Here is the code ...I left debugging code intentionally, because it can help you to learn how you can use SKActions in different situations.
class GameScene: SKScene, SKPhysicsContactDelegate {
let player = SKSpriteNode(color: .blackColor(), size: CGSize(width: 50, height: 50))
var timePassed = 0.0 {
didSet {
self.label.text = String(format: "%.1f",timePassed)
}
}
let label = SKLabelNode(fontNamed: "ArialMT")
override func didMoveToView(view: SKView) {
/* Debugging - Not really needed, I added it just because of a better example */
let wait = SKAction.waitForDuration(0.1)
let updateLabel = SKAction.runBlock({[unowned self] in self.timePassed += wait.duration})
self.label.position = CGPoint(x: frame.midX, y: frame.midY+100.0)
addChild(self.label)
let timerSequence = SKAction.sequence([updateLabel, wait])
self.runAction(SKAction.repeatActionForever(timerSequence), withKey: "counting")
//This is used later, at the end of scaleUpAndDownSequence to reset the timer
let resetTimer = SKAction.runBlock({[unowned self] in self.timePassed = 0.0})
/* End Debugging */
self.player.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(self.player)
let waitBeforeScaleToOriginalSize = SKAction.waitForDuration(3.0)
let waitBeforeRestart = SKAction.waitForDuration(4.0)
let scaleUp = SKAction.scaleTo(2.0, duration: 1)
let scaleDown = SKAction.scaleTo(1.0, duration: 1)
let scaleUpGroup = SKAction.group([waitBeforeScaleToOriginalSize, scaleUp])
let scaleDownGroup = SKAction.group([scaleDown, waitBeforeRestart])
//One cycle, scale up, scale down, reset timer
let scaleUpAndDownSequence = SKAction.sequence([scaleUpGroup, scaleDownGroup, resetTimer])
let loop = SKAction.repeatActionForever(scaleUpAndDownSequence)
self.player.runAction(loop, withKey: "scalingUpAndDown")
}
}
So here, I have two groups of actions:
1) scaleUpGroup
2) scaleDownGroup
scaleUpGroup group of actions has two actions in it: a scaleUp action, and an action which says how much to wait before the scale down action should occur. Because we want scaling up to happen immediately, we run it in parallel with the waitBeforeScaleToOriginalSize.
Same logic goes for scaleDownGroup. When scaleUpGroup is finished (its duration is determined by the longest action in the group) we start scaleDownGroup which scales down the player to its default size, and waits certain amount of time to repeat the whole thing.
Here is the result:
I start an animation on touch ( I've removed that code) and as you can see, scale up animation starts immediately, then after 3 seconds the player get scaled down to its original size, and after 4 seconds the whole animation repeats (player gets scaled up again etc).

Swift: how to invalidate timer on animated SKShapenode countdown?

Alright I'm building a sprite kit game and on one of my previous questions, I got the code that lets a circle skshapenode "unwind" in accordance with a timer that runs out (so that the circle has disappeared by the time the timer is done).
Here was my question: Swift, sprite kit game: Have circle disappear in clockwise manner? On timer?
And here is the code that I've been using:
func addCircle() {
circle = SKShapeNode(circleOfRadius: 50)
let circleColor = UIColor(red: 102/255, green: 204/255, blue: 255/255, alpha: 1)
circle.fillColor = circleColor
circle.strokeColor = SKColor.clearColor()
circle.position = CGPoint (x: self.frame.size.width * 0.5, y: self.frame.size.height * 0.5+45)
circle.zPosition = 200
circle.zRotation = CGFloat(M_PI_2)
addChild(circle)
countdown(circle, steps: 120, duration: 5) { //change steps to change how much of the circle it takes away at a time
//Performed when circle timer ends:
print("circle done")
self.gameSoundTrack.stop()
self.removeAllChildren()
self.removeAllActions()
self.viewController.removeSaveMe(self)
self.GOSceneNodes() //Implements GOScene
self.viewController.addGOScene(self) //Implements GOViewController
}
}
// Creates an animated countdown timer
func countdown(circle:SKShapeNode, steps:Int, duration:NSTimeInterval, completion:()->Void) {
let radius = CGPathGetBoundingBox(circle.path).width/2
let timeInterval = duration/NSTimeInterval(steps)
let incr = 1 / CGFloat(steps)
var percent = CGFloat(1.0)
let animate = SKAction.runBlock({
percent -= incr
circle.path = self.circle(radius, percent:percent)
})
let wait = SKAction.waitForDuration(timeInterval)
let action = SKAction.sequence([wait, animate])
runAction(SKAction.repeatAction(action,count:steps-1)) {
self.runAction(SKAction.waitForDuration(timeInterval)) {
circle.path = nil
completion()
}
}
}
// Creates a CGPath in the shape of a pie with slices missing
func circle(radius:CGFloat, percent:CGFloat) -> CGPath {
let start:CGFloat = 0
let end = CGFloat(M_PI*2) * percent
let center = CGPointZero
let bezierPath = UIBezierPath()
bezierPath.moveToPoint(center)
bezierPath.addArcWithCenter(center, radius: radius, startAngle: start, endAngle: end, clockwise: true)
bezierPath.addLineToPoint(center)
return bezierPath.CGPath
}
This all works well, however I need a way to invalidate the countdown for the circle if another button within the game is pressed so that everything that is performed when the timer runs out (everything after //Performed when circle timer ends: above) is NOT performed.
I have tried
circle.removeAllActions()
circle.removeFromParent()
when the button is pressed, however this just removes the circle and everything is still executed even though the circle is gone.
I'm pretty new to Swift and I can't figure out what part of those 3 functions I need to invalidate or stop in order to stop the countdown from finishing. How can I accomplish this?
You can stop an action by adding a key to the action and then run
removeActionForKey("actionKey")
To add a key to the action, replace the countdown method with the following:
// Creates an animated countdown timer
func countdown(circle:SKShapeNode, steps:Int, duration:NSTimeInterval, completion:()->Void) {
let radius = CGPathGetBoundingBox(circle.path).width/2
let timeInterval = duration/NSTimeInterval(steps)
let incr = 1 / CGFloat(steps)
var percent = CGFloat(1.0)
let animate = SKAction.runBlock({
percent -= incr
circle.path = self.circle(radius, percent:percent)
})
let wait = SKAction.waitForDuration(timeInterval)
let action = SKAction.sequence([wait, animate])
let completed = SKAction.runBlock{
circle.path = nil
completion()
}
let countDown = SKAction.repeatAction(action,count:steps-1)
let sequence = SKAction.sequence([countDown, SKAction.waitForDuration(timeInterval),completed])
runAction(sequence, withKey: "actionKey")
}

[SpriteKit Swift]Trying to spawn cars and move them down the screen at a pace that varies

So, I am making an app for my friend and I need to make cars spawn in 1 of 3 lanes on a road randomly, then move down the screen at a pace that can and will vary at multiple points during the car's lifetime.
let createCarAction = SKAction.runBlock({ //Creates the cars every 4 seconds w/ a waitAction
let laneNum = laneList[Int(arc4random_uniform(3))] //There is an array higher up with the X values of each lane that the cars can use
let newCar: SKSpriteNode
let num = arc4random_uniform(4) //picks random car color
if num == 0 {
newCar = SKSpriteNode(imageNamed: "Audi")
} else if num == 1 {
newCar = SKSpriteNode(imageNamed: "Audi2")
} else if num == 2 {
newCar = SKSpriteNode(imageNamed: "Audi3")
} else {
newCar = SKSpriteNode(imageNamed: "Audi4")
}
newCar.xScale = 0.30
newCar.yScale = 0.30
newCar.position = CGPoint(x: laneNum, y: CGRectGetMaxY(self.frame) + newCar.size.height/2) //puts the car in the picked lane and just above the screen
newCar.zPosition = CGFloat(2)
self.addChild(newCar)
let carAction = SKAction.runBlock({ //this is where it starts going bad. This action is run so many times in the first second the scene starts that it does not switch transition from the scene before it.
if newCar.position.y > (CGRectGetMinY(self.frame)-newCar.size.height) { //if the car is on the screen basically
newCar.position = CGPoint(x: newCar.position.x, y: newCar.position.y-CGFloat(spd))
print("test")
} else { // not on the screen
newCar.removeFromParent()
print("car y: \(CGRectGetMaxY(self.frame) + newCar.size.height/2)")
newCar.removeAllActions() // This does not seem to have any affect as when only one car has been created, carAction is still running seconds after the car has left the screen
}
})
newCar.runAction(SKAction.repeatActionForever(carAction))
self.newCarList.append(newCar)
print("made new car")
})
I have the spawning working, but when newCar.runAction(SKAction.repeatActionForever(carAction)) comes into play, the app does not move to the GameScene from the MenuScene. I think that the car just instantly moves to the bottom, then the game just lags out trying to print where the car is. I have the road moving in the update function.
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if road1.position.y > CGRectGetMinY(self.frame) {
road1.position = CGPoint(x: CGRectGetMidX(self.frame), y: road1.position.y - CGFloat(spd)/2)
road2.position = CGPoint(x: road1.position.x, y: road1.position.y + road1.size.height)
} else {
road1.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
road2.position = CGPoint(x: road1.position.x, y: road1.position.y + road1.size.height)
}
}
I can't think of a way to move the cars that doesn't involve a massive array or a complex SKAction that moves the car instantly instead of at the same pace as the road.
Thanks in advance.
Edit: Formatting
I ended up figuring out a way to use an SKAction.
let carWaitAction = SKAction.waitForDuration(1/60)
let carAction = SKAction.runBlock({
if newCar.position.y > (CGRectGetMinY(self.frame)-newCar.size.height) {
newCar.position = CGPoint(x: newCar.position.x, y: newCar.position.y-CGFloat(spd/3))
} else {
self.score = self.score+5
newCar.removeFromParent()
print("car removed")
newCar.removeAllActions()
print (newCar.hasActions())
}
})
newCar.runAction(SKAction.repeatActionForever(SKAction.group([carWaitAction, carAction])))

How to add score to SpriteKit using SKAction

runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(addScore),
SKAction.waitForDuration(1.0) ]) ))
func addScore() {
let scoreSprite = SKSpriteNode(imageNamed: "scoreSprite")
let actualY = random1(min: scoreSprite.size.height/2, max: size.height - scoreSprite.size.height/2)
scoreSprite.position = CGPoint(x: size.width + scoreSprite.size.width/2, y: actualY)
self.addChild(scoreSprite)
let actualDuration = random1(min: CGFloat(0.5), max: CGFloat(0.5))
let actionMove = SKAction.moveTo(CGPoint(x: -scoreSprite.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
scoreSprite.runAction(SKAction.sequence([actionMove, actionMoveDone])) }
Okay, so I want my score to increase by 1 for every second the person plays the game. I looked into using NSTimeInterval but from what I could gather, the better option is to use SKAction. The only way I could think of was to use an almost clear sprite and to move it across the screen every second. What I want to know is how to make a score increase each time the node moves across the screen, or if this is no good what a better option would be. The code I am using to move the node is above. Thanks in advance.
Try adding this to your didMoveToView method
let wait = SKAction.waitForDuration(1.0)
let incrementScore = SKAction.runBlock ({
++self.score
self.scoreLabel.text = "\(self.score)"
})
self.runAction(SKAction.repeatActionForever(SKAction.sequence([wait,incrementScore])))
I fixed it using by adding this to the end of the scoreSprite function:
if scoreSprite.position.x > endOfScreenLeft {
updateScore()
}
Then did this to update the score:
func updateScore() {
score++
scoreLabel.text = String(score)
}
End of screen left is a CGFloat:
endOfScreenLeft = (self.size.width / 2) * CGFloat(-1)