2D Scrolling Game with Cocos2D TileMap with Zoom!


A common question I see on the cocos2d forums is ‘when I want to make my game scroll, do I move the camera or the layer?’ or some variant of that. I also got some more detailed questions about how to make a functioning 2D scroller, so I’m going to described how I got it to work.

First of all, the answer to the above question is you move the layer. If you follow the examples in the cocos2d download, you have a “GameScene” and a “GameLayer”. Well, everything that needs to be moved when your game scrolls should be added as a child to that GameLayer. If you are using a TileMap as a background, this includes that tilemap. The only thing that you don’t add as a child to the GameLayer is stuff that does not move with the scrolling view, such as your HUDLayer that has text that shows your characters health. Other than that, your character, the background, other characters, should all be added to the GameLayer.

You have to remember which objects are absolute (attached to your GameScene or other layers) versus those that are relative (a child of your GameLayer) when you set up your touch handling code. For my HUD that has buttons you can press at any time, say to pause the game, you want to add the touch handling object to your GameScene or HUDLayer class, since it doesn’t move. But if you want to be able to touch objects that scroll along with your view in the game itself, your touch handling code needs to be in an object that is a child of your GameLayer.

This might be a little confusing, so let’s see some code:

	gameLayer = [GameLayer node];
	[gameScene addChild:gameLayer z:zOrder_GameLayer];
 
	hudLayer = [HUDLayer node];
	[gameScene addChild:hudLayer z:zOrder_HudLayer];
 
	tileMap = [BGTileMap node];
	[gameLayer addChild:tileMap z:-1];
 
	[gameScene addChild:[PauseGameButton node] z:zOrder_GameButtons];
	[gameLayer addChild:[FireGunAtTouchPoint node]];

The pause game button is always on your screen, while the point at which your character fires the gun depends on how how far your view has been scrolled (by moving GameLayer).

Let’s see some of the code that actually moves this game layer:

- (void)setViewpointCenter:(CGPoint)point {
	CGPoint centerPoint = ccp(240, 160);
	viewPoint = ccpSub(centerPoint, point);
 
	// dont scroll so far so we see anywhere outside the visible map which would show up as black bars
	if(point.x < centerPoint.x)
		viewPoint.x = 0;
	if(point.y < centerPoint.y)
		viewPoint.y = 0;
 
	// while zoomed out, don't adjust the viewpoint
	if(!isZoomedOut)
		gameLayer.position = viewPoint;
}

When do you call that method? Well, it depends on what you want, but generally these scrolling games follow around the movement of your ‘main’ character, right? So whatever character the you want to follow, add this to override the standard CocosNode setPosition method so you update your viewpoint whenever the character moves

- (void)setPosition:(CGPoint)point {
	[[StandardGameController sharedSingleton] setViewpointCenter:point]; 
	[super setPosition:point];
}

Note that the StandardGameController is a construct of mine that I use to separate the game logic out from the display code. It doesn’t matter exactly how you do it, you just need a way to have your main character object call back to something that contains a reference to GameLayer so it can adjust the position of your GameLayer.

Now remember, for your background to scroll properly, you need to add your background tileMap as a child of your GameLayer that is being moved around.

That being said, I found that an important method was missing from the cocos2d tilemap that I need to use in order to detect collisions based on the types of tiles encountered. I created a subclass of TMXTiledMap and added in these methods:

- (CGPoint)coordinatesAtPosition:(CGPoint)point {
	return ccp((int)(point.x / self.tileSize.width), (int)(self.mapSize.height - (point.y / self.tileSize.height)));
}
 
- (unsigned int)getGIDAtPosition:(CGPoint)point {
	return [layer tileGIDAt:[self coordinatesAtPosition:point]];
}

That way it’s easy to figure out what tile any individual object is colliding with. For example, in my main character object I can have code that runs in step: function with this:

	BGTileMap* tileMap = [StandardGameController sharedSingleton].tileMap;
	CGPoint coordinate = [tileMap coordinatesAtPosition:self.position];
	BBLog(@"Right now on tile %d",[tileMap.layer tileGIDAt:coordinate]);

Now I know what type of tile I am overlapping, and I can respond to the environment accordingly.

This is really all the code that you need to make a scrolling game view…I think some people overthink it and try adjusting the position of every object individually with some offset, but it’s not necessary since your objects can use relative positions with their parent.

I have one last bit of code to add, and this is something kind of fun. It’s not complete, but at least it’s a start. What this allows you to do is ‘zoom out’ so you can see your entire map with ALL the objects shrunk down, and then zoom back in to your character. The only tricky part is when you zoom back in, you have to slowly adjust your viewpoint in steps so the zoom in action is centered on your character, instead of jumping at the end.

