SpriteKit world wrong positioning - ios

I'm trying to make my first game using SpriteKit & Swift. I think I understood the concept of World SKNode, but I can't make it work properly.
I add world:SKNode as a child to the GameScene. Then I add 9 child SKSpriteNodes (400px X 400px from starting from 200px X 200px point, because spritesNodes are centered). And they position perfectly, BUT I can't figure out how to get bounds of my world SKNode.
For example, how can I show left down corner of my world?
import SpriteKit
class GameScene: SKScene {
let cityPartSide:Int = 400
let startCoord:Int = 200
var world:SKNode = SKNode()
override func didMoveToView(view: SKView) {
//self.world = SKNode()
self.world.name = "world"
self.addChild(self.world)
let map:[[UInt8]] = [[16, 16, 16],
[16, 16, 16],
[16, 16, 16]]
drawMap(world, map: map)
}
func drawMap(world:SKNode, map:[[UInt8]]) {
let lineSize:Int = map[0].count
var lineCounter = map.count
while lineCounter > 0 {
lineCounter -= 1
var elemCounter = 0
while elemCounter < lineSize {
var sprite:SKSpriteNode = SKSpriteNode()
let currentPartNumb = map[lineCounter][elemCounter]
if currentPartNumb == 0 {
elemCounter += 1
continue
} else {
sprite = SKSpriteNode(imageNamed: "cell")
}
//CHOOSE IMAGE
sprite.position = CGPoint(x: startCoord + elemCounter * cityPartSide, y: startCoord + ((lineSize-1)-lineCounter)*cityPartSide)
world.addChild(sprite)
elemCounter += 1
}
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
But what I get by running this code is that vertically it is positioned perfectly, but left side of the world is somewhere out of the screen.

Oh, that was quite easy. I needed to add this
scene.size = self.view.frame.size
in GameViewController.swift's viewDidLoad like this
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
scene.size = self.view.frame.size //<---HERE
skView.presentScene(scene)
}
}
//...etc.

Related

How do you adapt your nodes position to landscape & portrait

Im trying to make a game board that is universal and can be played on ipad/iphones either in portrait or landscape. The tiles looks fine in portrait but when i switch to landscape it gets cut off. How can i make sure the board will always be centered?
override func didMove(to view: SKView) {
let numRows = 7
let numCols = 7
var counter = 0
let squareSize = CGSize(width: view.bounds.width/7, height: view.bounds.width/7)
for row in 0..<numRows{
for col in 0..<numCols {
if counter == 49 {
break
}
let spriteNode = SKSpriteNode(imageNamed: "piece\(counter)")
spriteNode.size = squareSize
spriteNode.position = CGPoint(x: 0-(frame.width*0.43)+(CGFloat(col)*spriteNode.size.width), y: -CGFloat(row)*spriteNode.size.height + frame.height/3 )
addChild(spriteNode)
counter = counter+1
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
if let scene = SKScene(fileNamed: "GameScene") {
scene.scaleMode = .resizeFill
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
It looks like this:
I would change the Scene scale mode from scene.scaleMode = .resizeFill to scene.scaleMode = .aspectFit. In a game the SKView frame size is not required and you can work within your own coordinate space (size property of the scene) e.g. set to 100 x 100. -> Interface Builder setting.
Don't relate anything on the view frame/bounds of the SKView. The code from your scene controller should work then as below, all related to the size of the scene.
override func didMove(to view: SKView) {
let numRows = 7
let numCols = 7
var counter = 0
let squareSize = CGSize(width: size.width/7, height: size.width/7)
for row in 0..<numRows{
for col in 0..<numCols {
if counter == 49 {
break
}
let spriteNode = SKSpriteNode(imageNamed: "piece\(counter)")
spriteNode.size = squareSize
spriteNode.position = CGPoint(x: 0-(size.width*0.43)+(CGFloat(col)*spriteNode.size.width), y: -CGFloat(row)*spriteNode.size.height + size.height/3 )
addChild(spriteNode)
counter = counter+1
}
}
}

How To Have Character Slide Back and Forth Automatically and Infinitely?

I'm an ubernoob developing a game using SpriteKit from scratch and im trying to make a character that will slide back and forth off the sides of the screen (in landscape mode) until collision is detected with another node (that I will add later). Think of pong and how the paddle can move side to side except I want that movement to be completely automatic/infinite.
Side Note: I plan on having this character jump when the screen is touched but continue with the back and forth movement. idk if that makes a difference in your approach.
Ok, so this answer isn't perfect but it's working for me right now so:
basically you touch the screen to spawn a boxd, and when the paddle touches the box some stuff happens (it stops moving):
import SpriteKit
// constants!
class GameScene: SKScene, SKPhysicsContactDelegate {
// A little complicated, but basically we want to have a constant speed across all screen sizes
var sliderSpeed: CGFloat { return self.size.width / 3 }
var slider = SKSpriteNode()
var sliderVelocity = CGFloat(0)
var sliderIsContacted = false
let boxMask = UInt32(2)
let sliderMask = UInt32(4)
// For use to contact slider
func spawnBox(at pos: CGPoint) {
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 45, height: 45))
let shape = SKShapeNode(rect: rect)
shape.fillColor = .green
shape.position = pos
let pb = SKPhysicsBody(rectangleOf: rect.size)
pb.categoryBitMask = boxMask
pb.contactTestBitMask = sliderMask
shape.physicsBody = pb
addChild(shape)
}
func setupSlider() {
sliderVelocity = sliderSpeed
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 45, height: 10))
let shape = SKShapeNode(rect: rect)
shape.fillColor = .blue
let pb = SKPhysicsBody(rectangleOf: rect.size)
pb.categoryBitMask = sliderMask
pb.contactTestBitMask = boxMask
pb.velocity.dx = sliderVelocity // moves our slider to the right!
// A little complicated, but basically we want to have a spritenode, not a shapenode:
slider = SKSpriteNode(texture: view!.texture(from: shape))
slider.physicsBody = pb
addChild(slider)
}
func setupWorld() {
let pb = SKPhysicsBody(edgeLoopFrom: frame)
pb.categoryBitMask = UInt32(0)
self.physicsBody = pb
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector.zero
}
override func didMove(to view: SKView) {
setupSlider()
setupWorld()
}
}
// Game loop:
extension GameScene {
// touchesBegan in iOS:
override func mouseDown(with event: NSEvent) {
let location = event.location(in: self)
spawnBox(at: location)
}
override func update(_ currentTime: TimeInterval) {
let sliderPB = slider.physicsBody!
let halfWidth = slider.size.width/2
// move slider left when it reaches far right border:
if sliderPB.velocity.dx > 0 {
if slider.position.x >= (frame.maxX - halfWidth) {
sliderVelocity = -sliderSpeed
}
}
// move slider right when it reaches far left border:
else {
if slider.position.x <= (frame.minX + halfWidth) {
sliderVelocity = sliderSpeed
}
}
// Keep slider at constant rate:
if sliderIsContacted == false {
sliderPB.velocity.dx = sliderVelocity
}
}
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask == sliderMask + boxMask {
sliderIsContacted = true
slider.physicsBody!.velocity.dx = 0 // stop slider
}
}
}
more complicated than it shoudl be, but I"m sdrunk so this is what I got :)
}hope it helps.

