Create Your Own Level Editor: Part 2/3

Barbara Reichart
Add an interactive level editor to your games!

Add an interactive level editor to your games!

This is the second part of a tutorial series that shows you how to make a level editor for the Cut the Verlet game that was previously covered on this site.

In the previous tutorial, you designed the XML storage mechanism of your level files, implemented some model classes to hold your rope and pineapple data, and finished off by creating the load file functionality of your level editor.

At this point, your project is still just a level “loader”, as opposed to a level “editor”. (No, editing XML files by hand does not count as a level editor — just in case you were wondering!)

Rest assured that you’ll not have to manually edit those level files any longer. In this second part of the tutorial, you will implement a portion of the editing capabilities of your level editor. You’ll work through adding popup menus, dynamically positioning and sizing your objects on screen, and much more.

If you don’t have it already, download a copy of the sample project where you left it off in the previous tutorial.

Getting Started

So what functionality should a level editor contain? At the very least it should be able to create, reposition and delete objects in the level. As well, it should make the best use of the screen real estate available. And as always, the functions in your app should be as intuitive as possible for the user.

With those basic requirements in place, it’s time to set out the design of the editor.

First, the user needs a way to launch the editor. The easiest way to do this is by using a menu. The bottom of the screen is probably the best place for the menu, as it does not contain any dynamic game elements, as seen below:

iOS-Simulator.png

Second, the user must be able to add ropes and pineapples to the level. This means you need a mechanism that will allow them to create those new objects. Again, the easiest way to expose this functionality would be through a menu.

However, you already have a menu to be added to the bottom of your screen, and you’re rapidly running out of real estate.

What to do?

Instead of having a menu that is permanently visible, it would be better to have a popup menu that appears by tapping the screen. From this popup menu, the user can choose between adding a pineapple or a rope. The object will then be created automatically at the current position of the popup menu.

Moving objects will also be important to the user — and the user interaction for this on a touch screen is almost a no-brainer. Drag and drop is the logical choice for this functionality.

Last but not least, the user needs to be able to delete objects. One very common way to do this in iOS is to use a long press action to indicate that the user wants to remove the item being pressed on.

Now that the design of the editor has been decided, you can get busy constructing it!

Creating The LevelEditor Class

Create an Objective-C class under the LevelEditor group. Name the file LevelEditor, make its super class CCLayer, and be sure to change the file extension of the file from .m to .mm.

The .mm file extension tells the compiler that the file uses Objective-C++.

Why do you need to use Objective-C++ in the LevelEditor class?

Simply put, Box2D uses C++. As well, LevelEditor references other game classes which rely on Box2D.

To start, replace the contents of LevelEditor.h with the following:

#import "cocos2d.h"
#import "LevelFileHandler.h"
#import "CoordinateHelper.h"

@interface LevelEditor : CCLayer<CCTouchOneByOneDelegate>

+(CCScene *)sceneWithFileHandler:(LevelFileHandler*)fileHandler;

@end

This adds the CCTouchOneByOneDelegate protocol to the declaration, which allows the new LevelEditor layer to receive touch events.

Next, switch to LevelEditor.mm and replace its contents with the following:

#import "LevelEditor.h"
#import "LevelFileHandler.h"
#import "PineappleModel.h"
#import "RopeModel.h"
#import "CutTheVerletGameLayer.h"

@interface LevelEditor () {
    LevelFileHandler* fileHandler;
    CCSprite* background;
    CCSpriteBatchNode* pineapplesSpriteSheet;
    CCSpriteBatchNode* ropeSpriteSheet;
}

@end

@implementation LevelEditor

@end

The above code adds all necessary imports to LevelEditor and then creates some instance variables. fileHandler will store the level you are currently editing, while background is simply a sprite that displays the jungle background. There are also two CCSpriteBatchNodes for all pineapples and ropes.

Now add the sceneWithFileHandler: implementation to LevelEditor.mm, as follows:

+(CCScene *)sceneWithFileHandler:(LevelFileHandler*)fileHandler {
    CCScene* scene = [CCScene node];
    LevelEditor *layer = [[LevelEditor alloc] initWithFileHandler:fileHandler];
	[scene addChild: layer];
    return scene;
}

This code is pretty similar to the scene creation code in the Cocos2D project template. It simply creates a CCScene that contains the LevelEditor scene.

At the moment this scene is empty. That won’t be terribly interesting to look at! :] Start by adding the editor menu to your scene.

Adding the Editor Menu

Add the following code anywhere in LevelEditor.mm:

-(void) createMenu {
    CGSize winSize = [CCDirector sharedDirector].winSize;
    
    // Place Buttons at bottom of game
    CCLabelTTF* saveLabel = [CCLabelTTF labelWithString:@"Save" fontName:@"Marker Felt" fontSize:24];
    CCMenuItem* saveItem = [CCMenuItemLabel itemWithLabel:saveLabel target:self selector:@selector(save)];
    
    CCLabelTTF* resetLabel = [CCLabelTTF labelWithString:@"Reset" fontName:@"Marker Felt" fontSize:24];
    CCMenuItem* resetItem = [CCMenuItemLabel itemWithLabel:resetLabel target:self selector:@selector(resetLevel)];
    
    CCLabelTTF* playLabel = [CCLabelTTF labelWithString:@"Play Level" fontName:@"Marker Felt" fontSize:24];
    CCMenuItem* playLevelItem = [CCMenuItemLabel itemWithLabel:playLabel target:self selector:@selector(playLevel)];
    
    // Create menu with buttons
    CCMenu* menu = [CCMenu menuWithItems:saveItem, resetItem, playLevelItem, nil];
    [menu alignItemsHorizontallyWithPadding:winSize.width*0.1f];
    menu.position = CGPointMake(winSize.width/2, saveItem.contentSize.height/2);
    [self addChild:menu z:100];
}

-(void) save {
    // TODO: save level
}

-(void) resetLevel {
    // TODO: reset to last saved version of currently opened file
}

-(void) playLevel {
	[[CCDirector sharedDirector] replaceScene: [HelloWorldLayer sceneWithFileHandler: fileHandler]];
}

In the code above, first you get the screen size. Next, you create three menu items with the labels “Save”, “Reset” and “Play Level”. You then create a CCMenu and add the menu items to it. Finally, you add the CCMenu to the scene so that it becomes visible.

When you look at the code that creates the menu items, you’ll note that each item has a selector attached to a method that is called when the item is tapped. For the moment only playLevel is implemented — tapping this menu item simply switches to the game scene. You’ll come back to implement the other two methods later.

