Progress this week has been slow on most of my projects. Noel and I were working on Casey‘s tutorial, then we started implementing more objects so we can create some more levels and start beta–testing. Sneak peek:

Some of the new objects introduced this week.

I also finished the design of the “Goal Completed” screen! This is a big one for me, mainly because the damn thing has changed so many times during the life of the project I got like a dozen different designs archived! That may require its own post, but here is the final one (as of this week):

Final(?) Level Completed Screen

Since obviously I am not involved in enough projects already, I decided to promote yet another Twitter community project: #iSketchADay is an original idea by Craig Sharpe (the artistic half of Retro Dreamer) to promote more doodling. Join with your own sketches on the Twittverse.


My “DEMAND REAL CHARACTERS” campaign for #iSketchADay

I also continued working on Evil Project #7. After some more Twitter voting it even got a name: Elephant Run!. Owen‘s suggestion, “Elephant Run: A Banana Bonanza” was my favorite by far.

Head Over Heels

In the few hours I could spend on this I got the elephant sprite to walk. You may think this was solved last week, but there was a little problem with the previous sprite: the head and the body were stuck together in the same graphic, making it impossible to swing the trunk in the direction of the bait while walking.

After a few tweaks in Illustrator I ended up with this:

Now I just needed to position the trunk with respect to the body so the whole thing moved together. Since the body was already one of my sprite layers, the idea was to make the trunk a sublayer of it. That guarantees that moving the body would make the trunk follow. If only it had been that easy…

Origin of Space

Just a few lines later I had my first WTF.

Let me explain you what’s going on there.

My main problem was that each of the frames in my animation had a different size, so every frame the sprite bounds where changing (red rectangle). The bounds change with respect to the layer’s position, that by default is in the center (the point in the center of the red rectangle that doesn’t move).

Now, the easy way out of this was to just make all the frames the same size. But there are two things on this diagram I wanted to talk about first.

How a layer’s content draws around its position

The reason the bounds grow out from the center point is because of the layer’s anchorPoint property. You can check the docs here for a long explanation of what the anchorPoint represents. But basically is a CGPoint in the range {0.0, 0.0}-{1.0, 1.0} that indicates where inside the bounds rectangle the layer’s position is. Or in this case the other way around, how the bounds (and the layer’s content) are drawn with respect to the position point. The default value is {0.5, 0.5}, and that’s the reason our red rectangle above grows a few pixels both on the top and the bottom when its size changes. If we change it, this is what will happen:


Anchor point at {0.5, 0} on the left with bounds growing down, {0.5, 1} on the right with bounds growing up.

As you can see neither option will help with our original problem; the one on the left creates a gap between the body and the trunk, and the one on the right inserts the trunk further than necessary into the body.

How a layer is positioned with respect to its superlayer

The question is, why the position vector of the trunk (the blue arrow) starts where it does? I was half expecting it to go from the body’s anchor point, to the trunk’s anchor point, but instead it starts in the upper left corner of the body’s bounds. Can we change that point, start in a more convenient place?

The answer is yes. This line in the documentation tip me off:

The bounds property is a CGRect that provides the size of the layer (bounds.size) and the origin (bounds.origin). The bounds origin is used as the origin of the graphics context when you override a layer’s drawing methods

As I show you last week when changing the bounds of the layer I was setting them like this:

self.bounds = CGRectMake(0, 0, currentFrameWidth, currentFrameHeight);

If you change the bounds.origin the origin of coordinates for all its sublayers change to that point. For some reason, negative numbers (in points) move it right and down, so doing this…

self.bounds = CGRectMake(-currentFrameWidth/2, -currentFrameHeight, currentFrameWidth, currentFrameHeight);

…and changing the anchor points for the head to waggle, finally got me here:

Origin of Time

After that I decided to embark in another easy one: have different speeds for your elephant. I am going for just 3 speeds in the game, slow, normal, and fast, plus completely stopped (with some idle animation). For now I was just going to run the animation I have at different speeds (later on I will probably create different images for each, so I can give a bit more personality to running like crazy vs trotting vs pacing around).

Both CALayer and CAAnimation objects depend on the Media Timing Protocol for time related tasks. You can set duration for your animations, how many times they repeat, or if they autoreverse through properties in this protocol, and yes, there is even a speed property.

The thing is, as with the spatial properties the time properties are also organized hierarchically. This means that changing the speed of the elephant’s body would also change the speed of any other animations applied to its sublayers. Say we set the body’s animation speed to 2; animations applied to the trunk will proceed now at double the frame rate! Even worse, if we set the speed to 0, the trunk will not be able to move at all. I could apply the inverse speed to all the layer’s children, but since we will eventually end with the rider, bananas in the baskets, bait, and who knows what else, that can get messy fast.

So the plan was to create a new animation each time there was a speed change; same parameters except for the duration, that would depend on the required speed.

A couple minutes later I had my second WTF.

secondMoveError.gif

This time the problem was that recreating the animation was resetting it, so each time I changed speeds it started from the first frame, instead of continuing along with whatever frame was next. Note also that at the moment the animation stops, we only know what frame we are in (through our sampleIndex property), but not if we were going towards the next frame or towards the previous.

How long since the animation started?

There are 2 properties in the Media Timing Protocol that allow us to start an animation not from the beginning, but some time into it: beginTime and timeOffset. The main difference between them is that the first one is scaled to the timespace of the animation’s layer, and the second one is not. What does that mean? Remember our animation is part of that time hierarchy created by the differences between the layers’ speeds. 4 seconds in beginTime applied to a layer with speed 2 is equal to 2 seconds in timeOffset.

What I needed to do was:

  1. Calculate how long the animation had been running by substracting the current time from the time when it started (dt).
  2. Create the new animation with the new duration.
  3. Set beginTime so the new animation starts in the same relative point (dt * newDuration / previousDuration).

So how do I know when the animation started without keeping track of it on my own. A non-documented fact about the beginTime property is that if you don’t set it to something different, it is internally populated with the animation start time timestamp.

Remember though that beginTime is scaled by the time hierarchy, so some transformation is required. At least the Media Timing Protocol gives us methods to do this; by passing nil as the 2nd parameter to convertTime:fromLayer: and convertTime:toLayer: you can convert to and from the layer’s timeframe to the standard one.

// Setting up my basic animation; all properties are the same each time...
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"sampleIndex"];
anim.autoreverses = YES;
anim.repeatCount = MAXFLOAT;
anim.fromValue = [NSNumber numberWithInt:1];
anim.toValue = [NSNumber numberWithInt:6];
//...except duration
anim.duration = newDuration;

// This animation was already running, so let's grab it; notice is read-only
CAAnimation *prevAnim = [self animationForKey:@"bodyMovement"];
CFTimeInterval prevStart = [self convertTime:prevAnim.beginTime toLayer:nil]; // transform into real time
CFTimeInterval dt = CACurrentMediaTime() - prevStart - prevAnim.timeOffset; // standard time

Notice we also subtract the timeOffset from the starting time (without converting, since timeOffset is not scaled). If the offset is used, it needs to be taken into account.

Finally we can set the new animation beginTime, making sure to set it to a timestamp for the code above to continue working:

CFTimeInterval prevDuration = prevAnim.duration;
CFTimeInterval newDt = (newDuration/prevDuration)*dt;

// Set beginTime (layer time)
anim.beginTime = [self convertTime:CACurrentMediaTime()-newDt fromLayer:nil];

[self addAnimation:anim forKey:@"bodyMovement"];

As I said at the beginning not much progress overall, but at least it helped me understand a bit better how everything works together. Hope it is helpful to you too!

Other articles in this series:

  1. Frame-by-Frame Sprites with Core Animation
  2. Scrolling Hell (Core Animation Games 3)
  3. Parallax Scrolling (Core Animation Games 4)

5 Responses

  1. A great blog posting, but it makes me wonder. Are there any tools that artists can use to manage the animations without the programmer getting involved? Even the sound could be included. As a programmer I should just have to say play this animation here. Even static images could be trivial animations.

    todd; January 20, 2011, 8:49 AM

  2. I know it’s only the smallest part of this post, but I fully appreciate your “challenge completed” screen. The level done screen for the game we’re working on is now in its third iteration and I still don’t have it to a point where I’m satisfied.

    Sometimes it’s these little “hangers on” parts that seem the hardest, but without them (finely tuned menuing also) the magic of continuity is lost and the overall effort suffers.

    Another one of those “I know I did a good job if no one notices the job I did”…

    rohn; January 20, 2011, 9:47 AM

  3. Slick post. Very insightful with the animation bit. I’ll have to tuck this post away for future ref.

    The completed screen looks slick! Friend’s solutions? Yes please. :)

    I’m on the 4th iteration of my completed screen myself. Oye! I think I have it, but we’ll see.

    David McGraw; January 20, 2011, 11:39 AM

  4. Appreciated the Head Over Heels guy — bring back some early childhood memories!

    Luke; January 22, 2011, 11:31 AM

  5. Miguel Angel: casi me meo de la risa con la viñeta de “Lara Croft vs. your Mom”. Eres un genio. Y lo digo desde la objetividad que me da ser amigo tuyo. (Lo de “mi brazo tiene mas pelo” en la foto de la tableta gráfica tampoco tiene desperdicio).

    Un abrazo. Jose Miguel

    Jose Miguel; March 3, 2011, 2:37 AM

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>