Cocos2D and UIScrollView

August 21st, 2009 by Rob Segal Leave a reply »

Learning now to work with a new framework is usually a challenge.  You often delve into a new API or SDK expecting things to work in a certain manner.  When those expectations fail you need to delve deeper to find your answer in several different ways.  It often starts with visiting the website of the package you are working with.  This often leads to..

All of these approaches have their pros and cons and I have often found its some combination of these sources that will lead you to your answer.  In my case I have been trying over the past couple of weeks to get Cocos2D working with a UIScrollView container.  This is a requirement for the game we are working on to allow players to get the familiar scrolling movement you see in many iPhone apps.

We tried a number of approaches to get this to work properly…

  1. Cocos window embedded inside a UIScrollView instance
  2. Changing the size of the OpenGL rendering window to render a larger space.  Works but there is a serious performance hit.
  3. A UIScrollView instance overlaid on top of a Cocos window.  If the scroll movements were caught they could be passed onto the Cocos window manually.

Ultimately we chose option #3 and this is the option I will cover details for in this article as it is ultimately the only solution which worked for us properly.

Modifications to the Cocos source code

The first step to get this to work properly is a small modification to the Cocos source code.  The reason being that Cocos will freeze while working with a UIScrollView by default.  Luckily the location to you need to modify is noticeably documented in the Cocos source code.  In Director.m look for this block of comment with code…

//
// If you want to attach the opengl view into UIScrollView
// uncomment this line to prevent 'freezing'. It doesn't work on
// with the Fast Director
//
// [[NSRunLoop currentRunLoop] addTimer:animationTimerforMode:NSRunLoopCommonModes];

Simply uncomment this line of code to get freeze free scrolling.  Leave the lines commented out to see the difference in movement as my description may not make it readily apparent.

Using a view controller and a UIScrollView together

Detecting touches in UIScrollView

One of the necessary steps to getting this to work properly would be to detect both touches and swipes in the UIScrollView instance and pass those messages along to the Cocos window.  Researching this in Google revealed the following forum posting which was a huge help.  The solution there is to use a UIViewController where the view in that controller is an instance of UIScrollView.  Here’s what that setup looks like in our codebase…

CocosOverlayViewController.h

#import 
 
@interface CocosOverlayViewController : UIViewController
{
}
@end

CocosOverlayViewController.m

#import "CocosOverlayViewController.h"
#import "CocosOverlayScrollView.h"
 
