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

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

发布时间:2013-06-25 11:35:21 Tags:,,,,

作者:Barbara Reichart

这是关于如何创造《Cut the Verlet》游戏的关卡编辑器教程系列文章的第2部分。(请点击此处阅读本文第1部分

在之前的教程中,你设计了关卡文件的XML存储机制,并执行了一些模型类别去控制绳索和菠萝的数据,最后还创造了关卡编辑器的加载文件功能。

这时候,你的项目仍只是一个关卡“载入程序”,与关卡“编辑器”还差得很远。(游戏邦注:手动编辑XML文件不能算是关卡编辑器!)

可以肯定的是你将不会再手动编辑这些关卡文件。在这系列教程的第二部分,你将执行关卡编辑器的编辑功能的某些部分。你将添加弹出菜单,在屏幕上动态地放置并调整目标对象的尺寸等等。

如果你未拥有之前的内容,你可以下载在第一部分教程中所创造的样本项目。

开始

所以关卡编辑器应该包含哪些功能?至少它必须能在关卡中创造,重置并删除对象。同时它还需要能够有效利用屏幕的基板面。并且和往常一样,你的应用功能必须尽可能直观(让用户能够一目了然)。

有效设置了这些基本要求后,你便可以开始设计编辑器了。

首先,用户需要有一种方法去打开编辑器。最简单的方法便是使用菜单。屏幕最下方便是设置菜单的最佳位置,并且不能包含任何动态游戏元素,如下:

iOS-Simulator(from raywenderlich)

iOS-Simulator(from raywenderlich)

其次,用户必须能够在关卡上添加绳子和菠萝。这便意味着你需要添加一种机制让他们创造这些全新的对象。

再次,最简单的方法仍是使用菜单去呈现这些功能。

而因为你在屏幕最下方已经拥有一个菜单了,所以你可以快速运行基板面。

该做什么?

比起拥有一个始终呈现的菜单,你最好设置一个轻敲屏幕后出现的弹出菜单。用户可以在这个弹出菜单上选择添加菠萝或绳索。然后在弹出菜单当前的位置上便会自动生成对象。

对于用户来说移动对象非常重要—-用户在触屏上与它们进行互动是非常容易的事。对于这一功能而言,拖放是一种逻辑选择。

最后但同样重要的是,用户必须能够删除对象。在iOS上一个非常常见的方法便是使用长按行动去表示用户想要删除那个被按住的对象。

现在我们便决定了编辑器的设计,你可以开始构建它了!

创造LevelEditor类

在LevelEditor群组中创造一个Objective-C类。将文件命名为LevelEditor,将其设置为超级类CCLayer,并确保将文件的扩展名由.m改为.mm。

.mm文件扩展名告诉编辑器文件使用的是Objective-C++。

为什么你需要在LevelEditor类中使用Objective-C++?

简单地来说,Box2D使用C++。同时,LevelEditor参考了其它依赖于Box2D的游戏类。

让我们如下放置LevelEditor.h内容:

#import “cocos2d.h”
#import “LevelFileHandler.h”
#import “CoordinateHelper.h”

@interface LevelEditor : CCLayer<CCTouchOneByOneDelegate>

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

@end

这添加了CCTouchOneByOneDelegate协议,让新LevelEditor层面能够接收碰触事件。

接下来转换到LevelEditor.mm并用如下代码替换里面的内容:

#import “LevelEditor.h”
#import “LevelFileHandler.h”
#import “PineappleModel.h”
#import “RopeModel.h”
#import “CutTheVerletGameLayer.h”

@interface LevelEditor () {
LevelFileHandler* fileHandler;
CCSprite* background;
CCSpriteBatchNode* pineapplesSpriteSheet;
CCSpriteBatchNode* ropeSpriteSheet;
}

@end

@implementation LevelEditor

@end

上述代码在LevelEditor中添加了所有必要的输入内容,还创造了一些实例变量。fileHandler将储存你现在正编辑的关卡,而背景是一个呈现丛林背景的精灵。对于所有菠萝和绳索还有两个CCSpriteBatchNode。

现在添加sceneWithFileHandler:执行LevelEditor.mm,如下:

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

这一代码与Cocos2D项目模版的场景创造类似。它只创造了一个包含LevelEditor场景的CCScene。

这时候的场景还很空。所以让我们开始往场景中添加编辑器菜单。

添加编辑器菜单

在LevelEditor.mm的任何位置上添加如下代码:

-(void) createMenu {
CGSize winSize = [CCDirector sharedDirector].winSize;

// Place Buttons at bottom of game
CCLabelTTF* saveLabel = [CCLabelTTF labelWithString:@"Save" fontName:@"Marker Felt" fontSize:24];
CCMenuItem* saveItem = [CCMenuItemLabel itemWithLabel:saveLabel target:self selector:@selector(save)];

CCLabelTTF* resetLabel = [CCLabelTTF labelWithString:@"Reset" fontName:@"Marker Felt" fontSize:24];
CCMenuItem* resetItem = [CCMenuItemLabel itemWithLabel:resetLabel target:self selector:@selector(resetLevel)];

CCLabelTTF* playLabel = [CCLabelTTF labelWithString:@"Play Level" fontName:@"Marker Felt" fontSize:24];
CCMenuItem* playLevelItem = [CCMenuItemLabel itemWithLabel:playLabel target:self selector:@selector(playLevel)];

// Create menu with buttons
CCMenu* menu = [CCMenu menuWithItems:saveItem, resetItem, playLevelItem, nil];
[menu alignItemsHorizontallyWithPadding:winSize.width*0.1f];
menu.position = CGPointMake(winSize.width/2, saveItem.contentSize.height/2);
[self addChild:menu z:100];
}

-(void) save {
// TODO: save level
}

-(void) resetLevel {
// TODO: reset to last saved version of currently opened file
}

-(void) playLevel {
[[CCDirector sharedDirector] replaceScene: [HelloWorldLayer sceneWithFileHandler: fileHandler]];
}

在上面的代码中,首先你将获得屏幕尺寸。接下来你创造了3个菜单条款,分别带有“保存”,“重置”和“游戏关卡”三种标签。然后你创造了CCMenu并在此添加了菜单项目。最后你将CCMenu添加到场景中,从而让我们能够看到它。

当你着眼于创造菜单项目的代码时,你将会注意到每个项目拥有一个附加在方法(轻敲项目时便会被调用)上的选择器。这时候只执行了playLevel—-轻敲这一菜单项目将转换到游戏场景中。你将在之后回来执行其它两个方法。

现在你需要一些代码去调用菜单并在屏幕上绘制出背景。

在LevelEditor.mm添加如下代码:

-(id)initWithFileHandler:(LevelFileHandler*)levelFileHandler {
self = [super init];
if (self) {
fileHandler = levelFileHandler;
[self createMenu];

background = [CCSprite spriteWithSpriteFrameName:@"bg.png"];
CGSize winSize = [CCDirector sharedDirector].winSize;
background.position = CGPointMake(winSize.width/2, winSize.height/2);
background.color = kDefaultBackgroundColor;

[self addChild:background];

// Load the sprite sheet into the sprite cache
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@”CutTheVerlet.plist”];
pineapplesSpriteSheet = [CCSpriteBatchNode batchNodeWithFile:@"CutTheVerlet.pvr.ccz"];
[self addChild:pineapplesSpriteSheet];
ropeSpriteSheet = [CCSpriteBatchNode batchNodeWithFile:@"rope_texture.png"];
[self addChild:ropeSpriteSheet];

[self drawLoadedLevel];
}
return self;
}

-(void) drawLoadedLevel {
// TODO: draw pineapples
// TODO: draw ropes
}

上述代码初始化了关卡编辑器的屏幕。然后通过复制LevelFileHandler储存了关卡布局。接下来它调用了方法去绘制菜单。之后它在屏幕中间加载了背景和位置。。

然后代码还初始化了背景,从而让它比最初的背景更暗。这将避免让用户混淆自己是在游戏模式中还是在编辑模式中。

这一绘画代码很棒,但是现在我们还不能让用户切换到关卡编辑器中!

作为一种实践,让我们尝试着创造运行用户切换到关卡编辑器的菜单。提示:你可以通过遵循与之前提到的相同的菜单创造步骤去创造新菜单。

所以试试看吧!

创建并运行你的项目!当游戏正在运行时,碰触菜单切换到编辑器。你将看到丛林背景和底部的菜单。

选择“游戏关卡”选择将带你回到游戏中,如下:

SwitchingWithMenu(from raywenderlich)

SwitchingWithMenu(from raywenderlich)

使用菜单来回切换

在屏幕上绘制绳索

现在你需要执行代码在屏幕上绘制关卡元素。菠萝较为简单。但是绳索却优点困难。

你可以先处理较为复杂的问题,所以让我们先从绘制绳索开始!

绳索可以拥有各种长度和方向。最简单的方法便是使用一个精灵和比例,然后相应地旋转。但是这么做所创造出的绳索可能会很丑,如下:

ScaledRope(from raywenderlich)

ScaledRope(from raywenderlich)

朝任何方向旋转并拉升绳索精灵看起来很丑。

你喜欢绳索的规格能够始终保持一样。最简单的方法便是使用被系住的较小绳段图像去创造任何理想长度的绳索。

为了整合这一绘制代码,我们需要在LevelEditor组中创造一个全新的Objective-C类。将其命名为RopeSprite并将其设置为子类NSObject。

切换到RopeSprite.h并用如下代码替换里面的内容:

#import “cocos2d.h”
#import “Constants.h”
#import “RopeModel.h”

@interface RopeSprite : NSObject

@property (readonly, getter = getID) int id;

-(id)initWithParent:(CCNode*)parent andRopeModel:(RopeModel*)aRopeModel;

-(int)getID;
-(CGPoint)getAnchorA;
-(CGPoint)getAnchorB;
-(int)getBodyAID;
-(int)getBodyBID;

-(void)setAnchorA:(CGPoint)anchorA;
-(void)setAnchorB:(CGPoint)anchorB;

@end

这定义了一些必要的方法和属性。大多数方法都是简单的getter和setter。

你可能会问:“为什么我们不能使用属性?”属性是执行getter和setter的简单方法;但是在这种情况下你会想要添加定制代码到setter上从而让它能够在属性改变时重新绘制绳索。

注:你可能已经使用了属性并在执行中覆盖了getter和setter。但是在这个教程中的getter和setter方法能够更清楚地解释每个执行步骤。

