显示标签为“Development”的博文。显示所有博文
显示标签为“Development”的博文。显示所有博文

2012年2月4日星期六

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 2

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 2:
This is a post by special contributor Andreas Loew, the creator of TexturePacker and PhysicsEditor.

Create this vertical scrolling platformer with Cocos2D!
Create this vertical scrolling platformer with Cocos2D!

Welcome back to the Monkey Jump tutorial! In this series, we are creating a fun vertical scrolling platformer with Cocos2D, TexturePacker and PhysicsEditor.

In Part One on the tutorial series, we introduced the MonkeyJump! game design, created the sprite sheets and shapes we needed, and began coding the game.

Before we stopped for a break, we had all of our game layers set up and had just finished making random objects drop from the sky, with sound effects.

In this second part of the tutorial, we will add our hero to the game, make him move and jump, and start adding some gameplay.

We’ll be starting with the project where we left off last time. If you don’t have it already, grab the source code for this tutorial series and open up 3-DraggingObjects.

Without further ado, let’s get back to (monkey) business! :]



Getting Started


We created the monkey’s shape in PhysicsEditor in Part One of this tutorial, but haven’t added the monkey to the game yet. Let’s do that now!

Add the Monkey class by creating a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Monkey, and make it a subclass of GB2Sprite. (Again, remember to change extension for the Monkey.m file to .mm)

The monkey will react to different events in the game world: he’ll put up his hands when something drops from above, push items, jump, etc. This is why you won’t be using Cocos2d’s standard animation routines, and instead implement some of your own.

For this, you need some member variables to store the additional data. Paste this code into Monkey.h replacing what’s there already:


#pragma once

#import "Cocos2d.h"
#import "GB2Sprite.h"

@class GameLayer;

@interface Monkey : GB2Sprite
{
float direction;      // keeps monkey's direction (from accelerometer)
int animPhase;        // the current animation phase
ccTime animDelay;     // delay until the next animation phase is stated
GameLayer *gameLayer; // weak reference
}

-(id) initWithGameLayer:(GameLayer*)gl;
-(void) walk:(float)direction;

@end


Now switch to Monkey.mm and replace it with the following lines:


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

#define JUMP_IMPULSE 6.0f
#define WALK_FACTOR 3.0f
#define MAX_WALK_IMPULSE 0.2f
#define ANIM_SPEED 0.3f
#define MAX_VX 2.0f

@implementation Monkey

-(id) initWithGameLayer:(GameLayer*)gl
{  
// 1 - Initialize the monkey 
self = [super initWithDynamicBody:@"monkey"
spriteFrameName:@"monkey/idle/1.png"];

if(self)
{
// 2 - Do not let the monkey rotate
[self setFixedRotation:true];

// 3 - The monkey uses continuous collision detection
// to avoid getting stuck inside fast-falling objects
[self setBullet:YES];

// 4 - Store the game layer
gameLayer = gl;
}

return self;
}

@end


Let’s go through the initWithGameLayer method step-by-step:

  1. First, we initialize the monkey. The monkey’s movement will be affected by the physics engine, so make him a dynamic object. We’ll use the idle frame as both the first frame of the monkey animation and the monkey’s physics shape.
  2. The monkey should stand straight up all the time, so we set his rotation to fixed. This means the monkey is moved by Box2d, but does not rotate or tilt.
  3. Set the monkey to bullet mode. Bullet mode enables continuous collision detection on an object. Without it, Box2d moves objects and then performs the collision checks. With fast-moving objects it’s possible that an object will pass through another without any collision detection at all or that an object will get stuck in another. Continuous collision detection calculates collisions all the way from an object’s current position to its new position – not just for the end point.
  4. Finally, you need to store the game layer – keep it as a weak reference and just assign the value.

A monkey bullet!  Bullet mode enables continuous collision detection.
A monkey bullet! Bullet mode enables continuous collision detection.


With regards to bullet mode for step #3, if you were coding a project with only a few objects, you could set the Box2d engine to run continuous collision detection for all game objects. However, when a game has a lot of objects that would add a lot of CPU overhead. So, for our game we’ll set the continuous mode on just the monkey and fast-moving (dropping) objects.

To enable the monkey, add him to the GameLayer. Open GameLayer.h and add the following lines at the top, just below the import statements:


@class Monkey;


Now, add the following member variable to the GameLayer class:


Monkey *monkey;


Then switch to GameLayer.mm and import Monkey.h at the top of the file:


#import "Monkey.h"


At the end of the init selector in GameLayer.mm, initialize the monkey, add him to the game layer and set a starting position for the monkey:


monkey = [[[Monkey alloc] initWithGameLayer:self] autorelease];
[objectLayer addChild:[monkey ccNode] z:10000];
[monkey setPhysicsPosition:b2Vec2FromCC(240,150)];


Compile and run, and you’ll see the following:



The monkey is in the house! Objects drop onto him and he gets pushed away – perfect. That’s exactly what we want.

Do the Monkey Walk


Our next goal is to make the monkey walk, using the accelerometer as input.

Go back to GameLayer.mm and add the following code at the end of the init method:


self.isAccelerometerEnabled = YES;


This will ensure that for each change in the built-in accelerometer values, the GameLayer class gets an automatic notification. The notification handler has to be added to GameLayer.mm at the end of the file, before the @end marker:


- (void)accelerometer:(UIAccelerometer*)accelerometer
didAccelerate:(UIAcceleration*)acceleration
{
// forward accelerometer value to monkey
[monkey walk:acceleration.y];
}


The accelerometer handler calls the walk method of the monkey object with the y-axis value of the accelerometer. This method will handle actually moving the monkey back and forth based on the accelerometer input.

So, move to Monkey.mm and add the walk method to the end of the file (before the @end marker). This method simply stores the new movement direction for the monkey in a member variable.


-(void) walk:(float)newDirection
{
direction = newDirection;
}


Try compiling and running the code now … Surprise! Nothing new happens. This is due to the fact that while the direction value has been stored, it has not been applied to the physics simulation yet. In order to update the physics simulation based on the new movement direction, we need to override the updateCCFromPhysics selector, which is called by the GB2Engine on every frame for a GB2Node object to update the physics.

Update Monkey Physics

Add the following code to Monkey.mm:


-(void) updateCCFromPhysics
{
// 1- Call the super class
[super updateCCFromPhysics];

// 2 - Apply the directional impulse
float impulse = clamp(-[self mass]*direction*WALK_FACTOR,
-MAX_WALK_IMPULSE,
MAX_WALK_IMPULSE);           
[self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]];       
}


In the above, you first call the selector of the super class. This will update the monkey’s sprite based on physics simulation.

Then, you push the monkey in the right direction based on the stored direction value. It is important not to take complete control of the monkey. Otherwise, his “natural” behavior in response to events such as items dropping or collision detection won’t work properly.

All you really do is to give the monkey a nudge in the right direction. Since the physics engine updates happen 60 times per second, it’s important to keep the push quite light. It’s a monkey – not a bullet, even if he is in bullet mode!

You can move a box2D object by applying an impulse to the object. And you do this using the GB2Sprite’s applyLinearImpulse method, which takes two parameters: the impulse to apply and the point of application.

For the point of application, we’ll use the world center of the object. Applied at the world center, the object will be pushed without any torque that would result into a rotation. (Which, by the way, would not happen to the monkey anyway, since we already set him not to rotate.)




When applying an impulse, I recommend using the mass of the object, which you can get with [self mass]. This is because the impulse is the product of mass and velocity.

Multiply the mass value by the stored direction value. This gives you small impulses when the device is tilted slightly, and bigger impulses when its tilted sharply.

Scaling the impulse with the object’s mass frees us from having to worry about the movement changing when we change the object’s shape in PhysicsEditor. If we didn’t scale the impulse and later we made the monkey a bit smaller (for example), the same impulse applied to a monkey of less mass will result in a faster-moving monkey.

We will also clamp the value to a maximum to avoid impulses that are too strong. The maximum impulse is defined with the MAX_WALK_IMPULSE variable.

Compile and run. Still nothing? Ah, I forgot to tell you one thing: the iPhone simulator does not simulate the accelerometer. So, from now on, we need to test on the device! Switch to the device and test.

The monkey now slides left and right – but the movement doesn’t look very natural.

Make the Monkey Move

We’re going to add some code to Monkey.mm to get the monkey animations working. Add the following to the end of the updateCCFromPhysics method:


animDelay -= 1.0f/60.0f;
if(animDelay <= 0)
{
animDelay = ANIM_SPEED;
animPhase++;
if(animPhase > 2)
{
animPhase = 1;
}
}


The first line simply updates the time till the next animation by decreasing the time delay till the next animation phase. I use the value 1.0f/60.0f because I assume that the application runs at 60 fps and the updateCCFromPhysics method does not have a delta time parameter which would provide the timer interval between each update accurately.

If the animation time delay drops below zero, reset the animation delay value to the animation speed and increase the current phase by one. If the highest phase is reached, loop back to 1 so that the animation will continue to play in a loop.

Next, we need to determine the direction the monkey is facing. There are two ways to do this:

  1. Use the direction from the accelerometer
  2. Use the monkey’s velocity vector

I prefer to use the accelerometer, since it gives the player immediate feedback when he or she tries to change the direction by tilting the device. We’ll respond to velocity changes via accelerometer later.

Add this code to the end of updateCCFromPhysics:


// determine direction of the monkey
bool isLeft = (direction < 0);

// direction as string
NSString *dir = isLeft ? @"left" : @"right";   

