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 3 of 5 of this article. Click here to view the first page.

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

[spoiler]
Solution: Creating and adding a GDataXMLElement for the second anchor point

        // Anchor B
        GDataXMLElement* anchorBElement = [GDataXMLElement elementWithName:@"anchorB"];
        GDataXMLElement* bodyAttributeB = [GDataXMLElement elementWithName:@"body" stringValue:[NSString stringWithFormat:@"%i", rope.bodyBID]];
        [anchorBElement addAttribute:bodyAttributeB];
        if (rope.bodyBID == -1) {
            GDataXMLElement* xAttributeB = [GDataXMLElement elementWithName:@"x" stringValue:[NSString stringWithFormat:@"%.3f", rope.anchorB.x]];
            GDataXMLElement* yAttributeB = [GDataXMLElement elementWithName:@"y" stringValue:[NSString stringWithFormat:@"%.3f", rope.anchorB.y]];
            [anchorBElement addAttribute:xAttributeB];
            [anchorBElement addAttribute:yAttributeB];
        }
        [ropeElement addChild:anchorBElement];

[/spoiler]

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.

Barbara Reichart

Contributors

Barbara Reichart

Author

Over 300 content creators. Join our team.