How To Build a Monkey Jump Game Using Cocos2d 2.X, PhysicsEditor & TexturePacker – Part 1

Learn how to make a fun 2D physics game called Monkey Jump in this 3-part tutorial series covering Physics Editor, Texture Packer, and Cocos2D. By .

Leave a rating/review
Save for later
Share
You are currently viewing page 5 of 6 of this article. Click here to view the first page.

Dropping Objects

You have two related goals for this section of the tutorial: make your objects drop from the sky and add your sound effects.

Our base class for all the dropping objects will be called "Object". It will handle the sound and some basic collision detection. You will derive other subclasses later on in the tutorial from the Object class.

First, create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Object, and make it a subclass of GB2Sprite. (And remember to change the extension for Object.m to .mm)

Object is a simple class, derived from GB2Sprite. This means that it comes with physics and graphical capabilities built in.

To make your life easier, I've named the sound files in the same way as the physics sprites and the images from the sprite sheet. This allows you to simply use the object's name to create the right shape and sound when needed. You're welcome!

In order for this to work, you need a property named objName - objName is passed into the initWithObject selector and stored as part of the class.

RandomObject is a factory method that creates a random object and hands over the right object name upon creation.

Paste this code into Object.h:

    
#pragma once

#import "cocos2d.h"
#import "GB2Sprite.h"

@interface Object : GB2Sprite
{
    NSString *objName; // type of the object
}

@property (retain, nonatomic) NSString *objName;

-(id) initWithObject:(NSString*)objName;
+(Object*) randomObject;

@end

Let's now go to Object.mm. Start with some needed imports and with synthesizing the objName property.

#import "Object.h"
#import "GB2Contact.h"
#import "SimpleAudioEngine.h"
#import "GMath.h"

@implementation Object

@synthesize objName;

GMath.h contains some helper functions – for example, gFloatRand, a ranged floating-point random number generation.

Next, add the init selector and instantiate the physics object. You can use the object's name as it is to instantiate the physics shape. For the sprite frame name, you'll need to add the folder's name (which is object) and the .png extension. Store the objName in the property – you'll need it during collision detection to play the sound effect.

-(id) initWithObject:(NSString*)theObjName
{
    self = [super initWithDynamicBody:theObjName
              spriteFrameName:[NSString stringWithFormat:@"objects/%@.png", theObjName]];
    if(self)
    {
        self.objName = theObjName;
    }
    return self;
}

In the dealloc selector, simply release the objName property and call super dealloc:

-(void) dealloc
{
    [objName release];
    [super dealloc];
}

The next thing to add is your static factory method, which will simply create a random object. I decided to use a simple switch-case statement for this. The reason is that you'll need to create special classes for banana and banana bunch later on. These two objects get only one case entry, while the other objects get three each, so that there's a higher probability they appear more often.

Switch-case constructs are quite efficient (usually implemented by the compiler using a jump table). You might save some CPU cycles by using an array with the names instead, but since the routine will be called once in a second, your way is fine.

+(Object*) randomObject
{
    NSString *objName;
    switch(rand() % 18)
    {
        case 0:
            objName = @"banana";
            break;

        case 1:
            objName = @"bananabunch";
            break;

        case 2: case 3: case 4: case 5:
            objName = @"backpack";
            break;

        case 6: case 7: case 8:
            objName = @"canteen";
            break;

        case 9: case 10: case 11:
            objName = @"hat";
            break;

        case 12: case 13: case 14:
            objName = @"statue";
            break;

        default:
            objName = @"pineapple";
            break;
    }
    return [[[self alloc] initWithObject:objName] autorelease];
}

Finally, add the closing end to the file:

@end

Now switch to GameLayer.h and add a forward declaration for the object class, directly after the #import statement:

#import "cocos2d.h"
@class Object;

Add these new members to the GameLayer class:

ccTime nextDrop;    // Will keep the time until the next drop.
ccTime dropDelay;     // The delay between two drops.
Object *nextObject;   // Contains a reference to the next item to drop.

Switch to GameLayer.mm and add an import of Object.h to the imports at the start of the file. Also import GMath.h:

#import "Object.h"
#import "GMath.h"

Initialize the new variables at the end of the init selector, and schedule an update selector with every frame update:

nextDrop = 3.0f;  // drop first object after 3s
dropDelay = 2.0f; // drop next object after 1s

[self scheduleUpdate];

The last line will call a selector called "update" for every frame. The parameter to this selector is the time elapsed since the selector was last called. Add the update method right after init:

-(void) update: (ccTime) dt
{
    // 1 - drop next item
    nextDrop -= dt;
    if(nextDrop <= 0)
    {
        // 2 - do you have the next object?
        if(nextObject)
        {
            // 3 - set the object as active, making it drop
            [nextObject setActive:YES];

            // 4 - set next drop time
            nextDrop = dropDelay;
            // reduce delay to the drop after this
            // this will increase game difficulty
            dropDelay *= 0.98f;            

        }

        // 5 - create new random object
        nextObject = [Object randomObject];
        // but keep it disabled
        [nextObject setActive:NO];

        // 6 - set random position
        float xPos = gFloatRand(40,440);
        float yPos = 400;
        [nextObject setPhysicsPosition:b2Vec2FromCC(xPos, yPos)];

        // 7 - add it to your object layer
        [objectLayer addChild:[nextObject ccNode]];
    }
}

Let's go through the above code section by section.

  1. This section simply reduces the time interval since update was last called from nextDrop. If nextDrop falls below 0, it's time to create a new item to drop.
  2. If the nextDrop timer runs out, this section checks if there is already an object stored in nextObject.
  3. If so, it's set to active in here. Setting the object to active gives the physics engine control over the object.
  4. This section sets the time until the next drop to the current drop delay, and reduces the drop delay by 2%, making the game a bit more difficult with each dropped item.
  5. This section creates a new object to drop using your factory method in Object - randomObject - and sets the object to inactive, which keeps the object from dropping and participating in the physics simulation.
  6. This section gives the object a random position. The playfield is 480pt wide. The code ensures that the object's position is somewhere between 40 and 440 points. The section also sets the y-coordinate to 400 for the starting position so that the object will start offscreen from the top of the screen. The b2Vec2FromCC method is used to create a box2db2Vec2 from the point coordinates. B2Vec2FromCC transforms Cocos2d's points to Box2d's meters-based values.
  7. Finally, this section adds the object to the object layer.

Compile and run! You should see something similar to the following but of course, with different items. The items look a bit blurry since debug drawing is still enabled:

Disable the debug draw layer as follows by commenting out the relevant line in GameLayer.mm:

    //        [self addChild:[[GB2DebugDrawLayer alloc] init] z:30];

Now your game should look much nicer:

Notice how the items can tumble out-of-screen to the left and right? The goal of the game is to let the items pile up, so you need to add a wall on each side of the screen.

To do this, simply create two new GB2Node objects. They will be out of the screen to the left and right.

Since GB2Nodes add themselves to the current physics simulation, you don't need to add them manually. They are not represented graphically, so creating them will suffice.

Add these lines to the init in GameLayer.mm, right after the floor layer:

GB2Node *leftWall = [[GB2Node alloc] initWithStaticBody:nil node:nil];
[leftWall addEdgeFrom:b2Vec2FromCC(0, 0) to:b2Vec2FromCC(0, 10000)];

GB2Node *rightWall = [[GB2Node alloc] initWithStaticBody:nil node:nil];
[rightWall addEdgeFrom:b2Vec2FromCC(480, 0) to:b2Vec2FromCC(480, 10000)];

Build and run. See how the objects are now kept inside the screen by your walls?

This looks nice, but there is still something missing. I think the objects should make some noise when colliding with each other. Don't you agree?

I don't want the objects to make sounds all the time, just when they hit each other at a decent speed. So you'll check the object's velocity, and play a sound only when it collides at a fast enough speed.

Add this code to Object.mm:

-(void) beginContactWithObject:(GB2Contact*)contact
{
    b2Vec2 velocity = [self linearVelocity];

    // play the sound only when the impact is high
    if(velocity.LengthSquared() > 3.0)
    {
        // play the item hit sound
        // pan it depending on the position of the collision
        // add some randomness to the pitch
        [[SimpleAudioEngine sharedEngine] playEffect:[NSString stringWithFormat:@"%@.caf", objName]
                    pitch:gFloatRand(0.8,1.2)
                    pan:(self.ccNode.position.x-240.0f) / 240.0f
                    gain:1.0 ];    

    }
}

The above method must be named beginContactWithObject so that it will be automatically called by GBox2D each time two objects collide.

The linearVelocity method gives you the velocity of the object. Calling Length or LengthSquared on the object delivers the velocity's value. I prefer using LengthSquared when comparing with a constant value, since it doesn't require calculating the square root of the value.

You'll play the sound with a call to SimpleAudioEngine's playEffect method. The first parameter is the name of the audio file.

Remember that to make your life easier I gave the sound effects the same name as the objects and sprites. So you can use the objName you stored earlier to get the right sound file. Use NSString to append .caf to the name.

Add some variation to the pitch by using gFloatRand with 0.8 and 1.2. It would be boring if every object made the same sound all the time.

The last trick to apply is to pan the sound's source to the position of the object. Pan allows values between -1.0 and 1.0. The object's x position (in points) will be somewhere between 0 and 480, so subtracting 240 and dividing by 240 will deliver that range.

If you want objects to make a sound when they hit the floor without rewriting a lot of the code, add the following method which forwards the object-floor collision to the object-object collision to Object.mm:

-(void) beginContactWithFloor:(GB2Contact*)contact
{
    [self beginContactWithObject:contact];
}

Compile, run, and see how the objects drop and make a sound upon collision.

Ah, but there is one more thing I don't like about your game right now. The first item drops and then pauses in mid-air while the sound engine is initialized.

This won't be a problem once you add the theme music, since the music will initialize the sound engine right away. But if you want to fix this now, first add an import statement to the top of GameLayer.mm:

#import "SimpleAudioEngine.h"

Then, add a call to SimpleAudioEngine's shared object inside GameLayer's init selector:

[SimpleAudioEngine sharedEngine];

The above implementation plays the same basic sound (with pitch variations) for all falling object collisions. If you're ambitious, you could play different sounds depending upon the types of objects colliding. That is, play one sound when a canteen hits a canteen, and another when a banana hits a canteen...

Another way to improve this code would be to vary the gain of the effect with the speed of the collision.