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.

61 comments

  1. 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. 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]];
    }

  3. 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

    • 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. 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

  5. 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?

    • 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.

  6. [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?

    • 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?

    • 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.

    • 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).

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

    • 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.

      • 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?

        • 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.

  8. 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?

    • 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 :( .

      • 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.

        • 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.

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

            Thanks Again!!

  9. 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?

    • 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.

  10. 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.

    • 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!

  11. 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.

    • 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.

  12. Hi Eric,

    Some quick questions for you, does the ‘centerPoint’ variable refer to the centre of the iphone screen or the centre tile of the map itself?

    I’m running my game in Landscape mode, does that also affect things?

    • This works in landscape, that is actually the same as the game that was running this code.

      centerPoint refers to the center of the iPhone screen (240, 180). If you wanted it portrait you’d do (180, 240). Easy!

  13. It is possibile to have a zip of this example? I’m having troubles setting up a simple working project with cocos2d.

    Tkz a lot!

    • Sorry I realized this is old code before cocos2d made a lot of changes (like adding CC prefix). I don’t have a zip for this, but I will eventually put together some tutorials using cocos2d 1.0 that people will be able to download.

  14. Eric – Regardless of the changes to the api, your explanations have been extremely helpful in my effort to get off the ground with cocos2d – thanks. I have a really basic noob-level question… if you were to construct an RPG using the premises outlined in your code, would you create one single game layer, regardless of how many scenes are in the game and just remove/add child objects to the layer when the scene changes? Or would you create a game layer specific to each scene (e.g. GameLayer_Castle, GameLayer_Forest, etc)? I’m assuming it’s the former, but I want to make sure I understand correctly. Thanks again for your helpful examples and explanations.

    • Thanks, I’m glad this stuff helped you out some.

      If I was going to make an RPG, it depends on how the game is structured. If the different locations in the game have different interactions (castle vs forest) that require different behavior code, then split up the Layers. If all that is changed is the visuals, then use the same Layer and load up different images based on the location.

      Sorry for the delay in getting back to you, if you post again I’ll be on it quicker.

  15. Thanks for this interesting information actually i m working on a game and i want to know whether there is a way to give a pre specified path to a moving body in box2d so that the obstacle (ball) should move only in that way any sample code or hints how it can be achieved.

    • There is certainly a way to do this, but I don’t have an answer as there are many different ways to approach this. Just apply forces to approach a pre-set list of points.

  16. Hi:

    I’ve been reading this and I am trying to figure out how to work with it.

    Can somebody help me to understand how does this if work?

    if(point.x < centerPoint.x)
    viewPoint.x = 0;
    if(point.y < centerPoint.y)
    viewPoint.y = 0;

    I don’t get what are they asking :S

    Thanks for any help you can give me!

    • Those two lines should be:

      if(point.x < centerPoint.x)
      viewPoint.x = 0;
      if(point.y < centerPoint.y)
      viewPoint.y = 0;

      They stop the scrolling action when your character reachers the bottom left side of the screen, since you don’t want to show area outside the map.

      • Hi, thanks for your reply. I didn’t notice that the “&lt” was a bad rendered html character. I copy and paste the lines and din’t see that “&lt” is a “less than”.

        Thaks anyway!

  17. Hi,
    Thanks, this works great.

    I now would like to rotate the tilemap around the center of the screen but I can’t get this to work.
    Rotation of the layer itself works fine but how can I adjust the centerpoint for the rotation?

  18. Hi, this one is helpful(at least a start), but can you please tell that how can I track the sprites changed location after zoom out, (I’ not using tile map) and zooming out with the help of camera(by increasing eyeZ).

  19. Hi, first of all thanks for your great tutorial!
    I know my question isn’t quite referred to scrolling with zoom using tilemap but I’m trying to achieve the same thing without a tilemap (I’m not sure if it can be done using a tilemap).

    I’m trying to implement a parallax effect; at first I’ve tried with different CCLayers but no clue how to manage that… at the end I’ve decided to add them as sprites into my layer.
    So my question sounds something like this: Have you (Johnny or somebody else) ever met this situation ? [ Implementing a parallax effect with scroll and zoom ]

    Can point me to the right direction, I’d really appreciate that!

  20. My wife and i were quite peaceful when Chris could conclude

    his basic research while using the precious recommendations he received using your web

    pages. It is now and again perplexing to simply happen to be releasing tips and

    tricks that many men and

    women may have been selling. And we consider
    we’ve got the website owner to appreciate for this. The main explanations

    you have made, the simple blog menu, the friendships your site help engender – it’s most
    overwhelming, and it is aiding our son and our family
    believe that that issue is enjoyable, which is certainly

    exceedingly vital. Many thanks for all!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">