This tutorial will teach you how to use the Cocos2D iOS framework in order to create simple yet advanced 2D games aimed at all iOS devices. It's designed for both novice and advanced users. Along the way, you'll learn about the Cocos2D core concepts, touch interaction, menus and transitions, actions, particles, and collisions. Read on!
This tutorial is the second entry in the three-part Monster Smashing series. Make sure you've completed the previous section before beginning.
Organization of the Series:
In today's tutorial we'll program the game board for Monster Smashing. This part focuses on several things, including the MonsterRun
class, movements, animations, touches, and so on. We'll explain everything in the tutorial below.
1. Monsters
Monsters are the main objects of the game. Each monster has a few unique attributes like sprites, movement, velocity, and kill action. To represent an object we can use a specific class, so we've created a Monster
class to represent a monster.
Usually, in Objective-C, we create a new NSObject
subclass. In this case, we need to use the CCNode
class, which is a class that includes the Foundation.h
and the cocos2d.h
headers. To create a new class in Xcode, choose File -> New -> New File.... In the left-hand table of the panel that appears, select Cocos2D from the iOS section. Select CCNode class from the upper panel and hit next. We'll name the class "Monster".
Now we need to declare the instance variables. In Monster.h, add five instance variables:
NSString *monsterSprite
NSString *splashSprite
int movement
float minVelocity
float maxVelocity
int killMethod
Now, as with every oriented object language, we need to implement the setters and getters. In objective-C, properties are a convenient alternative to writing out accessors for instance variables. It saves a lot of typing and it makes your class files easier to read. Add the above-mentioned properties on the .h file.
@property(nonatomic,readwrite,retain) NSString *monsterSprite; @property(nonatomic,readwrite,retain) NSString *splashSprite; @property(nonatomic, readwrite) int movement; @property(nonatomic, readwrite) float minVelocity; @property(nonatomic, readwrite) float maxVelocity; @property(nonatomic, readwrite) int killMethod;
2. Initializing the Gameboard
Before continuing, you'll need to initialize several things. we can add a background to our game just like we did in the first section of the tutorial. We can use the same one in the menu scene. If you don't remember how to do that, you can use the following code on the -(id) init
method (MonsterRun.m).
self.isTouchEnabled = YES; // Add background winSize = [CCDirector sharedDirector].winSize; CCSprite *background = [CCSprite spriteWithFile:@"WoodRetroApple_iPad_HomeScreen.jpg"]; background.position = ccp(winSize.width/2, winSize.height/2); [self addChild:background z:-2];
Now we need to initialize some "monsters". Before you start adding monsters, we need to create a NSMutableArray in order to store them. Once again, the images for the monster sprites are available in the resources folder.
We'd also like to thank Rodrigo Bellão for providing us with the images for the monsters.
In MonsterRun.m
add two NSMutableArray objects: _monsters and _monstersOnScreen. Then, in the init
method, we must initialize those arrays in order to use them. The following snippet can help us achieve that.
//initializing monsters _monsters = [[NSMutableArray alloc] init]; _monstersOnScreen = [[NSMutableArray alloc] init]; Monster *m1 = [[Monster alloc] init]; [m1 setTag:1]; [m1 setMonsterSprite:[[NSString alloc] initWithString:@"monsterGreen.png"]]; [m1 setSplashSprite:[[NSString alloc] initWithString:@"splashMonsterGreen.png"]]; [m1 setMinVelocity:2.0]; [m1 setMaxVelocity:8.0]; [m1 setMovement:1]; [m1 setKillMethod:1]; [_monsters addObject:m1]; Monster *m2 = [[Monster alloc] init]; [m2 setTag:2]; [m2 setMonsterSprite:[[NSString alloc] initWithString:@"monsterBlue.png"]]; [m2 setSplashSprite:[[NSString alloc] initWithString:@"splashMonsterBlue.png"]]; [m2 setMinVelocity:2.0]; [m2 setMaxVelocity:8.0]; [m2 setKillMethod:2]; [m2 setMovement:1]; [_monsters addObject:m2]; Monster *m3 = [[Monster alloc] init]; [m3 setTag:3]; [m3 setMonsterSprite:[[NSString alloc] initWithString:@"monsterRed.png"]]; [m3 setSplashSprite:[[NSString alloc] initWithString:@"splashMonsterRed.png"]]; [m3 setMinVelocity:3.0]; [m3 setMaxVelocity:6.0]; [m3 setKillMethod:1]; [m3 setMovement:2]; [_monsters addObject:m3];
These arrays should be used to store all the monsters for a given level.
3. Adding Monsters to the Screen
Now that we have some monsters on memory, the next step is to put them on screen. To do that, we just need a few lines.
First we need to create a scheduler to invoke a method to a specific action which, in this case, is the monster location and movement. Right next to the monster's allocation, we can create the scheduler as the next line presents.
[self schedule:@selector(addMonster:) interval:1.0];
The selector indicates the method that will be called addMonster
. At the moment, we don't have that method, but it will be created soon. The scheduler can also receive a time interval that represents the time when a specific sprite is updated.
The addMonster
method is the one responsible for the monster movement. Since we've added three different monsters, we will also create three different movement patterns. The addMonster
method is depicted below.
- (void) addMonster:(ccTime)dt { //select a random monster from the _monsters Array int selectedMonster = arc4random() % [_monsters count]; //get some monster caracteristics Monster *monster = [_monsters objectAtIndex:selectedMonster]; int m = [monster movement]; //!IMPORTANT -- Every Sprite in Screen must be an new CCSprite! Each Sprite can only be one time on screen CCSprite *spriteMonster = [[CCSprite alloc] initWithFile:[monster monsterSprite]]; spriteMonster.tag = [monster tag]; //BLOCK 1 - Determine where to spawn the monster along the Y axis CGSize winSize = [CCDirector sharedDirector].winSize; int minX = spriteMonster.contentSize.width / 2; int maxX = winSize.width - spriteMonster.contentSize.width/2; int rangeX = maxX - minX; int actualY = (arc4random() % rangeX) + minX; //BLOCK 2 - Determine speed of the monster int minDuration = [monster minVelocity]; int maxDuration = [monster maxVelocity]; int rangeDuration = maxDuration - minDuration; int actualDuration = (arc4random() % rangeDuration) + minDuration; if(m == 1){ //STRAIGHT MOVIMENT //BLOCK 3 - Create the monster slightly off-screen along the right edge, // and along a random position along the Y axis as calculated above spriteMonster.position = ccp( actualY,winSize.height + spriteMonster.contentSize.height/2); [self addChild:spriteMonster]; //BLOCK 4 - Create the actions CCMoveTo * actionMove = [CCMoveTo actionWithDuration:actualDuration position:ccp( actualY,-spriteMonster.contentSize.height/2)]; CCCallBlockN * actionMoveDone = [CCCallBlockN actionWithBlock:^(CCNode *node) { [_monstersOnScreen removeObject:node]; [node removeFromParentAndCleanup:YES]; }]; [spriteMonster runAction:[CCSequence actions:actionMove, actionMoveDone, nil]]; [_monstersOnScreen addObject:spriteMonster]; } else if(m == 2){ //ZIGZAG-SNAKE MOVIMENT /* Create the monster slightly off-screen along the right edge, and along a random position along the Y axis as calculated above */ spriteMonster.position = ccp( actualY,winSize.height + spriteMonster.contentSize.height/2); [self addChild:spriteMonster]; CCCallBlockN * actionMoveDone = [CCCallBlockN actionWithBlock:^(CCNode *node) { [_monstersOnScreen removeObject:node]; [node removeFromParentAndCleanup:YES]; }]; // ZigZag movement Start NSMutableArray *arrayBezier = [[NSMutableArray alloc] init]; ccBezierConfig bezier; id bezierAction1; float splitDuration = actualDuration / 6.0; for(int i = 0; i< 6; i++){ if(i % 2 == 0){ bezier.controlPoint_1 = ccp(actualY+100,winSize.height-(100+(i*200))); bezier.controlPoint_2 = ccp(actualY+100,winSize.height-(100+(i*200))); bezier.endPosition = ccp(actualY,winSize.height-(200+(i*200))); bezierAction1 = [CCBezierTo actionWithDuration:splitDuration bezier:bezier]; } else{ bezier.controlPoint_1 = ccp(actualY-100,winSize.height-(100+(i*200))); bezier.controlPoint_2 = ccp(actualY-100,winSize.height-(100+(i*200))); bezier.endPosition = ccp(actualY,winSize.height-(200+(i*200))); bezierAction1 = [CCBezierTo actionWithDuration:splitDuration bezier:bezier]; } [arrayBezier addObject:bezierAction1]; } [arrayBezier addObject:actionMoveDone]; id seq = [CCSequence actionsWithArray:arrayBezier]; [spriteMonster runAction:seq]; // ZigZag movement End [_monstersOnScreen addObject:spriteMonster]; } }
You may have noticed that there are comments within the code. The comments will guide us (both programmers and readers) to implement and learn each instruction. In this tutorial, the monsters are chosen randomly (however, in part three we'll show how use pList to put the monsters on the screen).
The first line of our code does exactly that. We get the total number of monsters on our array and then we randomly choose which one we will put onscreen next. After the monster is selected, we define some properties such as movement, velocity, range over the XX axis and its tag.
We defined three Blocks that define specific properties.
- Block 1 determines where on screen we will span the monsters. Note that the monster will only spawn in a visible area of screen. For that, we get the range of the XX axis and then randomly place the monster.
- Block 2 determines the speed of the monster. Note that each monster's type will have the minimum and maximum speed defined. The speed of the monster is the duration in seconds that the monster will pass through the screen. This duration will be between the minDuration and the maxDuration defined on the monster object on the
init
method - Block 3 creates the monster slightly off-screen along the right edge and along a random position along the XX axis, calculated above
- Block 4 creates the actions for the straight movement. Two actions are created, the movement itself
CCMoveTo * actionMove
and one that is called when the sprite leaves the screen (CCCallBlockN * actionMoveDone
). To create an action with straight movement, we use theCCMoveTo
object, which receives the duration of the action and the final position. We also use theCCCallBlockN
. This particular object will detect when a monster gets off the screen. Using theCCCallBlockN
is advantageous because Cocos2D already has some mechanisms in place to detect if a particular resource leaves the screen.
We have defined two movements: straight and zig-zag. The straight one can be analyzed in Block 4, mentioned above. The zig-zag uses the bezier path to create the zig-zag effect and will be depicted below.
4. The Zig-Zag (Snake) Movement
To make the zig-zag movement we use the CCBezier
. This class allow us to create an action that will move the target with a cubic bezier curve to a destination point.
The created curves will always move 100 pixels to the left or right, and at the same time the object will move 200 pixels below. The for
loop will create the path of the monster. Since the screen, which is the normal iPad resolution, has 1024 pixels in the vertical position, we need to run the loop six times (200px * 6 = 1200px). The condition inside the for
loop will determine if the iteration is odd or pair. When the number is odd, the monster move to the left. If the number is pair, the monster moves to the right.
Now each condition will calculate the controlPoint1
and controlPoint2
, which are used to control and guide the curve on screen. The endPosition
, as the name suggests, is the final position that each monster will end up in after each curve. We concatenate all these actions into an array and create a sequence from it. Just as we did with the straight movement, we'll run the action and add the monster to the array.
It's now time to build and run the project. Your game should look similar to the image below.
5. Touch Interaction
A game with monsters that cannot be killed is not a game, right?
It's time to add some interaction between the screen and the user. We'll follow the same philosophy we used above, code and explanation.
Cocos2D provides us with several mechanisms for touch interaction. For now, we'll only focus on one touch. For that, we need to add a specific method called ccTouchesBegan
. The method receives an event and will act accordingly to that event. Obviously, the event is a touch event. The snippet is presented below.
-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSMutableArray *monstersToDelete = [[NSMutableArray alloc] init]; UITouch * touch = [[touches allObjects] objectAtIndex:0]; CGPoint touchLocation = [self convertTouchToNodeSpace:touch]; for (CCSprite *monster in _monstersOnScreen) { if (CGRectContainsPoint(monster.boundingBox, touchLocation)) { [monstersToDelete addObject:monster]; //add animation with fade - splash Monster *m = [_monsters objectAtIndex:(monster.tag-1)]; CCSprite *splashPool = [[CCSprite alloc] initWithFile:[m splashSprite]]; if([m killMethod] == 1){ splashPool.position = monster.position; [self addChild:splashPool]; CCFadeOut *fade = [CCFadeOut actionWithDuration:3]; //this will make it fade CCCallFuncN *remove = [CCCallFuncN actionWithTarget:self selector:@selector(removeSprite:)]; CCSequence *sequencia = [CCSequence actions: fade, remove, nil]; [splashPool runAction:sequencia]; //finish splash } if([m killMethod] == 2){ // in Part 3 - Particles section } break; } } for (CCSprite *monster in monstersToDelete) { [monster stopAllActions]; [_monstersOnScreen removeObject:monster]; [self removeChild:monster cleanup:YES];
The first line of this method is the allocation of an auxiliary array monstersToDelete
that defines what monsters will be deleted at each action.
The next step is to know the touch interaction location. With that location, we'll loop all the sprites on the screen and test to see if the touch location is inside an invisible box that contains a monster. We'll use f (CGRectContainsPoint(monster.boundingBox, touchLocation))
. If it is true, we'll add the monsters to the auxiliary array. Plus, we'll add two splash animations when a monster is killed. The first one is a simple splash, while the second is a particle. We'll discuss that in part three. To create the splash, we verify the monster type and then we add that monster to a splash pool. CCFadeOut
will create a fade effect on the splash image, and the CCCallFuncN
will call another method to remove the Monster sprite.
Now we just need one little thing. In the CCCallFuncN *remove
, the call method removeSprite
needs to be created and the objective is to remove that sprite. Add that method using the following code.
-(void) removeSprite:(id)sender { [self removeChild:sender cleanup:YES]; }
Finally, when the loop is over, the touch interaction is tested against all objects in the scene we have in another loop, for (CCSprite *monster in monstersToDelete)
. It will iterate over monstersToDelete
and remove the object.
It's now time to build and run the project. We should see some monsters popping up onscreen. Now we can start killing them! You will notice that the red monsters don't have any splash when they die. This will be covered in the next part. The final image in this part is the following.
6. Conclusion
At this point you should be able to understand and perform the following tasks:
- Add sprites to the screen.
- Define sprite properties.
- Define custom classes.
- Initialize the game board.
- Define custom movements and actions.
- Know how to use touch interaction.
In the next tutorial we'll learn about sound and game mechanics!
Comments