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 aCGRect
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 showed you last week, when changing the bounds
of the layer I was setting them like this:
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…
…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.
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:
- Calculate how long the animation had been running by substracting the current time from the time when it started (dt).
- Create the new animation with the new duration.
- 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.
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 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!
todd; January 20, 2011, 8:49 AM
rohn; January 20, 2011, 9:47 AM
David McGraw; January 20, 2011, 11:39 AM
Luke; January 22, 2011, 11:31 AM
Jose Miguel; March 3, 2011, 2:37 AM