Many games have some kind of countdown to add to the jeopardy. Many of them consist of a numeric countdown because that's 'easy' to do. Next step up is to have a slowly shrinking bar that can be achieved by adjusting the size of the node. These approaches work, but they seem a little too simplistic and can be improved upon relatively easily.

My wish was for a graduated image, going from green to amber to red. Unfortunately, using a shrinking image wasn't going to work here as the graphic shrank as the node was resized. What I needed was a countdown bar that gradually truncated rather then resized.

After several attempts at doing this, the solution was simple, in theory. I needed to use an SKClipNode to clip an image. In theory, that was simple. I found several examples online but they all suffered the same problem. They basically repeated the official documentation with few useful examples to show exactly how to use it.

Countdown Bar

First thing I was going to need was a graphic for the countdwn bar. That was very straightforward to achieve in Affinity Designer for the iPad:

countdown graphic

It's a simple 500x25 image consisting of a gradient fill from green down to red.

Countdown Node

The full code for the node is pretty short:

import SpriteKit

class CountdownNode: SKSpriteNode {

    let fuel: SKSpriteNode
    var maskNode: SKSpriteNode

    override init(texture: SKTexture?, color: UIColor, size: CGSize) {

        // Fuel gradient is anchored on the left
        fuel = SKSpriteNode(imageNamed: "FuelGuage")
        fuel.anchorPoint = CGPoint(x: 0, y: 0.5)

        // The mask matches the size of the fuel. The colour
        // does not matter as it is only a mask for the Crop Node
        maskNode = SKSpriteNode(color: .white, size: fuel.size)
        maskNode.anchorPoint = CGPoint(x: 0, y: 0.5)

        super.init(texture: texture, color: color, size: size)
        self.anchorPoint = .zero

        let cropNode = SKCropNode()
        cropNode.maskNode = maskNode
        cropNode.addChild(fuel)
        cropNode.position = CGPoint(x: 0, y: 0)
        addChild(cropNode)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func percentRemaining(_ remaining: Double) {
        maskNode.size.width = fuel.size.width * remaining
    }
}

The component has two nodes that are necessary to make the countdown work. The background image is our gradient fill. We set the anchor point to (0, 0.5) so we hide the image alng the X axis.

The second node is a mask. This mask is what will be used to control how much of the background image is visible. In our case, we have defined the mask as a white rectangle, with the same anchor point as the background image and the same size. The colour of the node is actually irrelevant, so we could have coded it as red, white, blue, green or any other colour you like. The node itself will never actually be visible.

Then the magic happens. We define a third node, which is the SKCropNode:

let cropNode = SKCropNode()
cropNode.maskNode = maskNode
cropNode.addChild(fuel)
cropNode.position = CGPoint(x: 0, y: 0)
addChild(cropNode)

The SKCropNode is a container for our other nodes. The mask node, which is what we will be adjusting to crop the background image, is assigned to the crop nodes masknode property. We then add our baskground image as a child of the crop node. Finally the crop node is added to the game scene.

To get the countdown to work, the caller sets the percentremaining property to the required percentage of the game remaining. That, in turn, sets the width of the mask node.

func percentRemaining(_ remaining: Double) {
    maskNode.size.width = fuel.size.width * remaining
}

As the mask width changes, the crop node crops the background image.

In Action

In action, the countdown starts full width and slowly shrinks to nothing:

countdown in action

It is the responsibility of the caller to handle the time running out. The countdown node will justr work with the values it is given to adjust the width of the mask.

Using The Countdown Node

Using the node is simple. Firstlt, define the code in your GameScene

let countdown = CountdownNode()

As part of the didMove initialisation, set the position you desire and add the node to the game

countdown.position = CGPoint(x:  frame.minX + 60, y: frame.maxY - 70)
addChild(countdown)

Finally, somewhere in your code, calculate the percentage of the game remaining and set the countdown nodes percentRemaining property

let percentDone = max(0, CGFloat(remaining) / 100)
countdown.percentRemaining(percentDone)

As you set the percentRemaining property, the countdown image shrinks.

An added bonus is that, if the player gains extra time by winning awards, you can set the percentage remaining to a larger percentage and the countdown bar will become bigger. It is not limited to shrinking.

 

Testimonials
Testimonials

Am I really any good?

Don't take my word for my abilities, take a look at other peoples opinions about me.