// update animation phase
NSString *frameName;
const float standingLimit = 0.1;
float vX = [self linearVelocity].x;
if((vX > -standingLimit) && (vX < standingLimit))
{
// standing
frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];           
}
else
{
// walking
NSString *action = @"walk";
frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];       
}

// set the display frame
[self setDisplayFrameNamed:frameName];


Basically, all the code above does is, if the monkey’s speed is lower than standingLimit, it makes him look directly at the player with his idle animation frame. Otherwise, it uses a walk display frame matching the current direction and animation frame number.

Compile and run. The monkey now runs about – nice!

Slow Down Little Fella

I’m still not happy with one thing: I think the monkey moves too fast. We could reduce the impulse we’re applying to make him walk, but this will also make him slow and clumsy.

We need a strong enough impulse to make him react fast – but not too fast.

Replace the current code for updateCCFromPhysics in Monkey.mm with the following code:


// 1 - Call the super class
[super updateCCFromPhysics];

// 2 - Update animation phase
animDelay -= 1.0f/60.0f;
if(animDelay <= 0)
{
animDelay = ANIM_SPEED;
animPhase++;
if(animPhase > 2)
{
animPhase = 1;
}
}

// 3 - Get the current velocity
b2Vec2 velocity = [self linearVelocity];
float vX = velocity.x;

// 4 - Determine direction of the monkey
bool isLeft = (direction < 0);

if((isLeft && (vX > -MAX_VX)) || ((!isLeft && (vX < MAX_VX))))
{
// apply the directional impulse
float impulse = clamp(-[self mass]*direction*WALK_FACTOR,
-MAX_WALK_IMPULSE,
MAX_WALK_IMPULSE);           
[self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]];       
}

// 5 - Get direction as string
NSString *dir = isLeft ? @"left" : @"right";       

// 6 - Update animation phase
NSString *frameName;
const float standingLimit = 0.1;
if((vX > -standingLimit) && (vX < standingLimit))
{
// standing
frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];           
}
else
{
// walking
NSString *action = @"walk";
frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];       
}

// 7 - Set the display frame
[self setDisplayFrameNamed:frameName];


As you’ll notice, we’ve moved a few code blocks around but the major change is the addtion of a new line to section #3 for the vX variable and moving the impulse code to section #4 to be wrapped within an if condition that checks if the velocity is below a maximum value for the current direction. This allows the monkey to steer against being pushed away by an object, but keeps him from accelerating too fast on his own.

Compile and run. I thinks this looks much better now.



The source code of the project in its current state is available in the folder 4-WalkingMonkey.

Jump, Jump!


Now, let’s make the monkey jump. For this, we need to make the game detect touch events anywhere on the GameLayer, because we want the monkey to jump at every touch.

Open GameLayer.mm and enable touch detection by adding the following line to the init selector:


// enable touches
self.isTouchEnabled = YES;


Also add the following selector at the end of the file (but before the @end marker). It forwards the touches to the monkey object via the jump method.


-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[monkey jump];   
}


You might notice at this point that we don’t have a jump method in the Monkey class. That’s what we’ll add next. Switch to Monkey.h and add the following method definition just before @end:


-(void) jump;


Now open Monkey.mm and add the following code before the @end marker:


-(void) jump
{
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE)
point:[self worldCenter]];  

[[SimpleAudioEngine sharedEngine] playEffect:@"jump.caf"
pitch:gFloatRand(0.8,1.2)
pan:(self.ccNode.position.x-240.0f) / 240.0f
gain:1.0 ];
}


All the above code does is to apply an impulse to make the monkey jump and play the jump sound. We’ll pan the sound as we did for the object sounds in Part One of this tutorial.

Compile and run. Tap the screen to make the monkey jump.

Outta Sight!

But this is far from perfect: if you tap the screen multiple times, the monkey goes through the roof! Worse still, the camera does not follow the monkey.

Let’s fix the camera first. First, add the following code to the top of the update selector, above the existing code, in GameLayer.mm.


// 0 - monkey's position
float mY = [monkey physicsPosition].y * PTM_RATIO;


The above code copies the monkey’s y-position into a new variable mY. Of course, we could also access the monkey’s ccNode and take the y-coordinate from there. The end result will be the same as multiplying his physics position by the PTM_RATIO.

Now, add these lines to the end of the update selector after the closing curly brace after section #7.


// 8 - Adjust camera
const float monkeyHeight = 70.0f;
const float screenHeight = 320.0f;
float cY = mY - monkeyHeight - screenHeight/2.0f;
if(cY < 0)
{
cY = 0;
}


Here, we calculate a good value for the camera’s y-coordinate, one that will more or less center the monkey on the middle of the screen. Clamp the value so that it does not go below 0 so that the camera does not move below ground level.

Now let’s implement parallax scrolling of the background so that the monkey’s movement appears more natural. The effect is quite easy to accomplish: just multiply the background layer by a factor below 1.0 and set the position for the layers. This will make the background layer scroll slower. The further away a layer is from the camera, the smaller the factor must be.

Add this code below the last few lines added to update:


// 9 - Do some parallax scrolling
[objectLayer setPosition:ccp(0,-cY)];
[floorBackground setPosition:ccp(0,-cY*0.8)]; // move floor background slower
[background setPosition:ccp(0,-cY*0.6)];      // move main background even slower


Feel free to adjust the values to your liking.

That’s it – compile and run. Tap the screen multiple times to see the monkey rise. And beware of flying monkeys!!!!




And It All Piles Up!

If you play the game for a while, you’ll notice that the items continue to drop from the same fixed height – sometimes way below the monkey’s current position. In fact, after some time, items don’t even drop because they’ve piled up beyond the spawning point.

Fix this by changing the following line in GameLayer.mm‘s update method:


float yPos = 400;


Change the above line to this:


float yPos = 400 + mY;


Items will now spawn 400pt above the monkey’s head, wherever it may be.

Back to the monkey. You’ll have noticed that the monkey can actually jump again and again while up in the air. This just won’t do. We need to fix it so the monkey only jumps when he has contact with the floor.

Let’s start by counting the number of contacts the monkey makes with the floor.

Add a new variable to Monkey.h:


int numFloorContacts;     // number of floor contacts


Switch to Monkey.mm and add the following two new collision detection handlers to the end of the file (but above the @end marker):


-(void) beginContactWithFloor:(GB2Contact*)contact
{
numFloorContacts++;
}

-(void) endContactWithFloor:(GB2Contact*)contact
{
numFloorContacts--;
}


As the names imply, the first one detects the beginning of a contact/collision with the floor and the second the end of a collision. In the beginContact selector, we increase the value of the floor contact variable, and then decrease it in the endContact selector.

These selectors are going to get called by GBox2D every time a contact starts or ends between the monkey and the floor. (Remember: we created a separate class for the floor so that GBox2D now can call the appropriate selector with the class’s name).

Now, if the monkey is standing on the floor, then the numFloorContacts value should be at least one. Use this to our advantages by wrapping the code in the jump method in Monkey.mm with an if condition to see if the monkey is actually standing on the floor before jumping:


-(void) jump
{
if(numFloorContacts > 0)
{
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE)
point:[self worldCenter]];  

...
}
}


Compile and run. Everything seems fine. Well… except that when the monkey lands on an object, he loses the ability to jump. To fix this, we’re going to consider any contact the monkey has with objects similar to a contact with the floor.

And it’s very simple to implement. Simply add a couple more collision-handling routines to the end of Monkey.mm and count object contacts the same way we counted floor contacts:


-(void) beginContactWithObject:(GB2Contact*)contact
{
// count object contacts as floor contacts 
numFloorContacts++;       
}

-(void) endContactWithObject:(GB2Contact*)contact
{
// count object contacts as floor contacts 
numFloorContacts--;       
}


Compile and run. Isn’t it much better now? And our game is already playable!

Push It!


Let’s improve the gameplay and allow the monkey to push objects to the left and right – and give him the ability to put his hands above his head to shield him from dropping objects.

Do you remember how you added the sensors to the left and right sides of the monkey in Part One? They come into play now! The key to the sensors was setting up the “Id” parameter in PhysicsEditor. You are going to retrieve this value now!

But before we do that, we need to add a few instance variables to keep count of the left and right sensors, as well as the number of contacts made with the monkey’s head. Add these variables to Monkey.h:


int numPushLeftContacts;
int numPushRightContacts;
int numHeadContacts;


Next, the beginContactWith* and endContactWith* selectors have a contact parameter we can use to determine which part of the monkey has contact with an object – the “Id” value we added in PhysicsEditor is stored as user data in each fixture. So replace the existing object contact handlers for Monkey.mm with the following:


-(void) beginContactWithObject:(GB2Contact*)contact
{
NSString *fixtureId = (NSString *)contact.ownFixture->GetUserData();
if([fixtureId isEqualToString:@"push_left"])
{
numPushLeftContacts++;
}
else if([fixtureId isEqualToString:@"push_right"])
{
numPushRightContacts++;
}
else if([fixtureId isEqualToString:@"head"])
{
numHeadContacts++;
}
else
{
// count others as floor contacts 
numFloorContacts++;       
}
}

-(void) endContactWithObject:(GB2Contact*)contact
{
NSString *fixtureId = (NSString *)contact.ownFixture->GetUserData();
if([fixtureId isEqualToString:@"push_left"])
{
numPushLeftContacts--;
}
else if([fixtureId isEqualToString:@"push_right"])
{
numPushRightContacts--;
}
else if([fixtureId isEqualToString:@"head"])
{
numHeadContacts--;
}
else
{
// count others as floor contacts 
numFloorContacts--;       
}
}


