In this article by Siddharth Shekar, author of the book Cocos2d Cross-Platform Game Development Cookbook, Second Edition, we will cover the following recipes:
(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.
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.
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.
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:
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];
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.
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.
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.
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:
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
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.
In this article, we learned about adding level selection scenes and scrolling level selection scenes in Cocos2d.
Further resources on this subject: