5
\$\begingroup\$

I was writing a game in Swift and Sprite Kit and it is very simple. It works perfectly other than the fact that it gets a bit laggy sometimes. I'd like to use this experience as both a learning tool, and to get the 'lagginess' resolved. My code is relatively short, actually:

import SpriteKit
import UIKit
import Foundation

var lastImage: UIImage?
class GameScene: SKScene, SKPhysicsContactDelegate {


let obstacleCategory : UInt32 = 0x1 << 1
let ballCategory : UInt32 = 0x1 << 0
let revolutionBarCategory : UInt32 = 0x1 << 2
var scoreLabel = SKLabelNode(fontNamed: "Heiti SC")

let revolutionBar = SKShapeNode(circleOfRadius: 25)

let circle = SKShapeNode(circleOfRadius: 20)
let largeCircle = SKShapeNode(circleOfRadius: 200)//SKSpriteNode(imageNamed: "circle")
var action = SKAction.rotateByAngle(CGFloat(M_PI), duration:2)


let leftSpeedUpLabel = SKLabelNode()
let rightSlowDownLabel = SKLabelNode()

override func didMoveToView(view: SKView) {
    leftSpeedUpLabel.text = "Touch the left half to accelerate"
    leftSpeedUpLabel.position = CGPoint(x: (self.frame.midX - 30), y: (self.frame.midY*2/3))
    leftSpeedUpLabel.fontColor = UIColor.whiteColor()
    leftSpeedUpLabel.fontSize = 20
    leftSpeedUpLabel.fontName = "Heiti SC"
    self.addChild(leftSpeedUpLabel)
    rightSlowDownLabel.text = "Touch the right half to decelerate"
    rightSlowDownLabel.position = CGPoint(x: (self.frame.midX + 30), y: (self.frame.midY*5/3))
    rightSlowDownLabel.fontColor = UIColor.whiteColor()
    rightSlowDownLabel.fontSize = 20
    rightSlowDownLabel.fontName = "Heiti SC"
    self.addChild(rightSlowDownLabel)

    revolutionBar.alpha = 0
    revolutionBar.fillColor = SKColor.whiteColor()
    revolutionBar.position = CGPoint(x: (self.frame.midX),y: (largeCircle.position.y + largeCircle.frame.height/2 - 10))
    revolutionBar.physicsBody = SKPhysicsBody(circleOfRadius: 0.5)
    revolutionBar.physicsBody?.affectedByGravity = false
    revolutionBar.physicsBody?.pinned = true
    revolutionBar.physicsBody?.contactTestBitMask = revolutionBarCategory
    self.addChild(revolutionBar)

    scoreLabel.text = "0"
    scoreLabel.fontSize = 200
    scoreLabel.position = CGPoint(x: self.frame.midX, y: self.frame.midY-75)
    self.addChild(scoreLabel)
    self.physicsWorld.contactDelegate = self
    self.physicsWorld.gravity = CGVector(dx: 0, dy: -5.8)
    self.scene!.backgroundColor = UIColor.blueColor()
    /* Setup your scene here */
    largeCircle.fillColor = UIColor.clearColor()
    largeCircle.strokeColor = SKColor.whiteColor()
    largeCircle.glowWidth = 2.0
    self.addChild(largeCircle)


    var middleX = self.scene?.frame.midX
    largeCircle.position.x = middleX!
    var middleY = self.scene?.frame.midY
    largeCircle.position.y = middleY!


    circle.position.x += largeCircle.frame.width/2
    circle.physicsBody = SKPhysicsBody(circleOfRadius: 20)
    circle.fillColor = SKColor.whiteColor()
    circle.strokeColor = SKColor.blueColor()
    circle.glowWidth = 1.0
    circle.physicsBody?.contactTestBitMask = ballCategory
    circle.physicsBody?.affectedByGravity = false
    largeCircle.addChild(circle)
    largeCircle.runAction(SKAction.repeatActionForever(action))

}




func createObstacle(){

    var ball = SKShapeNode(circleOfRadius: 20)
    var width = UInt32(self.frame.width)
    var random_number = arc4random_uniform(width)
    ball.position = CGPointMake(CGFloat(random_number), frame.height+20)
    ball.strokeColor = SKColor.blackColor()
    ball.glowWidth = 1.0
    ball.fillColor = SKColor.darkGrayColor()
    ball.physicsBody = SKPhysicsBody(circleOfRadius: 20)
    ball.physicsBody!.affectedByGravity = true
    ball.physicsBody?.contactTestBitMask = obstacleCategory
    self.addChild(ball)

}


func accelerate(){
    if(action.duration>1){
        var duration = action.duration
        duration -= 0.2
        largeCircle.removeAllActions()
        action = SKAction.rotateByAngle(CGFloat(M_PI), duration: duration)
        largeCircle.runAction(SKAction.repeatActionForever(action))
    }

}

func decelerate(){
    if(action.duration<3){
        var duration = action.duration
        duration += 0.2
        largeCircle.removeAllActions()
        action = SKAction.rotateByAngle(CGFloat(M_PI), duration: duration)
        largeCircle.runAction(SKAction.repeatActionForever(action))
    }

}

let accel = "accelerating"
let decel = "decelerating"

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
    /* Called when a touch begins */

