This week my post is going to be necessarily short. Noel and I have been working on preparing the first beta for Casey’s Contraptions, and there is still plenty to do before our intended milestone on Friday. He is deep at work squeashing bugs and working on loose ends, while I have been working on creating levels. Yes, you could say I got all the fun while he was hard working, but somehow solving those bugs always involves the rag doll thrown around, poked, squashed, and generally mistreated during his tests (I have this theory that is all due to his repressed feelings as a game developer that doesn’t want to make violent games; he is probably dying to make a zombie FPS). Anyway, look at the pretty thumbnails of my levels!

I still have plenty to do before Friday, so you will have to tune in some other day for part 3 of the Designing Casey’s Contraptions series (if you have not read the previous posts, they are here, and here). While the more artsy readers wait, the coders will have another (small) ration of… Core Animation (I bet you were not expecting it, given the title of the post and everything).

The Story So Far

As you may remember I started working on the Elephant Run game with the intention of doing it all with Core Animation, and so demonstrate what I called Thesis A:

Thesis A: Core Animation is just fine for most 2D games…

But giving the last few weeks, let me present you with Postulate B:

Postulate B: …but you will suffer if you try.

To explain this postulate, I will present you with the latest in layer scrolling! And some other issues. Yes, that’s the noise of my teeth grinding, shut up.

Scrolling is easy! Cannot you just use…

Let’s start by defining what I was looking for:

  1. a window into a coordinate system plane populated by objects…
  2. that smoothly changes how its borders map to that underlying coordinate system…
  3. and loads the objects to be displayed as they are needed, while keeping memory requirements in check.

If you were just idly browsing the documentation, you have probably seen a few CA classes that look pretty promising: there is a CAScrollLayer that looks exactly right, and even a fancy CATiledLayer that can make you think of something like this the first time you read about it:

Sadly, it is actually more like this:

CATiledLayer

What does the documentation say about tiled layers?

CATiledLayer is a subclass of CALayer providing a way to asynchronously provide tiles of the layer’s content, potentially cached at multiple levels of detail.

As more data is required by the renderer, the layer’s drawLayer:inContext: method is called on one or more background threads to supply the drawing operations to fill in one tile of data.

That sounds pretty promising to take care of point 3, and since it lets you create tiles of the content you would say 1 can be covered too.

When you start messing with it though, the first impression is that is slow. Like really slow. You may even notice that slow fade of tiles and think “hey! Is just my old friends, the damn implicit animations trying to make it look cool when what I need is to make it appear NOW! Let’s just disable those pesky animations!”. But I am going to save you the effort by editing the docs for you so they are a bit more useful.

CATiledLayer is a subclass of CALayer providing a way to asynchronously provide tiles of the layer’s content, potentially cached at multiple levels of detail.

As more data is required by the renderer, the layer’s drawLayer:inContext: method is called on one or more background threads to supply the drawing operations to fill in one tile of data.

Don’t ask me why I didn’t see that the first time around. Or the 20th. But anyway, CATiledLayer good for photo galleries and zooming into big images… for games, not so much.

CAScrollLayer

CAScrollLayer has no problems providing both points 1 and 2. To make it work, you create a layer, give it the bounds size of your “window”, then add sublayers to it. The moment you send either scrollToPoint: or scrollToRect: to the scroll layer, its sublayers will scroll by. (Note the layer doesn’t have scroll bars, and will not do a thing when you touch it; you need a UIScrollView for that).

You can also send a scrollPoint: or scrollRectToVisible: to any CALayer, and it will look through its superlayers for a CAScrollLayer to forward these messages to after the proper adjustment of coordinates.

The way CAScrollLayer accomplishes scrolling is pretty straightforward. It just animates changes to its bounds origin (that as we saw in a previous post determines the origin of coordinates for all the sublayers), and by default masks contents to its bounds (maskToBounds property is NO for any other type of layer). To adjust the speed at which the layer scrolls, you can use a transaction to override the duration around your scrolling messages.

All this is great, but point 3 is the tricky one. To be able to add and remove sublayers on the fly, change their contents, or any other dynamic behavior to happen while scrolling, we need notifications. And you see, the bounds property never sends letters while being animated. Instead of a nice “Hey, I am scrolling! I am at (1.0, 1.0)! Now (1.01, 1.0)! Guess what? Now is (1.03, 1.0)!”, it just tells you something like “You want me to go to (20.0, 1.0)? OK, ciao!”. Some properties are just not sociable.

