A Paging UIScrollView in Cocos2d (with previews)

I’ve created a sample project that shows how to do a paged UIScrollView within Cocos2d. Here’s a video showing the effect:

You can find the code on github

My solution’s main ideas are adapted from these two pages:

My contribution is combining the UIScrollView with previews with Cocos2d and cleaning it up.

If you haven’t tried to implement this before it might not be obvious why this is tricky to implement. Apple’s UIScrollView allows you to have a view which scrolls and optionally snaps to pages. The effect you see in the video above (and in Angry Birds level selection and many other apps) shows a preview of each panel on either side. This let’s you easily see if a next or previous page exists and you see a preview of that page.

The problem is that Apple’s UIScrollView doesn’t let you set the width of the frame, so you can’t page less than a whole screen (well, a whole width of theUIScrollView, more on that later).

To get around this I originally tried writing my own paging controller. If you’ve tried this you’ll know that it is extremely tricky to get the same interaction dynamics as Apple’s. (For instance, pull out your phone and play with the Photo application. Notice if you just drag slowly you lack enough inertia to go to the next page so it will snap back to the frame your are on. If you flick fast over a small area the page will skip to the next frame. Etc.) While at first glance the rules seem easy to reimplement, you have to cover a lot of edge cases to recreating the familiar paging interaction.

So ideally we need to figure out a way to use Apple’s UIScrollView and we should save ourselves a lot of work.

