This tutorial includes the full source code of the Rag Doll Stunt Man project and covers the details on how to make a 2D ragdoll with Cocos2d and Chipmunk.
You can create this app by following along in this post or you can download the complete source code HeyaldaRagDoll.zip and follow along to learn how it works. The md5 digital signature of the HeyaldaRagDoll.zip file is ad2e567dfa1e767f1f53dae4f252c942.
In this tutorial you will learn how to:
- add retina and non-retina display sprite sheets to an app,
- load a sprite sheet into a CCSpriteBatchNode and use it to create sprites for the ragdoll character,
- create a ragdoll physics model in Chipmunk Physics,
- configure which device orientations your Cocos2d game will support,
- enable retina display in a Cocos2d project,
- convert an app from an iPhone app to an iPhone/iPad/iPod universal app, and
- change the pixel format of textures your app uses.
Creating a Sprite Sheet
Creating a sprites sheet is easy if you have the sprites for your game saved as individual PNG files. To create the sprite sheet used in the app discussed in this project, I used Photoshop to create separate images for the head, torso, upper arm, lower arm, upper leg, and lower leg of the rag doll character.
I then saved PNG files for each body part and used Zwoptex to create the PNG sprite sheet and the configuration file which Cocos2d loads to determine how to use the sprite sheet. The configuration file generated by Zwoptex tells Cocos2d the rectangle size and location of each sprite frame in the sprite sheet.
Another detail worth mentioning is how I created the sprite sheet for both retina mode and non-retina mode. I always start by creating the higher resolution retina sprites and then use Apple’s Automator to automatically scale each image file by 50% to create a second set of PNG image files for the non-retina sprite sheet. Zwoptex is then used to create each sprite sheet.
Here are the retina and non-retina versions of the sprite sheet that I created for this project using Zwoptex. You can download them and add them to your project if you are building this app as you follow along to this tutorial.
Here you can download a zip file containing the plist configuraiton files that are used to tell Cocos2d how to select individual sprites after loading the sprite sheet into a texture.
Rag Doll Sprite Sheet Property List Files
To verify the integrity of the files, the MD5 digital signature of the RagDollSpriteSheetPlist.zip file is: c4bddc3e19bb96999b93e100910ef413
Creating the Foundation Xcode Project
You can follow this tutorial by building a new Cocos2d + Chipmunk template project in Xcode or you can download the complete project here. The md5 digital signature of the HeyaldaRagDoll.zip file is ad2e567dfa1e767f1f53dae4f252c942.
At the time of this writeup I am using Xcode 4.3.3 with Cocos2d 2.0 (stable version) that contains Chipmunk 6.0.2. You do not have to have these exact versions to follow along, but if you are using other versions there might be minor things that you have to change to get the app to work the same way as I designed it.
Start by creating a Cocos2d + Chipmunk physics engine project from the Xcode template included with Cocos2d and run it to ensure that your development environment is functioning properly. If you have difficulties with this, check out this tutorial titled Getting Started with Cocos2d and Chimpunk.
After running the Cocos2d + Chipmunk HelloWorld app, you should see something like the following in the simulator or on your device.
This is the foundation for the app that we will be crating in this tutorial. In the template app, the Grossini characters are each modeled by a single rectangle.
I thought it would be funny to replace dancing Grossini characters with ragdoll stunt men. The following images show the app created in this tutorial. The ragdoll stunt men are doing what seems to be some sort of impressive circus stunts. You can tilt the device in different directions to watch the ragdoll men flip, flop, and fall as they pile up and bounce around.
Adding the Ragdoll Sprite Resources to the Project
In order to use any resource in a Cocos2d game, you must first add the resource to each target in the Xcode project that will use the resource.
To add a resource to your project, I recommend using Finder to add the resource inside the Resource folder where the Xcode project is saved.
You can create subfolders in the Resource folder in Finder to organize your resources if you like.
Then you can drag and drop either single files or entire folders from finder into the Xcode Project Navigator and then select which target the resource should be added to.
When prompted to copy the resource say no since you already added it to the project where you want it.
Creating the Ragdoll Classes
For the design of this sample app, I chose to create two classes to model the Ragdoll, one called RagDollSprites and the other called RagDollCharacter.
When creating physics models for characters, it is often a good idea to separate out the sprites from the physics model. This way the physics model can control the sprite or you can choose to have some other game logic control the sprites for other purposes if need be.
One example of having a separate object control the sprites is the case of replaying a physics simulation. A class could be created to record the position and rotation of each sprite in the ragdoll at a rate of 60 times per second (or whatever your games frame rate is). That data could then be used by a separate class to replay the ragdoll physics simulation by controlling each sprite in the sprite class.
RagDollSprites Class Details
The purpose of the RagDollSprites class is to:
1) manage the creation of the sprites needed for each ragdoll,
2) provide a simple interface to those sprites, and
3) manage cleaning up the sprites when they are no longer needed.
For simple games where most of the characters sprites are contained on one or a few sprite sheets, I prefer to have the main scene or a game manager load the sprite sheets as CCSpriteBatchNodes and add the sprite frames defined in the sprite sheet plist file to the CCSpriteFrameCache object.
Then all game objects that are created can be passed a reference to the sprite batch node that will draw its sprites, create the sprites the character needs from the CCSpriteFrameCache and add the newly created sprites to the CCSpriteBatchNode.
Then when each character is destroyed, each character is responsible for removing its sprites from the CCSpriteBatchNode and then cleans up any other resources that it might have been using.
In this sample app, I have employed the above described technique of loading a sprite sheet. After the sprite sheet is loaded, a reference to it is passed to each character (each rag doll) that should add its sprites to it.
The following code shows the interface of the RagDollSprites class.
If you are creating this app as you follow along, then in File menu add a new class using the Cocos2d template and create it as a subclass of NSObject.
Q: Why choose NSObject instead of CCNode for the class of the characters? A: The reason for choosing NSObject is that the character classes created in this sample app only manages Cocos2d objects and is not directly added as children to any Cocos2d objects, so there is no need to make them a subclass of CCNode. If for some reason as you build your character you find that you want to add CCNode, CCLayer, or CCSprite behavior directly to a character class, it is very easy to change the type of subclass by just replacing NSObject in the interface with whatever parent class you need.
#import <Foundation/Foundation.h> #import "cocos2d.h" @interface RagDollSprites : NSObject { CCSprite* head; CCSprite* torso; CCSprite* upperArm1; CCSprite* lowerArm1; CCSprite* upperLeg1; CCSprite* lowerLeg1; CCSprite* upperArm2; CCSprite* lowerArm2; CCSprite* upperLeg2; CCSprite* lowerLeg2; CCSpriteBatchNode* spriteBatchNode; // Weak reference } @property (nonatomic, retain) CCSprite* head; @property (nonatomic, retain) CCSprite* torso; @property (nonatomic, retain) CCSprite* upperArm1; @property (nonatomic, retain) CCSprite* lowerArm1; @property (nonatomic, retain) CCSprite* upperLeg1; @property (nonatomic, retain) CCSprite* lowerLeg1; @property (nonatomic, retain) CCSprite* upperArm2; @property (nonatomic, retain) CCSprite* lowerArm2; @property (nonatomic, retain) CCSprite* upperLeg2; @property (nonatomic, retain) CCSprite* lowerLeg2; -(id) initWithSpriteBatchNode:(CCSpriteBatchNode*)batchNode; @end
Each ragdoll character has a head, torso, two arms and two legs. The arms and legs each consist of and upper and lower component.
Note that the sprites are all defined as properties that are directly accessible by the whatever class will be controlling the sprites.
The following code shows the entire RagDollSprites class implementation.
#import "RagDollSprites.h" @implementation RagDollSprites @synthesize head; @synthesize torso; @synthesize upperArm1; @synthesize lowerArm1; @synthesize upperLeg1; @synthesize lowerLeg1; @synthesize upperArm2; @synthesize lowerArm2; @synthesize upperLeg2; @synthesize lowerLeg2; -(id) initWithSpriteBatchNode:(CCSpriteBatchNode*)batchNode { self = [super init]; if (self) { spriteBatchNode = batchNode; self.head = [CCSprite spriteWithSpriteFrameName:@"head.png"]; self.torso = [CCSprite spriteWithSpriteFrameName:@"torso.png"]; self.upperArm1 = [CCSprite spriteWithSpriteFrameName:@"upperArm.png"]; self.lowerArm1 = [CCSprite spriteWithSpriteFrameName:@"lowerArm.png"]; self.upperLeg1 = [CCSprite spriteWithSpriteFrameName:@"upperLeg.png"]; self.lowerLeg1 = [CCSprite spriteWithSpriteFrameName:@"lowerLeg.png"]; self.upperArm2 = [CCSprite spriteWithSpriteFrameName:@"upperArm.png"]; self.lowerArm2 = [CCSprite spriteWithSpriteFrameName:@"lowerArm.png"]; self.upperLeg2 = [CCSprite spriteWithSpriteFrameName:@"upperLeg.png"]; self.lowerLeg2 = [CCSprite spriteWithSpriteFrameName:@"lowerLeg.png"]; [spriteBatchNode addChild:head z:-1]; [spriteBatchNode addChild:torso z:0]; [spriteBatchNode addChild:upperArm1 z:2]; [spriteBatchNode addChild:lowerArm1 z:1]; [spriteBatchNode addChild:upperLeg1 z:-1]; [spriteBatchNode addChild:lowerLeg1 z:-2]; [spriteBatchNode addChild:upperArm2 z:-10]; [spriteBatchNode addChild:lowerArm2 z:-11]; [spriteBatchNode addChild:upperLeg2 z:-8]; [spriteBatchNode addChild:lowerLeg2 z:-9]; } return self; } -(void) dealloc { // Remove the sprites from the CCSpriteBatchNode that they were added to. if (spriteBatchNode != nil) { [spriteBatchNode removeChild:head cleanup:YES]; [spriteBatchNode removeChild:torso cleanup:YES]; [spriteBatchNode removeChild:upperArm1 cleanup:YES]; [spriteBatchNode removeChild:lowerArm1 cleanup:YES]; [spriteBatchNode removeChild:upperLeg1 cleanup:YES]; [spriteBatchNode removeChild:lowerLeg1 cleanup:YES]; [spriteBatchNode removeChild:upperArm2 cleanup:YES]; [spriteBatchNode removeChild:lowerArm2 cleanup:YES]; [spriteBatchNode removeChild:upperLeg2 cleanup:YES]; [spriteBatchNode removeChild:lowerLeg2 cleanup:YES]; } self.head = nil; self.torso = nil; self.upperArm1 = nil; self.lowerArm1 = nil; self.upperLeg1 = nil; self.lowerLeg1 = nil; self.upperArm2 = nil; self.lowerArm2 = nil; self.upperLeg2 = nil; self.lowerLeg2 = nil; [super dealloc]; } @end
Sprite zOrder
Take a look at how the zOrder is set as each sprite is added to the CCSpriteBatchNode. The z value assigns the zOrder of each sprite to tell it’s parent, in this case the CCSpriteBatchNode object, when to draw it relative to other sprites owned by the parent. The larger positive values are drawn first followed by the lessor and negative values of z.
I start with the torso being set to zero because I want to make all other sprites relative to the torso. This could be some other base value if you wanted to have the rag doll character be in front of or behind other sprites that are on the same texture owned by the CCSpriteBatchNode that is passed to the character.
The head is drawn behind the torso with a z of -1 so that the bottom of the neck is hidden.
The arms are made of upper and lower components. One arm and one leg is in front of the torso and one arm and one leg is behind the torso.
Each lower arm and lower leg is drawn behind the upper component that it is attached to.
This is the general thought process I went through when determining what z values to assign each sprite to properly position the depth of each ragdoll body part.
Removing Sprites From the Sprite Sheet
Take a look at the code in the dealloc method. If the sprite batch node was assigned to this character, then the charter assumes that the sprite batch node is still around when the character is destroyed.
The CCSpriteBatchNode also has retained a reference to each sprite that was added to it. The only reason for creating the property of type retain and not of type assign is that it is possible that there could be a race condition when tearing down the components of a game and if for some reason the CCSpriteBatchNode of the character is removed before the character, and one of the character sprites is accessed, then a bad access crash could occur. Technically this should not happen in most cases, but since this way of building a character kind of bends the standard way of working with Cocos2d, it is an added bit of insurance against a crash that does not cost much. And when ARC is fully implemented in Cocos2d, this insurance will cost nothing.
Removing the sprites from the batch node is not necessary in this app because when the entire scene is destroyed the CCSpriteBatchNode is also destroyed. However, in a game that might add and remove many characters while the game is being played, it is a good idea to have the characters clean up after themselves.
You could also run into undesirable lingering sprites that the sprite batch node has retained and the game character that created the sprite has been destroyed bud did not remove its sprites from the CCSpriteBatchNode that it uses. These sprites will continue to be drawn as they were last left by the character class until the CCSpriteBatchNode object is destroyed. So there are multiple benefits to practicing good memory management house keeping in character design.
RagDollCharacter Class Details
The RagDollCharacter class is designed to own the physics model for each ragdoll character and owns an instance of the RagDollSprites class so that it can tell the ragdoll sprites how to move when the physics simulation runs.
It is kind of surprising how much code is required to create a simple ragdoll character in the Chipmunk physics engine. If you want a good looking ragdoll, you need to at least have separate bodies for the head, torso, upper arms, lower arms, upper legs, and lower legs. This results in needing to create ten physics bodies, ten physics shapes, and nineteen constraints that define how the bodies can move relative to each other. This sample app also adds an extra constraint to the head that is a cpDampedRotarySpring. This is a damped rotary spring constraint that is setup to create a bobble head effect.
Here is RagDollCharacter interface code. If you are building this app while following along, create a class called RagDollCharacter that is a subclass of NSObject.
#import <Foundation/Foundation.h> #import "chipmunk.h" #import "cocos2d.h" @class RagDollSprites; @interface RagDollCharacter : NSObject { cpSpace* physicsSpace; // Weak Reference RagDollSprites* ragDollSprites; // --- Bodies ------- cpBody* headBody; cpBody* torsoBody; cpBody* upperArmBody1; cpBody* lowerArmBody1; cpBody* upperLegBody1; cpBody* lowerLegBody1; cpBody* upperArmBody2; cpBody* lowerArmBody2; cpBody* upperLegBody2; cpBody* lowerLegBody2; // --- Shapes --------- cpShape* headShape; cpShape* torsoShape; cpShape* upperArmShape1; cpShape* lowerArmShape1; cpShape* upperLegShape1; cpShape* lowerLegShape1; cpShape* upperArmShape2; cpShape* lowerArmShape2; cpShape* upperLegShape2; cpShape* lowerLegShape2; // --- Constraints ------- // Head cpConstraint* headToTorsoPivotJoint; cpConstraint* headRotLimit; cpConstraint* headDampedRotSpringJoint; // Leg cpConstraint* torsoToUpperLegPivotJoint1; cpConstraint* upperLegToLowerLegPivotJoint1; cpConstraint* torsoToUpperLegPinJoint2; cpConstraint* upperLegToLowerLegPinJoint2; // Arm cpConstraint* upperArmToLowerArmPivotJoint1; cpConstraint* upperArmToTorsoPivotJoint1; cpConstraint* upperArmToLowerArmPinJoint2; cpConstraint* upperArmToTorsoPinJoint2; // Sholder, hip, elbow, and knee rotary limit joints cpConstraint* sholderRotLimit1; cpConstraint* hipRotLimit1; cpConstraint* elbowRotLimit1; cpConstraint* kneeRotLimit1; cpConstraint* sholderRotLimit2; cpConstraint* hipRotLimit2; cpConstraint* elbowRotLimit2; cpConstraint* kneeRotLimit2; // --- Parameters to adjust how the ragdoll can move float elbowBendMinimumNormal; float elbowBendMaxNormal; float kneeBendMinimuNormal; float waistBendMinNormal; float waistBendMaxNormal; float sholderBendMinNormal; float sholderBendMaxNormal; float ragDollElbowMin; float ragDollElbowMax; float ragDollKneeMin; float ragDollKneeMax; float ragDollWaistMin; float ragDollWaistMax; } @property (nonatomic, retain) RagDollSprites* ragDollSprites; -(id) initWithPosition:(CGPoint)p withSpriteBatchNode:(CCSpriteBatchNode*)spriteBatchNode withChipmunkPhysicsSpace:(cpSpace*)_physicsSpace; -(void) updateSprites; @end
This character has two methods defined. One for creating it and one to synchronize the updating of the sprites with the main game loop. While there are other methods to synchronize each character’s sprite updates with the main game loop, I prefer to have a selector that is called for each character so that I can pass data to the character when it is updated if need be. It also makes it easier to pause some components of the game and not others, something that can be problematic if you have many update methods registered with the CCScheduler.
As you will see in the RagDollCharacter implementation below, all constraints that hold the bodies together are cpPinJoint’s and the range of motion of each if constrained with cpRotaryLimitJoint’s.
The parameters to adjust the range of motion of the cpRotaryLimtJoint’s are all floating point values that measure angles in radians, the default way that Chipmunk measure angles. Note that the default way that angles are measured in Cocos2d is in degrees.
Since the RagDollCharacter implementation is over 500 lines long, I am going to break the code down into chunks below. The order of the chunks of code below are the same as in the RagDollCharacter.m implementation file, so if you are building this projects step by step you can copy each chunk in order into the RagDollCharacter.m file to complete the RagDollCharacter class.
The first code in the RagDollCharacter implementation file is the following.
#import "RagDollCharacter.h" #import "RagDollSprites.h" #define kRagDollCollisionType 1 static NSUInteger ragDollCollisionGroups = 1; @interface RagDollCharacter (private) -(void) initRagdollPhysicsWithPosition:(CGPoint)p; -(void) destroyRagdollPhysicsObjects; @end
This code imports the RagDollCharacter class interface, the RagDollSprites interface, defines the collision type and collision group for the all ragdoll Chipmunk physics bodies, and includes the private interface with selectors to create and destroy the ragdoll physics model.
Note the ragDollCollisionGroups static unsigned integer variable. Since this variable is static it becomes a class variable. This essentially means that its scope is all instances of this type of class as well as the class itself, which in Objective-C is itself technically an object. This variable is incremented each time a new RagDollCharacter is created so that each RagDollCharacter’s shapes will have their own Chipmunk collision group. This results in the shapes that make up the 10 parts of each ragdoll’s body not colliding with each other, but they will collide with any shape on any other ragdoll. If all ragdolls had the same collision group, then non of them would collide with each other. Note that this variable starts with a value of 1. The reason for this is that any shape with a collision group of zero will collide with all other shapes.
Next is the init and dealloc methods of the RagDollCharacter class.
The init method is passed the position to place the ragdoll, the CCSpriteBatchNode that the character will add it’s sprites to, and the cpSpace that the character will add its Chipmunk phyiscs objects to.
The init method then saves a weak reference to the physics space for later use, creates a single instance of the RagDollSprites object, calls the selector to create the ragdoll physics model, and then increments the ragDollCollisionGroups static variable so that each ragdoll gets its own collision group.
In the dealloc method the destroyRagdollPhysicsObjects selector is called and ragDollSprites instance is released and set to nil.
@implementation RagDollCharacter @synthesize ragDollSprites; -(id) initWithPosition:(CGPoint)p withSpriteBatchNode:(CCSpriteBatchNode*)spriteBatchNode withChipmunkPhysicsSpace:(cpSpace*)_physicsSpace { self = [super init]; if (self){ physicsSpace = _physicsSpace; self.ragDollSprites = [[[RagDollSprites alloc] initWithSpriteBatchNode:spriteBatchNode] autorelease]; [self initRagdollPhysicsWithPosition:p]; // Each ragdoll gets its own collision group, so ++ragDollCollisionGroups; } return self; } -(void) dealloc { [self destroyRagdollPhysicsObjects]; self.ragDollSprites = nil; [super dealloc]; }
The next block of code creates the physics model for the RagDollCharacter instance.
The size and shape of each ragdoll component are based on the rectangles of the sprites that were loaded into the RagDollSprites class.
I added a desiredScale variable that can be set to scale all of the sprites and physics objects. In my opinion the simulation is funnier to watch when you set the scale to about 0.2 and see the little ragdoll bodies fall and pile up in the simulation.
The masses of the bodies is hard coded into this method.
When creating the collision shapes, I use a fudge factor called shapeScaleFactor that scales down the width and height of each box shape that is created to model each body component. The reason for this is that it looks more natural if the sprites slightly overlap before they collide verses having them collide when they are almost touching. You can experiment by changing the value of shapeScaleFactor to see the effect of growing or shrinking the collision shapes while keeping the sprite size constant.
I chose to make the bodies be quite bouncy by assigning a value of 1 to the elasticity of each shape.
The friction coefficient of each shape was set to 0.2 to make the bodies have less friction and slide around a bit more.
This method then uses cpPinJoints to connect the ragdoll components together and rotary limit joints to constrain their movement. The position of the pin joints on each body has been hard coded into this method and are offset from the ends of each shape by a percentage of the size of each body part. These could be adjusted to for example move the head down lower on the torso, move the upper leg forward some relative to the torso, etc.
Note that it is important to at least roughly position the bodies before the simulation begins. The reason for this is that when the simulation starts the constraints will attempt to correct the bodies to acceptable relative positions and angles. The initial positions selected in this sample app are slightly off so that the body parts expand out to give the body some initial energy and movement. If the bodies initial positions and rotations are significantly far away from allowable relative positions and rotations, Chipmunk may over compensate to try to correct them. This over compensation can be quite jarring and even result in a divergent solution in the numerical integration process resulting in what may seem like a nuclear explosion in your physics world simulation. In short, be sure to set the initial position of bodies in a physics simulation.
Edit to this post on July 19, 2012: Originally I connected the bodies of the ragdoll in this sample app together with cpPinJoints. Later I was working on a separate project with Chipmunk Pro. Chipmunk Pro warned me in the debug log that if the distance between the anchor points of a cpPinJoint are zero, then using a cpPivot joint instead of the cpPinJoint would be more stable. I then updated this sample app to use cpPivotJoint instead of cpPinJoints.
-(void) initRagdollPhysicsWithPosition:(CGPoint)p { float desiredScale = 0.35; ragDollSprites.head.scale = desiredScale; ragDollSprites.torso.scale = desiredScale; ragDollSprites.upperArm1.scale = desiredScale; ragDollSprites.lowerArm1.scale = desiredScale; ragDollSprites.upperArm2.scale = desiredScale; ragDollSprites.lowerArm2.scale = desiredScale; ragDollSprites.upperLeg1.scale = desiredScale; ragDollSprites.lowerLeg1.scale = desiredScale; ragDollSprites.upperLeg2.scale = desiredScale; ragDollSprites.lowerLeg2.scale = desiredScale; // --------------------------------------------------------- // Determine the size of the physics bodies and shapes based on the sprite shapes and a desired scale. // --------------------------------------------------------- CGSize headSize = CGSizeMake( ragDollSprites.head.contentSize.width * desiredScale, ragDollSprites.head.contentSize.height * desiredScale); CGSize torsoSize = CGSizeMake( ragDollSprites.torso.contentSize.width * desiredScale, ragDollSprites.torso.contentSize.height * desiredScale); CGSize upperArmSize = CGSizeMake( ragDollSprites.upperArm1.contentSize.width * desiredScale, ragDollSprites.upperArm1.contentSize.height * desiredScale); CGSize lowerArmSize = CGSizeMake( ragDollSprites.lowerArm1.contentSize.width * desiredScale, ragDollSprites.lowerArm1.contentSize.height * desiredScale); CGSize upperLegSize = CGSizeMake( ragDollSprites.upperLeg1.contentSize.width * desiredScale, ragDollSprites.upperLeg1.contentSize.height * desiredScale); CGSize lowerLegSize = CGSizeMake( ragDollSprites.lowerLeg1.contentSize.width * desiredScale, ragDollSprites.lowerLeg1.contentSize.height * desiredScale); // --------------------------------------------------------- // Create the moment of inertia for each physics body. // --------------------------------------------------------- cpFloat momentHead = cpMomentForBox(5, headSize.width, headSize.height ) ; cpFloat momentTorso = cpMomentForBox(20, torsoSize.width, torsoSize.height) ; cpFloat momentUpperArm = cpMomentForBox(15, upperArmSize.width, upperArmSize.height) ; cpFloat momentLowerArm = cpMomentForBox(15, lowerArmSize.width, lowerArmSize.height) ; cpFloat momentUpperLeg = cpMomentForBox(15, upperLegSize.width, upperLegSize.height) ; cpFloat momentLowerLeg = cpMomentForBox(15, lowerLegSize.width, lowerLegSize.height) ; // --------------------------------------------------------- // Create the bodies // --------------------------------------------------------- headBody = cpBodyNew(5, momentHead); headBody->data = self; cpSpaceAddBody(physicsSpace, headBody); torsoBody = cpBodyNew(20, momentTorso); torsoBody->data = self; cpSpaceAddBody(physicsSpace, torsoBody); upperArmBody1 = cpBodyNew(15, momentUpperArm); upperArmBody1->data = self; cpSpaceAddBody(physicsSpace, upperArmBody1); lowerArmBody1 = cpBodyNew(15, momentLowerArm); lowerArmBody1->data = self; cpSpaceAddBody(physicsSpace, lowerArmBody1); upperLegBody1 = cpBodyNew(15, momentUpperLeg); upperLegBody1->data = self; cpSpaceAddBody(physicsSpace, upperLegBody1); lowerLegBody1 = cpBodyNew(15, momentLowerLeg); lowerLegBody1->data = self; cpSpaceAddBody(physicsSpace, lowerLegBody1); upperArmBody2 = cpBodyNew(15, momentUpperArm); upperArmBody2->data = self; cpSpaceAddBody(physicsSpace, upperArmBody2); lowerArmBody2 = cpBodyNew(15, momentLowerArm); lowerArmBody2->data = self; cpSpaceAddBody(physicsSpace, lowerArmBody2); upperLegBody2 = cpBodyNew(15, momentUpperLeg); upperLegBody2->data = self; cpSpaceAddBody(physicsSpace, upperLegBody2); lowerLegBody2 = cpBodyNew(15, momentLowerLeg); lowerLegBody2->data = self; cpSpaceAddBody(physicsSpace, lowerLegBody2); // --------------------------------------------------------- // Create the shapes and attach them to the bodies // --------------------------------------------------------- // Used to scale the size of the shape relative to the sprite. // Use a value less than on to shrink the size of the physics shape relative to the sprite size. float shapeScaleFactor = 0.75; headShape = cpBoxShapeNew(headBody, headSize.width* shapeScaleFactor, headSize.height * shapeScaleFactor); torsoShape = cpBoxShapeNew(torsoBody, torsoSize.width* shapeScaleFactor , torsoSize.height* shapeScaleFactor ); upperArmShape1 = cpBoxShapeNew(upperArmBody1, upperArmSize.width * shapeScaleFactor, upperArmSize.height * shapeScaleFactor); lowerArmShape1 = cpBoxShapeNew(lowerArmBody1, lowerArmSize.width * shapeScaleFactor, lowerArmSize.height * shapeScaleFactor); upperLegShape1 = cpBoxShapeNew(upperLegBody1, upperLegSize.width * shapeScaleFactor, upperLegSize.height * shapeScaleFactor); lowerLegShape1 = cpBoxShapeNew(lowerLegBody1, lowerLegSize.width* shapeScaleFactor , lowerLegSize.height * shapeScaleFactor); headShape->e = 1; headShape->u = 0.15; headShape->collision_type = kRagDollCollisionType; headShape->group = ragDollCollisionGroups; headShape->data = nil; torsoShape->e = 1; torsoShape->u = 0.15; torsoShape->collision_type = kRagDollCollisionType; torsoShape->group = ragDollCollisionGroups; torsoShape->data = nil; upperArmShape1->e = 1; upperArmShape1->u = 0.15; upperArmShape1->collision_type = kRagDollCollisionType; upperArmShape1->group = ragDollCollisionGroups; upperArmShape1->data = nil; lowerArmShape1->e = 1; lowerArmShape1->u = 0.15; lowerArmShape1->collision_type = kRagDollCollisionType; lowerArmShape1->group = ragDollCollisionGroups; lowerArmShape1->data = nil; upperLegShape1->e = 1; upperLegShape1->u = 0.15; upperLegShape1->collision_type = kRagDollCollisionType; upperLegShape1->group = ragDollCollisionGroups; upperLegShape1->data = nil; lowerLegShape1->e = 1; lowerLegShape1->u = 0.15; lowerLegShape1->collision_type = kRagDollCollisionType; lowerLegShape1->group = ragDollCollisionGroups; lowerLegShape1->data = nil; cpSpaceAddShape(physicsSpace, headShape); cpSpaceAddShape(physicsSpace, torsoShape); cpSpaceAddShape(physicsSpace, upperArmShape1); cpSpaceAddShape(physicsSpace, lowerArmShape1); cpSpaceAddShape(physicsSpace, upperLegShape1); cpSpaceAddShape(physicsSpace, lowerLegShape1); upperArmShape2 = cpBoxShapeNew(upperArmBody2, upperArmSize.width * shapeScaleFactor, upperArmSize.height * shapeScaleFactor); lowerArmShape2 = cpBoxShapeNew(lowerArmBody2, lowerArmSize.width * shapeScaleFactor, lowerArmSize.height * shapeScaleFactor); upperLegShape2 = cpBoxShapeNew(upperLegBody2, upperLegSize.width * shapeScaleFactor, upperLegSize.height * shapeScaleFactor); lowerLegShape2 = cpBoxShapeNew(lowerLegBody2, lowerLegSize.width * shapeScaleFactor, lowerLegSize.height * shapeScaleFactor); upperArmShape2->e = 1; upperArmShape2->u = 0.15; upperArmShape2->collision_type = kRagDollCollisionType; upperArmShape2->group = ragDollCollisionGroups; upperArmShape2->data = nil; lowerArmShape2->e = 1; lowerArmShape2->u = 0.15; lowerArmShape2->collision_type = kRagDollCollisionType; lowerArmShape2->group = ragDollCollisionGroups; lowerArmShape2->data = nil; upperLegShape2->e = 1; upperLegShape2->u = 0.15; upperLegShape2->collision_type = kRagDollCollisionType; upperLegShape2->group = ragDollCollisionGroups; upperLegShape2->data = nil; lowerLegShape2->e = 1; lowerLegShape2->u = 0.15; lowerLegShape2->collision_type = kRagDollCollisionType; lowerLegShape2->group = ragDollCollisionGroups; lowerLegShape2->data = nil; cpSpaceAddShape(physicsSpace, upperArmShape2); cpSpaceAddShape(physicsSpace, lowerArmShape2); cpSpaceAddShape(physicsSpace, upperLegShape2); cpSpaceAddShape(physicsSpace, lowerLegShape2); // --------------------------------------------------------------- // Connect the bodies together with pin joints. // --------------------------------------------------------------- CGPoint torsoAnchorForhead = cpv(0, torsoSize.height * 0.65 * 0.85); CGPoint headAnchorForTorso = cpv(-headSize.width * 0.1, -headSize.height * 0.15); headToTorsoPivotJoint = cpPivotJointNew2(torsoBody, headBody, torsoAnchorForhead, headAnchorForTorso); cpSpaceAddConstraint(physicsSpace, headToTorsoPivotJoint); CGPoint torsoAnchorForUppperArm = cpv(0, torsoSize.height * 0.5 * 0.85); CGPoint uppperArmAnchorForTorso = cpv(0, upperArmSize.height * 0.5 * 0.95); upperArmToTorsoPivotJoint1 = cpPivotJointNew2(torsoBody, upperArmBody1, torsoAnchorForUppperArm, uppperArmAnchorForTorso); cpSpaceAddConstraint(physicsSpace, upperArmToTorsoPivotJoint1); CGPoint torsoAnchorForUppperLeg = cpv(-torsoSize.width * 0.2, -torsoSize.height * 0.5 * 0.85); CGPoint uppperLegAnchorForTorso = cpv(0, upperLegSize.height * 0.5 * 0.95); torsoToUpperLegPivotJoint1 = cpPivotJointNew2(torsoBody, upperLegBody1, torsoAnchorForUppperLeg, uppperLegAnchorForTorso); cpSpaceAddConstraint(physicsSpace, torsoToUpperLegPivotJoint1); CGPoint lowerArmAnchorForUpperArm = cpv(0, lowerArmSize.height * 0.5 * 0.95); CGPoint uppperArmAnchorForLowerArm = cpv(0, -upperArmSize.height * 0.5 * 0.95); upperArmToLowerArmPivotJoint1 = cpPivotJointNew2(upperArmBody1, lowerArmBody1, uppperArmAnchorForLowerArm, lowerArmAnchorForUpperArm); cpSpaceAddConstraint(physicsSpace, upperArmToLowerArmPivotJoint1); CGPoint lowerLegAnchorForupperLeg = cpv(lowerLegSize.width * 0.1, lowerLegSize.height * 0.5 * 0.95); CGPoint uppperLegAnchorForLowerLeg = cpv(0, -upperLegSize.height * 0.5 * 0.85); upperLegToLowerLegPivotJoint1 = cpPivotJointNew2(upperLegBody1, lowerLegBody1, uppperLegAnchorForLowerLeg, lowerLegAnchorForupperLeg); cpSpaceAddConstraint(physicsSpace, upperLegToLowerLegPivotJoint1); upperArmToTorsoPinJoint2 = cpPivotJointNew2(torsoBody, upperArmBody2, torsoAnchorForUppperArm, uppperArmAnchorForTorso); cpSpaceAddConstraint(physicsSpace, upperArmToTorsoPinJoint2); torsoToUpperLegPinJoint2 = cpPivotJointNew2(torsoBody, upperLegBody2, torsoAnchorForUppperLeg, uppperLegAnchorForTorso); cpSpaceAddConstraint(physicsSpace, torsoToUpperLegPinJoint2); upperArmToLowerArmPinJoint2 = cpPivotJointNew2(upperArmBody2, lowerArmBody2, uppperArmAnchorForLowerArm, lowerArmAnchorForUpperArm); cpSpaceAddConstraint(physicsSpace, upperArmToLowerArmPinJoint2); upperLegToLowerLegPinJoint2 = cpPivotJointNew2(upperLegBody2, lowerLegBody2, uppperLegAnchorForLowerLeg, lowerLegAnchorForupperLeg); cpSpaceAddConstraint(physicsSpace, upperLegToLowerLegPinJoint2); // --------------------------------------------------------------- // Set the rough position of the parts // --------------------------------------------------------------- headBody->p = ccp(p.x, p.y + torsoSize.height * 0.5); torsoBody->p = p; upperArmBody1->p = ccp(p.x, p.y + torsoSize.height * 0.5);; lowerArmBody1->p = ccp(p.x, p.y + torsoSize.height * 0.25);; upperLegBody1->p = ccp(p.x, p.y - torsoSize.height * 0.5);; lowerLegBody1->p = ccp(p.x, p.y - torsoSize.height );; upperArmBody2->p = upperArmBody1->p; lowerArmBody2->p = lowerArmBody1->p; upperLegBody2->p = upperLegBody1->p; lowerLegBody2->p = lowerLegBody1->p; // ---------------------------------------------------------------- // Add the rotary limit joints // ---------------------------------------------------------------- // Elbow elbowBendMinimumNormal = M_PI_4; kneeBendMinimuNormal = -M_PI_4; elbowBendMaxNormal = M_PI_2; elbowRotLimit1 = cpRotaryLimitJointNew(upperArmBody1, lowerArmBody1, elbowBendMinimumNormal, elbowBendMaxNormal); cpSpaceAddConstraint(physicsSpace, elbowRotLimit1); elbowRotLimit2 = cpRotaryLimitJointNew(upperArmBody2, lowerArmBody2, elbowBendMinimumNormal, elbowBendMaxNormal); cpSpaceAddConstraint(physicsSpace, elbowRotLimit2); // Knee kneeRotLimit1 = cpRotaryLimitJointNew(upperLegBody1, lowerLegBody1, kneeBendMinimuNormal , 0); cpSpaceAddConstraint(physicsSpace, kneeRotLimit1); kneeRotLimit2 = cpRotaryLimitJointNew(upperLegBody2, lowerLegBody2, kneeBendMinimuNormal , 0); cpSpaceAddConstraint(physicsSpace, kneeRotLimit2); // Hip waistBendMinNormal = 0; waistBendMaxNormal = M_PI_2; hipRotLimit1 = cpRotaryLimitJointNew(torsoBody, upperLegBody1, waistBendMinNormal, waistBendMaxNormal); cpSpaceAddConstraint(physicsSpace, hipRotLimit1); hipRotLimit2 = cpRotaryLimitJointNew(torsoBody, upperLegBody2, waistBendMinNormal, waistBendMaxNormal); cpSpaceAddConstraint(physicsSpace, hipRotLimit2); // Sholder sholderBendMinNormal = 0; sholderBendMaxNormal = M_PI_2; sholderRotLimit1 = cpRotaryLimitJointNew(torsoBody, upperArmBody1, sholderBendMinNormal, sholderBendMaxNormal); cpSpaceAddConstraint(physicsSpace, sholderRotLimit1); sholderRotLimit2 = cpRotaryLimitJointNew(torsoBody, upperArmBody2, sholderBendMinNormal, sholderBendMaxNormal); cpSpaceAddConstraint(physicsSpace, sholderRotLimit2); // Head headRotLimit = cpRotaryLimitJointNew(torsoBody, headBody, M_PI_4 * 0.5 , M_PI_4); cpSpaceAddConstraint(physicsSpace, headRotLimit); headDampedRotSpringJoint = cpDampedRotarySpringNew(headBody, torsoBody, 0, 1000000, 1000); cpSpaceAddConstraint(physicsSpace, headDampedRotSpringJoint); }
The next chunk of code is called by the dealloc method of the RagDollCharacter class. It is used to remove all of the constraints, shapes, and bodies from the physics world to enure that Chipmunk memory is cleaned up nicely.
When tearing down physics models, I always start with removing constraints, then shapes, then bodies. I think I saw this recommended in the Chipmunk documentation but I am not certain.
The important thing to note here is that any Chipmunk object that you create that has the word New in it requires that you clean up its memory.
Sometimes the chipmunk ittereators work great, for example, if you wanted to itterate over all bodies in the current simulation and remove and free them along with their constraints and shapes. However, I usually manage this per object so that characters can be added and removed and practice good memory use of freeing memory when it is no longer needed.
-(void) destroyRagdollPhysicsObjects { // ------------------------------------------ // Remove and free all of the constraints. // ------------------------------------------ cpSpaceRemoveConstraint(physicsSpace, headToTorsoPinJoint); cpConstraintFree(headToTorsoPinJoint); cpSpaceRemoveConstraint(physicsSpace, upperLegToLowerLegPinJoint1); cpConstraintFree(upperLegToLowerLegPinJoint1); cpSpaceRemoveConstraint(physicsSpace, torsoToUpperLegPinJoint1); cpConstraintFree(torsoToUpperLegPinJoint1); cpSpaceRemoveConstraint(physicsSpace, upperArmToLowerArmPinJoint1); cpConstraintFree(upperArmToLowerArmPinJoint1); cpSpaceRemoveConstraint(physicsSpace, upperArmToTorsoPinJoint1); cpConstraintFree(upperArmToTorsoPinJoint1); cpSpaceRemoveConstraint(physicsSpace, headDampedRotSpringJoint); cpConstraintFree(headDampedRotSpringJoint); cpSpaceRemoveConstraint(physicsSpace, kneeRotLimit1); cpConstraintFree(kneeRotLimit1); cpSpaceRemoveConstraint(physicsSpace, hipRotLimit1); cpConstraintFree(hipRotLimit1); cpSpaceRemoveConstraint(physicsSpace, elbowRotLimit1); cpConstraintFree(elbowRotLimit1); cpSpaceRemoveConstraint(physicsSpace, sholderRotLimit1); cpConstraintFree(sholderRotLimit1); cpSpaceRemoveConstraint(physicsSpace, headRotLimit); cpConstraintFree(headRotLimit); cpSpaceRemoveConstraint(physicsSpace, upperArmToLowerArmPinJoint2); cpConstraintFree(upperArmToLowerArmPinJoint2); cpSpaceRemoveConstraint(physicsSpace, upperArmToTorsoPinJoint2); cpConstraintFree(upperArmToTorsoPinJoint2); cpSpaceRemoveConstraint(physicsSpace, torsoToUpperLegPinJoint2); cpConstraintFree(torsoToUpperLegPinJoint2); cpSpaceRemoveConstraint(physicsSpace, upperLegToLowerLegPinJoint2); cpConstraintFree(upperLegToLowerLegPinJoint2); cpSpaceRemoveConstraint(physicsSpace, sholderRotLimit2); cpConstraintFree(sholderRotLimit2); cpSpaceRemoveConstraint(physicsSpace, hipRotLimit2); cpConstraintFree(hipRotLimit2); cpSpaceRemoveConstraint(physicsSpace, elbowRotLimit2); cpConstraintFree(elbowRotLimit2); cpSpaceRemoveConstraint(physicsSpace, kneeRotLimit2); cpConstraintFree(kneeRotLimit2); // ------------------------------------------ // Remove and free all of the shapes // ------------------------------------------ cpSpaceRemoveShape(physicsSpace, headShape); cpShapeFree(headShape); cpSpaceRemoveShape(physicsSpace, torsoShape); cpShapeFree(torsoShape); cpSpaceRemoveShape(physicsSpace, upperArmShape1); cpShapeFree(upperArmShape1); cpSpaceRemoveShape(physicsSpace, lowerArmShape1); cpShapeFree(lowerArmShape1); cpSpaceRemoveShape(physicsSpace, upperLegShape1); cpShapeFree(upperLegShape1); cpSpaceRemoveShape(physicsSpace, lowerLegShape1); cpShapeFree(lowerLegShape1); cpSpaceRemoveShape(physicsSpace, upperArmShape2); cpShapeFree(upperArmShape2); cpSpaceRemoveShape(physicsSpace, lowerArmShape2); cpShapeFree(lowerArmShape2); cpSpaceRemoveShape(physicsSpace, upperLegShape2); cpShapeFree(upperLegShape2); cpSpaceRemoveShape(physicsSpace, lowerLegShape2); cpShapeFree(lowerLegShape2); // ------------------------------------------ // Remove and free all of the bodies // ------------------------------------------ cpSpaceRemoveBody(physicsSpace, headBody); cpBodyFree(headBody); cpSpaceRemoveBody(physicsSpace, torsoBody); cpBodyFree(torsoBody); cpSpaceRemoveBody(physicsSpace, upperArmBody1); cpBodyFree(upperArmBody1); cpSpaceRemoveBody(physicsSpace, lowerArmBody1); cpBodyFree(lowerArmBody1); cpSpaceRemoveBody(physicsSpace, upperLegBody1); cpBodyFree(upperLegBody1); cpSpaceRemoveBody(physicsSpace, lowerLegBody1); cpBodyFree(lowerLegBody1); cpSpaceRemoveBody(physicsSpace, upperArmBody2); cpBodyFree(upperArmBody2); cpSpaceRemoveBody(physicsSpace, lowerArmBody2); cpBodyFree(lowerArmBody2); cpSpaceRemoveBody(physicsSpace, upperLegBody2); cpBodyFree(upperLegBody2); cpSpaceRemoveBody(physicsSpace, lowerLegBody2); cpBodyFree(lowerLegBody2); }
The next chunk of code below completes the RagDollCharacter class implementation.
The updateSprites selector is called by the main game loop to tell the RagDollCharacter that it is time to update the position and rotation of each sprite that makes up the rag doll. All it does in this sample app is assign to each rag doll sprite the position it should be at and the scales the angle in radians provided by the Chipmunk phyiscs body to the angle in degrees that the Cocos2d sprite needs. Note that the sign of the angle is also inverted since positive rotation in Cocos2d is opposite that of Chipmunk.
If the ragdoll character had more complicated artificial intelligence, you could also use the calling of the characters updateSprites selector to check the current state of the AI and make any other needed changes to the ragdoll’s behavior.
Modifying the Template App
Now that we have the two ragdoll classes created, lets modify the HelloWorldLayer class and the RootViewController classes to get them ready to add ragdoll characters to the physics simulation.
Setting the Auto Rotate Behavior of the App in the RootViewController
One of the cool things about this template app is that when you tip the device in different directions it changes the direction of gravity to make the characters fall in whichever direction is down based on how the device is being held. However, this effect is ruined by the fact that the template app also has the shouldAutorotateToInterfaceOrientation: always returning YES. This makes the device auto rotate to any of the four device orientations.
To fix this problem, I chose to disable the auto rotation of the screen.
In the RootViewController.m class, find the selector named shouldAutorotateToInterfaceOrientation:. The following code shows how I changed this method so that the app will only display the Portrait Up orientation. The important part of the change is that this method now only returns YES if the device orientation is equal to
UIDeviceOrientationPortrait
.
// Override to allow orientations other than the default portrait orientation. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // // // return YES for the supported orientations // Only landscape ? // return ( UIInterfaceOrientationIsLandscape( interfaceOrientation ) ); // Only portrait ? // return ( ! UIInterfaceOrientationIsLandscape( interfaceOrientation ) ); if (interfaceOrientation == UIDeviceOrientationPortrait) { return YES; }else { return NO; } // All orientations ? // return YES; }
Updating the HelloWorldLayer Class
In the HelloWorldLayer class we will be adding a new method that will create the rag doll characters. This method will replace the addNewSpriteAtPosition: method.
We also will be adding a new interface variable named ragDolls to the HelloWorldLayer class that is an NSMutableArray that will own each ragdoll character after it is created, be used in the main game loop to update each ragdoll character and is responsible for telling the the ragdoll characters when to be destroyed.
Add the following interface variable to the HelloWorldLayer class interface.
NSMutableArray* ragDolls;
Add the following property definition to the HelloWorldLayer class interface.
@property (nonatomic, retain) NSMutableArray* ragDolls;
Add the following synthsize directive to the HellowWorldLayer class implementation.
@synthesize ragDolls;
While I would prefer to be using Apple’s Automatic Reference Counting (ARC), since Cocos2d and the templates that you are using as of July 2012 do not support ARC, don’t forget to release the ragDolls array in the HelloWorldLayer.m dealloc method.
// Destroy the array that retained all of the ragdolls. self.ragDolls = nil;
In the HelloWorldLayer implementation file, update the private interface definition to look like the following by adding the addNewRadDollCharacterAtPosition: selector and optionally comment out or remove the addNewSpriteAtPosition: selector. In the following code, I deleted the addNewSpriteAtPosition: selector and added the -(void) addNewRadDollCharacterAtPosition:(CGPoint)pos; selector.
@interface HelloWorldLayer () -(void) addNewRadDollCharacterAtPosition:(CGPoint)pos; -(void) createResetButton; -(void) initPhysics; @end
In the init method of the HelloWorldLayer class, make the following changes by commenting out how the CCSpriteBatchNode is loaded and replacing it with the code that loads the ragDoll.png and ragDoll.plist files.
/* #if 1 // Use batch node. Faster CCSpriteBatchNode *parent = [CCSpriteBatchNode batchNodeWithFile:@"grossini_dance_atlas.png" capacity:100]; spriteTexture_ = [parent texture]; #else // doesn't use batch node. Slower spriteTexture_ = [[CCTextureCache sharedTextureCache] addImage:@"grossini_dance_atlas.png"]; CCNode *parent = [CCNode node]; #endif [self addChild:parent z:0 tag:kTagParentNode]; */ [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"ragDoll.plist"]; CCSpriteBatchNode* ragDollSpriteBatchNode = [CCSpriteBatchNode batchNodeWithFile:@"ragDoll.png" capacity:6]; [self addChild:ragDollSpriteBatchNode z:0 tag:kTagParentNode];
Add the following method to the HelloWorldLayer implementaiton. It is this method that creates the rag doll characters and hands over ownership of them to the ragDolls NSMutableArray object.
-(void) addNewRadDollCharacterAtPosition:(CGPoint)pos { if (ragDolls == nil) { self.ragDolls = [NSMutableArray array]; } CCSpriteBatchNode *ragDollSpriteBatchNode = (CCSpriteBatchNode*)[self getChildByTag:kTagParentNode]; RagDollCharacter* aRagDollCharacter = [[[RagDollCharacter alloc] initWithPosition:pos withSpriteBatchNode:ragDollSpriteBatchNode withChipmunkPhysicsSpace:space_] autorelease]; [ragDolls addObject:aRagDollCharacter]; }
In the init method, replace the addNewSpriteAtPosition: selector call with the addNewRagDollAtPosition: selector as shown here.
//[self addNewSpriteAtPosition:ccp(200,200)]; [self addNewRadDollCharacterAtPosition:ccp(200,200)];
Also in the ccTouchesEnded:withEvent: selector replace the addNewSpriteAtPosition: selector call with the addNewRagDollAtPosition: selector as shown here.
//[self addNewSpriteAtPosition: location]; [self addNewRadDollCharacterAtPosition:location];
The last change to make to the HelloWorldLayer class is to add the import directive to add the RagDollCharacter.h interface file.
// Class that enables creating a Ragdoll #import "RagDollCharacter.h"
Pixel Format
The Cocos2d template used to create this app has set the pixel format of the EAGLView to be kEAGLColorFormatRGB565 and the default pixel format of the CCTexture2d objects to be kCCTexture2DPixelFormat_RGBA4444.
These settings can increase performance by reducing the number of bytes required to hold and process the color of a pixel. While increasing performance by requiring less memory to store and less bytes to process when rendering, the quality of your images may look worse than if you used more bytes that are capable of defining more colors.
Try changing the EABLView kEAGLColorFormatRGB565 to kEAGLColorFormatRGBA8 and the kCCTexture2DPixelFormat_RGBA4444 to kCCTexture2DPixelFormat_RGBA888 in the AppDelegate and see if you can notice a difference in image quality. The following code shows how to do this in the AppDelegate.
// // Create the EAGLView manually // 1. Create a RGB565 format. Alternative: RGBA8 // 2. depth format of 0 bit. Use 16 or 24 bit for 3d effects, like CCPageTurnTransition // // EAGLView *glView = [EAGLView viewWithFrame:[window_ bounds] pixelFormat:kEAGLColorFormatRGBA8 // kEAGLColorFormatRGBA8, kEAGLColorFormatRGB565 depthFormat:0 // GL_DEPTH_COMPONENT16_OES ]; .... // Default texture format for PNG/BMP/TIFF/JPEG/GIF images // It can be RGBA8888, RGBA4444, RGB5_A1, RGB565 // You can change anytime. [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGBA8888];
Retina Display Graphics
What are Retina Display Graphics?
Retina display graphics are double the width and height of the non-retina display graphics.
An important concept to understand is the use of points as a measurement of space in Cocos2d. In non-retina mode, one point is equal to one pixel. But on a retina display, one point equals two pixels.
The coordinates in the physics simulation (when using Chipmunk) and the coordinates for moving sprites around are both in points. This is important to understand because when you create a physics model for a sprite, you want to use the non-retina display version of the graphics to do so.
Making Retina Graphics
When I make graphics for a game or have someone else make the graphics for me, I always start with the retina versions of the graphics. In most cases, the non-retina graphics can easily be created by just scaling down the retina graphics by 50%. You can even automate the scaling with tools like Photoshop or Apple’s Automator.
It is important to consider that if you did it the other way and scaled the non-retina graphics to be larger, regardless of scaling algorithm used, the quality of the retina graphics will likely not be as good of a quality.
Loading Retina Display Graphics
Games that enable the retina display can look really great. The process of detecting if the retina display is available on a device and then using an alternative set of graphics is easy to manage with Cocos2d.
In the the AppDelegate’s applicationDidFinishLaunching method, you can add the following line of code to enable the retina display on devices that support it.
BOOL isRetinaDisplayEnabled = [director enableRetinaDisplay:YES]
You can optionally choose to store the BOOL value returned by this selector indicating if enabling the retina display was enabled. However, because Cocos2d will automatically load the retina display graphics instead of the standard resolution graphics, it is not necessary to store the return value.
You are not required to do anything different in your code to load a the retina image file.
To load a image file, such as a sprite sheet called ragDoll.png in non-retina mode, in your code you request the resource named ragDoll.png.
To load the retina display version of this image file with retina display mode enabled, you request the same file resource, ragDoll.png.
The key to managing retina display files is that for each image file, such as ragDoll.png, you also need to have a double sized image resource added to your project and it must be similarly named but with the -hd in the file name, like this: ragDoll-hd.png.
Universal iPhone/iPod/iPad app
The Cocos2d template creates an iPhone only app. When it is run on an iPad, it runs in the scaled down iPhone size on the iPad. Creating a universal app is often not difficult. It does require creating some extra graphics and redesigning the user interface and making changes regarding how your game displays game content.
The first step to enable a universal app is to just go to the target’s summary settings for the app and change the Devices setting from iPhone to Universal. The following screenshot shows the Devices setting set to Universal.
iPad 3rd Generation in Retina Mode Cocos2d 2.0 rc2 issue If on a third generation iPad the app only shows a blank screen, first make sure that you are running the Cocos2d 2.0 stable release version and not the Cocos2d 2.0 rc2 version. The rc2 version used different logic in the Cocos2d ccFileUtils class when determining which files to load if the device was an iPad or not and if it was running in retina mode or not. The new stable version defaults to attempting to loading the retina -hd file if it is running in retina mode. If the retina -hd version does not exist or the retina display is not enabled, it will then check to see if the -ipad version of the file exists. If no -ipad file exists, then it will attempt to load the file without adding any type (-hd or -ipad) before the file suffix. You can fix this problem by upgrading to the Cocos2d 2.0 stable version. Alternatively, if you want to get your hands dirty by digging into Cocos2d rc2 you can change the fullPathFromRelativePath:resolutionType method in the Cocos2d ccFileUtils.m file to deal with the iPad HD case as follows: fall back on loading the requested image file resource with the -hd in the file name if the -ipadhd does not exists and then fall back to loading the requested file name if the file with the -hd in the name does not exist.
Adding a background Image
To make things a little more interesting, lets add a background image to the app. In the HelloWorldLayer init method add the following code to load the Default.png image as the background.
CCSprite* background = [CCSprite spriteWithFile:@"Default.png"]; background.anchorPoint = ccp(0,0); background.opacity = 100; [self addChild:background z:-20]; if( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { // Really ugly hack to warp a background image to fit the ipad. // Should really create a seperate artwork for iPad. background.scaleX = 768.0f / 320.0f; background.scaleY = 1024.0f / 480.0f; }
Notice the ugly hack that I added to scale the background image if the device it is running on is an iPad. I only did this for this sample program to show how you can detect in a universal app if the code is running on an iPad or not. In a real game, you would want to have a separate image to load if the device is an iPad. You would also likely want to consider loading a retina image instead of just using the Default.png 320 x 480 image.
Conclusion
Ragdoll characters in games can be a lot of fun. For something that seems quite simple on the surface, the actual creation of them can be quite complex.
Let me know what you think of this tutorial. I really appreciate getting feedback about what people like and dislike about the tutorials I create.
Cheers,
Jim