Create Your Own Level Editor: Part 3/3

Barbara Reichart
Finish your level editor by giving it the ability to save the levels

Finish your level editor by giving it the ability to save the levels

Welcome to the third 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 first part of this three-part 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.

In the second part, you added a popup menu along with a state machine to control the flow of the editor and provide a visual indicator of the current mode of the editor. Then you finished off by adding the ability to create new objects and place them on the screen.

In this last portion of the tutorial, you’ll add the ability to delete objects, move them around, complete the level save and reset mechanism, and implement multiple level handling to round out your level editor!

Ready to get started?

This project starts where you left off in the last tutorial, so make sure you have a copy of the project from part 2.

Getting Started: Removing Objects From Your Level

The ability to add objects to the screen is pretty rewarding, but your screen is getting more crowded by the second and — whoops! You anchored a rope where you didn’t mean to. Now what do you do?

Looks like you’re going to need some object deletion methods.

Think for a minute about the steps required to delete an object in your level:

  1. Detect which object was touched or selected by the user
  2. Remove the visual object from the scene
  3. Delete model from the level file handler
  4. Delete copy from the list in level editor

Most of the steps are quite straightforward, and you likely already know everything you need in order to implement these methods on your own. The tricky bit is knowing if a rope was selected; it’s not quite as straightforward as clicking on pineapples.

To understand what makes this touch detection more complicated, take a look at the following graphic:

Bounding box for a diagonal rope – much bigger than the rope itself…

When you rotate a rope sprite, the sprite’s bounding box will not rotate with it. Instead, it only adjusts the y and x coordinate, so that the whole sprite is contained in the bounding box. As a result, the bounding box is often several times bigger than the actual rope. Because of this, the bounding box alone is not enough to determine whether a user tapped a rope or not.

The solution is dead simple — analytic geometry and linear equations to the rescue!

Okay, okay — that makes it sound a LOT harder than it actually is! :]

You’re really just using a simple formula to detect the distance between the rope — which is essentially a line — and the spot the user touched on the screen.

This concept is illustrated in the image below:

Basic geometry helps you to calculate the distance between the touch location and the rope.

Calculating the Distance to a Line

To calculate the distance between a point and a line, simply construct two lines: one that goes through both anchor points of the rope, and another which is orthogonal, or perpendicular, to the first and passes through the touch location. The orthogonal line will always represent the shortest distance possible between the point and the line.

You next calculate the intersection point of the two lines, and then you calculate the distance between the intersection point and the touch location. Done!

Well, not quite. You know what they say about theory — it works perfectly, in theory. :]

The calculations above work for the general case of a rope of infinite length. But there isn’t quite enough room on your screen for a rope of that size — not even on your 27″ Thunderbolt display, sadly.

Since the rope is of finite length, you’ll need one extra check — to see if the user touched within the bounding box of the rope. Since fingers are larger than the ropes, you will want to add a little bit of buffer around the two anchor points, just to be sure.

Open RopeSprite.m and add the following method:

-(float)distanceToPoint:(CGPoint)position {
   CGPoint anchorA = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
   CGPoint anchorB = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
   float deltaX = anchorA.x-anchorB.x;
   float deltaY = anchorA.y-anchorB.y;
   float distance;
   if (deltaX == 0) {
       distance = fabs(anchorA.x - position.x);
   } else if (deltaY == 0) {
       distance = fabs(anchorA.y - position.y);
   } else {
       // calculate slope by dividing y-coordinate distance with x-coordinate distance
       float slope = deltaY/deltaX;
       // calculate y-intercept of rope t = y-m*x
       float yIntercept = anchorA.y - anchorA.x * slope;
       // construct line that is orthographic to rope and goes through point, we want to calculate distance for
       float slopeOrthogonal = -1/slope;
       // position.y = slopeOrthogonal * position.x + yInterceptOrthogonal => solve for yInterceptOrthogonal
       float yInterceptOrthogonal = position.y - slopeOrthogonal * position.x;
       // calculate interception between rope and orthogonal line
       float x = (yInterceptOrthogonal - yIntercept) / (slope - slopeOrthogonal);
       float y = slope * x + yIntercept;
 
       distance = ccpDistance(position, CGPointMake(x, y));
   }
   return distance;
}

Okay, there’s a fair bit of mathematical calculation going on in there, so take it step-by-step.

The method first calculates the distance between the anchors along the x and y axis. This tells you the angle of the rope. Next, there’s some code that handles two special cases.

The first special case is when deltaX is 0. This means that the rope is horizontal. You can’t use the standard calculation in this case, as you need to calculate the slope by dividing deltaY with deltaX — and the compiler gets a little cranky when you try to divide by zero. :]

However, you can easily calculate the distance between a horizontal line and a point as the absolute value of the difference between the y-coordinate of line and point. Luckily, Objective-C has a method called fabs, that calculates the absolute value of a variable for you.

The second exception is when deltaY is 0. Pop quiz: what orientation does a rope with a deltaY value of 0 have?

Solution Inside SelectShow

When deltaY is zero, you could calculate the slope of the rope; however you can’t calculate the slope of the orthogonal line due that whole division by zero problem. In this case you can calculate the distance for ropes with deltaY alone — very similar to how you handled the first special case.

If the distance calculation is not a special case, you need to create a line equation that represents the rope in the form y = mx + t; m is the slope of the line, while t point where the line intersects with the y-axis.

The slope m is calculated by dividing deltaY by deltaX. Then you insert the x- and y-coordinates of one anchor point into the above equation and solve for t.

Next you construct the equation of the orthogonal another line, which is perpendicular to the first line and passes through the touch point. The slope of the perpendicular line is the negative multiplicative inverse of the slope of the original line: that is, -1/slope.