切换到RopeSprite.m并用以下代码替换其中的内容:

#import “RopeSprite.h”
#import “CoordinateHelper.h”

@interface RopeSprite () {
CCSprite* ropeSprite;
RopeModel* ropeModel;
}

@end

@implementation RopeSprite

@end

上述代码在RopeSprite上添加了一些私有变量。要求使用CCSprite去绘制绳索。此外,你需要RopeModel,它将提供给你所有有关绳索的放置信息。

现在在RopeSprite.m上添加以下代码:

-(id)initWithParent:(CCNode*)parent andRopeModel:(RopeModel*)aRopeModel {
self = [super init];
if (self) {
ropeModel = aRopeModel;

ropeSprite = [CCSprite spriteWithFile:@"rope_texture.png"];
ccTexParams params = {GL_LINEAR,GL_LINEAR,GL_REPEAT,GL_CLAMP_TO_EDGE};
[ropeSprite.texture setTexParameters:&params];

[self updateRope];
[parent addChild:ropeSprite];
}
return self;
}

上述方法包含了两个参数。父参数是关于绘制绳索的节点。这一参数后遵循着一个模式,即包含了绘制绳索所需要的所有信息。随后该代码还在一个实例变量中储存了绳索模式。

接下来,代码从文件“rope_texture.png”中创造了一个CCSprite。该图像文件只包含绳索的一小段。随后你可以通过重复这一精灵而绘制出完整的绳索。

你可以多次绘制同样的精灵去完成同样的内容,但这需要更多的代码。并且这样能够帮助你更好地设置绳索纹理去处理绘制任务。

OpenGL中的问题具有一些你可能不熟悉的参数。ccTexParams结构包含以下领域:

OpenGL-Texture-Parameters(from raywenderlich)

OpenGL-Texture-Parameters(from raywenderlich)

OpenGL纹理参数

当界面的纹理小于纹理本身时,你便可以使用minFilter,但是当纹理小于界面时,你则需要使用magFilter。

关卡编辑器会使用GL_LINEAR去处理minFilter和magFilter。GL_LINEAR将传回四个纹理元素(最靠近带有纹理的像素中心)的加权平均数。换句话说,基于这一设置OpenGl我们可以使用线性插值去估算像素值。

wrapS和wrapT参数让你能够在s和t坐标设置纹理的包装行为。

注:如果你是直接面对OpenGL,你便会好奇s和t代表什么。在OpenGL中,x,y和z坐标是用于定义3D空间中对象的位置。而如果再次使用x和y轴去命名纹理坐标的话便会让人感到混淆,所以便重新选择了s和t字母表示。

你希望绳索能够沿着s轴不断重复,这时候便可以使用GL_REPEAT 值。沿着t轴,你希望它可以不缩放地进行呈现。对此你便可以使用GL_CLAMP_TO_EDGE 值。

现在你拥有带有纹理的CCSprite,它能够沿着一条轴线不断重复并在其它轴上保持不变。多么整洁!

而现在你唯一需要做的便是在屏幕上适当地呈现绳索去更新其长度和旋转。这一切都发生在updateRope中,你将在下方执行。

如下在RopeSprite.m中添加updateRope执行:

-(void)updateRope {
CGPoint anchorA = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
CGPoint anchorB = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
float distance = ccpDistance(anchorA, anchorB);
CGPoint stickVector = ccpSub(ccp(anchorA.x,anchorA.y),ccp(anchorB.x,anchorB.y));
float angle = ccpToAngle(stickVector);
ropeSprite.textureRect = CGRectMake(0, 0, distance, ropeSprite.texture.pixelsHigh);
ropeSprite.position = ccpMidpoint(anchorA, anchorB);
ropeSprite.rotation = -1 * CC_RADIANS_TO_DEGREES(angle);
}

上述代码使用了ccpDistance去估算两个定位点间的距离,这也等于是绳索的长度。然后使用ccpToAngle去估算绳索的旋转角度,这将得出一个矢量并将其转换到角度中(弧度)。

然后,代码使用上述估算的绳索长度而改变了绳索的纹理。并更新了绳索的位置。你需要记住另外一个定位点是在ropeSprite的中间,所以它的位置是在两个定位点之间。

最后,代码设置了绳索精灵的角度。因为现在的角度是弧度,所以你需要使用CC_RADIANS_TO_DEGREES将其变成角度符号。

这便是关于你如何在未使用纹理的前提下绘制出一个任意长度的绳索。尽管比起简单的伸缩这要求更多的代码,但是却看起来更棒。作为额外的奖励,你可能已经借此掌握了OpenGL,并能够将其用于其它项目中了!

现在你便完成了RopeSprite类的设置。只剩下添加getter和setter调用了。

所以你需要在RopeSprite.m上添加如下代码:

-(void)setAnchorA:(CGPoint)theAnchorA {
ropeModel.anchorA = [CoordinateHelper screenPositionToLevelPosition:theAnchorA];
[self updateRope];
}

-(void)setAnchorB:(CGPoint)theAnchorB {
ropeModel.anchorB = [CoordinateHelper screenPositionToLevelPosition:theAnchorB];
[self updateRope];
}

-(int)getID {
return ropeModel.id;
}

-(CGPoint)getAnchorA {
return [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
}

-(CGPoint)getAnchorB {
return [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
}

-(int)getBodyAID {
return ropeModel.bodyAID;
}

-(int)getBodyBID {
return ropeModel.bodyBID;
}

当其中一个绳索的定位点发生改变,getter和setter只是简单的调用updateRope便可。这将重新绘制绳索去反应改变。

创建并运行你的应用!等等,现在你看到的仍像之前的屏幕那样空旷,不是吗?

iOS-Simulator(from raywenderlich)

iOS-Simulator(from raywenderlich)

为什么不能看到新添加的内容?

虽然你已经执行了RopeSprite类,但是你并未使用它在屏幕上绘制任何内容。

在屏幕上绘制游戏对象

在将这么多内容整合到绘制方法后,你便已经能够开始绘制关卡了。

你需要将一个新变量添加到LevelEditor.mm中,这是储存了所有关卡中的绳索精灵的数组。

首先在LevelEditor.mm中添加如下输入内容:

#import “RopeSprite.h”

现在在LevelEditor.mm最上方的类扩展中(@interface组块)添加新绳索数组的声明:

NSMutableArray* ropes;

好了,现在你便能够在屏幕上绘制当前的关卡了。是时候整合所有内容并检测结果了!

在LevelEditor.mm的drawLoadedlevel中添加如下代码,并替换现有的TODO行:

// Draw pineapple
for (PineappleModel* pineapple in fileHandler.pineapples) {
[self createPineappleSpriteFromModel:pineapple];
}
// Draw ropes
ropes = [NSMutableArray arrayWithCapacity:5];
for (RopeModel* ropeModel in fileHandler.ropes) {
[self createRopeSpriteFromModel:ropeModel];
}

上述代码只迭代了fileHandler中储存的所有菠萝和绳索。而面对每个模式,你需要在之后创造一个视觉再现。

现在通过添加方法到LevelEditor.mm而执行方法去创造菠萝精灵:

-(void)createPineappleSpriteFromModel:(PineappleModel*) pineappleModel {
CCSprite* pineappleSprite = [CCSprite spriteWithSpriteFrameName:@"pineapple.png"];
pineappleSprite.tag = pineappleModel.id;
CGPoint position = [CoordinateHelper levelPositionToScreenPosition:pineappleModel.position];
pineappleSprite.position = position;
[pineapplesSpriteSheet addChild:pineappleSprite];
}

上述方法创造了一个包含菠萝图像的精灵。然后从pineappleModel变量中检索了菠萝的ID和位置,并将其相应地分配到pineappleSprite中。最后,它添加了菠萝精灵到pineapplesSpriteSheet中。

创造绳索精灵的方法也是遵循着这一逻辑。

在LevelEditor.mm下方添加如下代码:

-(void)createRopeSpriteFromModel:(RopeModel*)ropeModel {
CGPoint anchorA;
if (ropeModel.bodyAID == -1) {
anchorA = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
} else {
PineappleModel* pineappleWithID = [fileHandler getPineappleWithID:ropeModel.bodyAID];
anchorA = [CoordinateHelper levelPositionToScreenPosition:pineappleWithID.position];
}

CGPoint anchorB;
if (ropeModel.bodyBID == -1) {
anchorB = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
} else {
PineappleModel* pineappleWithID = [fileHandler getPineappleWithID:ropeModel.bodyBID];
anchorB = [CoordinateHelper levelPositionToScreenPosition:pineappleWithID.position];
}

RopeSprite* ropeSprite = [[RopeSprite alloc] initWithParent:ropeSpriteSheet andRopeModel:ropeModel];
[ropes addObject:ropeSprite];
}

该方法首先明确了绳索的定位点。如果bodyID是-1的话,它便会将anchorPosition值储存在ropeModel中。否则它会根据特定的bodyID去使用菠萝的位置。然后它使用了信息去创造一个RopeSprite实例并将其添加到ropes数组中。

创建并运行你的游戏,然后切换到关卡编辑器中。现在你便能够看到屏幕上自己所创造的图像了,如下截图所示:

Screen-Shot(from raywenderlich)

Screen-Shot(from raywenderlich)

检测用户输入:碰触,移动和长按

看到屏幕上的内容已经很棒了,但是你还需要一些其它行动!现在你不能只是做一些关卡编辑。你需要让关卡编辑器去处理用户输入。

你需要在编辑器上处理的一些用户互动只是一些场景的碰触,如拖放和长按。

首先,在LevelEditor.mm上添加一个实例变量去储存能够识别长按的手势识别器:

UILongPressGestureRecognizer* longPressRecognizer;

现在在LevelEditor.mm上添加如下代码:

-(void)onEnter {
[super onEnter];
[[CCDirector sharedDirector].touchDispatcher addTargetedDelegate:self priority:0 swallowsTouches:NO];
longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
UIView* openGLView = [CCDirector sharedDirector].view;
[openGLView addGestureRecognizer: longPressRecognizer];
}

当视图切换到LevelEditor层面时,onEnter便会被调用。在此你记录下LevelEditor实例作为碰触处理者,从而它将收到像ccTouchBegan和ccTouchEnded等输入方法的调用。同时创造longPressRecognizer并将其添加到openGLView作为一个手势识别器。

因为关卡编辑器是代表碰触事件,所以你需要添加能够在之后处理碰触输入的相关代表方法。

在LevelEditor.mm中添加如下代码:

-(void)longPress:(UILongPressGestureRecognizer*)longPressGestureRecognizer {
if (longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) {
NSLog(@”longpress began”);
}
}

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@”touch began”);
return YES;
}

