Create Your Own Level Editor: Part 1/3

In this tutorial, you’ll learn how to make a level editor for the Cut the Rope clone that was previously covered on this site. Using the level editor you can easily make new levels. All you have to do is drag and drop the ropes and pineapples to where you like them. By Barbara Reichart.

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

Loading Pineapple Information into Model Classes

Great! With the console output showing the XML content, you now know that you have all of the parts working together as intended.

Your next task is to get all that XML data loaded up into the proper places in your model classes.

Compared to the model classes themselves, the code mechanisms for populating the model classes look pretty messy! But this is where you’re doing a lot of the heavy lifting — taking the data from the file and translating it into a format that makes sense to your app.

Start with the pineapple model class.

Add the following code to the end of loadFile in LevelFileHandler.m, replacing the line //TODO: parse XML and store into model classes as follows:

    NSArray* pineappleElements = [doc.rootElement elementsForName:@"pineapple"];

    for (GDataXMLElement* pineappleElement in pineappleElements) {
        PineappleModel* pineappleModel = [[PineappleModel alloc] init];

        // load id
        pineappleModel.id = [pineappleElement attributeForName:@"id"].stringValue.intValue;

        // load level coordinate, for display on screen needs to be multiplied with screen size
        float x = [pineappleElement attributeForName:@"x"].stringValue.floatValue;
        float y = [pineappleElement attributeForName:@"y"].stringValue.floatValue;
        pineappleModel.position = CGPointMake(x, y);

        // load damping if set, otherwise keep default value
        GDataXMLNode* dampingElement = [pineappleElement attributeForName:@"damping"];
        if (dampingElement) {
            pineappleModel.damping = [pineappleElement attributeForName:@"damping"].stringValue.floatValue;
        }

        [self.pineapples addObject:pineappleModel];
    } 

In the code above, you first get all of the elements named “pineapple” stored in the root element of your XML file. Next, you iterate over all the pineapple elements and create a instance of pineappleModel for each one. Finally, you fill it parameter-by-parameter with the information you loaded from the XML file.

Populating your model instance is fairly straightforward for most of the elements above. However, the damping property requires a little more work.

Recall that you set the damping default value to a non-zero value, and the presence of the damping element in the XML file is optional. When the damping attribute doesn’t exist in the file, you want to assign the default value.

However, if you try to cast a non-existent value returned by attributeForName: into a float, you’ll get zero — which is not what you want!

In order to figure out whether an attribute exists, you simply check whether the attributeForName: return value is set. If so, assign it to the damping variable of the pineapple, otherwise leave it at the default value.

The final step in the code is to add the newly created pineapple model to the list of pineapples by calling [self.pineapples addObject:pineappleModel].

Okay, you now have all of the pineapple data loaded — time to put it to use in the game!

Switch to LevelFileHandler.h and add the method prototype as shown below:

-(PineappleModel*) getPineappleWithID:(int) id;

The method above takes id as an argument to uniquely identify the pineapple model, and returns the pineapple that matches the ID.

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

+(AbstractModel*) getModelWithID:(int) id fromArray:(NSArray*) array {
    for (AbstractModel* model in array) {
        if (model.id == id) {
            return model;
        }
    }
    return nil;
}

getModelWithID:fromArray: is a private method that accepts as its arguments an ID and an array containing classes of type AbstractModel. Within the method, you iterate over all the elements in the array, check their IDs and if the ID is equal to the ID requested, return the current AbstractModel.

It might seem that this method is overly complicated. Why not directly iterate over the array containing the pineapples since that’s the information you’re looking for?

Right now, you’re really only interested in searching for a pineapple with a specific ID. However, it’s extremely likely that you will need the exact same code for other types of game objects.

In this project there is only one other object — the ropes — but in other projects there could be many more object to manage. Creating a method for simply searching for pineapples would then lead to lots of lines of duplicate code when you implemented a method for searching for ropes. This in turn would increase development and maintenance time and cost!

