2011年9月25日星期日

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

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, over 3 years of iOS experience and founder of CodeCrop Software.


Create a cute-a-pult game with Cocos2D and Box2D!  :D

Create a cute-a-pult game with Cocos2D and Box2D! :D


In this tutorial series we’ll build a cool catapult type game from scratch using Cocos2D and Box2D!


We’ll use the art created by Ray’s lovely and talented wife Vicki to create a game about catapults, acorns, dogs, cats, and angry squirrels.


In this tutorial series, you’ll learn:



  • How to use rotation joints

  • How to use weld joints

  • How to have the camera follow a projectile

  • How to use a collision’s impact ‘force” to decide if it should eliminate an enemy

  • And tons more!


This tutorial series assumes that you’ve already gone through the Intro to Box2D with Cocos2D Tutorial: Bouncing Balls Tutorial or have equivalent knowledge. It also uses a bunch of concepts found on the How To Create A Breakout Game with Box2D and Cocos2D Tutorial (part 1 and part 2).



Getting Started


Fire up Xcode and click “Create a new Xcode project”. Under iOS, choose cocos2d and choose the cocos2d_box2d template and click Next.


Selecting the Cocos2D with Box2D template


On the next screen give your project a name and fill out with a company identifier. We’ll call the project “cute-a-pult” – and if you look at the artwork, I’ll bet you can guess why! :]


On the next step select the folder where you want to store your project. You don’t have to create a new folder, Xcode will create one for you. I’d suggest enabling source control but this is not necessary.


When you click create your project will be created and we’ll be ready to start coding!


You probably know what this template project is all about, but just for the fun of it, click Run and let’s see what it does:


The Box2D Sample Project that comes with Cocos2D


Cool, so it runs and you can play with a few blocks. Cool, but not nearly as cool as what we’re about to make!


I Will Follow Him (on Github)


There isn't an ocean too deep that will keep me away from GitHub!

There isn't an ocean too deep that will keep me away from GitHub!


If you want to follow me wherever I may go, check out this project’s GitHub page!


Yes that’s right, for the first time in tutorial history (maybe I’m exaggerating a bit, I haven’t checked this in any way…) I’ll publish the entire tutorial files, with comments and tags for every steps on GitHub!


You might find this handy in case you get stuck somewhere or want to look at the code at a particular stage instead of typing it out yourself. Otherwise, keep following (and singing) along here! :]


The repository tag for this point in the tutorial is ProjectTemplate. You can download the project zip at this state here.


Cleaning Up


Before we get out hands dirty, let’s do some cleanup. Open HelloWorldLayer.h and remove the declaration for addNewSpriteWithCoords:(CGPoint)p.


