How To Make a Catapult Shooting Game with Cocos2D and Box2D Part 1

This is a blog post by iOS Tutorial Team member Gustavo Ambrozio, a software engineer with over 20 years experience, including over three years of iOS experience. He is the founder of CodeCrop Software. You can also find him on Google+. In this tutorial series we’ll build a cool catapult type game from scratch using […] By Gustavo Ambrozio.

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

Pulling The Catapult’s Leg (or Arm)

Ok, now it’s time to move this arm. To accomplish this we’ll use a mouse joint. If you went through Ray’s breakout game tutorial you already know what a mouse joint does.

But if you didn’t go through this, here it is, straight from Ray:

“In Box2D, a mouse joint is used to make a body move toward a specified point.”

That’s exactly what we want to do. So, first let’s declare the mouse joint’s variable on the HelloWorldLayer.h file:

b2MouseJoint *mouseJoint;

Then let’s add the touchesBegan method to our implementation file:

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (mouseJoint != nil) return;
   
    UITouch *myTouch = [touches anyObject];
    CGPoint location = [myTouch locationInView:[myTouch view]];
    location = [[CCDirector sharedDirector] convertToGL:location];
    b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO);
   
    if (locationWorld.x < armBody->GetWorldCenter().x + 50.0/PTM_RATIO)
    {
        b2MouseJointDef md;
        md.bodyA = groundBody;
        md.bodyB = armBody;
        md.target = locationWorld;
        md.maxForce = 2000;
       
        mouseJoint = (b2MouseJoint *)world->CreateJoint(&md);
    }
}

Again, quoting from Ray:

"When you set up a mouse joint, you have to give it two bodies. The first isn’t used, but 
the convention is to use the ground body. The second is the body you want to move”.

The target is where we want the joint to pull our arm’s body. We have to convert the touch first to the Cocos2d coordinates and then to the Box2d world coordinates. We only create the joint if the touch is to the left of the arm’s body. The 50 pixels offset is because we’ll allow the touch to be a little to the right of the arm.

The maxForce parameter will determine the max force applied to the catapult’s arm to make it follow the target point. In our case we have to make it strong enough to counteract the torque applied by the motor of the revolute joint.

If you make this value too small you won’t be able to pull the arm back because it’s applying a large torque to it. You can then decrease the maxMotorTorque specified in our revolute joint or increase the maxForce of the mouse joint.

I suggest you play with the maxForce of the mouse joint and the maxMotorTorque of the revolute joint to check what values work. Decrease the maxForce to 500 and try out and you’ll see you can’t pull the arm. Then decrease the maxMotorTorque to 1000 and you’ll see that you can do it again. But let’s finish implementing this first…

You’ll notice that the groundBody variable is not declared yet. We created the body on the init method but we didn’t keep a reference to it. Let’s fix this real quick. Add this to the .h file:

b2Body *groundBody;

Then go back to the init method and change this line:

 b2Body* groundBody = world->CreateBody(&groundBodyDef);

to this:

groundBody = world->CreateBody(&groundBodyDef);

We now have to implement the touchesMoved so the mouse joint follows your touch:

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (mouseJoint == nil) return;
   
    UITouch *myTouch = [touches anyObject];
    CGPoint location = [myTouch locationInView:[myTouch view]];
    location = [[CCDirector sharedDirector] convertToGL:location];
    b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO);
   
    mouseJoint->SetTarget(locationWorld);
}

This method is simple enough. It just convert the point to world coordinates and then changes the target point of the mouse joint to this point.

To finish it off we just have to release the arm by destroying the mouse joint. Let’s do this by implementing the touchesEnded method:

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (mouseJoint != nil)
    {
        world->DestroyJoint(mouseJoint);
        mouseJoint = nil;
    }
}

Simple enough. Just destroy the joint and clear the variable. Try it out now. Run the project and pull the arm back with your finger. When you let go you’ll see the arm go back very fast to it’s resting position.

Pulling the Catapult Arm

This is actually too fast. What controls this speed, as you remember, is the motorSpeed of the revolute joint and the maxMotorTorque applied. Let’s try to decrease the value of the motorSpeed and see what happens.

Go to the init method and try a few smaller values for yourself to get a sense of it. A value that worked well for me was -10. Change it to this value and you’ll see the speed is something that seems more natural for a catapult.

armJointDef.motorSpeed  = -10; //-1260;

The repository tag for this point in the tutorial is MouseJoint.

Ready, Aim, Fire!

You know what time it is – heavy ammunition time, heh heh! Or acorns, in this case.

We’ll create every bullet body on the beginning of the game and we’ll use them one by one. So we need a place to store them all. Go to the .h file and add these variables to our class:

NSMutableArray *bullets;
int currentBullet;

Go back to the implementation file and, before we forget, add this to the dealloc method:

[bullets release];

Add a method to create all the bullets above the init method:

- (void)createBullets:(int)count
{
    currentBullet = 0;
    CGFloat pos = 62.0f;
    
    if (count > 0)
    {
        // delta is the spacing between corns
        // 62 is the position o the screen where we want the corns to start appearing
        // 165 is the position on the screen where we want the corns to stop appearing
        // 30 is the size of the corn
        CGFloat delta = (count > 1)?((165.0f - 62.0f - 30.0f) / (count - 1)):0.0f;
        
        bullets = [[NSMutableArray alloc] initWithCapacity:count];
        for (int i=0; i<count; i++, pos+=delta)
        {
            // Create the bullet
            //
            CCSprite *sprite = [CCSprite spriteWithFile:@"acorn.png"];
            [self addChild:sprite z:1];
            
            b2BodyDef bulletBodyDef;
            bulletBodyDef.type = b2_dynamicBody;
            bulletBodyDef.bullet = true;
            bulletBodyDef.position.Set(pos/PTM_RATIO,(FLOOR_HEIGHT+15.0f)/PTM_RATIO);
            bulletBodyDef.userData = sprite;
            b2Body *bullet = world->CreateBody(&bulletBodyDef);
            bullet->SetActive(false);
            
            b2CircleShape circle;
            circle.m_radius = 15.0/PTM_RATIO;
            
            b2FixtureDef ballShapeDef;
            ballShapeDef.shape = &circle;
            ballShapeDef.density = 0.8f;
            ballShapeDef.restitution = 0.2f;
            ballShapeDef.friction = 0.99f;
            bullet->CreateFixture(&ballShapeDef);
            
            [bullets addObject:[NSValue valueWithPointer:bullet]];
        }
    }
}

Most of this method should be familiar to you by now. Our method will create a variable number of bullets, evenly spaced between the first squirrel and the catapult’s body.

One detail you might not have seen before are the “bullet” parameter of the bulletBodyDef. This tells box2d that this will be a fast moving body so box2d will be extra careful with it during the simulation.

The box2d manual explains it well why we need to mark these bodies as bullets:

“Game simulation usually generates a sequence of images that are played at some frame
rate. This is called discrete simulation. In discrete simulation, rigid bodies can move 
by a large amount in one time step. If a physics engine doesn't account for the large
motion, you may see some objects incorrectly pass through each other. This effect is 
called tunneling."

By default, Box2D uses continuous collision detection (CCD) to prevent dynamic bodies from tunneling through static bodies. This is done by sweeping shapes from their old position to their new positions. The engine looks for new collisions during the sweep and computes the time of impact (TOI) for these collisions. Bodies are moved to their first TOI and then halted for the remainder of the time step.

Normally CCD is not used between dynamic bodies. This is done to keep performance reasonable. In some game scenarios you need dynamic bodies to use CCD. For example, you may want to shoot a high speed bullet at a stack of dynamic bricks. Without CCD, the bullet might tunnel through the bricks.

We’ll now create a method to attach the bullet to the catapult. We’ll need two more class variables for this so let’s add them to the .h file:

b2Body *bulletBody;
b2WeldJoint *bulletJoint;

The bulletBody will keep track of the currently attached bullet body so we can track it’s movement later. The bulletJoint will keep a reference to the joint we’ll create between the bullet and the catapult’s arm.

Now go back to the implementation file and add the following right after createBullets:

