In Cocos2D you display text, any number of ways. A popular method for displaying text is to use BitmapFont, that is – a font created in a program which outputs two files. A bitmap (say a png for example), and a plain text ‘.fnt’ file, containing information regarding where each character is in that file.
There are many programs that do this well, I use Heiro which is a free java based bitmap font creator.
A benefit of these programs as well, is that can ‘bake’ in certain effects such as an inset/glow/drop shadow – on to the text so it doesn’t have to be calculated every frame.
The problem with that however, is that it requires a lot of ‘padding’ on the font so the glow from the letter ‘A’ doesn’t spill to the letter ‘B’.
So you end up with something that looks like this


However what you would really like is something like this:

Much better!
So how do we get that, well my method was to implement a iVar ‘forcedKerning’ variable into the bitmap font class – CCLabelBMFont .
This left me with a problem, when i upgraded Cocos2D, i over wrote my changes, and also for a while before that i was afraid to upgrade Cocos2D library version because of what i would break. So i started trying to figure out a different way to implement it using Categories.
Storing a variable using categories:
The Main problem, was that I needed to store the variable ‘forcedKerning’ for each instance of the class, however I did not want to create a global dictionary or the like, and you cannot create iVars in a category.
Since iOS 3.1, and OSX 10.6 there is something called ‘objc_getAssociatedObject‘ and ‘objc_setAssociatedObject‘
Which allows you to, essentially create a dynamic property to an existing class.
The ‘get’ takes two parameters, the object itself (the class instance), and a void UNIQUE pointer. Because that is used as the key. So the other bit of trickiery was figuring out a pointer, i can use that i can then access in a different method. Namely the setter.
The trick was to use @selector(setMyKey) as the pointer to the property!
Putting it together, how to force extra kerning with Cocos2D Bitmap font!
Update for Cocos2D 1.0.x, update by orninn
// CCLabelBMFont+Kerning.m
// Junny
//
// Created by Mario Gonzalez on 1/30/11.
// Copyright 2011 Whale Island Games. All rights reserved.
//
#import <objc/runtime.h>
@interface CCLabelBMFont (kerning)
-(void) createFontChars;
@property (nonatomic, assign) float forcedKerning;
@end
@implementation CCLabelBMFont (kerning)
-(float) forcedKerning {
return [objc_getAssociatedObject(self, @selector(forcedKerning)) floatValue];
}
-(void) setForcedKerning:(float)aValue {
NSNumber *aNumber = [NSNumber numberWithFloat:aValue];
objc_setAssociatedObject(self, @selector(forcedKerning), aNumber, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self createFontChars];
}
-(void) createFontChars
{
int nextFontPositionX = 0;
int nextFontPositionY = 0;
unichar prev = -1;
int kerningAmount = 0;
CGSize tmpSize = CGSizeZero;
int longestLine = 0;
int totalHeight = 0;
int quantityOfLines = 1;
NSUInteger stringLen = [string_ length];
if( ! stringLen )
return;
// quantity of lines NEEDS to be calculated before parsing the lines,
// since the Y position needs to be calcualted before hand
for(NSUInteger i=0; i < stringLen-1;i++) {
unichar c = [string_ characterAtIndex:i];
if( c=='\n')
quantityOfLines++;
}
totalHeight = configuration_->commonHeight_ * quantityOfLines;
nextFontPositionY = -(configuration_->commonHeight_ - configuration_->commonHeight_*quantityOfLines);
for(NSUInteger i=0; i<stringLen; i++) {
unichar c = [string_ characterAtIndex:i];
NSAssert( c < kCCBMFontMaxChars, @"BitmapFontAtlas: character outside bounds");
if (c == '\n') {
nextFontPositionX = 0;
nextFontPositionY -= configuration_->commonHeight_;
continue;
}
kerningAmount = [self kerningAmountForFirst:prev second:c];
ccBMFontDef *fontDef = NULL;
unsigned int charKey = c;
HASH_FIND_INT(configuration_->BMFontHash_,&charKey, fontDef);
if(fontDef) {
CGRect rect = fontDef->rect;
CCSprite *fontChar;
fontChar = (CCSprite*) [self getChildByTag:i];
if( ! fontChar ) {
fontChar = [[CCSprite alloc] initWithBatchNode:self rectInPixels:rect];
[self addChild:fontChar z:0 tag:i];
}
else {
// reusing fonts
[fontChar setTextureRectInPixels:rect rotated:NO untrimmedSize:rect.size];
// restore to default in case they were modified
fontChar.visible = YES;
fontChar.opacity = 255;
}
float yOffset = configuration_->commonHeight_ - fontDef.yOffset;
fontChar.positionInPixels = ccp( (float)nextFontPositionX + fontDef->xOffset + fontDef->rect.size.width*0.5f + kerningAmount,
(float)nextFontPositionY + yOffset - rect.size.height*0.5f );
// update kerning
nextFontPositionX += fontDef->xAdvance + kerningAmount;
nextFontPositionX += [self forcedKerning];
prev = c;
// Apply label properties
[fontChar setOpacityModifyRGB:opacityModifyRGB_];
// Color MUST be set before opacity, since opacity might change color if OpacityModifyRGB is on
[fontChar setColor:color_];
// only apply opacity if it is different than 255 )
// to prevent modifying the color too (issue #610)
if( opacity_ != 255 )
[fontChar setOpacity: opacity_];
if (longestLine < nextFontPositionX)
longestLine = nextFontPositionX;
}
}
tmpSize.width = longestLine;
tmpSize.height = totalHeight;
[self setContentSizeInPixels:tmpSize];
}
@end
Original version (left for reference)
// Create a file called CCLabelBMFont+Kerning.m
//
// CCLabelBMFont+Kerning.m
// Junny
//
// Created by Mario Gonzalez on 1/30/11.
// Copyright 2011 Whale Island Games. All rights reserved.
//
#import <objc/runtime.h>
@interface CCLabelBMFont (kerning)
-(void) createFontChars;
@property (nonatomic, assign) float forcedKerning;
@end
@implementation CCLabelBMFont (kerning)
-(float) forcedKerning {
return [objc_getAssociatedObject(self, @selector(forcedKerning)) floatValue];
}
-(void) setForcedKerning:(float)aValue {
NSNumber *aNumber = [NSNumber numberWithFloat:aValue];
objc_setAssociatedObject(self, @selector(forcedKerning), aNumber, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self createFontChars];
}
-(void) createFontChars
{
int nextFontPositionX = 0;
int nextFontPositionY = 0;
unichar prev = -1;
int kerningAmount = 0;
CGSize tmpSize = CGSizeZero;
int longestLine = 0;
int totalHeight = 0;
int quantityOfLines = 1;
float localForcedKerning = [self forcedKerning];
NSUInteger stringLen = [string_ length];
if( ! stringLen )
return;
// quantity of lines NEEDS to be calculated before parsing the lines,
// since the Y position needs to be calcualted before hand
for(NSUInteger i=0; i < stringLen-1;i++) {
unichar c = [string_ characterAtIndex:i];
if( c=='\n')
quantityOfLines++;
}
totalHeight = configuration_->commonHeight_ * quantityOfLines;
nextFontPositionY = -(configuration_->commonHeight_ - configuration_->commonHeight_*quantityOfLines);
for(NSUInteger i=0; i<stringLen; i++) {
unichar c = [string_ characterAtIndex:i];
NSAssert( c < kCCBMFontMaxChars, @"BitmapFontAtlas: character outside bounds");
if (c == '\n') {
nextFontPositionX = 0;
nextFontPositionY -= configuration_->commonHeight_;
continue;
}
kerningAmount = (int) [self kerningAmountForFirst:prev second:c];
ccBMFontDef fontDef = configuration_->BMFontArray_[c];
CGRect rect = fontDef.rect;
CCSprite *fontChar;
fontChar = (CCSprite*) [self getChildByTag:i];
if( ! fontChar ) {
fontChar = [[CCSprite alloc] initWithBatchNode:self rectInPixels:rect];
[self addChild:fontChar z:0 tag:i];
[fontChar release];
}
else {
// reusing fonts
[fontChar setTextureRectInPixels:rect rotated:NO untrimmedSize:rect.size];
// restore to default in case they were modified
fontChar.visible = YES;
fontChar.opacity = 255;
}
float yOffset = configuration_->commonHeight_ - fontDef.yOffset;
fontChar.positionInPixels = ccp( (float)nextFontPositionX + fontDef.xOffset + fontDef.rect.size.width*0.5f + kerningAmount,
(float)nextFontPositionY + yOffset - rect.size.height*0.5f );
// update kerning
nextFontPositionX += configuration_->BMFontArray_[c].xAdvance + kerningAmount;
nextFontPositionX += localForcedKerning;
prev = c;
// Apply label properties
[fontChar setOpacityModifyRGB:opacityModifyRGB_];
// Color MUST be set before opacity, since opacity might change color if OpacityModifyRGB is on
[fontChar setColor:color_];
// only apply opacity if it is different than 255 )
// to prevent modifying the color too (issue #610)
if( opacity_ != 255 )
[fontChar setOpacity: opacity_];
if (longestLine < nextFontPositionX)
longestLine = nextFontPositionX;
}
tmpSize.width = longestLine;
tmpSize.height = totalHeight;
[self setContentSizeInPixels:tmpSize];
}
@end
For now i’m duplicating the original createFontChars method, via copy paste, this is a big nono – but im looking to undo that, and it’s still much better than modifying the Libraries class file and being stuck to it forever.
Tags: categories, cocos2d, howto, obj-c-runtime