By the way, if you know how to get the bounds to behave, please tell me. But beware, have a running program to show me, because I cannot tell you how many times I was like “this trickery SHOULD do it! It SHOULD work!”, just to find it didn’t.

Anyway, practical solutions to this? Only 2 I can think of. Either scroll in grid sized chunks that allow you to swap pieces at the end of the scrolling, or create your own animatable CGPoint property to take care of the notifications while scrolling (your own properties, as seen previously, can send nice display and drawInContext: messages periodically while animating).

Next Core Animation article I will go about creating a custom scroll layer that does all this, but that will be another day. Until then, don’t be like me and have fun creating games with OpenGL or freaking Cocos2D!

Other articles in this series:

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

15 Responses

  1. Casey’s Contraptions looks like a monster hit in the making !

    Lazrhog; February 9, 2011, 4:07 PM

  2. UIScrollView does provide such notifications on each frame of scrolling. AFAIK it uses a callback akin to CADisplayLink to accomplish this.

    Did you consider making a subclass and moving around your own tiles?

    Andrew Pouliot; February 9, 2011, 4:25 PM

  3. @Andrew: UIScrollView can, but it also comes with its own baggage of responder chain, scrollbars, and other stuff I don’t need around. That’s why I am using CAScrollLayer.

    Miguel A. Friginal; February 9, 2011, 4:41 PM

  4. Have you tried checking

    [[yourCAScrollLayer presentationLayer] bounds]

    This should give you the actual bounds at the moment rather than the “end location” bounds the layer will give you.

    Brett Park; February 9, 2011, 5:42 PM

  5. @Brett: *sigh* I don’t want to pull the data, I want it pushed to me. CALayer can do this thanks to methods like needsDisplayForKey: that make it send display:/displayLater:/drawInContext:/drawLayer:inContext: messages, not to talk about all the defaultActionForKey:/actionForKey: ones, while the animation is happening. But apparently not for already defined properties like bounds.

    Miguel A. Friginal; February 9, 2011, 5:57 PM

  6. This may be a dumb question, but have you tried registering as a Key-Value Observer on the UILayer bounds (or position, or contentsRect) property?

    Then you would get notified when it changes. I’m not sure at what frequency, though.

    Sean Kelleher; February 10, 2011, 3:12 PM

  7. @Sean: yep, I tried that. The problem is the layer itself does not animate the bounds. That happens in the presentationLayer. And the presentationLayer cannot be observed, because every time you get it is a brand new copy of the layer with the correct values (you don’t get the same object every time you send a [layer presentationLayer] message). Ergo, you cannot observe these changes :’(

    Miguel A. Friginal; February 10, 2011, 7:21 PM

  8. Your first post inspired me to try the prototype I was working on in only UIKit/CoreAnimation instead of Cocos2d. So far it’s looking like it’s going to work fine, the game is more or less a platformer with only a few sprites on the screen at a time.

    I guess reading this post I’m a little confused as to your overall game structure. Does this mean you’re trying to make the entire game code run in more of an event driven way as opposed to having a central game loop based off CADisplayLink? Because if you have a game loop I don’t see why animating bounds is necessary instead of just updating it at 30fps, not to say that’s not animating but you know what I mean.

    Your post of course made me have to look into this problem for myself and I had exactly the same problems you did. Including the near hundreds of “Ah ha! That… didn’t work?” moments. It seems the problem is in how CABasicAnimation’s work for animatable properties vs custom properties. For custom properties it looks like the animation fires causing your CALayer to use initWithLayer: to create a new version of the presentationLayer and calling display: each time. On the other hand animating so called “animatable properties” such as bounds, position or backgroundColor instead just creates the presenation layer once and seems to handle everything on the render tree.

    Unfortunately I can’t find anything on why the “animatable properties” are handled this way. I assume it has something to do with optimization because how they should effect the rendering is known and Apple has used this to the advantage of not recalling display or redrawing the contents every time giving a performance boost to most simple case of CALayer.

    I was however able to get it “working” using a custom property “leaps” as basically an empty shell to call display every frame of animation. Quick version:

    @interface AFLayer : CAScrollLayer
    @property (assign) CGRect leaps;
    @end

    @implementation AFLayer
    - (void) setLeaps:(CGRect)newLeaps {
    [self setBounds:newLeaps];
    }

    - (CGRect) leaps {
    return [self bounds];
    }

    + (BOOL)needsDisplayForKey:(NSString *)key
    {
    if ([key isEqualToString:@"leaps"]) return YES;
    return [super needsDisplayForKey: key];
    }

    - (void) display {
    NSLog(@"Log");
    [self setBounds: [[self presentationLayer] bounds]];
    [super display];
    }

    - (id ) actionForKey:(NSString *)key {
    if ([key isEqualToString:@"bounds"])
    return nil;
    return [super actionForKey:key];
    }
    @end

    This calls display every frame of animation, and currently the way I set it up makes bounds = to the presentationLayers bounds. This doesn’t seem like best implementation but it seems to work. From here scrollToPoint: could be overridden as well though again not exactly sure how you’d be using it.

    Alex; February 14, 2011, 5:04 PM

  9. Yes, I got to the same conclusion. I guess I don’t need to write a post about that then :)

    About the game loop; I was trying as much as possible not to use a CADisplayLink loop to force myself to learn as much of the internals of CA as possible, but you are right is a process that doesn’t have any advantages, and gives plenty of headaches instead. :)

    After so many experiments though, I think I had enough. I am ready to call it quits and go the OpenGL/normal game loop route!

    Miguel A. Friginal; February 14, 2011, 5:20 PM

  10. Hold on! I wouldn’t throw in the towel just yet! What I meant by needing a run loop I meant specifically for elephant run, or any “racing game.” Though for Tripolar (puzzle game) it’s all event and notification driven. We weren’t to familiar with the lower workings of Core Animation and a lot of it could be stripped from views to layers if I were to do it again, some animations would even get easier!

    I still think there’s benefits to doing a game using only UIKit/CoreAnimation (Further known as UI/CA) even if you’re going to have a run loop (Which CADisplayLink works great for) IMHO UI/CA is much simpler. While obviously openGL has a lot of power I’m a big fan of using the simplest tool I can get away with. Using UI/CA makes it real easy to have a good MVC system going keeping your models going during the loop and rendering as much as needed.

    I modeled a sprite class similar to your first blog except had it import from zwoptex output. I use sprite names instead of numbers and use CAKeyframeAnimation’s to animate between the frames. It seems to work well so far!

    Disclaimer: This is holds true up until I potentially start over in OpenGL/Cocos2D in a month or so. I’m well aware of the possibility so no “I told you so”s

    Alex; February 14, 2011, 7:05 PM

  11. LOL All true. Your previous comment made me actually go back to my scrolling experiments, throw all the CAScrollLayer code away, and instead use a CALayer for the terrain whose bounds I change inside the CADisplayLink at the same time as the player position (all this inside a nice transaction that disables unnecessary animations of those properties). Add a needsDisplayOnBoundsChange and in 10 minutes everything was working perfectly.

    So thanks, I guess I will keep going this way :) When are you posting about this new game of yours? Join the fun!

    Miguel A. Friginal; February 14, 2011, 7:33 PM

  12. Success! So far at least. Currently I have a tilemap layer that updates it’s tiles from the tilemap model as various parts of the model become visible. And on it I have some sprites animating a simple walk animation. The tilemap can be optimized more but so far it runs at mostly 30fps, when moving the screen around really fast it dips to around 25fps (though way faster than likely to ever need at least in my game)

    I’ve tried to be really good about MVC and I think it will pay off in that I should be able to update the model of the whole level while the display only shows a screen’s worth. I always preferred that to when objects so far off the screen have to go bye bye from memory.

    To answer your question about blogging the answer is… soon. My progress tends to be sporadic as I work full time and this is on the side so I’ve been trying to build up a large chuck of successful stuff I can write about before I start.

    Alex; February 18, 2011, 1:13 PM

  13. Any code examples you could share? I’m struggling with using CALayer/CAScrollLayer to create a tiled map. A short example would be nice to push me on my way.

    Bill; March 1, 2011, 11:28 PM

  14. I just recently successfully implemented a UIScrollView subclass that takes views from one side to move to the other side when scrolling using the layoutSubviews method. After a lot of searching I found a post that mentioned that’s how you do things when subclassing. I would think that layoutSublayers would be the equivalent for a CALayer. Should give it a try. In my case layoutSubviews was called at the appropriate times and I would use the bounds to determine what views needed to go where and when.

    Justin; March 27, 2011, 1:43 PM

  15. @Justin: views and layers have some points in common, but that is totally not one of them.

    To any other commenters, I will write about how to do this properly with layers soon.

    Miguel A. Friginal; March 27, 2011, 2:01 PM

Comments are closed.