#define ZOOM_BACK_IN_INTERVALS 10
#define ZOOM_OUT_RATE 0.3
// TODO need to refine this so for each step it uses the new viewpoint
- (void)setZoom:(BOOL)zoomedIn {
	BBLog(@"Zooming in %d", zoomedIn);
 
	// this scales it out so the whole height of the tilemap is in the screen
	float zoomScaleFactor = 320 / (tileMap.mapSize.height * tileMap.tileSize.height);
	if(zoomedIn) {
		[gameLayer runAction:[ScaleTo actionWithDuration:ZOOM_OUT_RATE scale:1.0]];
		[gameLayer runAction:[MoveTo actionWithDuration:ZOOM_OUT_RATE position:viewPoint]];
		[self schedule:@selector(setZoomedBackIn:) interval:ZOOM_OUT_RATE]; // need this for the transistion 
	} else {
		[gameLayer runAction:[ScaleTo actionWithDuration:ZOOM_OUT_RATE scale:zoomScaleFactor]];
		[gameLayer runAction:[MoveTo actionWithDuration:ZOOM_OUT_RATE position:ccp(0,0)]];
		isZoomedOut = YES;
	}
}
 
// need this small correction at the end to account of the player is moving and the viewpoint has changed to avoid jitter
#define ZOOM_OUT_CORRECTION_RATE 0.3
- (void)setZoomedBackIn:(ccTime)dt {
	[self unschedule:@selector(setZoomedBackIn:)];
	[gameLayer runAction:[MoveTo actionWithDuration:ZOOM_OUT_CORRECTION_RATE position:viewPoint]];
	[self schedule:@selector(setZoomedBackInFinished:) interval:ZOOM_OUT_CORRECTION_RATE]; // need this for the transistion 
}
 
- (void)setZoomedBackInFinished:(ccTime)dt {
	[self unschedule:@selector(setZoomedBackInFinished:)];
	isZoomedOut = NO;
}

Now you see why my viewPoint variable was a class member…you’ll need it for these methods to work.

The reason this code isn’t complete is when it starts zooming in, it creates the actions to zoom in on the current viewpoint, but if the player is moving by the time the zoom animation is done, that viewpoint is changed and needs to ’snap’ to the new viewpoint. That is the reason for the setZoomedBackIn: method, which really shouldn’t have to move the gameLayer anymore. However, I haven’t yet written the code to continuously create smaller move actions to take into account a moving viewpoint as the animation continues, but doing so shouldn’t be that hard. If you want to see that bit when I finish, post in the comments and I’ll add it in.

  1. #1 by Brad on October 6th, 2009

    hey… thanks for the post… I’d definitely like to see your zoom correction code when you get it done…. and I’m wondering, why did you choose “move the layer” over “move the camera”? It sounds like moving the camera would be best from a philosophical point of view ;-) I just started to implement this, and implemented the follow the character part using the camera…. but I was about to go figure out the zooming stuff and came across your post…

  2. #3 by Ed on October 6th, 2009

    Thanks for posting this info.

    I have a question regarding subclassing TMXtilemap. Where should layer be declared?

    - (unsigned int)getGIDAtPosition:(CGPoint)point {
    return [layer tileGIDAt:[self coordinatesAtPosition:point]];
    }

    • #4 by Eric on October 6th, 2009

      layer is a TMXLayer*, and when I load the TileMap I set

      layer = [self layerNamed:@"Layer 0"];

  3. #5 by Ed on October 6th, 2009

    Is this correct?

    //
    //BGTileMap.h

    #import “cocos2d.h”
    #import “TMXTiledMap.h”

    @interface BGTileMap : TMXTiledMap {

    TMXLayer *layer;

    }

    - (TMXLayer *) layer;
    - (CGPoint)coordinatesAtPosition:(CGPoint)point;
    - (unsigned int)getGIDAtPosition:(CGPoint)point;

    @end

    //BGTileMap.m

    #import “cocos2d.h”
    #import “BGTileMap.h”

    @implementation BGTileMap

    -(TMXLayer *) layer
    {
    return layer = [self layerNamed:@"Layer 0"];
    }

    - (CGPoint)coordinatesAtPosition:(CGPoint)point {
    return ccp((int)(point.x / self.tileSize.width), (int)(self.mapSize.height – (point.y / self.tileSize.height)));
    }

    - (unsigned int)getGIDAtPosition:(CGPoint)point {
    return [layer tileGIDAt:[self coordinatesAtPosition:point]];
    }

    @end

    • #6 by Eric on October 6th, 2009

      What you did works, as long as you call your Layer method once. Instead of this…
      -(TMXLayer *) layer
      {
      return layer = [self layerNamed:@"Layer 0"];
      }

      You could do this to clean up your code…

      - (id)init {
      if(self = [super initWithTMXFile:@"debug_level_small.tmx"]) {
      layer = [self layerNamed:@"Layer 0"];
      }
      return self;
      }

      but it works either way.

  4. #7 by Jim on October 6th, 2009

    Very interested in seeing more! ^^

  5. #8 by Artakus on October 11th, 2009

    Thanks for posting this nice tutorial. I’ve read through all this but somehow I’ve stuck at some part. I’ve created separate Layer Classes, GameLayer and BGTileMap. Which layer we should initialize tilemap ?. I wish I could see the a bit more code to understand the concept especially about the movement map. :-| Thanks again for this nice tutorial

    • #9 by Eric on October 12th, 2009

      Initialize the tilemap in BGTileMap (which is why the name is similar.

  6. #10 by Artakus on October 11th, 2009

    Allright, you wrote:
    “touch handling code needs to be in an object that is a child of your GameLayer.”

    I put my touch handling (ccTouchesBegan: ) in BGTileMap class where it is a child of GameLayer as you said. How could I be able to touch it the BGTileMap Layer interface if it doesn’t extends layer?

    • #11 by Eric on October 12th, 2009

      You can add the interface to your class and then register it as a touch event handler in order to receive touch events even if it is not a Layer. You can find how to do this in any of the coocs2d example files.

  7. #12 by yuen on October 18th, 2009

    [gameScene addChild:gameLayer z:zOrder_GameLayer];
    [gameScene addChild:hudLayer z:zOrder_HudLayer];

    can i know what mean by zOrder_GameLater??
    and why need to use it?

    • #13 by Eric on October 19th, 2009

      Those numbers just describe which layer is on top. You usually want your HUD layer on top of your game layer, so just do this:

      [gameScene addChild:gameLayer z:2];
      [gameScene addChild:hudLayer z:3];

      You don’t want your paused button covered up by your game character, right?

      • #14 by yuen on October 19th, 2009

        If I have this both layer, HUDLayer on top of the gameLayer, so means I can only touch the HUDLayer right? So, if I want to touch the character in game Layer so how do i touch the gameLayer if the layer covered by hudLayer? How do you order the gameLayer on top of the HUDLayer?

        • #15 by Eric on October 20th, 2009

          Touch events can pass through layers, the ordering only refers to the visual z-order of the elements. You want the HUDLayer to be over the gamelayer, so give it a higher z value like in the code I listed above.

    • #17 by Eric on October 27th, 2009

      To reply to comment #16, you can manage touch events with layers how ever you want.

      I really think you have a loose grasp on a lot of the cocos2d framework, and rather than trying to ‘fix bugs’ you should review all the example code and play around with all the samples. It will help you to see working source code to understand how to work with it.

  8. #18 by yuen on October 19th, 2009

    What I mean above is in runTime. How do you do that?

  9. #19 by Artakus on October 20th, 2009

    Regarding to the post above, where did you put the touch event handler? in HUDLayer or gameLayer?

    • #20 by Eric on October 21st, 2009

      It depends what you want the touch position relative to. If you want to be able to tap on a bomb near your player and compare the two positions between the bomb and the player on the gameLayer, than add the touch event handler to your gamelayer. If you want to be able to press a pause button that doesn’t move with the gamelayer, then add it to the hudlayer (or gamescene, doesn’t really matter).

  10. #21 by Tima on October 22nd, 2009

    Thanks for posting, it’s good tutuorial.
    But I don’t understand, how detect collisions based on the types, with rectangle and CGRectIntersectsRect?

    • #22 by Eric on October 22nd, 2009

      You can run a timer in the character code that does something like:

      if([[StandardGameController sharedSingleton].hudLayer getGIDAtPosition:self.position] == kGroundTile) {
      // do stuff
      }

      where kGroundTile is a constant (like 5) which matches up with a particular tile type that you figure out when drawing your TileMap in the editor.

      • #23 by Tima on October 22nd, 2009

        But if sprite a big it fix collisions with one center point. Suppose my sprite present rectangle, and I want detect collisions with CGRectIntersectsRect method.

        if (CGRectIntersectsRect( SpriteRectangle, groundTileRectangle))
        {
        // do stuff
        }

        How I can create getGIDAtPosition: (CGRectangle) method (not CGPoint) method?

        • #24 by Eric on October 22nd, 2009

          It all depends on your game. You could possibly just check the 4 corners of the rectangle to see if any of them intersect the certain tile. If you wanted to check with a rectangle, then you’ll have to write that method yourself.

  11. #25 by Nabeel Shahzad on November 23rd, 2009

    Hi, Great Article..

    I am using your method for my game scrolling. I am using TMXTileMap..

    The map scrolls perfectly.. The problem is when my main character is moving I can see grid lines when the character is not moving these lines disappear. Right now my main character is not moving from the step function I am moving my character by move actions. Is that the problem?

    Did you had a similar problem?

    • #26 by Eric on November 23rd, 2009

      I’ve seen people ask about this issue on the cocos2d forums. I’m nearly sure you can find the answer there, just search around a bit. Sorry for not providing the link myself, but I’m busy these days :( .

      • #27 by Nabeel Shahzad on November 23rd, 2009

        never mind I found the solution on the forums..

        thanks anyway for the great post.

        Just another quick question.. did you had any problems while scrolling on the actual iphone? Now my game works nicely on the simulator, but on the iphone I see a thin black line moving up and down as the game layer scrolls.

        • #28 by Eric on November 26th, 2009

          Make your background images larger than the screen size. If you make them 480×320, you will see lines due to some latency. If you increase it to something like 600×500, you won’t see those lines.

          • #29 by Nabeel Shahzad on November 30th, 2009

            thanks.. I fixed using the artifact fixer on cocos2d download page. It removed the black in the tileset.

            Thanks Again!!

  12. #30 by Damian on November 28th, 2009

    Nice article, thanks! I am interested though in how you got the “PAUSE” button of the HUDLayer handled? I have an “always there” menu composed of buttons, each being a TextureNode, and I would like to handle touch events for them, it’s working fine until I scroll a bit, then these buttons no longer get the touch events, here’s the code I am using, basically I used the same code from “TouchesTest\Paddle.m” for the menu buttons, handling touch events as follows:

    - (BOOL)containsTouchLocation:(UITouch *)touch
    {
    return CGRectContainsPoint(self.rect, [self convertTouchToNodeSpaceAR:touch]);
    }

    This works but only when not scrolling the view, any thoughts?

    • #31 by Eric on November 30th, 2009

      Make sure you add your HUDLayer to your GameScene, and don’t move your GameScene or HUDLayer, only move your GameLayer when you have to scroll. That way you will continue to get the correct positions for events sent to your HUDLayer.

  13. #32 by Richard on February 23rd, 2010

    Great tutorial, many thanks!

    I would like to build my tilemap using Tiled but I’m still confused concerning how to define and detect certain types of tile in my game.

    For example, I build a tilemap using Tiled that contains 2 layers as below:

    Rooms layer
    Empty space layer (i.e. where player can freely move)

    I want to be able to do the following…

    1) Position the player sprite on a section of the Empty space layer

    2) Detect when the player sprite collides with any tile on the Rooms layer and block them

    I’m assuming that the code you provided in the tutorial will work for me, but I don’t really understand it fully.

    • #33 by Eric on February 23rd, 2010

      I wish I could help out more, but I have switched gears to a project that does not use Tiled and have not kept up to date with the latest update (such as multiple layers).

      Plus I’m kinda crazy busy at this moment. Good luck trying to figure it out though!

  14. #34 by baz on February 26th, 2010

    great post – cheers! translation and zooming is pretty easy, now I’ve seen your tricks ;)

  15. #35 by Daniel on March 7th, 2010

    I am making a game where I have the background scroll to follow a ball that the player moves. Whenever the ball moves, I am trying to pass the value of the new location to a singleton object which contains the instance of my game layer, so that the appropriate method can be called with the new location. Is this the right way to do this? My problem is that my game layer class needs to import the singleton class so that it can put the instance into the singleton, but my singleton class also needs to import the game layer class so it can create an instance field into which the game layer is put. This creates an error, so what is the better way to do this?
    Thanks.

    • #36 by Eric on March 7th, 2010

      In your singleton class header file, instead of importing “GameLayer.h” use the @class GameLayer so you can store a pointer to it, and then in your single class .m file go ahead and import “GameLayer.h”.

      That will fix the errors. In case that isn’t clear, just google how to use the @class statement.

      • #37 by Daniel on March 7th, 2010

        Thanks so much for the quick reply :)
        That did the trick.

(will not be published)

  1. No trackbacks yet.