As you see from the new code, you retrieve the Id, which we’ll call fixtureId here, by accessing the contact parameter’s fixture and then accessing the fixture’s user data via the GetUserData method.

Now that we’re tracking the contacts, we can update the monkey’s animation frames to handle additional events.

Here’s the decision table for the various animations:




























Monkey

Objects

Animation
StandingNo object above headIdle
StandingObject above headArms up
MovingNo push sensor active in the directionWalk
MovingPush sensor active in the directionPush

Using the above table, we modify section #6 of updateCCFromPhysics in Monkey.mm as follows:


// 6 - Update animation phase
const float standingLimit = 0.1;
NSString *frameName = nil;
if((vX > -standingLimit) && (vX < standingLimit))
{
if(numHeadContacts > 0)
{
// Standing, object above head
frameName = [NSString stringWithFormat:@"monkey/arms_up.png"];                       
}
else
{
// Just standing
frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];           
}
}
else
{
if(numFloorContacts == 0)
{
// Jumping, in air
frameName = [NSString stringWithFormat:@"monkey/jump/%@.png", dir];
}
else
{
// Determine if monkey is pushing an item
bool isPushing =  (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 0));

// On the floor
NSString *action = isPushing ? @"push" : @"walk";

frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];       
}       
}


Compile and test. Perfect! The monkey now behaves just as we want.



It Takes a Strong Monkey …

Playing a little bit more though, I think the monkey should be a bit stronger under certain conditions. Currently, he’s too weak to break free when an object is above his head. Let’s give him some extra strength when he’s trapped like that and wants to jump.

This is the current line from the jump selector in Monkey.mm that makes the monkey jump:


[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE)
point:[self worldCenter]];


Replace it with:


float impulseFactor = 1.0;

// if there is something above monkey's head make the push stronger
if(numHeadContacts > 0)
{
impulseFactor = 2.5;
}
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE*impulseFactor)
point:[self worldCenter]];


That’s our monkey on steroids! He now uses a 2.5-times stronger impulse when there’s an object resting above him – this should allow him to break free of most of the objects.

Let’s also change the walking impulse in case the monkey needs to push an object to the side to break free. Go to updateCCFromPhysics and cut the following lines from section #6:


// Determine if monkey is pushing an item
bool isPushing =  (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 0));


Now paste that code into section #4 and modify it as follows:


// 4 - Determine direction of the monkey
bool isLeft = (direction < 0);

// Determine if monkey is pushing an item
bool isPushing =  (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 0));

if((isLeft && (vX > -MAX_VX)) || ((!isLeft && (vX < MAX_VX))))
{
// apply the directional impulse
float impulse = clamp(-[self mass]*direction*WALK_FACTOR,
-MAX_WALK_IMPULSE,
MAX_WALK_IMPULSE);      
if(isPushing)
{
impulse *= 2.5;
}
[self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]];       
}


Compile and test – that’s much better. But there’s still a problem: when the monkey slightly grazes an object with his head, the new jump power makes him go through the roof!

We need to clamp his maximum speed inside the updateCCFromPhysics method. Add this to the end of section #3 of the updateCCFromPhysics method:


const float maxVelocity = 5.0;
float v = velocity.Length();
if(v > maxVelocity)
{
[self setLinearVelocity:maxVelocity/v*velocity];
}


Notice that in the code above we are directly modifying values controlled by the Box2d engine, thus affecting the overall behaviour of the physics engine. You should try to avoid doing this kind of manipulation.

Compile and test. I like the monkey’s behavior now. He reacts quickly, and is strong enough to push objects but does not become uncontrollable.

Where To Go From Here?


If you don’t have it already, here is all of the source code for this tutorial series.

You’ve now reached the end of Part Two of the MonkeyJump tutorial! The project in its current form is available in the source code zip in the folder called 5-MonkeyJumpAndRun.

Stay tuned for the final part of the series, where we’ll add some performance improvements, add a HUD layer to the game, and yes – kill the monkey! :]

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

This is a post by special contributor Andreas Loew, the creator of TexturePacker and Physics Editor.

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 2 is a post from: Ray Wenderlich

2012年1月6日星期五

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 2

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 2:
This is a blog post by iOS Tutorial Team member Tope Abayomi, an iOS developer and Founder of App Design Vault, your source for iPhone App Design.

Learn how to create a PDF programmatically!
Learn how to create a PDF programmatically!

Welcome to the second part of the tutorial series on how to generate a PDF using Quartz 2D!

In part one, we set up an app framework for the PDF, and we drew some rudimentary text and lines with Quartz 2D inside our PDF.

Since our PDF will be an invoice document, it needs to look professional. To accomplish this, here in Part Two we’ll be adding a logo image and drawing a table to hold the invoice data. By the end of this article, our PDF will be complete.

Let’s jump back in!



Adding An Image


