so-that-was-three-weeks-of-my-life

icon_lineup

I recently quit my job to work full-time on a mobile game. I gave myself two weeks for due diligence to research game frameworks and now, three weeks later, I'm ready to commit. Here is what I've discovered.

This is a technical post; if you're not interested in mobile game development you may want to skip this one.

Just Put It On The Screen

The game I'm writing is not technically demanding. It's 2d, the graphics are minimalist, and the input handling is dead simple. All I need are the basics: a display list, some UI controls, basic audio (I'll be doing some tricky timing stuff with the music, but I'm not expecting any support from the game framework here), and maybe some extras like tweening and sprite sheets. I wanted to get my hands dirty as soon as I could, so I drew up a shortlist and started going through some tutorials.

The Shortlist

For a 3d project Unity would have been a shoo-in, but it seemed like overkill for this game. Pure Adobe AIR is also a popular option, but I have several reasons not to choose it. The remaining candidates were:

NME is based on Haxe, which is an ActionScript-like language that compiles to several targets, including html5, iOS, and Android.

Sparrow and Starling have a nearly identical API based on the Flash API. Sparrow is Objective-C and is iOS-only; Starling is ActionScript and targets Stage3d through Adobe AIR.

Cocos2d was originally written in python but has been ported to Objective-C (“-iphone”) and C++ (“-x”).

Early Findings; Narrowing the Field

This blog post was helpful when I was assembling the shortlist; their favorite is Haxe. I was excited about Haxe/NME too, especially because I'd played with the language before, it was similar to the familiar Flash API, and it felt good to work with.

However my early experiences with Haxe gave me second thoughts, first with benchmarks targeting html5 and second struggling to get it working in Xcode on the iPhone simulator. Simple shapes displayed inconsistently across different targets, a major red flag. Another thing that made me uncomfortable about Haxe was nicely articulated in this article on transparent compilation that showed up just a week ago: all the benefits from transparency you get in CoffeeScript are completely missing from the spaghetti that Haxe produces.

I tried out Sparrow next. What a difference! The Xcode project template compiled quickly and without warnings and it just worked. The demo was smooth as silk and had 90% of the elements of a basic game, ready for the taking. This iPhone game engine roundup, though it's a few years old, had good things to say about Sparrow (it also mentions Cocos2d favorably), so this looked promising.

Starling, I decided, I could put on the back burner. My first priority is to get the game working on iOS; I can always write an Android port later on. From this discussion on the Gamua forums, I felt that easier integration with iOS-specific features like Game Center was worth the limitations in distribution. I also felt, and still feel, that multiplatform capabilities can be a liability, since they tend to support the lowest common denominator of functionality. For the record, I did briefly try to get the Citrus Engine and Starling working in Xcode without success.

For Cocos2d, which seemed like the popular choice, I focused on Cocos2d-x, the C++ port. The functionality and coding styles are nearly identical across the -iphone and -x flavors, and I wanted to test out a cross-platform option. It took a bit more work to set things up, and it was hard to find quick answers on Google, but the tutorials were straightforward and I was impressed with the flexible Actions (tweening) system. This was another option worth looking into.

Prototyping

Now that I had a couple of candidates – Sparrow and Cocos2d-x – I wanted to build a quick prototype of the game. Demos and tutorials are one thing, but when you build something a little bit more complex you quickly run into the quirks and limitations of your frameworks. I wanted to find these out early on.

Parallel Functionality (or Lack Thereof)

There are several features that are pretty standard:

  • Events and flow of control
    • Sparrow has ActionScript-like event listeners
    • Cocos2d has CCNotificationCenter and callfunc*_selector()
  • Interpolation / tweening
    • Sparrow has SPTween and SPJuggler
    • Cocos2d has CCAction and subclasses
  • Display list, UI Controls, anchor points / pivotX, pivotY
    • Cocos2d has some weird defaults – a flipped coordinate system and anchors at the center of sprites – but it turns out these actually save some time when prototyping
  • Multi-resolution support
    • Sparrow basically works out of the box; Cocos2d takes a little extra work
  • Sprite sheets / texture atlases
  • etc.