-(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@”touch moved”);
}

-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@”touch ended”);
}

longPress首先检查了手势识别器当前的状态。手势识别器可以拥有其中一个不同的状态。但是你只有知道长按何时开始,所以你只需要处理UIGestureRecognizerStateBegan状态。这时候,这包含了一个简单的记录信息。

这时候你还漏掉一件事:在用户离开关卡编辑器后进行清理。

在LevelEditor.mm上添加如下代码:

-(void)onExit {
[super onExit];
[[CCDirector sharedDirector].touchDispatcher removeDelegate:self];
UIView* openGLView = [CCDirector sharedDirector].view;
[openGLView removeGestureRecognizer: longPressRecognizer];
}

上述代码删除了作为碰触调度程序的碰触以及手势识别器。

创建并运行你的项目,再次切换到编辑模式。着着眼于Xcode输出窗口中的记录信息列,这是关于你在轻敲,拖放或长按屏幕所产生的信息:

iOS-Simulator(from raywenderlich)

iOS-Simulator(from raywenderlich)

添加弹出编辑器菜单

看到所有的这些行动真的很棒。但是单凭记录信息并不能创造一个编辑器!现在你需要让用户能够在屏幕上与对象进行互动。

你需要解决的第一个问题便是如何添加新对象。你已经决定了需要保守设置屏幕空间。这也是为何你不能在屏幕上添加另一个菜单去添加新道具,相反地,编辑器能够打开一个弹出菜单,让用户选择是添加绳索还是菠萝。

当玩家轻敲菜单时你希望弹出菜单能够出现让他们再次做出选择。如下:

popupmenu-default(from raywenderlich)

popupmenu-default(from raywenderlich)

使用CCLayer创造一个名为PopupMenu的全新Objective-C类,作为超级类。

现在切换到PopupMenu.h并用如下代码替换其中的内容:

#import “cocos2d.h”

@protocol PopupMenuDelegate

-(void)createPineappleAt:(CGPoint)position;
-(void)createRopeAt:(CGPoint)position;

@end

@interface PopupMenu : CCLayer

@property id<PopupMenuDelegate> delegate;

-(id)initWithParent:(CCNode*)parent;

-(void)setPopupPosition:(CGPoint)position;
-(void)setMenuEnabled:(BOOL)enable;
-(void)setRopeItemEnabled:(BOOL)enabled;

-(BOOL)isEnabled;

@end

上述代码提出了一个新协议。该协议定义了PopupMenu和任何其它类(游戏邦注:会在用户选择菜单中一个条目时得到通知)之间的界面。

该协议定义了两个方法,createPineappleAt:和createRopeAt:,他们将在各自对象实例被创造出来时进行调用。

PopupMenu类定义在PopupMenuDelegate实例中添加了一个参考。这将成为一个具体实例并在用户于菜单中做出选择时被调用。

打开PopupMenu.m并用以下代码替换其中的内容:

#import “PopupMenu.h”
#import “RopeSprite.h”

@interface PopupMenu () {
CCSprite* background;
CCMenu* menu;
CCMenuItem* ropeItem;
CGPoint tapPosition;
BOOL isEnabled;
}

@end

@implementation PopupMenu

@end

与之前一样,这是一个有关输入和私有变量的架构。变量往背景和菜单中添加了参考。此外,这里还有一个关于绳索菜单项的指示器,让你可以改变菜单项的状态。因为你只能在有菠萝可以捆绑的前提下才能创造绳索。

这里有一个tapPosition,它将储存玩家轻拍屏幕打开弹出菜单的位置。这是弹出菜单的箭头将指向的位置。isEnabled指示着玩家是否能够看到现在屏幕上的弹出菜单。

现在在PopupMenu.m上添加如下代码:

-(id)initWithParent:(CCNode*) parent {
self = [super init];
if (self) {
CCSprite* pineappleSprite = [CCSprite spriteWithFile:@"pineappleitem.png"];
CCSprite* pineappleSpriteSelected = [CCSprite spriteWithFile:@"pineappleitem.png"];
pineappleSpriteSelected.color = ccc3(100, 0, 0);
CCMenuItemImage* pineappleItem = [CCMenuItemImage itemWithNormalSprite:pineappleSprite selectedSprite:pineappleSpriteSelected target:self selector:@selector(createPineapple:)];

CCSprite* ropeSprite = [CCSprite spriteWithFile:@"ropeitem.png"];
CCSprite* ropeSpriteSelected = [CCSprite spriteWithFile:@"ropeitem.png"];
CCSprite* ropeSprite3 = [CCSprite spriteWithFile:@"ropeitem.png"];
ropeSpriteSelected.color = ccc3(100, 0, 0);
ropeSprite3.color = ccc3(100, 100, 100);
ropeItem = [CCMenuItemImage itemWithNormalSprite:ropeSprite selectedSprite:ropeSpriteSelected disabledSprite:ropeSprite3 target:self selector:@selector(createRope:)];

menu = [CCMenu menuWithItems: pineappleItem, ropeItem, nil];
background = [CCSprite spriteWithFile:@"menu.png"];
[background addChild:menu z:150];
[self addChild:background];
[parent addChild:self z:1000];
[self setMenuEnabled:NO];
}
return self;
}

这一新方法将CCNode作为参数。这一节点将是弹出菜单的根源。剩下的方法执行相对较直接;它创造了一些CCSprite和一个CCMenu并将其添加到源节点上。这一方法也关闭了菜单的使用,因为它只能在用户提出要求时使用。

下图是创造弹出菜单的组成部分:

structure-popup-menu(from raywenderlich)

structure-popup-menu(from raywenderlich)

首先你需要拥有背景图像。背景图像包含了气泡(包含菜单)和箭头(指向菜单)。菜单包含了两个菜单项:一个是关于菠萝,另一个是关于绳索。

菜单的位置

在PopupMenu.m上添加如下代码去设置菜单的准确位置:

-(void)setPopupPosition:(CGPoint)position {
tapPosition = position;

// load defaultBackground and use its size to determine whether the popup still fits there
CCSprite* defaultBackground = [CCSprite spriteWithFile:@"menu.png"];
CGSize defaultBackgroundSize = defaultBackground.contentSize;
float contentScaleFactor = [CCDirector sharedDirector].contentScaleFactor;
float padding = defaultBackgroundSize.width*0.1f*contentScaleFactor;
[menu alignItemsHorizontallyWithPadding:padding];

CGPoint anchorPoint = CGPointMake(0.5f, 0.0f);
CGPoint menuPosition = CGPointMake(defaultBackgroundSize.width/2, defaultBackgroundSize.height*0.7f);

// TODO: adjust anchorPoint and orientation of menu, to make it fit the screen

background.anchorPoint = anchorPoint;
background.position = position;
background.opacity = menu.opacity;

menu.position = menuPosition;
}

为了有效地为菜单定位,上述代码先加载了菜单的背景精灵然后明确了它的大小。该数值能够用于计算菠萝和绳索菜单项之间该如何填充。

然后方法从CCDirector中请求了contentScaleFactor。contentScaleFactor能够将像素位置换成点位置。

在iOS上,所有坐标都能够作为点。当点的位置在视网膜与非视网膜显示中都是一样的话,这便具有优势。

但是基于Cocos2D,菜单中两个项目间的填充从某种原因上来看仍是鉴于像素。因此你需要使用contentScaleFactor将填充从点转换成像素。

接下来,anchorPoint和menuPosition被设置为默认值。定位点也被设置成箭头(在图像的中下方)的尖端。

菜单位置设置如下:x在菜单背景图像的中间。y需要考虑箭头的设置。所以它的位置是在背景图像三分之一的高度。将菜单放置在背景图像三分之二位置便能够最准确地出现在玩家面前。

到现在为止一切设置似乎都很顺利。除了一些来自Xcode的未生效方法的糟糕指示。

所以现在你需要添加那些遗漏掉的方法。

在PopupMenu.m添加如下方法:

-(BOOL)isEnabled {
return isEnabled;
}

-(void)setMenuEnabled:(BOOL)enable {
for (CCMenuItem* item in menu.children) {
item.isEnabled = enable;
}
isEnabled = enable;
int opacity;
if (enable) {
opacity = 255;
} else {
opacity = 0;
}
background.opacity = opacity;
menu.opacity = opacity;
}

-(void)setRopeItemEnabled:(BOOL)enabled {
ropeItem.isEnabled = enabled;
}

-(void)createPineapple:(id)sender {
[self.delegate createPineappleAt:tapPosition];
}

-(void)createRope:(id)sender {
[self.delegate createRopeAt:tapPosition];
}

上述的方法非常直接。setMenuEnabled:,顾名思义就是让你打开或关闭菜单。该方法将所有菜单项设置为适当的状态,然后调整菜单的不透明度,255意味着完全可见,0则意味着不可见。

setRopeltemEnabled:让绳索菜单项的状态为固定。如果你在当前环境下不能添加绳索的话这一方法便很重要,它能够防止用户创造出无效的关卡。

当你轻敲菠萝或绳索的菜单项时便能够调用最后两个方法。它们都是代表的标志。

是使用运行菜单了。切换到LevelEditor.h并添加如下输入:

#import “PopupMenu.h”

接下来将PopupMenuDelegate添加到@interface行,如下:

@interface LevelEditor : CCLayer<CCTouchOneByOneDelegate, PopupMenuDelegate>

现在你的LevelEditor类正在执行PopupMenuDelegate。这便意味着它能够听从当前弹出菜单的顺序了。

切换到LevelEditor.mm并在最上方的@interface添加弹出菜单的实例变量:

PopupMenu* popupMenu;

现在在LevelEditor.mm中执行弹出菜单代表方法:

-(void) createPineappleAt:(CGPoint) position {
NSLog(@”create pineapple”);
[popupMenu setMenuEnabled:NO];
}

-(void) createRopeAt:(CGPoint) position {
NSLog(@”create rope”);
[popupMenu setMenuEnabled:NO];
}

现在的代表方法只能暂时记录它们被调用的状态,并关闭弹出菜单。