The image we’re going to draw to our PDF is the logo of none other than RayWenderlich.com. All part of Ray’s scheme to take over the world! ;]

Go ahead and download the image, then add it to the project. To do this, control-click on the PDFRenderer group in the Project navigator and choose “Add Files To iOSPDFRenderer.” Select the ‘ray-logo.png’ image you downloaded and then click Add.



Next open PDFRenderer.m and add this method:


+(void)drawImage:(UIImage*)image inRect:(CGRect)rect
{

[image drawInRect:rect];

}


Yes, drawing an image with Core Graphics is that easy! All we need is this one-liner to draw an image to the current context.

The method takes in the image we want to draw and the frame where it will be drawn, and draws the image into the context.

Next add the definition of this method in PDFRenderer.h:


+(void)drawImage:(UIImage*)image inRect:(CGRect)rect;


Now we need to call the method so that it’s displayed on the PDF. Add the following lines of code to the drawPDF method on the PDFRenderer.m file (right before the call to UIGraphicsEndPDFContext):


UIImage* logo = [UIImage imageNamed:@"ray-logo.png"];
CGRect frame = CGRectMake(20, 100, 300, 60);
[PDFRenderer drawImage:logo inRect:frame];


In the code above, we create a UIImage from the image file, define the location and size of the image to be drawn, then call the drawImage method we created above with these two parameters.

Here’s the complete version of the drawPDF method:


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

[self drawText];

CGPoint from = CGPointMake(0, 0);
CGPoint to = CGPointMake(200, 300);
[PDFRenderer drawLineFromPoint:from toPoint:to];

UIImage* logo = [UIImage imageNamed:@"ray-logo.png"];
CGRect frame = CGRectMake(20, 100, 300, 60);

[PDFRenderer drawImage:logo inRect:frame];

// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
}


Now if you run the application in the simulator, you’ll see our PDF as we left it in Part One, only now with Ray’s logo. Of course, it still looks pretty crappy. Time to fix that!



Drawing the Labels


At this point, we know how to draw the basic elements of the invoice: text, lines and images. The next step is to combine all the elements to create a polished layout.

To do this, we’ll use a little trick. We’ll create a View in the interface builder and add the elements for the invoice. Then we’ll use the locations of the views to make the layout.

This will make things a lot easier because we can visually layout the PDF, and won’t have to hard-code as many coordinates for drawing. Don’t worry, it will begin to make sense as we do it!

Create a new View in the application by choosing File\New\New File. Select the iOS\User Interface\View template and click on Next. Make sure the Device family is set to iPhone and click Next.

Give the new View the name InvoiceView and click Create. Select the View on the Interface Builder and delete it by hitting the backspace button.



Add a new View from the Objects tab onto the canvas. Resize the view to be 612 wide and 792 tall. These are the default dimensions for an A4 PDF file.



Add eight labels to the View and give them the following names:

  • Recipient [Name]
  • Recipient’s Address
  • Recipient’s City
  • Recipient’s Postal Code
  • Invoicer [Name]
  • Invoicer’s Address
  • Invoicer’s City
  • Invoicer’s Postal Code



The positions of these labels will form our layout for the invoice. Give each label a tag from 0-7. For example, the label “Recipient” will have a tag of 0, “Recipient’s Address” will have a tag of 1, and so on.



We’re done with the layout view for now. We’ll come back to it in a bit.

But first we need to open PDFRenderer.m and refactor the drawText method. We want to pass in the text to be drawn and the frame that will encompass it, rather than hard-coding it in.

Go ahead and replace drawText with the following (pretty much the same except pulling out the hardcoded string and frame):


+(void)drawText:(NSString*)textToDraw inFrame:(CGRect)frameRect
{
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;
// Prepare the text using a Core Text Framesetter.
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);

CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);

// Get the frame that will do the rendering.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);

// Get the graphics context.
CGContextRef    currentContext = UIGraphicsGetCurrentContext();

// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);

// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);

// Draw the frame.
CTFrameDraw(frameRef, currentContext);

CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);
}


Then add the new definition to PDFRenderer.h:


+(void)drawText:(NSString*)textToDraw inFrame:(CGRect)frameRect;


The next step is to load the labels from the InvoiceView and use the text and the frame to draw to the PDF. Add this new method to PDFRenderer.m, right above drawPDF:


+(void)drawLabels
{   
NSArray* objects = [[NSBundle mainBundle] loadNibNamed:@"InvoiceView" owner:nil options:nil];

UIView* mainView = [objects objectAtIndex:0];

for (UIView* view in [mainView subviews]) {
if([view isKindOfClass:[UILabel class]])
{
UILabel* label = (UILabel*)view;

[self drawText:label.text inFrame:label.frame];
}
}
}


This will load the labels from the InvoiceView, loop through all the labels and call the drawText method with its text and frame variables.

Next we’ll modify the drawPDF method call this method and remove all the test drawing code we used previously. Replace drawPDF with the following:


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

[self drawText:@"Hello World" inFrame:CGRectMake(0, 0, 300, 50)];

[self drawLabels];

// Close the PDF context and write out the contents.
UIGraphicsEndPDFContext();
}


Let’s try it out! Run the application in the simulator, and you will see something similar to the image below.



Yikes – it looks like garbage, right? That’s because the text is being flipped and translated multiple times in the the drawText method. Up until now, we’ve only drawn one line of text, but the problem arises when we want to draw multiple lines of text.

To fix this, we need to modify the drawText method to flip the current context back to its original coordinates. Change the following lines in the drawText method to add the reverse-flip actions and take the origin into consideration:


// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
// Modify this to take into consideration the origin.
CGContextTranslateCTM(currentContext, 0, frameRect.origin.y*2);
CGContextScaleCTM(currentContext, 1.0, -1.0);

// Draw the frame.
CTFrameDraw(frameRef, currentContext);


// Add these two lines to reverse the earlier transformation.
CGContextScaleCTM(currentContext, 1.0, -1.0);
CGContextTranslateCTM(currentContext, 0, (-1)*frameRect.origin.y*2);


Now run the application again. You should see a nicely laid-out PDF with all our labels mapped.



Adding the Logo


Next, open InvoiceView.xib and add a UIImageView to the upper right for the logo:



Then add this new method into PDFRenderer.m (right before drawPDF):


+(void)drawLogo
{   
NSArray* objects = [[NSBundle mainBundle] loadNibNamed:@"InvoiceView" owner:nil options:nil];

UIView* mainView = [objects objectAtIndex:0];

for (UIView* view in [mainView subviews]) {
if([view isKindOfClass:[UIImageView class]])
{           
UIImage* logo = [UIImage imageNamed:@"ray-logo.png"];
[self drawImage:logo inRect:view.frame];
}
}   
}


This loads the UIImageView from the Xib file in the same way we load the labels. Then it draws the logo onto the PDF using the coordinates of the UIImageView.

Finally, call this method after drawLabels in the drawPDF method:


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

[self drawText:@"Hello World" inFrame:CGRectMake(0, 0, 300, 50)];

[self drawLabels];
[self drawLogo];

// Close the PDF context and write out the contents.
UIGraphicsEndPDFContext();
}


Run the application in the simulator, and you’ll see the logo in the top right corner just as we specified!



Pretty cool how easy it is to visually lay out the PDF with the aid of UIView coordinates, eh?

Drawing the Table


It’s time to add the table that will hold all the invoice information. Our table will be nothing more than an arrangement of vertical and horizontal lines.

In this case, we won’t use the InvoiceView. Instead we’ll use a series of variables like the table height and width, and the row height and column width.

Add the following method to PDFRenderer.m (right before drawPDF):


+(void)drawTableAt:(CGPoint)origin
withRowHeight:(int)rowHeight
andColumnWidth:(int)columnWidth
andRowCount:(int)numberOfRows
andColumnCount:(int)numberOfColumns

{  
for (int i = 0; i <= numberOfRows; i++)
{       
int newOrigin = origin.y + (rowHeight*i);

CGPoint from = CGPointMake(origin.x, newOrigin);
CGPoint to = CGPointMake(origin.x + (numberOfColumns*columnWidth), newOrigin);

[self drawLineFromPoint:from toPoint:to];
}
}


This method draws our horizontal lines. At the beginning of the method, we pass in the values for the starting position of the table, the number of rows and columns, the height of each row and the width of each column.

The loop then goes through the motions for each row, calculating where the row should start and where it should end. Finally, the drawLine:from:to method is called to draw the line. We then add the following code to the drawPDF method:


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

[self drawText:@"Hello World" inFrame:CGRectMake(0, 0, 300, 50)];

[self drawLabels];
[self drawLogo];

int xOrigin = 50;
int yOrigin = 300;

int rowHeight = 50;
int columnWidth = 120;

int numberOfRows = 7;
int numberOfColumns = 4;

[self drawTableAt:CGPointMake(xOrigin, yOrigin) withRowHeight:rowHeight andColumnWidth:columnWidth andRowCount:numberOfRows andColumnCount:numberOfColumns];

// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
}


Run the application in the simulator and you should see the horizontal lines on the PDF.



The next step is to draw the vertical lines. Add another loop to the drawTable method below the first loop:


+(void)drawTableAt:(CGPoint)origin
withRowHeight:(int)rowHeight
andColumnWidth:(int)columnWidth
andRowCount:(int)numberOfRows
andColumnCount:(int)numberOfColumns