Now you’ll need some code to call the menu and draw the background on the screen.

Add the following code to LevelEditor.mm:

-(id)initWithFileHandler:(LevelFileHandler*)levelFileHandler {
    self = [super init];
    if (self) {
        fileHandler = levelFileHandler;
        [self createMenu];
        
        background = [CCSprite spriteWithSpriteFrameName:@"bg.png"];
		   CGSize winSize = [CCDirector sharedDirector].winSize;
        background.position = CGPointMake(winSize.width/2, winSize.height/2);
        background.color = kDefaultBackgroundColor;

        [self addChild:background];
        
        // Load the sprite sheet into the sprite cache
        [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"CutTheVerlet.plist"];
        pineapplesSpriteSheet = [CCSpriteBatchNode batchNodeWithFile:@"CutTheVerlet.pvr.ccz"];
        [self addChild:pineapplesSpriteSheet];
        ropeSpriteSheet = [CCSpriteBatchNode batchNodeWithFile:@"rope_texture.png"];
        [self addChild:ropeSpriteSheet];
        
        [self drawLoadedLevel];
    }
    return self;
}

-(void) drawLoadedLevel {
    // TODO: draw pineapples
    // TODO: draw ropes
}

The above code initializes the screen of the level editor. It then stores the level layout by making a copy of LevelFileHandler. Next, it calls the method that draws the menu. After that, it loads the background and positions it in the middle of the screen.

Next, the code tints the background so that it is darker than the original background. This will help the user avoid any confusion as to whether they are in game mode or in edit mode.

This drawing code looks pretty nice, but right now there’s no way for the user to switch to the level editor!

As an exercise, try to create the menu that will allow the user to switch to the level editor. Protip: you can create the new menu by following the same menu creation steps as above.

So go ahead, give it a shot!

Solution Inside SelectShow

Build and run your project! Once the game is running, hit the menu to switch to the editor. You should see the jungle background and a menu at the bottom.

Selecting the “Play Level” option should bring you back to the game, as seen below:

Switching back and forth using your menus

Drawing Ropes On-screen

You now need to implement the code that draws the level elements on the screen. For the pineapple, this is relatively easy. However, the ropes are slightly more complex.

You might as well tackle the harder problem first — start with the ropes! :]

The ropes can have variable length and orientation. The easiest way to draw them would be to use one sprite and scale and rotate it accordingly. However, this can end up looking rather ugly, as shown in the example below:

Rotating and stretching the rope sprite to any orientation: Looking ugly.

What you want is to have the ropes sized consistently. The easiest way to do this is to use a small rope segment graphic that is chained or tiled so as to create a rope of any desired length.

In order to encapsulate this drawing code, create a new Objective-C class in the LevelEditor group. Name the class RopeSprite and make it a sub-class of NSObject.

Switch to RopeSprite.h and replace its contents with the following:

#import "cocos2d.h"
#import "Constants.h"
#import "RopeModel.h"

@interface RopeSprite : NSObject

@property (readonly, getter = getID) int id;

-(id)initWithParent:(CCNode*)parent andRopeModel:(RopeModel*)aRopeModel;

-(int)getID;
-(CGPoint)getAnchorA;
-(CGPoint)getAnchorB;
-(int)getBodyAID;
-(int)getBodyBID;

-(void)setAnchorA:(CGPoint)anchorA;
-(void)setAnchorB:(CGPoint)anchorB;

@end

This defines some essential methods and properties. Most of the methods are simple getters and setters.

You might ask, “Why aren’t we simply using properties?”. Properties are easy ways to implement getters and setters; however, in this case you want to add custom code to the setter so that it redraws the rope when a property is changed.

Note: You could have used properties and then overridden the getter and setter in your implementation. However in this tutorial the getter and setter approach used above does a better job of explaining each implementation step in detail.

Switch to RopeSprite.m and replace its contents with the following code:

#import "RopeSprite.h"
#import "CoordinateHelper.h"

@interface RopeSprite () {
    CCSprite* ropeSprite;
    RopeModel* ropeModel;
}

@end

@implementation RopeSprite

@end

The above adds some private variables to RopeSprite. The CCSprite is required to draw the rope. Additionally, you need a RopeModel, which gives you all the placement information for the rope.

Now add the following to RopeSprite.m:

-(id)initWithParent:(CCNode*)parent andRopeModel:(RopeModel*)aRopeModel {
    self = [super init];
    if (self) {
        ropeModel = aRopeModel;
        
        ropeSprite = [CCSprite spriteWithFile:@"rope_texture.png"];
        ccTexParams params = {GL_LINEAR,GL_LINEAR,GL_REPEAT,GL_CLAMP_TO_EDGE};
        [ropeSprite.texture setTexParameters:&params];
        
        [self updateRope];
        [parent addChild:ropeSprite];
    }
    return self;
}

The above method takes two parameters. The parent parameter is the reference to the node on which the rope will be drawn. This parameter is followed by the model, which contains all information required to draw the rope. The code then stores the rope model in an instance variable.

Next, the code creates a CCSprite from the file “rope_texture.png”. This image file contains only one small segment of the rope. The whole rope is then drawn by repeating this sprite over and over.

You could accomplish the same thing by drawing the same sprite several times, but this would require a large amount of code. Instead, it’s much more efficient to set up a rope texture to handle the drawing.

Textures in OpenGL have a few parameters that you might not be familiar with. The ccTexParams struct has the following fields:

OpenGL texture parameters

OpenGL texture parameters

minFilter is used when the surface the texture is drawn upon is smaller than the texture itself, whereas magFilter is used when the texture is smaller than the surface.

The level editor uses GL_LINEAR for both minFilter and magFilter. GL_LINEAR returns the weighted average of the four texture elements that are closest to the center of the pixel being textured. In other words, with this setting OpenGL uses linear interpolation to calculate the values of the pixels.

The wrapS and wrapT parameters let you set the wrapping behavior of the texture in the s and t coordinate.

Note: If you haven’t worked with OpenGL directly, you’re probably wondering what s and t stand for. In OpenGL the x, y, and z coordinate are used to define the position of an object in 3D space. As it would be confusing to reuse the x and y coordinate names for the texture coordinates, they’re simply re-labeled s and t.

You want the rope to repeat itself along the s-axis, which is done using the GL_REPEAT value. Along the t-axis, you want it to display just once without any scaling. For this, you use the GL_CLAMP_TO_EDGE value.

Note: There are several other filter values for texture parameters. You can look them up in the OpenGL Documentation.

You now have a CCSprite with a texture which will repeat along one axis while it stays unchanged along the other axis. That’s pretty neat! :]

The only thing you need to do now in order to display the rope properly on the screen is to update its length and rotation. This magic takes place in updateRope, which you’ll implement below.

Add the updateRope implementation to RopeSprite.m as shown below:

-(void)updateRope {
    CGPoint anchorA = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
    CGPoint anchorB = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
    float distance = ccpDistance(anchorA, anchorB);
    CGPoint stickVector = ccpSub(ccp(anchorA.x,anchorA.y),ccp(anchorB.x,anchorB.y));
    float angle = ccpToAngle(stickVector);
    ropeSprite.textureRect = CGRectMake(0, 0, distance, ropeSprite.texture.pixelsHigh);
    ropeSprite.position = ccpMidpoint(anchorA, anchorB);
    ropeSprite.rotation = -1 * CC_RADIANS_TO_DEGREES(angle);
}

The code above uses ccpDistance to calculate the distance between the two anchor points, which equals the length of the rope. It then calculates the rotation angle of the rope with ccpToAngle, which takes a vector and translates it into an angle in radians.

Next, the code changes the rope texture’s rectangle using the rope length calculated above. Then it updates the rope’s position. Keep in mind that the anchor point is at the middle of the ropeSprite, so its position is exactly in the middle of the two anchor points.

Finally, the code sets the angle of the rope sprite. As the angle is currently in radians, you need to transform it into degree notation with CC_RADIANS_TO_DEGREES.

This is how you draw a rope having arbitrary length without using a texture that is stretched in ugly ways. While it requires more code than simple scaling, it looks far, far better. As an added bonus, you’ve probably learned something along the way about OpenGL that you can use in your other projects!

The RopeSprite class is almost done. All that’s left is to add the getter and setter calls.

Add the following code to RopeSprite.m:

-(void)setAnchorA:(CGPoint)theAnchorA {
    ropeModel.anchorA = [CoordinateHelper screenPositionToLevelPosition:theAnchorA];
    [self updateRope];
}

-(void)setAnchorB:(CGPoint)theAnchorB {
    ropeModel.anchorB = [CoordinateHelper screenPositionToLevelPosition:theAnchorB];
    [self updateRope];
}

-(int)getID {
    return ropeModel.id;
}

-(CGPoint)getAnchorA {
    return [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
}

-(CGPoint)getAnchorB {
    return [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
}

-(int)getBodyAID {
    return ropeModel.bodyAID;
}

-(int)getBodyBID {
    return ropeModel.bodyBID;
}

The getters and setters simply call updateRope whenever the position of one of the rope anchors is changed. This will redraw the rope to reflect the changes.

Build and run your app! You should now see…oh, wait. That’s just the same empty screen as before, isn’t it?

iOS-Simulator.png

Why don’t you see anything new?

You’ve implemented the RopeSprite class — but you’re not using it yet to draw anything on screen! That comes next. :]

Drawing the Game Objects On-screen

After putting so much work into the rope drawing methods, you are nearly ready to draw the level.

There’s a new variable to add to LevelEditor.mm, an array that stores all of the rope sprites in the level.

First add the following import to the top of LevelEditor.mm:

#import "RopeSprite.h"

Now add the declaration of the new ropes array to the class extension (the @interface block) at the top of LevelEditor.mm:

    NSMutableArray* ropes;

Okay, now you have everything you need to draw the current level on the screen. Time to pull everything together and see the results of your hard work!

Add the following lines of code to drawLoadedLevel in LevelEditor.mm, replacing the existing TODO lines:

    // Draw pineapple
    for (PineappleModel* pineapple in fileHandler.pineapples) {
        [self createPineappleSpriteFromModel:pineapple];
    }
    // Draw ropes
    ropes = [NSMutableArray arrayWithCapacity:5];
    for (RopeModel* ropeModel in fileHandler.ropes) {
        [self createRopeSpriteFromModel:ropeModel];
    }

The above code simply iterates over all pineapples and ropes stored in the fileHandler. For each of the models you then create a visual representation.

Now implement the method that creates the pineapple sprites by adding the following method to LevelEditor.mm:

-(void)createPineappleSpriteFromModel:(PineappleModel*) pineappleModel {
    CCSprite* pineappleSprite = [CCSprite spriteWithSpriteFrameName:@"pineapple.png"];
    pineappleSprite.tag = pineappleModel.id;
    CGPoint position = [CoordinateHelper levelPositionToScreenPosition:pineappleModel.position];
    pineappleSprite.position = position;
    [pineapplesSpriteSheet addChild:pineappleSprite];
}

The above method creates a sprite that contains the pineapple graphic. It then retrieves the ID and position of the pineapple from the pineappleModel variable and assigns them to the pineappleSprite accordingly. Finally, it adds the pineapple sprite to the pineapplesSpriteSheet.

The method for creating the rope sprites follows the same logic.

Add the method below to LevelEditor.mm:

-(void)createRopeSpriteFromModel:(RopeModel*)ropeModel {
    CGPoint anchorA;
    if (ropeModel.bodyAID == -1) {
        anchorA = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
    } else {
        PineappleModel* pineappleWithID = [fileHandler getPineappleWithID:ropeModel.bodyAID];
        anchorA = [CoordinateHelper levelPositionToScreenPosition:pineappleWithID.position];
    }
    
    CGPoint anchorB;
    if (ropeModel.bodyBID == -1) {
        anchorB = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
    } else {
        PineappleModel* pineappleWithID = [fileHandler getPineappleWithID:ropeModel.bodyBID];
        anchorB = [CoordinateHelper levelPositionToScreenPosition:pineappleWithID.position];
    }
    
    RopeSprite* ropeSprite = [[RopeSprite alloc] initWithParent:ropeSpriteSheet andRopeModel:ropeModel];
    [ropes addObject:ropeSprite];
}

The method first determines the anchor position for the rope. If the bodyID is -1 it takes the anchorPosition value stored in the ropeModel. Otherwise, it uses the position of the pineapple with the given bodyID. Then it creates a RopeSprite instance using the information and adds it to the ropes array.

Build and run your game, and switch to the level editor. You should finally see something on the screen for all your hard work, as demonstrated in the screenshot below:

Sure looks finished, doesn’t it?

Detecting User Inputs: Touch, Movement and Long Press

Seeing things on screen is great and all, but you need some action too! Currently, you can’t actually do any level editing. You need to enable the level editor to handle user input.

The user interactions that you’ll handle in your editor are normal touches, drag & drop and long press.

First, add an instance variable to LevelEditor.mm to store the gesture recognizer that recognizes a long press:

    UILongPressGestureRecognizer* longPressRecognizer;

Now add the following code to LevelEditor.mm:

-(void)onEnter {
    [super onEnter];
    [[CCDirector sharedDirector].touchDispatcher addTargetedDelegate:self priority:0 swallowsTouches:NO];
    longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
    UIView* openGLView = [CCDirector sharedDirector].view;
    [openGLView addGestureRecognizer: longPressRecognizer];
}

onEnter is called whenever the view switches to the LevelEditor layer. Here, you register the LevelEditor instance as a touch handler so that it will receive calls to input methods like ccTouchBegan and ccTouchEnded. Also create longPressRecognizer and add it to the openGLView as a gesture recognizer.

Since the level editor is the delegate for touch events, you need to add the relevant delegate methods that will later handle touch input.

Add the following code to LevelEditor.mm:

-(void)longPress:(UILongPressGestureRecognizer*)longPressGestureRecognizer {
    if (longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) {
        NSLog(@"longpress began");
    }
}

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"touch began");
    return YES;
}

-(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"touch moved");
}

-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"touch ended");
}

longPress first checks the current state of the gesture recognizer. A gesture recognizer can have one of several different states. However, you only need to know when the long press began, so you just handle the UIGestureRecognizerStateBegan state. For the moment, this consists of a simple log message.

Only one more thing is missing: cleaning up after the user leaves the level editor.

Add the following code to LevelEditor.mm:

-(void)onExit {
    [super onExit];
    [[CCDirector sharedDirector].touchDispatcher removeDelegate:self];
    UIView* openGLView = [CCDirector sharedDirector].view;
    [openGLView removeGestureRecognizer: longPressRecognizer];
}

The above simply removes the layer as a touch dispatcher and also removes the gesture recognizer.

Build and run your project, and again switch to the editor mode. Take a look at the list of log messages produced in Xcode’s output window when you either tap, drag, or long press on the screen, as shown in the example below:

IOS Simulator

Adding the Popup Editor Menu

It feels pretty good to see all this in action, doesn’t it? However, log messages alone do not make an editor! Time to allow the user to interact with the objects on the screen.

The first problem to be solved is how to add new objects. You already determined that you need to be conservative with screen space. This is why you won’t have another menu on screen to select new items to be added. Instead, the editor will open a popup menu, which will allow the user to select between adding ropes or pineapples.

When the player taps the screen you want a popup menu to appear that allows the user to select between creating a pineapple or a rope. Here’s how it will look:

The popup menu

Create a new Objective-C class named PopupMenu with CCLayer as the super class.

Now switch to PopupMenu.h and replace its contents with the following:

#import "cocos2d.h"

@protocol PopupMenuDelegate

-(void)createPineappleAt:(CGPoint)position;
-(void)createRopeAt:(CGPoint)position;

@end

@interface PopupMenu : CCLayer

@property id<PopupMenuDelegate> delegate;

-(id)initWithParent:(CCNode*)parent;

-(void)setPopupPosition:(CGPoint)position;
-(void)setMenuEnabled:(BOOL)enable;
-(void)setRopeItemEnabled:(BOOL)enabled;

-(BOOL)isEnabled;

@end

The above code declares a new protocol. A protocol defines an interface between the PopupMenu and any other classes that want to be notified whenever the user selects an item in the menu.

This protocol defines two methods, createPineappleAt: and createRopeAt:, which will be called when an instance of the respective object is created.

The PopupMenu class definition adds a reference to an instance of PopupMenuDelegate. This will be the concrete instance that will be called when the user does something in your menu.

Open up PopupMenu.m and replace its contents with the following:

#import "PopupMenu.h"
#import "RopeSprite.h"

@interface PopupMenu () {
    CCSprite* background;
    CCMenu* menu;
    CCMenuItem* ropeItem;
    CGPoint tapPosition;
    BOOL isEnabled;
}

@end

@implementation PopupMenu

@end

As usual, this is simply a skeleton with the relevant imports and private variables. The variables add references to the background and to the menu. Additionally, there’s a pointer to the rope menu item which allows you to change the menu item state. You need this because creating a rope should only be possible if there is at least one pineapple you can tie it to.

Then there’s tapPosition, which will store the screen position where the player tapped to open the popup menu. This is the location where the arrow of the popup menu will point. isEnabled indicates whether the popup menu is currently visible on the screen for the player to tap.

Now add the following code to PopupMenu.m:

-(id)initWithParent:(CCNode*) parent {
    self = [super init];
    if (self) {
        CCSprite* pineappleSprite = [CCSprite spriteWithFile:@"pineappleitem.png"];
        CCSprite* pineappleSpriteSelected = [CCSprite spriteWithFile:@"pineappleitem.png"];
        pineappleSpriteSelected.color = ccc3(100, 0, 0);
        CCMenuItemImage* pineappleItem = [CCMenuItemImage itemWithNormalSprite:pineappleSprite selectedSprite:pineappleSpriteSelected target:self selector:@selector(createPineapple:)];
        
        CCSprite* ropeSprite = [CCSprite spriteWithFile:@"ropeitem.png"];
        CCSprite* ropeSpriteSelected = [CCSprite spriteWithFile:@"ropeitem.png"];
        CCSprite* ropeSprite3 = [CCSprite spriteWithFile:@"ropeitem.png"];
        ropeSpriteSelected.color = ccc3(100, 0, 0);
        ropeSprite3.color = ccc3(100, 100, 100);
        ropeItem = [CCMenuItemImage itemWithNormalSprite:ropeSprite selectedSprite:ropeSpriteSelected disabledSprite:ropeSprite3 target:self selector:@selector(createRope:)];
        
        menu = [CCMenu menuWithItems: pineappleItem, ropeItem, nil];
        background = [CCSprite spriteWithFile:@"menu.png"];
        [background addChild:menu z:150];
        [self addChild:background];
        [parent addChild:self z:1000];
        [self setMenuEnabled:NO];
    }
    return self;
}

This new method takes a CCNode as parameter. This node will be the parent of the popup menu. The rest of the method implementation is relatively straightforward; it creates some CCSprites and a CCMenu and adds them to the parent node. The method also disables the menu since it should only appear and be enabled when requested by the user.

The image below shows the parts that combine to make the popup menu:

Parts of the popup menu.

To start, you have the background image. The background image consists of a bubble (containing the menu) and an arrow (the anchor point for the menu should be set to tip of this arrow). The menu contains two menu items: one for the pineapple, the other for the rope.

Positioning the Menu

Add the following code to PopupMenu.m set the correct position for the menu:

-(void)setPopupPosition:(CGPoint)position {
    tapPosition = position;
    
    // load defaultBackground and use its size to determine whether the popup still fits there
    CCSprite* defaultBackground = [CCSprite spriteWithFile:@"menu.png"];
    CGSize defaultBackgroundSize = defaultBackground.contentSize;
    float contentScaleFactor = [CCDirector sharedDirector].contentScaleFactor;
    float padding = defaultBackgroundSize.width*0.1f*contentScaleFactor;
    [menu alignItemsHorizontallyWithPadding:padding];
    
    CGPoint anchorPoint = CGPointMake(0.5f, 0.0f);
    CGPoint menuPosition = CGPointMake(defaultBackgroundSize.width/2, defaultBackgroundSize.height*0.7f);
    
    // TODO: adjust anchorPoint and orientation of menu, to make it fit the screen
    
    background.anchorPoint = anchorPoint;
    background.position = position;
    background.opacity = menu.opacity;
    
    menu.position = menuPosition;
}

In order to align the menu properly, the above code first loads the background sprite of the menu and then gets its size. This value is used to calculate the padding between the pineapple and rope menu items.

The method then requests the contentScaleFactor from CCDirector. The contentScaleFactor can be used to translate pixel positions to point positions.

On iOS, all coordinates are usually given as points. This has an advantage in that a position in points is the same on both retina and non-retina displays. However, the padding between items in a menu in Cocos2D is for some reason still given in pixels. Therefore, you need to use the contentScaleFactor to translate the padding from points into pixels.

Next, the anchorPoint and menuPosition are set to their default values. The anchor point is set to the tip of the arrow, which is in the bottom center of the graphic.

The menu position is set as follows: the x position is set to the center of the menu’s background graphic. The y position needs to take the arrow into account. It is about as tall as one-third of the background graphic. Placing the menu at about two thirds of the background graphic height will therefore make it appear just in the right spot.

So far, this seems to be going pretty well. There’s only a few nasty warnings about unimplemented methods from Xcode. Good guy, that Xcode. Nice of him to remind you. :]

Keep him happy and add those missing methods now.

Add the following methods to PopupMenu.m:

-(BOOL)isEnabled {
    return isEnabled;
}

-(void)setMenuEnabled:(BOOL)enable {
    for (CCMenuItem* item in menu.children) {
        item.isEnabled = enable;
    }
    isEnabled = enable;
    int opacity;
    if (enable) {
        opacity = 255;
    } else {
        opacity = 0;
    }
    background.opacity = opacity;
    menu.opacity = opacity;
}

-(void)setRopeItemEnabled:(BOOL)enabled {
    ropeItem.isEnabled = enabled;
}

-(void)createPineapple:(id)sender {
    [self.delegate createPineappleAt:tapPosition];
}
 
-(void)createRope:(id)sender {
    [self.delegate createRopeAt:tapPosition];
}

Most of the above methods are fairly straightforward. setMenuEnabled:, as the name implies, allows you to enable or disable the menu. The method simply sets all the menu items to the appropriate state and then adjusts the opacity of the menu, where 255 means completely visible and 0 means invisible.

setRopeItemEnabled: allows the state of the rope menu item to be toggled. This is necessary as it does not always make sense to add a rope in the current context, and you want to prevent your users from creating invalid levels.

The last two methods are called whenever you tap the menu items for a pineapple or a rope. All they do is to forward the signal to the delegate.

Time to put your menu to work! Go to LevelEditor.h and add the following import:

#import "PopupMenu.h"

Next, add PopupMenuDelegate to the @interface line so that it looks like this:

@interface LevelEditor : CCLayer<CCTouchOneByOneDelegate, PopupMenuDelegate>

Your LevelEditor class now implements PopupMenuDelegate. This means that it can listen to orders from the popup menu now.

Switch to LevelEditor.mm and add an instance variable for the popup menu to the @interface block at the top:

    PopupMenu* popupMenu;

Now implement the popup menu delegate methods as follows in LevelEditor.mm:

-(void) createPineappleAt:(CGPoint) position {
    NSLog(@"create pineapple");
    [popupMenu setMenuEnabled:NO];
}

-(void) createRopeAt:(CGPoint) position {
   NSLog(@"create rope");
   [popupMenu setMenuEnabled:NO];
}

For the moment, the delegate methods will simply log that they were called and then close the popup menu.

If you were to build and run the code now, the popup menu would never even show up, since there’s nothing to enable the menu.

Add the following method to LevelEditor.mm:

-(void)togglePopupMenu:(CGPoint)touchLocation {
    if (!popupMenu) {
        popupMenu = [[PopupMenu alloc] initWithParent:self];
        popupMenu.delegate = self;
    }
    if (popupMenu.isEnabled) {
        [popupMenu setMenuEnabled:NO];
    } else {
        [popupMenu setPopupPosition:touchLocation];
        [popupMenu setMenuEnabled:YES];
        if ([[pineapplesSpriteSheet children] count] < 1) {
            [popupMenu setRopeItemEnabled:NO];
        }
    }
}

The code above, as advertised, toggles the state of the menu. It first determines whether the popup menu already exists. If it doesn't, it creates a new instance and registers LevelEditor as the delegate.

If the menu is currently enabled, you disable it. If it is disabled, you enable it, and set its position to the touch location. If there are currently no pineapples in the level, you disable the rope item in the popup menu.

All that is left now is to actually call togglePopupMenu: when the player taps the screen. What method in LevelEditor.mm would you need to change for that?

ccTouchBegan:, of course!

Replace the current code for ccTouchBegan: with the following:

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint touchLocation = [touch locationInView:touch.view];
    touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
    
    [self togglePopupMenu:touchLocation];
    return YES;
}

The first two lines calculate the location where the user tapped the screen. That position is then used to display or hide the popup menu.

Build and run your project and switch to the level editor. You should now be able to show and hide your newly created popup menu, as shown below:

The current look of your popup menu

So what do you think? Did you notice anything awkward about the presentation of the menu as you tapped around the top or edges of the screen?

Depending on how close you click to the edge, there are a few places where the menu is cut off by the edge of the screen! It looks unpolished, to say the least.

To correct this, you'll need to adjust the background graphic and its anchor point so that the tip of arrow always points directly at the tap position and the menu box is always fully visible.

Implementing Dynamic Popup Menu Positioning

Instead of using complex algorithms to calculate the orientation and position of the popup, you'll separate the screen into several areas. Each area will have its own configuration for the popup menu. You can see a possible screen division in the following graphic:

Separation of the screen into different ares where the overlay menu is rotated so that it fits on the screen.

The actual division of the screen into areas should be done based on the size of the menu background graphic in its default orientation. Why? If you base all of the position calculations on the popup size, then you can later switch to a different popup graphic while still retaining the popup menu and its positioning mechanism.

The image below shows the adjustments needed to position the menu in each of the screen locations:

A demonstration of the changes to the popup menu when it is positioned in the top-right corner.

First, you want the arrow of your background image to point to the touched location. You can easily do this by setting the anchor point of the arrow. In its default orientation, the arrow points at the bottom middle, which corresponds to an anchor point of (0.5, 0.0).

However, when the popup menu is in the top-right corner, the arrow needs to point at the top-right corner. Therefore, you need to adjust the anchor point of the arrow to be (1, 0.75). You also need to adjust the position of the menu accordingly.

Make note that the menu is a child of your menu background, so you must always place it within the background coordinates. Also note that the alignment of the menu items sometimes needs to be switched from horizontal to vertical.

It sounds daunting, but you'll see that it's a fairly straightforward exercise to change the drawing code so that it positions everything properly.

Switch to PopupMenu.m and find the TODO line in setPopupPosition:.

Replace the TODO line with the following code:

    // Menu horizontal alignment
    CGSize winSize = [CCDirector sharedDirector].winSize;
    NSString* horizontalAlignment;
    if (position.x < defaultBackgroundSize.width/2) {
        // left
        horizontalAlignment = @"left";
        anchorPoint.x = 0.0f;
        [menu alignItemsVerticallyWithPadding:padding];
        menuPosition.x = defaultBackgroundSize.height * 0.7f;
        menuPosition.y = defaultBackgroundSize.width * 0.5f;
    } else if (winSize.width-position.x < defaultBackgroundSize.width/2) {
        // right
        horizontalAlignment = @"right";
        anchorPoint.x = 1.0f;
        [menu alignItemsVerticallyWithPadding:padding];
        menuPosition.x = defaultBackgroundSize.height * 0.3f;
        menuPosition.y = defaultBackgroundSize.width * 0.5f;
    } else {
        // center
        horizontalAlignment = @"center";
        [menu alignItemsHorizontallyWithPadding:padding];
    }

Have a look at how you divide the screen area into left, center and right sections. This is done based on the width of defaultBackgroundImage. If the x-coordinate is smaller than half of the background image's width, the menu would stick out on the left. Therefore, you set the horizontal alignment to left and adjust anchorPoint.

As well, you set the alignment of the menu to vertical and adjust the position of the menu within the popup menu.

The same is done for the right side, only mirrored along the x-axis. In all other cases, the horizontal alignment is set to center, and the menu is aligned horizontally.

Here's a graphic showing the coordinates and alignment of the popup menu elements for each possible orientation:

The horizontal alignment of the popup has already been handled in the code you added above, but what about vertical orientation and the corresponding anchor point? That's your job to figure out! :]

Don't worry, the spoiler below is there to reference once you've attempted to implement the code in setPopupPosition: yourself.

Solution Inside SelectShow

How did you do? With the addition of the vertical alignment code, you now have the vertical and horizontal alignment of the popup menu and you have stored the matching anchor point. Now use that information to perfect the drawing of your popup menu.

Add the following code to PopupMenu.m right after the horizontal and vertical alignment detection code in setPopupPosition::

    // Draw the menu 
    NSString* filename = [NSString stringWithFormat: @"menu-%@-%@.png", verticalAlignment, horizontalAlignment];
    CCTexture2D* tex = [[CCTextureCache sharedTextureCache] addImage:filename];
    if (!tex) {
        tex = [[CCTextureCache sharedTextureCache] addImage:@"menu.png"];
    }
    [background setTexture:tex];
    [background setTextureRect: CGRectMake(0, 0, tex.contentSize.width, tex.contentSize.height)];

The first line builds a string containing the filename of the background image that is suitable for the current orientation. Conveniently, the files for each orientation have been named following the pattern menu-verticalAlignment-horizontalAlignment.png.

A texture is then created using the filename of the image. If the texture could be created (i.e. it's valid), then set it as the new texture for the background sprite. Otherwise, use the default texture.

Don't forget that the background images aren't all the same size. This is why you also need to set the texture rect to fit that of the new texture.

Build and run your app, and change to the editor mode. After all this hard work, the menu will now adjust its orientation to always fit the screen, as shown below:

iOS-Simulator.png

Adding New Game Objects - Pineapples

Now that the popup menu works properly, you can use it to insert new game objects into your level. Sounds like you're getting close to the end product, doesn't it? :]

Start with the pineapples, as they are much simpler to implement than the ropes.

Think for a moment about what you'll need to do to add a new object to your level:

  1. Create a model that represents the game object
    • Generate a unique ID
    • Set all object parameters
  2. Add the model to the level file handler, which is responsible for loading and storing level data
  3. Create a visualization that represents the model on screen

First, you need a unique ID. If you make the assumption that there is only one application thread which is responsible for creating and deleting objects, then a relatively simple way to generate a unique ID is to sort the array of game objects.

Once that array is sorted, you iterate over the sorted list. The first index that is missing is also the first unused ID.

Open LevelFileHandler.m and add the following method to sort the game object array:

+(void)sortArrayById:(NSMutableArray*)objectArray {
    NSSortDescriptor* sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"id" ascending:YES];
    NSArray* sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
    [objectArray sortUsingDescriptors:sortDescriptors];
}

This might look quite short for a sorting algorithm. That's because Objective-C already has a handy sorting algorithm implemented! No need to re-invent the wheel. :]

You only need to tell the sorting algorithm the criteria it should use to sort. You do this by using NSSortDescriptors. In this particular case, you create a very simple descriptor that sorts by a property named "id" in ascending order.

You then add this sort descriptor to an array and pass the array on to sortUsingDescriptors: which actually performs the sorting of the array. While passing the sort descriptor via an array might seem cumbersome, it can also come in handy if you ever want to sort the array based on more than one property.

On to the next step! Add the following method to LevelFileHandler.m:

+(NSNumber*)firstUnusedIdInArray:(NSArray*)array {
    NSNumber* firstUnusedID = nil;
    int lastID = 0;
    for (AbstractModel* object in array) {
        if (object.id - lastID > 1) {
            firstUnusedID = [NSNumber numberWithInt:lastID + 1];
            break;
        }
        lastID++;
    }
    if (!firstUnusedID) {
        firstUnusedID = [NSNumber numberWithInt:lastID + 1];
    }
    return firstUnusedID;
}

This method first creates two variables. firstUnusedID stores the first unused ID, while lastID is used to store the last ID that the code looked at. You then iterate over all models contained in the array of game objects.

In each iteration you check whether the difference between the ID of the current game object and the last ID is greater than one. If yes, then you have found an ID that is currently not in use, so you store the value of the unused ID and exit the loop.

It is possible that you won't find an unused ID in the game object array. In this case, firstUnusedID would still be nil. Then all you need to do is set firstUnusedID to the value of lastID plus one.

You can now use the ID returned from the above method to generate a PineappleModel for your new pineapple.

Add the following code to LevelFileHandler.m:

-(PineappleModel*)addPineappleAt:(CGPoint)position {
    NSNumber* firstUnusedID = [LevelFileHandler firstUnusedIdInArray:self.pineapples];
    PineappleModel* newPineapple = [[PineappleModel alloc] init];
    newPineapple.id = [firstUnusedID intValue];
    newPineapple.position = position;
    [self.pineapples addObject:newPineapple];
    return newPineapple;
}

This code is now a straightforward implementation of the bullet points above for generating a new game object that was discussed earlier. Have a look back to the bullet list if you don't believe it! :]

You grab an unused ID, create a new PineappleModel instance, and assign all the important parameters like ID and position. Then you add the newly created pineapple to the list of pineapples. Finally, you return the newly created pineapple.

Since you'll need to access the above method from outside the LevelFileHandler class, it needs to be public.

Add the following method prototype to LevelFileHandler.h:

-(PineappleModel*)addPineappleAt:(CGPoint)position;

Now you can call addPineappleAt: from the level editor to create new pineapples when the user requests them.

Switch to LevelEditor.mm and replace the existing dummy implementation of createPineappleAt: with the following:

-(void) createPineappleAt:(CGPoint) position {
    CGSize winSize = [CCDirector sharedDirector].winSize;
    PineappleModel* pineappleModel = [fileHandler addPineappleAt:CGPointMake(position.x/winSize.width, position.y/winSize.height)];
    [self createPineappleSpriteFromModel:pineappleModel];
    [popupMenu setMenuEnabled:NO];
}

The above code gets the size of screen and uses it to calculate the level position and create a PineappleModel from this information. The PineappleModel in turn is then used to create a PineappleSprite, which presents the pineapple on-screen. Finally, the popup menu is disabled so that it does not show anymore.

Congratulations! You now have implemented the first interaction that lets the user modify the level.

Build and run your app, and go crazy adding as many pineapples as you like to the level!

iOS-Simulator.png

Adding New Game Objects - Ropes

Adding new ropes to the level requires a little more thought. If you simply let the user add a rope wherever they want, most of the levels will be invalid since a rope needs to be connected to two different bodies. These bodies can either be two different pineapples, or a pineapple and the background.

You want to ensure that a user cannot create any invalid levels by creating ropes that aren't connected to two valid bodies.

One way to do this is to only accept a pineapple as the first anchor. You can then accept any other object, except the first pineapple, as the second anchor.

But how do you know which step the user is currently on so that you can determine whether you're placing the first or the second anchor?

You can use a state machine for this. Take a look at the following diagram that shows the state machine to be used in this project:

How to add a rope - presented to you by the state chart ;)

How to add a rope - presented to you by the state chart ;)

To implement this, you'll require three states:

  1. kEditMode, where the user can move objects, delete objects, and add new objects. As soon as the user choses to add a rope, the level editor switches to the second state.
  2. kRopeAnchorPineappleMode, where only pineapples can be selected. When a selection is made, the mode switches to the third state.
  3. kRopeAnchorAnyMode, where the user is limited to choosing between any pineapples other than the first one or any position on the background. Once a choice is made, the editor switches back to the first mode.

Switch to LevelEditor.mm and add the following code directly after the imports:

enum {
    kEditMode,
    kRopeAnchorPineappleMode,
    kRopeAnchorAnyMode
} typedef editorMode;

The code above creates an enum for the aforementioned states to simplify your implementation of the state machine.

Add the following code to the @interface block of LevelEditor.mm:

    editorMode mode;
    RopeModel* newRope;

Here, you add an instance variable that stores the current mode, along with a variable to store a reference to the new rope.

Although using states in your code to prevent the creation of invalid levels is useful, you should also indicate the current state to the user. By visually indicating state transitions to the user, it's readily apparent which mode they are currently working in and what actions they can perform.

You'll indicate the current state by adding a color effect to the screen. Red will indicate items that cannot be selected, and green for items that can be selected.

Add the following method to LevelEditor.mm:

-(void)setMode:(editorMode)newMode {
    mode = newMode;
    switch (mode) {
        case kRopeAnchorPineappleMode:
            background.color = kDefaultDisabledColor;
            for (CCSprite* pineapple in [pineapplesSpriteSheet children]) {
                pineapple.color = kDefaultSelectableColor;
            }
            break;
        case kRopeAnchorAnyMode:
            background.color = kDefaultSelectableColor;
            for (CCSprite* pineapple in [pineapplesSpriteSheet children]) {
                if (pineapple.tag == newRope.bodyAID) {
                    pineapple.color = kDefaultDisabledColor;
                } else {
                    pineapple.color = kDefaultSelectableColor;
                }
            }
            break;
        case kEditMode:
        default:
            background.color = kDefaultBackgroundColor;
            for (CCSprite* pineapple in [pineapplesSpriteSheet children]) {
                pineapple.color = ccc3(255, 255, 255);
            }
            break;
    }
}

So how does the code above work? First, you store the new mode, then you create a switch statement to distinguish between the three different states, where the kEditMode state is assumed to be the default.

For each state, you use CCSprite’s color property to color the object on the screen appropriately:

  • In state kRopeAnchorPineappleMode, you set the background color to disabled, and enable all pineapples.
  • In state kRopeAnchorMode, you change the coloring so that the background is now active, as well as all pineapples — except the one with newRope's first anchor's ID.
  • Finally, in kEditMode, set the background color and the color of all pineapples to their default color.

It's now time to see whether this code does actually what you want!

Find createRopeAt: in LevelEditor.mm and replace the current dummy implementation with the following:

-(void)createRopeAt:(CGPoint)position {
    [self setMode:kRopeAnchorPineappleMode];
    newRope = [[RopeModel alloc] init];
    [popupMenu setMenuEnabled:NO];
}

When the user decides to create a new rope, you set the mode to kRopeAnchorPineappleMode. This will highlight all of the pineapples, which indicates to the user that only these objects are available as the first anchor for the rope. Next, the new rope is set up with an empty model. Finally, you close the popup menu since it's not needed anymore.

Build and run your project, and select the rope in the popup menu. You'll see the background turn red and the pineapples all turn green, as in the screenshot below:

iOS-Simulator.png

Well, that looks pretty neat, but you still can't actually add a new rope yet! Have a look at the state chart again and figure out what you have to do next:

How to add a rope - presented to you by the state chart ;)

How to add a rope - presented to you by the state chart ;)

Ah — you need to detect if the player touched a pineapple. To do this you'll need a method which takes a position on the screen as a parameter and returns the CCSprite containing the pineapple at that position.

Add the following code to LevelEditor.mm:

-(CCSprite*)pineappleAtPosition:(CGPoint)position {
    for (CCSprite* pineapple in [pineapplesSpriteSheet children]) {
        if (CGRectContainsPoint(pineapple.boundingBox, position)) {
            return pineapple;
        }
    }
    return nil;
}

The above code iterates over all of the pineapples. For each pineapple, it uses CGRectContainsPoint to see if the given position is within the pineapple's boundingBox. If so, the appropriate pineapple is returned. If the given point doesn't fall within any of the pineapple's bounding boxes, then the method will return nil.

Now you'll need a method that will perform the selection of the first anchor point.

Add the following code to LevelEditor.mm:

-(void)selectFirstAnchor:(CGPoint)touchLocation {
    // if user tapped on pineapple set it as anchor of the new rope
    CCSprite* tappedPineapple = [self pineappleAtPosition:touchLocation];
    if (tappedPineapple) {
        [newRope setAnchorA:[CoordinateHelper screenPositionToLevelPosition:tappedPineapple.position]];
        newRope.bodyAID = tappedPineapple.tag;
        [self setMode:kRopeAnchorAnyMode];
    }
}

The above method takes a touch location as input. It then looks for a pineapple at that location. If it finds one, it sets the anchor of the rope to the location of the pineapple. This creates the visual impression that the rope is connected to the pineapple.

After the anchor is set, you store the ID of the anchor in the rope, Finally, the method kicks off the transition to the next state to allow the user to select the second anchor.

Head back to ccTouchBegan: in LevelEditor.mm and change it as follows:

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint touchLocation = [touch locationInView:touch.view];
    touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
    
    switch (mode) {
        case kEditMode:
            [self togglePopupMenu:touchLocation];
            break;
        case kRopeAnchorPineappleMode:
            [self selectFirstAnchor:touchLocation];
            break;
        case kRopeAnchorAnyMode:
            
            break;
    }
    return YES;
}

The above code is slightly more complicated than before. Now, it uses a switch statement to distinguish between the different states.

In kEditMode, the user can freely toggle the popup menu. However, in kRopeAnchorPineappleMode the user can only select a pineapple as the first anchor point, and the popup menu will not show up.

Build and run your app, open the popup menu and select the rope. Next, select a pineapple to attach the rope to. You should see the transition between kEditMode, kRopeAnchorPineappleMode, and kRopeAnchorAnyMode, as shown in the successive screenshots below:

Visualizations of the different edit modes for the LevelEditor

Visualizations of the different edit modes for the LevelEditor

The visualization works, and the screen cycles between the different colored states, and the user can select the first anchor point.

But the ability to select the second anchor point still missing!

Luckily, the implementation of selectSecondAnchor: is pretty similar to selectFirstAnchor.

Add the following code to LevelEditor.mm:

-(void)selectSecondAnchor:(CGPoint)touchLocation {
    // set second end of rope, can be either background or other pinapple, but not same pinapple as first one
    CCSprite* tappedPineapple = [self pineappleAtPosition:touchLocation];
    if (tappedPineapple && tappedPineapple.tag != newRope.bodyAID) {
        [newRope setAnchorB:[CoordinateHelper screenPositionToLevelPosition:tappedPineapple.position]];
        newRope.bodyBID = tappedPineapple.tag;
    }
    if (!tappedPineapple) {
        [newRope setAnchorB:[CoordinateHelper screenPositionToLevelPosition:touchLocation]];
        newRope.bodyBID = -1;
    }
    [self createRopeSpriteFromModel:newRope];
    [fileHandler addRopeFromModel: newRope];
    [self setMode:kEditMode];
}

The above code handles the selection of the second anchor. When the user selects a second pineapple, everything works nearly the same as in kRopeAnchorPineappleMode. The only difference here is that you check the tag for the pineapple and make sure it is different from the one selected as the first anchor.

In case the user taps somewhere on the background, you use the touch location as the position for the anchor and set the ID to -1. In both cases, you then create the rope from the model and switch back to kEditMode.

You're nearly done, but first you need to tie everything together and take your editor for a test run!

Switch to LevelFileHandler.m and add the following method:

-(void)addRopeFromModel:(RopeModel*)newRope {
    [LevelFileHandler sortArrayById: self.ropes];
    if (!newRope.id) {
        NSNumber* firstUnusedID = [LevelFileHandler firstUnusedIdInArray:self.ropes];
        newRope.id = firstUnusedID.intValue;
    }
    [self.ropes addObject:newRope];
}

This method sorts the rope array. After that, it checks whether the rope already has an ID. If not, it requests one and sets this as the ID for the new rope. Finally, the new rope is added to the ropes array.

Now add the method prototype to LevelFileHandler.h so that the method is publicly accessible:

-(void)addRopeFromModel:(RopeModel*)newRope;

But don't forget the most crucial step — setting the second anchor point when the screen is touched. Otherwise, nothing will happen when the user tries to anchor the end of the rope!

Add the following line to ccTouchBegan:, directly after the case kRopeAnchorAnyMode: line:

            [self selectSecondAnchor:touchLocation];

Build and run your app to give the editor another test drive. Add a few new pineapples and ropes to make sure that part works as designed, as shown in the screenshot below:

iOS-Simulator.png

If you tap "Play Level", you should even be able to play the level with the changes you made!

Where To Go From Here?

Here is a sample project with all of the code from the above tutorial.

You've made some serious progress on your editor so far. You'll notice that you don't yet have a way to move objects around or delete them — and your changes aren't yet persisted between sessions.

That will come in the third part of this tutorial. Until then, come discuss your progress in the forums, and I'll see you when it's time to finish everything off!

Barbara Reichart

Barbara Reichart is currently doing her PhD at TUM, where she teaches software engineering and iOS development. In her free time she develops games (Treeo Games). Her first published game is Tw!nkle. You can also follow her on Google+.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

iOS Team

... 78 total!

Android Team

... 26 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 18 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!