- (BOOL)attachBullet
{
    if (currentBullet < [bullets count])
    {
        bulletBody = (b2Body*)[[bullets objectAtIndex:currentBullet++] pointerValue];
        bulletBody->SetTransform(b2Vec2(230.0f/PTM_RATIO, (155.0f+FLOOR_HEIGHT)/PTM_RATIO), 0.0f);
        bulletBody->SetActive(true);
        
        b2WeldJointDef weldJointDef;
        weldJointDef.Initialize(bulletBody, armBody, b2Vec2(230.0f/PTM_RATIO,(155.0f+FLOOR_HEIGHT)/PTM_RATIO));
        weldJointDef.collideConnected = false;
        
        bulletJoint = (b2WeldJoint*)world->CreateJoint(&weldJointDef);
        return YES;
    }
    
    return NO;
}

We first get the pointer to the current bullet (we’ll have a way to cycle through them later). The SetTransform method changes the position of the center of the body. The position in the code is the position of the tip of the catapult. We then set the bullet body to active so Box2d starts simulating it’s physics.

We then create a weld joint. A weld joint attaches two bodies on the position we specify in the Initialize method and don’t allow any movement between them from that point forward.

We set collideConnected to false because we don’t want to have collisions between the bullet and the catapult’s arm.

Notice that we return YES if there were still bullets available and NO otherwise. This will be useful later to check if the level is over because we ran out of bullets.

Let’s create another method to call all these initialization methods right after attachBullet:

- (void)resetGame
{
    [self createBullets:4];
    [self attachBullet];
}

Now add a call to this method at the end of the init method:

[self resetGame];        

Run the project and you’ll see something weird:

Acorn attached at incorrect position

The corn is a little off the mark. That’s because the position I set for the corn to attach is the position for when the catapult’s arm is at 9 degrees, at the resting position. But at the end of the init method the catapult is still at the zero degree angle so the bullet actually gets attached to the wrong position.

To fix this we only have to give the simulation some time to put the catapult’s arm to rest. So let’s change the call to resetGame on the init method to this:

[self performSelector:@selector(resetGame) withObject:nil afterDelay:0.5f];

This will make the call half a second later. We’ll have a better solution for this later on but for now it’ll do. If you run the project now you’ll see the correct result:

Catapult arm with acorn attached correctly

If we now pull the catapult’s arm and release the corn will not be released because it’s welded to the catapult. We need a way to release the bullet. To do this we just have to destroy the joint. But where and when to destroy the joint?

The best way is to check for some conditions on the tick method that gets called on every simulation step.

First we need a way to know if the catapult’s arm is being released. Let’s add a variable to our class for this first:

BOOL releasingArm;

Now go back to the ccTouchesEnded method and add this condition right before we destroy the mouse joint:

if (armJoint->GetJointAngle() >= CC_DEGREES_TO_RADIANS(20))
{
    releasingArm = YES;
}

This will set our release variable to YES only if the arm gets released then the arm is at least at a 20 degree angle. If we just pull the arm a little bit we won’t release the bullet.

Now add this to the end of the tick method:

// Arm is being released.
if (releasingArm && bulletJoint)
{
    // Check if the arm reached the end so we can return the limits
    if (armJoint->GetJointAngle() <= CC_DEGREES_TO_RADIANS(10))
    {
        releasingArm = NO;
        
        // Destroy joint so the bullet will be free
        world->DestroyJoint(bulletJoint);
        bulletJoint = nil;
        
    }
}

Pretty simple, right? We wait until the arm is almost at it’s resting position and we release the bullet.

Run the project and you should see the bullet flying off very fast!

Acorn flying through the air

In my opinion a little too fast. Let’s try to slow it down a bit by decreasing the max torque of the revolute joint.

Go back to the init method and decrease the max torque value from 4800 to 700. You can try out some other values to see what the effect is.

armJointDef.maxMotorTorque = 700; //4800;

Try again and ah much better – acorn flying action!

Gustavo Ambrozio

Contributors

Gustavo Ambrozio

Author

Over 300 content creators. Join our team.