    for touch in (touches as! Set<UITouch>) {
        let location = touch.locationInNode(self)
        if(location.x<self.scene?.frame.midX){
            if(action.duration>1){
                let wait = SKAction.waitForDuration(0.1)
                let block = SKAction.runBlock{self.accelerate()}
                let sequence = SKAction.sequence([wait,block])
                self.runAction(SKAction.repeatActionForever(sequence), withKey: accel)
            }
        }else{
            if(action.duration<3){
                let wait = SKAction.waitForDuration(0.1)
                let block = SKAction.runBlock{self.decelerate()}
                let sequence = SKAction.sequence([wait,block])
                self.runAction(SKAction.repeatActionForever(sequence), withKey: decel)
            }
        }
    }
}

override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
    self.removeActionForKey(accel)
    self.removeActionForKey(decel)
}

var probability : UInt32 = 25
override func update(currentTime: CFTimeInterval) {
    if(scoreLabel.text.toInt() >= 4){
        rightSlowDownLabel.alpha -= 0.1
        leftSpeedUpLabel.alpha -= 0.1
    }
    if(newGoToUpdateFunFun){
        UIGraphicsBeginImageContext(controller.view.frame.size)

        CGContextBeginPath(UIGraphicsGetCurrentContext())

        self.view?.drawViewHierarchyInRect(CGRect(origin: CGPoint.zeroPoint, size: UIGraphicsGetImageFromCurrentImageContext().size), afterScreenUpdates: true)
        lastImage = UIGraphicsGetImageFromCurrentImageContext()
        newGoToUpdateFunFun = false
        self.paused = true
        var mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
        var vc = mainStoryboard.instantiateViewControllerWithIdentifier("menu") as! UIViewController
        controller.presentViewController(vc, animated: true, completion: nil)
    }
    /* Called before each frame is rendered */

    var randNum = arc4random_uniform(probability)
    if ((randNum == 1) && (defaults.valueForKey("canRun")! as! NSObject == true)){
        createObstacle()
    }

    for child in self.children {
        if(child.position.y < -10){
            child.removeFromParent()
        }
    }
}



var newGoToUpdateFunFun=false


func didBeginContact(contact: SKPhysicsContact) {
    var a: SKPhysicsBody
    var b: SKPhysicsBody

    if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
        a = contact.bodyA
        b = contact.bodyB
    }else{
        b = contact.bodyA
        a = contact.bodyB
    }

    if a.contactTestBitMask == ballCategory && b.contactTestBitMask == obstacleCategory || b.contactTestBitMask == ballCategory && a.contactTestBitMask == obstacleCategory{

        defaults.setValue(false, forKey: "canRun")
        largeCircle.removeAllActions()

        var highscore: Int = defaults.valueForKey("highscore")! as! Int
        if(highscore < scoreLabel.text.toInt()){
            defaults.setValue(scoreLabel.text.toInt(), forKey: "highscore")
        }
        defaults.setValue((scoreLabel.text).toInt(), forKey: "lastscore")
        newGoToUpdateFunFun=true
        scoreLabel.alpha = 0
        rightSlowDownLabel.alpha = 0
        leftSpeedUpLabel.alpha = 0
               }

    if a.contactTestBitMask == ballCategory && b.contactTestBitMask == revolutionBarCategory || b.contactTestBitMask == ballCategory && a.contactTestBitMask == revolutionBarCategory{
        if(defaults.valueForKey("canRun")! as! NSObject == true){
            if(scoreLabel.text.toInt()! % 3 == 0){
                probability -= 3
            }

            var currentScore = (scoreLabel.text).toInt()
            var newScore = currentScore! + 1
            scoreLabel.text = String(newScore)

        }
    }
}
}

This sometimes gets laggy when I test it on an iPhone (I tested separately on iPhone 5 and 6) and I have no idea why. I have checked for infinite loops and stuff like that.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I'm going to ignore the lagginess concerns. Without you taking the time to run it through appropriate tools, like Xcode Instruments' Time Profiler and narrowing down the bottlenecks, it'd be pure guesswork. \$\endgroup\$
    – nhgrif
    Commented Jun 29, 2015 at 23:49

2 Answers 2

9
\$\begingroup\$

You've posted quite a lot of code, and as such, for now I'm going to focus simply on a big picture overview of your code (and its organization).

This is the first thing that stands out to me is that you've got property declarations interspersed through all of your method declarations. Move all of the properties to the top.

There's a lot of code in didMoveToView(), and some of it is duplicated:

leftSpeedUpLabel.text = "Touch the left half to accelerate"
leftSpeedUpLabel.position = CGPoint(x: (self.frame.midX - 30), y: (self.frame.midY*2/3))
leftSpeedUpLabel.fontColor = UIColor.whiteColor()
leftSpeedUpLabel.fontSize = 20
leftSpeedUpLabel.fontName = "Heiti SC"
self.addChild(leftSpeedUpLabel)
rightSlowDownLabel.text = "Touch the right half to decelerate"
rightSlowDownLabel.position = CGPoint(x: (self.frame.midX + 30), y: (self.frame.midY*5/3))
rightSlowDownLabel.fontColor = UIColor.whiteColor()
rightSlowDownLabel.fontSize = 20
rightSlowDownLabel.fontName = "Heiti SC"
self.addChild(rightSlowDownLabel)

Copy & paste isn't a design pattern. Any time you find yourself copy-pasting like this, it's time to create a method:

func setupLabel(#label: SKLabelNode, withText text: String, atPoint point: CGPoint) {
    label.text = text
    label.position = point
    label.fontColor = UIColor.whiteColor()
    label.fontSize = 20
    label.fontName = "Heiti SC"
    self.addChild(label)
}

And now we can simply do this:

self.setupLabel(
    label: leftSpeedUpLabel,
    withText:"Touch the left half to accelerate", 
    atPoint: CGPoint(x: (self.frame.midX - 30), y: (self.frame.midY*2/3))
)
self.setupLabel(
    label: rightSlowDownLabel,
    withText:"Touch the right half to decelerate", 
    atPoint: CGPoint(x: (self.frame.midX + 30), y: (self.frame.midY*5/3))
)

And ultimately, we should just refactor most of the code in didMoveToView() down into individual methods that didMoveToView() calls so that the method is simply fewer lines. There's no reason for an individual method to be so long.


We also have some duplication here:

func accelerate(){
    if(action.duration>1){
        var duration = action.duration
        duration -= 0.2
        largeCircle.removeAllActions()
        action = SKAction.rotateByAngle(CGFloat(M_PI), duration: duration)
        largeCircle.runAction(SKAction.repeatActionForever(action))
    }

}

func decelerate(){
    if(action.duration<3){
        var duration = action.duration
        duration += 0.2
        largeCircle.removeAllActions()
        action = SKAction.rotateByAngle(CGFloat(M_PI), duration: duration)
        largeCircle.runAction(SKAction.repeatActionForever(action))
    }

}

We can turn this into a single method.

func alterDuration(duration: NSTimeInterval) {
    var duration = action.duration + acceleration

    // ensure duration is between 1 and 3, inclusive
    duration = max(min(duration,3),1)

    // be careful of == with floating point comparisons:
    if abs(duration - action.duration) > 0.0001 {
        // if we actually changed duration
        largeCircle.removeAllActions()
        action = SKAction.rotateByAngle(CGFloat(M_PI), duration: duration)
        largeCircle.runAction(SKAction.repeatActionForever(action))        
    }
}

And now we can keep the accelerate() and decelerate() methods if we want:

func accelerate() {
    alterDuration(-0.2)
}

func decelerate() {
    alterDuration(0.2)
}

Beyond this, you have a lot of magic numbers and magic strings and other commonly used constants that should certainly be cleaned up.

\$\endgroup\$
7
  • \$\begingroup\$ Does the condensation of code (however short it is) usually reduce lag? Also, is it possible to add a video to show what is happening? \$\endgroup\$
    – Daniel
    Commented Jul 3, 2015 at 18:28
  • 1
    \$\begingroup\$ No, condensing code doesn't usually effect performance in any way whatsoever. There are tons of benefits to condensing your code though, so don't dismiss it as unimportant just because it doesn't address the lag you are seeing. \$\endgroup\$
    – nhgrif
    Commented Jul 3, 2015 at 18:46
  • \$\begingroup\$ Ok. I'll repost condensed/fixed code? \$\endgroup\$
    – Daniel
    Commented Jul 3, 2015 at 18:51
  • \$\begingroup\$ In a new question? Sure if you want. I can't address performance concerns until you've put it in a time profiler and identified bottlenecks though. \$\endgroup\$
    – nhgrif
    Commented Jul 3, 2015 at 19:01
  • \$\begingroup\$ OK I will run it through the necessary programs. \$\endgroup\$
    – Daniel
    Commented Jul 3, 2015 at 19:35
3
\$\begingroup\$

Even though @nhgrif was very helpful and his answer was very helpful in writing code in general, here is the answer that solves my specific issue:

I realized that I had gravity and an SKAction acting on a node at the same time. I had

sprite.runAction(SKAction.moveToY(-20, duration: 5))

and

 sprite.physicsBody.affectedByGravity = true

. This caused confusion on the system's side which caused the lag effect. I realized something quite obvious: that gravity and SKActions can be mutually inclusive, so beware.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.