Simulate zero gravity style player movement

I am having a little trouble modelling the correct way to move my player node in the way
that I want.
This is my first foray into Spritekit and I have the basics up and running (I have a static background, added player node and have a playable bounds rectangle with bounds checking)
The way I have added my player movement is to track the beginning touch position and
store this in scene class scoped variable (Called beginningTouchPosition) and to also store the current touch position (Called currentTouchPosition).
I also track the players sprite node position (currentPlayerPosition)
What I do is onTouchesBegan I update 'beginningTouchPosition' and then within onTouchesMoved I update 'currentTouchPosition', this way I can know the direction the user wants his ship to move by getting the direction relative to the 'beginningTouchPosition' as he/she moves their finger around. Also the distance of 'currentTouchPosition' from 'beginningTouchPosition' determines how fast the ship moves.
I move the player in the update by creating a CGVector using the above points and using this with an SKAction.MoveBy call.
I did it this way as I wanted the user to be able to touch anywhere on the screen to be able to control movement.
How I wanted the player to move. I'd rather have the ship move by applying a certain set velocity with a set acceleration in a certain direction. So that the player will accelerate from zero to say 1 in the space of 1/2 second when the finger is moved and to continue in that direction until the finger is either moved again or lifted.
If the finger is lifted then the ship should continue moving in the last direction but to start decelerate until the velocity is back to zero.
I am basically trying to simulate how a object would move in zero gravity, with the obvious non-realistic feature of deceleration.
I've found tutorials that show how to move an object towards a finger touch but this isnt what I want as I am trying to make a game that is a side scrolling space shooter where the player can go anywhere within the playable area, as opposed to simply up and down. Similar to the old retro game 'Nemesis', see screen below:
I've attached my player class code and scene code for better visualization of how I am currently doing it all.
Any pointers to literature on how to apply velocities with acceleration in a specified direction would be helpful :)
Scene file - Level_1.swift
import SpriteKit
// Global
/*
Level_1 set up and control
*/
class Level_1: SKScene {
// Instance variables
var lastUpdateTime:NSTimeInterval = 0
var dt:NSTimeInterval = 0
var player = Player() // Sub classed SKSpriteNode for all player related stuff
var currentTouchPosition: CGPoint!
var beginningTouchPosition:CGPoint!
var currentPlayerPosition: CGPoint!
let playableRectArea:CGRect
override init(size: CGSize) {
// Constant - Max aspect ratio supported
let maxAspectRatio:CGFloat = 16.0/9.0
// Calculate playable height
let playableHeight = size.width / maxAspectRatio
// Determine margin on top and bottom by subtracting playable height
// from scene height and then divide by 2
let playableMargin = (size.height-playableHeight)/2.0
// Calculate the actual playable area rectangle
playableRectArea = CGRect(x: 0, y: playableMargin,
width: size.width,
height: playableHeight)
super.init(size: size)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToView(view: SKView) {
/* Setup your scene here */
currentTouchPosition = CGPointZero
beginningTouchPosition = CGPointZero
let background = SKSpriteNode(imageNamed: "background1")
background.position = CGPoint(x: size.width/2, y: size.height/2)
background.zPosition = -1
self.addChild(background)
currentPlayerPosition = CGPoint(x: 100, y: size.height/2)
player.position = currentPlayerPosition
self.addChild(player)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
currentTouchPosition = touch.locationInNode(self)
}
let dxVectorValue = (-1) * (beginningTouchPosition.x - currentTouchPosition.x)
let dyVectorValue = (-1) * (beginningTouchPosition.y - currentTouchPosition.y)
player.movePlayerBy(dxVectorValue, dyVectorValue: dyVectorValue, duration: dt)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
player.removeAllActions()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch: AnyObject in touches {
beginningTouchPosition = touch.locationInNode(self)
currentTouchPosition = beginningTouchPosition
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
currentPlayerPosition = player.position
if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
}else{
dt = 0
}
lastUpdateTime = currentTime
player.boundsCheckPlayer(playableRectArea)
}
}
Player node - Player.swift
import Foundation
import SpriteKit
struct PhysicsCategory {
static let None : UInt32 = 0
static let All : UInt32 = UInt32.max
static let Player : UInt32 = 0b1 // 1
static let Enemy : UInt32 = 0b10 // 2
}
class Player: SKSpriteNode{
init(){
// Initialize the player object
let texture = SKTexture(imageNamed: "ship1")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
self.xScale = 2
self.yScale = 2
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.zPosition = 1
// Player physics
self.physicsBody?.allowsRotation = false
self.physicsBody?.dynamic = false
self.physicsBody?.categoryBitMask = PhysicsCategory.Player
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Check if the player sprite is within the playable area bounds
func boundsCheckPlayer(playableArea: CGRect){
let bottomLeft = CGPoint(x: 0, y: CGRectGetMinY(playableArea))
let topRight = CGPoint(x: playableArea.size.width, y: CGRectGetMaxY(playableArea))
if(self.position.x <= bottomLeft.x){
self.position.x = bottomLeft.x
// velocity.x = -velocity.x
}
if(self.position.x >= topRight.x){
self.position.x = topRight.x
// velocity.x = -velocity.x
}
if(self.position.y <= bottomLeft.y){
self.position.y = bottomLeft.y
// velocity.y = -velocity.y
}
if(self.position.y >= topRight.y){
self.position.y = topRight.y
// velocity.y = -velocity.y
}
}
/*
Move the player in a certain direction by a specific amount
*/
func movePlayerBy(dxVectorValue: CGFloat, dyVectorValue: CGFloat, duration: NSTimeInterval)->(){
let moveActionVector = CGVectorMake(dxVectorValue, dyVectorValue)
let movePlayerAction = SKAction.moveBy(moveActionVector, duration: 1/duration)
self.runAction(movePlayerAction)
}
}
Basically we need a scene with zero gravity and a player where the touches cause force type physics actions. This is instead of moveBy type digital actions that simple move a character on the screen by such and such.
I went ahead and tested the code to try and get you something similar to what you describe. I altered some of your code a tad... to get it to work with my own set-up, as you didn't provide your GameViewController code so ask if you have any questions.
I've provided the code at the end with comments that say // IMPORTANT CODE with a # beside.
Here's details on why you use each piece of "IMPORTANT CODE
We need physics to accomplish what you describe so first ensure the player class will have a physics body. The body will be dynamic and affected by gravity (Zero Gravity), however you may want to fiddle with the gravity slightly for gameplay sake.
let body:SKPhysicsBody = SKPhysicsBody(texture: texture, alphaThreshold: 0, size: texture.size() )
self.physicsBody = body
self.physicsBody?.allowsRotation = false
self.physicsBody?.dynamic = true
self.physicsBody?.affectedByGravity = true
Since you want zero gravity we need to change our physics worlds gravity in our scene
scene?.physicsWorld.gravity = CGVectorMake(0, 0)
Next we change your movePlayerBy() to work with forces instead of simple digital movement. We do this with SKAction.applyForce.
This gives you a set-up based on force that's correlated with the swipe.
However, you may want a constant velocity no matter how hard the swipe. You can do that by normalizing the vector.. See here for somehow who asked that question and how it may apply here
(http://www.scriptscoop2.com/t/adc37b4f2ea8/swift-giving-a-physicsbody-a-constant-force.html)
func movePlayerBy(dxVectorValue: CGFloat, dyVectorValue: CGFloat, duration: NSTimeInterval)->(){
print("move player")
let moveActionVector = CGVectorMake(dxVectorValue, dyVectorValue)
let movePlayerAction = SKAction.applyForce(moveActionVector, duration: 1/duration)
self.runAction(movePlayerAction)
}
If you want the player to decelerate , we must add a function to set the player's velocity to 0. I've made it so this happens 0.5 seconds after the function is initially called.. otherwise the "floating through gravity" effect isn't really noticed as the movement would end with touchesEnded().
You can experiment with other ways to de-accelerate like a negative force of what was used initially, before the pause action in the sequence below.
There's many other ways to make it more of a true deceleration ... like a second sequence that subtracts -1 from velocity at a set time interval until it hits 0, before we hard code velocity to 0.
But, that's up to you from a gameplay standpoint.
So this should be enough to give you an idea.
func stopMoving() {
let delayTime: NSTimeInterval = 0.5 // 0.5 second pause
let stopAction: SKAction = SKAction.runBlock{
self.physicsBody?.velocity = CGVectorMake(0, 0)
}
let pause: SKAction = SKAction.waitForDuration(delayTime)
let stopSequence: SKAction = SKAction.sequence([pause,stopAction])
self.runAction(stopSequence)
}
We alter touchesEnded() to call stopMoving() .. But, try it without this to see it without that "deceleration".
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
player.removeAllActions()
player.stopMoving()
}
Other Notes.
Currently the bounds only catch the player on the left and right with the code I created... I'm not sure if that will happen in your set-up. But, as that's another question to figure out, I didn't further look into it.
Here's my code I used ... I'm providing it since I made a few other minor alterations for the sake of testing. I wouldn't worry about anything other than where I place the new important pieces of code.
GameScene.Swift
import SpriteKit
// Global
/*
Level_1 set up and control
*/
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
class Level_1: GameScene {
// Instance variables
var lastUpdateTime:NSTimeInterval = 0
var dt:NSTimeInterval = 0
var player = Player() // Sub classed SKSpriteNode for all player related stuff
var currentTouchPosition: CGPoint = CGPointZero
var beginningTouchPosition:CGPoint = CGPointZero
var currentPlayerPosition: CGPoint = CGPointZero
var playableRectArea:CGRect = CGRectZero
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// IMPORTANT CODE 2 //
scene?.physicsWorld.gravity = CGVectorMake(0, 0)
// Constant - Max aspect ratio supported
let maxAspectRatio:CGFloat = 16.0/9.0
// Calculate playable height
let playableHeight = size.width / maxAspectRatio
// Determine margin on top and bottom by subtracting playable height
// from scene height and then divide by 2
let playableMargin = (size.height-playableHeight)/2.0
// Calculate the actual playable area rectangle
playableRectArea = CGRect(x: 0, y: playableMargin,
width: size.width,
height: playableHeight)
currentTouchPosition = CGPointZero
beginningTouchPosition = CGPointZero
let background = SKSpriteNode(imageNamed: "Level1_Background")
background.position = CGPoint(x: size.width/2, y: size.height/2)
background.zPosition = -1
self.addChild(background)
// CHANGED TO Put my own texture visible on the screen
currentPlayerPosition = CGPoint(x: size.width/2, y: size.height/2)
player.position = currentPlayerPosition
self.addChild(player)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
currentTouchPosition = touch.locationInNode(self)
}
let dxVectorValue = (-1) * (beginningTouchPosition.x - currentTouchPosition.x)
let dyVectorValue = (-1) * (beginningTouchPosition.y - currentTouchPosition.y)
player.movePlayerBy(dxVectorValue, dyVectorValue: dyVectorValue, duration: dt)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
player.removeAllActions()
// IMPORTANT CODE 5 //
player.stopMoving()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
print("touch")
for touch: AnyObject in touches {
beginningTouchPosition = touch.locationInNode(self)
currentTouchPosition = beginningTouchPosition
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
currentPlayerPosition = player.position
if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
}else{
dt = 0
}
lastUpdateTime = currentTime
player.boundsCheckPlayer(playableRectArea)
}
}
GameViewController.swift
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
Player.swift
import Foundation
import SpriteKit
struct PhysicsCategory {
static let None : UInt32 = 0
static let All : UInt32 = UInt32.max
static let Player : UInt32 = 0b1 // 1
static let Enemy : UInt32 = 0b10 // 2
}
class Player: SKSpriteNode{
init(){
// Initialize the player object
let texture = SKTexture(imageNamed: "Player1")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
self.xScale = 2
self.yScale = 2
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.zPosition = 1
// Player physics
// IMPORTANT CODE 1 //
let body:SKPhysicsBody = SKPhysicsBody(texture: texture, alphaThreshold: 0, size: texture.size() )
self.physicsBody = body
self.physicsBody?.allowsRotation = false
self.physicsBody?.dynamic = true
self.physicsBody?.affectedByGravity = true
self.physicsBody?.categoryBitMask = PhysicsCategory.Player
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// Check if the player sprite is within the playable area bounds
func boundsCheckPlayer(playableArea: CGRect){
let bottomLeft = CGPoint(x: 0, y: CGRectGetMinY(playableArea))
let topRight = CGPoint(x: playableArea.size.width, y: CGRectGetMaxY(playableArea))
if(self.position.x <= bottomLeft.x){
self.position.x = bottomLeft.x
// velocity.x = -velocity.x
}
if(self.position.x >= topRight.x){
self.position.x = topRight.x
// velocity.x = -velocity.x
}
if(self.position.y <= bottomLeft.y){
self.position.y = bottomLeft.y
// velocity.y = -velocity.y
}
if(self.position.y >= topRight.y){
self.position.y = topRight.y
// velocity.y = -velocity.y
}
}
/*
Move the player in a certain direction by a specific amount
*/
// IMPORTANT CODE 3 //
func movePlayerBy(dxVectorValue: CGFloat, dyVectorValue: CGFloat, duration: NSTimeInterval)->(){
print("move player")
let moveActionVector = CGVectorMake(dxVectorValue, dyVectorValue)
let movePlayerAction = SKAction.applyForce(moveActionVector, duration: 1/duration)
self.runAction(movePlayerAction)
}
// IMPORTANT CODE 4 //
func stopMoving() {
let delayTime: NSTimeInterval = 0.5 // 0.5 second pause
let stopAction: SKAction = SKAction.runBlock{
self.physicsBody?.velocity = CGVectorMake(0, 0)
}
let pause: SKAction = SKAction.waitForDuration(delayTime)
let stopSequence: SKAction = SKAction.sequence([pause,stopAction])
self.runAction(stopSequence)
}
}

Why does SpriteKit crash when transitioning back and forth between scenes

I checked the answer here and on other sites but didn't get a definitive solution.
I have two scenes
Scene 1:
class GameMenuScene: SKScene {
override func didMoveToView(view: SKView) {
// Add background
var background: SKSpriteNode = SKSpriteNode(imageNamed: "Starfield")
background.position = CGPointMake(-20, 0)
background.size = CGSizeMake(self.size.width + 40, self.size.height)
background.anchorPoint = CGPointZero
background.blendMode = SKBlendMode.Replace
self.addChild(background)
// Add game title
gameTitle = SKSpriteNode(imageNamed: "Title")
gameTitle.name = "Game Title"
gameTitle.xScale = scale
gameTitle.yScale = scale
gameTitle.position = CGPoint(x: CGRectGetMidX(self.frame), y: self.frame.height * 0.75)
self.addChild(gameTitle)
// Add scoreboard
scoreboard = SKSpriteNode(imageNamed: "ScoreBoard")
scoreboard.name = "Scoreboard"
scoreboard.xScale = scale
scoreboard.yScale = scale
scoreboard.position = CGPoint(x: CGRectGetMidX(self.frame), y: self.frame.height * 0.50)
self.addChild(scoreboard)
// Add play button
playButton = SKSpriteNode(imageNamed: "PlayButton")
playButton.name = "PlayButton"
playButton.xScale = scale
playButton.yScale = scale
playButton.position = CGPoint(x: CGRectGetMidX(self.frame), y: self.frame.height * 0.25)
self.addChild(playButton)
// Add menu score label
var menuScoreLabel = SKLabelNode()
menuScoreLabel.fontName = fontName
menuScoreLabel.fontSize = scoreFontsize
menuScoreLabel.text = String(score)
menuScoreLabel.position = CGPointMake(scoreboard.position.x - (scoreboard.size.width / 4), scoreboard.position.y - (scoreboard.size.height / 4))
menuScoreLabel.zPosition = 10
self.addChild(menuScoreLabel)
// Add menu top score label
var menuTopScoreLabel = SKLabelNode()
menuTopScoreLabel.fontName = fontName
menuTopScoreLabel.fontSize = scoreFontsize
menuTopScoreLabel.text = String(userDefaults.integerForKey("TopScore"))
menuTopScoreLabel.position = CGPointMake(scoreboard.position.x + (scoreboard.size.width / 4), scoreboard.position.y - (scoreboard.size.height / 4))
menuTopScoreLabel.zPosition = 10
self.addChild(menuTopScoreLabel)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
// Get touch location
var touchLocation = touch.locationInNode(self)
// Check if Play button is presssed
if playButton.containsPoint(touchLocation) == true {
//println("right node touched")
// Transition to GameScene.swift
var transition: SKTransition = SKTransition.fadeWithDuration(1)
// Configure the view.
let scene = GameScene()
let skView = self.view! as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.size = skView.bounds.size
scene.scaleMode = .AspectFill
skView.presentScene(scene, transition: transition)
}
}
}
}
Scene 2:
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// Add background
var background: SKSpriteNode = SKSpriteNode(imageNamed: "Starfield")
background.position = CGPointMake(-20, 0)
background.size = CGSizeMake(self.size.width + 40, self.size.height)
background.anchorPoint = CGPointZero
background.blendMode = SKBlendMode.Replace
self.addChild(background)
// Add mainlayer & labelHolderLayer
self.addChild(mainLayer)
self.addChild(labelHolderLayer)
// Add cannon
cannon = SKSpriteNode(imageNamed: "Cannon")
cannon.name = "Cannon"
cannon.position = CGPoint(x: CGRectGetMidX(self.frame), y: 0)
self.addChild(cannon)
// Add score label
scoreLabel.name = "Score Label"
scoreLabel.fontName = fontName
scoreLabel.fontSize = scoreFontsize
scoreLabel.text = String(score)
scoreLabel.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.size.height - scoreFontsize)
scoreLabel.zPosition = 10
self.addChild(scoreLabel)
// Add LivesDisplay
livesDisplay = SKSpriteNode(imageNamed: "Ammo5")
livesDisplay.name = "livesDisplay"
livesDisplay.position = CGPoint(x: self.size.width - livesDisplay.size.width, y: self.size.height - livesDisplay.size.height)
self.addChild(livesDisplay)
// Settings for new game
newGame()
}
func gameIsOver() {
// Set game over flag and stop movement
gameOver = 1
mainLayer.speed = 0
mainLayer.paused = true
// Add game over label
gameOverLabel.fontName = fontName
gameOverLabel.fontSize = gameOverFontsize
gameOverLabel.text = "Game Over!"
gameOverLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
gameOverLabel.zPosition = 10
labelHolderLayer.addChild(gameOverLabel)
// Set main menu top score
if score > userDefaults.integerForKey("TopScore") {
userDefaults.setInteger(score, forKey: "TopScore")
userDefaults.synchronize()
}
// Run acton sequence (wait a few seconds before transitioning)
runAction(SKAction.sequence([SKAction.waitForDuration(1), SKAction.runBlock({ () -> Void in
// Transition back to GameMenuScene
var transition: SKTransition = SKTransition.fadeWithDuration(1)
// Configure the view.
let scene = GameMenuScene()
let skView = self.view! as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.size = skView.bounds.size
scene.scaleMode = .AspectFill
skView.presentScene(scene, transition: transition)
})]))
}
}
When I initially launch the app, I can transition from scene 1 to scene 2. when the game is over, the app transitions back to scene 1. BUT when I try to go to scene 2 again, the app crashes. This only happens on a device, on the simulator, i can go back and forth without any problems.
the error I get is that a SKNode is being added that already exists. I know what it means and the error starts when I try to
// Add mainlayer & labelHolderLayer
self.addChild(mainLayer)
self.addChild(labelHolderLayer)
But I don't see why It can add the background with no problem but crash from there on. Also why does it work on the simulator but not on a device? do I really need to check in my didMoveToView if the nodes are already created?
I found out the mistake I was making.
I was instantiating the nodes outside my scene 2 class, making them global. Therefore when I went from scene 1 -> scene 2 -> scene 1 there was not problem but going then to scene 2 again caused a crash because the nodes were globally created.
Solution:
Moving the following code within the class solved the problem.
var mainLayer = SKSpriteNode()
var labelHolderLayer = SKSpriteNode()
var livesDisplay = SKSpriteNode()
var scoreLabel = SKLabelNode()

