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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Create Your Own Level Editor: Part 1/3
55 mins
- Getting Started
- Choosing a File Format to Save Your Level Data
- Calculating the Position of Your Pineapples
- Setting ID and Damping Parameters for the Pineapples
- Setting up Your Rope Parameters
- Putting Your XML File Format Together
- Creating Your XML File Handler
- Create a Handler for File Access
- File Handler: Getting the Full Path to a File
- File Handler: Checking if a File Exists
- File Handler: Getting the Path for an Existing File
- Creating Model Classes for Game Objects
- Creating the Pineapple Model Class
- Creating the Pineapple Model Class
- Loading the Level Data File
- Loading Pineapple Information into Model Classes
- Loading Rope Information into Model Classes
- Displaying Your Pineapple Objects On-screen
- Displaying Your Rope Objects On-screen
- Where to Go From Here?
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.
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?
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: