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

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

发布时间:2018-11-17 19:57:24
作者:Barbara Reichart
 
这是关于如何创造《Cut the Verlet》游戏的关卡编辑器教程系列文章的第2部分。(点击此处阅读本文第1部分
 
在之前的教程中,你设计了关卡文件的XML存储机制,并执行了一些模型类别去控制绳索和菠萝的数据,最后还创造了关卡编辑器的加载文件功能。
 
这时候,你的项目仍只是一个关卡“载入程序”,与关卡“编辑器”还差得很远。(注:手动编辑XML文件不能算是关卡编辑器!)
 
可以肯定的是你将不会再手动编辑这些关卡文件。在这系列教程的第二部分,你将执行关卡编辑器的编辑功能的某些部分。你将添加弹出菜单,在屏幕上动态地放置并调整目标对象的尺寸等等。
 
如果你未拥有之前的内容,你可以下载在第一部分教程中所创造的样本项目。
 
开始
 
所以关卡编辑器应该包含哪些功能?至少它必须能在关卡中创造,重置并删除对象。同时它还需要能够有效利用屏幕的基板面。并且和往常一样,你的应用功能必须尽可能直观(让用户能够一目了然)。
 
有效设置了这些基本要求后,你便可以开始设计编辑器了。
 
首先,用户需要有一种方法去打开编辑器。最简单的方法便是使用菜单。屏幕最下方便是设置菜单的最佳位置,并且不能包含任何动态游戏元素,如下:
 

iOS-Simulator
 
其次,用户必须能够在关卡上添加绳子和菠萝。这便意味着你需要添加一种机制让他们创造这些全新的对象。
 
再次,最简单的方法仍是使用菜单去呈现这些功能。
 
而因为你在屏幕最下方已经拥有一个菜单了,所以你可以快速运行基板面。
 
该做什么?
 
比起拥有一个始终呈现的菜单,你最好设置一个轻敲屏幕后出现的弹出菜单。用户可以在这个弹出菜单上选择添加菠萝或绳索。然后在弹出菜单当前的位置上便会自动生成对象。
 
对于用户来说移动对象非常重要—-用户在触屏上与它们进行互动是非常容易的事。对于这一功能而言,拖放是一种逻辑选择。
 
最后但同样重要的是,用户必须能够删除对象。在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
 
使用菜单来回切换
 
在屏幕上绘制绳索
 
现在你需要执行代码在屏幕上绘制关卡元素。菠萝较为简单。但是绳索却优点困难。
 
你可以先处理较为复杂的问题,所以让我们先从绘制绳索开始!
 
绳索可以拥有各种长度和方向。最简单的方法便是使用一个精灵和比例,然后相应地旋转。但是这么做所创造出的绳索可能会很丑,如下:
 

ScaledRope
 
朝任何方向旋转并拉升绳索精灵看起来很丑。
 
你喜欢绳索的规格能够始终保持一样。最简单的方法便是使用被系住的较小绳段图像去创造任何理想长度的绳索。
 
为了整合这一绘制代码,我们需要在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
 
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
 
为什么不能看到新添加的内容?
 
虽然你已经执行了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
 
检测用户输入:碰触,移动和长按
 
看到屏幕上的内容已经很棒了,但是你还需要一些其它行动!现在你不能只是做一些关卡编辑。你需要让关卡编辑器去处理用户输入。
 
你需要在编辑器上处理的一些用户互动只是一些场景的碰触,如拖放和长按。
 
首先,在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
 
添加弹出编辑器菜单
 
看到所有的这些行动真的很棒。但是单凭记录信息并不能创造一个编辑器!现在你需要让用户能够在屏幕上与对象进行互动。
 
你需要解决的第一个问题便是如何添加新对象。你已经决定了需要保守设置屏幕空间。这也是为何你不能在屏幕上添加另一个菜单去添加新道具,相反地,编辑器能够打开一个弹出菜单,让用户选择是添加绳索还是菠萝。
 
当玩家轻敲菜单时你希望弹出菜单能够出现让他们再次做出选择。如下:
 

popupmenu-default
 
使用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
 
首先你需要拥有背景图像。背景图像包含了气泡(包含菜单)和箭头(指向菜单)。菜单包含了两个菜单项:一个是关于菠萝,另一个是关于绳索。
 
菜单的位置
 
在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
 
你是怎么想的?你是否注意到当你轻拍屏幕上方或边缘时菜单的呈现有点奇怪?
 
当你的碰触越接近屏幕边缘,菜单便会被截去更多内容。这看起来很奇怪。
 
所以为了解决这一问题,你需要挑战背景图像及其定位点,从而让箭头的尖端能够直接指向轻拍屏幕的位置,而菜单也能够始终完整地呈现出来。
 
执行动态弹出菜单定位
 
比起使用复杂算式去计算弹出菜单的方向和位置,你可以将屏幕划分成一些区域。每个区域都拥有有关弹出菜单的特有配置。下图便是一种可行的屏幕划分:
 

Slide
 
你必须根据菜单背景图像的大小去确定屏幕的区域划分。为什么?因为如果你是根据弹出菜单的大小进行位置计算,之后你不仅可以切换到不同的弹出图像,同时也可以保留弹出菜单及其定位机制。
 
下图是关于在每个屏幕位置上定位菜单所需要的调整对象:
 

popup-menu-positioning
 
首先你希望背景图像的箭头是指向碰触位置。你可以通过设置箭头的定位点而轻松做到这点。在默认方向上,箭头是指向中西方,也就是定位点为(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
 
你之前添加的代码已经处理了弹出菜单的水平定线,但是垂直方向和相对应的定位点呢?这便是你接下来的工作!
 
不要担心,以下内容是你在独自执行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
 
添加新的游戏对象—-菠萝
 
现在的弹出菜单已经能够有效运行了,你可以使用它在关卡上插入新的游戏对象。这听起来好像很接近最终产品了!
 
让我们从菠萝开始设置,它们的执行比绳索更加简单。
 
你需要仔细思考往关卡中添加新对象需要做些什么:
 
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
 
添加新游戏对象——绳索
 
往关卡中添加新绳索需要考虑更多。如果你只是让用户在任何地方添加绳索的话,那么大多数关卡将是无效的,因为绳索需要连接到两个不同的主体。这些主体可以是两个不同的菠萝,或者一个菠萝和背景。
 
你希望确保用户不会创造出任何无效的关卡,即创造出未能与两个有效主体维系在一起的绳索。
 
做到这点的一种方法便是只接受菠萝作为第一个锚点。然后你可以接受任何其它对象作为第二个锚点(游戏邦注:除了第一个菠萝)。但是你是如何知道用户现在处于哪个步骤,从而让你能够决定是放置第一个锚点还是第二个?
 
为此你可以使用一个状态机器。以下图表是关于该项目中所使用的状态机器:
 

State-Diagram-for-Adding-Rope
 
为了执行这一机器,你需要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
 
这看起来很整洁,但是你还不能真正添加新绳索!再次着眼于状态图表并明确你接下来该做些什么:
 

State-Diagram-for-Adding-Rope
 
你需要察觉到玩家是否碰触了菠萝。为此你需要一个能在屏幕上设定位置的方法,并返回在该位置上包含菠萝的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
 
视觉效果发挥了作用,屏幕在不同颜色状态中循环着,用户可以选择第一个锚点。
 
但是还缺失选择第二个锚点的能力。
 
幸运的是,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
 
如果你轻敲“玩关卡”,你便能够伴随着自己创造的改变在关卡中游戏了!
 
接下来该往哪里去?
 
现在你已经在编辑器上取得了很大的进展。但是你也会注意到自己还不能移动对象或身处它们,并且你所做出的改变并不能持续到下一次的游戏。
 
所以我们将会在第三部分教程中继续阐述这些内容。
学员作品赏析
  • 2101期学员李思庭作品

    2101期学员李思庭作品

  • 2104期学员林雪茹作品

    2104期学员林雪茹作品

  • 2107期学员赵凌作品

    2107期学员赵凌作品

  • 2107期学员赵燃作品

    2107期学员赵燃作品

  • 2106期学员徐正浩作品

    2106期学员徐正浩作品

  • 2106期学员弓莉作品

    2106期学员弓莉作品

  • 2105期学员白羽新作品

    2105期学员白羽新作品

  • 2107期学员王佳蕊作品

    2107期学员王佳蕊作品

专业问题咨询

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

确定