如果你现在想要创建并运行代码,那么弹出菜单便不可能出现,因为不存在任何内容能够打开菜单。

你需要在LevelEditor.mm添加如下代码:

-(void) createPineappleAt:(CGPoint) position {
NSLog(@”create pineapple”);
[popupMenu setMenuEnabled:NO];
}

-(void) createRopeAt:(CGPoint) position {
NSLog(@”create rope”);
[popupMenu setMenuEnabled:NO];
}

上述代码能够切换菜单的状态。它首先明确了弹出菜单是否已经存在。如果菜单并不存在,它便会创造一个新实例并将LevelEditor记为代表。

而如果现在菜单被启动了,你便可以关闭它。如果它被关闭了,你也可以开启它,并将其位置设为碰触位置。如果现在在关卡中并不存在菠萝,你便可以在弹出菜单中关闭绳索的使用。

现在只剩下在玩家轻拍屏幕时调用togglePopupMenu:。你需要改变LevelEditor.mm中的哪些方法?

ccTouchBegan!

将当前代码替换为ccTouchBegan:

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchLocation = [touch locationInView:touch.view];
touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];

[self togglePopupMenu:touchLocation];
return YES;
}

前面两行计算了用户轻拍屏幕的位置。之后该位置用于呈现或隐藏弹出菜单。

创建并运行你的项目,并切换到关卡编辑器。现在你应该能够呈现并隐藏全新创造的弹出菜单了,如下图:

popup-menu-cut-off-smaller(from raywenderlich)

popup-menu-cut-off-smaller(from raywenderlich)

你是怎么想的?你是否注意到当你轻拍屏幕上方或边缘时菜单的呈现有点奇怪?

当你的碰触越接近屏幕边缘,菜单便会被截去更多内容。这看起来很奇怪。

所以为了解决这一问题,你需要挑战背景图像及其定位点,从而让箭头的尖端能够直接指向轻拍屏幕的位置,而菜单也能够始终完整地呈现出来。

执行动态弹出菜单定位

比起使用复杂算式去计算弹出菜单的方向和位置,你可以将屏幕划分成一些区域。每个区域都拥有有关弹出菜单的特有配置。下图便是一种可行的屏幕划分:

Slide(from raywenderlich)

Slide(from raywenderlich)

你必须根据菜单背景图像的大小去确定屏幕的区域划分。为什么?因为如果你是根据弹出菜单的大小进行位置计算,之后你不仅可以切换到不同的弹出图像,同时也可以保留弹出菜单及其定位机制。

下图是关于在每个屏幕位置上定位菜单所需要的调整对象:

popup-menu-positioning(from raywenderlich)

popup-menu-positioning(from raywenderlich)

首先你希望背景图像的箭头是指向碰触位置。你可以通过设置箭头的定位点而轻松做到这点。在默认方向上,箭头是指向中西方,也就是定位点为(0.5,0.0)。

但是当弹出菜单位于右上方时,箭头就需要指向右上方。因此你需要将箭头的定位点调整为(1,0.75)。你同样需要相对应地挑战菜单的位置。

你需要注意到菜单是菜单背景的产物,所以你必须将其设置在背景坐标轴内。同时注意到菜单项的定线有时候需要从水平变成垂直。

也许这听起来会让人感到却步,但是你将发现这是改变绘制代码的直接方法,所以它能够准确地为每个对象定位。

切换到PopupMenu.m并在setPopupPosition:中找到TODO行。

用如下代码换掉TODO行:

// Menu horizontal alignment
CGSize winSize = [CCDirector sharedDirector].winSize;
NSString* horizontalAlignment;
if (position.x < defaultBackgroundSize.width/2) {
// left
horizontalAlignment = @”left”;
anchorPoint.x = 0.0f;
[menu alignItemsVerticallyWithPadding:padding];
menuPosition.x = defaultBackgroundSize.height * 0.7f;
menuPosition.y = defaultBackgroundSize.width * 0.5f;
} else if (winSize.width-position.x < defaultBackgroundSize.width/2) {
// right
horizontalAlignment = @”right”;
anchorPoint.x = 1.0f;
[menu alignItemsVerticallyWithPadding:padding];
menuPosition.x = defaultBackgroundSize.height * 0.3f;
menuPosition.y = defaultBackgroundSize.width * 0.5f;
} else {
// center
horizontalAlignment = @”center”;
[menu alignItemsHorizontallyWithPadding:padding];
}

着眼于如何将屏幕划分为左,中,右三块区域。这是基于defaultBackgroundImage的宽度。如果x左边小于背景图像宽度的一半,那么菜单将突向左边。因此你需要设置水平向右对齐并调整anchorPoint。

你也可以采取同样的方法设置菜单定线为垂直,并在弹出菜单中调整菜单的位置。

右边区域的设置也是如此,即只要沿着x轴进行设置。在其它例子中,水平定线被设定在中间位置,而菜单也是水平对齐。

以下图像是关于每个可能方向的弹出菜单元素的坐标轴和定线:

positioning-left-and-right(from raywenderlich)

positioning-left-and-right(from raywenderlich)

你之前添加的代码已经处理了弹出菜单的水平定线,但是垂直方向和相对应的定位点呢?这便是你接下来的工作!

不要担心,以下内容是你在独自执行setPopupPosition中的代码的相关参考。

你该怎么做?加上垂直定线代码,现在你便拥有弹出菜单的垂直和水平定线,并且你也已经储存了相一致的定位点。现在使用信息去完善弹出菜单的绘制吧。

在setPopupPosition的水平和垂直定线检测代码后的PopupMenu.m添加如下代码:

// Draw the menu
NSString* filename = [NSString stringWithFormat: @"menu-%@-%@.png", verticalAlignment, horizontalAlignment];
CCTexture2D* tex = [[CCTextureCache sharedTextureCache] addImage:filename];
if (!tex) {
tex = [[CCTextureCache sharedTextureCache] addImage:@”menu.png”];
}
[background setTexture:tex];
[background setTextureRect: CGRectMake(0, 0, tex.contentSize.width, tex.contentSize.height)];

第一行创建了一串包含了背景图像(适合当前的方向)文件名的字符。每个方向的文件都是遵循menu-verticalAlignment-horizontalAlignment.png模式进行命名。

或者就是使用默认纹理。

不要忘记背景图像的大小并不都是相同的。这也是你为何需要设置纹理结构去适应新纹理的一大原因。

创建并运行你的应用,改变编辑器模式。在完成这些工作后,现在的此单已经能够根据屏幕调整方向了,如下:

iOS-Simulator(from raywenderlich)

iOS-Simulator(from raywenderlich)

添加新的游戏对象—-菠萝

现在的弹出菜单已经能够有效运行了,你可以使用它在关卡上插入新的游戏对象。这听起来好像很接近最终产品了!

让我们从菠萝开始设置,它们的执行比绳索更加简单。

你需要仔细思考往关卡中添加新对象需要做些什么:

1.创造能够代表游戏对象的模式

生成一个独特的ID

设置所有对象的参数

2.在关卡文件处理程序中添加模式,这是代表关卡数据加载和储存

3.创造代表屏幕上模型的形象

首先你需要一个独特的ID。如果你假设只有一种应用线程能够创造并删除对象,那么生成一个独特ID的相对简单的方法便是为游戏对象数组分类。

当数组被分类后,你便需要在类别列表中迭代。你所遗漏的第一个索引便是第一个未使用过的ID。

打开LevelFileHandler.m并添加如下方法为游戏对象数组分类:

+(void)sortArrayById:(NSMutableArray*)objectArray {
NSSortDescriptor* sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@”id” ascending:YES];
NSArray* sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[objectArray sortUsingDescriptors:sortDescriptors];
}

这看起来就像是简略版的分类算法。因为Objective-C已经执行了一种方便的分类算法,所以你可以无需重新创造了。

你只需要明确分类算法的分类标准便可。NSSortDescriptor便可以帮你做到这点。在这一特殊例子中,你创造了一个非常简单的描述项,即以属性命名的“id”进行升序排序。

然后你在数组中添加了这一分类描述,并将数组传达到sortUsingDescriptors:(真正执行数组分类)。尽管通过数组传达分类描述项看起来很麻烦,但是如果你想要基于更多属性进行分类的话,这一方法便能派上用场。

在LevelFileHanler.m上添加如下方法:

+(NSNumber*)firstUnusedIdInArray:(NSArray*)array {
NSNumber* firstUnusedID = nil;
int lastID = 0;
for (AbstractModel* object in array) {
if (object.id – lastID > 1) {
firstUnusedID = [NSNumber numberWithInt:lastID + 1];
break;
}
lastID++;
}
if (!firstUnusedID) {
firstUnusedID = [NSNumber numberWithInt:lastID + 1];
}
return firstUnusedID;
}

这一方法首先创造了2个变量。firstUnusedID储存了第一个未用过的ID,而lastID则是用于储存代码所考虑的最后一个ID。然后迭代游戏对象数组中所包含的所有模式。

在每次迭代中你都检查了现在游戏对象的ID与最后一个ID(比之前ID更好)间是否存在区别。如果有的话,你就需要找到一个没人使用过的ID,然后储存这个ID的值并退出循环。

你也可能在游戏对象数组中找不到一个未曾使用过的ID。在这种情况下,firstUnusedID将仍为nil。而你需要做的便是将firstUnusedID设置为lastID的值加1。

现在你可以使用来自上述方法的ID为新菠萝生成一个PineappleModel。

在LevelFileHandler.m添加如下代码:

-(PineappleModel*)addPineappleAt:(CGPoint)position {
NSNumber* firstUnusedID = [LevelFileHandler firstUnusedIdInArray:self.pineapples];
PineappleModel* newPineapple = [[PineappleModel alloc] init];
newPineapple.id = [firstUnusedID intValue];
newPineapple.position = position;
[self.pineapples addObject:newPineapple];
return newPineapple;
}

这一代码是在执行之前所讨论的新游戏对象生成的点句。

你可以抓取一个未被使用过的ID,创造一个新的PineappleModel实例,然后分配所有重要的参数,如ID和位置。之后你可以添加一个新创造的菠萝到菠萝列表上。最后回到最新创造的菠萝中。

因为你需要从LevelFileHandler类外部访问上述方法,所以它需要被公开。

在LevelFileHandler.h中添加如下方法:

-(PineappleModel*)addPineappleAt:(CGPoint)position;

