in-which-my-frustration-leads-to-less-of-yours

This is part one in a series. If you don't see what you're looking for here, try part two.

My papa always told me when I was young to channel my frustrations into a blog post. Just kidding. But reflecting on experiences that were frustrating at the time can be oddly gratifying. And if you want to be practical, think of it as a good way to consolidate lessons learned.

I've spent the last couple of months since starting my project working with Cocos2d-x. I've gotten pretty familiar with its capabilities and quirks, and now seems like as good a time as any to document them. I'm by no means an expert, but I expect knowing about these would be useful to someone just starting out.

Flickering Sprites

Sometimes sprites flicker at the edge of the screen, especially large ones. Try using a 2D projection matrix or disabling the depth test (I had better luck with the former):

CCDirector::sharedDirector()->setProjection(kCCDirectorProjection2D);
CCDirector::sharedDirector()->setDepthTest(false);

Weird Shader Behavior

Shaders doing weird stuff and you've double- and triple-checked everything? Floating point precision might be the problem: from section 4.5.2 of the spec, lowp floats have a range of (-2, 2). That's right, you can't represent a number with magnitude bigger than 2! Use mediump instead – I didn't see a performance hit even with complex full-screen fragment shaders.

Speaking of which, complex full-screen fragment shaders are generally a bad idea, since computation time is proportional to the number of pixel affected. You can probably get the effect you're after with some combination of textures and polygons, and far more efficiently too.

CCLayer Has Weird Positioning

CCLayer and friends have the flag m_bIgnoreAnchorPointForPosition set to true. Which means that even if your anchor point is the default (0.5, 0.5) the node's origin will be the lower left corner, not the center! This is often not what you want, so you may find yourself unsetting the flag like so:

layer->ignoreAnchorPointForPosition(false);

Render Texture Issues

First, CCRenderTexture renders upside down. This is normally ok, since the CCSprite within CCRenderTexture is flipped. It becomes a problem when you want to pull out the CCTexture2D and use it directly. Earlier today I wanted to render any old CCNode to a texture, so I ended up with this:

static CCTexture2D *makeBitmapCopy(CCNode *node)
{
    CCPoint oldPos = node->getPosition();
    CCPoint oldAnchor = node->getAnchorPoint();

    // flip, since CCRenderTexture renders upside down
    node->setScaleY(-node->getScaleY());
    node->setAnchorPoint(ccp(0.0f, 1.0f));
    node->setPosition(CCPointZero);

    CCSize size = node->getContentSize();

    CCRenderTexture *rt = CCRenderTexture::create(size.width, size.height);
    rt->begin();
    node->visit();
    rt->end();

    CCTexture2D *texture = rt->getSprite()->getTexture();

    node->setScaleY(-node->getScaleY());
    node->setAnchorPoint(oldAnchor);
    node->setPosition(oldPos);

    return texture;
}

One thing I haven't figured out is that CCScale9Sprite doesn't always render exactly right in CCRenderTexture even when it renders just fine in the normal display hierarchy.

Actions Speak Louder?

CCAction and subclasses are pretty useful, but they aren't as flexible as say TweenMax in ActionScript. Fortunately it's easy to subclass your favorite CCAction (typically CCActionInterval) for custom interpolation. I've used this pattern to encapsulate the action while allowing easy access to an object's private parts:

class ThingWithLotsOfKnobs
{
    friend class KnobTwiddlerAction;

    // ...

private:
    void tweakKnobs(float param);
    float m_knobA;
    float m_knobB;
    float m_knobC;
    // ...

};

class KnobTwiddlerAction
{
    // ...
    virtual void update(float time)
    {
        ThingWithLotsOfKnobs *twiddlee = (ThingWithLotsOfKnobs *)m_pTarget;
        // call private methods
        twiddlee->tweakKnobs(time);
        // mutate private members
        twiddlee->m_knobA = sinf(time);
    }
    // ...
}

Handle with care, of course, and consider refactoring complex twiddling into a single mutator with a single parameter if possible.

Clipping

Cocos2d comes with lots of classes that are best thought of as well-tested wrappers around OpenGL functionality. CCClippingNode, for example, uses the stencil buffer to render a clipping effect like what you'd expect from a scroll container. You might not know, however, that to get the stencil buffer to work you'll need to use a pixel format that supports it (I'm using GL_DEPTH24_STENCIL8_OES as in the linked discussion). There's also a bug in CCClippingNode in the latest version of Cocos2d-x (v2.1.2) that's been fixed in Cocos2d-iphone here. Here's a patch for Cocos2d-x:

diff --git a/cocos2dx/misc_nodes/CCClippingNode.cpp b/cocos2dx/misc_nodes/CCClippingNode.cpp
--- a/cocos2dx/misc_nodes/CCClippingNode.cpp
+++ b/cocos2dx/misc_nodes/CCClippingNode.cpp
@@ -245,20 +245,19 @@ void CCClippingNode::visit()
     ///////////////////////////////////
     // CLEAR STENCIL BUFFER

-    // manually clear the stencil buffer by drawing a fullscreen rectangle on it
     // setup the stencil test func like this:
-    // for each pixel in the fullscreen rectangle
+    // for each pixel in the stencil buffer
     //     never draw it into the frame buffer
     //     if not in inverted mode: set the current layer value to 0 in the stencil buffer
     //     if in inverted mode: set the current layer value to 1 in the stencil buffer
     glStencilFunc(GL_NEVER, mask_layer, mask_layer);
+    glClearStencil(!m_bInverted ? 0 : ~0);
     glStencilOp(!m_bInverted ? GL_ZERO : GL_REPLACE, GL_KEEP, GL_KEEP);

-    // draw a fullscreen solid rectangle to clear the stencil buffer
-    //ccDrawSolidRect(CCPointZero, ccpFromSize([[CCDirector sharedDirector] winSize]), ccc4f(1, 1, 1, 1));
-    ccDrawSolidRect(CCPointZero, ccpFromSize(CCDirector::sharedDirector()->getWinSize()), ccc4f(1, 1, 1, 1));
+    glClear(GL_STENCIL_BUFFER_BIT);

     ///////////////////////////////////
     // DRAW CLIPPING STENCIL

I also found that setting the opacity of children of CCClippingNode has no effect; this may be a limitation of using the stencil buffer – if you need both alpha blending and clipping you'll have to use a different approach. (OpenGL experts, please correct me.)

Incidentally, working in Cocos2d has been a great way for me to brush up on OpenGL. But if you're completely new to the OpenGL rendering pipeline you'll probably want to read up on it.

Cocos2d is All Right

So far I'm pleased with my choice of framework. Cocos2d-x seems quite capable and relatively clean, and I've been able to navigate around the gotchas without taking too much extra time. Please note, however, the following caveats:

  • I haven't done anything with audio yet
  • I haven't tried building on Android or anything other than iOS yet
  • Performance on the iPad 1 is poor (I've only had access to iPhones 4 and 5 otherwise) and I have not yet investigated whether it's possible to fix it easily

Also note that a lot of the resources I used to deal with these quirks refer to Cocos2d-iphone rather than Cocos2d-x, but since the codebases stay in sync pretty closely it's been pretty easy to translate. Some knowledge of Objective-C helps.

If this is helpful to you or if I've gotten something wrong I'd love to hear about it – write me a comment.


comments powered by