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

File Handler: Getting the Path for an Existing File

Add the following code to FileHelper.m:

+(NSString *)dataFilePathForFileWithName:(NSString*) filename withExtension:(NSString*)extension forSave:(BOOL)forSave {
    NSString *filenameWithExt = [filename stringByAppendingString:extension];
    if (forSave ||
        [FileHelper fileExistsInDocumentsDirectory:filenameWithExt]) {
        return [FileHelper fullFilenameInDocumentsDirectory:filenameWithExt];
    } else {
        return [[NSBundle mainBundle] pathForResource:filename ofType:extension];
    }
}

The above code handles several cases in a tight little bit of logic. If you want to save a file, or the specified file already exists, then this method returns the file path to the Documents directory.

However, if you’re not saving a file, then the Document directory file path is returned only in the case that file already exists. In all other cases, you return the default file that comes included with the app bundle.

The FileHelper class is almost done — all that’s left to do is implement the last helper method.

Add the following code to FileHelper.m:

+(void) createFolder:(NSString*) foldername {
    NSString *dataPath = [FileHelper fullFilenameInDocumentsDirectory:foldername];
    if (![[NSFileManager defaultManager] fileExistsAtPath:dataPath])
        [[NSFileManager defaultManager] createDirectoryAtPath:dataPath withIntermediateDirectories:NO attributes:nil error:nil];
}

This code simply checks to see if a folder with the specified name exists. If it doesn’t, it uses the file manager to create one.

You might be wondering why you’d need such a simple helper function. In the event your editor becomes more complex, the user might create many files in the course of editing their game. Without a decent way to create folders on the fly, you’d soon be engulfed in file management chaos! :]

Creating Model Classes for Game Objects

At this point you have everything you need to find and load a file. But what should you do with the contents of the file once it’s been read in?

The best practice in this case is to create model classes to store the information contained in the file. This makes it easy to access and manipulate the data inside your app.

Start by creating a class named AbstractModel with the iOS\Cocoa Touch\Objective-C class template. Make it a subclass of NSObject and place it in the Model group.

Open up AbstractModel.h and replace its contents with the following:

#import "Constants.h"

@interface AbstractModel : NSObject

@property int id;

@end

This adds a unique ID as property, which will be used to identify each model instance.

AbstractModel should never be instantiated. In some programming languages like Java you could indicate this to the compiler by using the abstract keyword.

However, in Objective-C there is no simple mechanism to make it impossible to instantiate a class. So you’ll have to trust in naming conventions and your memory to enforce this!

Note: If you don’t want to rely on conventions — or you don’t trust your memory! :] — you can look at some ways to create abstract classes in Objective-C as mentioned in this thread on StackOverflow.

The next step is to create a model class for the pineapple.

Creating the Pineapple Model Class

Create a new class using the iOS\Cocoa Touch\Objective-C class template. Name the class PineappleModel and set its subclass to AbstractModel.

You’ll first need to add some properties for the position and damping of your pineapple.

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

#import "AbstractModel.h"

@interface PineappleModel : AbstractModel

@property CGPoint position;
@property float damping;

@end

Now switch to PineappleModel.m and add the following code between the @implementation and @end lines:

-(id)init {
    self = [super init];
    if (self) {
        self.damping = kDefaultDamping;
    }
    return self;
}

All you do in this method is create an instance of the class and set proper default values for its properties. The constant you use for this is already defined in Constants.h.

Believe it or not, this is the complete model class for the Pineapple!

Model classes are almost always extremely simple and should not contain any program logic. They are really only designed to store information to be used in your app.

Creating the Pineapple Model Class

Now that the pineapple model is complete, as a challenge to yourself try to create the model for the rope!

If you don’t remember the properties needed to represent a rope, have a look at the level0.xml file in the levels folder.

[spoiler]
RopeModel.h:

#import "AbstractModel.h"

@interface RopeModel : AbstractModel

// The position of each of the rope ends.
// If an end is connected to a pineapple, then this property is ignored
// and the position of the pineapple is used instead.
@property CGPoint anchorA;
@property CGPoint anchorB;

// ID of the body the rope is connected to. -1 refers to the background.
// all other IDs refer to pineapples distributed in the level
@property int bodyAID;
@property int bodyBID;

// The sagginess of the line
@property float sagity;

@end

RopeModel.m:

#import "RopeModel.h"

@implementation RopeModel
-(id)init {
    self = [super init];
    if (self) {
        self.bodyAID = -1;
        self.bodyBID = -1;
        self.sagity = kDefaultSagity;
    }
    return self;
}

@end

[/spoiler]

All done? To be sure of your solution, check your implementation against the tutorial code above, making sure that all properties are defined correctly and that the names used for the class and properties match what’s in the tutorial. Otherwise, some code later down the line might not work for you! :]

Are you getting impatient to actually load the file and start working with it?

Please, pretty please! Can I load the files now?

Okay — go ahead and follow the steps below to load in your file!

Loading the Level Data File

Create a new class using the iOS\Cocoa Touch\Objective-C class template in the LevelEditor group. Name the new class LevelFileHandler and make it a subclass of NSObject.

Open LevelFileHandler.h and replace its contents with the following:

#import "Constants.h"

@class RopeModel, PineappleModel;

@interface LevelFileHandler : NSObject

@property NSMutableArray* pineapples;
@property NSMutableArray* ropes;

- (id)initWithFileName:(NSString*) fileName;

