Using CALayers as HUD elements – Part I.

When creating a game, an informative and seamlessly integrated HUD is crucial to provide feedback to players. Using the built-in system fonts could really hurt the aesthetic appeal of your game and you should always do your best to make the graphical style coherent in the whole app. Using CALayers for your HUD can help you achieve this very easily!

In today’s iDevBlogADay post, I am going to show you how you can build on MysteryCoconut’s great CALayer subclass (the MCSpriteLayer) to display numbers easily. His great CALayer tutorial can be found here. The new class we create today could be used for showing scores with custom text, or any other number you might need on-screen.

First, you should create a png with an alpha channel including all digits from 1 to 0. This png will be used as an atlas to save resources. The best way to do this is, create all digits in the same bounding box area, make the digit centered in the bounding box, then copy the bounding boxes next to each other in a big png file. The atlas in our example starts from 1 and ends with 0, it has 5 columns and 2 rows.

We will use this single image to display any number which is a great way to save resources. For best results create a @2x version of the atlas as well.

Now that we have our image, we can begin coding:

1. create a new NSObject subclass and add it to your project, let’s call it NumberDisplay.


#import <Foundation/Foundation.h>

@interface NumberDisplay : NSObject {

	CALayer *displayLayer;
	CGPoint startPoint;
	float height;

	NSMutableArray *digits;

	UIImage *digitImageCache;
	CGSize digitFrameSize;
}

- (id)initOnLayer:(CALayer*) _displayLayer startAt:(CGPoint) _origin withHeight:(float) _height withImage:(UIImage*) image;

- (void) showNumber:(int) number;

- (void) hide;
- (void) display;

@end

As we can change the displayed number dynamically, we do not know the number of digits beforehand thus we store them in an NSMutableArray. We will use a startPoint to let the display know where our number should always start from on the screen, and use the height parameter to change the number’s size according to our needs.

To save memory, we will use a UIImage provided to us during creation, this way we don’t have to parse and store the same UIImage all the time we want to show a new number on our HUD. Simply we store the UIImage pointer in our HUD class and pass on the pointer to the numberdisplays we are creating.

The methods we want to implement are the following:

  • init method: prepare our class for showing onscreen
  • showNumber: simply feed a number to the class and it should display it
  • hide: when we want to hide a number we should be able to do it easily

2. Let’s implement our methods:


- (id)initOnLayer:(CALayer*) _displayLayer startAt:(CGPoint) _origin withHeight:(float) _height withImage:(UIImage*) image {

    self = [super init];

	if (self) {

		displayLayer = _displayLayer;
		startPoint = _origin;
		height = _height;

		digits = [[NSMutableArray alloc] init];

		if (image == nil) {

			digitImageCache = [UIImage imageWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"numbers_atlas" ofType:@"png"]];
		}

		else {

			digitImageCache = image;
		}

		digitFrameSize = CGSizeMake(CGImageGetWidth(digitImageCache.CGImage) / 5, CGImageGetHeight(digitImageCache.CGImage) / 2);
    }

    return self;
}

This init method is pretty self-explanatory, we store the UIImage pointer if we received one, if not we will load it up ourselves (just for fallback, should not be called under normal circumstances). We calculate the bounding box for a single digit. As you can see in the picture above, we have 5 numbers in one row, and we have two rows in total, so we divide image width and height accordingly to get our unit size.


- (void) dealloc {

	[digits release];

	[super dealloc];
}

Nothing surprising in our dealloc, and the hide method is going to be simple as well:


- (void) hide {

	for (int i = 0; i < [digits count]; i++) {

		((MCSpriteLayer*)[digits objectAtIndex: i]).hidden = YES;
	}
}

The cool part happens in our showNumber method, we simply pass an int and the number is going to be displayed with the previously set height, starting from the previously set point.


- (void) showNumber:(int) number {

	int numberOfDigits = 1;
	int numberToDivide = number;

	NSMutableArray *values = [[NSMutableArray alloc] init];

	while (numberToDivide >= 10) {

		numberOfDigits += 1;

		if (numberToDivide % 10 == 0) {

			[values addObject: [NSNumber numberWithInt: 10]];
		}

		else {

			[values addObject: [NSNumber numberWithInt: numberToDivide % 10]];
		}

		numberToDivide = numberToDivide / 10;
	}

	[values addObject: [NSNumber numberWithInt: numberToDivide]];

	if (numberOfDigits > [digits count]) {

		MCSpriteLayer *newDigit;

		for (int i=[digits count]; i<numberOfDigits; i++) {

			newDigit = [MCSpriteLayer layerWithImage:digitImageCache.CGImage sampleSize:digitFrameSize];
			[newDigit setFrame:CGRectMake(0, 0, digitFrameSize.width * height / digitFrameSize.height, height)];
			newDigit.position = CGPointMake(startPoint.x + ((float)i) * 0.9*height, startPoint.y);
			[displayLayer addSublayer: newDigit];

			[digits addObject: newDigit];
		}
	}

	else if (numberOfDigits < [digits count]) {

		while (numberOfDigits < [digits count]) {

			[((MCSpriteLayer*)[digits objectAtIndex:[digits count] -1]) removeFromSuperlayer];
			[digits removeLastObject];
		}
	}

	for (int i = 0; i < [digits count]; i++) {

		int digitValue = [[values objectAtIndex: [values count] - 1 - i ] intValue];
		[(MCSpriteLayer*)[digits objectAtIndex: i] displayFrame: digitValue];
		[(MCSpriteLayer*)[digits objectAtIndex: i] setPosition: CGPointMake(startPoint.x + ((float)i) * 0.9*height, startPoint.y)];
		((MCSpriteLayer*)[digits objectAtIndex: i]).hidden = NO;
	}

	[values removeAllObjects];
	[values release];
}

First, we get all the different digits in the number to be displayed and collect them in an array called values. After we know the number of digits to be displayed, we face 3 scenarios:

  • we already created the proper number of MCSpriteLayers (CALayer subclass) according to the number of digits to be displayed, do nothing
  • we don’t have enough MCSpriteLayers, so create as many more as we need. This happens frequently when we use the NumberDisplay class to show scores
  • we have more MCSpriteLayers than we currently need, we should remove the excess. You can face this situation when you display decreasing numbers, like a countdown, showing player position in a race etc.

When we have all the layers we need to display each digit, we go through our digits array and assign them the proper values and set their position according to their decimal value.

Finally, we clean up after ourselves to avoid any leaks and there you go! You have a way to display custom-looking numbers on any view.

A simple way to use our freshly created NumberDisplay on our view:


UIImage *imageCache = [UIImage imageWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"numbers_atlas" ofType:@"png"]];
[imageCache retain];

NumberDisplay *aNumber = [[NumberDisplay alloc] initOnLayer: self.layer startAt: CGPointMake(12, 12) withHeight: 20 withImage: imageCache];

[aNumber showNumber: 100];

This will display the number 100 on your view, starting from coordinates 12,12 with a number height of 20 pixels.

Easy, isn’t it?

This entry was posted in idevblogaday. Bookmark the permalink.

3 comments on “Using CALayers as HUD elements – Part I.

  1. Pingback: Using CALayers as HUD elements – Part I. | Bitongo | iOS dev (iPhone, iPad) | Scoop.it

  2. Pingback: Using CALayers as HUD elements – Part I. | Bitongo | iPhone and iPad development | Scoop.it

  3. Pingback: Using CALayers as HUD elements – Part II. | Bitongo