So, with your ever-so-practical getModelWithID:fromArray: method, the implementation of getPineappleWithID: is essentially reduced to just one line, as you’ll see in the method implementation below.

Add the following method to LevelFileHandler.m:

-(PineappleModel*) getPineappleWithID:(int)id {
    return (PineappleModel*)[LevelFileHandler getModelWithID:id fromArray:self.pineapples];
}

And that neatly finishes off the complete implementation of loading pineapple data from the XML file!

Now on to the rope objects!

Loading Rope Information into Model Classes

Now that you’ve seen how to do it with the pineapples, try to write the methods that will load the rope data from the XML file and populate the appropriate model classes.

A few tips to help you out:

  • Don’t forget that each rope needs a unique ID — you didn’t store any rope IDs in the XML file since the IDs only have context in your level editor.
  • Your new code should go at the end of loadFile in LevelFileHandler.m
  • Your rope loading implementation will have a very similar structure to the pineapple loading implementation — but adapted for rope properties.

Ready to give it a go? Good luck — and no peeking! :]

[spoiler]

    NSArray* ropesElement = [doc.rootElement elementsForName:@"rope"];

    // IDs for ropes start at 1 and are given out in the file handler.
    // They are not stored in the XML file as they are only needed for the editor
    // and do not convey any substantial information about the level layout.
    int ropeID = 1;

    for (GDataXMLElement* ropeElement in ropesElement) {
        RopeModel* ropeModel = [[RopeModel alloc] init];
        ropeModel.id = ropeID;

        // Load the anchor points consisting of the body ID the rope is tied to
        // (-1 stands for the background) and the position, which will be ignored
        // by the game later on if the rope is tied to a pineapple.
        GDataXMLElement* anchorA = [[ropeElement elementsForName:@"anchorA"] objectAtIndex:0];
        ropeModel.bodyAID = [anchorA attributeForName:@"body"].stringValue.intValue;
        
        float ax;
        float ay;
        if (ropeModel.bodyAID == -1) {
            ax = [anchorA attributeForName:@"x"].stringValue.floatValue;
            ay = [anchorA attributeForName:@"y"].stringValue.floatValue;
        } else {
            PineappleModel* pineappleModel = [self getPineappleWithID:ropeModel.bodyAID];
            ax = pineappleModel.position.x;
            ay = pineappleModel.position.y;
        }
        
        ropeModel.anchorA = CGPointMake(ax, ay);
        
        GDataXMLElement* anchorB = [[ropeElement elementsForName:@"anchorB"] objectAtIndex:0];
        ropeModel.bodyBID = [anchorB attributeForName:@"body"].stringValue.intValue;
        
        float bx;
        float by;
        if (ropeModel.bodyBID == -1) {
            bx = [anchorB attributeForName:@"x"].stringValue.floatValue;
            by = [anchorB attributeForName:@"y"].stringValue.floatValue;
        } else {
            PineappleModel* pineappleModel = [self getPineappleWithID:ropeModel.bodyBID];
            bx = pineappleModel.position.x;
            by = pineappleModel.position.y;
        }
        
        ropeModel.anchorB = CGPointMake(bx, by);
        
        GDataXMLNode* sagityElement = [ropeElement attributeForName:@"sagity"];
        if (sagityElement) {
            ropeModel.sagity = [ropeElement attributeForName:@"sagity"].stringValue.floatValue;
        }
        
        [self.ropes addObject:ropeModel];
        
        // Increase ropeID as the IDs need to be unique.
        ropeID++;
    }

[/spoiler]

All done? Or did you give up? :]

Either way, check your implementation against the spoiler section above to make sure that you haven’t missed anything.

That’s the end of the level data format design and implementation. Now it’s finally time to put all that hard work to use and actually show some pineapples and ropes on the screen!

Barbara Reichart

Contributors

Barbara Reichart

Author

Over 300 content creators. Join our team.