Open HelloWorldLayer.mm and completely remove the implementations of these three methods:



  1. -(void) addNewSpriteWithCoords:(CGPoint)p (also remove from .h)

  2. - (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

  3. - (void)accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration


Now you’ll have a warning on this file on the init method because of a call to addNewSpriteWithCoords:(CGPoint)p. Let’s cleanup the init method.


First, delete the line that enables the accelerometer as we won’t be using it in this project:



// delete this line!
self.isAccelerometerEnabled = YES;


At the end of the method cleanup everything that add stuff to the scene:



// Delete these lines!
CCSpriteBatchNode *batch = [CCSpriteBatchNode batchNodeWithFile:@"blocks.png" capacity:150];
[self addChild:batch z:0 tag:kTagBatchNode];

[self addNewSpriteWithCoords:ccp(screenSize.width/2, screenSize.height/2)];

CCLabelTTF *label = [CCLabelTTF labelWithString:@"Tap screen" fontName:@"Marker Felt" fontSize:32];
[self addChild:label z:0];
[label setColor:ccc3(0,0,255)];
label.position = ccp( screenSize.width/2, screenSize.height-50);


That’s it for the HelloWorld.mm file. Let’s just remove the blocks png file so we don’t have any files that are not really necessary. Open the resources group on the project navigator and remove the blocks.png file. When ask if you want to remove the reference only or delete don’t be shy and hit Delete as we really don’t want this file anymore.


To make sure everything works, compile and run and you should see an empty scene:


An empty scene in Cocos2D


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


Adding Some Sprites


First let’s add the images we’ll use on the project. Again, I got these images from Vicki’s site but I had to modify them a bit for the purpose of the game. Basically I changed the catapult arm to already include the cup and straightened it to make the physics easier.


So go ahead and download my modified files from GitHub.


Now expand this archive. This will create a new folder called “images”. Drag this folder to the Resources group of your project. Be sure to check the “Copy items into destination group’s folder” to copy these files to your project folder.


Adding images to your Xcode project


Now we’re ready to start adding our sprites to the scene and try to reproduce the final scene that Vicki designed:


The level design for our catapult level


Open the HelloWorldLayer.mm file and insert this code right after m_debugDraw->SetFlags(flags);



CCSprite *sprite = [CCSprite spriteWithFile:@"bg.png"];
sprite.anchorPoint = CGPointZero;
[self addChild:sprite z:-1];

sprite = [CCSprite spriteWithFile:@"catapult_base_2.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointMake(181.0f, FLOOR_HEIGHT);
[self addChild:sprite z:0];

sprite = [CCSprite spriteWithFile:@"squirrel_1.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointMake(11.0f, FLOOR_HEIGHT);
[self addChild:sprite z:0];

sprite = [CCSprite spriteWithFile:@"catapult_base_1.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointMake(181.0f, FLOOR_HEIGHT);
[self addChild:sprite z:9];

sprite = [CCSprite spriteWithFile:@"squirrel_2.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointMake(240.0f, FLOOR_HEIGHT);
[self addChild:sprite z:9];

sprite = [CCSprite spriteWithFile:@"fg.png"];
sprite.anchorPoint = CGPointZero;
[self addChild:sprite z:10];


Right now we’re just adding sprites that will not be part of the physics simulation. We add the sprites that are on the back of the scene first and work our way to the front.


The default anchor point of CCSprite is the middle of the sprite. I’m changing it to the left-bottom corner of the image to make it easier to place them.


You probably noticed that a lot of Y coordinates are using the constant FLOOR_HEIGHT that we have not defined yet. Since this is used in many places it’s easier to use the constant so we can change this later if we change the floor height a bit. So let’s create this constant. On the top of this file, right after the PTM_RATIO definition, add this:



#define FLOOR_HEIGHT    62.0f


Let’s click Run to check out how we’re coming. This is what I got:


Sprites added to level


w00t, it’s starting to look good already!


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


Adding the Catapult Arm


It’s time to start adding some physics to this world. Right after the code you added above is the template’s code to create the world borders. Let’s change this a bit to describe our world.


The default world is exactly the size of the iPhone’s screen. Since our scene’s width is twice this size we’ll have to change our world’s width. To do this just find every instance of screenSize.width you can find when defining the world boundaries and multiply it by 2.0f


Also, since our world’s floor is not on the bottom of the screen change the bottom’s edge Y coordinates to FLOOR_HEIGTH/PTM_RATIO. The final code should be this:



// bottom
groundBox.SetAsEdge(b2Vec2(0,FLOOR_HEIGHT/PTM_RATIO), b2Vec2(screenSize.width*2.0f/PTM_RATIO,FLOOR_HEIGHT/PTM_RATIO));
groundBody->CreateFixture(&groundBox,0);

// top
groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width*2.0f/PTM_RATIO,screenSize.height/PTM_RATIO));
groundBody->CreateFixture(&groundBox,0);

// left
groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(0,0));
groundBody->CreateFixture(&groundBox,0);

// right
groundBox.SetAsEdge(b2Vec2(screenSize.width*2.0f/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width*2.0f/PTM_RATIO,0));
groundBody->CreateFixture(&groundBox,0);


Now let’s add the catapult arm. First let’s add some references to the Box2d body and fixture we’ll create as we’ll need this later on. Go to HelloWorldLayer.h and add this to the class’ instance variables:



b2Fixture *armFixture;
b2Body *armBody;


Now go back to the class implementation file and add this at the bottom of init:



// Create the catapult's arm
//
CCSprite *arm = [CCSprite spriteWithFile:@"catapult_arm.png"];
[self addChild:arm z:1];

b2BodyDef armBodyDef;
armBodyDef.type = b2_dynamicBody;
armBodyDef.linearDamping = 1;
armBodyDef.angularDamping = 1;
armBodyDef.position.Set(230.0f/PTM_RATIO,(FLOOR_HEIGHT+91.0f)/PTM_RATIO);
armBodyDef.userData = arm;
armBody = world->CreateBody(&armBodyDef);

b2PolygonShape armBox;
b2FixtureDef armBoxDef;
armBoxDef.shape = &armBox;
armBoxDef.density = 0.3F;
armBox.SetAsBox(11.0f/PTM_RATIO, 91.0f/PTM_RATIO);
armFixture = armBody->CreateFixture(&armBoxDef);


If you already went through the previous Box2d tutorials on the site (and if you haven’t I recommend going through those before continuing) most of this should be to you.


We first load the catapult’s arm sprite and add it to the scene. Notice the z index. We used z indexes when we added the static sprites to the scene, so this z index puts the arm between the two sides of the catapult to look nice.


Notice also that we didn’t specify the sprite’s position. That’s because the tick method will set the sprite’s position to the Box2D body’s position.


Next we create the Box2d body as a dynamic body. The userData here is important because of what I mentioned on the last paragraph, so that the sprite will follow the body. Also notice that the position is set to be above the FLOOR_HEIGHT we defined. That’s because here we have to use the position of the center of the body, we can’t use the left-bottom corner using Box2d.


Next comes the creation of the fixture that describes the body’s physical features. It will be a simple rectangle.


We set the fixture’s rectangle to be a bit smaller than the size of the sprite. This is because if you take a look at the sprite, you’ll see that the sprite is larger than the arm itself:


Making a fixture rectangle smaller than the sprite


In this picture, the black rectangle is the size of the sprite, and the red rectangle is the size we want for the fixture.


You can get these dimensions using any image editing software. Just remember to use the standard resolution image for this as the -hd version gets scaled to the standard size.


Run the app again and you’ll see the arm in a vertical position:


Catapult Arm added to level


So far so good – now let’s add some movement to this catapult!


Rotating with Joints


We need to somehow restrict the movement of the catapult so that it rotates around a point. The way you restrict motion of Box2D bodies relative to each other is via joints.


No Jay and Silent Bob, not that kind of joint!

No Jay and Silent Bob, not that kind of joint!


There’s one particular joint that is perfect for this – a revolute joint. Think of this as pinning two bodies together at a particular point, but still allowing them to rotate.


So we can “pin” the catapult arm to the ground at the base of the catapult to get the effect we want!


Let’s try this out. Go to HelloWorldLayer.h and add this to the class’ instance variables:



b2RevoluteJoint *armJoint;


Now go back to the class implementation file and add this right after the catapult’s arm creation:



// Create a joint to fix the catapult to the floor.
//
b2RevoluteJointDef armJointDef;
armJointDef.Initialize(groundBody, armBody, b2Vec2(233.0f/PTM_RATIO, FLOOR_HEIGHT/PTM_RATIO));
armJointDef.enableMotor = true;
armJointDef.enableLimit = true;
armJointDef.motorSpeed = -1260;
armJointDef.lowerAngle = CC_DEGREES_TO_RADIANS(9);
armJointDef.upperAngle = CC_DEGREES_TO_RADIANS(75);
armJointDef.maxMotorTorque = 4800;

armJoint = (b2RevoluteJoint*)world->CreateJoint(&armJointDef);


When creating the joint you have to specify 2 bodies and the hinge point. You might be thinking: “shouldn’t the catapult’s arm attach to the base?”. Well, in the real world, yes. But in Box2d not necessarily. You could do this but then you’d have to create another body for the base and add more complexity to the simulation.


Since the base would be static anyway and the in Box2d the hinge body doesn’t have to be in any of the to bodies, we can just use the groundBody that we already have.


The angle limits, combined with a motor make the catapult behave much like a catapult in the real world.


You’ll also notice we set a motor up on the joint, by setting “enableMotor”, “motorSpeed”, and “maxMotorTorque”. By setting the motor speed to negative, this makes the catapult arm want to continually rotate clockwise (kind of like a spring on a catapult).


However, we also enabled limits on the joint by setting “enableLimit”, “lowerAngle”, and “upperAngle”. This makes the arm stop rotating at the 9 degree angle (slightly bent to the left) and a 75 degree angle (pulled back a good bit to the left). This simulates the movement of the catapult arm we want.


In a while we’ll add another joint that will let you pull back the catapult. When we release this force the motor will make the arm fling forward, much like a real catapult would!


The motor speed value is in radians per second. Yes, not very intuitive, I know. What I did was to just try out different values until I had the desired effect. You can start out small and increase it until you get the desired speed for the arm. The maxMotorTorque parameter is the max torque the motor can apply. This value is also something that you can vary to see how things react. Again it will become clear soon.


Run the app and you’ll see the arm bent to the left now:


Catapult arm connected to ground via a revolute joint


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


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!


Gratuitous Camera Movement


One thing that would be nice is if we could make the scene move to follow the bullet so we can see it’s whole movement.


We can do this easily by changing the position property of the scene. Add this code to the end of the tick method:



// Bullet is moving.
if (bulletBody && bulletJoint == nil)
{
b2Vec2 position = bulletBody->GetPosition();
CGPoint myPosition = self.position;
CGSize screenSize = [CCDirector sharedDirector].winSize;

// Move the camera.
if (position.x > screenSize.width / 2.0f / PTM_RATIO)
{
myPosition.x = -MIN(screenSize.width * 2.0f - screenSize.width, position.x * PTM_RATIO - screenSize.width / 2.0f);
self.position = myPosition;
}
}


The condition checks is there’s a bullet that was attached but is not anymore, so it must be moving. We then get it’s position and check if it’s after the right of the screen’s middle. If it is we change the position of the screen to make the bullet be in the middle of the screen.


Notice the minus sign in front of the MIN. We do this because the position has to be negative to make the scene move to the left.


Try it out. It’s pretty cool now!


Flying acorn with screen scrolling


Where To Go From Here?


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


At this point, you should have a great start of a catapult game – the catapult works quite well! Stay tuned for the next part of the tutorial, where we expand this into a full game with enemies and collision detection!


In the meantime, if you have any questions or comments, join the forum discussion below!


This is a blog post by iOS Tutorial Team member Gustavo Ambrozio, a software engineer with over 20 years experience, over 3 years of iOS experience and founder of CodeCrop Software.


没有评论:

发表评论