现在你可以在用户需要时从关卡编辑器调用addPineappleAt:去创造全新菠萝。

切换到LevelEditor.mm并用下面代码替换现有的虚拟createPineappleAt:执行:

-(void) createPineappleAt:(CGPoint) position {
CGSize winSize = [CCDirector sharedDirector].winSize;
PineappleModel* pineappleModel = [fileHandler addPineappleAt:CGPointMake(position.x/winSize.width, position.y/winSize.height)];
[self createPineappleSpriteFromModel:pineappleModel];
[popupMenu setMenuEnabled:NO];
}

上述代码获得了屏幕的尺寸并基于此去估算关卡的位置,然后通过这些信息而创造了PineappleModel。反之,PineappleModel能够用于创造一个PineappleSprite,并在屏幕上呈现菠萝。最后,因为禁用了弹出菜单,所以你在这里便不会看到它。

恭喜你!现在你已经执行了首次互动,即让用户可以修改关卡了。

创建并运行你的应用,根据你的喜好往关卡里添加菠萝吧!

iOS-Simulator(from raywenderlich)

iOS-Simulator(from raywenderlich)

添加新游戏对象——绳索

往关卡中添加新绳索需要考虑更多。如果你只是让用户在任何地方添加绳索的话,那么大多数关卡将是无效的,因为绳索需要连接到两个不同的主体。这些主体可以是两个不同的菠萝,或者一个菠萝和背景。

你希望确保用户不会创造出任何无效的关卡,即创造出未能与两个有效主体维系在一起的绳索。

做到这点的一种方法便是只接受菠萝作为第一个锚点。然后你可以接受任何其它对象作为第二个锚点(游戏邦注:除了第一个菠萝)。但是你是如何知道用户现在处于哪个步骤,从而让你能够决定是放置第一个锚点还是第二个?

为此你可以使用一个状态机器。以下图表是关于该项目中所使用的状态机器:

State-Diagram-for-Adding-Rope(from raywenderlich)

State-Diagram-for-Adding-Rope(from raywenderlich)

为了执行这一机器,你需要3种状态:

1.kEditMode,在此用户可以移动对象,删除对象,并添加新对象。只要用户选择添加绳索,关卡编辑器便会转换到第二种状态。

2.kRopeAnchorPineappleMode,在此只能选择菠萝。当用户做出选择后,模式将转换到第三种状态。

3.kRopeAnchorAnyMode,在此用户只能在任何菠萝中做出选择,除了第一个或背景上的菠萝。当用户做出选择后,编辑器将转换会第一种模式。

切换到LevelEditor.mm并在输入后添加如下代码:

enum {
kEditMode,
kRopeAnchorPineappleMode,
kRopeAnchorAnyMode
} typedef editorMode;

代码已经为上述状态创造了一个枚举去简化状态机器的执行。

在LevelEditor.mm的@interface组块中添加如下代码:

editorMode mode;
RopeModel* newRope;

在此你添加了一个储存了当前模式的实例变量,并伴随着一个变量将储存新绳索的参考。

尽管在代码中使用状态去组织用户创造出无效的关卡很有用,但是你也应该向用户指明当前的状态。通过从视觉上向用户指示状态转换,他们便能够很明显地看出自己正处于怎样的模式并能够执行怎样的行动。

你可以通过在屏幕上添加颜色效果去指示当前的状态。红色代表项目不可选择,绿色代表项目可被选择。

在LevelEditor.mm中添加如下方法:

-(void)setMode:(editorMode)newMode {
mode = newMode;
switch (mode) {
case kRopeAnchorPineappleMode:
background.color = kDefaultDisabledColor;
for (CCSprite* pineapple in [pineapplesSpriteSheet children]) {
pineapple.color = kDefaultSelectableColor;
}
break;
case kRopeAnchorAnyMode:
background.color = kDefaultSelectableColor;
for (CCSprite* pineapple in [pineapplesSpriteSheet children]) {
if (pineapple.tag == newRope.bodyAID) {
pineapple.color = kDefaultDisabledColor;
} else {
pineapple.color = kDefaultSelectableColor;
}
}
break;
case kEditMode:
default:
background.color = kDefaultBackgroundColor;
for (CCSprite* pineapple in [pineapplesSpriteSheet children]) {
pineapple.color = ccc3(255, 255, 255);
}
break;
}
}

上述代码是如何运行的?首先,你储存了新模式,然后创造了一个switch语句去区分三种不同的状态,在此假设kEditMode声明为默认的。

对于每种状态,你使用了CCSprite颜色属性去设置屏幕上的每个对象的颜色。

在状态kRopeAnchorPineappleMode中,你将背景颜色设置为关闭,并打开所有菠萝。

在状态kRopeAnchorMode中,你改变了颜色所以背景便被激活了,同时还有所有菠萝—-除了带有newRope首个锚点ID的菠萝。

最后,在kEditMode中,将背景颜色以及所有菠萝颜色设为默认颜色。

是时候明确这一代码是否是你想看到的了!

在LevelEditor.mm中找到createRopeAt:,并用如下代码替换当前虚拟的执行内容:

-(void)createRopeAt:(CGPoint)position {
[self setMode:kRopeAnchorPineappleMode];
newRope = [[RopeModel alloc] init];
[popupMenu setMenuEnabled:NO];
}

当用户决定创造一条新的绳索时,你设置模式为kRopeAnchorPineappleMode。这将突出所有菠萝,并告知用户只有这些对象能够作为绳索的第一个锚点。接下来,新绳索将被设置在一个空旷的模式中。最后,因为不再需要弹出菜单,所以你便关闭它。

创建并运行你的项目,在弹出菜单中选择绳索。你将看到背景变成红色而菠萝都变成绿色,如下图:

iOS-Simulator(from raywenderlich)

iOS-Simulator(from raywenderlich)

这看起来很整洁,但是你还不能真正添加新绳索!再次着眼于状态图表并明确你接下来该做些什么:

State-Diagram-for-Adding-Rope(from raywenderlich)

State-Diagram-for-Adding-Rope(from raywenderlich)

你需要察觉到玩家是否碰触了菠萝。为此你需要一个能在屏幕上设定位置的方法,并返回在该位置上包含菠萝的CCSprite。

在LevelEditor.mm上添加如下代码:

-(CCSprite*)pineappleAtPosition:(CGPoint)position {
for (CCSprite* pineapple in [pineapplesSpriteSheet children]) {
if (CGRectContainsPoint(pineapple.boundingBox, position)) {
return pineapple;
}
}
return nil;
}

上述代码在所有菠萝中迭代。对于每个菠萝它使用了CGRectContainsPoint去明确特定位置是否位于菠萝的boundingBox内。如果是的话,将返回合适的菠萝。而如果特定点并未位于任何菠萝的边界区域,那么该方法将回到nil。

现在你需要一个方法去执行首个定位点选择。

在LevelEditor.mm添加如下代码:

-(void)selectFirstAnchor:(CGPoint)touchLocation {
// if user tapped on pineapple set it as anchor of the new rope
CCSprite* tappedPineapple = [self pineappleAtPosition:touchLocation];
if (tappedPineapple) {
[newRope setAnchorA:[CoordinateHelper screenPositionToLevelPosition:tappedPineapple.position]];
newRope.bodyAID = tappedPineapple.tag;
[self setMode:kRopeAnchorAnyMode];
}
}

上述方法将碰触位置作为一个输入。然后循环该位置上的菠萝。如果它找到了一个菠萝,它便会将绳索的锚点设置在菠萝的位置上。这便提高的视觉印象,即绳索能够与菠萝连接在一起。

在设置了锚点后,你在绳索中储存了锚点的ID。最后,方法开始转向下一种状态,即让用户可以选择第二个锚点。

回到LevelEditor.mm的ccTouchBegan并将其改为:

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchLocation = [touch locationInView:touch.view];
touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];

switch (mode) {
case kEditMode:
[self togglePopupMenu:touchLocation];
break;
case kRopeAnchorPineappleMode:
[self selectFirstAnchor:touchLocation];
break;
case kRopeAnchorAnyMode:

break;
}
return YES;
}

上述代码比之前的稍微复杂些。现在,它使用switch语句去区分两种不同的状态。在kEditMode,用户可以自由地切换弹出菜单。而在kRopeAnchorPineappleMode中,用户只能选择菠萝作为第一个锚点,弹出菜单将不再出现。

创建并运行你的应用,打开弹出窗口并选择绳索。接下来,选择一个菠萝与绳索维系在一起。你将看到kEditMode, kRopeAnchorPineappleMode和kRopeAnchorAnyMode之间的转换,就像如下截图上所示:

Visualization-of-Different-Modes(from raywenderlich)

Visualization-of-Different-Modes(from raywenderlich)

视觉效果发挥了作用,屏幕在不同颜色状态中循环着,用户可以选择第一个锚点。

但是还缺失选择第二个锚点的能力。

幸运的是,selectSecondAnchor:的执行与selectFirstAnchor非常类似。

所以你只要在LevelEditor.mm添加如下代码便可:

-(void)selectSecondAnchor:(CGPoint)touchLocation {
// set second end of rope, can be either background or other pinapple, but not same pinapple as first one
CCSprite* tappedPineapple = [self pineappleAtPosition:touchLocation];
if (tappedPineapple && tappedPineapple.tag != newRope.bodyAID) {
[newRope setAnchorB:[CoordinateHelper screenPositionToLevelPosition:tappedPineapple.position]];
newRope.bodyBID = tappedPineapple.tag;
}
if (!tappedPineapple) {
[newRope setAnchorB:[CoordinateHelper screenPositionToLevelPosition:touchLocation]];
newRope.bodyBID = -1;
}
[self createRopeSpriteFromModel:newRope];
[fileHandler addRopeFromModel: newRope];
[self setMode:kEditMode];
}

上述代码处理了第二个锚点的选择。当用户选择了第二个菠萝时,所有的一切与在kRopeAnchorPineappleMode中非常相似。唯一的不同则是你需要检查菠萝的标签并确保它与第一个锚点是不同的。

以防用户轻拍背景的某些地方,你使用了碰触位置作为锚点的位置并将ID设置为-1。在这两种情况下,你通过模式创造了绳索并切换回kEditMode。

你快要完成了,但首先你需要将所有内容整合在一起并测试编辑器!

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

-(void)addRopeFromModel:(RopeModel*)newRope {
[LevelFileHandler sortArrayById: self.ropes];
if (!newRope.id) {
NSNumber* firstUnusedID = [LevelFileHandler firstUnusedIdInArray:self.ropes];
newRope.id = firstUnusedID.intValue;
}
[self.ropes addObject:newRope];
}

这一方法划分了绳索的数组。之后,它检查了绳索是否已经具有一个ID。如果没有,它会对此提出要求并为新绳索设置ID。最后,新绳索便被添加到ropes数组中了。

现在在LevelFileHandler.h添加方法原型,从而公开方法:

-(void)addRopeFromModel:(RopeModel*)newRope;

但是不要忘记最重要的一步—-在碰触屏幕时设第二个定位点。如果你忽略了这点,那么在用户尝试着连接绳索最末端时便什么都不会发生!

直接在case kRopeAnchorAnyMode:行之后将如下代码添加到ccTouchBegan:上:

[self selectSecondAnchor:touchLocation];

创建并运行你的应用,并对编辑器进行再一次测试。添加一些新的菠萝和绳索以确保该部分能像设计那样有效运行,如下图所示:

iOS-Simulator(from raywenderlich)

iOS-Simulator(from raywenderlich)

如果你轻敲“玩关卡”,你便能够伴随着自己创造的改变在关卡中游戏了!

接下来该往哪里去?

现在你已经在编辑器上取得了很大的进展。但是你也会注意到自己还不能移动对象或身处它们,并且你所做出的改变并不能持续到下一次的游戏。

所以我们将会在第三部分教程中继续阐述这些内容。

本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

Create Your Own Level Editor: Part 2/3

By Barbara Reichart

This is the second part of a tutorial series that shows you how to make a level editor for the Cut the Verlet game that was previously covered on this site.

In the previous tutorial, you designed the XML storage mechanism of your level files, implemented some model classes to hold your rope and pineapple data, and finished off by creating the load file functionality of your level editor.

At this point, your project is still just a level “loader”, as opposed to a level “editor”. (No, editing XML files by hand does not count as a level editor — just in case you were wondering!)

Rest assured that you’ll not have to manually edit those level files any longer. In this second part of the tutorial, you will implement a portion of the editing capabilities of your level editor. You’ll work through adding popup menus, dynamically positioning and sizing your objects on screen, and much more.

If you don’t have it already, download a copy of the sample project where you left it off in the previous tutorial.

Getting Started

So what functionality should a level editor contain? At the very least it should be able to create, reposition and delete objects in the level. As well, it should make the best use of the screen real estate available. And as always, the functions in your app should be as intuitive as possible for the user.

With those basic requirements in place, it’s time to set out the design of the editor.

First, the user needs a way to launch the editor. The easiest way to do this is by using a menu. The bottom of the screen is probably the best place for the menu, as it does not contain any dynamic game elements, as seen below:

Second, the user must be able to add ropes and pineapples to the level. This means you need a mechanism that will allow them to create those new objects. Again, the easiest way to expose this functionality would be through a menu.

However, you already have a menu to be added to the bottom of your screen, and you’re rapidly running out of real estate.

What to do?

Instead of having a menu that is permanently visible, it would be better to have a popup menu that appears by tapping the screen. From this popup menu, the user can choose between adding a pineapple or a rope. The object will then be created automatically at the current position of the popup menu.

Moving objects will also be important to the user — and the user interaction for this on a touch screen is almost a no-brainer. Drag and drop is the logical choice for this functionality.

Last but not least, the user needs to be able to delete objects. One very common way to do this in iOS is to use a long press action to indicate that the user wants to remove the item being pressed on.

Now that the design of the editor has been decided, you can get busy constructing it!

Creating The LevelEditor Class

Create an Objective-C class under the LevelEditor group. Name the file LevelEditor, make its super class CCLayer, and be sure to change the file extension of the file from .m to .mm.

The .mm file extension tells the compiler that the file uses Objective-C++.

Why do you need to use Objective-C++ in the LevelEditor class?

Simply put, Box2D uses C++. As well, LevelEditor references other game classes which rely on Box2D.

To start, replace the contents of LevelEditor.h with the following:

This adds the CCTouchOneByOneDelegate protocol to the declaration, which allows the new LevelEditor layer to receive touch events.

Next, switch to LevelEditor.mm and replace its contents with the following:

The above code adds all necessary imports to LevelEditor and then creates some instance variables. fileHandler will store the level you are currently editing, while background is simply a sprite that displays the jungle background. There are also two CCSpriteBatchNodes for all pineapples and ropes.

Now add the sceneWithFileHandler: implementation to LevelEditor.mm, as follows:

This code is pretty similar to the scene creation code in the Cocos2D project template. It simply creates a CCScene that contains the LevelEditor scene.

At the moment this scene is empty. That won’t be terribly interesting to look at! :] Start by adding the editor menu to your scene.

Adding the Editor Menu

Add the following code anywhere in LevelEditor.mm:

In the code above, first you get the screen size. Next, you create three menu items with the labels “Save”, “Reset” and “Play Level”. You then create a CCMenu and add the menu items to it. Finally, you add the CCMenu to the scene so that it becomes visible.

When you look at the code that creates the menu items, you’ll note that each item has a selector attached to a method that is called when the item is tapped. For the moment only playLevel is implemented — tapping this menu item simply switches to the game scene. You’ll come back to implement the other two methods later.

Now you’ll need some code to call the menu and draw the background on the screen.

Add the following code to LevelEditor.mm:

The above code initializes the screen of the level editor. It then stores the level layout by making a copy of LevelFileHandler. Next, it calls the method that draws the menu. After that, it loads the background and positions it in the middle of the screen.

Next, the code tints the background so that it is darker than the original background. This will help the user avoid any confusion as to whether they are in game mode or in edit mode.

This drawing code looks pretty nice, but right now there’s no way for the user to switch to the level editor!

As an exercise, try to create the menu that will allow the user to switch to the level editor. Protip: you can create the new menu by following the same menu creation steps as above.

So go ahead, give it a shot!

Build and run your project! Once the game is running, hit the menu to switch to the editor. You should see the jungle background and a menu at the bottom.

Selecting the “Play Level” option should bring you back to the game, as seen below:

Switching back and forth using your menus

Drawing Ropes On-screen

You now need to implement the code that draws the level elements on the screen. For the pineapple, this is relatively easy. However, the ropes are slightly more complex.

You might as well tackle the harder problem first — start with the ropes! :]

The ropes can have variable length and orientation. The easiest way to draw them would be to use one sprite and scale and rotate it accordingly. However, this can end up looking rather ugly, as shown in the example below:

Rotating and stretching the rope sprite to any orientation: Looking ugly.

What you want is to have the ropes sized consistently. The easiest way to do this is to use a small rope segment graphic that is chained or tiled so as to create a rope of any desired length.

In order to encapsulate this drawing code, create a new Objective-C class in the LevelEditor group. Name the class RopeSprite and make it a sub-class of NSObject.

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

This defines some essential methods and properties. Most of the methods are simple getters and setters.

You might ask, “Why aren’t we simply using properties?”. Properties are easy ways to implement getters and setters; however, in this case you want to add custom code to the setter so that it redraws the rope when a property is changed.

Note: You could have used properties and then overridden the getter and setter in your implementation. However in this tutorial the getter and setter approach used above does a better job of explaining each implementation step in detail.

Switch to RopeSprite.m and replace its contents with the following code:

The above adds some private variables to RopeSprite. The CCSprite is required to draw the rope. Additionally, you need a RopeModel, which gives you all the placement information for the rope.

Now add the following to RopeSprite.m:

The above method takes two parameters. The parent parameter is the reference to the node on which the rope will be drawn. This parameter is followed by the model, which contains all information required to draw the rope. The code then stores the rope model in an instance variable.

Next, the code creates a CCSprite from the file “rope_texture.png”. This image file contains only one small segment of the rope. The whole rope is then drawn by repeating this sprite over and over.

You could accomplish the same thing by drawing the same sprite several times, but this would require a large amount of code. Instead, it’s much more efficient to set up a rope texture to handle the drawing.

Textures in OpenGL have a few parameters that you might not be familiar with. The ccTexParams struct has the following fields:

OpenGL texture parameters

minFilter is used when the surface the texture is drawn upon is smaller than the texture itself, whereas magFilter is used when the texture is smaller than the surface.

The level editor uses GL_LINEAR for both minFilter and magFilter. GL_LINEAR returns the weighted average of the four texture elements that are closest to the center of the pixel being textured. In other words, with this setting OpenGL uses linear interpolation to calculate the values of the pixels.

The wrapS and wrapT parameters let you set the wrapping behavior of the texture in the s and t coordinate.

Note: If you haven’t worked with OpenGL directly, you’re probably wondering what s and t stand for. In OpenGL the x, y, and z coordinate are used to define the position of an object in 3D space. As it would be confusing to reuse the x and y coordinate names for the texture coordinates, they’re simply re-labeled s and t.

You want the rope to repeat itself along the s-axis, which is done using the GL_REPEAT value. Along the t-axis, you want it to display just once without any scaling. For this, you use the GL_CLAMP_TO_EDGE value.

Note: There are several other filter values for texture parameters. You can look them up in the OpenGL Documentation.

You now have a CCSprite with a texture which will repeat along one axis while it stays unchanged along the other axis. That’s pretty neat! :]

The only thing you need to do now in order to display the rope properly on the screen is to update its length and rotation. This magic takes place in updateRope, which you’ll implement below.

Add the updateRope implementation to RopeSprite.m as shown below:

The code above uses ccpDistance to calculate the distance between the two anchor points, which equals the length of the rope. It then calculates the rotation angle of the rope with ccpToAngle, which takes a vector and translates it into an angle in radians.

Next, the code changes the rope texture’s rectangle using the rope length calculated above. Then it updates the rope’s position. Keep in mind that the anchor point is at the middle of the ropeSprite, so its position is exactly in the middle of the two anchor points.

Finally, the code sets the angle of the rope sprite. As the angle is currently in radians, you need to transform it into degree notation with CC_RADIANS_TO_DEGREES.

This is how you draw a rope having arbitrary length without using a texture that is stretched in ugly ways. While it requires more code than simple scaling, it looks far, far better. As an added bonus, you’ve probably learned something along the way about OpenGL that you can use in your other projects!

The RopeSprite class is almost done. All that’s left is to add the getter and setter calls.