Both frameworks are missing some things, like good support for drawing vector-based paths in the style of flash.display.Graphics. Bummer!

No Really, Just Get Me Some Shapes

I need to dynamically draw shapes similar to these:

circles_patterns

I worked around Sparrow's drawing limitations by using Core Graphics to draw to a texture. Drawing simple things like circles is easy, but once you want to do anything more complicated you have to write code like this:

- (id)initWithWidth:(float)width height:(float)height
{
    if ((self = [super init]))
    {
        // ...

        SPTexture *tex = [SPTexture
            textureWithWidth:128
            height:128
            draw:^(CGContextRef context)
            {
                CGPatternCallbacks callbacks = { 0, &cb;, NULL };
                CGContextSaveGState(context);
                CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
                CGContextSetFillColorSpace(context, patternSpace);
                CGColorSpaceRelease(patternSpace);
                CGPatternRef pattern = CGPatternCreate(NULL,
                    CGRectMake(0, 0, 32, 32),
                    CGAffineTransformIdentity,
                    32, 32,
                    kCGPatternTilingConstantSpacing,
                    true,
                    &callbacks;);
                CGFloat alpha = 1.0;
                CGContextSetFillPattern(context, pattern, α);
                CGPatternRelease(pattern);
                CGContextFillRect(context, CGRectMake(0, 0, 128, 128));
                CGContextRestoreGState(context);
            }];
        SPImage *i = [SPImage imageWithTexture:tex];
        [self addChild:i];

        // ...
    }
    return self;
}

void cb(void *info, CGContextRef context)
{
    CGContextSetFillColorWithColor(context, [UIColor colorWithHex:SP_WHITE].CGColor);
    CGContextAddRect(context, CGRectMake( 0,  0, 16, 16));
    CGContextAddRect(context, CGRectMake(16, 16, 16, 16));
    CGContextDrawPath(context, kCGPathFill);
}

And that's just for drawing a simple checkerboard pattern, without even adding a mask. I can live with this, though – I'll just wrap it in a nice class of its own and only touch the code once. A bigger problem comes later when I port the game to Android, since Core Graphics only exists on iOS and Mac OS. It's less than ideal to write drawing code twice.

The situation with Cocos2d is similar, but it felt wrong to call into a proprietary API from a supposedly multiplatform codebase for functionality as basic as drawing. The built-in option for drawing is CCDrawNode, which is extremely limited and doesn't support anti-aliasing. So I looked into vector graphics libraries, decided MonkVG looked pretty good (there's even a Cocos2d-iphone project that uses it), and spent a day trying and failing to get it working.

Shaders FTW

To better understand the options I took another look at CCDrawNode. I noticed that it used shaders to draw circles. I worked a little bit with shaders in school back when the GeForce 3 was the bleeding edge (I am dating myself here), but all I remembered was something about teapots. I suspected that I'd find a solution once I reminded myself what shaders are and how they work. And I was right.

Since fragment shaders (also known as pixel shaders) allow you to do calculations on a per-pixel basis, you can easily apply image masks, compose textures, and map colors. This isn't quite the same as having a vector graphics library, but it turns out to be sufficient for my purposes. Ultimately I decided to take this hybrid approach, creating mask and pattern images by hand in Inkscape and using a custom fragment shader in a new CCSprite subclass. It took some time to get it all working, and in particular I had a hard time subclassing from CCSprite without clobbering textures later in the rendering pass and otherwise mucking up the OpenGL state. But the result is quite a bit more flexible than the Core Graphics approach, and should work on Android too. The shader program I'm using looks like this:

#ifdef GL_ES
precision lowp float;
#endif

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform sampler2D u_mask;
uniform vec4 v_foreColor;
uniform vec4 v_backColor;
uniform float f_scale;

void main()
{
    vec4 texColor = texture2D(u_texture, v_texCoord);
    vec4 maskColor = texture2D(u_mask, v_texCoord / f_scale);
    vec4 finalColor = vec4(
        texColor.r * v_foreColor.r + (1.0 - texColor.r) * v_backColor.r,
        texColor.g * v_foreColor.g + (1.0 - texColor.g) * v_backColor.g,
        texColor.b * v_foreColor.b + (1.0 - texColor.b) * v_backColor.b,
        maskColor.a * texColor.a);
    gl_FragColor = v_fragmentColor * finalColor;
}

Now that's nice and clean, especially compared to calling into a stateful platform-specific API. I haven't gone through the exercise, but I expect this shader-based approach is also possible in Sparrow with some work.

The Decision / Considerations You Probably Don't Share (But Might)

My exact situation is likely unique and my conclusions may not be universally appropriate. In particular,

  1. I don't have a team of artists, so I need to procedurally generate as much content as possible
  2. Along the same lines, I'm a one-person team, so I need to be very careful about development costs
  3. The game is simple and 2d
  4. I know people who have done a lot of work with Cocos2d from whom I can get direct advice
  5. I don't care about IDEs
  6. I am comfortable with C-like languages and I understand how memory management works, but I'm also very familiar with the Flash API

Other considerations are probably more common, especially for solo developers or small teams:

  1. How is the documentation? Can I find what I need easily in the forums or on stackoverflow?
    • Sparrow/Starling have a slight edge here
  2. Is the framework under active development? How many experts regularly answer questions on the forums and how responsive are they?
    • A few months ago I would have said Cocos2d-x in particular is at a disadvantage, but now the different Cocos2d flavors are being released in tandem
    • Sparrow/Starling do seem to have more expert users on the forums
  3. What feels good to develop in? Do the paradigms make sense?
    • Sparrow is a little bit more intuitive, but see below
  4. What is used in the industry at large? What might be useful later on in my career?
    • Cocos2d probably wins out
  5. What is your target platform?
    • Cocos2d-x seems best for multiplatform
    • Sparrow seems best for just iOS

Most of these considerations come out pretty even. But for me, a combination of #4 on my personal list, #4 and #5 on the common list, and the result of my investigation into drawing shapes put Cocos2d-x into the winner's circle.


Postscript: Incidental Observations on Process

If I don't see something right away I try to build it myself. In college, for example, I started writing a command-line calculator and gave up when a friend told me about bc. Early on in the process of writing the Cocos2d prototype I was looking for a PubSub implementation and when I didn't find one I spent several hours trying to write one myself. My C++ skills are rusty, and I had enough self-awareness to realize that what I had written was hacked together and dangerous. Then I found CCNotificationCenter and was able to throw away the garbage I had written.

Typically when this happens the problem is harder than I anticipated and someone's already solved it. This tendency is probably OK on balance because it engenders humility and a healthy appreciation for others' work and thought, not to mention a better understanding of the issues involved. The time I used on the failed PubSub implementation (and the bc clone) was time well spent. Also, sometimes these efforts don't fail.

Once I put work into something I become emotionally invested, provided everything works as advertised. I think of this as a variant of the sunk cost fallacy. Cocos2d took more time and effort to get working compared with Sparrow, and now I associate the effortful success with the harder tech. It is difficult to separate the “objective” reasons from the emotional ones. It is worth noting that often it's perfectly valid to prefer technology (or brands, food, or especially friends) for emotional reasons, since emotions code for more concrete justifications that are hard to articulate.

I feel an attraction towards unreasonably difficult problems because (1) I expect to learn by bashing my head against them and (2) I suspect the easy way runs afoul of the ease/power tradeoff. This is part of the reason I spent a day trying to get MonkVG working. I suspect, also for this reason, that Sparrow might not be as flexible as I'd like.


comments powered by