Cocos2D and UIScrollView

Aug 21, 2009   //   by Rob Segal   //   Development  //  126 Comments

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.

EDIT: If you are using Cocos 0.99.4 or later use the following directions provided by one of our helpful community members “drootang” for solving the freezing issue…


I’m using 0.99.4 and it took me a while to figure out that the new hello world uses the following line in applicationDidFinishLaunching:

CC_DIRECTOR_INIT();

This macro (among other things) does the following:

if( ! [CCDirector setDirectorType:kCCDirectorTypeDisplayLink] )
[CCDirector setDirectorType:kCCDirectorTypeNSTimer];

You need to force it to set the director type to kCCDirectorTypeNSTimer to prevent cocos2d from “freezing” while scrolling. Either change
the line in the macro, or copy the macro contents to your appDidFinishLaunching method, and change the line above to simply:

[CCDirector setDirectorType:kCCDirectorTypeNSTimer] ;


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.

126 Comments

  • Hey that’s a good catch drootang. I certainly haven’t updated this code in a while but clearly I should! Thanks for sharing that.

  • Hey Familyturtle after reading drootang’s post its likely you are getting freezing on scrolling because you need to modify a portion of the Cocos source code first to get this to work.

    I do have a section of my blog post which indicates you need to uncomment a line of code to get freeze free scrolling to work. This has changed in the later versions of Cocos since I originally posted. See drootang’s directions above on how to modify Cocos to disable the freeze.

  • Hello!
    First if all.. Thank you for this post! It’s so helpful.

    I do however have problems starting it all up with my example.. Might just be the Holidays hangover, but here is my problem:

    I tried to apply you code to this example:
    http://geekanddad.wordpress.com/2010/06/22/enemies-and-combat-how-to-make-a-tile-based-game-with-cocos2d-part-3/

    Here is the Scene I am making:

    +(id) scene
    {
    // ‘scene’ is an autorelease object.
    CCScene *scene = [CCScene node];

    // ‘layer’ is an autorelease object.
    HelloWorld *layer = [HelloWorld node];

    // add layer as a child to scene
    [scene addChild: layer];

    HelloWorldHud *hud = [HelloWorldHud node];
    [scene addChild: hud];

    layer.hud = hud;

    hud.gameLayer = layer;

    CocosOverlayViewController *scrollView = [CocosOverlayViewController alloc];
    [[[CCDirector sharedDirector] openGLView] addSubview:scrollView.view];

    // return the scene
    return scene;
    }

    It almost works.. The UIScrollView looks fine but the CCTMXTiledMap is off, up and to the right. I am scrolling in the dark until I get there, and the TileMap cannot be seen completely.
    My guess is that I have to somehow link the positions of the scrollview to the position of the actual scene.. but I don’t understand how.

    Thanks again for all your effort!

  • Well for now, a simple workaround is to substract half the size of the scene from the drag point value. I still don’t understand why it would place my scene translated to the upper left corner, but it works for now:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    CGPoint dragPt = [scrollView contentOffset];

    CCScene* currentScene = [[CCDirector sharedDirector] runningScene];

    // Only take the top layer to modify but other layers could be retrieved as well
    //
    CCLayer* topLayer = (CCLayer *)[currentScene.children objectAtIndex:0];

    dragPt = [[CCDirector sharedDirector] convertToGL:dragPt]; //or maybe convertToUI

    dragPt.y = dragPt.y * -1 – SCENE_HEIGHT/2;
    dragPt.x = dragPt.x * -1 – SCENE_WIDTH/2;

    CGPoint newLayerPosition = CGPointMake(dragPt.x + (scrollView.contentSize.height * 0.5f), dragPt.y + (scrollView.contentSize.width * 0.5f));

    [topLayer setPosition:newLayerPosition];
    }

  • Well for now, a simple workaround is to substract half the size of the scene from the drag point value. I still don’t understand why it would place my scene translated to the upper left corner, but it works for now:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    CGPoint dragPt = [scrollView contentOffset];

    CCScene* currentScene = [[CCDirector sharedDirector] runningScene];

    // Only take the top layer to modify but other layers could be retrieved as well
    //
    CCLayer* topLayer = (CCLayer *)[currentScene.children objectAtIndex:0];

    dragPt = [[CCDirector sharedDirector] convertToGL:dragPt]; //or maybe convertToUI

    dragPt.y = dragPt.y * -1 – SCENE_HEIGHT/2;
    dragPt.x = dragPt.x * -1 – SCENE_WIDTH/2;

    CGPoint newLayerPosition = CGPointMake(dragPt.x + (scrollView.contentSize.height * 0.5f), dragPt.y + (scrollView.contentSize.width * 0.5f));

    [topLayer setPosition:newLayerPosition];
    }

  • Well for now, a simple workaround is to substract half the size of the scene from the drag point value. I still don’t understand why it would place my scene translated to the upper left corner, but it works for now:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    CGPoint dragPt = [scrollView contentOffset];

    CCScene* currentScene = [[CCDirector sharedDirector] runningScene];

    // Only take the top layer to modify but other layers could be retrieved as well
    //
    CCLayer* topLayer = (CCLayer *)[currentScene.children objectAtIndex:0];

    dragPt = [[CCDirector sharedDirector] convertToGL:dragPt]; //or maybe convertToUI

    dragPt.y = dragPt.y * -1 – SCENE_HEIGHT/2;
    dragPt.x = dragPt.x * -1 – SCENE_WIDTH/2;

    CGPoint newLayerPosition = CGPointMake(dragPt.x + (scrollView.contentSize.height * 0.5f), dragPt.y + (scrollView.contentSize.width * 0.5f));

    [topLayer setPosition:newLayerPosition];
    }

  • Well for now, a simple workaround is to substract half the size of the scene from the drag point value. I still don’t understand why it would place my scene translated to the upper left corner, but it works for now:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    CGPoint dragPt = [scrollView contentOffset];

    CCScene* currentScene = [[CCDirector sharedDirector] runningScene];

    // Only take the top layer to modify but other layers could be retrieved as well
    //
    CCLayer* topLayer = (CCLayer *)[currentScene.children objectAtIndex:0];

    dragPt = [[CCDirector sharedDirector] convertToGL:dragPt]; //or maybe convertToUI

    dragPt.y = dragPt.y * -1 – SCENE_HEIGHT/2;
    dragPt.x = dragPt.x * -1 – SCENE_WIDTH/2;

    CGPoint newLayerPosition = CGPointMake(dragPt.x + (scrollView.contentSize.height * 0.5f), dragPt.y + (scrollView.contentSize.width * 0.5f));

    [topLayer setPosition:newLayerPosition];
    }

  • Lool… I knew I shouldn’t drink and program or at least not post on the net when I am hungover. I just realized the problem was from this line:

    CGPoint newLayerPosition = CGPointMake(dragPt.x + (scrollView.contentSize.height * 0.5f), dragPt.y + (scrollView.contentSize.width * 0.5f));

    Easy as pie if your head is clear enough to see it. So sorry to bother you, please ignore my previous posts. But just a question… Why was this line necessary in the first place? Maybe the sdk has changed meanwhile and you don’t need this hack anymore?
    Once again.. you’ve been very helpful with this post and thank you!

  • I would like to know how to enable zoom function with this scroll view , cuz i can’t return a CCLayer or wt else

  • I would like to know how to enable zoom function with this scroll view , cuz i can’t return a CCLayer or wt else

  • I would like to know how to enable zoom function with this scroll view , cuz i can’t return a CCLayer or wt else

  • I would like to know how to enable zoom function with this scroll view , cuz i can’t return a CCLayer or wt else

  • Hello!

    Can someone please tell me how to add a tile Map to a Scroll View?

  • [...] help me understand this cocos 2d code So I have implemented this code http://getsetgames.com/2009/08/21/cocos2d-and-uiscrollview/ for to get UIScrollview working with cocos2d. My question is, does the camera traverse across the [...]

  • [...] scrollviews in cocos2d. Turns out this isn’t as straight forward as I thought, but I found a tutorial over at getsetgames.com which gave me a good place to start. I have to warn you, the code in that tutorial is pretty [...]

  • I don’t see CC_DIRECTOR_INIT() called in 1.0 cocos2d HelloWorld sample.  Is this still relevant?

  • Yes it exists, try finding it in “Search”.

  • OK, thanks.

  • [...] rest of the UIScrollView stuff can be found here, http://getsetgames.com/2009/08/21/cocos2d-and-uiscrollview/. I hope someone finds my blogpost useful [...]

  • Can anybody help me solve this problem of scroll in cocos2d. http://stackoverflow.com/questions/8366443/how-to-scroll-characters-on-a-label-in-cocos2d

  • I tried to implement this as well and got the following error message:

    “The view returned from viewForZoomingInScrollView: must be a subview of the scroll view. It can not be the scroll view itself.”

    What I gather from this is that if I want to use the scrollView to zoom a cocos2d layer, I would have to first:
    - create a subview- then attach my layer to it
    The scrollView should then zoom the subview and since the layer is statically bound to the subview, it will scale with it.

    That’s the idea at least….

  • I found a solution:

    in - (void)loadView in your UIViewController add:

     UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(myLayer.position.x, 
    myLayer.position.y, 
    myLayer.contentSize.height, myLayer.contentSize.width)];        [self.view addSubview:
    subView ];as well as:yourScrollView.minimumZoomScale = 0.25;  // your own valueyourScrollView.maximumZoomScale = 1.0; // your own valuein your UIScrollView delegate implement:

    - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{    return [[self subviews] objectAtIndex:0];}

    - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {}

    - (void)scrollViewDidZoom:(UIScrollView *)scrollView{    CCLayer *targetLayer = [[[CCDirector sharedDirector]runningScene].children objectAtIndex:0 ];    targetLayer.scale = self.zoomScale;}

    This will enable zooming of your layer, however, I still encounter a problem that when zooming, the scaled layer’s position gets pushed to the top right outside of the pannable, zoomable view when zooming scale is lower than 1.0      Does anyone have a solution to this?

  • Formatting of the middle section got messed up:

    in - (void)loadView in your UIViewController add: UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(myLayer.position.x, myLayer.position.y, myLayer.contentSize.height, myLayer.contentSize.width)];        [self.view addSubview:subView ];

    as well as:

    yourScrollView.minimumZoomScale = 0.25;  // your own value

    yourScrollView.maximumZoomScale = 1.0; // your own value

    in your UIScrollView delegate implement:

    - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{    return [[self subviews] objectAtIndex:0];}- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {}- (void)scrollViewDidZoom:(UIScrollView *)scrollView{    CCLayer *targetLayer = [[[CCDirector sharedDirector]runningScene].children objectAtIndex:0 ];    targetLayer.scale = self.zoomScale;} 

  • I give up :)

  • that is not with tilemap scrolling that is parallax scrolling check for tilemap in depth

Leave a comment

Our Games

Latest Tweets