4

SO I created a game where you tap balls to make them jump up. You get a point for every tap. Why do my sprites start flashing when I reach 10 points. I'm thinking it has something to do with the update(timeInterval) method. It may be a bug or bad coding.

import SpriteKit

class GameScene: SKScene {
let backgroundNode = SKSpriteNode(imageNamed: "redbackground")
override func didMoveToView(view: SKView) {



    backgroundNode.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
    self.addChild(backgroundNode)

 //=======Ball 1=======//
    let ball = Ball()

    ball.position = CGPoint(x:self.frame.midX, y:440)

    addChild(ball)



    ball.physicsBody = SKPhysicsBody(circleOfRadius: 90)
    ball.physicsBody?.dynamic = true
    ball.physicsBody?.allowsRotation = false

    ball.physicsBody?.friction = 0
    ball.physicsBody?.angularDamping = 0
    ball.physicsBody?.linearDamping = 0
    ball.physicsBody?.usesPreciseCollisionDetection = true
    ball.physicsBody!.categoryBitMask = 0
}

class Ball: SKSpriteNode {



    init() {
        let texture = SKTexture(imageNamed: "Ball")
        super.init(texture: texture, color: .clearColor(), size: texture.size())
        userInteractionEnabled = true

    }


    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let scene = self.scene as! GameScene
        scene.score += 1

        physicsBody?.velocity = CGVectorMake(0, 100)
        physicsBody?.applyImpulse(CGVectorMake(0, 900))



    }

 override func update(currentTime: CFTimeInterval) {


    if (score >= 10){
        self.backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
    }


    if (score >= 20){
        self.backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
    }


    if (score >= 30) {
        self.backgroundNode.texture = SKTexture(imageNamed: "greenbackground")
    }

}

See this to see what I mean.(Click or tap link)

This shows the sprites flashing at 10 points

thanks in advance, Jordan

DrPepperGrad
  • 361
  • 1
  • 4
  • 17

3 Answers3

4

Instead of changing the background image in update, you can add a property observer to score that changes the background only when the score reaches specific values. For example:

let backgroundNames = ["orangebackground","yellowbackground","greenbackground"]
// The didSet observer is called when the score is updated
var score:Int = 0 {
    didSet {
        // Check if score is divisible by 10
        if score % 10 == 0 {
            // Determine the array index
            let index = score / 10
            // Wrap to avoid index out of range
            let name = backgroundNames[index % backgroundNames.count]
            print (name)
        }
    }
}

That said, the flashing could be caused by the order in which the nodes are rendered. From Apple's documentation,

...with [ignoresSiblingOrder = true], you cannot predict the rendering order for nodes that share the same height. The rendering order may change each time a new frame is rendered.

You can resolve this by setting the zPosition of the background to a negative value, for example

self.backgroundNode.zPosition = -100

so the background is always drawn before the game nodes.

Epsilon
  • 1,016
  • 1
  • 6
  • 15
  • Although I didn't choose this as the answer, I did use your advice about adding a `.zposition` to the backgroundNode. Thank you. – DrPepperGrad Aug 12 '16 at 13:50
  • one problem with this is you do not cover the possibility of the score incrementing by 2 or more – Knight0fDragon Aug 12 '16 at 14:20
  • @Knight0fDragon clearly, the key to this approach is to use a property observer instead of changing the background in `update`. – Epsilon Aug 12 '16 at 18:52
  • Absolutely, I was just saying, if score went from 9 to 11, your code would fail, I still gave you the upvote – Knight0fDragon Aug 12 '16 at 18:55
3

It is flashing because of this

override func update(currentTime: CFTimeInterval) {
    if (score >= 10){
        self.backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
    }


    if (score >= 20){
        self.backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
    }


    if (score >= 30) {
        self.backgroundNode.texture = SKTexture(imageNamed: "greenbackground")
    }

}

Infact inside the update method, when the score become >= 10, you are updating the texture EVERY FRAME.

This is absolutely wrong.

If you want to update the texture just do it when you need to change it (e.g. when the score goes from 9 to 10, or from 19 to 20 or from 29 to 30.).

But there's no point in updating the texture when it stays at 10 of when it goes from 10 to 11 since you will update it with another texture which is the same.

How can you do it?

Just create a shouldUpdateTexture: Bool = false property, then each time you update the score, if the score goes from 9 to 10 or 19 to 20 or 29 to 30 then turn shouldUpdateTexture to true.

Finally inside the update method check if shouldUpdateTexture is true, in this case set it back to false and update the texture.

Full code

class GameScene: SKScene {

