Create Your Own Level Editor: Part 3/3

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! By Barbara Reichart.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

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. :]

[spoiler]

// Add method prototype to LevelFileHandler.h
-(void)removePineappleWithID:(int)id

// Add method implementation to LevelFileHandler.m
-(void)removePineappleWithID:(int)id {
   PineappleModel* pineappleToBeRemoved;
   for (PineappleModel* pineapple in self.pineapples) {
       if (pineapple.id == id) {
           pineappleToBeRemoved = pineapple;
           break;
       }
   }
   [self.pineapples removeObject:pineappleToBeRemoved];
}

The above code really just iterates over all the pineapples and finds the one matching the one you want to remove. If it finds a match, then it removes it from the pineapples array.
[/spoiler]

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

Barbara Reichart

Contributors

Barbara Reichart

Author

Over 300 content creators. Join our team.