Wednesday, February 11, 2015

Open Source Hacking

Cocos2D is my framework of choice for games these days: its native and supports both iOS devices like the new iPhone 6, and also Android via the Apportable system which builds native NDK binaries out of Objective-C source code.
Space Bot Alpha's word balloons

Another thing to like about it is that its Open Source, which means when there's a pain point you can dive in and fix it yourself!  This is what I've been doing for the last few days, as an annoying regression appeared in Space Bot Alpha after recent updates to Cocos2D.  My speech bubbles in the current AppStore version feature coloured text, but this had stopped working in the version I'm trying to get out for release!  Also there were also problems with line splitting when the label had a width specified to fit the text into.

CCLabelBMFont is a pretty nice class: it allows making high performance labels that re-use their pre-rendered font glyphs thus avoiding overhead during those 60 frames-per-second when you might be changing the text in a label.  Changing the text simply reassigns an existing CCSprite that currently points to a texture rect to point to a different rect inside the batch-node (which is what the label is under the hood).  Performance plus!



However the re-use and hackery over the last months getting new features in to Cocos2D had meant that the old system of using getChildByTag: had gone away and an attempt to work around this had defeated the logic of being able to address individual sprites.  Also the ability to set width and subsequently wrap had been implemented by internally modifying the actual string to include more line feeds.  Basically the promise of being able to address the sprites that matched characters in the string had stopped working.

I addressed the problem by making a private sub-class of CCSprite that handles hard and soft line breaks by an enum property.  The sprites are also linked together in a list so that iterating over them doesn't require using getChildByTag:  I guess the reason it was done this way was that in Cocos2D its possible to add child nodes to anything - including the CCLabelBMFont - and you might do this if you had some graphic that you wanted to move along with the label.  The tagged children were known to be the font glyphs and not some other child that might have been added.

As well as fixing all these issues my patch adds in a function that allows addressing these child nodes via a range into the original string (which works because no extra line feeds are required) and looks like this:

    NSString *highlightString = @"even if the sound";
    NSString *testString = @"Supercalifragilisticexpialidocious even if the sound of it is something quite atrocious";
    NSRange highlightRange = [testString rangeOfString:highlightString];
    CCLabelBMFont *label = [CCLabelBMFont labelWithString:testString fntFile:@"arial16.fnt" width:currentWidth alignment:CCTextAlignmentLeft];
    [label setColor:[CCColor whiteColor]];
    NSArray *highlightChars = [label characterSpritesForRange:highlightRange];
    for (CCSprite *fontSprite in highlightChars)
    {
        [fontSprite setColor:[CCColor cyanColor]];
    }
    [label setPositionType:CCPositionTypeNormalized];
    [label setPosition:ccp(0.5f, 0.5f)];
    [[self contentNode] addChild:label];

    NSString *explanation = @"Styling font characters - color";


You can even get bold and italic or other effects using a similar technique.

    NSString *boldString = @"boldly go";
    NSString *italicString = @"new civilizations";
    NSString *testString = @"to seek out new life and new civilizations, to boldly go where no man has gone before.";
    NSRange boldRange = [testString rangeOfString:boldString];
    NSRange italicRange = [testString rangeOfString:italicString];
    CCLabelBMFont *label = [CCLabelBMFont labelWithString:testString fntFile:@"arial16.fnt" width:currentWidth alignment:CCTextAlignmentLeft];
    [label setColor:[CCColor whiteColor]];
    {
        NSArray *highlightChars = [label characterSpritesForRange:boldRange];
        CCEffectBloom *boldEffect = [CCEffectBloom effectWithBlurRadius:1 intensity:0.5 luminanceThreshold:0.1];
        for (CCSprite *fontSprite in highlightChars)
        {
            [fontSprite setEffect:boldEffect];
        }
    }
    {
        NSArray *highlightChars = [label characterSpritesForRange:italicRange];
        for (CCSprite *fontSprite in highlightChars)
        {
            [fontSprite setSkewX:20.0f];
        }

    }



My pull request on the Cocos2D Github project has been submitted so lets see what upstream Cocos2D folks make of it!