Now that you have the slope, you again need to calculate the y-intersect. As before, insert the x- and y-coordinates of the touch point and solve for t.

Next, you need to calculate the intersection point of the two lines. You know that the (x,y) coordinate pair of both lines is exactly the same at the point where the two lines intersect. Therefore you can take the right part of both equations (the mx + t portion), and write them as being equal to each other, as below:

EquationPartI

Then move the constants to one side and collect the slope calculations on the other:

EquationPartII

Then divide both sides by the same equation to leave x by itself on the left hand side:

EquationPartIV

Now all you need to do is enter the known slopes and intercepts to get the x component of the coordinate.

Then to get the y-coordinate, simply use your original equation y = mx + t to solve for y. The code uses the orthogonal slope and intercept, but you could have easily used the rope slope and intercept to get the same answer.

You then simply call ccpDistance to calculate the distance between the touch location and the intersection point.

After all that math, you now have the distance to the rope! However, don’t forget that you also need to know the bounding box of the rope!

Oh no — will this mean another round of mathematical derivation?

No. It’s quite simple, in fact. (Did you just breathe a sigh of relief?)

Add the following code to RopeSprite.m:

-(CGRect)getBoundingBox {
   return ropeSprite.boundingBox;
}

Oh — that wasn’t so hard! :]

You’ll need a clean-up method as well to truly remove the rope.

Add the following code to RopeSprite.m as well:

-(void)cleanupSprite {
   [ropeSprite removeFromParentAndCleanup:YES];
}

The above code removes the rope sprite from the scene.

Now add the following method prototypes to RopeSprite.h:

-(float)distanceToPoint:(CGPoint)position;
-(CGRect)getBoundingBox;
-(void)cleanupSprite;

It’s time to pull it all together. Switch back to LevelEditor.mm and the following method:

-(RopeSprite*)ropeAtPosition:(CGPoint)position {
   float minDistanceToRope = 5;
   RopeSprite* closestRope;
   for (RopeSprite* ropeSprite in ropes) {
       if (CGRectContainsPoint([ropeSprite getBoundingBox], position) && [ropeSprite distanceToPoint:position] < minDistanceToRope) {
           closestRope = ropeSprite;
           minDistanceToRope = [ropeSprite distanceToPoint:position];
       }
   }
   return closestRope;
}

The above method determines if a user touched a rope.

First, you define a small distance from the rope for a touch that will still count as a touch on the rope; in this case, 5 pixels.

Next, you iterate over all the ropes and determine whether the touch position is within the bounding box of the rect and also within the minimal distance. If so, take the current rope to be the one closest to the touch location.

As well, update the minDistanceToRope variable so that you can match it against the next touch distance. This ensures that if there are multiple ropes in the touch area that you return the rope closest to the touch location.

You now have the methods for detecting the touched rope and a way to remove the rope from the scene. But don’t forget that you need to save your changes, or else next time the ropes will still be in your level!

Add the following method prototype to LevelFileHandler.h:

-(void)removeRopeWithID:(int)id;

Now switch to LevelFileHandler.m and add the following method implementation:

-(void)removeRopeWithID:(int)id {
   RopeModel* ropeToBeRemoved;
   for (RopeModel* rope in self.ropes) {
       if (rope.id == id) {
           ropeToBeRemoved = rope;
           break;
       }
   }
   [self.ropes removeObject:ropeToBeRemoved];
}

This is pretty straightforward code. Simply iterate over all of the ropes, and check the ID of each rope. If it matches the rope you should remove, store it in ropeToBeRemoved. Since there’s only one rope with a given ID, exit the loop and remove the ropeToBeRemoved instance from the array that contains all the ropes.

Now you can add the code that detects a rope at a given position on screen and removes it.

Switch to LevelEditor.mm and add the following method:

-(void)removeRopeAtPosition:(CGPoint)position {
   RopeSprite* selectedRope = [self ropeAtPosition:position];
   if (selectedRope) {
       // remove rope (from ropes, ropesSpriteSheet, fileHandler
       [selectedRope cleanupSprite];
       [fileHandler removeRopeWithID:selectedRope.id];
       [ropes removeObject:selectedRope];
       return;
   }
}

The above code looks up the rope that is at the given position. It then removes this rope from the scene, from the file handler, and from the array in the level editor. At that point, the rope is gone from the level — at least, it would be if you actually called this new method! :]

Since a long press handles removal of items, that’s where you need to add the call to this new method.

Replace the existing method stub for longPress: in LevelEditor.mm with the following code:

-(void)longPress:(UILongPressGestureRecognizer*)longPressGestureRecognizer {
   if (mode == kEditMode && longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) {
       NSLog(@"longpress began");
       NSValue* locationObject = [longPressGestureRecognizer valueForKey:@"_startPointScreen"];
       CGPoint touchLocation = [locationObject CGPointValue];
       touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
 
       [self removeRopeAtPosition:touchLocation];
   }
}

The new code simply adds some lines that calculate the location of the point the user touched on the screen. Then it calls removeRopeAtPosition: with the touch location.

Build and run your project, and switch to editing mode. Use a long press to remove a rope from the level, as shown in the before and after screenshots below:

LevelEdit

You’ve solved the harder problem first — removing ropes. Implementing the logic to remove a pineapple from the screen will be child’s play in comparison!

Deleting Pineapples From Your Level

To refresh your memory, go back to the steps required to delete an item. The code is already in place that detects if a touch falls on a pineapple, so step one is done. The second step — removing the visualization from the scene — is also quite simple as you can directly access the sprite that draws the pineapple via the level editor.

For step three, you need a method to remove a pineapple with a given ID from the level file handler. This code is very similar to the matching method for removing a rope with a given ID.

See if you can create this method yourself! If you get stuck, you can always take a peek at the spoiler below. :]

Solution Inside SelectShow

How did you do? The pineapple removal method is quite similar — and much simpler — than the rope removal method, isn’t it?

Once you have the pineapple removal in place, it’s time to tie it all together.

Add the following code to LevelEditor.mm:

-(void)removePineappleAtPosition:(CGPoint)position {
   CCSprite* selectedPineapple = [self pineappleAtPosition:position];
   if (selectedPineapple) {
       [fileHandler removePineappleWithID:selectedPineapple.tag];
       [selectedPineapple removeFromParentAndCleanup:YES];
 
       // remove all connected ropes
       NSArray* connectedRopesToBeRemoved = [self getAllRopesConnectedToPineappleWithID:selectedPineapple.tag];
       for (RopeSprite* rope in connectedRopesToBeRemoved) {
           [fileHandler removeRopeWithID:rope.id];
           [rope cleanupSprite];
           [ropes removeObject:rope];
       }
   }
}

The code above first determines which object is at the touch location. Then it removes the model from the file handler and the sprite from the scene.

In the case of pineapples, these are the only references to the pineapple in the level editor, so the pineapple has been completely removed from the level at this point.

Sorry, pineapple. Your time is up.

Sorry, pineapple. Your time is up.

Deleting Ropes Attached to Pineapples

There’s one final cleanup detail that has to be taken care of. Think about your level, and in particular, the ropes. Since a rope must be connected to a pineapple at one of the anchor points, what happens when you remove a pineapple from the scene?

That’s right — it makes sense to remove all of the ropes connected to the pineapple, for when the pineapple is removed, you’ll have a bunch of ropes in your level with invalid anchor points.

But you don’t have a way to find all ropes connected to a pineapple, do you? Hmm. Well, since a rope knows the ID of the object it’s attached to — either a rope or the background — then it should be fairly easy to iterate over the ropes and find the ones attached to the problematic pineapple!

Add the following method to LevelEditor.mm:

-(NSMutableArray*)getAllRopesConnectedToPineappleWithID:(int)pineappleID {
   NSMutableArray* tempRopes = [NSMutableArray arrayWithCapacity:5];
   for (RopeSprite* rope in ropes) {
       int bodyAID = [rope getBodyAID];
       int bodyBID = [rope getBodyBID];
 
       if (pineappleID == bodyAID || pineappleID == bodyBID) {
           [tempRopes addObject:rope];
       }
   }
   return tempRopes;
}

The above method first creates an array to store the ropes connected to the pineapple. It then iterates over all the ropes, getting the ID of both anchors. Then it’s simply a matter of comparing the anchor IDs with that of the pineapple, and removing the ropes that match that anchor ID.

Of course, the new method for removing a pineapple at the touch location now needs to be called from within longPress:. That’s easy enough.

Simply add the following line right after the call to removeRopeAtPosition: in longPress::

       [self removePineappleAtPosition:touchLocation];

Now you have the ability to remove both ropes and pineapples from the level using a long press gesture. Awesome! :)

Moving Objects In Your Level

Okay, so far you can add objects to your level and delete them as well. There’s just one more user interaction to add to your level editor — moving objects via drag and drop.

Note: if you want a real in-depth discussion about drag-and-drop, you can get the lowdown in the great tutorial How To Drag and Drop Sprites with Cocos2D.

Add the following private variables to the @interface block in LevelEditor.mm:

   NSObject* selectedObject;
   CGPoint originalRopeAnchor;

The first variable stores a reference to the currently selected object to be moved. The second stores the original anchor position before the user started the dragging action. This allows you to reset the rope to its original position in case the user attempts to position the rope on a spot other than a pineapple or the background.

Moving Objects – Ropes

You want to allow your user to move each anchor point of the rope separately in order to create ropes at any angle and of any length. Doing this requires remembering which anchor of the rope is being moved. This requires some changes to the RopeSprite class.

First, add the following method prototypes to RopeSprite.h:

-(void)setSelectedAnchorType:(anchorType)type;
-(anchorType)getSelectedAnchorType;
-(CGPoint)getSelectedAnchor;
-(void)moveSelectedAnchorTo:(CGPoint)vector;
-(BOOL)isValidNewAnchorID:(int)newAnchorID;

Now add the following private variable to the @interface block of RopeSprite.m to store the currently selected anchor:

   anchorType selectedAnchor;

Now add the following method implementations to RopeSprite.m:

-(void)setSelectedAnchorType:(anchorType)type {
   selectedAnchor = type;
}
 
-(anchorType)getSelectedAnchorType {
   return selectedAnchor;
}
 