Add the following code to RopeSprite.m:

The getters and setters simply call updateRope whenever the position of one of the rope anchors is changed. This will redraw the rope to reflect the changes.

Build and run your app! You should now see…oh, wait. That’s just the same empty screen as before, isn’t it?

Why don’t you see anything new?

You’ve implemented the RopeSprite class — but you’re not using it yet to draw anything on screen! That comes next. :]

Drawing the Game Objects On-screen

After putting so much work into the rope drawing methods, you are nearly ready to draw the level.

There’s a new variable to add to LevelEditor.mm, an array that stores all of the rope sprites in the level.

First add the following import to the top of LevelEditor.mm:

Now add the declaration of the new ropes array to the class extension (the @interface block) at the top of LevelEditor.mm:

Okay, now you have everything you need to draw the current level on the screen. Time to pull everything together and see the results of your hard work!

Add the following lines of code to drawLoadedLevel in LevelEditor.mm, replacing the existing TODO lines:

The above code simply iterates over all pineapples and ropes stored in the fileHandler. For each of the models you then create a visual representation.

Now implement the method that creates the pineapple sprites by adding the following method to LevelEditor.mm:

The above method creates a sprite that contains the pineapple graphic. It then retrieves the ID and position of the pineapple from the pineappleModel variable and assigns them to the pineappleSprite accordingly. Finally, it adds the pineapple sprite to the pineapplesSpriteSheet.

The method for creating the rope sprites follows the same logic.

Add the method below to LevelEditor.mm:

The method first determines the anchor position for the rope. If the bodyID is -1 it takes the anchorPosition value stored in the ropeModel. Otherwise, it uses the position of the pineapple with the given bodyID. Then it creates a RopeSprite instance using the information and adds it to the ropes array.

Build and run your game, and switch to the level editor. You should finally see something on the screen for all your hard work, as demonstrated in the screenshot below:

Sure looks finished, doesn’t it?

Detecting User Inputs: Touch, Movement and Long Press
Seeing things on screen is great and all, but you need some action too! Currently, you can’t actually do any level editing. You need to enable the level editor to handle user input.

The user interactions that you’ll handle in your editor are normal touches, drag & drop and long press.

First, add an instance variable to LevelEditor.mm to store the gesture recognizer that recognizes a long press:

Now add the following code to LevelEditor.mm:

onEnter is called whenever the view switches to the LevelEditor layer. Here, you register the LevelEditor instance as a touch handler so that it will receive calls to input methods like ccTouchBegan and ccTouchEnded. Also create longPressRecognizer and add it to the openGLView as a gesture recognizer.

Since the level editor is the delegate for touch events, you need to add the relevant delegate methods that will later handle touch input.

Add the following code to LevelEditor.mm:

longPress first checks the current state of the gesture recognizer. A gesture recognizer can have one of several different states. However, you only need to know when the long press began, so you just handle the UIGestureRecognizerStateBegan state. For the moment, this consists of a simple log message.

Only one more thing is missing: cleaning up after the user leaves the level editor.

Add the following code to LevelEditor.mm:

The above simply removes the layer as a touch dispatcher and also removes the gesture recognizer.

Build and run your project, and again switch to the editor mode. Take a look at the list of log messages produced in Xcode’s output window when you either tap, drag, or long press on the screen, as shown in the example below:

Adding the Popup Editor Menu

It feels pretty good to see all this in action, doesn’t it? However, log messages alone do not make an editor! Time to allow the user to interact with the objects on the screen.

The first problem to be solved is how to add new objects. You already determined that you need to be conservative with screen space. This is why you won’t have another menu on screen to select new items to be added. Instead, the editor will open a popup menu, which will allow the user to select between adding ropes or pineapples.

When the player taps the screen you want a popup menu to appear that allows the user to select between creating a pineapple or a rope. Here’s how it will look:

The popup menu

Create a new Objective-C class named PopupMenu with CCLayer as the super class.

Now switch to PopupMenu.h and replace its contents with the following:

The above code declares a new protocol. A protocol defines an interface between the PopupMenu and any other classes that want to be notified whenever the user selects an item in the menu.

This protocol defines two methods, createPineappleAt: and createRopeAt:, which will be called when an instance of the respective object is created.

The PopupMenu class definition adds a reference to an instance of PopupMenuDelegate. This will be the concrete instance that will be called when the user does something in your menu.

Open up PopupMenu.m and replace its contents with the following:

As usual, this is simply a skeleton with the relevant imports and private variables. The variables add references to the background and to the menu. Additionally, there’s a pointer to the rope menu item which allows you to change the menu item state. You need this because creating a rope should only be possible if there is at least one pineapple you can tie it to.

Then there’s tapPosition, which will store the screen position where the player tapped to open the popup menu. This is the location where the arrow of the popup menu will point. isEnabled indicates whether the popup menu is currently visible on the screen for the player to tap.

Now add the following code to PopupMenu.m:

This new method takes a CCNode as parameter. This node will be the parent of the popup menu. The rest of the method implementation is relatively straightforward; it creates some CCSprites and a CCMenu and adds them to the parent node. The method also disables the menu since it should only appear and be enabled when requested by the user.

The image below shows the parts that combine to make the popup menu:

Parts of the popup menu.

To start, you have the background image. The background image consists of a bubble (containing the menu) and an arrow (the anchor point for the menu should be set to tip of this arrow). The menu contains two menu items: one for the pineapple, the other for the rope.

Positioning the Menu

Add the following code to PopupMenu.m set the correct position for the menu:

In order to align the menu properly, the above code first loads the background sprite of the menu and then gets its size. This value is used to calculate the padding between the pineapple and rope menu items.

The method then requests the contentScaleFactor from CCDirector. The contentScaleFactor can be used to translate pixel positions to point positions.

On iOS, all coordinates are usually given as points. This has an advantage in that a position in points is the same on both retina and non-retina displays. However, the padding between items in a menu in Cocos2D is for some reason still given in pixels. Therefore, you need to use the contentScaleFactor to translate the padding from points into pixels.

Next, the anchorPoint and menuPosition are set to their default values. The anchor point is set to the tip of the arrow, which is in the bottom center of the graphic.

The menu position is set as follows: the x position is set to the center of the menu’s background graphic. The y position needs to take the arrow into account. It is about as tall as one-third of the background graphic. Placing the menu at about two thirds of the background graphic height will therefore make it appear just in the right spot.

So far, this seems to be going pretty well. There’s only a few nasty warnings about unimplemented methods from Xcode. Good guy, that Xcode. Nice of him to remind you. :]

Keep him happy and add those missing methods now.

Add the following methods to PopupMenu.m:

Most of the above methods are fairly straightforward. setMenuEnabled:, as the name implies, allows you to enable or disable the menu. The method simply sets all the menu items to the appropriate state and then adjusts the opacity of the menu, where 255 means completely visible and 0 means invisible.

setRopeItemEnabled: allows the state of the rope menu item to be toggled. This is necessary as it does not always make sense to add a rope in the current context, and you want to prevent your users from creating invalid levels.

The last two methods are called whenever you tap the menu items for a pineapple or a rope. All they do is to forward the signal to the delegate.

Time to put your menu to work! Go to LevelEditor.h and add the following import:

Next, add PopupMenuDelegate to the @interface line so that it looks like this:

Your LevelEditor class now implements PopupMenuDelegate. This means that it can listen to orders from the popup menu now.

Switch to LevelEditor.mm and add an instance variable for the popup menu to the @interface block at the top:

Now implement the popup menu delegate methods as follows in LevelEditor.mm:

For the moment, the delegate methods will simply log that they were called and then close the popup menu.

If you were to build and run the code now, the popup menu would never even show up, since there’s nothing to enable the menu.

Add the following method to LevelEditor.mm:

The code above, as advertised, toggles the state of the menu. It first determines whether the popup menu already exists. If it doesn’t, it creates a new instance and registers LevelEditor as the delegate.

If the menu is currently enabled, you disable it. If it is disabled, you enable it, and set its position to the touch location. If there are currently no pineapples in the level, you disable the rope item in the popup menu.

All that is left now is to actually call togglePopupMenu: when the player taps the screen. What method in LevelEditor.mm would you need to change for that?

ccTouchBegan:, of course!

Replace the current code for ccTouchBegan: with the following:

The first two lines calculate the location where the user tapped the screen. That position is then used to display or hide the popup menu.

Build and run your project and switch to the level editor. You should now be able to show and hide your newly created popup menu, as shown below:

The current look of your popup menu

So what do you think? Did you notice anything awkward about the presentation of the menu as you tapped around the top or edges of the screen?

Depending on how close you click to the edge, there are a few places where the menu is cut off by the edge of the screen! It looks unpolished, to say the least.

To correct this, you’ll need to adjust the background graphic and its anchor point so that the tip of arrow always points directly at the tap position and the menu box is always fully visible.

Implementing Dynamic Popup Menu Positioning

Instead of using complex algorithms to calculate the orientation and position of the popup, you’ll separate the screen into several areas. Each area will have its own configuration for the popup menu. You can see a possible screen division in the following graphic:

Separation of the screen into different ares where the overlay menu is rotated so that it fits on the screen.

The actual division of the screen into areas should be done based on the size of the menu background graphic in its default orientation. Why? If you base all of the position calculations on the popup size, then you can later switch to a different popup graphic while still retaining the popup menu and its positioning mechanism.

The image below shows the adjustments needed to position the menu in each of the screen locations:

A demonstration of the changes to the popup menu when it is positioned in the top-right corner.

First, you want the arrow of your background image to point to the touched location. You can easily do this by setting the anchor point of the arrow. In its default orientation, the arrow points at the bottom middle, which corresponds to an anchor point of (0.5, 0.0).

However, when the popup menu is in the top-right corner, the arrow needs to point at the top-right corner. Therefore, you need to adjust the anchor point of the arrow to be (1, 0.75). You also need to adjust the position of the menu accordingly.

Make note that the menu is a child of your menu background, so you must always place it within the background coordinates. Also note that the alignment of the menu items sometimes needs to be switched from horizontal to vertical.

It sounds daunting, but you’ll see that it’s a fairly straightforward exercise to change the drawing code so that it positions everything properly.

Switch to PopupMenu.m and find the TODO line in setPopupPosition:.

Replace the TODO line with the following code:

The horizontal alignment of the popup has already been handled in the code you added above, but what about vertical orientation and the corresponding anchor point? That’s your job to figure out! :]

