游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

分享开发者自制游戏关卡编辑器的教程(1)

发布时间:2013-06-21 17:47:11 Tags:,,,

作者:Barbara Reichart

在本教程中,你将学习如何制作山寨版《切绳子》的关卡编辑器。(请点击此处阅读本文第2部分

使用关卡编辑器可以新关卡制作变得更简单。你要做的就是拖动和在你希望的地方放置绳子和菠萝。

这个关卡编辑器的优点在于,它是内置于游戏的,所以玩家可以直接在自己的设备上制作自己的关卡。

关卡编辑器不仅对于终端玩家来说是非常有趣的,对于游戏开发者,使用关卡编辑器制作关卡也比手动编码来得方便和迅速。

一个额外的好处是,你还可以用关卡编辑器来检验你的游戏概念,这对于像《切绳子》这种物理游戏来说尤其重要,因为有时候很难预测物理引擎的行为,但却容易在真实环境下测试那些行为。

在游戏中加入关卡编辑器是增加游戏寿命和实用性的好办法,因为玩家可以用它制作自己的关卡——和甚至把自己的成果分享给其他游戏粉丝。

开始

你将使用该游戏的更新版本作为起始项目。如果你还没有新版本,那就去下载一个并在Xcode中打开。

起始项目的代码几乎与原来的教程一样,最大的区别就是,这个项目现在支持Objective-C ARC,而原来的项目不支持。我们也已经把XML解析器添加到原来的项目中,在你制作你的关卡编辑器时你会使用到。

注:起始项目没有经过调整,不适合iPhone 5的4英寸屏幕。所以当你在模拟器上运行时,确保使用iPhone的3.5英寸模拟器,而不是4英寸模拟器!

选择保存关卡数据的文件格式

制作关卡的第一步是决定保存关卡数据的文件格式。保存信息的格式有很多种,但要考虑的最重要的要求如下:

1、简单的储存格式

2、平台独立

3、机器可读性和人可读性——便于文件排错!

在这个项目中,你将使用XML来保存你的关卡;它满足以上所有要求,许多读者可能已经使用过XML了。

接着,你必须考虑你要储存什么信息。考虑以下截图,你可以推断出需要储存的信息是什么吗?

Screen-Shot(from raywenderlich)

Screen-Shot(from raywenderlich)

给你一点提示吧——想一想对象的属性,除位置之外。

所以你想到了什么?菠萝?绳子?背景?一个关卡中有许多信息——有时候比你的眼睛所看到的更多。

以下你的关卡编辑器文件中必须包含的关卡元素:

Pineapple Elements:

ID:确定各个菠萝并储存菠萝和绳子之间的关系

Position:X和Y轴座标

Damping:菠萝的活动情况——这是由游戏的物理引擎使用的信息

Rope Elements:

两个固定点,属性如下:

Body:body的ID和固定点

Position:可选择的,依附到菠萝时就不需要。在那种情况下,你应该直接使用菠萝的位置

Sagginess:绳子悬挂的松紧程度

General Elements(你在几乎所有XML文件中都会找到)

Heade ,包括XML的版本

Level:一个把所有元素捆绑在一起的顶层元素

你是不是漏掉了什么?没关系——只是看看难免会错过某些信息。

接下来的部分将更详细地介绍你的XML文件要储存的各个元素。

计算菠萝的位置

一切都是相对的—–甚至菠萝的位置!

因为你的编辑器将在视网膜和非视网膜显示屏上运行,所以你应该根据屏幕尺寸来保存所有相对位置,这样你就不用根据象素来计算各个放置位置了。

怎么做?相当简单——根据屏幕位置计算对象的位置,按屏幕宽度划分它的X座标,按屏幕高度划分它的Y座标。如下图所示:

Slide(from raywenderlich)

Slide(from raywenderlich)

(请计算一下右图中的关卡座标。屏幕尺寸是320×480。)

在左图中,菠萝的相对位置是分辨率320×480的屏幕的中间。据此,请计算一下右图中的菠萝的相对位置!

答案:屏幕位置为(240, 288),转关卡位置则是(0.75, 0.6)。

你必须继续把关卡位置转换成屏幕位置,反之亦然,如果能在helper class中执行就更好了。

为了制作helper class,请在Xcode中打开起始项目,然后在Utilities组中打开iOS\Cocoa Touch\Objective-C class创建新文件。将这个类命名为CoordinateHelper,并将其作为NSObject的子类。

打开CoordinateHelper.h,把它的内容替换成如下内容:

#import “cocos2d.h”

@interface CoordinateHelper : NSObject

+(CGPoint) screenPositionToLevelPosition:(CGPoint) position;
+(CGPoint) levelPositionToScreenPosition:(CGPoint) position;

@end

这段代码很直观。这里,你定义了两个方法的原型。都采用CGPoint座标,将转换后的位置作为CGPoint返回。

为了制作这个方法执行,切换到CoordinateHelper.m,在@implementation和@end之间增加如下内容:

+(CGPoint) screenPositionToLevelPosition:(CGPoint) position {
CGSize winSize = [CCDirector sharedDirector].winSize;
return CGPointMake(position.x / winSize.width, position.y / winSize.height);
}

上述方法把屏幕位置转换成关卡位置。为了理解这段代码,请思考一下关卡位置与屏幕位置的区别。

屏幕位置是屏幕上的绝对位置。因此,screenPositionToLevelPosition的结果应该是关卡位置,也就是相对屏幕尺寸的位置。你需要做的,首先是用CCDirector的winSize属性获得屏幕尺寸。然后按这个屏幕尺寸划分屏幕位置参数,最后返回结果座标。就是这样!

现在,试一下执行以上方法的相反方法——levelPositionToScreenPosition: in CoordinateHelper.m。

如果有困难,请参考以下代码:

+(CGPoint) levelPositionToScreenPosition:(CGPoint) position {
CGSize winSize = [CCDirector sharedDirector].winSize;
return CGPointMake(position.x * winSize.width, position.y * winSize.height);
}

如果你需要验证你的代码是否正确,请参考以上。新代码几乎与screenPositionToLevelPosition一样,除了不是用winSize划分。

给菠萝设置ID和Damping参数

现在,我们已经解决了位置问题了。除了位置,你还必须保存菠萝和绳子之间的关系。这要求你确定各个菠萝的位置。为此,你应该给各个菠萝一个专有ID,并保存在XML文件中。

Pineapple With ID(from raywenderlich)

Pineapple With ID(from raywenderlich)

(总是保证你的菠萝知道自己是谁)

另外,并非所有菠萝都必须有相同的表现。在本教程中,你可以通过改变菠萝的damping参数来调整各个菠萝的“弹力”。

然而,如果你必须手动设置各个菠萝的damping参数,那工作量就太大了!你可以通过设置适用于大多数情况的默认值来节省工作。你只需要专注于例外情况——没有默认弹力值的菠萝。这里,你将用0.3作为你的默认值,这也是原版游戏使用的默认值。

在XML中,菠萝的属性如下:

<pineapple id=”1″ x=”0.50″ y=”0.70″/>

如你所见,菠萝的ID是1,关卡座标是(0.5, 0.7),damping参数未指明,这意味着它将使用默认值0.3。

以下是未使用默认damping参数的菠萝的属性:

<pineapple id=”2″ x=”0.50″ y=”1.00″ damping=”0.01″/>

设置绳子的参数

现在可以考虑绳子的储存要求了。各个绳子有两个固定点——一个起点和一个终点。二者都必须依附到菠萝或背景上。所以你应该怎么把body与绳子相连?

回想一下菠萝都有特定的ID—-你可以使用这个作为绳子的一个固定点。但如果绳子与背景相连怎么办?你可以设置body ID为-1;或者,干脆放空body属性,使用背景作为默认值。

绳子依附菠萝的位置是什么?简单—-就是菠萝的位置。因此,你不必保存这个固定点位置,因为你可以直接引用菠萝的位置。

保存一次这个位置(引用菠萝的ID)的好处是,避免因在XML文件中保存重复的座标信息而导致混乱,特别是如果你是手动编写代码,这么做可以减少出错的概率。

然而,背景确实是一片大区域—–因此,你必须保存固定点的确切位置。另外,用相对座标保存绳子的终点,与菠萝的处理方法一样。

储存绳子的所有细节,你只需要最后一个属性。你可以把绳子悬挂得很紧,也可以很松。这个属性就是“sagginess”。这个值越高,绳子悬挂得越松。sagginess的默认值是1.1。

整合所有元素

把所有以上元素放在一起,形成绳子的XML,代码如下:

<rope>
<anchorA body=”1″/>
<anchorB body=”-1″ x=”0.85″ y=”0.80″/>
</rope>

此时,你已经差不多完成关卡文件格式的设计了。但还有两件事要做。

第一件是处理XML version标题,以显示你使用的XML的版本,如下所示:

<?xml version=”1.0″?>

现在你只需要给XML文件中的顶层根元素命名。给你的根元素找一个好名字吧,如level:

<level> </level>

好吧,现在可以对你的XML文件做最终测试了。使用你在之前定义的所有元素,给原版《切绳子》的关卡写XML。尽量不要偷看下面的参考答案吧:

<?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>

把你的XML文件与上面的答案比较一下,看看你是不是犯错了。

制作XML File Handler

现在你已经设计好关卡的XML格式了,但你还需要一个引出保存了关卡数据的XML文件的途径。

在本教程中,你将使用GDataXML来制作和解析项目的XML文件。

注:GDataXML不是唯一的XML解析器。

起始项目已经设置好GDataXML了。

起始项目包含XML文件levels/level0.xml。你要从这个文件中加载关卡数据,而不是使用原版游戏中的硬代码执行。

加载文件到你的游戏中,使用它的内容并不难,但需要几个步骤:

1、你必须能够定位和打开文件。

2、你需要一些模型类,用于映射文件的内容和用于在内存中临时储存和访问文件的所有信息。

3、你需要加载和解析XML文件,并将所有这些信息放入那些模型类中。

这就是你的文件处理方法的执行过程。

制作文件访问的Handler

如果你想阅读和编写文件,你首先要把它们从文件系统中加载出来。因为你要在关卡编辑器中多次处理文件,所以你要制作一个新class来压缩这个文件处理函数。

你的文件Handler应该包括以下情况:

1、寻找文件名的完整文件路径

2、查看文件是否存在

3、创建文件夹

执行你的文件Handler如下。

通过Utilities组下的iOS\Cocoa Touch\Objective-C class创建新文件。命名这个class为FileHelper,使它成为NSObject的子类。

打开FileHelper.h,把它的内容替换成如下内容:

@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

以上就是FileHelper执行一般文件相关任务的方法。

接着,你需要执行以上各个方法。你需要一些关于iOS文件系统的知识。

在台式电脑中,由程序员决定各个文件的位置。然而,在iOS中,各个应用必须符合Apple定义的文件夹结构。

基本上,所有东西都储存在四个文件夹中:

/AppName.app: 包含应用和所有资源文件的目录。这个文件夹是只读的。

/Documents/: 储存你的应用不可再生的重要文件,如用户生成内容。iTunes支持这个文件夹。

/Library/: 对用户完全不可见的文件夹,用于储存用户不可见的、特定应用的信息。

/tmp/:临时文件夹,在应用运行的不同阶段临时保存信息。

好吧,可以突击测试一下了。以上四个文件夹,你的关卡编辑器必须访问哪个?

答案:Bundle directory:用于从应用中读取当前关卡XML文件。

Documents directory:用于保存编辑好的文件。

既然你已经了解iOS文件系统的结构了,现在你可以执行你的文件Handler方法了。

File Handler:获得文件的完整路径

在FileHelper.m中添加以下方法:

+(NSString*) fullFilenameInDocumentsDirectory:(NSString*) filename {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectoryPath = [paths objectAtIndex:0];
NSString* filePath = [documentsDirectoryPath stringByAppendingPathComponent:filename];
return filePath;
}

以上是一个class方法。class方法直接与class而不是实例相关。为了调用class方法,你要使用class名称而不是class的实例。通过在class声明的开头部分使用+而不是-,可以让解析器知道某个方法是class方法。

以上方法返回documents directory中的文件名的完整路径为NSString(字符串)。

NSSearchPathForDirectoriesInDomains()返回一个明确的搜索路径的directory和域名掩码。在这种情况下,你可以使用NSDocumentDirectory作为搜索路径和NSUserDomainMask作为掩码来寻找用户的documents directory。

NSSearchPathForDirectoriesInDomains()的返回值不只是一个directory路径,还是一个数组。你只关心第一个结果,所以你只要选择第一个元素,然后添加文件名就能得到文件的完整路径。

现在请尝试一下你的文件handler class,看看你自己的Documents directory在哪里。

将以下代码添加到CutTheVerletGameLayer.mm的init中:

// 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);

}
}

创建和运行你的项目。你应该在游戏机上看到如下文件路径:

helloDirectory.xml(from raywenderlich)

helloDirectory.xml(from raywenderlich)

(documents directory中的helloDirectory.xml的完整文件路径)

如果你使用模似器,你可以轻易地查看到这个documents directory的内容。只要复制文件的路径,省略真正的文件名,右击Finder并选择Go to folder…。粘贴在文件路径上,并按下Enter。

现在, 你的应用的Documents folder可能是空的,但你很快就会往里面添加一些文件了。我们再看看另一个方法——查看文件是否存在。

File Handler:查看文件是否存在

添加如下方法到FileHelper.m:

+(BOOL) fileExistsInDocumentsDirectory:(NSString*) filename {
NSString* filePath = [FileHelper fullFilenameInDocumentsDirectory:filename];
return [[NSFileManager defaultManager] fileExistsAtPath: filePath];
}

这一个相当简单。你按fullFilenameInDocumentsDirectory:获得完整文件路径,然后询问文件管理器以带这个名称的文件是否存在。

你可以通过添加如下代码到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);
}

}
}

创建并运行你的应用。现在,游戏机的结果应该显示文件不存在。

如果你想测试你的代码发现文件存在的情况,那你就用正确的名称在Documents directory中新建一个空文件(游戏邦注:可以通过Finder访问Document directory)。

再次创建并运行你的应用,游戏机现在应该告诉你文件存在。

到目前为止,你只访问了Documents directory,但应用应该从应用bundle directory中加载文件,以防没有用户生成的文件。

为什么要这么做?

这使你的文件有文件的初始版本。你可以把它们放进主应用包中,然后在第一次运行时加载到编辑器中,你可以改变内容后再保存到Documents directory。

现在就是到了FileHelper class的第三个方法。

File Handler:获得存在文件的路径

添加如下代码到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];
}
}

这段代码中介查看带有特定名称的文件夹是否存在。如果不存在,它就使用文件管理器创建一个。

你可能会问,为什么你需要这么简单的helper 函数。当你的编辑器变得更复杂时,用户在编辑他们的游戏时可能会创建许多文件。没有现成的体面的创建文件夹的方法,你很快就会被混乱的文件包围!

创建Game Objects的Model Classes

现在,你已经具备寻找和加载文件所需的一切东西了。但读取文件后你要怎么处理那些内容?

这时候最好做一个model class来储存文件中的信息。这样你就可以很容易地在你的应用中访问和操作这些数据。

首先通过iOS\Cocoa Touch\Objective-C class创建一个名为AbstractModel的class,使它成为NSObject的子类,并放在Model组中。

找开AbstractModel.h,用以下代码替换它的内容:

