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.
-
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– nhgrif2015年06月29日 23:49:49 +00:00Commented Jun 29, 2015 at 23:49
2 Answers 2
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.
-
\$\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– Daniel2015年07月03日 18:28:35 +00:00Commented 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– nhgrif2015年07月03日 18:46:45 +00:00Commented Jul 3, 2015 at 18:46
-
\$\begingroup\$ Ok. I'll repost condensed/fixed code? \$\endgroup\$Daniel– Daniel2015年07月03日 18:51:18 +00:00Commented 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– nhgrif2015年07月03日 19:01:05 +00:00Commented Jul 3, 2015 at 19:01
-
\$\begingroup\$ OK I will run it through the necessary programs. \$\endgroup\$Daniel– Daniel2015年07月03日 19:35:09 +00:00Commented Jul 3, 2015 at 19:35
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.