SpriteKit Lagging on iPhone 5 but not on iPhone 6

I have been working on my first SpriteKit game that perfectly works on iPhone 6 but not on iPhone 5 (iOS 8.2 on both, written in Swift). I explain my problem: there is a simple ball (a SkShapeNode) that bounces when tapping on the screen. The move is very smooth on iPhone 6 but it has some lagging on iPhone 5 but NOT the first time. The FPS is 60 all the time. I have simplified my project a maximum and I have tried everything, I really need your help so much, I am a little bit desperate. I am ready to try anything, any help would be very appreciated. Thank you so much in advance!
Here is the video (smooth the first time then lagging):
https://www.youtube.com/watch?v=xNxp2uGIJew
Here is the repository if you want to test it:
https://github.com/jonmars8/SpriteKit-Lag-iPhone5
Here is the code in the GameScene: (empty but it was the menu before simplification)
import SpriteKit
class GameScene: SKScene {
var viewController:GameViewController?
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
viewController?.presentPlayScene()
}
}
Here is the code in the PlayScene:
import SpriteKit
let PlayerCategory: UInt32 = 1 << 2
let ObstacleCategory: UInt32 = 1 << 3
class PlayScene: SKScene, SKPhysicsContactDelegate {
let world = SKNode()
var player : SKShapeNode!
var firsttap = true
var viewController : GameViewController?
override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVectorMake(0.0, -9.8)
physicsWorld.contactDelegate = self
self.createPlayer()
let obstacle = SKSpriteNode(color: SKColor.whiteColor(), size: CGSizeMake(100.0, 38.0))
obstacle.position = CGPointMake(frame.size.width / 2.0, CGRectGetMidY(frame) * 0.75 + 400)
let body = SKPhysicsBody(rectangleOfSize: obstacle.size)
body.categoryBitMask = ObstacleCategory
body.contactTestBitMask = PlayerCategory
body.collisionBitMask = PlayerCategory
body.linearDamping = 0.0
body.angularDamping = 0.0
body.dynamic = false
obstacle.physicsBody = body
self.addChild(world)
self.world.addChild(obstacle)
self.world.addChild(player)
}
func createPlayer() {
self.player = SKShapeNode(circleOfRadius: 13)
player.fillColor = SKColor.greenColor()
player.strokeColor = SKColor.blackColor()
player.lineWidth = 1
let body = SKPhysicsBody(circleOfRadius:13)
body.dynamic = false
body.linearDamping = 0.0
body.angularDamping = 0.0
body.allowsRotation = false
body.affectedByGravity = true
body.categoryBitMask = PlayerCategory
body.contactTestBitMask = ObstacleCategory
body.collisionBitMask = ObstacleCategory
player.physicsBody = body
player.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame) * 0.75)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
if firsttap {
player.physicsBody?.dynamic = true
firsttap = false
}
let touchLoc = touch.locationInNode(self)
var right = true
if touchLoc.x > CGRectGetMidX(frame) {
right = false
}
player.physicsBody?.velocity = right ?
CGVectorMake(CGFloat(-65), CGFloat(650)) :
CGVectorMake(CGFloat(65), CGFloat(650))
}
}
override func update(currentTime: NSTimeInterval) {
var pos = -player.position.y + CGRectGetMidY(frame)
if pos < world.position.y {
world.position = CGPointMake(world.position.x, pos)
}
}
func didBeginContact(contact: SKPhysicsContact) {
viewController?.presentGameScene()
}
}
Here is the code in the gameViewController:
extension SKNode {
class func unarchiveFromFile(file : NSString) -> SKNode? {
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData!)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
if file == "GameScene"{
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as GameScene
archiver.finishDecoding()
return scene
}
else {
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as PlayScene
archiver.finishDecoding()
return scene
}
} else {
return nil
}
}
}
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.presentGameScene()
}
func presentPlayScene() {
if let scene = PlayScene.unarchiveFromFile("PlayScene") as? PlayScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
//reference to self
scene.viewController = self
skView.presentScene(scene, transition: SKTransition.crossFadeWithDuration(0.5))
}
}
func presentGameScene() {
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.showsQuadCount = true
skView.showsDrawCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
//reference to self
scene.viewController = self
skView.presentScene(scene, transition: SKTransition.crossFadeWithDuration(0.5))
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
} else {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
Note: on iPhone 6 there is a very small lagging when quickly tapping on the screen at the very first beginning of the game.
I have replaced the SKShapeNode by SKSpriteNode and it hasn't changed anything.
Please help me, thank you so much in advance!
I posted my view controller code over here. I do not observe performance difference between iPhone 5 and iPhone 6. Can you try my code and let me know if the lag is still there?
import UIKit
import SpriteKit
class GameViewController: UIViewController
{
var playScene: PlayScene?
override func viewDidLoad() {
}
override func viewDidAppear(animated: Bool) {
let view = self.view as SKView
playScene = PlayScene(size: view.bounds.size)
playScene!.viewController = self
view.showsFPS = true
view.showsNodeCount = true
view.presentScene(playScene!)
}
func presentGameScene()
{
let view = self.view as SKView
playScene = PlayScene(size: view.bounds.size)
playScene!.viewController = self
view.presentScene(playScene!)
}
}
Maybe you can try to apply impulse instead of changing velocity directly:
if(right){
player.physicsBody?.velocity = CGVectorMake(0,0)
player.physicsBody?.applyImpulse(CGVectorMake(-3, 15))
}else{
player.physicsBody?.velocity = CGVectorMake(0,0)
player.physicsBody?.applyImpulse(CGVectorMake(3, 15))
}
Also I can't produce that lag with your code(but only tested on iPhone 6)...Try to experiment with update vs didSimulatePhysics though. And try to isolate the part which causing the lag. Delete the code from update method to see what causing the lag (ball bouncing or moving the world node or something else).

Resources