Don’t worry, the spoiler below is there to reference once you’ve attempted to implement the code in setPopupPosition: yourself.

How did you do? With the addition of the vertical alignment code, you now have the vertical and horizontal alignment of the popup menu and you have stored the matching anchor point. Now use that information to perfect the drawing of your popup menu.

Add the following code to PopupMenu.m right after the horizontal and vertical alignment detection code in setPopupPosition::

The first line builds a string containing the filename of the background image that is suitable for the current orientation. Conveniently, the files for each orientation have been named following the pattern menu-verticalAlignment-horizontalAlignment.png.

A texture is then created using the filename of the image. If the texture could be created (i.e. it’s valid), then set it as the new texture for the background sprite. Otherwise, use the default texture.

Don’t forget that the background images aren’t all the same size. This is why you also need to set the texture rect to fit that of the new texture.

Build and run your app, and change to the editor mode. After all this hard work, the menu will now adjust its orientation to always fit the screen, as shown below:

Adding New Game Objects – Pineapples

Now that the popup menu works properly, you can use it to insert new game objects into your level. Sounds like you’re getting close to the end product, doesn’t it? :]

Start with the pineapples, as they are much simpler to implement than the ropes.

Think for a moment about what you’ll need to do to add a new object to your level:

1.Create a model that represents the game object

Generate a unique ID

Set all object parameters

2.Add the model to the level file handler, which is responsible for loading and storing level data

3.Create a visualization that represents the model on screen

First, you need a unique ID. If you make the assumption that there is only one application thread which is responsible for creating and deleting objects, then a relatively simple way to generate a unique ID is to sort the array of game objects.

Once that array is sorted, you iterate over the sorted list. The first index that is missing is also the first unused ID.

Open LevelFileHandler.m and add the following method to sort the game object array:

This might look quite short for a sorting algorithm. That’s because Objective-C already has a handy sorting algorithm implemented! No need to re-invent the wheel. :]

You only need to tell the sorting algorithm the criteria it should use to sort. You do this by using NSSortDescriptors. In this particular case, you create a very simple descriptor that sorts by a property named “id” in ascending order.

You then add this sort descriptor to an array and pass the array on to sortUsingDescriptors: which actually performs the sorting of the array. While passing the sort descriptor via an array might seem cumbersome, it can also come in handy if you ever want to sort the array based on more than one property.

On to the next step! Add the following method to LevelFileHandler.m:

This method first creates two variables. firstUnusedID stores the first unused ID, while lastID is used to store the last ID that the code looked at. You then iterate over all models contained in the array of game objects.

In each iteration you check whether the difference between the ID of the current game object and the last ID is greater than one. If yes, then you have found an ID that is currently not in use, so you store the value of the unused ID and exit the loop.

It is possible that you won’t find an unused ID in the game object array. In this case, firstUnusedID would still be nil. Then all you need to do is set firstUnusedID to the value of lastID plus one.

You can now use the ID returned from the above method to generate a PineappleModel for your new pineapple.

Add the following code to LevelFileHandler.m:

This code is now a straightforward implementation of the bullet points above for generating a new game object that was discussed earlier. Have a look back to the bullet list if you don’t believe it! :]

You grab an unused ID, create a new PineappleModel instance, and assign all the important parameters like ID and position. Then you add the newly created pineapple to the list of pineapples. Finally, you return the newly created pineapple.

Since you’ll need to access the above method from outside the LevelFileHandler class, it needs to be public.

Add the following method prototype to LevelFileHandler.h:

Now you can call addPineappleAt: from the level editor to create new pineapples when the user requests them.

Switch to LevelEditor.mm and replace the existing dummy implementation of createPineappleAt: with the following:

The above code gets the size of screen and uses it to calculate the level position and create a PineappleModel from this information. The PineappleModel in turn is then used to create a PineappleSprite, which presents the pineapple on-screen. Finally, the popup menu is disabled so that it does not show anymore.

Congratulations! You now have implemented the first interaction that lets the user modify the level.

Build and run your app, and go crazy adding as many pineapples as you like to the level!

Adding New Game Objects – Ropes

Adding new ropes to the level requires a little more thought. If you simply let the user add a rope wherever they want, most of the levels will be invalid since a rope needs to be connected to two different bodies. These bodies can either be two different pineapples, or a pineapple and the background.

You want to ensure that a user cannot create any invalid levels by creating ropes that aren’t connected to two valid bodies.

One way to do this is to only accept a pineapple as the first anchor. You can then accept any other object, except the first pineapple, as the second anchor.

But how do you know which step the user is currently on so that you can determine whether you’re placing the first or the second anchor?

You can use a state machine for this. Take a look at the following diagram that shows the state machine to be used in this project:

How to add a rope – presented to you by the state chart ;)
To implement this, you’ll require three states:

1.kEditMode, where the user can move objects, delete objects, and add new objects. As soon as the user choses to add a rope, the level editor switches to the second state.

2.kRopeAnchorPineappleMode, where only pineapples can be selected. When a selection is made, the mode switches to the third state.

3.kRopeAnchorAnyMode, where the user is limited to choosing between any pineapples other than the first one or any position on the background. Once a choice is made, the editor switches back to the first mode.

Switch to LevelEditor.mm and add the following code directly after the imports:

The code above creates an enum for the aforementioned states to simplify your implementation of the state machine.

Add the following code to the @interface block of LevelEditor.mm:

Here, you add an instance variable that stores the current mode, along with a variable to store a reference to the new rope.

Although using states in your code to prevent the creation of invalid levels is useful, you should also indicate the current state to the user. By visually indicating state transitions to the user, it’s readily apparent which mode they are currently working in and what actions they can perform.

You’ll indicate the current state by adding a color effect to the screen. Red will indicate items that cannot be selected, and green for items that can be selected.

Add the following method to LevelEditor.mm:

So how does the code above work? First, you store the new mode, then you create a switch statement to distinguish between the three different states, where the kEditMode state is assumed to be the default.

For each state, you use CCSprite’s color property to color the object on the screen appropriately:

In state kRopeAnchorPineappleMode, you set the background color to disabled, and enable all pineapples.

In state kRopeAnchorMode, you change the coloring so that the background is now active, as well as all pineapples — except the one with newRope‘s first anchor’s ID.

Finally, in kEditMode, set the background color and the color of all pineapples to their default color.

It’s now time to see whether this code does actually what you want!

Find createRopeAt: in LevelEditor.mm and replace the current dummy implementation with the following:

When the user decides to create a new rope, you set the mode to kRopeAnchorPineappleMode. This will highlight all of the pineapples, which indicates to the user that only these objects are available as the first anchor for the rope. Next, the new rope is set up with an empty model. Finally, you close the popup menu since it’s not needed anymore.

Build and run your project, and select the rope in the popup menu. You’ll see the background turn red and the pineapples all turn green, as in the screenshot below:

Well, that looks pretty neat, but you still can’t actually add a new rope yet! Have a look at the state chart again and figure out what you have to do next:

How to add a rope – presented to you by the state chart ;)
Ah — you need to detect if the player touched a pineapple. To do this you’ll need a method which takes a position on the screen as a parameter and returns the CCSprite containing the pineapple at that position.

Add the following code to LevelEditor.mm:

The above code iterates over all of the pineapples. For each pineapple, it uses CGRectContainsPoint to see if the given position is within the pineapple’s boundingBox. If so, the appropriate pineapple is returned. If the given point doesn’t fall within any of the pineapple’s bounding boxes, then the method will return nil.

Now you’ll need a method that will perform the selection of the first anchor point.

Add the following code to LevelEditor.mm:

The above method takes a touch location as input. It then looks for a pineapple at that location. If it finds one, it sets the anchor of the rope to the location of the pineapple. This creates the visual impression that the rope is connected to the pineapple.

After the anchor is set, you store the ID of the anchor in the rope, Finally, the method kicks off the transition to the next state to allow the user to select the second anchor.

Head back to ccTouchBegan: in LevelEditor.mm and change it as follows:

The above code is slightly more complicated than before. Now, it uses a switch statement to distinguish between the different states.

In kEditMode, the user can freely toggle the popup menu. However, in kRopeAnchorPineappleMode the user can only select a pineapple as the first anchor point, and the popup menu will not show up.

Build and run your app, open the popup menu and select the rope. Next, select a pineapple to attach the rope to. You should see the transition between kEditMode, kRopeAnchorPineappleMode, and kRopeAnchorAnyMode, as shown in the successive screenshots below:

Visualizations of the different edit modes for the LevelEditor
The visualization works, and the screen cycles between the different colored states, and the user can select the first anchor point.

But the ability to select the second anchor point still missing!

Luckily, the implementation of selectSecondAnchor: is pretty similar to selectFirstAnchor.

Add the following code to LevelEditor.mm:

The above code handles the selection of the second anchor. When the user selects a second pineapple, everything works nearly the same as in kRopeAnchorPineappleMode. The only difference here is that you check the tag for the pineapple and make sure it is different from the one selected as the first anchor.

In case the user taps somewhere on the background, you use the touch location as the position for the anchor and set the ID to -1. In both cases, you then create the rope from the model and switch back to kEditMode.

You’re nearly done, but first you need to tie everything together and take your editor for a test run!

Switch to LevelFileHandler.m and add the following method:

This method sorts the rope array. After that, it checks whether the rope already has an ID. If not, it requests one and sets this as the ID for the new rope. Finally, the new rope is added to the ropes array.

Now add the method prototype to LevelFileHandler.h so that the method is publicly accessible:

But don’t forget the most crucial step — setting the second anchor point when the screen is touched. Otherwise, nothing will happen when the user tries to anchor the end of the rope!

Add the following line to ccTouchBegan:, directly after the case kRopeAnchorAnyMode: line:

Build and run your app to give the editor another test drive. Add a few new pineapples and ropes to make sure that part works as designed, as shown in the screenshot below:

If you tap “Play Level”, you should even be able to play the level with the changes you made!

Where To Go From Here?

Here is a sample project with all of the code from the above tutorial.

You’ve made some serious progress on your editor so far. You’ll notice that you don’t yet have a way to move objects around or delete them — and your changes aren’t yet persisted between sessions.

That will come in the third portion of this tutorial. Until then, come discuss your progress in the forums, and I’ll see you when it’s time to finish everything off!(source:raywenderlich)


上一篇:

下一篇: