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?
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.
What is cool about this level editor is that it is build into the game, so players can create their own levels directly on their device.
Although a level editor can be incredibly fun for the end-user, it’s also pretty handy for the game developer to quickly assemble levels instead of hand-coding them.
An added benefit is that a level editor allows you to test-drive your game concepts. This can be especially important for physics games like Cut the Rope, as sometimes it can be hard to predict the behavior of the physics engine, but very easy to test those behaviors in real-time.
A level editor is a great way to increase the longevity and utility of your game by providing your players with the power to create their own levels — and even share their creations with other game fanatics.
In this tutorial, you will create a level editor for the Cut the Verlet game that was previously covered on this site. Didn’t catch the game creation tutorial the first time around? You can read about the game implementation in the tutorials below:
Getting Started
You’ll use the updated version of the game available here as a starter project. Download the project and open it in Xcode.
The code in the starter project is nearly the same as in the original tutorial; the biggest difference is that the project now supports Objective-C ARC, where the original project did not. An XML parser has also been added to the original project, which you’ll use in creating your level editor.
Note: the starter project has not been modified to work with an iPhone 5 4″ screen. So when you run the app on a simulator, make sure to use an iPhone 3.5″ simulator instead of the 4″ one!
Choosing a File Format to Save Your Level Data
The first step in creating a level editor is to decide upon a file format to use when saving your level data. There are a lot of ways to persist information in your apps, but the most important features to consider are the following:
- simple storage format
- platform-independent
- machine-readable and human readable — which helps with file debugging! :]
In this project, you’ll use XML to store your levels; it ticks all the boxes above, and lots of readers have likely used XML in some format before.
Next, you need to think about what information you need to store in order to create (or recreate) the level. What can you deduce about the information that needs to be saved just by considering the game screenshot below?
Here’s a hint to help make your list complete — think about the properties of objects, besides their position.
So what did you list? Pineapples? Ropes? The background? There’s a lot of information in a level — sometimes more than meets the eye!
Open the spoiler below to see the complete list of the elements in the level that need to be captured in your level editor file:
[spoiler]
- Pineapple Elements:
- ID: to identify each pineapple uniquely and to store the connection between a pineapple and ropes
- Position: x- and y-coordinates
- Damping: how bouncy your pineapple is — this information is used by the physics engine of the game
- Rope Elements:
- Two anchor points, which have the following properties:
- Body: ID of the body the anchor point is bound to
- Position: optional, not required when bound to a pineapple. In that case, you should use the position of the pineapple directly
- Sagginess: how loosely the rope hangs
- General Elements (which you’ll find in almost every XML file)
- Header including the XML version
- Level: One top-level element to bind everything together
[/spoiler]
Did you miss any? Don’t feel bad if you did — it isn’t always easy to tell what information is contained in a level just by looking at it.
The sections below describe each of the elements you’re going to store in your XML file in more detail.
Calculating the Position of Your Pineapples
Everything is relative — even pineapple positioning! :]
Since you want your editor to run on both retina and non-retina displays, you should store all positions relative to the screen size. That way, you don’t have to calculate individual placement based on pixels.
How do you do that? It’s pretty easy — you calculate the object’s location by taking the screen location, divide its x coordinate by the screen width, and its y coordinate by the screen height. To see this illustrated, check out the image below:
On the left, the relative position is shown for a pineapple in the middle of the screen with a resolution of 320×480. As a quick example, try to calculate the level coordinates yourself for the example on the right!
[spoiler]
(240, 288) translates to (0.75, 0.6).
[/spoiler]
You’ll need to continually translate level positions to screen positions and vice versa throughout your editor, so it makes sense to implement it in a helper class that is easily accessible.
To create the helper class, open the starter project in Xcode, and create a new file with the iOS\Cocoa Touch\Objective-C class template under the Utilities group. Name the class CoordinateHelper, and make it a subclass of NSObject
.
Open CoordinateHelper.h and replace its contents with the following:
#import "cocos2d.h"
@interface CoordinateHelper : NSObject
+(CGPoint) screenPositionToLevelPosition:(CGPoint) position;
+(CGPoint) levelPositionToScreenPosition:(CGPoint) position;
@end
The code is quite straightforward. Here, you define prototypes for two methods. Both take a CGPoint
coordinate, and return the translated position as a CGPoint
.
To create the method implementations, switch to CoordinateHelper.m and add the method below between the @implementation
and @end
lines:
+(CGPoint) screenPositionToLevelPosition:(CGPoint) position {
CGSize winSize = [CCDirector sharedDirector].winSize;
return CGPointMake(position.x / winSize.width, position.y / winSize.height);
}
The above method translates screen positions to level positions. To understand the code, think about the difference between level positions and screen positions for a moment.
The screen position is the absolute position on screen. The result of screenPositionToLevelPosition:
therefore should be the level position, which is the position relative to the screen size. All you need to do is first acquire the size of the screen with the winSize
property of CCDirector. Then divide the screen position parameter by this screen size and return the resulting coordinate. That’s it!
Now try to implement the reverse of the above method – levelPositionToScreenPosition:
in CoordinateHelper.m.
You can do it! If you need help, the spoiler code is below.
[spoiler]
+(CGPoint) levelPositionToScreenPosition:(CGPoint) position {
CGSize winSize = [CCDirector sharedDirector].winSize;
return CGPointMake(position.x * winSize.width, position.y * winSize.height);
}
[/spoiler]
OK, go ahead and take a look at the method if you need to verify that your code is correct. The new code is almost exactly the same as screenPositionToLevelPosition:
, but instead of dividing by winSize
, you now need to multiply.
Setting ID and Damping Parameters for the Pineapples
Now the position handling is complete. But in addition to the position, you need to store the relationship between the pineapple and ropes. This is only possible if you can identify each pineapple uniquely. You can do this by giving each pineapple a unique ID, which you’ll store in the XML file.
Additionally, not all pineapples need to behave identically. In the tutorial where you created the game, you implemented the ability to adjust the “bounciness” of each pineapple by changing its damping factor. If you didn’t work through that tutorial — no worries! The links to that tutorial are at the beginning of this one, so head over and take a look.
However, if you have to manually set up each pineapple’s damping parameter, that’ll be a lot of work! You can avoid this by setting a default value that is reasonable in most cases. This will allow you to focus on the exceptions — the pineapples that don’t have the default bounciness value. Here you’ll use 0.3 as your default, which is the same default that was used in the game tutorial.
The XML representing a pineapple looks something like this:
<pineapple id="1" x="0.50" y="0.70"/>
As you can see, this represents a pineapple with ID 1 and level coordinates of (0.5, 0.7). The damping is not specified, which means that the default of 0.3 will be used.
Here’s a definition of a pineapple that does not use the default damping:
<pineapple id="2" x="0.50" y="1.00" damping="0.01"/>
Setting up Your Rope Parameters
Now it’s time to consider the storage requirements of the ropes. Each rope has two anchor points — a starting point and an ending point — which both need to be tied to either a pineapple or the background. So how do you reference the bodies to attach your rope?
Recall that the pineapples all have a unique ID — you can use this as one anchor point of your rope. But what if a rope is tied to the background? For this you can set the body ID attribute to -1; alternately, just leave the body attribute empty and use the background as the default value if one is not supplied.
Quick — what’s the position of a rope that’s tied to a pineapple? That’s easy — it’s the position of the pineapple. Therefore, you don’t need to store this anchor point’s position, as you can just reference the position of the pineapple instead.
The benefit of storing the position just once (and referencing it by pineapple ID) is that you avoid the conundrum of storing contradictory information in your XML file if the values are stored more than once — especially if you’re editing it by hand, which is where mistakes tend to happen.
However, the background is a really big area — in this case, you’ll need to store the exact position of the anchor. Again, store this endpoint of the rope using relative coordinates, just as you did with the pineapple.
You only need one last property to store all the details about your rope. You can tie a rope really tightly, or you can let it hang loosely between its two anchor points. This property is defined as “sagginess”. The higher the sagginess value, the looser your rope. The default value sagginess value will be 1.1.
Putting Your XML File Format Together
Putting all of the above elements together to form the XML for your rope information, you’ll have something very similar to the following:
<rope>
<anchorA body="1"/>
<anchorB body="-1" x="0.85" y="0.80"/>
</rope>
At this point, you are almost done with designing the format of your level file. There’s only two things left to implement.
The first thing to handle is the XML version
header that indicates the version of XML being used, as shown below:
<?xml version="1.0"?>
Now you just need a good name for your top-level root element in your XML file. So pick a nice, descriptive name for your root element — like level:
<level> </level>
Okay — here’s the final test for your XML file creation. Can you bring it all together? Using all of the elements that you have defined above, try to write the XML for the level used in the original Cut the Verlet tutorial. Try not to peek at the spoiler below! :]
[spoiler title=”XML file representing level from tutorial”]
<?xml version="1.0"?>
<level>
<pineapple id="1" x="0.50" y="0.70"/>
<pineapple id="2" x="0.50" y="1.00" damping="0.01"/>
<rope>
<anchorA body="-1" x="0.15" y="0.80"/>
<anchorB body="1"/>
</rope>
<rope>
<anchorA body="1"/>
<anchorB body="-1" x="0.85" y="0.80"/>
</rope>
<rope>
<anchorA body="1"/>
<anchorB body="-1" x="0.83" y="0.60"/>
</rope>
<rope sagity="1.0">
<anchorA body="-1" x="0.65" y="1.0"/>
<anchorB body="2"/>
</rope>
</level>
[/spoiler]
Before you move on, compare your XML file to the spoiler code above to make sure you haven’t missed anything!
Creating Your XML File Handler
Now that you’ve designed the XML format for your level, you’ll need a mechanism to handle the XML files that store your level’s data.
In this tutorial, you’ll use GDataXML
for creating and parsing XML files in your project.
If you need specifics on how GDataXML
works and how to set it up for your own projects, you can check out our tutorial How To Read and Write XML Documents with GDataXML.
Note: GDataXML
isn’t the only player in the XML parser game. In fact, there’s another tutorial that compares GDataXML
to other parsers available for iOS here: How To Choose The Best XML Parser for Your iPhone Project.
The starter project has already been set up to work with GDataXML.
The starter project contains an XML file, levels/level0.xml, with the same level data that was used in the game tutorial. You’ll load the level data from this file, instead of using the hard coded implementation in the original game.
Loading a file into your game and using its contents is not terribly difficult, but it does require several steps.
- First, you need to be able to locate and open the file.
- Second, you’ll need some model classes that mirror the contents of the file and will be used to temporarily store and access all the file’s information in memory.
- And finally, you’ll need to load and parse the XML file to put all of its information into those model classes!
Here’s how to implement your file handling methods, step-by-step.
Create a Handler for File Access
If you want to read and write files, you first need to load them from their location in the file system. Since working with files is something that you’ll do many times in your level editor, you’ll create a new class that encapsulates this file handling functionality.
Your file handler should cover the following scenarios:
- finding the full file path to a filename
- checking for the existence of a file
- creating a folder
Implement your file handler as follows.
Create a new file with the iOS\Cocoa Touch\Objective-C class template under the Utilities group. Name the class FileHelper, and make it a subclass of NSObject.
Open FileHelper.h and replace its contents with the following:
@interface FileHelper : NSObject
+(NSString*) fullFilenameInDocumentsDirectory:(NSString*) filename;
+(BOOL) fileExistsInDocumentsDirectory:(NSString*) fileName;
+(NSString *)dataFilePathForFileWithName:(NSString*) filename withExtension:(NSString*)extension forSave:(BOOL)forSave;
+(void) createFolder:(NSString*) foldername;
@end
These are the methods that FileHelper
provides for doing common file-related tasks.
Next, you need to implement each of the above methods. This requires some knowledge of the iOS file system.
On a desktop computer, it is up to the programmer to decide the location of each file as desired. However, in iOS each app has to stick to a folder structure defined by Apple.
Basically, everything is stored under four folders:
- /AppName.app: The bundle directory containing your app and all its resource files. This folder is read-only.
- /Documents/: Storage for critical documents that your app cannot recreate, such as user-generated content. This folder is backed up by iTunes.
- /Library/: A folder completely hidden from the user that’s used to store app-specific information that should not be exposed to the user.
- /tmp/: For temporary files that do not need to persist between different sessions of your app.
If you want more detailed insight into the file structure you can look at Apple’s Documentation here:
File System Overview on iOS.
Okay, time for a quick pop quiz. Looking at the four storage areas above, which ones will your level editor need to access?
[spoiler]
- Bundle directory: For reading preset level XML files from the app.
- Documents directory: For saving edited files.
[/spoiler]
Now that you have a better idea about the structure of the iOS file system, you can implement your file handler methods.
File Handler: Getting the Full Path to a File
Add the following method implementation to FileHelper.m:
+(NSString*) fullFilenameInDocumentsDirectory:(NSString*) filename {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectoryPath = [paths objectAtIndex:0];
NSString* filePath = [documentsDirectoryPath stringByAppendingPathComponent:filename];
return filePath;
}
The above is a class method. Class methods are directly associated with a class instead of an instance. To call the method, you use the class name instead of an instance of the class. You indicate to the compiler that a method is a class method by using a + instead of a – at the beginning of the method declaration.
The above method returns the full path for a filename in the documents directory as an NSString.
NSSearchPathForDirectoriesInDomains()
returns a list of directories for a specific search path and a domain mask. In this case, you ask for the user’s Documents directory by using NSDocumentDirectory
as the search path and NSUserDomainMask
as the mask.
The return value of NSSearchPathForDirectoriesInDomains()
is not just a single directory path but an array. You only care about the first result, so you simply select the first element and append the filename to get the full path to the file.
Now you can try out your file handler class and see where your own Documents directory lives.
Add the following code to init
in CutTheVerletGameLayer.mm:
// Add to the top of the file
#import "FileHelper.h"
...
-(id) init
{
if( (self=[super init])) {
// Add the following lines
NSString* filePath = [FileHelper fullFilenameInDocumentsDirectory:@"helloDirectory.xml"];
NSLog(@"%@", filePath);
...
}
}
Build and run your project. You should see the file path in the console, like so:
If you run using the simulator, you can easily check the contents of the document directory. Just copy the path to the file, omitting the actual filename, right click on the Finder on your dock and select Go to folder…. Paste in the file path and press Enter.
Right now the Documents folder for your app is probably empty, but you’ll soon add some files to it.
On to the next method — checking if a file exists.
File Handler: Checking if a File Exists
Add the following method to FileHelper.m:
+(BOOL) fileExistsInDocumentsDirectory:(NSString*) filename {
NSString* filePath = [FileHelper fullFilenameInDocumentsDirectory:filename];
return [[NSFileManager defaultManager] fileExistsAtPath: filePath];
}
This one is rather simple. You take the full file path given to you by fullFilenameInDocumentsDirectory:, and ask the file manager whether a file with this name exists.
You can test this method by adding the following code to CutTheVerletGameLayer.mm:
-(id) init {
if( (self=[super init])) {
NSString* filename = @"helloDirectory.xml";
BOOL fileExists = [FileHelper fileExistsInDocumentsDirectory:filename];
if (fileExists) {
NSLog(@"file %@ exists", filename);
} else {
NSLog(@"file %@ does not exist", filename);
}
...
}
}
Build and run your app. Right now, the console output should show that the file does not exist.
If you want to test that your code really does discover the file when it exists, create an empty file with the correct name in the Documents directory. (You can access the Document directory via Finder as described above.)
Build and run your app again, and the console should now tell you that the file exists.
So far you have only accessed the Documents directory, but the app should load files from the app bundle directory just in case there is no user-generated file.
Why would you do this?
This allows your app to have an initial version of the files. You can put them into the main app bundle, and load them into the editor on first run, where you can change the contents and then save them to the Documents directory.
This is where the third method of the FileHelper class comes in.
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?
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:
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!
Displaying Your Pineapple Objects On-screen
Your level-loading code will replace the current hard-coded game implementation in order to create a playable game scene from the information in the XML file.
Since the hard work has already been done, all you need to do at this point is to iterate over the loaded level information and create a physical body for each pineapple and rope contained in the scene.
Sounds easy, doesn’t it? :]
Start by adding some more imports to the top of CutTheVerletGameLayer.mm:
#import "PineappleModel.h"
#import "RopeModel.h"
#import "CoordinateHelper.h"
Okay, pineapples — enter stage left!
In CutTheVerletGameLayer.mm, replace all the code between the two #warning
lines (including the two #warning lines
themselves) in initLevel with the following:
NSMutableDictionary* pineapplesDict = [NSMutableDictionary dictionary];
for (PineappleModel* pineapple in levelFileHandler.pineapples) {
b2Body* body = [self createPineappleAt:[CoordinateHelper levelPositionToScreenPosition:pineapple.position]];
body->SetLinearDamping(pineapple.damping);
[pineapplesDict setObject:[NSValue valueWithPointer:body] forKey:[NSNumber numberWithInt: pineapple.id]];
}
In the above code, you first create a dictionary that will contain all the pineapple bodies.
Why would you do this, when you already have the pineapple data stored elsewhere? Think ahead for a moment. After loading and displaying the physical pineapples, you’ll need to connect them with the ropes.
To do this, you’ll need to know the body that represents the pineapple, so it makes sense to temporarily store them in a dictionary, where the key to each body is the pineapple’s ID.
In order to create a body for each pineapple, iterate over all of the pineapple models in the file handler. For each pineapple, create a body and set its position. Calculate the screen coordinates from the relative level coordinates by calling levelPositionToScreenPosition
.
Next, the damping property is set. Finally, you add the newly created body to the dictionary.
All the pineapples are now loaded and should show up at their respective positions.
Build and run your project. Your game fires up, the pineapples display on the screen and…
Uh oh. You expected your game to make a splash — but not literally! The pineapples aren’t staying in the trees, as seen below:
If you think about it, you probably should have expected this result. Gravity is acting on the pineapples, but there aren’t any ropes to hold them in position!
Time to tie this one off by adding some ropes! :]
Displaying Your Rope Objects On-screen
Add the following code to CutTheVerletGameLayer.mm, just after the code that loads the pineapples:
for (RopeModel* ropeModel in levelFileHandler.ropes) {
b2Vec2 vec1;
b2Body* body1;
if (ropeModel.bodyAID == -1) {
body1 = groundBody;
CGPoint screenPositionRopeAnchorA = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
vec1 = cc_to_b2Vec(screenPositionRopeAnchorA.x, screenPositionRopeAnchorA.y);
} else {
body1 = (b2Body *)[[pineapplesDict objectForKey: [NSNumber numberWithInt:ropeModel.bodyAID]] pointerValue];
vec1 = body1->GetLocalCenter();
}
// TODO: Mysteriously, the second connection is missing. Can you create it?
[self createRopeWithBodyA:body1 anchorA:vec1 bodyB:body2 anchorB:vec2 sag:ropeModel.sagity];
}
This loops through the rope model objects and fills in the body1
and vec1
values for the rope’s first anchor point, depending on whether the rope is tied to the background or tied to a pineapple.
The code looks pretty good, but what’s with that TODO
note? The code above only implements a single anchor point — it’s left to you to determine how to implement the second anchor point.
If you aren’t sure how to do this, go through the code for creating the first anchor point step-by-step.
To set up an anchor you need two things: a body to attach to, and a vector which indicates the position of the anchor point.
You have to distinguish between two cases for your second anchor point.:
- Body ID is -1: this means that the rope is tied to the background and you need to convert the anchor point coordinates stored in the model class to determine its location. Don’t forget to convert the level coordinates to screen coordinates.
-
The rope is tied to a pineapple: Get the
b2Body
out of the pineapple dictionary and use its center for the anchor’s position.
Okay, don’t panic — if you’re really stuck, the complete spoiler code is below. But give it a go before you give it up! :]
[spoiler]
b2Vec2 vec2;
b2Body* body2;
if (ropeModel.bodyBID == -1) {
body2 = groundBody;
CGPoint screenPositionRopeAnchorB = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
vec2 = cc_to_b2Vec(screenPositionRopeAnchorB.x, screenPositionRopeAnchorB.y);
} else {
body2 = (b2Body *)[[pineapplesDict objectForKey: [NSNumber numberWithInt:ropeModel.bodyBID]] pointerValue];
vec2 = body2->GetLocalCenter();
}
[/spoiler]
Build and run the game and you should now be able to feed the crocodile. That poor animal has probably been starving, waiting for the level to be finished! :]
If you have done everything correctly, the level should play identically to the original hard-coded version. The major difference is that now you can simply edit the XML file, restart the game and a slightly different level layout will show up!
Where to Go From Here?
Here is a sample project with all of the code from the above tutorial.
Take some time and freely edit your XML level file to try out as many combinations of pineapple and rope placements as you can think of. Right now, the level editor only gives you the ability to load existing XML files — you can’t actually edit them yet. It’s more of a level “loader” than a level “editor” at this point.
Don’t despair — the second part of this tutorial is all about getting the editor working, and allowing you to edit levels live on your device!