Hello all. For my first legit blog post, I’d like to share a very basic progress timer I made while messing around with SpriteKit. It’s very similar to CCProgressTimer if you’ve ever used cocos2d.
In this prototype game, the player must position her units appropriately to successfully defend against waves of creeps and prevent them from reaching the end of a path. I created a nice wave indicator node that shows how long it will take for the next wave of creeps to arrive on the map. The wave indicator node’s background is gray and slowly fills with an orange color. You can see the wave indicator in this screenshot from a prototype of the game:
I decided to pack up the progress timer into an open source demo project so that you all could modify it for usage in your own SpriteKit games. The progress timer object is named TCProgressTimer.
Usage of TCProgressTimer is simple. Initialize the object using one of the two initializers
- (id)initWithForegroundImageNamed:(NSString *)foregroundImageName
- (id)initWithForegroundTexture:(SKTexture *)foregroundTexture
The background and accessory parameters are optional – you can pass nil if you don’t need a background or an accessory view on the progress timer. Once initialized, just throw the progress timer into your scene somewhere and use the setProgress: method to set the fill percentage (a value from 0.0 to 1.0).
The implementation is pretty simple as well. TCProgressTimer is a SKSpriteNode subclass which has three layers: a background texture, a foreground texture, and an accessory texture.
@interface TCProgressTimerNode ()
@property (nonatomic, strong) SKSpriteNode *backgroundImageSpriteNode;
@property (nonatomic, strong) TCProgressTimerForegroundCropNode *foregroundCropNode;
@property (nonatomic, strong) SKSpriteNode *accessorySpriteNode;
The background and accessory nodes are nothing special. The magic lies in the TCProgressTimerForegroundCropNode that is sandwiched between the two. TCProgressTimerForegroundCropNode is an SKCropNode subclass that consists of a SKSpriteNode and an SKShapeNode that is used as a mask.
@interface TCProgressTimerForegroundCropNode ()
@property (nonatomic, strong) SKSpriteNode *indicatorSpriteNode;
@property (nonatomic, strong) SKShapeNode *maskShapeNode;
All the magic is in two methods:
_maskShapeNode = [SKShapeNode node];
_maskShapeNode.antialiased = NO;
_maskShapeNode.lineWidth = _indicatorSpriteNode.texture.size.width;
self.maskNode = _maskShapeNode;
progress = 1.0f - progress;
CGFloat startAngle = M_PI / 2.0f;
CGFloat endAngle = startAngle + (progress * 2.0f * M_PI);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointZero
self.maskShapeNode.path = path.CGPath;
The SKShapeNode is initialized, given a lineWidth equal to the width of the texture provided upon initialization, and assigned as a mask node. Later, when setProgress: is called, a UIBezierPath is generated using bezierPathWithArcCenter: and the newly generated path is applied to the mask node. That’s really all there is to it!
Here’s a shot from the demo showing three versions of the progress timer. The first uses the foreground image only. The second adds a background image. And the third adds the accessory image.
I probably won’t be touching the progress timer much more because this basic implementation fit my prototyping needs. I’ve posted the complete source code wrapped up in a demo project on BitBucket.
You can download the source code here.
Note: At the time of this writing, SKShapeNode appears to leak a small amount of memory. Several other developers in the Apple dev forums have reported having the same leak with SKShapeNode. Hopefully we will get a fix soon!
Also Note: Since writing this, I’ve discovered that SKShapeNode really sucks. I wouldn’t recommend anyone use SKShapeNode for anything other than prototyping. For alternative approaches, see the source code for my new control, TCProgressBarNode.
Thanks for reading!