@end

LevelFileHandler assumes all responsibility for the handling of the level data; it will be responsible for loading — and later, writing — the data files. The level editor will access LevelFileHandler to get all the information it needs and to write changes.

Here you’ve set up some properties in LevelFileHandler that will store the data about all the pineapples and ropes in the level that are read in from the XML files.

Now you’ll need to add all the requisite imports to LevelFileHandler.m. This includes the model classes and the file helper you just created, along with GDataXMLNode.h, which you’ll need to parse the XML file.

Switch to LevelFileHandler.m and add the following code:

#import "PineappleModel.h"
#import "RopeModel.h"
#import "FileHelper.h"
#import "GDataXMLNode.h"

Next, add a private variable to LevelFileHandler.m by adding the following class extension just below the #import lines:

@interface LevelFileHandler () {
    NSString* _filename;
}

@end

The above variable stores the name of the currently loaded level. You’re using a private instance variable here since you won’t use this information anywhere outside of the class. By hiding this information from any other classes, you’ve made sure that it won’t be changed in ways you hadn’t anticipated!

Now add the following code to LevelFileHandler.m between the @implementation and @end lines:

-(id)initWithFileName:(NSString*)filename {
    self = [super init];
    if (self) {
	_filename = filename;
        [self loadFile];
    }
    return self;
}

init simply stores the filename in the instance variable and calls loadFile.

Where’s loadFile, you ask?

Excellent question — you’re going to implement that method right now! :]

Add the following code to LevelFileHandler.m:

/* loads an XML file containing level data */
-(void) loadFile {
    // load file from documents directory if possible, if not try to load from mainbundle
    NSString *filePath = [FileHelper dataFilePathForFileWithName:_filename withExtension:@".xml" forSave:NO];
    NSData *xmlData = [[NSMutableData alloc] initWithContentsOfFile:filePath];
    GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:xmlData options:0 error:nil];
	
    // clean level data before loading level from file
    self.pineapples = [NSMutableArray arrayWithCapacity:5];
    self.ropes = [NSMutableArray arrayWithCapacity:5];
	
    // if there is no file doc will be empty and we simply return from this method
    if (doc == nil) { 
        return; 
    }
    NSLog(@"%@", doc.rootElement);

    //TODO: parse XML and store into model classes
}

The above code finally gets to the meat of the FileHelper class. It first gets the data file path for the saved file name, then loads the data contained in the file. It then initializes a GDataXMLDocument and passes in the loaded file data to parsed.

In case your file isn’t a well-formed XML document, the init method of GDataXMLDocument will let you know via the error parameter. In this tutorial, you will just ignore any errors passed back from GDataXMLDocument — horror of horrors! — and continue with an empty level that has no pineapples and no ropes.

In a consumer-ready app, you would definitely need to handle these errors in a way that made sense depending on the context of the app. But for now, just be aware that you’re taking a shortcut in order to focus on the rest of your level editor.

Before you can use this new functionality, you’ll need a way to pass the file handler to your game scene so that the scene can make use of the level data contained in LevelFileHandler.

You can accomplish this by passing the LevelFileHandler instance as a parameter when creating the scene.

To do this, open CutTheVerletGameLayer.h and replace the following line:

+(CCScene *) scene;

with this line:

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

Now, you’ll need to make sure your implementation knows what the heck LevelFileHandler is.

Switch to CutTheVerletGameLayer.mm, and add the following import statement at the top of the file:

#import "LevelFileHandler.h"

Then, add a class extension just above the @interface line in CutTheVerletGameLayer.mm to declare a private variable to store the LevelFileHandler instance:

@interface HelloWorldLayer () {
    LevelFileHandler* levelFileHandler;
}

@end

Next, replace the scene implementation of CutTheVerletGameLayer.mm with the following code:

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

Just as the original scene method, this creates the HelloWorldLayer object that runs the game, but now it also passes the LevelFileHandler object to that layer.

Finally, modify the init method implementation of CutTheVerletGameLayer.mm as follows:

// Change method name
-(id) initWithFileHandler:(LevelFileHandler*) fileHandler {
    if( (self=[super init])) {
        // Add the following two lines
        NSAssert(!levelFileHandler, @"levelfilehandler is nil. Game cannot be run.");
        levelFileHandler = fileHandler;
	...
    }
    return self;
}

Note that in the above code the method name has changed — and there’s now a parameter passed in.

Now that you have all of the required pieces in place to load up your new level, you can set up the LevelFileHandler in AppDelegate.mm where the game scene is first created.

But again, in order for AppDelegate to know what LevelFileHandler is, you’ll need to add the following import statement to the top of AppDelegate.mm:

#import "LevelFileHandler.h"

Still in AppDelegate.mm, add the following lines to the bottom of application:didFinishLaunchingWithOptions: to create the LevelFileHandler object and pass it to the scene:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
	…

    // Create LevelFileHandler and pass it to scene
    LevelFileHandler* fileHandler = [[LevelFileHandler alloc] initWithFileName:@"levels/level0"];
    [director_ pushScene:[HelloWorldLayer sceneWithFileHandler:fileHandler]];	

    return YES;
}

Build and run your project!

If everything works correctly, you should see the contents of the XML file in the console, like so:

XML successfully loaded and content written to console

XML successfully loaded and content written to console
Barbara Reichart

Contributors

Barbara Reichart

Author

Over 300 content creators. Join our team.