当前位置:首页 >教程首页 > 游戏设计 > 3D模型大师班 >分享开发者自制游戏关卡编辑器的教程(1)

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

发布时间:2018-11-17 19:57:24
作者:Barbara Reichart
 
在本教程中,你将学习如何制作山寨版《切绳子》的关卡编辑器。
 
使用关卡编辑器可以新关卡制作变得更简单。你要做的就是拖动和在你希望的地方放置绳子和菠萝。
 
这个关卡编辑器的优点在于,它是内置于游戏的,所以玩家可以直接在自己的设备上制作自己的关卡。
 
关卡编辑器不仅对于终端玩家来说是非常有趣的,对于游戏开发者,使用关卡编辑器制作关卡也比手动编码来得方便和迅速。
 
一个额外的好处是,你还可以用关卡编辑器来检验你的游戏概念,这对于像《切绳子》这种物理游戏来说尤其重要,因为有时候很难预测物理引擎的行为,但却容易在真实环境下测试那些行为。
 
在游戏中加入关卡编辑器是增加游戏寿命和实用性的好办法,因为玩家可以用它制作自己的关卡——和甚至把自己的成果分享给其他游戏粉丝。
 
开始
 
你将使用该游戏的更新版本作为起始项目。如果你还没有新版本,那就去下载一个并在Xcode中打开。
 
起始项目的代码几乎与原来的教程一样,最大的区别就是,这个项目现在支持Objective-C ARC,而原来的项目不支持。我们也已经把XML解析器添加到原来的项目中,在你制作你的关卡编辑器时你会使用到。
 
注:起始项目没有经过调整,不适合iPhone 5的4英寸屏幕。所以当你在模拟器上运行时,确保使用iPhone的3.5英寸模拟器,而不是4英寸模拟器!
 
选择保存关卡数据的文件格式
 
制作关卡的第一步是决定保存关卡数据的文件格式。保存信息的格式有很多种,但要考虑的最重要的要求如下:
 
1、简单的储存格式
 
2、平台独立
 
3、机器可读性和人可读性——便于文件排错!
 
在这个项目中,你将使用XML来保存你的关卡;它满足以上所有要求,许多读者可能已经使用过XML了。
 
接着,你必须考虑你要储存什么信息。考虑以下截图,你可以推断出需要储存的信息是什么吗?
 

Screen-Shot
 
给你一点提示吧——想一想对象的属性,除位置之外。
 
所以你想到了什么?菠萝?绳子?背景?一个关卡中有许多信息——有时候比你的眼睛所看到的更多。
 
以下你的关卡编辑器文件中必须包含的关卡元素:
 
Pineapple Elements:
 
ID:确定各个菠萝并储存菠萝和绳子之间的关系
 
Position:X和Y轴座标
 
Damping:菠萝的活动情况——这是由游戏的物理引擎使用的信息
 
Rope Elements:
 
两个固定点,属性如下:
 
Body:body的ID和固定点
 
Position:可选择的,依附到菠萝时就不需要。在那种情况下,你应该直接使用菠萝的位置
 
Sagginess:绳子悬挂的松紧程度
 
General Elements(你在几乎所有XML文件中都会找到)
 
Heade ,包括XML的版本
 
Level:一个把所有元素捆绑在一起的顶层元素
 
你是不是漏掉了什么?没关系——只是看看难免会错过某些信息。
 
接下来的部分将更详细地介绍你的XML文件要储存的各个元素。
 
计算菠萝的位置
 
一切都是相对的—–甚至菠萝的位置!
 
因为你的编辑器将在视网膜和非视网膜显示屏上运行,所以你应该根据屏幕尺寸来保存所有相对位置,这样你就不用根据象素来计算各个放置位置了。
 
怎么做?相当简单——根据屏幕位置计算对象的位置,按屏幕宽度划分它的X座标,按屏幕高度划分它的Y座标。如下图所示:
 

Slide
 
(请计算一下右图中的关卡座标。屏幕尺寸是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
 
(总是保证你的菠萝知道自己是谁)
 
另外,并非所有菠萝都必须有相同的表现。在本教程中,你可以通过改变菠萝的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
 
(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
 
(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
 
(菠萝像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文件——你其实还不能够编辑它们,所以现在这个关卡编辑器更像是“载入器”。
 
不要失望——本教程的第二部分将告诉你怎么把它变成真正的“关卡编辑器”!
学员作品赏析
  • 2101期学员李思庭作品

    2101期学员李思庭作品

  • 2104期学员林雪茹作品

    2104期学员林雪茹作品

  • 2107期学员赵凌作品

    2107期学员赵凌作品

  • 2107期学员赵燃作品

    2107期学员赵燃作品

  • 2106期学员徐正浩作品

    2106期学员徐正浩作品

  • 2106期学员弓莉作品

    2106期学员弓莉作品

  • 2105期学员白羽新作品

    2105期学员白羽新作品

  • 2107期学员王佳蕊作品

    2107期学员王佳蕊作品

专业问题咨询

你担心的问题,火星帮你解答
×

确定