Part of my animation is not working in SpriteKit - swift

init () {
super.init(texture: nil, color: .clear, size: initialSize)
// Create physicsBody based on a frame of the sprite
// basically giving it gravity and physics components
let bodyTexture = textureAtlas.textureNamed("MainCharacterFlying1")
self.physicsBody = SKPhysicsBody(texture: bodyTexture, size: self.size)
// Lose momentum quickly with high linear dampening
self.physicsBody?.linearDamping = 0.9
// weighs around 30 kg
self.physicsBody?.mass = 30
// no rotation
self.physicsBody?.allowsRotation = false
createAnimations()
self.run(soarAnimation, withKey: "soarAnimation")
}
// Animations to make the main character seem like it is flying
func createAnimations() {
let rotateUpAction = SKAction.rotate(byAngle: 0, duration: 0.475)
rotateUpAction.timingMode = .easeOut
let rotateDownAction = SKAction.rotate(byAngle: -1, duration: 0.8)
rotateDownAction.timingMode = .easeIn
let flyFrames: [SKTexture] = [textureAtlas.textureNamed("MainCharacterFlying1"), textureAtlas.textureNamed("MainCharacterFlying2"),textureAtlas.textureNamed("MainCharacterFlying3"), textureAtlas.textureNamed("MainCharacterFlying4"),textureAtlas.textureNamed("MainCharacterFlying5"),textureAtlas.textureNamed("MainCharacterFlying4"),textureAtlas.textureNamed("MainCharacterFlying3"),textureAtlas.textureNamed("MainCharacterFlying2")]
var flyAction = SKAction.animate(with: flyFrames, timePerFrame: 0.1)
flyAction = SKAction.repeatForever(flyAction)
flyAnimation = SKAction.group([flyAction,rotateUpAction])
let soarFrames: [SKTexture] = [textureAtlas.textureNamed("MainCharacterFlying5")]
var soarAction = SKAction.animate(with: soarFrames, timePerFrame: 1)
soarAction = SKAction.repeatForever(soarAction)
let soarAnimation = SKAction.group([soarAction,rotateDownAction])
}
When I run this code on the IOS Simulator, I have to click on the screen once in order for my main sprite to show up on the screen, or else it will not. And when I click on the sprite, the sprite will start flapping its wings and go up (I have other code for that) however, the rotateUpAction and rotateDownAction are not showing up at all on the Simulator. So I was wondering if there were any solutions and anyone willing to answer.
Thank you for your time. Also, this code from the class of the main character, the name of the class is "Player"

you declare let soarAnimation inside your createAnimations function, meaning it's not in scope when you call self.run(soarAnimation). Solution: declare soarAnimation as a class property. Alternate solution: have createAnimations() return the SKAction and grab it in init that way

Related

Spawn SKSpriteNode objects at random times

