I scribbled the first sketch of Bobo on a piece of construction paper. My first concern was to come up with a robot that would be easy to animate. It looked like this:
Bobo was constructed from simple parts that only needed to be translated and rotated to convey movement and behavior. However despite my best initial hopes, Bobo turned out quite a bit more complicated in the end. When Dean, an incredibly talented professional illustrator from San Diego, got a hold of the concept, he came up with a very different and infinitely cuter version of the robot:
However as a consequence of the visual boost, the simple 2D character acquired a faux 3D look complete with a bulbous head, a key rotating in and out of the display plane, and arms that extended and retracted to snake all over the page. Yikes! But I couldn’t deny that Bobo looked adorable and so the idea of using only a couple of sprites from which to build our little hero went right out of the window.
Problem 1: The bulbous head
Despite the 2D nature of the robot, Bobo’s face is in perspective. When looking left, Bobo’s left eye is smaller than the right. The left eye is slightly rotated counter-clockwise, while his right eye is slightly rotated clockwise (following the contour of his tapered head). If Bobo needs to turns his face in the other direction, the eyes and the mouth need to sweep an arc to their new positions which have the reverse scaling and rotation applied. If he looks up, the eyes and the mouth once again need to follow the contour of the head to give his bulb the appropriate sense of shape.
To model this fairly complicated movement, I came up with the following system. I created a hierarchy of empty CCNodes that correspond to the various features of the face – one CCNode for each eye, both parented to a CCNode representing the nose, another CCNode for the mouth, and an uber-parent CCNode that represented the face as a whole. All of these nodes live in a coordinate space of a square from (-1, -1) to (1, 1), with the center being smack at (0, 0). The idea was that if I wanted Bobo to look left, I’d animate the position of the face CCNode to move to (-0.5, 0). If I wanted Bobo to look right, I’d animate the position to (0.5, 0). Since the eyes and the mouth were descendants of the face node, they’d follow.
CCNodes themselves have no visuals associated with them. So, I still needed to create a set of CCSprites to display the eyes, the eye lids, and the mouth, each of which was associated with its corresponding empty CCNode. Every frame, I looked up the position on a given empty CCNode in the (-1, -1) – (1, 1) square and I applied a 3D transform to it to convert it to a position mapped along a tapered 3D cylinder. I offset this cylindrical position by the position and rotation of Bobo’s body and voila! Bobo looked around his virtual world in 3D coordinates while I retained the ability to animate his face with simple CCAction statements in 2D.
Problem 2: The turning key
One way to fake the turning of Bobo’s key in 3D would be to illustrate a handful of static frames, each representing a slightly different rotation of the key, and then use these frames to produce the desired effect. The problem here was that since the animation of the rest of the robot was procedural, a frame-based component would immediately jump out as jerky. Similarly, I wanted to be able to significantly slow the key down or speed it up to convey Bobo’s excitement or boredom and having only a limited number of frames to choose from would limit my ability to do so. So, once again, I embarked on faking 3D.
Fortunately, this one wasn’t too hard. The key consists of three components – the top leaf, the bottom leaf, and the key rod. If you set up the anchor point of the top leaf to be at the bottom of the image and the anchor point of the bottom leaf to be at the top of the image, you can simply scale the image in the y-axis to create an illusion of rotation. Add subtle scaling in the x-axis as well, depending on whether the leaf is pointing towards you or away from you, and the illusion gets even better. The code for scaling looks like this:
float keyPos = some value from 0..1; float angle = keyPos * M_PI * 2.0f; topLeaf.scale = ccp(1 + 0.1f * sinf(angle), cosf(angle)); bottomLeaf.scale = ccp(1 - 0.1f * sinf(angle), cosf(angle));
To make this fake 3D look even better, I darken the color of the leaves when they are at a 45 degree angle, to simulate a reflection off of some distant light source:
float brightnessF = cosf(angle) < 0 ? (sinf(angle - M_PI_4) + 1) * 0.5f : (sinf(angle - M_PI_4 + M_PI) + 1) * 0.5f; const float LOW_BRIGHTNESS = 140; const float HIGH_BRIGHTNESS = 256; float brightnessScaled = HIGH_BRIGHTNESS * brightness + LOW_BRIGHTNESS * (1.0 - brightness); GLubyte b = (GLubyte) MIN(255, MAX(0, brightnessScaled)); topLeaf.color = ccc3(b, b, b); bottomLeaf.color = ccc3(b, b, b);
So, why go through all this trouble when I could have just implemented the rotation in pure 3D? A couple of reasons: I would need to write my own version of CCSprite that would support 3D or resort to using Cocos3D on a separate layer which would make z-ordering difficult and would consume an additional glDraw() call. I would also need to switch the OpenGL camera from orthographic matrix to a perspective matrix. That would, in turn, make all the text rendered through OpenGL appear fuzzy which was the opposite of what I needed. So, all in all, faking 3D rotation was a more straight forward solution in this case.
Problem 3: Retractable arms
Having a robot with hoses for arms, once brought up by Dean, became a very important feature of the character. For one, hose arms are very iconic of 50’s sci-fi comic books and they underline the mechanical workings of the robot. For the other, they are very flexible because they allow the robot to interact with objects all over the screen.
It took some iterating on the development side of things to come up with the right look and feel. I tried a number of approaches – vertlet rope simulation, dangling Chipmunk bodies distributed along a curve, a path following a free-form curve, … but nothing felt quite right. The more complicated simulation I devised, the more unnatural the behavior seemed. In the end I scrapped all of these approaches and went with a simple, 3-point cubic Bézier curve.
Cubic Béziers are defined thusly:
Now, if you are like me, instead of a clear equation you will see a jumble of Egyptian hieroglyphs. Fortunately, Wikipedia, the source of all light and harmony in the universe, has a neat geometric explanation of Bézier curves which not only makes a lot more sense to me, it also gives me an algorithm of how to approximate such curve parametrically using simple linear interpolation:
All hail Wikipedia! But back to Bobo’s arms. As I mentioned already, they are constructed using these curves with three fixed points:
- The attachment point on Bobo’s body
- Desired claw position
- Center point, smack in the middle inbetween the two
In addition to these 3 pass-through points, in code I create 4 additional control points to model a basic-looking S curve for each arm – two control points between the body attachment and the center point and two control points between the center point and the claw. Then, using the OpenGL triangle strip and a repeating texture, I draw the actual hose and place a CCSprite at each end of the hose to complete the illusion.
Animating the hose turned out being equally simple. At any given point in time, I know where the body attachment is (based on where Bobo’s body is) and where the claw should be. The rest of the hose curve is calculated for me through the Bézier approximation above. To move the arm, I linearly interpolate between the claw’s current position and the claw’s desired position and, at a slightly slower rate, between the arm’s current center position and the arms’s desired center position to give the hose a nice movement lag. The body attachment point is fixed by Bobo’s body. The rest of the curve automagically animates along.
Put it all together, add cute sound effects, and you have yourself a robot!
Confidential to @atzoum: I’ll do a post on Chipmunk backing (including how Bobo moves cpBodies around) next. Stay tuned!