Like we said above, a UIScrollView will only page the width of the entire UIScrollView. So to get this preview effect you can create a UIScrollView that is less than the width of the entire screen. The problem here is that any touches that lie outside of that UIScrollView (say on the edge of the screen won’t be sent to the UIScrollView.

Our solution, (again, borrowed largely from the above links) looks like this:

Jacob's Shapes Panels Layers

The idea is this:

  • We create a CCMenu and add it to a CCLayer
  • The UIScrollView is resized to the width of our panel images (smaller than the whole screen)
  • The UIScrollView transforms its scrolling action into moving the position of the CCLayer containing our CCMenu
  • We create a full-screen TouchDelegatingView that simply forwards its touches on to the UIScrollView

More Details

In Jacob’s Shapes (JS), we have a GameController which knows all of the levels. For the sake of this example, we’re just going to store all the level names in an NSArray.

# HCUPPanelScene.m (in onEnter)
NSArray* panelNames = [NSArray arrayWithObjects: 
    @"amazon", @"arctic",
    @"brkfst", @"camp", 
    @"city", nil];
int numberOfPages = [panelNames count];
 
// create an empty layer for us to work with
CCLayer* panels = [CCLayer node];

Custom CCMenu and CCMenuItem

We use a custom subclass of CCMenu and CCMenuItem, NMPanelMenu and NMPanelMenuItem, respectively. NMPanelMenu tweaks how the current item is determined. Overriding NMPanelMenuItem allows us to add metadata about the panel, play sounds, and optimize how we use the images for selected panels.

# HCUPPanelScene.m
NMPanelMenu* menu = [NMPanelMenu menuWithItems: nil];
float onePanelWide = -1;
 
// Now add the panels
for(int i=0; i < numberOfPages; i++) {
    NSString* currentName = [panelNames objectAtIndex:i];
    CCSprite* pane2 = [CCSprite spriteWithFile:[NSString stringWithFormat: @"%@-panel.png", currentName]];
    NMPanelMenuItem* menuItem2 = [[NMPanelMenuItem alloc] initFromNormalSprite:pane2 
                                                                selectedSprite:pane2
                                                                  activeSprite:pane2
                                                                disabledSprite:pane2
                                                                          name:currentName
                                                                        target:self selector:@selector(levelPicked:)];
    menuItem2.world = i;
    menuItem2.name = currentName;
    [menu addChild: menuItem2];
    [menuItem2 release];
    // set onePanelWide to be the width of the first panel
    if(i==0) onePanelWide = [pane2 textureRect].size.width;
}

Here we used CCSprite#spriteWithFile, but in JS we use Zwoptex-created sprite sheets for the panels and then create sprites from those frames. This makes a huge difference in the load time of this scene when you have 20 panels. In JS, instead of loading 20 textures (one for each panel) we only load 2 textures, each containing 10 panels each.

JS is graphics heavy and we definitely had to pay attention to file sizes to keep it under 22MB. Originally I had created two versions of each panel, one for “off” and one with a glow for “on” (active/selected). Each of the panels as a transparent png was somewhere around 100k. So 100k x 2 for each state x 20 panels was somewhere around 4MB just for this single scene.

We decided to sacrifice a bit of the quality of the glow for the “on” state and just create one transparent image for the glow and reuse that for every panel.

To use the glow a portion of our NMPanelMenuItem looks like this:

# NMPanelMenuItem.m
-(void) activate
{
    isActive_ = YES;
    // play sound here
    [super activate];
}
 
-(void) draw
{
    if(isActive_) {
        [self.activeImage draw];
        if(self.showGlow) [self.glow draw];
    } else {
        [super draw];
    }
}

Where self.glow is a CCSprite attached to the NMPanelMenuItem.

Adding the Cocos2d Panels

Next we need to setup some basic options for how much padding we want and what the total width of the panels layer is going to be. Then we add the panels to our scene and set the position.

# HCUPPanelScene.m
float padding = 15;
float totalPanelWidth = onePanelWide + padding*2;
float totalWidth = numberOfPages * totalPanelWidth; // (wait, do we need padding in here?)
 
int currentWorldOffset = 0;    // current world number. 
// int currentWorldOffset = 1; // Try changing to 1 and see what happens
 
[menu alignItemsHorizontallyWithPadding: padding*2];
 
// add our panels layer
[panels addChild:menu];
[self addChild:panels];
 
// set the position of the menu to the center of the very first panel
menu.position = ccpAdd(menu.position, ccp(totalWidth/2 - totalPanelWidth/2, 0));

Note that the panels are the visual representation but we haven’t added in any scrolling dynamics. To do that we need to add a UIScrollView.

Adding the UIScrollView

Here we do two things:

  1. Add our CocosOverlayScrollView which is only one panel wide (less than the whole screen). If we had this layer only then we wouldn’t be notified of touches on the edge of the screen.
  2. We add the TouchDelegatingView which is full screen. The TouchDelegatingView will delegate any touches it receives to our paging scroll view

# HCUPPanelScene.m
// Note that we're only concerned with a horizontal iPhone. If your game is
// vertical, change accordingly
touchDelegatingView = [[TouchDelegatingView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
scrollView = [[CocosOverlayScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, totalPanelWidth)
                                                  numPages: numberOfPages
                                                     width: totalPanelWidth
                                                     layer: panels];
touchDelegatingView.scrollView = scrollView;
 
// this is just to pre-set the scroll view to a particular panel
[scrollView setContentOffset: CGPointMake(0, currentWorldOffset * totalPanelWidth) animated: NO];
 
// Add views to cocos2d
// We called it a TouchDelegatingView, but it actually isn't containing anything at all.
// In reality it is just taking up any space under our ScrollView and delegating the touches. 
[[[CCDirector sharedDirector] openGLView] addSubview:touchDelegatingView];
[[[CCDirector sharedDirector] openGLView] addSubview:scrollView];
 
[scrollView release];
[touchDelegatingView release];

You can configure your UIScrollView options by simply changing the code in CocosOverlayScrollView#initWithFrame:numPages:width:layer. (Note that this class was originally written by Alexander Repty)

The TouchDelegatingView simply delegates any touches it receives to the CocosOverlayScrollView.

And there you have it! Feel free to fork and make any changes to the code and send me a pull request.

What do you think? Have any ideas for cleaning it up? Leave your comments below!

Share:
  • del.icio.us
  • Reddit
  • Technorati
  • Twitter
  • Facebook
  • Google Bookmarks
  • HackerNews
  • PDF
  • RSS
This entry was posted in programming. Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.
  • XPhoenix

    Hey man,

    Very nice, except your project on github has 3 problems.
    1. It has a depency on Adobe Device CS5 error when compiling.
    2. You are missing PreviewScrollContainer.h file
    3. CCCrossFadeTransition was renamed to CCTransitionCrossFade in cocos2d 99.5 if people try to use that template.

    Otherwise truly fun to use and great job.

  • http://www.xcombinator.com Nate Murray

    Hmm, I’ll take a look at that and try to fix it soon!

  • XPhoenix

    I could just email you my 2.5mb converted correctly compiling project?

  • Corey Sayers

    Nice! Works great! Although, I am having a problem with auto orientation through Cocos2d. Anyway you can provide any guidance?

    Thanks!

  • http://www.firstpixel.com Gil Beyruth

    Awsome, pretty nice fx, I ‘ll take a look.

  • Prashant

    @XPhoenix : can u mail me modified project @prashant_musale@ymail.com ???

  • Aeshvarya Verma

    Hey Nate

    Great work here but I am getting a dependency error——–

    Argument list too long: recursive header expansion failed at /Applications/Microsoft Office 2011/Microsoft Document Connection.app/Contents/Resources/DocumentConnection_DM.momd.

    Can you help me removing it?
    Thanks

  • Aeshvarya Verma

    I solved the above error and finally it compiled but crashed in simulator.
    Though there weren’t any error but 3 warnings:

    1) NMPanelMenuItem.m
    no -’initWithTexture:rect:offset:’ method found

    2) CocosOverlayScrollView.m
    Class ‘CocosOverlayScrollView’ does not implement the ‘UIScrollViewDelegate’ protocol.
    Unused variable ‘touch’.

    please help me out.

  • Aeshvarya Verma

    [Session started at 2010-12-24 17:54:26 +0530.]
    2010-12-24 17:54:29.165 PanelsExample[1435:207] cocos2d: cocos2d v0.99.5
    2010-12-24 17:54:29.168 PanelsExample[1435:207] cocos2d: Using Director Type:CCDirectorDisplayLink
    2010-12-24 17:54:29.176 PanelsExample[1435:207] cocos2d: OS version: 4.2 (0×04020000)
    2010-12-24 17:54:29.177 PanelsExample[1435:207] cocos2d: GL_VENDOR: Apple Computer, Inc.
    2010-12-24 17:54:29.178 PanelsExample[1435:207] cocos2d: GL_RENDERER: Apple Software Renderer
    2010-12-24 17:54:29.178 PanelsExample[1435:207] cocos2d: GL_VERSION: OpenGL ES-CM 1.1 APPLE
    2010-12-24 17:54:29.179 PanelsExample[1435:207] cocos2d: GL_MAX_TEXTURE_SIZE: 2048
    2010-12-24 17:54:29.180 PanelsExample[1435:207] cocos2d: GL_MAX_MODELVIEW_STACK_DEPTH: 16
    2010-12-24 17:54:29.181 PanelsExample[1435:207] cocos2d: GL_MAX_SAMPLES: 4
    2010-12-24 17:54:29.181 PanelsExample[1435:207] cocos2d: GL supports PVRTC: YES
    2010-12-24 17:54:29.182 PanelsExample[1435:207] cocos2d: GL supports BGRA8888 textures: YES
    2010-12-24 17:54:29.183 PanelsExample[1435:207] cocos2d: GL supports NPOT textures: YES
    2010-12-24 17:54:29.184 PanelsExample[1435:207] cocos2d: GL supports discard_framebuffer: YES
    2010-12-24 17:54:29.184 PanelsExample[1435:207] cocos2d: compiled with NPOT support: NO
    2010-12-24 17:54:29.205 PanelsExample[1435:207] cocos2d: compiled with VBO support in TextureAtlas : YES
    2010-12-24 17:54:29.210 PanelsExample[1435:207] cocos2d: compiled with Affine Matrix transformation in CCNode : YES
    2010-12-24 17:54:29.210 PanelsExample[1435:207] cocos2d: compiled with Profiling Support: NO
    2010-12-24 17:54:29.220 PanelsExample[1435:207] cocos2d: Frame interval: 1
    2010-12-24 17:54:29.226 PanelsExample[1435:207] cocos2d: surface size: 320×480
    2010-12-24 17:54:29.281 PanelsExample[1435:207] *** Assertion failure in -[NMPanelMenuItem addChild:z:tag:], /Users/aeshverma/Downloads/jashmenn-shapes-panels-bcf4e74/cocos2d/CCNode.m:360
    2010-12-24 17:54:29.286 PanelsExample[1435:207] *** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘child already added. It can’t be added again’
    *** Call stack at first throw:
    (
    0 CoreFoundation 0x01524be9 __exceptionPreprocess + 185
    1 libobjc.A.dylib 0x016795c2 objc_exception_throw + 47
    2 CoreFoundation 0x014dd628 +[NSException raise:format:arguments:] + 136
    3 Foundation 0x0048b47b -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 116
    4 PanelsExample 0x00034e02 -[CCNode addChild:z:tag:] + 328
    5 PanelsExample 0x000350c1 -[CCNode addChild:] + 249
    6 PanelsExample 0×00031965 -[CCMenuItemSprite setSelectedImage:] + 218
    7 PanelsExample 0x000315d5 -[CCMenuItemSprite initFromNormalSprite:selectedSprite:disabledSprite:target:selector:] + 141
    8 PanelsExample 0×00003529 -[NMPanelMenuItem initFromNormalSprite:selectedSprite:activeSprite:disabledSprite:name:target:selector:] + 93
    9 PanelsExample 0×00004074 -[HCUPPanelScene onEnter] + 754
    10 PanelsExample 0x0007a45e ccArrayMakeObjectsPerformSelector + 66
    11 PanelsExample 0x0007a416 -[CCArray makeObjectsPerformSelector:] + 46
    12 PanelsExample 0x00035db1 -[CCNode onEnter] + 65
    13 PanelsExample 0×00022002 -[CCDirector setNextScene] + 424
    14 PanelsExample 0x0007207f -[CCDirectorIOS drawScene] + 190
    15 Foundation 0x0040ef54 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] + 229
    16 Foundation 0×00421562 -[NSObject(NSThreadPerformAdditions) performSelectorOnMainThread:withObject:waitUntilDone:] + 184
    17 PanelsExample 0x0007778f -[EAGLView layoutSubviews] + 280
    18 QuartzCore 0x006b0451 -[CALayer layoutSublayers] + 181
    19 QuartzCore 0x006b017c CALayerLayoutIfNeeded + 220
    20 QuartzCore 0x006a937c _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 310
    21 QuartzCore 0x006a90d0 _ZN2CA11Transaction6commitEv + 292
    22 UIKit 0x0080a19f -[UIApplication _reportAppLaunchFinished] + 39
    23 UIKit 0x0080a659 -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] + 690
    24 UIKit 0x00814db2 -[UIApplication handleEvent:withNewEvent:] + 1533
    25 UIKit 0x0080d202 -[UIApplication sendEvent:] + 71
    26 UIKit 0×00812732 _UIApplicationHandleEvent + 7576
    27 GraphicsServices 0x02acca36 PurpleEventCallback + 1550
    28 CoreFoundation 0×01506064 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 52
    29 CoreFoundation 0x014666f7 __CFRunLoopDoSource1 + 215
    30 CoreFoundation 0×01463983 __CFRunLoopRun + 979
    31 CoreFoundation 0×01463240 CFRunLoopRunSpecific + 208
    32 CoreFoundation 0×01463161 CFRunLoopRunInMode + 97
    33 UIKit 0x00809fa8 -[UIApplication _run] + 636
    34 UIKit 0x0081642e UIApplicationMain + 1160
    35 PanelsExample 0x00002a60 main + 82
    36 PanelsExample 0x00002a05 start + 53
    37 ??? 0×00000001 0×0 + 1
    )
    terminate called after throwing an instance of ‘NSException’

  • http://Brandonreynolds.com Brandon Reynolds

    Hi! I have developed something very similar to your scroll view but wraps CCMenuItems into a scrollable grid like Angry Birds.

    Please take a look and let me know what you think!

    http://brandonreynolds.com/blog/2011/01/09/cocos2d-sliding-menu-grid/

  • Federico

    Hi, nice tutorial.

    I use this in my game but i have a little problem. i try to add a static button over the scroll panel, and the button works only in the first position of the scroll, if i move the scroll the button doesn’t work anymore and the sprite of the button not move. Hope you can help me, im very stuk with this. Sorry for my english. Thanks. FedeK.

  • Andrea

    Hi,
    I have the same problem, a button outside the scrolling layer which can only be pressed when the layer is at the first or the last position. Federico have you managed to fix it?

  • http://www.optimates.se Tommy Safstrom

    This really seems like something that I could use but I fail hard when loading it into Xcode4.
    1) Is there some special Jedi-trick I need to be aware of?
    2) What Version of cocos2d is the project comaptible with(cocos2d-iphone-0.99.4 seems to be closest)

    3) PLEASE HELP!
    :)

  • http://tonyngo.net/2011/11/scrolling-ccnode-in-cocos2d/ Scrolling CCNode in Cocos2d | Tony Ngo

    [...] A Paging UIScrollView in Cocos2d (with previews) [...]

  • Borfmaker

    Code does not compile Xcode 4.2. URL: https://github.com/jashmenn/shapes-panels.git, whack errors

  • Hugo

    it not working please help me!

  • Seamus Cranley

    Hello Nate,

    I’ve been trying to get this to work with xcode 4.5, it compiles. But when I get it on screen, it scrolls vertically not horizontally like your original.

    I suspect it has something to do with the new Director/appdelegate setup.