-(CGPoint)getSelectedAnchor {
   if (selectedAnchor == kAnchorA) {
       return [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
   }
   if (selectedAnchor == kAnchorB) {
       return [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
   }
   return CGPointMake(-1, -1);
}
 
-(void)moveSelectedAnchorTo:(CGPoint)position {
   if (selectedAnchor == kAnchorA) {
       ropeModel.anchorA = [CoordinateHelper screenPositionToLevelPosition:position];
   }
   if (selectedAnchor == kAnchorB) {
       ropeModel.anchorB = [CoordinateHelper screenPositionToLevelPosition:position];
   }
   [self updateRope];
}
 
-(BOOL)isValidNewAnchorID:(int)newAnchorID {
   int unselectedAnchorID;
   if (selectedAnchor == kAnchorA) {
       unselectedAnchorID = ropeModel.bodyBID;
   } else {
       unselectedAnchorID = ropeModel.bodyAID;
   }
   if (newAnchorID == unselectedAnchorID) {
       return NO;
   }
   return YES;
}

The first three methods are some simple getters and setters – nothing really special there.

The fourth method moves the selected anchor of a rope to a specified position.

The last method is probably the most important one. It checks whether a specified anchor ID would be a valid anchor for the rope.

This is done by comparing the ID of the unselected anchor against the new anchor ID passed to the method. The new rope position is only valid if the new ID does not match the non-selected anchor — that is, you can attach a rope to anything but itself.

Even ropes have to avoid circular references! :]

You can now use the above changes in the level editor to move the rope depending on the user input.

Add the following code to LevelEditor.mm:

-(void)selectRopeAnchor:(CGPoint)touchLocation {
   for (RopeSprite* rope in ropes) {
       float distanceToAnchorA = ccpDistance([rope getAnchorA], touchLocation);
       if (distanceToAnchorA < 10) {
           selectedObject = rope;
           [rope setSelectedAnchorType:kAnchorA];
           originalRopeAnchor = [rope getAnchorA];
       }
       float distanceToAnchorB = ccpDistance([rope getAnchorB], touchLocation);
       if (distanceToAnchorB < 10) {
           selectedObject = rope;
           [rope setSelectedAnchorType:kAnchorB];
           originalRopeAnchor = [rope getAnchorB];
       }
       if (selectedObject) {
           break;
       }
   }
}

The above method checks if the user has tapped the end of a rope. To do so, it iterates over all the ropes and calculates the distance between each rope’s anchor points and the touch location.

If this distance is smaller than a specified value (10 pixels), the method assumes that the user tried to touch that end of the rope and sets the rope as the currently selected object.

Next, the method stores the current position of the anchor. As soon as a selected object is found, the code exits the loop.

All that’s left is to make use of the code you’ve added so far in ccTouchBegan:.

Modify ccTouchBegan: in LevelEditor.mm:

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
   ...
   switch (mode) {
       case kEditMode:
           // check if a rope's anchor point was tapped, if yes select it
           [self selectRopeAnchor:touchLocation];
           if (!selectedObject && ![self ropeAtPosition:touchLocation]) {
               [self togglePopupMenu:touchLocation];
           }
           break;
       ...
   }
   return YES;
}

You want to restrict your user to moving objects in the kEditMode state alone, so you only need to implement that case. Instead of simply toggling the popup menu, you now call selectRopeAnchor: first to find out whether the user touched the anchor point of a rope. If yes, selectedObject is set along with the rope.

As well, the popup menu will only appear if no other object has been selected. If the user taps an existing object, you assume that they want to change the object, rather than creating a new one.

Now that rope selection is working, you need to implement ccTouchMoved: to handle dragging of objects.

Replace the existing method stub for ccTouchMoved: in LevelEditor.mm with the following code:

-(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
   if (selectedObject && mode == kEditMode) {
       CGPoint touchLocation = [touch locationInView:touch.view];
       touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
 
       CGPoint oldTouchLocation = [touch previousLocationInView:touch.view];
       oldTouchLocation = [[CCDirector sharedDirector] convertToGL:oldTouchLocation];
 
       if ([selectedObject isMemberOfClass:RopeSprite.class]) {
           RopeSprite* rope = (RopeSprite*) selectedObject;
           [rope moveSelectedAnchorTo:touchLocation];
       }
 
       // TODO: move pineapple
   }
}

The above code checks whether there’s currently a selected object and whether the editor is in kEditMode. If both conditions are met, processing continues. The next four lines calculate the current touch location and the old touch location. Then, if the selected object is an instance of RopeSprite, the method moves the position of the selected anchor to the new touch location.

Note: While moving a game object, you only update what the user sees on-screen; you don’t update the information in LevelFileHandler in real-time.

Why not? It would clearly be advantageous to have what is shown on the screen and what is saved be in synch, wouldn’t it?

When the rope anchor point is being moved, the rope endpoint is invalid most of the time while the rope is moving. Because of this, you should only update the rope endpoint information in the LevelFileHandler when the rope endpoint information is valid.

But when will the endpoint be valid? When should you check it? A good idea is to verify the movement of the rope as soon as the user lets go of the rope. That happens when ccTouchEnded: executes.

Replace the current ccTouchEnded: implementation in LevelEditor.mm with the following code:

-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
   if (mode == kEditMode) {
       if ([selectedObject isMemberOfClass: RopeSprite.class]) {
           RopeSprite* rope = (RopeSprite*) selectedObject;
           CGPoint touchLocation = [touch locationInView:touch.view];
           touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
 
           // snap to pineapple if rope ends up on top of it
           int newAnchorID = -1;
           CCSprite* pineappleAtTouchLocation = [self pineappleAtPosition:touchLocation];
           if (pineappleAtTouchLocation) {
               [rope moveSelectedAnchorTo:pineappleAtTouchLocation.position];
               // get id of pineapple to properly set anchor in connected data model
               newAnchorID = pineappleAtTouchLocation.tag;
           }
 
           BOOL isValidNewAnchor = [rope isValidNewAnchorID:newAnchorID];
           if (isValidNewAnchor) {
               [fileHandler moveRopeWithId:rope.id withAnchorType:[rope getSelectedAnchorType] withAnchorId:newAnchorID to:[CoordinateHelper screenPositionToLevelPosition:touchLocation]];
           } else {
               [fileHandler moveRopeWithId:rope.id withAnchorType:[rope getSelectedAnchorType] withAnchorId:[rope getSelectedAnchorType] to:[CoordinateHelper screenPositionToLevelPosition:originalRopeAnchor]];
               [rope moveSelectedAnchorTo:originalRopeAnchor];
           }
 
           // set selected anchor to none
           [rope setSelectedAnchorType:kAnchorNone];
       }
 
       // TODO: end moving pineapple
       selectedObject = nil;
   }
}

That is quite a bit of code — here’s a step-by-step analysis.

  • First, you check whether the editor is in kEditMode. Next, you determine whether the selected object is a RopeSprite. If so, then you check whether the user dropped the anchor point on a pineapple.
  • If you find a pineapple at this position, set the position of the anchor to that of the pineapple and set the new anchor ID to the ID of the pineapple.
  • Otherwise, the new anchor ID stays at the default of -1, which means that the rope is tied to the background.
  • If a pineapple was selected, then it will appear to the user that the rope snapped to the pineapple. This should communicate clearly to the user that the pineapple and rope are now connected.
  • Next you check whether the new anchor ID is valid. If the updated rope endpoints are valid, you update the rope information in the file handler with the new anchor ID and the new location. This method doesn’t exist yet — you’ll add it in a bit.
  • If the new position is invalid, you still call the same method in the file handler, but this time you keep the original anchor ID and you reset the anchor position to the original position. You also update the visualization to reflect this change in position.
  • At the end of the method you clear selectedObject and selectedAnchor. Otherwise, the editor would assume that the user is still tapping the same object, rendering the user unable to select any other object.

You’re probably itching to try out the new drag and drop functionality, aren’t you? There’s just one more method to add take the editor for a spin — you’re so close!

The last bit to add is a method which updates the anchor changes for the rope in the LevelFileHandler class.

First, add the following method prototype to LevelFileHandler.h:

-(void)moveRopeWithId:(int)id withAnchorType:(anchorType)anchor withAnchorId:(int)anchorId to:(CGPoint)position;

Now add the following method implementation to LevelFileHandler.m:

-(void)moveRopeWithId:(int)id withAnchorType:(anchorType)anchor withAnchorId:(int)anchorId to:(CGPoint)position {
   for (RopeModel* ropeModel in self.ropes) {
       if (ropeModel.id == id) {
           switch (anchor) {
               case kAnchorA:
                   ropeModel.anchorA = position;
                   ropeModel.bodyAID = anchorId;
                   break;
               case kAnchorB:
                   ropeModel.anchorB = position;
                   ropeModel.bodyBID = anchorId;
                   break;
               default:
                   break;
           }
       }
   }
}

The above method is not complicated at all. It simply takes the passed in values and updates the matching rope in the level with the appropriate position and anchor values.

Build and run your app, and spark up the editor! Now you should be able to move the ropes, as shown in the screenshot below:

iOS-Simulator.png

Moving Objects – Pineapples

Now that you can move the ropes, you can use the same approach to move the pineapples.

The approach is rather similar. First discover the pineapple that was tapped or selected, then update its position via ccTouchesMoved:. You also need to remember all of the ropes connected to the pineapple, so that you can move them together as the pineapple moves.

When the user completes their drag, update the file handler and release the selected object. Sounds simple, doesn’t it?

Start with the method to determine there’s a pineapple at a given touch location and if so, set it as the selected object.

Add the following method to LevelEditor.mm:

-(void)selectPineapple:(CGPoint)touchLocation {   
   CCSprite* sprite = [self pineappleAtPosition:touchLocation];
   if (sprite) {
       selectedObject = sprite;
       // remember which ropes are tied to the selectedSprite
       connectedRopes = [self getAllRopesConnectedToPineappleWithID:sprite.tag setSelectedAnchor:YES];
   }
}

The above code is fairly straightforward. The only new element here is the call to getAllRopesConnectedToPineappleWithID:setSelectedAnchor:. This method, which you’ll add next, fetches an array of ropes connected to the selected pineapple.

Looks like you’ll need to add a variable to contain this new array of ropes!

Add a new private variable to the @interface block at the top of LevelEditor.mm:

   NSMutableArray* connectedRopes;

As for the method implementation, you currently have a getAllRopesConnectedToPineappleWithID: method which can be modified to handle the additional functionality (and the parameter) required by the code above. However, if you modify that method, then your existing code would need to be updated to handle the extra parameter in the method call.

The easiest way to deal with this is to replace the existing implementation for getAllRopesConnectedToPineappleWithID: in LevelEditor.mm with the following block of code:

-(NSMutableArray*)getAllRopesConnectedToPineappleWithID:(int)pineappleID {
   return [self getAllRopesConnectedToPineappleWithID:pineappleID setSelectedAnchor:NO];
}
 
-(NSMutableArray*)getAllRopesConnectedToPineappleWithID:(int) pineappleID setSelectedAnchor:(BOOL) setSelectedAnchor {
   NSMutableArray* tempRopes = [NSMutableArray arrayWithCapacity:5];
   for (RopeSprite* rope in ropes) {
       int bodyAID = [rope getBodyAID];
       int bodyBID = [rope getBodyBID];
 
       if (pineappleID == bodyAID || pineappleID == bodyBID) {
           [tempRopes addObject:rope];
       }
       if (setSelectedAnchor) {
           if (pineappleID == bodyAID) {
               [rope setSelectedAnchorType:kAnchorA];
           } else if (pineappleID == bodyBID) {
               [rope setSelectedAnchorType:kAnchorB];
           }
       }
   }
   return tempRopes;
}

The first method adds a redirect to the new method with the additional parameter so that existing code will still continue to work.

The second method maintains the functionality of the original but adds additional functionality via the second parameter. If the second parameter is set, the code checks which anchor of the rope is connected to the pineapple by comparing IDs and sets the selected anchor to the one connected to the pineapple.

You can now use the new methods in ccTouchBegan: to set the selected object correctly for pineapples.

Modify ccTouchBegan: in LevelEditor.mm as follows:

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
   ...    
   switch (mode) {
       case kEditMode:
           // check if a rope's anchor point was tapped, if yes select it
           [self selectRopeAnchor:touchLocation];
           if (!selectedObject) {
               [self selectPineapple:touchLocation];
           }
           if (!selectedObject && ![self ropeAtPosition:touchLocation]) {
               [self togglePopupMenu:touchLocation];
           }
           break;
       ...    
   }
   return YES;
}

When the editor is in the kEditMode state, first check whether the user touched a rope. If there is no rope at the touch location, check if a pineapple was selected. If so, then the pineapple and all attached ropes need to be moved via ccTouchMoved:.

To do this, replace the TODO line in ccTouchMoved: in LevelEditor.mm with the following:

       CGPoint movementVector = ccpSub(touchLocation, oldTouchLocation);
       if ([selectedObject isMemberOfClass:CCSprite.class]) {
           CCSprite* selectedSprite = (CCSprite*) selectedObject;
           CGPoint newPosition = ccpAdd(selectedSprite.position, movementVector);
           selectedSprite.position = newPosition;
           // remember which ropes are tied to the selectedSprite
           for (RopeSprite* rope in connectedRopes) {
               [rope moveSelectedAnchorTo:newPosition];
           }
       }

The above code first checks whether the selected object is a sprite. If it is, then calculate its new position and update it. Next, iterate over all the ropes connected to the pineapple and update their positions as well.

You’ll also need to clean up all of the old rope data and store the updated level in the LevelFileHandler.

Go to ccTouchEnded: in LevelEditor.mm and replace the TODO line with the following code:

       if ([selectedObject isMemberOfClass: CCSprite.class]) {
           CCSprite* pineappleSprite = (CCSprite*) selectedObject;
           int pineappleID = pineappleSprite.tag;
           CGPoint newPineapplePosition = [CoordinateHelper screenPositionToLevelPosition:pineappleSprite.position];
           [fileHandler movePineappleWithId:pineappleID to:newPineapplePosition];
       }
       [connectedRopes removeAllObjects];

The above code updates the pineapple’s position in the level file handler and also cleans out the connected ropes array since you don’t need it for the current pineapple object. It will be repopulated when a new pineapple object is selected.

You’re almost there! You just need the missing method that will update the pineapple’s position.

Add the following method prototype to LevelFileHandler.h:

-(void)movePineappleWithId:(int)id to:(CGPoint)position;

Then, add the following method implementation to LevelFileHandler.m:

-(void)movePineappleWithId:(int)id to:(CGPoint)position {
   for (PineappleModel* pineappleModel in self.pineapples) {
       if (pineappleModel.id == id) {
           pineappleModel.position = position;
           break;
       }
   }
}

The code simply iterates over all the pineapples till it finds the one you want to update. It then updates the pineapple’s position property.

That’s it! Build and run your project, and try out all of the editing functions. Add some pineapples and ropes, delete a rope, delete a pineapple and try to move the ropes and pineapples around.

Looks pretty slick, doesn’t it? Once you have the level edited the way you want it, treat yourself to a well-deserved break and play your newly created level!

The last part left to implement is persisting, or saving, your level edits between sessions.

Saving Your Level Data

Open LevelFileHandler.h and add the following method prototype:

-(void)saveFile;

Now add the following method implementation to LevelFileHandler.m:

-(void)saveFile {
    // create empty xml structure
    GDataXMLElement* levelElement = [GDataXMLElement elementWithName:@"level"];
    // add all Pineapple Elements
    for (PineappleModel* pineapple in self.pineapples) {
        GDataXMLElement* pineappleElement = [GDataXMLElement elementWithName:@"pineapple"];
        GDataXMLElement* idAttribute = [GDataXMLElement elementWithName:@"id" stringValue:[NSString stringWithFormat:@"%i", pineapple.id]];
        GDataXMLElement* xAttribute = [GDataXMLElement elementWithName:@"x" stringValue:[NSString stringWithFormat:@"%.3f", pineapple.position.x]];
        GDataXMLElement* yAttribute = [GDataXMLElement elementWithName:@"y" stringValue:[NSString stringWithFormat:@"%.3f", pineapple.position.y]];
        if (pineapple.damping != kDefaultDamping) {
            GDataXMLElement* dampingAttribute = [GDataXMLElement elementWithName:@"damping" stringValue:[NSString stringWithFormat:@"%.1f", pineapple.damping]];
            [pineappleElement addAttribute:dampingAttribute];
        }
        [pineappleElement addAttribute:idAttribute];
        [pineappleElement addAttribute:xAttribute];
        [pineappleElement addAttribute:yAttribute];
        [levelElement addChild:pineappleElement];
    }
}

The above code uses GDataXML to do the grunt work in saving the XML data for the level. The code creates a new GDataXMLElement named level, then simply adds the pineapples to the level as children of that element by iterating over the items in the pineapples array.

For each pineapple in the array, the code creates a new GDataXMLElement and adds the properties for that pineapple as attributes of the new GDataXMLElement.

Note: If some of the objects you’re saving have the default object values set, then there’s no point in saving them since it’s just a waste of space and time to persist them to the level file. In this case, check the attribute’s value to see whether it differs from the default value. If so, save it. Otherwise, simply disregard that property and move on.

The next step to save your level data is to persist the rope information for all the ropes in the level. You’ll start by implementing the save mechanism for the first anchor of each rope.

Add the following code to the end of saveFile, right after the loop for iterating through the pineapples:

    // Add all rope elements
    for (RopeModel* rope in self.ropes) {
        GDataXMLElement* ropeElement = [GDataXMLElement elementWithName:@"rope"];
 
        //Anchor A
        GDataXMLElement* anchorAElement = [GDataXMLElement elementWithName:@"anchorA"];
        GDataXMLElement* bodyAttributeA = [GDataXMLElement elementWithName:@"body" stringValue:[NSString stringWithFormat:@"%i", rope.bodyAID]];
        [anchorAElement addAttribute:bodyAttributeA];
        if (rope.bodyAID == -1) {
            GDataXMLElement* xAttributeA = [GDataXMLElement elementWithName:@"x" stringValue:[NSString stringWithFormat:@"%.3f", rope.anchorA.x]];
            GDataXMLElement* yAttributeA = [GDataXMLElement elementWithName:@"y" stringValue:[NSString stringWithFormat:@"%.3f", rope.anchorA.y]];
            [anchorAElement addAttribute:xAttributeA];
            [anchorAElement addAttribute:yAttributeA];
        }        
        [ropeElement addChild:anchorAElement];
 
        // TODO: anchorB is not saved yet
 
        [levelElement addChild:ropeElement];
    }

The above code is quite similar to the mechanism for saving the pineapples. The code iterates over the array of ropes and creates a new GDataXMLElement for each rope. But that’s where the rope-specific handling comes in, as each rope has two anchor points to save.

Thus a new GDataXMLElement must be created for each anchor point, and the properties for the anchor points are added as attributes of the anchor point element. Finally, the anchor points are added to the rope XML element as children, and the rope element is added to the level XML element as a child.

You’ll notice that the above code only implements saving the first anchor point. There’s a TODO where the code for saving the second anchor point should be. Are you up to the challenge of writing this bit of code on your own?

Go ahead, give it a try! There’s always the spoiler below in case you get stuck — but don’t cheat! :]

Solution Inside SelectShow

Hopefully you did well — the code for saving the second anchor point is quite similar to saving the first anchor point. Check your implementation against the spoiler above to see how well you did.

You’re getting close to finishing off this feature. All that’s left now is to actually save all of this information to a file.

Add the following code to the end of saveFile, right after the loop for iterating through the ropes:

    // Write to file in document directory
    GDataXMLDocument *document = [[GDataXMLDocument alloc] initWithRootElement:levelElement];
    NSData *xmlData = document.XMLData;
 
    // creates an empty folder where all the levels should be stored, if necessary
    [FileHelper createFolder:@"levels"];
    NSString* filePath = [FileHelper dataFilePathForFileWithName:_filename withExtension:@".xml" forSave:YES];
    [xmlData writeToFile:filePath atomically:YES];

The above code first creates a GDataXMLDocument from the XML tree you have created that contains the level save data. Next, it gets the XML’s XMLData as an NSData object. It then gets the file path, creates the destination folder if it doesn’t exist, and writes the XML data to the file path.

The save method is now fully implemented! Now you just need to trigger it from the “Save” menu option.

To hook this up to the menu option, switch to LevelEditor.mm and replace the existing stub for save with the following:

-(void)save {
    [fileHandler saveFile];
}

Short and sweet! You only need to call saveFile in the file handler and the level is saved properly.

Is it time to wrap things up and call it a day? Well, not quite.

If you look at the menu, there’s a third button that to reset the level. But you don’t yet have a method that will reset the level.

Resetting Your Level Data

First, add the following method prototype to LevelFileHandler.h:

-(void)reset;

Next, add the following method implementation to LevelFileHandler.m:

-(void)reset {
    [self loadFile];
}

All that this method does is to load the level file again from the save directory, which overwrites the current changes you’ve made to the level.

Now you can call the reset method when the user taps the corresponding menu item in the level editor.

Replace the existing stub for resetLevel in LevelEditor.mm with the following code:

-(void)resetLevel {
    [pineapplesSpriteSheet removeAllChildrenWithCleanup:YES];
    [ropeSpriteSheet removeAllChildrenWithCleanup:YES];
    [ropes removeAllObjects];
    selectedObject = nil;
    [connectedRopes removeAllObjects];
    [fileHandler reset];
    [self drawLoadedLevel];
}

The above code first removes all onscreen elements, clears out the selected object, and resets all level data. It then calls reset on the file handler to reload the level and then draws the newly loaded level.

And that completes the saving and reloading functionality!

Build and run your project, start the editor, make some changes, then tap Save and restart the simulator. The level that loads should now contain your changes. Awesome!

As well, make a few changes to the level and tap Reset. Boom! A nice clean slate to work from!

Menu – Scrolling with CCMenuAdvanced

OK, so far your editor can edit and save a level. But you can only edit, load, and play a single level. That’s going to get boring really quickly!

It’s time to implement multiple file handling in your level editor.

Create an Objective-C class named MenuLayer with superclass CCLayer. Also change the file extension of the implementation file from .m to .mm.

Replace the contents of MenuLayer.h with the following:

#import "cocos2d.h"
#import "CCMenuAdvanced.h"
#import "FileHelper.h"
#import "LevelFileHandler.h"
#import "CutTheVerletGameLayer.h"
 
@interface MenuLayer : CCLayer
 
+(CCScene*) scene;
 
@end

In addition to the imports, the above adds a static method that will return a scene containing the menu.

Implement the following static method in MenuLayer.mm:

+(CCScene*)scene {
    CCScene* scene = [CCScene node];
    [scene addChild: [MenuLayer node]];
    return scene;
}

Nothing too complicated here; you’re simply creating the MenuLayer and adding it to an otherwise empty scene.

Now switch to AppDelegate.mm and remove the following import from the top of the file:

#import "LevelFileHandler.h"

Next, add the following import to the top of the file:

#import "MenuLayer.h"

Now switch to AppDelegate.mm and replace the following two lines of application:didFinishLaunchingWithOptions::

    LevelFileHandler* fileHandler = [[LevelFileHandler alloc] initWithFileName:@"levels/level0"];
    [director_ pushScene:[HelloWorldLayer sceneWithFileHandler:fileHandler]];

With these two lines:

    // Create menu and pass it to scene
    [director_ pushScene: [MenuLayer scene]];

Build and run your project; you should see an amazing…black screen?

iOS-Simulator.png

Hmm — it looks like you still have some work to do! You’ll start by adding a menu to your MenuLayer.

Add the following initialization method to MenuLayer.mm:

-(id)init {
    self = [super init];
    if (self) {
        NSMutableArray* items = [NSMutableArray array];
 
        int levelNumber = 0;        
        BOOL fileExists;
        do {
            fileExists = [FileHelper fileExistsInDocumentsDirectory:[NSString stringWithFormat:@"levels/level%i.xml", levelNumber]];
            CCMenuItem* item;
            if (fileExists) {
                NSString* itemLabel = [NSString stringWithFormat:@"Level %i", levelNumber];
                item = [CCMenuItemFont itemWithString:itemLabel target:self selector:@selector(selectLevel:)];
            } else {
                item = [CCMenuItemFont itemWithString:@"New Level" target:self selector:@selector(selectLevel:)];
            }
            item.tag = levelNumber;
            [items addObject:item];
 
            levelNumber++;
        } while (fileExists);
        CCMenuAdvanced* menu = [CCMenuAdvanced menuWithArray:items];
        [menu alignItemsVerticallyWithPadding:0.0f bottomToTop:NO];
        menu.position = CGPointMake(0, -menu.contentSize.height/2);
 
        CGSize winSize = [[CCDirector sharedDirector] winSize];
        menu.boundaryRect = CGRectMake(winSize.width/2-menu.contentSize.width/2, 0, menu.boundingBox.size.width, winSize.height);
 
        [self addChild:menu];
        [menu fixPosition];
    }
    return self;
}

The above method first creates an empty array to contain the menu items to be displayed on the screen. The menu should contain one item for each level file present in the documents directory.

As all files in that directory will have been created by your level editor, you can assume that the files will be named like level0.xml, level1.xml, level2.xml and so on.

You next create a loop to iterate over all those files using a counter. At each step, you use the FileHelper class to check for the existence of a file for the given counter. If the file exists, create a menu item with the file name. If there is no file with the given name, assume that there are no more level files to load, and instead add a menu item named “New Level”.

You assign the current level number to each menu item as a tag so that you can identify each menu item uniquely. Then you add the menu item to the array of items.

At this point you have a nice array containing an item for each level — now you need to display them on the screen.

At this point you might see a class that is completely unfamiliar to you – the CCMenuAdvanced class, which is part of the Cocos2D extensions. Why should you use it here instead of the normal CCMenu?

The screenshot below does a pretty good job of showing what would happen if you used CCMenu:

It just won’t fit :(

The screen on an iPhone or iPod touch is relatively small, and it’s hard to fit more than a few items on the screen without crowding.

If you were to use a standard CCMenu, then you might have to do something drastic like limit the number of levels in your menu list. But CCMenuAdvanced supports scrolling and so does away with any screen size limitations.

To display the menu, you first create a CCMenuAdvanced instance with your array of menu items. Then you set the menu alignment so that it starts adding items from top.

Next you position it on the screen and give it a boundary rectangle which determines the scrolling area. Finally, you add the menu to the menu layer so that it is displayed on screen.

Build and run your app, and you should see your menu on the screen. Go ahead and select one of the menu items!

Now the menu scrolls when there are more items than fit the screen

Now the menu scrolls when there are more items than fit the screen

Whoops — you probably just crashed your app. Oh, right — you need to add the menu handler!

Add the following method to MenuLayer.mm:

-(void)selectLevel:(id)sender {
    if ([sender isKindOfClass:CCMenuItem.class]) {
        CCMenuItem* item = (CCMenuItem*) sender;
        int levelNumber = item.tag;
        NSString* fileName = [NSString stringWithFormat:@"levels/level%i", levelNumber];
        LevelFileHandler* fileHandler = [[LevelFileHandler alloc] initWithFileName:fileName];
        [[CCDirector sharedDirector] replaceScene: [HelloWorldLayer sceneWithFileHandler:fileHandler]];
    }
}

The above code simply gets the sender object, casts it back to a CCMenuItem and retrieves its tag. Next, the tag is used to assemble the level’s file name. Finally, the method creates a new game scene with the filename as parameter.

Build and run your project — you should now be able to load any levels you’ve created or create a new level from the main menu. Have fun creating challenging new levels for your game!

Where To Go From Here?

Well, what seemed like a pretty daunting task — adding a level editor to your game — turned out to be relatively straightforward job.

While your level editor looks pretty complete, there’s still a few things missing. The numerical properties to control the damping of the pineapple and sagginess of the rope can’t be set through your editor. One way to implement this is to create a second popup menu with a slider that allows you to set the damping and sagginess properties.

As well, you should probably create a tutorial or help file on how to use all of the controls in your editor. How meta — a tutorial that creates an editor that contains a tutorial! :]

Hopefully this tutorial has helped you with some ideas for creating a level editor in your own game. Just remember to follow the same basic steps in all parts of this tutorial:

  • Define a file format
  • Write load and save methods for your files
  • Run your game with information from the level file
  • Create the editor
    • Design user interactions
    • Display the level
    • Add menus for navigation
    • Implement object manipulation methods for:
      • Creation
      • Modification
      • Deletion
    • Make sure only valid levels can be created
  • Improve it!

Here is the sample project with the completed project from the tutorial series.

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

... 19 total!

Swift Team

... 15 total!

iOS Team

... 33 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 15 total!