{  
for (int i = 0; i <= numberOfRows; i++)
{       
int newOrigin = origin.y + (rowHeight*i);

CGPoint from = CGPointMake(origin.x, newOrigin);
CGPoint to = CGPointMake(origin.x + (numberOfColumns*columnWidth), newOrigin);

[self drawLineFromPoint:from toPoint:to];       
}

for (int i = 0; i <= numberOfColumns; i++)
{       
int newOrigin = origin.x + (columnWidth*i);

CGPoint from = CGPointMake(newOrigin, origin.y);
CGPoint to = CGPointMake(newOrigin, origin.y +(numberOfRows*rowHeight));

[self drawLineFromPoint:from toPoint:to];       
}
}


The second loop in the above code does a similar run through all the columns in the table, calculating the start and end points of each line and drawing the line to the PDF.

If you run the application again, you will see that our table is complete!



But what use is a table without data?

Populating the Table


We’re going to manually add some dummy data to our table using a series of arrays. But you could easily modify this to feed in data inputed by the user.

Add the following method called drawTableDataAt to PDFRenderer.m (right above drawPDF):


+(void)drawTableDataAt:(CGPoint)origin
withRowHeight:(int)rowHeight
andColumnWidth:(int)columnWidth
andRowCount:(int)numberOfRows
andColumnCount:(int)numberOfColumns
{ 
NSArray* headers = [NSArray arrayWithObjects:@"Quantity", @"Description", @"Unit price", @"Total", nil];
NSArray* invoiceInfo1 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo2 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo3 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo4 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];

NSArray* allInfo = [NSArray arrayWithObjects:headers, invoiceInfo1, invoiceInfo2, invoiceInfo3, invoiceInfo4, nil];

for(int i = 0; i < [allInfo count]; i++)
{
NSArray* infoToDraw = [allInfo objectAtIndex:i];

for (int j = 0; j < numberOfColumns; j++)
{

int newOriginX = origin.x + (j*columnWidth);
int newOriginY = origin.y + ((i+1)*rowHeight);

CGRect frame = CGRectMake(newOriginX, newOriginY, columnWidth, rowHeight);

[self drawText:[infoToDraw objectAtIndex:j] inFrame:frame];
}       
}   
}


This code block begins by creating the data that will be in the table. The first array contains the values that will be in the header (first row of the table). The next three arrays contain the values for each row and column of the table.

The final array is a master array containing all the others, effectively making it a two-dimensional array modeling our table data.

After that, there are two nested loops. The outer loop runs through each row and extracts the data for the row.

The inner loop runs through each column and calculates the starting point of the text, depending on its location in the table. It creates a frame for the text and then draws the text in the frame.

Add a call to this new method in the drawPDF method (right before the call to UIGraphicsEndPDFContext):


[self drawTableDataAt:CGPointMake(xOrigin, yOrigin) withRowHeight:rowHeight andColumnWidth:columnWidth andRowCount:numberOfRows andColumnCount:numberOfColumns];


Run the application in the simulator and you will see our table is populated with data.



Looks good, doesn’t it? But let’s make one final tweak: we could use some padding between the table lines and the data itself.

Below is the final state of the drawTableAt method:


+(void)drawTableDataAt:(CGPoint)origin
withRowHeight:(int)rowHeight
andColumnWidth:(int)columnWidth
andRowCount:(int)numberOfRows
andColumnCount:(int)numberOfColumns
{
int padding = 10;

NSArray* headers = [NSArray arrayWithObjects:@"Quantity", @"Description", @"Unit price", @"Total", nil];
NSArray* invoiceInfo1 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo2 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo3 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo4 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];

NSArray* allInfo = [NSArray arrayWithObjects:headers, invoiceInfo1, invoiceInfo2, invoiceInfo3, invoiceInfo4, nil];

for(int i = 0; i < [allInfo count]; i++)
{
NSArray* infoToDraw = [allInfo objectAtIndex:i];

for (int j = 0; j < numberOfColumns; j++)
{           
int newOriginX = origin.x + (j*columnWidth);
int newOriginY = origin.y + ((i+1)*rowHeight);

CGRect frame = CGRectMake(newOriginX + padding, newOriginY + padding, columnWidth, rowHeight);

[self drawText:[infoToDraw objectAtIndex:j] inFrame:frame];
}       
}   
}


Now we can sit back and admire the final invoice PDF, displaying image, table and data. Our work is done… that is, until it’s time for all of us to start billing for RayWenderlich.com. :P



Where to Go From Here?


Here is an example project with all of the code from the above tutorial.

That’s all folks! This tutorial series should have given you an idea of how to use Quartz 2D to generate a PDF displaying results from your app. From here, there are many implementation possibilities!

As I said at the beginning of this tutorial series, going through this project should have also helped you appreciate all of the low-level layout Apple gives us for free in the UIKit.

I look forward to reading your questions and comments in the forum discussion below!



This is a blog post by iOS Tutorial Team member Tope Abayomi, an iOS developer with a passion for easy to use, useful, aesthetically pleasing apps. Here are some of his videos teaching you how to design an app with custom design.

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 2 is a post from: Ray Wenderlich