#import “Constants.h”

@interface AbstractModel : NSObject

@property int id;

@end

这添加了一个特殊的ID作为属性,将用于确定各个model实例。

AbstractModel不要实例化。在某些程序语言如Java中,你可以通过使用抽象关键词告之解析器。

然而,在Objective-C中,没有什么简单的机制能使它不实例化成class。所以你必须依靠命名惯例和你的记忆来强制!

注:如果你不想依靠命名惯例或你不信任你的记性,你可以寻找其他方法用Objective-C制作abstract classe。

下一步是制作菠萝的model class。

制作菠萝的model class

通过iOS\Cocoa Touch\Objective-C class创建新class。命名class为PineappleModel,并使之成为AbstractModel的子类。

你首先需要添加菠萝的position和damping属性。

切换到PineappleModel.h,用如下代码替换它的内容:

#import “AbstractModel.h”

@interface PineappleModel : AbstractModel

@property CGPoint position;
@property float damping;

@end

现在切换到PineappleModel.m,并在@implementation和@end之间添加如下代码:

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

你在这个方法中做的就是制做一个class实例,并给它的属性设置合适的默认值。你使用的常量已经在Constants.h定义了。

无论你信不信,这就是菠萝的完整model class!

model class几乎总是极其简单的,不包含任何程序逻辑。它们其实只是用于储存应用将使用到的信息。