I am making a game where I would like to spawn obstacles at random times. The obstacles are going to make contact with the player.
Here the code that I have:
var obstacle : SKSpriteNode?
func createObstacles() {
let obstaclesArray = ["obstacle_1", "obstacle_2", "obstacle_3", "obstacle_4", "obstacle_5"]
let randomObs = obstaclesArray.randomElement()
let selectedTexture = SKTexture(imageNamed: randomObs!)
obstacle = SKSpriteNode(imageNamed: randomObs!)
obstacle?.physicsBody = SKPhysicsBody(texture: selectedTexture, size: selectedTexture.size())
obstacle?.position = CGPoint(x: scene!.size.width/2 + selectedTexture.size().width, y: -120)
obstacle?.zPosition = -15
if let obs = obstacle?.physicsBody {
obs.affectedByGravity = true
obs.allowsRotation = false
obs.isDynamic = true
obs.restitution = 0
obs.categoryBitMask = obstacleCategory
obs.collisionBitMask = floorCategory
obs.contactTestBitMask = heroCategory
}
addChild(obstacle!)
}
func spawnObstacles() {
let wait = SKAction.wait(forDuration: 1, withRange: 0.4)
let spawn = SKAction.run {
createObstacles()
}
let moveLeft = SKAction.moveBy(x: -scene!.size.width - obstacle.size.width - 10, y: 0, duration: 2)
let sequence = SKAction.sequence([wait, spawn, moveLeft, SKAction.removeFromParent()])
self.run(SKAction.repeatForever(sequence))
}
I read some a similar questions but the response is my same code but it is not working.
Spawning a Spritekit node at a random time
I also tried other way:
var randDelay = Double.random(in: 0.7 ..< 1.4)
DispatchQueue.main.asyncAfter(deadline: randDelay, execute: {
if self.canCreateObstacle == true {
self.spawnObstacle()
}})
But it is not working and everytime I restart the game it seems like the function is being called two times, if I restart the game a third time it is called 3 times and so on.
Anyone with a good and clean solution to spawn objects at random times?
Do not use DispatchQueue Set up a repeating sequential action using wait(forDuration:withRange:) like you previously had.
https://developer.apple.com/documentation/spritekit/skaction/1417760-wait
First, create a generic node used to spawn obstacles, then attach this generic node to the scene.
Finally assign the repeating sequential action to this node.
Boom, you are done.
The reason why you want to assign it to a random node is because you want to be able to give your game the opportunity to stop generating obstacles, plus you alter the speed property to make the node generate nodes faster or slower.
You also want to detach the spawning/waiting from the moving/destroying, because as of right now, your code is confused. You are saying move the scene left for 2 seconds, then wait a random amount of time to spawn the next enemy, but I think you are trying to just spawn enemies on a time interval and move the enemy to the left.
Your scene code should look something like this
class GameScene : SKScene{
let obstacleGenerator = SKNode()
func didMove(to: view){
let wait = SKAction.wait(forDuration: 1, withRange: 0.4)
let spawn = SKAction.run({createObstacles()})
let sequence = SKAction.sequence([wait, spawn])
obstacleGenerator.run(SKAction.repeatForever(sequence))
addChild(obstacleGenerator)
}
func createObstacles() {
let obstaclesArray = ["obstacle_1", "obstacle_2", "obstacle_3", "obstacle_4", "obstacle_5"]
let randomObs = obstaclesArray.randomElement()
let selectedTexture = SKTexture(imageNamed: randomObs!)
obstacle = SKSpriteNode(imageNamed: randomObs!)
obstacle.position = CGPoint(x: scene!.size.width/2 + selectedTexture.size().width, y: -120)
obstacle.zPosition = -15
let body = SKPhysicsBody(texture: selectedTexture, size: selectedTexture.size())
body.affectedByGravity = true
body.allowsRotation = false
body.isDynamic = true
body.restitution = 0
body.categoryBitMask = obstacleCategory
body.collisionBitMask = floorCategory
body.contactTestBitMask = heroCategory
obstacle.physicsBody = body
addChild(obstacle!)
let moveLeft = SKAction.moveBy(x: -scene!.size.width - obstacle.size.width - 10, y: 0, duration: 2)
let seq = SKAction.sequence([moveLeft,SKAction.removeFromParent()])
obstacle.run(seq)
}
}
Now as for your spawning increasing with each reset, you never post how you are resetting. I am going to assume you never remove the previous action upon reset, and this is why your rate increases. It is always better to just create a new GameScene instance when doing a reset.

Remove a function from SKScene when Touches Begin

I have a blue sky texture moving from the top of the screen to the bottom, repeating forever in a function. However, when touches begin, I would like the blue sky to completely fade out after a duration, then completely remove from the scene.
I am able to get the sky to fade out momentarily, then it seems as though half of the loop is removed, but I still get the sky coming down every other time. Here's a video of the problem: Blue Sky Video
In the video, touches begin when the pause button appears at the top left of the screen - before touches begin, the blue sky moves from top to bottom seamlessly as it should.
FUNCTION
func createBlueSky() {
let blueSkyTexture = SKTexture(imageNamed: "blueSky")
for i in 0 ... 1 {
let blueSky = SKSpriteNode(texture: blueSkyTexture)
blueSky.name = "blueSky"
blueSky.zPosition = -60
blueSky.anchorPoint = CGPoint.zero
blueSky.position = CGPoint(x: 0, y: (blueSkyTexture.size().height * CGFloat(i)))
worldNode.addChild(blueSky)
let moveDown = SKAction.moveBy(x: 0, y: -blueSkyTexture.size().height, duration: 10)
let moveReset = SKAction.moveBy(x: 0, y: blueSkyTexture.size().height, duration: 0)
let moveLoop = SKAction.sequence([moveDown, moveReset])
let moveForever = SKAction.repeatForever(moveLoop)
blueSky.run(moveForever)
}
}
I've added the above function to didMove(toView)
override func didMove
createBlueSky()
Then in my touches began I added this code to fade out and remove from parent.
I saw in another post that to access the blue sky from another function, I'd need to give it a name, which I did. Still no luck :/
override func touchesBegan
let blueSky = worldNode.childNode(withName: "blueSky") as! SKSpriteNode
let blueSkyFade = SKAction.fadeOut(withDuration: 3)
let blueSkyFadeWait = SKAction.wait(forDuration: 3)
let removeBlueSky = SKAction.removeFromParent()
blueSky.run(SKAction.sequence([blueSkyFadeWait, blueSkyFade, removeBlueSky]))
I'm very new to Swift, but I hope the question was specific enough. I'm happy to provide any other necessary information.
You are creating 2 blueSky nodes, but you only fade and remove 1 of them. You need to enumerate through all of the nodes with the name you are looking for. To do this, you call enumerateChildNodes(withName:}
let blueSkyFade = SKAction.fadeOut(withDuration: 3)
let blueSkyFadeWait = SKAction.wait(forDuration: 3)
let removeBlueSky = SKAction.removeFromParent()
enumerateChleNodes(withName:"blueSky")
{
(blueSky,stop) in
blueSky.run(SKAction.sequence([blueSkyFadeWait, blueSkyFade, removeBlueSky]))
}

How to scale to be at the same distance in all devices?

I'm having problems opening my game in different divices , in the 6s iphone plus looks much bigger the circle the center, also the small circle that is on the line changes position , I would like that the center circle was the same size and that the small circle always this half on the line.
import SpriteKit
struct Circle {
var position:CGPoint
var radius:CGFloat
}
class GameScene: SKScene {
let node = SKNode()
let sprite = SKShapeNode(circleOfRadius: 6)
var rotation:CGFloat = CGFloat(M_PI)
var circles:[Circle] = []
var circuloFondo = SKSpriteNode()
var orbita = SKSpriteNode()
let padding2:CGFloat = 26.0
let padding3:CGFloat = 33.5
let padding5:CGFloat = 285.5
var circulo = SKSpriteNode()
override func didMoveToView(view: SKView) {
scaleMode = .ResizeFill
backgroundColor = UIColor(red: 0.3, green: 0.65, blue: 0.9, alpha: 1)
orbita = SKSpriteNode(imageNamed: "orbita2")
orbita.size = CGSize(width:view.frame.size.width - padding2 , height: view.frame.size.width - padding2)
orbita.color = UIColor.whiteColor()
orbita.colorBlendFactor = 1
orbita.alpha = 1
orbita.position = view.center
self.addChild(orbita)
orbita.zPosition = 3
circuloFondo = SKSpriteNode(imageNamed: "circuloFondo")
circuloFondo.size = CGSize(width:view.frame.size.width - padding5 , height: view.frame.size.width - padding5)
circuloFondo.color = UIColor.whiteColor()
circuloFondo.alpha = 1
circuloFondo.position = view.center
self.addChild(circuloFondo)
circuloFondo.zPosition = 0
let radius1:CGFloat = (view.frame.size.width - padding3)/2 - 1
let radius2:CGFloat = (view.frame.size.width - padding5)/2 + 6.5
circles.append(Circle(position: view.center, radius: radius1))
circles.append(Circle(position: view.center, radius: radius2))
addChild(node)
node.addChild(sprite)
if let circle = nextCircle() {
node.position = circle.position
sprite.fillColor = SKColor.whiteColor()
sprite.zPosition = 4.0
sprite.position = CGPoint(x:circle.radius, y:0)
rotate()
}
You can get the width of the screen like this:
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
and then set elements in your UI to be a proportion of the screenWidth. For instance:
let radius1:CGFloat = screenWidth/4
//this would always give you a radius that is one quarter of the screen width
I've used this method a few times with success, hope it works for you.
Your main problem is your scene mode is set to ResizeFill. This will cause you so many headaches as you will have to do all the scaling yourself, hence the circles are different sizes on different devices. Having scale mode resizeFill will also affect things such as the physics engine or fontSizes which you will need to adjust for on every device.
I would recommend you use scene scale mode .AspectFill with the default scene size of 1024*768 (landscape) or 768*1024 (portrait). This is the same as the xCode default game template.
This way everything will look exactly the same on all iPhones. On iPads there will be slightly more screen space at the top and bottom which you simply cover with your background. The main trick is that you position your stuff from the center.
Furthermore you can use the universal assets in the asset catalogue and everything will look great and not blurry.
The only thing that you might have to adjust for this way is that on iPads you might need to move some buttons up/down if you want them on the top/bottom edge.
I strongly recommend you consider this as I can talk from experience that using scale mode ResizeFill is really bad. I have been through this pain with 2 games before I rewrote them because they were so inconsistent on all devices causing me so many bugs in the process. Lets not talk about the time I wasted testing on all devices, adjusting values until it felt right.
Hope this helps.

Spawning an Enemy

In my game I want an enemy to spawn every 10 seconds. I attempt to accomplish this by, in the GameViewController, writing
var secondEnemyTimer = NSTimer.scheduledTimerWithTimeInterval(10.0, target: self, selector: "secondEnemyFunction", userInfo: nil, repeats: false)
in the viewWillLayoutSubviews method. Then in the secondEnemyFunction I write:
let skView = self.view as! SKView
let gameScene = GameScene(size: skView.bounds.size)
gameScene.enemy2Function()
Then in the enemy2Function in the GameScene class I write:
println("Called!")
enemy2.name = enemyCategoryName
enemy2.size.width = 57
enemy2.size.height = 57
let randomX = randomInRange(Int(CGRectGetMinX(self.frame)), hi: Int(CGRectGetMaxX(self.frame)))
let randomY = randomInRange(Int(CGRectGetMinY(self.frame)), hi: Int(CGRectGetMaxY(self.frame)))
let randomPoint = CGPoint(x: randomX, y: randomY)
enemy2.position = randomPoint
self.addChild(enemy2)
enemy2.physicsBody = SKPhysicsBody(circleOfRadius: enemy1.size.width / 2)
enemy2.physicsBody?.friction = 0
enemy2.physicsBody?.restitution = 1
enemy2.physicsBody?.linearDamping = 0
enemy2.physicsBody?.allowsRotation = false
enemy2.physicsBody?.applyImpulse(CGVectorMake(50, -50))
enemy2.physicsBody?.categoryBitMask = enemyCategory
In the log "Called!" appear yet the enemy is not spawned. Just so you know I did create the enemy at the top of the class by doing:
let enemy2 = SKSpriteNode(imageNamed: "enemy")
Does anyone know how I can spawn my second enemy? Thank you in advance!
-Vinny
You should keep things simple and just do everything inside GameScene. Another thing is to drop NSTimer and use SKAction to spawn enemies. NSTimer don't respect scene's paused state, so you can get into some trouble eventually. This is how you can spawn enemies using SKAction:
GameScene.swift:
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
generateEnemies()
}
func stopGeneratingCoins(){
if(self.actionForKey("spawning") != nil){removeActionForKey("spawning")}
}
func generateEnemies(){
if(self.actionForKey("spawning") != nil){return}
let timer = SKAction.waitForDuration(10)
//let timer = SKAction.waitForDuration(10, withRange: 3)//you can use withRange to randomize duration
let spawnNode = SKAction.runBlock {
var enemy = SKSpriteNode(color: SKColor.greenColor(), size:CGSize(width: 40, height:40))
enemy.name = "enemy" // name it, so you can access all enemies at once.
//spawn enemies inside view's bounds
let spawnLocation = CGPoint(x:Int(arc4random() % UInt32(self.frame.size.width - enemy.size.width/2) ),
y:Int(arc4random() % UInt32(self.frame.size.height - enemy.size.width/2)))
enemy.position = spawnLocation
self.addChild(enemy)
println(spawnLocation)
}
let sequence = SKAction.sequence([timer, spawnNode])
self.runAction(SKAction.repeatActionForever(sequence) , withKey: "spawning") // run action with key so you can remove it later
}
}
When it comes to positioning, I assumed that your scene already has the correct size. If scene is not correctly initialized and has different size (or more precisely, different aspect ratio) than a view, it could happen that enemy get off-screen position when spawned. Read more here on how to initialize the scene size properly.

Endless scrolling background

I am trying to get my background to move endless from right to left.
My let:
let background = SKSpriteNode(imageNamed: "background")
let background2 = SKSpriteNode(imageNamed: "background")
my didMoveToView
background.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
background.zPosition = 2
background2.position = CGPointMake(CGRectGetMidX(self.frame)+(background.position.x/2),CGRectGetMidY(self.frame))
background2.zPosition = 2
background.setScale(2)
background2.setScale(2)
self.addChild(background)
self.addChild(background2)
var backgroundMovement = SKAction.moveByX(background.size.width, y: 0, duration: 1)
background.runAction(backgroundMovement, completion: {() -> Void in
self.background.position = CGPointZero
})
var backgroundMovement2 = SKAction.moveByX(background2.size.width, y: 0, duration: 1)
background2.runAction(backgroundMovement2, completion: {() -> Void in
self.background2.position = CGPointZero
})}
My update func is empty.
I have uploaded a picture of how it looks like when running on device: http://s18.postimg.org/kbn83tvmx/i_OS_Simulator_Screen_Shot_09_Aug_2015_23_33_39.png
The image is still on half of the screen, and the image does not move now either.
The reason your background only takes up half of the screen is because you are using the regular size of the image in pixels. You need to edit the xScale and yScale properties in order to make it make it bigger. What you need is:
background.setScale(2)
Next, to move the background sprites, use SKAction instead since you have more control over timing. The motion within update won't be consistant because, unless you have an algorithm to control exactly when update: is called, you will see that the movement speeds up or slows down. To move the background, use code that looks something like this after your didMoveToView method:
var backgroundMovement = SKAction.moveByX(background.size.x, y: 0, duration: someDuration)
background.runAction(background, completion: {() -> Void in
//place background image at start again
background.position = ...
})
Loop this code if you want the background to move continuously and edit it in any way you'd like to make it look better (like having both backgrounds move with SKAction). Hope this helps
I'm going to copy and paste my code from another game I've made. This code moves a background from right-left forever. I'll do best to explain this code.
func createBackground () {
var backgroundTexture = SKTexture(imageNamed: "bg")
var moveBackgroundByX = SKAction.moveByX(-backgroundTexture.size().width, y: 0, duration: backgroundSpeed)
var replaceBackground = SKAction.moveByX(backgroundTexture.size().width, y: 0, duration: 0)
var moveBackgroundForever = SKAction.repeatActionForever(SKAction.sequence([moveBackgroundByX, replaceBackground]))
for var i:CGFloat = 0; i < 3; i++ {
background = SKSpriteNode(texture: backgroundTexture)
background.position = CGPoint(x: backgroundTexture.size().width/2 + (backgroundTexture.size().width * i), y: CGRectGetMidY(self.frame))
background.size.height = self.frame.height
background.runAction(moveBackgroundForever)
movingObjects.addChild(background)
}
}
What you need to do is:
Move background (moveBackgroundByX). This moves the background in the X direction. You need to set a duration.
Replace background1 (replaceBackground).
Create an ACTION forever that moves background1, then replaces it automatically with NO delay.
Run a LOOP that decides how many backgrounds you will need. Maybe you might need more than 3.
Inside that loop, adjust the background.position.
Call runAction on your SKSpriteNode instance.
Add the new SKSpriteNode into your another SKView.
If you have any questions please let me know.