@implementation CocosOverlayViewController
 
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView
{
  CocosOverlayScrollView *scrollView = [[CocosOverlayScrollView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
 
  // NOTE - I have hardcoded the size to 1024x1024 as that is the size of the levels in
  // our game.  Ideally this value would be parameterized or configurable.
  //
  scrollView.contentSize = CGSizeMake(1024, 1024);
 
  scrollView.delegate = scrollView;
  [scrollView setUserInteractionEnabled:TRUE];
  [scrollView setScrollEnabled:TRUE];
 
  self.view = scrollView;
 
  [scrollView release];
}
@end

CocosOverlayScrollView.h

#import 
 
@interface CocosOverlayScrollView : UIScrollView
{
}
@end

CocosOverlayScrollView.m

#import "CocosOverlayScrollView.h"
#import "cocos2d.h"
 
@implementation CocosOverlayScrollView
 
-(void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event
{
  if (!self.dragging)
  {
    UITouch* touch = [[touches allObjects] objectAtIndex:0];
    CGPoint location = [touch locationInView: [touch view]];
 
    [self.nextResponder touchesBegan: touches withEvent:event];
    [[[Director sharedDirector] openGLView] touchesBegan:touches withEvent:event];
  }
 
  [super touchesBegan: touches withEvent: event];
}
 
-(void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event
{
  if (!self.dragging)
  {
    [self.nextResponder touchesEnded: touches withEvent:event];
    [[[Director sharedDirector] openGLView] touchesEnded:touches withEvent:event];
  }
 
  [super touchesEnded: touches withEvent: event];
}
 
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
  // TODO - Custom code for handling deceleration of the scroll view
}
 
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
  CGPoint dragPt = [scrollView contentOffset];
 
  Scene* currentScene = [[Director sharedDirector] runningScene];
 
  // Only take the top layer to modify but other layers could be retrieved as well
  //
  Layer* topLayer = (Layer *)[currentScene.children objectAtIndex:0];
 
  dragPt = [[Director sharedDirector] convertCoordinate:dragPt];
 
  dragPt.y = dragPt.y * -1;
  dragPt.x = dragPt.x * -1;
 
  CGPoint newLayerPosition = CGPointMake(dragPt.x + (scrollView.contentSize.height * 0.5f), dragPt.y + (scrollView.contentSize.width * 0.5f));
 
  [topLayer setPosition:newLayerPosition];
}
 
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
  CGPoint dragPt = [scrollView contentOffset];
 
}
@end

This is by no means a perfect solution yet as I still have some values hardcoded such as the size of the content for the scroll view window. I will certainly be working to improve the finer points but hopefully the core issues I’ve put together will be useful to some of you.  Our next dev video (when we get it posted) will show the scroll view in action.

View Comments

  1. colinator says:

    Is this supposed to scroll the underlying layer in real-time? I'm not seeing it. What I see is that I can scroll, but the underlying layer, or whatever I'm modifying the position of, doesn't actually change its position until I mouse/finger up, or until deceleration is finished, finishing the scroll action. I thought it might be related to that first thing (uncomment that line), but that line never gets called anyway…

  2. Damian says:

    Yes, that is exactly the problem I outlined above.

  3. Damian says:

    Hmm, the text is statically created, but it's also part of a layer which in turn is scrolled in your code, I don't see why I would need to move the actual text.

    The scroll indicators are there by default so I do see them. The problem is exactly as colinator described in the post below.

  4. Rob Segal says:

    Good point about the layer Damian. The search continues then.

  5. Damian says:

    Yes, that is exactly the problem I outlined above.

  6. Damian says:

    Hmm, the text is statically created, but it's also part of a layer which in turn is scrolled in your code, I don't see why I would need to move the actual text.

    The scroll indicators are there by default so I do see them. The problem is exactly as colinator described in the post below.

  7. Rob Segal says:

    Good point about the layer Damian. The search continues then.

  8. Rob Segal says:

    Ok I think I have tracked down the source of this issue. I will update the article to reflect the details but the core of it comes down to setting the Director type properly when you are first making use of it.

    In the HelloWorld sample you'll notice the following lines of code at the beginning of the applicationDidFinishLaunching callback…

    // Try to use CADisplayLink director
    // if it fails (SDK < 3.1) use Threaded director
    if( ! [Director setDirectorType:CCDirectorTypeDisplayLink] )
    [Director setDirectorType:CCDirectorTypeDefault];

    If you comment these lines out you should now get the real time scrolling effect. The reason has to do with how Cocos initializes itself based on the use of the setDirectorType method and how scrolling is able to work. If you want the full details I can review here but I figure its better just to update the article. Colinator and Damian if you could both try this out and let me know how things turn out it I would most appreciate it.

  9. Rob Segal says:

    Ok I think I have tracked down the source of this issue. I will update the article to reflect the details but the core of it comes down to setting the Director type properly when you are first making use of it.

    In the HelloWorld sample you'll notice the following lines of code at the beginning of the applicationDidFinishLaunching callback…

    // Try to use CADisplayLink director
    // if it fails (SDK < 3.1) use Threaded director
    if( ! [Director setDirectorType:CCDirectorTypeDisplayLink] )
    [Director setDirectorType:CCDirectorTypeDefault];

    If you comment these lines out you should now get the real time scrolling effect. The reason has to do with how Cocos initializes itself based on the use of the setDirectorType method and how scrolling is able to work. If you want the full details I can review here but I figure its better just to update the article. Colinator and Damian if you could both try this out and let me know how things turn out it I would most appreciate it.

  10. Damian says:

    Still the same problem, however I've been modifying that code a bit, essentially adding “scrollView” to “window” as a subview, here's the modified the code:

    // before creating any layer, set the landscape mode
    [[Director sharedDirector] setDeviceOrientation:CCDeviceOrientationLandscapeLeft];
    [[Director sharedDirector] setAnimationInterval:1.0/60];
    [[Director sharedDirector] setDisplayFPS:YES];

    CocosOverlayScrollView* scrollView = [[CocosOverlayScrollView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
    scrollView.contentSize = CGSizeMake(320, 960);

    [scrollView setUserInteractionEnabled:TRUE];
    [scrollView setScrollEnabled:TRUE];

    [window addSubview:scrollView];

    // create an openGL view inside a window
    [[Director sharedDirector] attachInView:scrollView];
    [window makeKeyAndVisible];

    [[Director sharedDirector] runWithScene: [HelloWorld scene]];

    Now although the scene does stop for a moment when scrolling, you can still see the smooth scrolling movement once you lift up your finger, before the movement was jerky, kinda like jumping from one frame to another.

    However if I do comment the lines you mentioned Rob, that is no longer the case, only the scrolling movement is not as smooth as when they are not commented, not sure why?

  11. Damian says:

    Still the same problem, however I've been modifying that code a bit, essentially adding “scrollView” to “window” as a subview, here's the modified the code:

    // before creating any layer, set the landscape mode
    [[Director sharedDirector] setDeviceOrientation:CCDeviceOrientationLandscapeLeft];
    [[Director sharedDirector] setAnimationInterval:1.0/60];
    [[Director sharedDirector] setDisplayFPS:YES];

    CocosOverlayScrollView* scrollView = [[CocosOverlayScrollView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
    scrollView.contentSize = CGSizeMake(320, 960);

    [scrollView setUserInteractionEnabled:TRUE];
    [scrollView setScrollEnabled:TRUE];

    [window addSubview:scrollView];

    // create an openGL view inside a window
    [[Director sharedDirector] attachInView:scrollView];
    [window makeKeyAndVisible];

    [[Director sharedDirector] runWithScene: [HelloWorld scene]];

    Now although the scene does stop for a moment when scrolling, you can still see the smooth scrolling movement once you lift up your finger, before the movement was jerky, kinda like jumping from one frame to another.

    However if I do comment the lines you mentioned Rob, that is no longer the case, only the scrolling movement is not as smooth as when they are not commented, not sure why?

  12. Rob Segal says:

    Hi Damian sorry I haven't gotten back to you over the past couple of days. I think I got you for most of the details you covered. On this point though…

    “However if I do comment the lines you mentioned Rob, that is no longer the case, only the scrolling movement is not as smooth as when they are not commented, not sure why?”

    You're saying that when you don't comment out the two lines I mentioned the scrolling is smoother than when they are commented? I'll try out your code to see if I can pickup on anything.

  13. Rob Segal says:

    Hi Damian sorry I haven't gotten back to you over the past couple of days. I think I got you for most of the details you covered. On this point though…

    “However if I do comment the lines you mentioned Rob, that is no longer the case, only the scrolling movement is not as smooth as when they are not commented, not sure why?”

    You're saying that when you don't comment out the two lines I mentioned the scrolling is smoother than when they are commented? I'll try out your code to see if I can pickup on anything.

  14. Damian says:

    No worries Rob, I appreciate the time you're giving to help out here.

    Yes, when I don't comment the lines you previously mentioned, scrolling is indeed smoother for some reason, though you get your occasional hiccups. It's like with commenting these lines, the scrolling movement drops a couple frames but never stops, and when leaving these lines, the scrolling movement itself is very smooth, only the scene animation stops for a moment on the very beginning of the scroll movement. It's kinda hard to explain in words, but hopefully you'll see what I mean when testing the code yourself.

  15. Damian says:

    No worries Rob, I appreciate the time you're giving to help out here.

    Yes, when I don't comment the lines you previously mentioned, scrolling is indeed smoother for some reason, though you get your occasional hiccups. It's like with commenting these lines, the scrolling movement drops a couple frames but never stops, and when leaving these lines, the scrolling movement itself is very smooth, only the scene animation stops for a moment on the very beginning of the scroll movement. It's kinda hard to explain in words, but hopefully you'll see what I mean when testing the code yourself.

  16. Rob Segal says:

    Hi Damian,

    I have not forgotten about you. I just haven't had that much time to look into this issue over the past couple of days. I was trying out some things just now and found that the scrolling actually works fine for me using the code we've discussed. I'm wondering if you might be able to send me all the code files you've modified when working with the hello world sample. I would guess its just the one file (HelloWorld.m) but if not let me know. Send it to (rob #at# getsetgames.com).

  17. Rob Segal says:

    Hi Damian,

    I have not forgotten about you. I just haven't had that much time to look into this issue over the past couple of days. I was trying out some things just now and found that the scrolling actually works fine for me using the code we've discussed. I'm wondering if you might be able to send me all the code files you've modified when working with the hello world sample. I would guess its just the one file (HelloWorld.m) but if not let me know. Send it to (rob #at# getsetgames.com).

  18. Oscar says:

    Thank you this has worked great on my game!.
    However there is one problem I still haven't figured out a solution for while commenting:
    //if( ! [Director setDirectorType:CCDirectorTypeDisplayLink] )
    //[Director setDirectorType:CCDirectorTypeDefault];
    Does make the uiscrollbar scroll pretty smoothly, sometimes when the application starts 1 out of 5 times it will top at 40 fps and stay there, while usually when my agme starts it will do 60 fps. Apparently riq responded in the cocos2d forums that this has something to do precisely with not using the Director Displaylink type.

    Does anyone else have the same problem?. Any help will be greatly appreciated. Thank you.

    -Oscar

  19. Rob Segal says:

    Hi Oscar glad the article helped you out. Can't say I've heard anything about this issue but if you post some of your code it might give a better idea of how your trying to set things up and where you might be running into issues. You may also want to try posting on the Stack Overflow community (http://www.stackoverflow.com). There are many people using Cocos2D there.

  20. Carl Knott says:

    Hi, I am having problems integrating a UIScrollView within a cocos2D layer. Scrolling work's fine, however, when I scroll across the touch event's on my sprite are not registered. If I scroll back to the original position of the UIScrollView it picks up the touch events perfectly. TBH, I'm banging my head against a wall here and I haven't got a clue what to do. Bit of a background on the app: sprites span across three pages, 320*480, they are added dynamically, when a page is filled with sprites it will scroll onto the next one. Thanks in advance :)

  21. Rob Segal says:

    Sounds like the sprite positions are not being updated along with the movement of the underlying layer. I can't remember if this is done implicitly for you in Cocos2D. Something you could try is scrolling a little bit in one direction and see how that effects touch position. For example if you scroll to the right very slightly does your touch get detected to the left the amount you scrolled to the right? Could be you are get a touch position/sprite position reflection if that makes any sense. Let me know if not.

  22. Carl says:

    Thanks for all of your help BTW.

    Yes I think you might be on to something, this is the behaviour…

    If I move the UIScrollbar to the left, by the width of the sprite, the sprite will respond to a touch event double its orignal position to the left. Again, if I move the UIScrollbar to the left but this time by half the width of the sprite, the sprite will respond to a touch event double the movement of the UIScrollbar… man, I hope I have explained this correctly :)

    Any ideas?

  23. Rob Segal says:

    Ok so sounds like its an issue of updating the sprites position while you're scrolling. I don't have the code I used for our games in front of me so I can't tell you off hand if you need to update the sprites on the layer explicitly. When you do the scrolling action do the underlying sprites move in the direction you want?

  24. Carl says:

    Yes, the movement of the sprites if perfect.

  25. Rob Segal says:

    Ok well I'm not sure which one of these it is but there are several items which get their position changed when you scroll.

    -The layer
    -The UIScrollView area
    -The sprites

    We'll have to figure out which one exactly is not updating correctly perhaps with some log statements. I believe that is how I tracked down my issues with touches and scrolling. Put a print statement when the position changes for each of these different objects and you can see if they are being updated correctly. So when you scroll up and to the left do the other objects do the same thing? Does that make any sense or do you want some help setting that up?

  26. Carl says:

    Thanks for all of your help BTW.

    Yes I think you might be on to something, this is the behaviour…

    If I move the UIScrollbar to the left, by the width of the sprite, the sprite will respond to a touch event double its orignal position to the left. Again, if I move the UIScrollbar to the left but this time by half the width of the sprite, the sprite will respond to a touch event double the movement of the UIScrollbar… man, I hope I have explained this correctly :)

    Any ideas?

  27. Rob Segal says:

    Ok so sounds like its an issue of updating the sprites position while you're scrolling. I don't have the code I used for our games in front of me so I can't tell you off hand if you need to update the sprites on the layer explicitly. When you do the scrolling action do the underlying sprites move in the direction you want?

  28. Carl says:

    Yes, the movement of the sprites if perfect.

  29. Rob Segal says:

    Ok well I'm not sure which one of these it is but there are several items which get their position changed when you scroll.

    -The layer
    -The UIScrollView area
    -The sprites

    We'll have to figure out which one exactly is not updating correctly perhaps with some log statements. I believe that is how I tracked down my issues with touches and scrolling. Put a print statement when the position changes for each of these different objects and you can see if they are being updated correctly. So when you scroll up and to the left do the other objects do the same thing? Does that make any sense or do you want some help setting that up?

  30. orochi says:

    Hello, I did it and worked.

    The only problem is when I create another thread with:

    [NSThread detachNewThreadSelector:@selector(someSelector:) toTarget:self withObject:extraParams];

    The CocosNode freezes!

    What could be do to solve this problem?

  31. orochi says:

    “What could be do to solve this problem?”

    I'm sorry that was not the right question. The right one is:

    The creation of a NSThread can influence somehow the cocos2d Director?

    I tryed to update the code with the answeres on this post, but anything worked.

    The NSThred is used to handle NetWork events, but the Graphical interface and the UIScrollView is the same thread. This object and the GUI is not called anywhere in the second thread.

    The simple creation of a new NSThread makes the UIScrollView to freeze.

  32. Rob Segal says:

    Unfortunately I haven't played around too much with spawning other threads and Cocos2D. Do you really need to spawn a new thread for the work you are doing? If you post some of the details of your needs maybe we can come up with an alternative solution to your problem.

  33. orochi says:

    I really need, the game network, I believe, it must run in another thread. I don't know another way to make a network without put another thread.

    But I'm not sending and receiving anything by now. I just create another thread with NSThread. Try to do this and you'll see the problem when scrolling:

    - (void)someFunction
    {
    printf(“Just printing this in another threadn”);
    }

    - (void)initAThread
    {
    [NSThread detachNewThreadSelector:@selector(someFunction:) toTarget:self withObject:extraParams]
    }

    For me there is no sense on this. Maybe there are some specific stuff of NSRunLoop interaction with NSThreads.

  34. Rob Segal says:

    I don't recall the specific API calls and/or package names but Cocos2D does have a multiplayer/networking framework built into it. You should definitely check that out. Failing that there is likely some third party middleware to handle this functionality. Let me know if you can't find anything though.

  35. Peter says:

    Hi Rob,

    Thanks for sharing this post.

    I’ve managed to get the scrolling down, but when it comes to handling the touches at the CocosLayer (in your case TopLayer) I run into issues with objects like ccmenuitems. The coordinates of the scrollview don’t pair up well (when forwarded to the next responder) with the coordinates of the CocosLayer. I suspect this is because the CocosLayer has actually moved to a new location.

    To illustrate i have a CocosLayer which is 480*3 wide and 320 tall. In landscape mode I intend this view to be swiped horizontally with paging enabled. I have 3 menuitems in the middle of each page. As I attach your solution, I am able to mimic
    the scrollview very convincingly =). Also, when I touch the first menuItem, it works, however when i swipe to across to the next page and press the second menuItem, it actually activates the third menuItem. When I touch the third menuItem nothing happens.

    It seems as though the touches a misaligned. Any thoughts?

  36. Rob Segal says:

    Hi Peter sorry for my delayed reply. Since you have the scrolling aspect working I suspect there might be some kind of mismatch with how touches are handled on menu items. I would suggest trying a few things:

    1. Put a breakpoint in the CocosOverlayScrollView touchesBegan handler. Then when you scroll to the second and third pages touch a few places that are not the menu items and print to NSLog the position of those touches. You should be able to determine by looking at those numbers whether the touches are properly translating as you move from page to page.

    2. Create a sprite as a child of your layer and position it so it shows up on the third page only. One you scroll to the third page see if you can detect touches on that sprite using a touch delegate on that sprite and printing out a short log statement so you know its been pressed,

    Using these two methods you should be able to determine why your touch coordinates are not properly translating as you swipe and make the appropriate corrections. Let me know if my suggestions make sense though and if you need any more feedback.

  37. Peter says:

    Thanks for the reply. I’ve printed out the coordinates of the touches, and what they tell me is that the coordinates of the scrollview is being used. So as I am scrolling right, the center point of the scrollview is now (720, 160). Meanwhile, the CocosLayer has shifted left, making the center point of the first page (-240, 160), second page (240,160) and third page (720,160). So this explains why clicking the menu button on the 2nd page registers a touch on the menu on the third page.

    Ideally, I would like to specify that the CocosLayer coordinates be used. Is this possible?

    I’m not clear on #2 of your instructions. Do you want me to subclass a CCSprite and implement some touch methods?

  38. Paul says:

    Hi Peter and Rob. I am also working on this problem. Only difference is I am using portrait mode on the iPad. The solution should be the same though. I haven’t got one yet but If you figure it out please post it. If I get it working I will post the solution I came up with.

  39. Rob Segal says:

    Hi Peter. As far as I know the CocosLayer coordinates are being used so I’m not sure you need to change anything there. I believe Cocos takes care of changing what is considered x/y as you make changes to the orientation of the device. I am not 100% sure of that as its been a while since I revisited this code but I am pretty sure.

    Since you are scrolling one way and the layer is moving the opposite way what you could try modifying the code inside the function…

    – (void)scrollViewDidScroll:(UIScrollView *)scrollView

    Note the lines where the dragPt variable is multiplied by -1. Try changing the line where the x coordinate is multiplied by -1 to 1. So you’ll have…

    dragPt.x = dragPt.x * 1;

    Strange that you would be having these orientation issues but maybe some of the Cocos code has been changed I am not aware of.

    As for #2 I don’t mean that you should sublcass CCSprite but simply create a CCSprite on the layer which you are scrolling and attach it to the layer to show up on the 3rd page. If you were able to select that sprite which would show up on the 3rd page it would mean your touches are properly translated as you scroll. Looks like you have confirmed that’s already not happening via the print statements and so using the CCSprite was really just another way to confirm that fact.

  40. Rob Segal says:

    Hi Paul thanks for the input. Try out my suggestion of changing the multipler on the dragPt in the scrollViewDidScroll method. This is ultimately what controls which way the Cocos layer will scroll so it should be a matter of tweaking those values so scrolling is happening correctly.

  41. Peter says:

    Hey Rob and Paul,

    I had a suspicion that it was a CCMenu issue as I’ve run into previous problems involving touch propagation and menuItems. I simply replaced each menu item with a sprite and voila! Everything works perfectly. So my suspicion has been confirmed. Still, hate to leave a problem unsolved like that, as I still do not know why the menuItems are misbehaving. Another issue when using menuItems I found was: if you clicked on the menuItem and held it until the menuItem responded (bounce animation) and then scrolled, I found that all the menuItems
    would no longer respond to touch. If anyone could shed some light on this that would be great.

    Thx again Rob.

  42. Rob Segal says:

    Hi Peter glad to hear you were able to decipher some of the mystery. At this point I would say it would be best to pose your question on the Cocos2D forums (http://www.cocos2d-iphone.org/forum/). Speaking for myself I have not applied any of the scrolling functionality to CCMenu items so I don’t think I could be much more help. I know how you feel about leaving potential mysteries unsolved.

Leave a Reply

blog comments powered by Disqus