iPhone Tutorial: Creating a Labryinth game for the using Cocos2D

Creating a Labryinth game for the iPhone:

In this tutorial we're going to design a Labyrinth game for the iPhone using Cocos2d and Box2D. This tutorial will introduce you to the following concepts:







  • Box2D physics: We'll be using Shape Workshop to create a Box2d scene comprised of complex polygons. You'll learn how to create the scene then add physical properties to the game elements.
  • User input using gyro or mouse: We'll be tilting the labyrinth board with with mouse clicks or using the iPhone gyro.
  • Sensors and custom contact listeners: We'll write a custom contact listener so that when the ball touches a hole it falls in, triggering a custom animation.
  • Cocos2D Actions: We'll use a Cocos2D CCMoveTo action to simulate the ball falling into the hole and a CCCallFunc action to call a custom function to restart the game.
  • Cocos2D Sequence: We'll use the CCSequence object to chain together a series of actions.
  • Cocos2D Complex Animation: We'll use a combination of: CCMoveTo, CCScaleTo and CCAnimation (frame by frame animation) to create a realistic effect when the ball falls in the hole.
  • Menus: We'll create a menu to allow the user to play the game again when they reach the end zone.
  • Area forces: We'll use an area force to simulate the effect of a fan.

Before we start you need to download the project bundle. It contains all the resources you will need to complete this tutorial. It also contains a completed Shape Workshop project and a completed Xcode project for reference. For this project you will need to use Shape Workshop 1.0 Beta 3 or higher. You will also need the Shape Workshop starter project available here

Creating the scene in Shape Workshop:

One of the nice things about Shape Workshop is it automatically renders SVG elements and converts them into PNG images which are stored in the project's images folder. This means that after saving your level you can load up these images in Photoshop or another photo editing program and make changes. I wanted to make the labyrinth board more interesting so I decided to add a wood textures to the floor and walls. I changed the ball to a photo of a ball bearing and I added shading to the holes. Then I replaced the original images in my project with these updated images. The next time Shape Workshop loads the project it will use these new images. To do this for your project, open the optimised_images folder in the project bundle and replace the contents of the images folder of your Shape Workshop project with the optimised images. You should end up with something like the image shown.

Figure 1: Labyrinth level with optimised textures

Setting up XCode:

Now that we've setup our level geometry we need to start writing our game code. You should have downloaded the Shape Workshop Base Project so now you need to open it in XCode. The first thing we need to do is setup our export options so we can export the level from Shape Workshop to XCode. Once you've got your XCode project downloaded and setup navigate back onto your Shape Workshop project. Click "File->Edit Project" and set the location of the "iPhone - Publish Path" to the "Resources" folder of your XCode project. Now click "View" and make sure that: width, height and scale are 480, 320 and 2 respectively. This means that when you export the level, the scaling will be set so that about a quarter of the level would be visible at any given time. Next click the "Level" tab and change the name of the level to "labyrinth" and change the gravity to "[0,0]". Now we're ready to export the scene. Click "Publish->Publish to iPhone" Shape Workshop will now write all the necessary files to your resources directory in XCode.

Now open up XCode again and create a new group in the Resources folder in the side panel called "labyrinth". Select this group and right click and select "Add Files to.." in the selection window which pops up navigate to the "Resources->labyrinth" and select the three files: "labyrinth-pack.plist", "labyrinth-spritesheet1.png", and "labyrinth.plist". These files contain the sprite and level geometry data. For more details look at the Introduction to physics on the iPhone tutorial. Now we're ready to start building our game!

The final thing we need to do is make sure our game loads up in landscape mode. To do this Open up the AppDelegate file in XCode. Change the shouldAutoRotateToInterfaceOrientation method so it looks like this:

  1. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
  2. {
  3. return UIInterfaceOrientationIsLandscape(interfaceOrientation);
  4. }

This means that the game will run in landscape mode. Next click ShapeWorkshopBase the highest level in the left hand navigator. In the Supported Device Orientations section select: Landscape Left and Landscape Right.

Figure 1:Setting the project to landscape mode in XCode

Building our game:

This project if fairly complex to setup so I've tried to break it up into logical parts. We will start off by loading our level into XCode. After each stage you should have something which you can run to test if you've made any mistakes. If you find it doesn't run I recommend taking at look at the sample project included in the bundle. The first thing we need to do is reproduce our Shape Workshop level on the iPhone.

Loading our Physics Level:

The first step is to import our level into Cocos2D. Make the following changes to HelloWorldLayer.mm:

Interface:

  1. @interface HelloWorldLayer : CCLayer {
  2. BBox2DBuilder * _builder;
  3. BLevel * _level;
  4. BElement * _ball;
  5. BOOL _runPhysics;
  6. b2World * _world;
  7. }
  8.  

Implementation:

  1. -(id) init
  2. {
  3. if( (self=[super init])) {
  4.  
  5. /*
  6.   * Setting up the world
  7.   */
  8.  
  9. // Get a link to the level config file
  10. NSString * path = [[NSBundle mainBundle] pathForResource:@"labyrinth" ofType:@"plist"];
  11.  
  12. // Create a new Level object from the file
  13. _level = [BLevel levelWithContentsOfFile:path];
  14.  
  15. // Add the sprite sheet to Cocos2D
  16. [BBox2DBuilder setSpriteSheet:self withSpriteSheet:@"labyrinth-spritesheet1.png" withPackFile:@"labyrinth-pack.plist"];
  17.  
  18. // Build the Box2D world
  19. _builder = [BBox2DBuilder box2DBuilderWithLevel:_level withDebug:NO];
  20. _world = [_builder world];
  21.  
  22. // Add individual element's sprites to the layer
  23. [_builder addSpritesToLayer:self];
  24.  
  25. // Set the scale to match what we've setup in Shape Workshop
  26. [_builder setScaleFromLevel:self];
  27.  
  28. // Set the sprite positioning in the z-direction to match Shape Workshop
  29. [_builder orderByZPosition: self];
  30.  
  31. // Start the physics world running
  32. _runPhysics = YES;
  33.  
  34.  

This code imports the level data and builds a Box2D world. First we get a pointer to our level geometry file (labyrinth.plist). Next we populate our level object from the file. Then we add the sprite sheet to Cocos2D so the spites are made available. Using the BBox2DBuilder we use our level object to create a Box2D world. Having a specific Box2D builder is flexible because if we want to load our level in Chipmunk we can use exactly the same level data and just write a Chipmunk builder. When we've created the Box2D world we add the element sprites to our layer, then set the Cocos2D scale to match the scale set in Shape Workshop and finally organise the elements in the z-direction to match Shape Workshop. If we didn't do this the ball might be rendered behind the holes!

At this point you can hit run and see our level reproduced on the iPhone.

Getting pointers to the ball and fan using tags:

Next we're going to get pointers to the ball and the fan. To get the elements we're going to use the tags we setup.

Interface: add the following variables:

  1. BElement * _hole;
  2. BElement * _fan;
  3. b2Vec2 _startPosition;

Implementation:

  1. // get a pointer to our ball for use later on
  2. // Get elements by tag could return multiple items however in this case we've
  3. // only added the tag ball to one element
  4. NSMutableArray * ball = [_level getElementsByTag:@"ball"];
  5. if(ball != Nil && [ball count] > 0) {
  6. _ball = [ball objectAtIndex:0];
  7.  
  8. // A pointer to the Box2D body is stored in the physics link member variable
  9. // as an NSValue
  10. NSValue * bodyValue = _ball.physicsLink;
  11. b2Body * ballBody;
  12.  
  13. // Extract the body from the NSValue object
  14. [bodyValue getValue:&ballBody];
  15.  
  16. // Record the starting position of the ball for later
  17. _startPosition = ballBody->GetPosition();
  18. }
  19.  
  20. // Get a pointer to the fan
  21. NSMutableArray * fan = [_level getElementsByTag:@"fan"];
  22. if(fan != Nil && [fan count] > 0) {
  23. _fan = [fan objectAtIndex:0];
  24. }

We get the ball element by using the getElementsByTag method. This method returns a list of elements which have a specified tag. We then set our member variable _ball to point to this element. Next we need to find the balls starting position. This is used to return the ball to the start if it falls in a hole. To get the starting position we need to access the Box2D body associated with the ball. This is stored as a pointer inside an NSValue object. To extract the body we create a b2Body variable and then call getValue on the NSValue referencing the b2Body. Then we set the _startPosition member variable to this value.

We then repeat the process to get a pointer to the fan. Next we need to setup a custom contact listener.

Activating touch in Cocos2D:

Simulating tilt information using the mouse

We want to be able to move the ball around. Ideally we would use the iPhone gyros so that the ball would move when the phones was tilted. Unfortunately the iPhone simulator doesn't support gyros. To get around this we need to think of another way to control the ball. We're going to use the advanced method described in this tutorial: Simulating a gyroscope on the iPhone simulator.

The basic principle for this is that we're going to treat the level as if it were balanced on a pin in the centre. When you touch the level with the mouse it will be as if a force were applied to the level and it will tilt in that direction. For example if you click in the centre of the screen nothing will happen - you're pushing on the pin! If you click at the bottom, the ball will start to roll downwards. If you click at the top the ball will roll upwards. The further from the edge you click the more tilt will be applied.

First we're going to activate touch for out Cocos2D layer. In the init method o HelloWorldLayer.mm add this line:

  1. self.isTouchEnabled = YES;

Then override the ccTouchesBegan, ccTouchesMoved and ccTouchesEnded methods by adding the following functions. These will be called whenever the user touches the screen, moves their finger while touching and removes their finger respectively.

Interface:

  1. b2Vec2 _ballForce;

Implementation:

  1. - (void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  2. UITouch* touch = [touches anyObject];
  3. [self updateBallForce:touch];
  4. }
  5.  
  6. - (void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  7. UITouch* touch = [touches anyObject];
  8. [self updateBallForce:touch];
  9. }
  10.  
  11. -(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  12. b2Vec2 force (0,0);
  13. _ballForce = force;
  14. }
  15.  
  16. - (void) updateBallForce: (UITouch *) touch {
  17.  
  18. // Get the location of the touch
  19. CGPoint location = [touch locationInView:touch.view];
  20.  
  21. // Find the position relative to the centre of the screen
  22. // remember the screen is 480x320 with the origin at the
  23. // top left hand corner
  24. float xRel = location.x - 240;
  25. // Make it so y-up is positive negative
  26. float yRel = -location.y + 160;
  27.  
  28. // Scale xRel and yRel so they vary between 0 and 1
  29. xRel = xRel / 240;
  30. yRel = yRel / 160;
  31.  
  32.  
  33. // Now apply a force to the ball
  34. float maxForce = 15 ;
  35.  
  36. // Check that there is a ball
  37. if(_ball != Nil) {
  38. b2Body * ballBody;
  39.  
  40. // The body is stored as a pointer wrapped in an NSValue
  41. // get a reference to the NSValue
  42. NSValue * bodyValue = _ball.physicsLink;
  43.  
  44. // Populate ballBody with the reference
  45. [bodyValue getValue:&ballBody];
  46.  
  47. // Set the linear damping. This means that if the ball
  48. // will gradually come to rest if we don't tilt the
  49. // phone.
  50. ballBody->SetLinearDamping(0.5);
  51.  
  52. // Make sure the body is awake
  53. ballBody->SetAwake(YES);
  54.  
  55. // Create a new force depending on where we've touched on the screen
  56. b2Vec2 force (xRel * maxForce * ballBody->GetMass(), yRel * maxForce* ballBody->GetMass());
  57. _ballForce = force;
  58. }
  59. }

This code populates a force member variable whenever the user touches the screen. The force is calculated based on the position of the touch in relation to the centre of the screen. For more details look at the tutorial mentioned earlier. This force is applied to the ball every time step in the update method:

Implementation: add the following to the update method:

  1. b2Body * ballBody;
  2. NSValue * bodyValue;
  3.  
  4. // Apply a force to the ball
  5. if(_ball != Nil) {
  6. // Apply the force to the ball
  7.  
  8. bodyValue = _ball.physicsLink;
  9. [bodyValue getValue:&ballBody];
  10.  
  11. if(_ballForce.x!=0 && _ballForce.y != 0) {
  12. ballBody->ApplyForce(_ballForce, ballBody->GetWorldCenter());
  13. }
  14. }

This code is pretty simple. Get a pointer to the ball body and if the force is not zero apply the force to the ball.

After adding this code run your project. The labyrinth table should appear as before. Now click anywhere on the simulator screen. You should see that the ball starts accelerating towards this point! Try to steer the ball around the level. You should find that it collides with walls but rolls straight over the holes. The next thing we're going to do is make it fall in the holes.

Custom Contact Listener in Box2D:

For our game we need to know when the ball hits a hole so we can make it fall in. We also need to know when it hits the bouncer so we can make it bounce off. To do this we want Box2D to notify us when a collision occurs. We do this by creating a custom contact listener. This contains two methods: BeginContact (b2Contact* contact) and EndContact(b2Contact* contact). These methods will be called whenever a collision starts or ends between two objects. When we create our custom contact listener we will put our game logic in these methods.

Create a new Objective C file called: MyContactListener. When it's created, change the extension from .m to .mm. We do this because the contact listener needs to be written in C++. The .mm extension tells XCode to supports C++.

MyContactListener - Interface:

  1. #import "Box2D.h"
  2. #import "b2Contact.h"
  3. #import "b2ContactManager.h"
  4. #import "b2ContactSolver.h"
  5. #import "CCLayer.h"
  6. #import "HelloWorldLayer.h"
  7.  
  8. class MyContactListener : public b2ContactListener
  9. {
  10. public:
  11. // Setup a new contact listener by passing in a CCLayer
  12. MyContactListener (CCLayer * layer) {
  13. _layer = layer;
  14. }
  15.  
  16. virtual void BeginContact(b2Contact* contact);
  17. virtual void EndContact(b2Contact* contact);
  18.  
  19. CCLayer * _layer;
  20. };

MyContactListener - Implementation:

  1. #import "MyContactListener.h"
  2.  
  3. void MyContactListener::BeginContact(b2Contact* contact) {
  4.  
  5. // Extract the elements from the contact
  6. BElement * elmA = (BElement *) contact->GetFixtureA()->GetBody()->GetUserData();
  7. BElement * elmB = (BElement *) contact->GetFixtureB()->GetBody()->GetUserData();
  8.  
  9. // Cast the layer variable to a HelloWorldLayer
  10. HelloWorldLayer * hwl = (HelloWorldLayer *) _layer;
  11.  
  12. // Call the start contact method and pass it the two elements
  13. [hwl startContact:elmA withElmB:elmB];
  14. }
  15.  
  16. void MyContactListener::EndContact(b2Contact* contact) {
  17. BElement * elmA = (BElement *) contact->GetFixtureA()->GetBody()->GetUserData();
  18. BElement * elmB = (BElement *) contact->GetFixtureB()->GetBody()->GetUserData();
  19.  
  20. HelloWorldLayer * hwl = (HelloWorldLayer *) _layer;
  21.  
  22. [hwl endContact:elmA withElmB:elmB];
  23. }

When we create our contact listener we pass it a reference to our layer. This is stored a the member variable _layer. We want to handle the collisions in the HelloWorldLayer. For this reason when a collision happens we extract the BElement objects from the Box2D body's user data and then pass these back to the HelloWorldLayer through the startContact or endContact methods.

Now let's look at the code we need to add in the HelloWorldLayer file:

Interface: add the following variable:

  1. b2ContactListener * _contactListener;

Implementation:

Add this code to the init method below the previous section to register our custom contact listener with Box2D:

  1. // Setup our custom contact listener with a callback to this class
  2. // whenever there's a collision
  3. _contactListener = new MyContactListener(self);
  4.  
  5. // Add the contact listener to the Box2D world
  6. _builder.world->SetContactListener(_contactListener);

Next we're going to setup the startContact method. This will handle collisions with holes and with the bouncer:

  1. // Called when the ball collides with something
  2. -(void) startContact: (BElement *) elmA withElmB: (BElement *) elmB {
  3. // If the ball hits a wall
  4. if([self testElements:elmA withElmB:elmB withTag:@"hole" withTag2:@"ball"]) {
  5. // We want to shrink the ball as if it's falling and move it towarsd the centre of the hole
  6. // we will add a pointer to the hole and then check every loop if the ball is sufficiently
  7. // into the hole i.e. it won't fall if the ball just touches the hole the ball needs to be
  8. // half over the hole as it would in real life
  9. if([elmA containsTag:@"hole"]) {
  10. _hole = elmA;
  11. }
  12. else {
  13. _hole = elmB;
  14. }
  15. }
  16.  
  17. // If the ball hits the bouncer
  18. if([self testElements:elmA withElmB:elmB withTag:@"bouncer" withTag2:@"ball"]) {
  19. // Get the position of the bouncer
  20. BElement * bouncer;
  21.  
  22. if([elmA containsTag:@"bouncer"]) {
  23. bouncer = elmA;
  24. }
  25. else {
  26. bouncer = elmB;
  27. }
  28.  
  29. // Find the vector from the centre of the bouncer and the centre of the ball
  30. float xComp = (_ball.spriteLink.position.x + _ball.spriteLink.boundingBox.size.width/2 ) - (bouncer.spriteLink.position.x + bouncer.spriteLink.boundingBox.size.width/2);
  31.  
  32. float yComp = (_ball.spriteLink.position.y + _ball.spriteLink.boundingBox.size.height/2 ) - (bouncer.spriteLink.position.y + bouncer.spriteLink.boundingBox.size.height/2);
  33.  
  34. // Define the force of the bouncer
  35. float forceFactor = 1;
  36.  
  37. b2Vec2 force (xComp * forceFactor, yComp * forceFactor);
  38.  
  39. // Get the body associated with the ball
  40. NSValue * bodyValue = _ball.physicsLink;
  41. b2Body * ballBody;
  42. [bodyValue getValue:&ballBody];
  43.  
  44. // Apply an impulse to the ball
  45. ballBody->ApplyLinearImpulse(force, ballBody->GetWorldCenter());
  46.  
  47. // Animate the bouncer
  48. id scaleUp = [CCScaleTo actionWithDuration:0.05 scale:1.2];
  49. id scaleDown = [CCScaleTo actionWithDuration:0.05 scale:1];
  50.  
  51. [bouncer.spriteLink runAction:[CCSequence actions:scaleUp, scaleDown, nil]];
  52. }
  53.  
  54. // If the ball reaches the end zone show the "play again" menu
  55. if([self testElements:elmA withElmB:elmB withTag:@"endzone" withTag2:@"ball"]) {
  56. //_menu.visible = TRUE;
  57. _runPhysics = NO;
  58. }
  59. }
  60.  
  61. // A helper function to let us know if a collision has occurred between two objects
  62. -(BOOL) testElements: (BElement *) elmA withElmB: (BElement *) elmB withTag: (NSString *) tag1 withTag2: (NSString *) tag2 {
  63. if(([elmA containsTag:tag1] && [elmB containsTag:tag2]) ||
  64. ([elmB containsTag:tag1] && [elmA containsTag:tag2])) {
  65. return YES;
  66.  
  67. }
  68. return NO;
  69. }

If the ball collides with a hole we set the _hole member variable to point at the hole. We can then check in the update method to see if the ball is close enough to the hole to fall in. This isn't 100% robust however. If two holes are very close it could be that the ball is colliding with both of them at the same time. It would be more robust to store all the holes that were being collided with in an array. For this tutorial I want to keep things simple so I'll stick with the single hole variable.

If the ball hits the bouncer we want to make it ricochet away. To do this we need to work out the vector from the bouncer to the ball and then apply an impulse to the ball in this direction. An impulse is a force applied for an amount of time. Like giving a person a shove. First, we work out which element is the bouncer using the containsTag method. Then we calculate the vector from the centre of the bouncer to the centre of the ball. We do this by subtracting the centre of the bouncer sprite from the centre of the ball sprite. We then set our force to be equal to this vector and apply the impulse to the ball's Box2D body.

Finally, we animate the bouncer. To do this we create two CCScaleTo actions. CCScaleTo is used to change the scale of a node over a given period of time. We scale the bouncer sprite to 1.2 times it's normal size during 0.05 seconds and then scale it back to normal again. To apply these two actions one after the other we use a CCSequence. We then animate the sprite associated with the bouncer using the runAction method.

The endContact method is quite simple. All we need to set our _hole variable to Nil. If you were keeping track of multiple holes you would the hole from the array here.

  1. -(void) endContact: (BElement *) elmA withElmB: (BElement *) elmB {
  2. if([self testElements:elmA withElmB:elmB withTag:@"hole" withTag2:@"ball"]) {
  3. // If the ball leaves the hole without getting sufficently close set the
  4. // hole pointer to Nil
  5. _hole = Nil;
  6. }
  7. }

Now run the project and navigate the ball to the bouncer. When it hits the bouncer you should see the it is pushed away and that the bouncer is animated.

Making the ball fall in the hole:

When the ball gets close to a hole we want it to fall in. To make this look a bit realistic we need to create a sequence of animations. In real life a ball would fall in the hole when the ball was 50% over the hole. At this point it would move towards the centre of the hole, fall downwards a bit and roll sideways. To simulate this we can do the following. When the ball is close to a hole, check every frame to see if the ball is sufficiently close to a hole. If it is, animate the sprite to move it towards the centre of the hole. Next shrink it a bit to make it look like it's falling away from the screen. Finally, use a frame by frame animation to make it look like it's rolling sideways.

Cocos2D frame by frame animation using CCAnimation:

A frame by frame animation consists of a series of frames played one after the next. Because each frame is slightly different it gives the impression of an animation. To create a ball rolling animation I used Photoshop. I created a circular path mask on top of the ball image. I then moved the image to the right three pixels and masked out everything that was outside of the path and saved the image. I repeated this until the ball had disappeared. I then created a sprite sheet from this image which is included in the project resources. If you want to create your own sprite sheet search on google for "Free Sprite Sheet Creator".

Now I'm going to explain how to setup the animation. Add the Add the following files to your project: ball_falling_anim.plist and ball_falling_anim.png. Now add the following code to your init method below the code for the contact listener.

Figure 2: Ball disappearing animation sprite sheet

Interface:

  1. NSArray * _ballAnimationFrames;

Implementation:

  1. // Access the shared frames cache - global store of frames
  2. CCSpriteFrameCache * cache = [CCSpriteFrameCache sharedSpriteFrameCache];
  3.  
  4. // Load the available frames from the sprite sheet
  5. [cache addSpriteFramesWithFile:@"ball_falling_anim.plist" textureFilename:@"ball_falling_anim.png"];
  6.  
  7. // Create an array of frames - stored as a member variable
  8. NSMutableArray *ballFrames = [NSMutableArray new];
  9. for(NSInteger i =1; i<15; i++) {
  10. // The names of the objects in the sprite sheet are the same as the images files
  11. // before they are loaded into the sprite sheet i.e. ball-1, ball-2 etc...
  12. [ballFrames addObject:[cache spriteFrameByName:[NSString stringWithFormat:@"ball-%i.png", i]]];
  13. }
  14.  
  15. // Put these frames in a normal NSArray member variable
  16. _ballAnimationFrames = [NSArray arrayWithArray:ballFrames];
  17. [_ballAnimationFrames retain];

First we get a pointer to the Cocos2D frame cache. This is a global store of available frames. Then we add the sprite sheet containing the animation. Then we loop over the animation images and for each one we add a frame to the list of frames for our animation. The frames are called: "ball-1.png", "ball-2.png", etc… We use the stringWithFormat function to automatically choose the frames one by one. When we have all the frames loaded into an array we change this array to a NSArray (from our NSMutableArray) and set a pointer to it.

Next we need to know if the ball has actually fallen in the hole. To do this we need to add code to the update method:

Implementation:

  1. -(void) update: (ccTime) dt
  2. {
  3.  
  4. // If we've not paused the physics engine update the physics world
  5. if(_runPhysics)
  6. [_builder update:dt];
  7.  
  8. // Check if the ball is over half in the hole
  9. if(_hole != Nil && _runPhysics) {
  10. CGPoint holePos = _hole.spriteLink.position;
  11. CGPoint ballPos = _ball.spriteLink.position;
  12.  
  13. // Calculate the distance of ball from the hole
  14. float dist = sqrtf(powf(holePos.x - ballPos.x, 2) + powf(holePos.y - ballPos.y, 2));
  15.  
  16. // If the distance is less than 50% of the balls width it will fall
  17. // This is because in reality the contact surface of the ball on the
  18. // wood is very small and occurs at 50% of the balls width. Only when
  19. // this contact area is in the hole will the ball fall.
  20. if(dist < 0.5 * _ball.spriteLink.boundingBox.size.width) {
  21. [self triggerFallAnimation];
  22. }
  23. }

The update method is called every frame. It is responsible for our game logic. We first check the _runPhysics variable. This allows us to pause the physics engine. We do this when the ball falls in a hole so we can animate the sprite. If we didn't, the sprite's position would be updated to the position of the ball every frame. We check if _hole is Nil. If it's Nil it means that the ball is far from the hole. If it's not Nil it means that the ball is touching the hole. We calculate the distance of the ball from the hole and if it's less than 50% of the width of the ball we call triggerFallAnimation to make the ball fall in the hole.

Implementation:

  1. -(void) triggerFallAnimation {
  2. // Stop the physics engine from updating
  3. _runPhysics = NO;
  4.  
  5. // Get the position of the hole
  6. CGPoint holePos = _hole.spriteLink.position;
  7. // Get the hole size
  8. CGSize holeSize = _hole.spriteLink.boundingBox.size;
  9.  
  10. // Move the ball to the centre of the hole
  11. id move = [CCMoveTo actionWithDuration:0.1 position:ccp(holePos.x + holeSize.width*0.1,
  12. holePos.y + holeSize.height*0.1)];
  13. // Shrink the ball to 80% of it's normal size
  14. id shrink = [CCScaleTo actionWithDuration:0.1 scale:0.8];
  15.  
  16. // Setup the frame by frame rolling animation
  17. id animate = [CCAnimate actionWithAnimation:[CCAnimation animationWithSpriteFrames:_ballAnimationFrames delay:0.1]];
  18.  
  19. // When the animation has ended call the endAnimation function
  20. id end = [CCCallFunc actionWithTarget:self selector:@selector(endAnimation)];
  21.  
  22. // Put these actions into a sequence and run them
  23. [_ball.spriteLink runAction:[CCSequence actions:move, shrink, animate, end, nil]];
  24. }

This function first stops the physics engine from updating. It then creates a new CCMoveTo action which will move the ball's sprite to the centre of the hole. Next it creates a CCScaleTo to shrink the ball to 80% of it's normal size. Finally the ball is animated to make it look like it's rolling away. To do this we create a new CCAnimation object with the frames we setup earlier. This is encapsulated inside a CCAnimate object. A CCAnimation is the animation and CCAnimate is an object which allows the animation to be run as an action. Then we use a CCCallFunc to call a function when the animation comes to an end. We run these actions sequentially ont the ball's sprite.

Implementation:

  1. -(void) endAnimation {
  2. // When the animation comes to an end hide the ball
  3. _ball.spriteLink.visible = NO;
  4.  
  5. // Reset it's sprite to scale 1
  6. _ball.spriteLink.scale = 1;
  7.  
  8. // Get hold of the Box2D body
  9. NSValue * bodyValue = _ball.physicsLink;
  10. b2Body * ballBody;
  11.  
  12. [bodyValue getValue:&ballBody];
  13.  
  14. // Move it back to the start position
  15. ballBody->SetTransform(_startPosition, 0);
  16.  
  17. // The animation will have change the sprite frame. To reset
  18. // this we need to set the displayed frame back to the original ball
  19. [_ball.spriteLink setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:_ball.sprite]];
  20.  
  21. // Make the ball visible again
  22. _ball.spriteLink.visible = YES;
  23.  
  24. // Restart physics
  25. _runPhysics = YES;
  26. }

This end animation function is responsible for returning the ball to it's starting point again. We hide the ball then reset it's scale and move the ball body back to the starting point which we saved earlier. We then make it visible again and restart the physics engine.

Now run the project. Navigate the ball to a hole. You should find that when it is over 50% over the hole it appears to fall into the hole and appears back at the start point.

Setting up the menu:

Almost there! The next thing we need to do is setup the menu. The menu will show up when the player reaches the end zone and ask if they want to play again. Cocos2D makes this really straightforward for us to create menus. First you need to add the end_menu.png file from the bundle to your project.

Interface:

  1. CCMenu * _menu;
  2. CCMenuItemFont * _button;

Implementation:

  1. // Setup the menu for when the player completes the level
  2. _button = [CCMenuItemImage itemWithNormalImage:@"end_menu.png" selectedImage:@"end_menu.png" block:^(id) {
  3. if(_ball != Nil) {
  4. // Get the ball's box2d body
  5. NSValue * bodyValue = _ball.physicsLink;
  6. b2Body * ballBody;
  7.  
  8. [bodyValue getValue:&ballBody];
  9.  
  10. // Return the ball to it's start position
  11. ballBody->SetTransform(_startPosition, 0);
  12. ballBody->SetAwake(YES);
  13.  
  14. // Set the physics running
  15. _runPhysics = YES;
  16.  
  17. // Make the sprite visible again
  18. _ball.spriteLink.visible = YES;
  19.  
  20. // Hide this menu
  21. _menu.visible = NO;
  22.  
  23. }
  24. }];
  25.  
  26. // Add the button to the menu
  27. _menu = [CCMenu menuWithItems: _button, nil];
  28. // Hide the menu
  29. _menu.visible = NO;
  30. // Position the menu in the centre of the screen
  31. _menu.position = ccp(480, 320);
  32.  
  33. // Add the menu to this layer
  34. [self addChild:_menu z:200];
  35.  
  36.  
  37. [self scheduleUpdate];

Cocos2D has menu capability built in using CCMenu and CCMenuItems. We create a new button and set it's image to be the end_menu.png image The selected image is the image shown when the button is clicked. In this case it's the same. We then create a new block. A block is a way of defining a function which will be called when the button is pressed. A bit like an inner function in Java. We want this button to move the ball to it's starting position, and set the physics engine running. After that it hides the menu.

Next we add the button to the menu, hide the menu, set it's position in the middle of the screen and add the menu to the layer. Finally we schedule our update function to be called every frame.

Before running the project we need to make one change. Go to the startContact function. Uncomment the following line:

  1. // If the ball reaches the end zone show the "play again" menu
  2. if([self testElements:elmA withElmB:elmB withTag:@"endzone" withTag2:@"ball"]) {
  3. _menu.visible = TRUE; // Here!
  4. _runPhysics = NO;
  5. }

This means the menu is shows when the ball reaches the end zone. Now play the game. You should find that when you reach the end zone a menu pops up asking if you want to play again. Click yes and the ball will be brought back to the start.

Setting up the fan:

The last thing we're going to do is setup the fan. We want the fan to apply a repulsive force to the ball. To do this we need to add the following code to the update method:

Implementation:

  1. if( _fan != Nil && _ball != Nil) {
  2. // Get a pointer to the ball body
  3. b2Body * ballBody;
  4. NSValue * bodyValue = _ball.physicsLink;
  5. [bodyValue getValue:&ballBody];
  6.  
  7. // Find the vector from the fan to the ball
  8. float xComp = (_ball.spriteLink.position.x + _ball.spriteLink.boundingBox.size.width/2 ) - (_fan.spriteLink. position.x + _fan.spriteLink.boundingBox.size.width/2);
  9.  
  10. float yComp = (_ball.spriteLink.position.y + _ball.spriteLink.boundingBox.size.height/2 ) - (_fan.spriteLink.position.y + _fan.spriteLink.boundingBox.size.height/2);
  11.  
  12. // Get the distance from the ball to the fan
  13. float distSq = powf(xComp, 2) + powf(yComp, 2);
  14.  
  15. // Define a force multiplier
  16. float forceFactor = 5000;
  17.  
  18. // Inverse squared force - will be tiny at any great distance
  19. b2Vec2 force (forceFactor * (xComp / fabsf(xComp)) / distSq, forceFactor * (yComp/fabsf(xComp))/distSq);
  20.  
  21. // Apply the force to the ball
  22. ballBody->ApplyForce(force, ballBody->GetWorldCenter());
  23.  
  24. }

This code first gets a pointer to the ball's Box2D body. Then it works out the vector from the fan to the ball. Using Pythagoras' theorem we find the distance squared between the ball and the fan. We then create a repulsive force. We divide by the distance squared so the force is very small far from the fan and large near it. To get the direciton the force should be applied we normalise the x and y components by dividing both by the absolute value of the x component. Finally we apply the force to the ball.

Run the project…

Run the project and roll the ball near the fan. It should be repelled away. If it doesn't work it's probably because an error has been made somewhere. Don't worry, the completed sample project is provided in the bundle.

Congratulations you've made an iPhone game! This tutorial gives you all the skills you need to start making complex iPhone games. You know how to use physics, create menus, create custom collisions and handle user input. Next we'll look at how to use CoreMotion to control the ball using the iPhone's gyro! iPhone accelerometer tutorial

Comments

Thanks for this very nice tutorial. If I want to enable gyroscope for this game, how do I go about doing it? Thanks a lot!

Thank you. :)

Could you also write a tutorial on how to add multiple levels with different labryinth maps or maybe different difficulty levels? :)

Hi
Does Shape Workshop support iPad too or is it only for iPhone and Android?

Shape Workshop in principle can support any platform. It's just a graphical interface for creating geometric data from sprites and SVG files. What you do with that geometric is up to you. It should work straight away with the iPad.

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <c>, <cpp>, <drupal5>, <drupal6>, <java>, <javascript>, <php>, <python>, <ruby>. The supported tag styles are: <foo>, [foo].
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.