18 min read

In this article by Siddharth Shekar, author of the book Cocos2d Cross-Platform Game Development Cookbook, Second Edition, we will cover the following recipes:

  • Adding level selection scenes
  • Scrolling level selection scenes

(For more resources related to this topic, see here.)

Scenes are the building blocks of any game. Generally, in any game, you have the main menu scene in which you are allowed to navigate to different scenes, such as GameScene, OptionsScene, and CreditsScene. In each of these scenes, you have menus.

Similarly in MainScene, there is a play button that is part of a menu that, when pressed, takes the player to GameScene, where the gameplay code runs.

Adding level selection scenes

In this section, we will take a look at how to add a level selection scene in which you will have buttons for each level you want to play, and if you select it, this particular level will load up.

Getting ready

To create a level selection screen, you will need a custom sprite that will show a background image of the button and a text showing the level number. We will create these buttons first.

Once the button sprites are created, we will create a new scene that we will populate with the background image, name of the scene, array of buttons, and a logic to change the scene to the particular level.

How to do it…

We will create a new Cocoa Touch class with CCSprite as the parent class and call it LevelSelectionBtn.

Then, we will open up the LevelSelectionBtn.h file and add the following lines of code in it:

#import "CCSprite.h"
@interface LevelSelectionBtn : CCSprite
-(id)initWithFilename:(NSString *) filename
  StartlevelNumber:(int)lvlNum;
@end

We will create a custom init function; in this, we will pass the name of the file of the image, which will be the base of the button and integer that will be used to display the text at the top of the base button image.

This is all that is required for the header class. In the LevelSelectionBtn.m file, we will add the following lines of code:

#import "LevelSelectionBtn.h"
@implementation LevelSelectionBtn
-(id)initWithFilename:(NSString *) filename StartlevelNumber:
(int)lvlNum;
{
  if (self = [super initWithImageNamed:filename]) {
    CCLOG(@"Filename: %@ and levelNUmber: %d", filename, lvlNum);
    CCLabelTTF *textLabel = [CCLabelTTF labelWithString:[NSString
      stringWithFormat:@"%d",lvlNum ] fontName:@"AmericanTypewriter-Bold" fontSize: 12.0f];
    textLabel.position = ccp(self.contentSize.width / 2, self.contentSize.height / 2);
    textLabel.color = [CCColor colorWithRed:0.1f green:0.45f blue:0.73f];
    [self addChild:textLabel];
  }
  return self;
}
@end

In our custom init function, we will first log out if we are sending the correct data in. Then, we will create a text label and pass it in as a string by converting the integer.

The label is then placed at the center of the current sprite base image by dividing the content size of the image by half to get the center.

As the background of the base image and the text both are white, the color of the text is changed to match the color blue so that the text is actually visible.

Finally, we will add the text to the current class.

This is all for the LevelSelectionBtn class. Next, we will create LevelSelectionScene, in which we will add the sprite buttons and the logic that the button is pressed for.

So, we will now create a new class, LevelSelectionScene, and in the header file, we will add the following lines:

#import "CCScene.h"
@interface LevelSelectionScene : CCScene{
  NSMutableArray *buttonSpritesArray;
}
+(CCScene*)scene;
@end

Note that apart from the usual code, we also created NSMutuableArray called buttonsSpritesArray, which will be used in the code.

Next, in the LevelSelectionScene.m file, we will add the following:

#import "LevelSelectionScene.h"
#import "LevelSelectionBtn.h"
#import "GameplayScene.h"

@implementation LevelSelectionScene
+(CCScene*)scene{
    return[[self alloc]init];
}
-(id)init{
  if(self = [super init]){
    CGSize  winSize = [[CCDirector sharedDirector]viewSize];
    //Add Background Image
    CCSprite* backgroundImage = [CCSprite spriteWithImageNamed:@ "Bg.png"];
    backgroundImage.position = CGPointMake(winSize.width/2, winSize.height/2);
    [self addChild:backgroundImage];

    //add text heading for file
    CCLabelTTF *mainmenuLabel = [CCLabelTTF labelWithString:@
    "LevelSelectionScene" fontName:@"AmericanTypewriter-Bold" fontSize:36.0f];
    mainmenuLabel.position = CGPointMake(winSize.width/2, winSize.height * 0.8);
    [self addChild:mainmenuLabel];

    //initialize array
    buttonSpritesArray = [NSMutableArray array];
    int widthCount = 5;
    int heightCount = 5;
    float spacing = 35.0f;
    float halfWidth = winSize.width/2 - (widthCount-1) * spacing * 0.5f;
    float halfHeight = winSize.height/2 + (heightCount-1) * spacing * 0.5f;
    int levelNum = 1;
    for(int i = 0; i < heightCount; ++i){
      float y = halfHeight - i * spacing;
      for(int j = 0; j < widthCount; ++j){
        float x = halfWidth + j * spacing;
        LevelSelectionBtn* lvlBtn = [[LevelSelectionBtnalloc]
          initWithFilename:@"btnBG.png"
          StartlevelNumber:levelNum];
        lvlBtn.position = CGPointMake(x,y);
        lvlBtn.name = [NSString stringWithFormat:@"%d",levelNum];
        [self addChild:lvlBtn];
        [buttonSpritesArray addObject: lvlBtn];
        levelNum++;
      }
    }
  }
   return self;
}

Here, we will add the background image and heading text for the scene and initialize NSMutabaleArray.

We will then create six new variables, as follows:

  • WidthCount: This is the number of columns we want to have
  • heightCount: This is the number of rows we want
  • spacing: This is the distance between each of the sprite buttons so that they don’t overlap
  • halfWidth: This is the distance in the x axis from the center of the screen to upper-left position of the first sprite button that will be placed
  • halfHeight: This is the distance in the y direction from the center to the upper-left position of the first sprite button that will be placed
  • lvlNum: This is the counter with an initial value of 1. This is incremented each time a button is created to show the text in the button sprite.

In the double loop, we will get the x and y coordinates of each of the button sprites. First, to get the y position from the half height, we will subtract the spacing multiplied by the j counter. As the value of j is initially 0, the y value remains the same as halfWidth for the topmost row.

Then, for the x value of the position, we will add half the width of the spacing multiplied by the i counter. Each time, the x position is incremented by the spacing.

After getting the x and y position, we will create a new LevelSelectionBtn sprite and pass in the btnBG.png image and also pass in the value of lvlNum to create the button sprite.

We will set the position to the value of x and y that we calculated earlier.

To refer to the button by number, we will assign the name of the sprite, which is the same as the number of the level. So, we will convert lvlNum to a string and pass in the value.

Then, the button will be added to the scene, and it will also be added to the array we created globally as we will need to cycle through the images later.

Finally, we will increment the value of lvlNum.

However, we have still not added any interactivity to the sprite buttons so that when it is pressed, it will load the required level.

For added touch interactivity, we will use the touchBegan function built right into Cocos2d. We will create more complex interfaces, but for now, we will use the basic touchBegan function.

In the same file, we will add the following code right between the init function and @end:

-(void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event{
  CGPoint location = [touch locationInNode:self];
  for (CCSprite *sprite in buttonSpritesArray)
  {
    if (CGRectContainsPoint(sprite.boundingBox, location)){
      CCLOG(@" you have pressed: %@", sprite.name);
      CCTransition *transition = [CCTransition transitionCrossFadeWithDuration:0.20];
      [[CCDirector sharedDirector]replaceScene:[[GameplayScene 
      alloc]initWithLevel:sprite.name] withTransition:transition];
      self.userInteractionEnabled = false;
    }
  }
}

The touchBegan function will be called each time we touch the screen.

So, once we touch the screen, it gets the location of where you touched and stores it as a variable called location.

Then, using the for in loop, we will loop through all the button sprites we added in the array.

Using the RectContainsPoint function, we will check whether the location that we pressed is inside the rect of any of the sprites in the loop.

We will then log out so that we will get an indication in the console as to which button number we have clicked on so that we can be sure that the right level is loaded.

A crossfade transition is created, and the current scene is swapped with GameplayScene with the name of the current sprite clicked on.

Finally, we have to set the userInteractionEnabled Boolean false so that the current class stops listening to the touch.

Also, at the top of the class in the init function, we enabled this Boolean, so we will add the following line of code as highlighted in the init function:

    if(self = [super init]){
      self.userInteractionEnabled = TRUE;
      CGSize  winSize = [[CCDirector sharedDirector]viewSize];

How it works…

So, we are done with the LevelSelectionScene class, but we still need to add a button in MainScene to open LevelSelectionScene.

In MainScene, we will add the following lines in the init function, in which we will add menubtn and a function to be called once the button is clicked on as highlighted here:

        CCButton *playBtn = [CCButton buttonWithTitle:nil
          spriteFrame:[CCSpriteFrame frameWithImageNamed:@"playBtn_normal.png"]
          highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:@ "playBtn_pressed.png"]
          disabledSpriteFrame:nil];
        [playBtn setTarget:self selector:@selector(playBtnPressed:)];
         CCButton *menuBtn = [CCButton buttonWithTitle:nil
          spriteFrame:[CCSpriteFrame frameWithImageNamed:@"menuBtn.png"]
          highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:@"menuBtn.png"]
          disabledSpriteFrame:nil];
         [menuBtn setTarget:self selector:@selector(menuBtnPressed:)];
        CCLayoutBox * btnMenu;
        btnMenu = [[CCLayoutBox alloc] init];
        btnMenu.anchorPoint = ccp(0.5f, 0.5f);
        btnMenu.position = CGPointMake(winSize.width/2, winSize.height * 0.5);
         btnMenu.direction = CCLayoutBoxDirectionVertical;
        btnMenu.spacing = 10.0f;
         [btnMenu addChild:menuBtn];
         [btnMenu addChild:playBtn];
         [self addChild:btnMenu];

Don’t forget to include the menuBtn.png file included in the resources folder of the project, otherwise you will get a build error.

Next, also add in the menuBtnPressed function, which will be called once menuBtn is pressed and released, as follows:

-(void)menuBtnPressed:(id)sender{
  CCLOG(@"menu button pressed");
  CCTransition *transition = [CCTransition transitionCrossFadeWith Duration:0.20];
  [[CCDirector sharedDirector]replaceScene:[[LevelSelectionScene alloc]init] withTransition:transition];
}

Now, the MainScene should similar to the following:

Click on the menu button below the play button, and you will be able to see LevelSelectionScreen in all its glory.

Now, click on any of the buttons to open up the gameplay scene displaying the number that you clicked on.

In this case, I clicked on button number 18, which is why it shows 18 in the gameplay scene when it loads.

Scrolling level selection scenes

If your game has say 20 levels, it is okay to have one single level selection scene to display all the level buttons; but what if you have more? In this section, we will modify the previous section’s code, create a node, and customize the class to create a scrollable level selection scene.

Getting ready

We will create a new class called LevelSelectionLayer, inherit from CCNode, and move all the content we added in LevelSelectionScene to it. This is done so that we can have a separate class and instantiate it as many times as we want in the game.

How to do it…

In the LevelSelectionLayer.m file, we will change the code to the following:

#import "CCNode.h"
@interface LevelSelectionLayer : CCNode {
  NSMutableArray *buttonSpritesArray;
}
-(id)initLayerWith:(NSString *)filename
  StartlevelNumber:(int)lvlNum
  widthCount:(int)widthCount
  heightCount:(int)heightCount
  spacing:(float)spacing;
@end

We changed the init function so that instead of hardcoding the values, we can create a more flexible level selection layer.

In the LevelSelectionLayer.m file, we will add the following:

#import "LevelSelectionLayer.h"
#import "LevelSelectionBtn.h"
#import "GameplayScene.h"

@implementation LevelSelectionLayer
- (void)onEnter{
  [super onEnter];
  self.userInteractionEnabled = YES;
}

- (void)onExit{
  [super onExit];
  self.userInteractionEnabled = NO;
}

-(id)initLayerWith:(NSString *)filename StartlevelNumber:(int)lvlNum 
widthCount:(int)widthCount heightCount:(int)heightCount spacing:
(float)spacing{
  if(self = [super init]){
    CGSize  winSize = [[CCDirector sharedDirector]viewSize];
    self.contentSize = winSize;
    buttonSpritesArray = [NSMutableArray array];
    float halfWidth = self.contentSize.width/2 - (widthCount-1) * spacing * 0.5f; 
    float halfHeight = self.contentSize.height/2 + (heightCount-1) * spacing * 0.5f;
    int levelNum = lvlNum;
    for(int i = 0; i < heightCount; ++i){
      float y = halfHeight - i * spacing;
      for(int j = 0; j < widthCount; ++j){
        float x = halfWidth + j * spacing;
        LevelSelectionBtn* lvlBtn = [[LevelSelectionBtn alloc]
        initWithFilename:filename StartlevelNumber:levelNum];
        lvlBtn.position = CGPointMake(x,y);
        lvlBtn.name = [NSString stringWithFormat:@"%d",levelNum];
        [self addChild:lvlBtn];
        [buttonSpritesArray addObject: lvlBtn];
        levelNum++;
      }
    }
  }
   return self;
}

-(void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event{
  CGPoint location = [touch locationInNode:self];
  CCLOG(@"location: %f, %f", location.x, location.y);
  CCLOG(@"touched");

  for (CCSprite *sprite in buttonSpritesArray)
  {
    if (CGRectContainsPoint(sprite.boundingBox, location)){
      CCLOG(@" you have pressed: %@", sprite.name);
      CCTransition *transition = [CCTransition transitionCross FadeWithDuration:0.20];
      [[CCDirector sharedDirector]replaceScene:[[GameplayScene 
      alloc]initWithLevel:sprite.name] withTransition:transition];
    }
  }
}
@end

The major changes are highlighted here. The first is that we added and removed the touch functionality using the onEnter and onExit functions. The other major change is that we set the contentsize value of the node to winSize. Also, while specifying the upper-left coordinate of the button, we did not use winsize for the center but the contentsize of the node.

Let’s move to LevelSelectionScene now; we will execute the following code:

#import "CCScene.h"

@interface LevelSelectionScene : CCScene{
  int layerCount;
  CCNode *layerNode;
}

+(CCScene*)scene;
@end

In the header file, we will change it to add two global variables in it:

  • The layerCount variable keeps the total layers and nodes you add
  • The layerNode variable is an empty node added for convenience so that we can add all the layer nodes to it so that we can move it back and forth instead of moving each layer node individually

Next, in the LevelSelectionScene.m file, we will add the following:

#import "LevelSelectionScene.h"
#import "LevelSelectionBtn.h"
#import "GameplayScene.h"
#import "LevelSelectionLayer.h"

@implementation LevelSelectionScene

+(CCScene*)scene{
  return[[self alloc]init];
}

-(id)init{
  if(self = [super init]){
    CGSize  winSize = [[CCDirector sharedDirector]viewSize];
    layerCount = 1;
    //Basic CCSprite - Background Image
    CCSprite* backgroundImage = [CCSprite spriteWithImageNamed:@"Bg.png"];
    backgroundImage.position = CGPointMake(winSize.width/2, winSize.height/2);
    [self addChild:backgroundImage];
    CCLabelTTF *mainmenuLabel = [CCLabelTTF labelWithString:
    @"LevelSelectionScene" fontName:@"AmericanTypewriter-Bold" fontSize:36.0f];
    mainmenuLabel.position = CGPointMake(winSize.width/2, winSize.height * 0.8);
    [self addChild:mainmenuLabel];

    //empty node
    layerNode = [[CCNode alloc]init];
    [self addChild:layerNode];
    int widthCount = 5;
    int heightCount = 5;
    float spacing = 35;
    for(int i=0; i<3; i++){
      LevelSelectionLayer* lsLayer = [[LevelSelectionLayer alloc]initLayerWith:@"btnBG.png"
        StartlevelNumber:widthCount * heightCount * i + 1
        widthCount:widthCount
        heightCount:heightCount
        spacing:spacing];

      lsLayer.position = ccp(winSize.width * i, 0);
      [layerNode addChild:lsLayer];
    }

    CCButton *leftBtn = [CCButton buttonWithTitle:nil
      spriteFrame:[CCSpriteFrame frameWithImageNamed:@"left.png"]
      highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:@"left.png"]
      disabledSpriteFrame:nil];
    [leftBtn setTarget:self selector:@selector(leftBtnPressed:)];
    CCButton *rightBtn = [CCButton buttonWithTitle:nil
      spriteFrame:[CCSpriteFrame frameWithImageNamed:@"right.png"]
      highlightedSpriteFrame:[CCSpriteFrame frameWithImageNamed:@"right.png"]
      disabledSpriteFrame:nil];
    [rightBtn setTarget:self selector:@selector(rightBtnPressed:)];
    CCLayoutBox * btnMenu;
    btnMenu = [[CCLayoutBox alloc] init];
    btnMenu.anchorPoint = ccp(0.5f, 0.5f);
    btnMenu.position = CGPointMake(winSize.width * 0.5, winSize.height * 0.2);
    btnMenu.direction = CCLayoutBoxDirectionHorizontal;
    btnMenu.spacing = 300.0f;
    [btnMenu addChild:leftBtn];
    [btnMenu addChild:rightBtn];
    [self addChild:btnMenu z:4];
  }
  return self;
}

-(void)rightBtnPressed:(id)sender{
  CCLOG(@"right button pressed");
  CGSize  winSize = [[CCDirector sharedDirector]viewSize];
  if(layerCount >=0){
    CCAction* moveBy = [CCActionMoveBy actionWithDuration:0.20
      position:ccp(-winSize.width, 0)];
    [layerNode runAction:moveBy];
    layerCount--;
  }
}

-(void)leftBtnPressed:(id)sender{
  CCLOG(@"left button pressed");
  CGSize  winSize = [[CCDirector sharedDirector]viewSize];
  if(layerCount <=0){
    CCAction* moveBy = [CCActionMoveBy actionWithDuration:0.20
      position:ccp(winSize.width, 0)];
    [layerNode runAction:moveBy];
    layerCount++;
  }
}
@end

How it works…

The important piece of the code is highlighted. Apart from adding the usual background and text, we will initialize layerCount to 1 and initialize the empty layerNode variable.

Next, we will create a for loop, in which we will add the three level selection layers by passing the starting value of each selection layer in the btnBg image, the width count, height count, and spacing between each of the buttons.

Also, note how the layers are positioned at a width’s distance from each other. The first one is visible to the player. The consecutive layers are added off screen similarly to how we placed the second image offscreen while creating the parallax effect.

Then, each level selection layer is added to layerNode as a child.

We will also create the left-hand side and right-hand side buttons so that we can move layerNode to the left and right once clicked on. We will create two functions called leftBtnPressed and rightBtnPressed in which we will add functionality when the left-hand side or right-hand side button gets pressed.

First, let’s look at the rightBtnPressed function. Once the button is pressed, we will log out this button. Next, we will get the size of the window. We will then check whether the value of layerCount is greater than zero, which is true as we set the value as 1. We will create a moveBy action, in which we give the window width for the movement in the x direction and 0 for the movement in the y direction as we want the movement to be only in the x direction and not y. Lastly, we will pass in a value of 0.20f.

The action is then run on layerNode and the layerCount value is decremented.

In the leftBtnPressed function, the opposite is done to move the layer in the opposite direction. Run the game to see the change in LevelSelectionScene.

As you can’t go left, pressing the left button won’t do anything. However, if you press the right button, you will see that the layer scrolls to show the next set of buttons.

Summary

In this article, we learned about adding level selection scenes and scrolling level selection scenes in Cocos2d.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here