    private var score = 0 {
        didSet {
            shouldUpdateTexture = oldValue < 10 && score >= 10 || oldValue < 20 && score >= 20 || oldValue < 30 && score >= 30
        }
    }
    private var shouldUpdateTexture = false
    private var backgroundNode = SKSpriteNode(imageNamed: "defaultbackground")

    
    override func update(currentTime: CFTimeInterval) {
        if shouldUpdateTexture {
            shouldUpdateTexture = false
            switch score {
            case 10...19: backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
            case 20...29: backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
            case 30...Int.max: backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
            default: break
            }
        }
    }

}

That's it.

Update

As @Knight0fDragon pointed out, it would probably be faster moving the code I put into the update method into the didSet.

Pros: this way you don't need to perform a boolean equality check every frame

Cons: if you change the score value multiple times within the same frame you will perform multiple loads of the texture.

Community
  • 1
  • 1
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
  • Whenever you use the update method, it goes 60 fps, meaning it's messing up the sprites(ball)? How do you implement the `shouldUpdateTexture` property – DrPepperGrad Aug 12 '16 at 02:49
  • why would you do the didset the way you are doing, just update the texture inside the didSet, and eliminate checking bools and performing switches on every update – Knight0fDragon Aug 12 '16 at 14:18
  • @Knight0fDragon: Hi, good point. However if the `score` values is changed multiple times within the same frame (it depends on game logic) we wast time because the `didSet` is executed multiple times. So I prefer moving the actual texture loading inside the update method. – Luca Angeletti Aug 12 '16 at 14:20
  • better to waste time once in a while than all the time – Knight0fDragon Aug 12 '16 at 14:21
  • @Knight0fDragon: With _all the time_ you mean the `guard shouldUpdateTexture else { return }` inside the `update` method? It's a very quick check. However I think it really depends on game logic. If the `score` value is not changed multiple times within the same frame your approach is better. On the other hand if it is changed several times then mine is faster. – Luca Angeletti Aug 12 '16 at 14:24
  • yours is not faster, because you can do a check on the texture before actually setting it. plus using the guard the way you are doing it is bad, guard should be used to protect the code from crashing, the way you are doing it prevents the code from having logic beyond your texture swapping. It should be an if check – Knight0fDragon Aug 12 '16 at 14:27
  • @Knight0fDragon: __(1)__ You showed me [here](https://stackoverflow.com/questions/37825039/swift-how-to-handle-a-lot-of-textures-in-memory) that 2 texture variables could have different memory addresses (even if they represent the same texture) so checking for textures equality won't work. __(2)__ Good point, I updated my code. – Luca Angeletti Aug 12 '16 at 14:32
  • @Knight0fDragon: I updated my answer with your suggestion of moving the code inside the `didSet`. Let me know if you think I should add something more. – Luca Angeletti Aug 12 '16 at 14:41
  • I posted what I was referring to for you to look at – Knight0fDragon Aug 12 '16 at 14:45
2

Just an alternative that allows for multiple changes in score to happen, without flooding the update function with unnecessary code

class GameScene: SKScene {

    private var score = 0 {
        didSet {
             ///if a change in the 10s happen,  then update texture
             if(score/10 > oldValue/10)
             {
                 switch score{
                     case 10...19: backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
                     case 20...29: backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
                     case 30...Int.max: backgroundNode.texture = SKTexture(imageNamed: "greenbackground")
                     default: break
                 }
            }
        }
    }
    private var shouldUpdateTexture = false
    private var backgroundNode = SKSpriteNode(imageNamed: "defaultbackground")



}

Then for optimization: (Not really needed, but may help with how you think about code)

class GameScene: SKScene {
    let bgTextures = [SKTexture(imageNamed: "orangebackground"),SKTexture(imageNamed: "yellowbackground"),SKTexture(imageNamed: "greenbackground")]

    private var score = 0 {
        didSet {
             ///if a change in the 10s happen,  then update texture
             let bgIndex = score / 10
             if((bgIndex > oldValue/10) && score < bgTextures.count * 10)
             {
                backgroundNode.texture = bgTextures[bgIndex - 1]
             }
             else
             {
                backgroundNode.texture = bgTextures[bgTextures.count - 1]

             }

        }
    }
    private var shouldUpdateTexture = false
    private var backgroundNode = SKSpriteNode(imageNamed: "defaultbackground")



}
Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44
  • Yes nice approach. Personally I just don't like the idea of directly linking huge operations (e.g. loading a texture) to potentially tiny model changes (increasing an `Int` property). I usually prefer updating the model and then start preparing the UI for the next frame. Imagine a game logic where several conditions are verified and for each condition a `score++`... oops a `score += 1` is performed. In the worst case scenario you could load 3 textures within the same frame where only the last one is actually needed (and this only gets worst if we add more _"score ranges"_). [CONTINUE] – Luca Angeletti Aug 12 '16 at 15:27
  • Of course the user will never notice a delay (unless we are talking about HUGE textures), as the user won't notice my boolean check inside the `update` method. Anyway I see your point against my decision of adding a check inside the update method. But since we are performing such a deep performance analysis we should be aware of the potential drawback in your solution too. Last thing: for most real games your solution will probably perform (slightly) better then mine, I agree. – Luca Angeletti Aug 12 '16 at 15:27
  • @appzYourLife, what are you talking about? In the version 1, it is called everytime score 10s spot changes within 1 update cycle, so the only thing different between yours and mine, is if the score goes from 9 to 20 in 1 update cycle via += 1 (in this case, the coder needs to reeavaluate his code anyway), I do 2 calls, you do one, and in the optimized version, it is a reference swap, so size would never matter anyway so 2 calls to 1 means nothing. – Knight0fDragon Aug 12 '16 at 15:42
  • Design your code to be adaptable, that is what separates the hobbyists from the professionals. Stop bad habits early on. Remember, you only have 16.666ms in the update frame, keep as much crap out of that as you possibly can, and use it when you need it. Also, do not write code you will regret later on. – Knight0fDragon Aug 12 '16 at 15:42
  • I am just analysing your code. In version __1__ you are risking multiple texture loading even when not needed. You could even load 3 textures when only the last one is used. In version __2__ you are keeping in memory 3 textures when only one is actually used. And since this is a background texture could potentially be quite big. And this only get worst if we add more node ranges (0...9, 10...19, 20...29, 30...39, 40...49). The perfect approach for every scenario does not exist. – Luca Angeletti Aug 12 '16 at 16:00
  • We only have solutions that do perform better then others under some circumstances. I was just pointing out where you code is less good then other solutions. E.g. in a game where we have 10 backgrounds and each one is the size of an iPad Pro display I would avoid keeping in memory all of them all the time. – Luca Angeletti Aug 12 '16 at 16:00
  • 1
    I have no problem admitting the drawbacks of my solution. In general suggesting several ideas and pointing out the pros/cons of each one is the best thing for the OP and future readers. – Luca Angeletti Aug 12 '16 at 16:04
  • again, like I said, if its the case where += is called > 10 times in 1 update cycle, then the code needs reevaluation anyway. As for your background argument, I could go on and on about super optimizing this thing lol, the point is not to waste time and energy in the update function, it should be minimal and clean. Perform changes when they are needed, where they are needed. [Continue...] – Knight0fDragon Aug 12 '16 at 16:08
  • 1
    Make the code easy to read. In English, the logic is "When 10's spot in score changes, update background" It is easier to find the logic where it happens (When score changes) instead of going to where score changes, then going to update function, then going to potentially another function because your update is too big you needed to make it more readable. I am not arguing your way is wrong, I am just pushing to not do all code in the update method, save those precious cycles – Knight0fDragon Aug 12 '16 at 16:10
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/120822/discussion-between-appzyourlife-and-knight0fdragon). – Luca Angeletti Aug 12 '16 at 16:10