制作菠萝的Model Class

现在菠萝model已经完成了,作为挑战,你自己尝试一下制作绳子的model吧!

如果你不记得绳子的属性,请看看levels文件夹中的level0.xml文件。

答案:

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

这就完了?请参照上述答案检查你自己的代码,确保所有的属性都定义正确,以及class使用的名称和属性与教程中的一样。另外,有些代码之后可能对你完全不适用!

你是不是迫不及待地想载入文件了?

好吧,按以下步骤加载你的文件吧!

载入Level Data File

通过LevelEditor组的iOS\Cocoa Touch\Objective-C class创建新class。命名新class为LevelFileHandler并使之成为NSObject的子类。

打开LevelFileHandler.h,并用如下代码替换它的内容:

#import “Constants.h”

@class RopeModel, PineappleModel;

@interface LevelFileHandler : NSObject

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

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

@end

LevelFileHandler负责处理所有关卡数据:加载然后写入数据文件。关卡编辑器会访问LevelFileHandler来获得所有它需要的和写入变化的信息。

这里,你已经在LevelFileHandler中设置了一些属性。它保存了从XML文件读取的所有菠萝和绳子的数据。

现在你需要把所有这些数据导入到LevelFileHandler.m中。这包括model class和你刚才创建的file helper,以及你用于解析XML文件的GDataXMLNode.h。

切换到LevelFileHandler.m并添加如下代码:

#import “PineappleModel.h”
#import “RopeModel.h”
#import “FileHelper.h”
#import “GDataXMLNode.h”

接着,添加私用变量到LevelFileHandler.m,即在#import语句下添加如下class扩展:

@interface LevelFileHandler () {
NSString* _filename;
}

@end

以上变量储存了当前已载入的关卡名称。这里你使用的是私用实例变量,因为你在类之外你并不使用这个信息。通过对所有其他class隐藏这个信息,你已经确保它不会出乎你的意料地改变!

现在在@implementation和@end语句之间添加如下代码到LevelFileHandler.m中,

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

init只储存在实例变量中的文件名,以及调用loadFile。

你是不是想问,loadFile在哪里?

好问题——你马上就要执行那个方法了!

添加如下代码到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
}

以上代码就引出了FileHelper class。它首先获得保存文件名的数据文件路径,然后载入该文件中包含的数据。之后,它初始化GDataXMLDocument并进入要被解析的加载文件数据。

当你的文件不是良好的XML文件的文件时,GDataXMLDocument的init方法会让你设置误差参数。在本教程中,你只要忽略GDataXMLDocument反馈的所有错误,继续使用没有菠萝和绳子的空关卡。

在最终版应用中,你绝对必须正确地处理这些错误。但现在,你只是为了照顾关卡编辑器的其他部分而走了捷径罢了。

在你可以使用这个新函数以前,你需要把file handler传送到你的游戏场景中,这样场景才能利用LevelFileHandler中的关卡数据。

为此,当制作场景时,你可以把LevelFileHandler作为实例传送。

打开CutTheVerletGameLayer.h并用如下语句替换:

+(CCScene *) scene;

还有这一句:

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

现在,你得确保你的执行文件知道LevelFileHandler是什么。

切换到CutTheVerletGameLayer.mm,在文件顶部添加如下导入声明:

#import “LevelFileHandler.h”

然后,在CutTheVerletGameLayer.mm的@interface之前添加一个类扩展,以命令私用变量储存这个LevelFileHandler实例:

@interface HelloWorldLayer () {
LevelFileHandler* levelFileHandler;
}

@end

接着,用如下代码替换CutTheVerletGameLayer.mm的scene执行文件:

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

就像原来的scene方法,这产生了运行游戏的HelloWorldLayer对象,但现在它还把LevelFileHandler对象传送到那一层。

最后,用如下代码修改CutTheVerletGameLayer.mm的init方法执行文件:

// 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;
}

注意,在以上代码中,方法名称已经变了——现在有参数进入了。

既然加载新关卡的所有必须组件都到位了,现在你可以给设置场景第一次生成的地方——AppDelegate.mm中的LevelFileHandler了。

但是,为了让AppDelegate知道LevelFileHandler是什么,你必须添加如下导入声明到AppDelegate.mm的顶部:

#import “LevelFileHandler.h”

仍然是在AppDelegate.mm中,添加如下语句到application:didFinishLaunchingWithOptions:的底部,以生成LevelFileHandler对象和把它传送到场景:

- (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;
}

创建并运行你的项目!

如果一切顺利,你应该在游戏机中看到如下XML文件的内容:

loaded to console(from raywenderlich)

loaded to console(from raywenderlich)

(XML成功地把内容加载和写入到游戏机)

把菠萝信息载入到Model Class

太好了!游戏机正确地显示了XML内容,你现在知道所有部分都按计划运作了。

你的下一个任务是让所有XML数据加入到你的model class的合适位置。

与model class本身相比,占用model class的代码机制看起来相当混乱!但这就是你要多费功夫的地方——从文件中取得数据,然后将其转换成适用于你的应用的格式。

从菠萝的model class开始。

添加如下代码到LevelFileHandler.m的loadFile末尾,替换//TODO: parse XML and store into model classes语句如下:

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];
}

在以上代码中,你首先得到保存在你的XML文件的根元素的所有命名为“pineapple”的元素。接着,你重复所有pineapple元素,并给第一个菠萝元素做一个pineappleModel实例。最后,你根据XML文件载入的信息填充它的参数。

对于上述大多数元素,获得你的model实例是相当简单的。但damping属性就比较麻烦了。

回想一下你设置damping默认值为非零,damping元素出现在XML文件是可选择的。当damping属性不存于文件时,你就要赋默认值。

然而,如果你试图计算由attributeForName:返回的不存在的值,你得到的结果将为零——这不是你想要的!

为了知道一个属性是否存在,你只要查看attributeForName:的返回值是否设置了。如果是,那么把它赋给菠萝的damping变量,否则就让它为默认值。

这段代码的最后一步是添加新建的菠萝model到菠萝列表中,即调用[self.pineapples addObject:pineappleModel]。

好了,你现在已经加载了所有的菠萝数据了——可以在游戏中运用了!

切换到LevelFileHandler.h并添加如下方法:

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

以上方法把ID当作一个参数来确定菠萝model,然后返回匹配这个ID的菠萝。

现在切换到LevelFileHandler.m然后添加如下方法:

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

getModelWithID:fromArray:是一个私用方法,把它的参数作为ID和包含AbstractModel的数组。在这个方法中,你重复所有数组中的元素,查看它们的ID,并看看这些ID是否与需要的ID相同,返回当前AbstractModel。

这个方法看来起似乎太复杂了。为什么不直接重复包含菠萝的数组,因为那就你在寻找的信息呀?

现在,你确实只对寻找带特定ID的菠萝有兴趣。然而,极有可能你的游戏的其他对象会需要完全相同的代码。

在这个项目中,只有一个其他对象—绳子,但在其他项目中可能会有更多其他对象。创建一个只寻找菠萝的方法,当你搜索绳子时也可以利用这段代码语句。如果不这样做,开发时间和成本就会增加。

所以,使用这种非常实用的getModelWithID:fromArray:方法吧,getPineappleWithID:的执行文件基本上减少到一条语句,如下所见。

添加以下方法到LevelFileHandler.m:

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

以上方法清楚地结束了从XML文件载入菠萝数据的完整执行文件。

现在是绳子对象!

将绳子信息载入到Model Class

既然你已经知道如何处理菠萝了,那就把从XML文件载入绳子数据和占用正确的model class的方法写下来吧。

给你几点提示:

1、不要忘了各个绳子需要专用ID——你不能在XML文件中储存所有绳子ID,因为ID只有你关卡编辑器的内容。

2、你的新代码应该放在LevelFileHandler.m的loadFile末尾。

3、你的绳子载入执行文件的结构与菠萝载入执行文件的非常类似——但要修改成绳子属性。

可以开始了吗?不要偷看下面的答案!

答案:

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++;
}

完成了?还是你放弃了?

无论如何,将你的执行文件对比一下上述参考答案,看看你弄错什么。

这样关卡数据格式设计和执行文件就结束了。现在可以把所有这些成果显示在屏幕上了!

把菠萝对象显示在屏幕上

你的关卡加载代码会替换当前的游戏执行文件的硬代码,以便按XML文件的信息生成可玩的游戏场景。

因为你已经完成艰难的工作了,剩下要做的就是重复加载的关卡信息,和制作该场景中的各个菠萝和绳子的物理实体。

听起来很容易,是吧?

添加如下代码到CutTheVerletGameLayer.mm的顶部:

#import “PineappleModel.h”
#import “RopeModel.h”
#import “CoordinateHelper.h”

好了,菠萝登场了!

在CutTheVerletGameLayer.mm中替换initLevel中的介于两条#warning语句(包括两条#warning语句本身)之间的所有代码如下:

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]];
}

在上述代码中,你首先创建包含所有菠萝body的dictionary。

当你在其他地方已经储存有菠萝数据的时候,为什么还要这么做?再想一想。加载和显示实体菠萝后,你必须将它们与绳子相连。

为此,你必须知道代表菠萝的body,所以把它们临时储存在dictionary中是合理的,各个body的关键就是菠萝的ID。

为了给各个菠萝制作body,重复file handler中的所有菠萝model。对于各个菠萝,制作body和设置它的位置。按相对关卡座标计算屏幕座标,即调用levelPositionToScreenPosition。

接着,设置damping属性。最后,添加新建的body到dictionary。

所有菠萝现在都加载好了,且应该显示在各自的位置上。

创建和运行你的项目。你的游戏生动起来了,菠萝就显示在屏幕上……

你希望你的游戏溅水花——不是字面上的意思。但菠萝并不是长在树上的,如下图:

pineapples(from raywenderlich)

pineapples(from raywenderlich)

(菠萝像BOSS一样从天而降)

如果你想这么做,你的结果就如上图所示。重力作用于菠萝,但没有绳子把它们固定起来!

应该添加一些绳子了!

把绳子对象显示在屏幕上

添加如下代码到CutTheVerletGameLayer.mm,就放在加载菠萝的代码后面:

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];
}

这依次通过绳子model对象,并为绳子的第一个固定点添加body1和vec1值,取决于绳子是否与背景或菠萝相连。

这段代码看起来相当不错,但TODO是怎么回事?这句代码只执行一个固定点——这要由你来决定如何执行第二个固定点。

如果你不确定怎么做,那就回顾一下制作第一个固定点的步骤。

为了设置固定点,你需要两样东西:可依附的body和指示固定点的位置的矢量。

你必须区别第二个固定点的两种情况:

1、Body ID是-1时:这意味着绳子依附在背景上,你必须转换储存在model class中的固定点座标,以确定它的位置。不要忘了根据屏幕座标转换关卡座标。

2、绳子依附到菠萝上时:从菠萝dictionary中取出b2Body,并用它的中点作为固定点的位置。

好了,别怕——如果你确实难住了,参考答案就在下面。不过别急着放弃,最好先自己尝试一下。

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();
}

创建并运行游戏,你现在应该能够喂养小鳄鱼了。可怜的小家伙饿得不行了,等着你通关呀!

如果你把一切都做对了,这个关卡应该与原来的版本一样。主要的区别就是现在你只要简单地编辑XML文件,重启游戏,一个稍有不同的关卡布局就出现了!

然后呢?

花一些时间自由地编辑你的XML关卡文件,尽可能多地尝试菠萝的座标和绳子的放置方法。现在,这个关卡编辑器只是让你能够加入现有的XML文件——你其实还不能够编辑它们,所以现在这个关卡编辑器更像是“载入器”。

不要失望——本教程的第二部分将告诉你怎么把它变成真正的“关卡编辑器”!(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

Create Your Own Level Editor: Part 1/3

By Barbara Reichart

Skill:

If you’re new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

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?

I spy with my little eye…some data to be saved!

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:

Solution Inside    Show

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:

Try to calculate the level position to the right. Screen width is 320, and the height is 480.

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!

Solution Inside    Show

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.

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.

Always make sure your pineapples know who they are.

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!

Solution Inside: XML file representing level from tutorial

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?

Solution Inside    Show

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:

Full file path to helloDirectory.xml in documents directory

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.

Solution Inside    Show

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

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!

Solution Inside    Show

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:

Pineapples falling down like a boss

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

Solution Inside    Show

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!(source:raywenderlich)


上一篇:

下一篇: