So you want to make a 2D game full of real 80s technology: sprites, tiled backgrounds, parallax scrolling, and all that goodness. The last console you played a 2D RPG game in, back when you had hair, is long lost in the rat’s nest behind your TV stand. Your iPhone’s processor can run circles around that old console but you wonder… how do I do it?
Now, if you were that kind of person, you would go to the platform’s documentation site for answers. But instead you check with Google, and after a few visits to random websites and auto–cloning forums you reach the following consensus with the rest of the internets:
- You need to use OpenGL for the ultimate in both power and beginner’s frustration.
- Or better, use one of those open source frameworks that will make your transition from ActionScript a breeze. Their forums hopefully will answer any questions that the non–existent documentation cannot. Cocos2D seems popular.
- The losers that read documentation keep asking about something called Core Animation, but wiser and more anonymous users know better than to trust Apple “technologies”. Core Animation is slow, buggy, and inappropriate for anything harder than a match–3 game.
I say, not so fast. You see, I have a problem with thesis that are never proved. I especially don’t get what in that screenshot you sent me of three bats, a rat, and a stick figure is so sophisticated that requires low–level access to the hardware. So let me present you a different thesis, and then let’s try to prove it:
Thesis A: Core Animation is just fine for most 2D games.
Before we begin: no, I have no idea if thesis A is true or not. I am making it up as I go, so consider it an exploration of the capabilities and limitations of Core Animation and related technologies. If we don’t end up with a whole game, at the very least we will gain some insight into how this fundamental part of any iPhone app actually works.
Elephants
Although there has not been much visible progress in any of the 7 evil projects, the elephant racing game seemed like a good one to tackle so I could explain how to make frame-by-frame Core Animation sprites. The only problem was I didn’t have any art for it, or even an idea of what style I wanted, so I spent some nights this past week trying to define how the game should look.
This is kind of a weird habit of mine. If you have read any articles about quickly prototyping your games, you already know starting with the art is the wrong thing to do. But I kind of like having a better picture of what the game is going to look like in general, think about its theme, colors, if is going to use realistic graphics, cartoony illustrations, or other styles, what’s the music going to be like, etc. Anything that makes it more clear in my mind how the game experience will feel.
For this game I wanted the tone to be light-hearted and frantic. I was imagining the elephants stomping through villages in the jungle, tearing down anything in their way, obsessed with the bananas that hysterical mahouts swing in front of them. Mobs in the background will either cheer from their roofs, or yell in hot pursuit cause their house has just developed new elephant-shaped doors. The player has to be quick-witted, and use any dirty trick to stop his opponents: steal their bananas, throw them into a devious path for their elephants to follow, scare them off with noises, bump into them, or share that tiger that has been following you around…
After a few sketches I came up with this:
As you can see is missing lots of the elements I was just talking about, but at the same time it has enough detail to get into the mood of the game, and create an interesting prototype.
Layers, not Views
So the first thing to understand about Core Animation is that is ever-present. If you are rendering into an iOS device screen by any means, Core Animation is being used under the hood for compositing images. Internally, Core Animation will use OpenGL, or whatever other hardware abstraction is in place in the future, to render your final image with hardware acceleration.
To do all this rendering and compositing CA uses a light weight object known as a CALayer (or just layer). After looking at its properties you would think a layer is kind of like a view. It has the same frame and bounds to define its size and position, layers can contain other layers in a hierarchy same as with views… the interesting part are the differences though.
A UIView is part of the Responder chain so it can handle touch and motion events, decide when it becomes the first responder, etc. Every view is backed up by a layer that handles all its rendering and animation (at least in UIKit, AppKit in the Mac makes the NSView/CALayer dependence optional). The reason the 2 classes have similar properties is cause the view works as a proxy for its internal layer. Note however that the view hierarchy and the layer hierarchy are different; you can have one view with a whole layer tree attached, that is also part of its own view tree. The thing to remember is that for each view you will have at least one layer.
For all our sprites we will use layers embedded in a single view. Why not use UIImageViews instead? So that we don’t incur in the event-processing overhead; we don’t need our sprites to handle input, so this way our Responder chain will be simpler, and less time will be used navigating its hierarchy whenever an event fires.
Layer Content
A nice thing about layers is that you can provide image content pretty easily. If you have a reference to a CGImage, all you have to do is set the layer’s contents property to it. Like this:
If you have multiple elements in the game sharing the same graphic, nothing easier than to load the image once, then use the CGImageRef as the content for a bunch of layers (you would think that’s obvious, but you probably have not looked through the internets for UIImage examples).
You can see the problem with this though. If we have to load separate images from disk for each type of sprite we do, loading the game is going to slow to a crawl, not to talk about the havoc caused by loading too many textures into the graphics card. That’s why texture atlases where invented.
Texture Atlases in Core Animation
By now you should know what a texture atlas is, but if you don’t check Owen’s description. Apart from the tools he mentions you can find nice GUI apps to convert a bunch of PNGs into a compact atlas, like Zwoptex. The problem is how to render only part of it with Core Animation.
This too is pretty easy. Imagine we have the following atlas with a simple 5 frame walking animation of our elephant:
(Notice this image doesn’t have a power of 2 width and height. Core Animation doesn’t require it, but is probably better if you do it anyway with your own atlases).
If we use the previous code to load this image into a layer we will see the above image. In addition we need to specify the contentsRect property:
layer.contents = (id)img;
CGSize size = CGSizeMake( 160, 198 ); // size in pixels of one frame
CGSize normalizedSize = CGSizeMake( size.width/CGImageGetWidth(img), size.height/CGImageGetHeight(img) );
layer.bounds = CGRectMake( 0, 0, size.width, size.height );
layer.contentsRect = CGRectMake( 0, 0, normalizedSize.width, normalizedSize.height );
In this case we set the bounds property to the size in pixels of one frame, and the contentsRect property to the normalized rectangle of the first frame (meaning the top/left of the atlas is coordinate 0,0 and the bottom/right is 1,1). If we render that layer, we will only see the first frame.
Impertinent Implicit Animation
So how do we make that elephant walk? In theory we could just change the contentsRect property so it moved from the first frame to the second, then the third, etc. But we would find a little quirk of CALayer that is probably one of the reasons most people abandon Core Animation in frustration: all changes to its properties are automatically animated. What does that mean? It means if later in our code we do this to our elephant layer…
…we will end up seeing something like the image to the left. This is the result of the default implicit animation linked to the contentRect property, the same thing that gently fades views when you change the opacity property, or slides them when changing their position.
So that’s a bummer. We need to deactivate the default animation, and you can do that in multiple ways (all of them confusing, by the way). So before we get there, what more do we need for this to work?
Well, we need to time how long one frame shows before the next appears, then change to the next one in the sequence, maybe loop back and forth or just back from the beginning, and we need to do this while the sprite changes position in the screen.
We can do all that updating each sprite’s state every so often in our game loop, or with Timers, delegate callbacks, etc. But that sounds like lots of work for something that should be easier. This easier:
anim.fromValue = [NSNumber numberWithInt:1]; // initial frame
anim.toValue = [NSNumber numberWithInt:6]; // last frame + 1
anim.duration = 1.0f; // from the first frame to the 6th one in 1 second
anim.repeatCount = HUGE_VALF; // just keep repeating it
anim.autoreverses = YES; // do 1, 2, 3, 4, 5, 4, 3, 2
[layer addAnimation:anim forKey:nil]; // start
What’s that sampleIndex property you ask? No, is not part of the basic CALayer class. Let me show you how to subclass CALayer with a dozen lines of code so it can handle this.
MCSpriteLayer
Core Animation can interpolate between more than just the basic CALayer properties. It can actually do it with any values of the following types:
- integers and doubles
CGRect
,CGPoint
,CGSize
, andCGAffineTransform
structures-
CATransform3D
data structures -
CGColor
andCGImage
references
You see integer there? That means we can have Core Animation take care of the counting of frames, the looping, the duration, add timing functions, refresh the layers that need it automatically, and well, do all the things that Core Animation is supposed to do! We can synchronize that with other animatable properties (like position) with a Core Animation group in a transaction. The only thing we need is that integer property to be recognized. For that, nothing easier than to subclass CALayer:
@interface MCSpriteLayer : CALayer {
unsigned int sampleIndex;
}
@property (readwrite, nonatomic) unsigned int sampleIndex;
I decided to use the name sample index to avoid confusion with the frame property. That’s actually all the code you need to have a new animatable property. The previous code should already work, and cycle through sampleIndex values in an autoreverse loop. Problem is that alone is not going to change what we see on the screen!
@implementation MCSpriteLayer
@synthesize sampleIndex;
+ (BOOL)needsDisplayForKey:(NSString *)key;
{
return [key isEqualToString:@"sampleIndex"];
}
- (void)display;
{
unsigned int currentSampleIndex = ((MCSpriteLayer*)[self presentationLayer]).sampleIndex;
if (!currentSampleIndex)
return;
CGSize sampleSize = self.contentsRect.size;
self.contentsRect = CGRectMake(
((currentSampleIndex - 1) % (int)(1/sampleSize.width)) * sampleSize.width,
((currentSampleIndex - 1) / (int)(1/sampleSize.width)) * sampleSize.height,
sampleSize.width, sampleSize.height
);
}
@end
But that will. The display method will now be called any time the sampleIndex changes, and will change the contentRect to select the next element in the atlas as long as all the frames are the same size and you organize them by rows.
Two problems though. First, as we saw before, changing the contentRect will activate the default implicit animation. We can solve this by deactivating that animation:
Second, what if our frames are not ordered by rows, or are all different sizes. In that case the class can call its delegate to provide a specific coordinate rectangle for each sample index. An example project is provided below containing examples of both fixed size, and variable size samples.
Update: I totally forgot to explain why the animation we created above goes from 1 to 6 instead of the expected 1 to 5 for 5 frames. It seems Core Animation transforms internally integers to float values to produce their animation. While the value goes through the decimals from 4 to 5, sampleIndex
maintains a value of 4. For the animation to spend the same amount of time in frame 5 we have to actually animate through all the decimals from 5 to 6. So in summary, we always need to set the toValue to one more than the last sample index for the animation to run properly.
In Other News…
After last week’s blog post a number of people joined in the fun of Open Development (not sure I like that name, but whatever sticks). Don’t hesitate to check out all the participant blogs!
- George Sealy from Acorn Heroes: Faerie’s Journey
- Jerrod Putman from Tiny Tim Games: Safety First
- Zaid Crouch from Third Raven: Project NP
If you want to participate, just start blogging, and send us some links through Twitter or on the comments. Is more fun (and less scary) when is not just one person doing it.
Apart from that Mike Acton from Insomniac decided to create an alternative iDevBlogADay blogging group (if you don’t know who Mike Acton or Insomniac are, you need some quality PlayStation time, OK?)! It’s called #altDevBlogADay and they are scheduled to start sometime this weekend. Go check them out!
CoreCastlevania Source Code!
Sorry about not giving you my elephant sprite to play, but I may still need it :) In its place, I found some Castlevania sprites of Richter running (Symphony of the Night maybe?)!
Have fun experimenting!
Jason Lee; January 13, 2011, 8:30 AM
Zaid Crouch; January 13, 2011, 5:53 PM
Mike Berg; January 13, 2011, 7:39 PM
Freerunnering; January 14, 2011, 4:04 AM
David; January 14, 2011, 6:47 PM
Miguel A. Friginal; January 14, 2011, 11:13 PM
Michael Potter; January 16, 2011, 11:20 PM
Miguel A. Friginal; January 16, 2011, 11:43 PM
Nader Eloshaiker; January 17, 2011, 9:23 PM
Miguel A. Friginal; January 17, 2011, 10:48 PM
Pingback: Core Animating Interfaces at Under The Bridge
Nader Eloshaiker; January 20, 2011, 5:26 PM
Miguel A. Friginal; January 20, 2011, 5:49 PM
Nader Eloshaiker; January 20, 2011, 7:14 PM
Miguel A. Friginal; January 20, 2011, 8:20 PM
Nader Eloshaiker; January 22, 2011, 3:06 AM
Miguel A. Friginal; January 22, 2011, 6:45 PM
Nader Eloshaiker; January 22, 2011, 7:34 PM
Chris; February 1, 2011, 6:59 AM
Miguel A. Friginal; February 1, 2011, 12:45 PM
nate; April 5, 2011, 9:10 AM
Alex; August 23, 2011, 11:12 AM
Miguel A. Friginal; August 23, 2011, 12:21 PM
Alex; August 26, 2011, 3:27 PM
Miguel A. Friginal; August 27, 2011, 10:25 AM
Alex; August 27, 2011, 5:19 PM
Miguel A. Friginal; August 27, 2011, 10:47 PM
Pingback: Using CALayers as HUD elements – Part I. | Bitongo
Pingback: Using CALayers as HUD elements – Part II. | Bitongo
Renaud; December 14, 2011, 6:45 PM
bob; June 4, 2015, 5:56 PM
Miguel A. Friginal; June 5, 2015, 8:56 AM