SpriteBuilder 学习笔记五

Chapter 6 Menus & Popovers

现在,你有了一个level和游戏元素可以原则上让游戏可以玩了。再添加更多levels或者游戏元素之前,更应该添加一些menus。

saws应该杀死player,但是这需要一个Game over menu让player重新开始或者退出level。你现在不能暂停游戏,或者在app回到backgrond时自动暂停游戏。

你会学到如何设计菜单,使得菜单的大小可以轻易适应屏幕,同时也能学到如何显示菜单为popovers或是全屏幕显示,而不需要改变scenes。你也会发现Timeline的Callbacks特性。

对于像暂停按钮或者得分牌之类的UI元素,你需要一个方法去固定这些元素在屏幕上,这样它们就不会随着level的滚动而移动。

Adding Menu Graphics

把Menu和UserInterface文件夹拖到SpriteSheets,然后Make Smart Sprite Sheet。

Adding a Static Game Menu Layer

游戏菜单layer是一个包含了所有in-game UI控制和labels的node。目前,你需要加一个暂停按钮,但是它仍然需要一个记分牌,一个倒计时器,player的血量条,一个虚拟手柄,激活特殊能力的button。

所有这些UI元素应该在它们自己的文件夹中,就像prefabs。你的第一个任务就是在SpriteBuilder中添加一个UserInterface文件夹。然后右键点击UserInterface文件夹,创建一个New CCB文件,命名为GameMenuLayer.ccb,类型为layer,默认568x384。

Aligning a Button with Reference Corners 

拖动一个Button到GameMenuLayer.ccb stage,尽量拖动到左上角。

下一步,打开GameScene.ccb,从Tileless Editor View中拖动GameMenuLayer到stage,确保位置为0,0.

如果你在iPad视角下,按下Cmd + 1切换到Phone模式。注意到Button仅仅是部分可见,或者不可见。

 

Caution:当编辑一个通过一个Sub File node包含另一个CCB文件的CCB文件时,你必须保存CCB文件以看到最新的编辑。

在这个例子中,GameMenuLayer.ccb作为Sub File node被GameScene.ccb包含,任何情况下,当你改变了GameMenuLayer.ccb,你必须做:File-》Save。

 

使用Document->Resolution->Table Landscape和Document->Resolution->Phone Landscape菜单以在iPad和iPhone屏幕尺寸之间切换。更简单的方法是,Cmd + 1和Cmd + 2快捷键。Scene类型的CCB文件也会提供一个Phone Landscape(short)(Cmd + 3)模式,表示3.5-inch的Iphone4s。

你会看到在iPad上看时全部可见的,但是在Iphone上,因为GameMenuLayer.ccb内容尺寸是默认的568x384,并不能全部可见。384points高使得layer比iPhone屏幕更高。

打开GameMenuLayer.ccb,选择root node。改变Content size属性类型,为%in parent container。确保width和height值都是100,这确保了layer的尺寸规模和parent node相同。

本质上,GameMenuLayer.ccb的尺寸现在可以适用于各种设备。

改变GameMenuLayer.ccb的尺寸不会自动的改变位置和它自身包含node的大小。但是,选择button,改变:

改变参考角会定位button,这样button就会永远和top-left相关。不管layer的高如何增加或者减少,button的位置会保持。

改变参考角和用scene改变layer的规模是忽略设备环境排列nodes的最好方法。

距离,假设button的参考角仍然是Bottom-Left,Y位置被设置为95%。那么在iPhone上时,button会出现在左上角320 - 320x95% = 16points处,但是在iPad上时,buton会出现在768 - 768x95%=38points处。考虑到SpriteBuilder在切换iPhone到iPad时采用2x scale,这让你有一个19points的绝对距离。

%pointions会导致3points(16points vs 19points)的Y轴差异(ipad和iphone)。如果你需要精确定位,你必须改变参考角。

 

Editing Button Properties

你刚刚加入到GameMenuLayer.ccb中的button目前有点丑,并且不伦不类。选择button,转到Item Properties tab,可以看到CCNode,CCControl,CCButton,CCLabelTTF。从哪里开始呢?你已经编辑过CCNode属性,那么看看其他的几个属性块。

CCControl Properties

CCControl属性不需要改变。注意所有继承自CCControl的nodes都提供这些属性。CCControl nodes包括Button,Text Field,Slider。

Preferred size(首选尺寸)曾经被叫做minimum size,因为它决定了button的最小尺寸。对应的,Max size决定了最大尺寸,除非它被设置为0.

这些设置对于会在程序中改变的buttons很有用,因为button的大小会随着其label的尺寸改变而增长或缩水。

User Interaction Enabled选项如果没有勾选,那么button不会回应touches或者clicks,除非在程序中把userInteractionEnbled属性设置为YES.所有从CCResponder继承这个属性的nods,都会受到touch和accelerometer事件。

 

CCButton Properties

CCButton属性是你常用的。Title应该设置为PAUSE,如图:

暂时忽略Localize复选框。会在Labels & Localization 章节解释。

当选择了Zoom when highlighted,那么当用户轻击或者按住button时,会放大。这是一个遗留特性,因为Cocos2D曾经对所有菜单buttons都这么做,不论好坏。我认为这是一个粗糙的效果,所有尽量避免它。取代的是,我回简单解释一个巧妙的技巧,你可以给用户视觉的感受而并不去zoom或者改变background image。

勾选Toggles selected state让button变成一个切换按钮,每次敲击都会改变选择状态。当button处于被选择状态时,Selected State部分就有用了。所以,如果你需要一个toggle button,确认你对于Normal state和Selected state使用了不同的属性;否认,你无法判断button是on还是off,normal还是selected。

有四种button状态:

Normal:默认状态

Heighlighted:当buton被触摸(轻击或者按住)的时间里,发生变化。

Disabled:当button的enabled属性被设置为NO(程序中设置或者添加一个自定义属性)时有效,button暂时没有任何功能。

Selected:当Toggles selected状态被勾选时,并且buton被点击了一次时。

Tip:Disabled状态和User Interaction Enabled复选框并不是对应的,你在SpriteBuilder中不能编辑enabled属性。除非你想给button添加一个自定义属性,并命名为enabled,并且设置它的类型为Bool,值为0.你不能添加自定义属性,除非你在Item Code Connections中添加自定义类。但是谁又能说这个类必须是一个真正的自定义类呢?事实上,你可以输入node的原始类(在这:CCButton)作为自定义类。这样你可以添加任何类的那些正常情况下不能在SpriteB中编辑的Bool,Int,Float,和String属性。

 

在这个例子中,button仅使用Normal和Highlighted状态。点击Sprite frame下拉菜单,选择SpriteSheets/UserInterface/P_exit.png。目前,对于normal和highlighted的sprite frame属性都这么做。

当用户正在点击这个button时,为了给用户一个视觉提示,改变Background透明度和Label透明度就很重要了,改变值为0.8,这使得button轻微透明,并且当触摸它时,变成完全不透明。

另一个可选方案是改变normal和highlighted状态的颜色。

Tip:当对highlights使用colors时,最好的效果是使用微妙的颜色变化。

 

CCLabelTTF properties

 button的CCLabelTTF属性和常规LabelTTF node是一样的。如图:

Font Effects区会在”Labels & Localization“章节讨论。

点击Font size的UI按钮可以阻止这些属性在iPad上运行时改变,使得在它和在iPhone上运行时一样。你可以使用Cmd + 1和Cmd + 2观察效果。

Horizontal padding和Vertical Padding设置允许你declare the button's label to be twice this many points wider and higher.

在这个例子中,设置Horizontal padding从默认的10改为20,确保label的字母全部显示。

 

Assigning the Button Selector

在button的Item Code Connections 中,在CCControl区域输入shouldPauseGame作为button的Selector,如图:

Target下拉菜单因为设置为Document root。那么,pause按钮会运行shouldPauseGame selector。

这里同样有Continuous复选框,你现在不应该勾选。如果你勾选了,只要你的手指按在button上,button会每一帧都运行一遍selector。这对于速射游戏很有用,但是在这里对这个功能没有需求。

 

Tip:我喜欢把用户发起的活动用should修饰,因为它们可以被看做请求,而不是一个部分情况就必须遵守的命令。把selectors命名为”should“是很好的。

 

Assigning the GameMenuLayer Class and Ivar

button的”shouldPauseGame“selector被发送到Document root。document root是GameMenuLayer.ccb的root node。而GameMenuLayer.ccb需要一个自定义类,这样selector可以被实现。

Caution:有一个很常见的错误是把button自身设置为一个自定义类,结果就是创建了一个CCButton的子类来处理button selectors。而Buttons永远都不能接收到自己的selectors。

选择GameMenuLayer.ccb的root node,转换到Item Code Connections。在Custom class处,输入GameMenuLayer。另外,你会需要从GameScene类中访问GameMenuLayer root node。打开GameScene.ccb,选择指向GameMenuLayer.ccb的sub file node,然后设置Doc root var为_gameMenuLaye.This will assign a reference to the GameMenLayer.ccb root node to the GameScene ivar named _gameMenuLayer,which you'll add next.

Caution:另一个很常见的错误是编辑GameMenuLayer.ccb root node的Doc root var,而不是它在GameScene.ccb中的引用。这样做会字面上会创建一个self引用 in a GameMenuLayer ivar.改变变量类型为Owner var同样也不能工作,因为owner是一个引用---当使用CCBReader方法load:owner:时,你在代码中指定。

 

Programming the GameMenuLayer

现在是时候在XCode中添加GameMenuLayer类和shouldPauseGame方法了。

你的第一个目标永远应该是让新代码和功能联系起来。如果没有第一时间确认连接实际运行的selectors和正确分配ivars或者属性,那么你的代码很可能是浪费你的时间。

Implementing and Confirming Code Connections

创建一个新的OC类。命名为GameMenuLayer,CCNode的子类。

添加这个类是为了满足分配给GameMenuLayer.ccb的root node自定义类的需求。你可以通过添加如下代码来判断这个类被实例化了,不论连接这个类的node是否通过CCBReader被加载。

#import "GameMenuLayer.h"
#import "GameScene.h"
@implementation GameMenuLayer
- (void)didLoadFromCCB {
    NSLog(@"YAY! didLoadFromCCB:%@",self);
}
- (void)shouldPauseGame {
    NSLog(@"Button:should pause game!");
}
@end

为了满足_gameMenuLayer变量分配给了GameScene.ccb的root node,打开GameScene.m,添加代码:

//Adding the _gameMenuLayer ivar to GameScene.m

#import "GameScene.h"
#import "Trigger.h"
#import "GameMenuLayer.h"
@implementation GameScene {
    //..exsting ivars omitted for brevity...
    
    __weak GameMenuLayer *_gameMenuLayer;
    __weak GameMenuLayer *_popoverMenuLayer;
}

你也要加上一个_popoverMenuLayer引用,当当前level暂停时,会立刻显示出来。popover layers 会在player死掉得时候,到出口的时候,或者暂停button被轻击的时候出现。

现在,你可以运行app,确保没有错误。检查log信息,YAY!,确保GameMenuLayer实例已经被创建。点击pause按钮也应该有log信息。

Assigning a GameScene Reference to GameMenuLayer

你现在已经在GameScene类中分配了GmaeMenuLayer类的_gameMenuLayer ivar。但是你同样也需要在GameMenuLayer中添加一个GameScenes实例的反向引用。如下,在GameMenuLayer.h中添加一个weak应用属性。

#import "CCNode.h"
@class GameScene;
@interface GameMenuLayer : CCNode
@property (weak) GameScene* gameScene;
@end

注意@class GameScene的用法;它告诉编译器GameScene是一个OC类,但是并没有import它。

Note:对于@property,标示符没有前下划线。于是,则个属性是weak的,变量是__weak。我也经常到开发者使用assign存储标示符,也许因为他们看的是过时的教程。assign关键字对于ARC-enabled的apps和unsafe_unretained一样,这不会保存引用或者当释放时置其为nil,并且会留下悬指针,和潜在的EXC_BAD_ACCESS崩溃。对于ARC_enabled apps来说,你应该使用weak存储标示符或者不使用(默认为strong)存储标示符。

 

现在,你仅仅需要在GameScene.m中得didLoadFromCCB中分配self给gameScene属性,如下:

- (void)didLoadFromCCB {
    _gameMenuLayer.gameScene = self;
  //.....     
}

Caution didLoadFromCCB方法运行时采取的时反向等级顺序,也就是说,一个child的didLoadFromCCB用于在其parent的didLoadFromCCB前运行。在这里,_gameMenuLayer就是GameScene node的child。这意味着_gameMenuLayer.gameScene属性需要等到GameMenuLayer的didLoadFromCCB方法运行后被分配。GameMenuLayer中得代码需要一个有效的_gameScene引用(延时),比如,知道onEnter方法运行。

 

如果你很快需要GameScene自定义类的引用,你还可以使用下面的代码片段,以从任何地方,除了node的init和didLoadFromCCB方法获得引用,因为这个时候self.scene还是nil。

GameScene * gameScene = (GameScene*)self.scene.children.firstObject;

 

Showing and Removing Popover Menus

现在,目标是通过名字显示popover屏幕,当暂停button被按下时,弹出pause menu。

在GameMenuLayer.m中,更新shouldPauseGame方法,如下:

- (void)shouldPauseGame {
    NSLog(@"Button:should pause game!");
    [_gameScene showPopoverNamed:@"UserInterface/Popovers/PauseMenuLayer"];
}

因为我们还需要移除popover menu,更新GameScene.h,添加代码:

#import "CCNode.h"

@interface GameScene : CCNode <CCPhysicsCollisionDelegate, CCBAnimationManagerDelegate>
- (void)showPopoverNamed:(NSString*)popoverName;
- (void)removePopover;
@end

在@interface区域中声明的方法,允许其他类调用它们。现在,在GameScene.m中添加showPopoverNamed:方法:

-(void) showPopoverNamed:(NSString*)popoverName
{
    // allow only one overlay menu at a time
    if (_popoverMenuLayer == nil)
    {
        // create a new menu manager with the pause menu
        GameMenuLayer* newMenuLayer = (GameMenuLayer*)[CCBReader load:popoverName];
        NSAssert1(newMenuLayer != nil, @"failed to load menu named: %@", popoverName);
        NSAssert1([newMenuLayer isKindOfClass:[GameMenuLayer class]], @"menu layer not using the GameMenuLayer class: %@", newMenuLayer);
        
        // since the menu loaded from CCB is a new node we need to add it as child
        [self addChild:newMenuLayer];
        
        // now that the new menu manager is retained by self.children we can assign it to its weak reference ivar
        // if we assigned to the __weak _overlayMenuManager ivar right away, ARC would have set it to nil before reaching the addChild: line!
        _popoverMenuLayer = newMenuLayer;
        _popoverMenuLayer.gameScene = self;
        
        // pause game and hide menu layer
        _gameMenuLayer.visible = NO;
        _levelNode.paused = YES;
    }
}

 

检查_popoverMenuLayer是否为nil确保了同一时间只会有一个可见的popover menu,即使这个方法意外的执行了两次。然后,CCBReader方法被用于根据名字载入CCB。返回值被强转为GameMenuLayer*,这假设了所有popover menus都使用GameMenuLayer作为它们的自定义类。newMenuLayer在被分配给ivar _popoverMenuLayer之前作为一个child加入。

你可以想知道,为什么不直接分配_popoverMenuLayer,移除本地变量newMenuLayer?原因是_popoverMenuLayer被声明为__weak,这样它不会保留分配给它的任何引用。但是当CCBReader load:返回时,如果只有很短的时间,没有强引用保留返回的node。这样,ARC会释放node,并且设置_popoverMenuLayer引用为nil,即使在下一行addChild运行之前。

精确来说,这也许能工作,依赖于设备的debug模式,但是它肯定不能在release模式工作。

 

Tip:建议添加一个NSAssert在addChild之前,以确定newMenuLayer是否为nil。

就像_gameMenuLayer.gameScene,_popoverMenuLayer.gameScene被分配给了self,以允许在GameMenuLayer类中得方法可以反向发送消息给GameScene实例。

当一个popover menu显示时,暂停按钮和其他静态UI元素应该被藏起来。设置_gameMenuLayer.visible为no即可隐藏layer,这也让button现在失效。也就是说,一个不能看到的button是无法触摸的。

暂停_levelNode是通过暂停levelNode的所有child nodes来阻止游戏进行的。暂停一个node就暂停了它的Timeline动画,移动动作和scheduled selectors。因为_physicsNode是child node,所以physics世界也一样被暂停。

为了移除一个popover菜单,你也需要添加removePopover方法。这样从它的parent(GameScene node)处移除了node,并且设置_popoverMenuLayer ivar为nil,来让新的popover显示。即使这样,_popoverMenuLayer应该立刻自动为nil,提醒自己有意的分配nil是一个好主意。

Removing a popover

- (void)removePopover {
    if(_popoverMenuLayer) {
        [_popoverMenuLayer removeFromParent];
        _popoverMenuLayer = nil;
        _gameMenuLayer.visible = YES;
        _levelNode.paused = NO;
    }
}

 最后,_gameMenuLayer和暂停按钮一起又可见了,_physicsNode也不处于暂停状态了。

使用这个结构,你可以简洁的显示和隐藏popover。还有最后一个事情需要做。在GameMenuLayer.m中,你应该添加另外的两个方法。

//Enabling the GameMenuLayer to resume the game by playing a specify timeline animation.

- (void)shouldResumeGame {
    CCAnimationManager *am = self.animationManager;
    if ([am.runningSequenceName isEqualToString:@"resume game"] == NO) {
        [am runAnimationsForSequenceNamed:@"resume game"];
    }
}
- (void)reumeGameDidEnd {
    [_gameScene removePopover];
}

这里的思路是让shouldResumeGame运行特定名字"resume game"的Timeline。但是仅仅是在这个指定的Timeline当前没有运行时,以防止用户多次敲击resume button,无意中重启Timeline。

 

Caution:Timeline的名字是大小写敏感的。

 

Pausing the Game

剩下的工作时创建一个Layer CCB,作为真正的pause menu popove。和一个名字为"resume game"的Timeline动画,它又一个Callbacks关键帧,并且会运行resumeGameDidEnd selector.

Creating the Pause Menu Popover

对于这个和接下来的popover menus,在UserInterface文件夹中添加一个名为Popovers的子文件夹。右键创建一个Layer类型的CCB文件,命名为PauseMenuLayer.ccb。

对于所有popover CCBs ,你需要做的第一个事情是设置root node的Content size属性设置为100%,预编匹配不同的设备屏幕尺寸。

选择root node,选择Item Code Connections,输入GameMenuLayer作为自定义类。

 

Note:之所以对所有的popover menus使用同样的类,包括GameMenuLayer自身,是因为它们共享很多代码。比如,它们都会暂停游戏,它们都有restart button.加上GameMenuLayer中的代码简单和简洁。

下一步,你将添加一个背景图片,该图片比任何设备的尺寸都小,这样就可以轻易的在所有设备上居中显示。320x240是一个好的尺寸。使用比任何屏幕尺寸都小的背景图片能够简化UI设计。你可以居中内容,还可以完全忽视屏幕尺寸。只有背景距离屏幕边缘的尺寸不同。

拖动一个Sprite到stage,改变Sprite fram为P_bg.png。或者,你也可以拖动P_bg从Tileless Editor view。不论你怎么创建这个sprite,你应该设置position类型为%,值为50,这样sprite就是居中的。

Tip:如果你很难看清sprite image,可以Document-》Stage Color,改变颜色。

你必须对接下来的nodes使用%positions。labels和buttons由你决定,唯一重要的是保证layer中所有的nodes都是backgroud sprite的children,这样他们的位置就总是和背景sprite的左下角相关了,而不是屏幕的左下角。这让你可以随着background移动sprites。

添加一个LabelTTF,作为P_bg sprite的child,改变position类型为%。

添加3个Button nodes,确保是P_bg sprite的children,改变potion类型为%。

对于每一个button,按如下步骤:

1.设置Title为Restart,Exit,Resume。

2.改变Normal state和Highlighted state的Sprite frame。P_restart.png,P_exit.png,P_resume.png.

3.对于Highlighted state,改变Backgound color 和Label color为gray color----比如,web color #999999.当按下buttons时,会有视觉提示;当highlighted时,会更暗。

4.在CCLabelTTF区域,编辑Font name和Font size。你也许会需要增加Horizontal padding,或许是Vertical padding,以便让background图片更大。

编辑完成后,转到Item Code Connections。对于每个button,输入对应的Selector:shouldRestartGame,shouldExitGame,shouldResumeGame.

运行APP,敲击pause button。注意到按任何layer中得button都会引起崩溃。

 

Interlude:Full-Screen Popovers

一个游戏内置menu暂时性的取代当前scene是很常见的需求,开发者经常不知道如何实现。

开发者们经常使用push和pop scene方法,或者甚至在存储game states后替换scenes。虽然这两种方法都有效,但是有一种很简单的方法,创建一个无损的全屏幕“scene”,覆盖在当前scene上,而不用改变scenes。

你可以在你刚才创建的popover layer加强这个功能。拖动一个Color node到PauseMenuLayer.ccb stage上。在Timeline上移动这个color node,确保它在P_bg background sprite上方。确保color node不是background sprite的child。然后设置color node的位置为0,0,改变Conten size为%类型,值为100。这就扩展了color node撑满了屏幕。

如果你运行APP,并且敲击pause button,突然你的游戏view消失了。这是因为你看到了一个全屏幕有着颜色背景的popover menu。你也可以用Gradient Node代替color node。

 

Tip:如果你在显示这样一个全屏幕的popoovers时,发现因为低帧率而有延迟,那么简单的设置background中得_levelNode的visible属性为No。这可以防止popover后面内容的渲染,提高渲染的表现。

 

但是等等,还有更多的东西可以加上!你可以在popover显示的时候同时使得level场景昏暗。这很容易实现,只要改变color node的Color属性为黑色,并且设置透明度为0.5左右即可。

你也可以试试color node的Blend src和Blend dst设置,来创建附加效果。试一试设置Blend src为One,Blend dst为One - Dst Color,来get false colors。同时试着改变color node的color 和opacity。

或者设置Blend src 为One-Dst Color,Blend dst为Src Alpha Saturate,Opacity设置为1,和白色的color,来创建一个photo-negative(底片)效果。如图:

 

Note:src和dst缩写表示source和destination。这些属性指的是OpenGl混合模式,

which affect how the colors of pixels in overlapping source and destination buffers are blended together.

(这影响到象素的重叠源和目的地缓冲器的颜色如何混合在一起)

 

对于gradient node来说,blend模式或许工作起来完全不同。改变blend模式不能告诉你真正的效果是什么,你必须在设备中尝试。

当然,你可以使用一个有着足够大的background图片的Sprite,来盖住最大的屏幕尺寸。

 

Changing Scenes With SceneManager

现在,restart和exit没有任何作用。restart和exit按钮会呈现一个新的scene,来移除现有的scene,所以你不用担心要关闭popover。

因为你要从project的不同地方呈现scenes,并且每个scenes应该用相同的transition来呈现,所以仅仅用一个方法来呈现各种scene是很合理的。

添加一个新的OC类,命名为SceneManager,在Subclass中写NSObject。如下:

#import <Foundation/Foundation.h>

@interface SceneManager : NSObject
+ (void) presentMainMenu;
+ (void) presentGameScene;
@end

ScenenManager.m实现了这两个类方法。如下:

#import "SceneManager.h"

@implementation SceneManager
+ (void)presentMainMenu {
    id s = [CCBReader loadAsScene:@"MainScene"];
    id t = [CCTransition transitionMoveInWithDirection:CCTransitionDirectionLeft duration:1.0];
    [[CCDirector sharedDirector] presentScene:s withTransition:t];
}
+ (void)presentGameScene {
    id s = [CCBReader loadAsScene:@"GameScene"];
    id t = [CCTransition transitionMoveInWithDirection:CCTransitionDirectionRight duration:1.0];
    [[CCDirector sharedDirector] presentScene:s withTransition:t];
}
@end

Note:使用通用类型id是合法的,而且很有用。

 

定义了SceneManager的方法后,打开GameMenuLayer.m,添加#import "SceneManager.h"

然后添加如下代码:

- (void)shouldRestartGame {
    [SceneManager presentGameScene];
}
- (void)shouldExitGame {
    [SceneManager presentMainMenu];
}

 

Adding a Callbacks Keyframe

在返回游戏之前,先用动画效果移除pause popover menu是很好的想法。这可以给用户一些时间去准备回到游戏,可以在取消掉pause menu后引入3...2...1倒计时。

现在,理论上,你可以分配一个animationManager.delegate,并且实现completedAnimationSequenceNamed:方法,以得知resume game Timeline被播放完成。

但是,我还没有解释如何在Timeline中使用Callbacks 关键帧,并且我也没有解释它们在任何Timeline的时间点上如何去运行自定义selectors。这种情形是使用Callbacks关键帧的好时机。事实上,任何情况下,再Timeline动画持续时间内,当你需要在指定的点上调用一个selector时,都可以使用Callbacks关键帧。

在SpriteBuilder中,打开PauseMenuLayer.ccb,左键点击Timeline List,选择New Timeline,这会创建和选择一个新的timeline,名字是Untitled Timeline.再一次左键点击Timeline List,选择Edit Timelines,打开编辑对话框,双击Untitled Timeline,编辑,重命名为resume game.保证Autoplay复选框不选中,点击Done。

Caution:一旦在一个CCB中你有超过一个timeline,那么当心你是否是在编辑正确的Timeline就很重要了。

 

下一步,编辑Timeline duration。确保Timeline持续2秒。现在,移动Timeline Cursor到Timeline结束的地方,也就是2-seconds处,这里就是你应该添加Callbacks关键帧的地方。

技巧在于按住Option键,然后点击Callbacks段,来添加Callbacks关键帧。

添加了关键帧后,它暂时还没有效果,你必须编辑关键帧的属性。方法是双击关键帧。一个popover会出现,这里你应该输入:resumeGameDidEnd。

Tip:Sound effects关键帧也是按住Option键添加的,编辑方式一样。

Note:当你视图cut或者delete一个关键帧或者Sound effects关键帧时,如果你收到一个message“the root node cannot be removed”,只要取消选择root node,并且选择任何其他node或者点击stage等。

 

你已经添加了必要的代码去处理返回游戏。shouldREsumeGame方法应该运行resume game Timeline,最终会运行resumeGameDidEnd selector.

如果你运行app,暂停游戏,点击resume,pause menu会在两秒后消息,游戏会继续。

 

Animating the Pause Menu on Resume

点击resume button后没有任何视觉反馈很不好。如何精确的给PauseMenuLevel.ccb的Timeline添加动画效果取决于你,除了Callbacks关键帧中得resumeGameDidEnd selector。下面是一些在这两秒钟你可以做的好主意。

第一,考虑到对P_bg background Sprite的动画同样也会移动,scale,或者旋转它的child nodes。唯一没有继承它的属性是opacity。如果你想要慢慢的渐变,恐怕你必须添加同样的透明度关键帧给每一个单独的button,label,和background sprite----并且在它们上方,让它们以同样的时间戳开始和结束,给它们同样的opacity值。所以,我决定不渐变layer。

tip:如果你做了blend-mode方面的改变,你会发现很难在stage上看清东西。这样的话,点击Timeline中CCNodeColor右侧的眼睛符号,隐藏color node,并且在游戏中设置为visible。

 

选择P_bg sprite node,按V,插入一个Visible keyframe。移动这个关键帧到最左边,时间戳为00:00:00.visible属性只能被打开或者关闭;因此,关键帧段看上去更厚重。

下一步,添加两个Position关键帧,在00:00:00,和00:01:00时间戳处按P。移动Timeline Cursor到第二个关键帧处,移动到右侧,把P_bg sprite的位置移动到屏幕左上角。右键Position关键帧段,选择Ease In easing模式。这实现动画移动到左上角。

选择P_bg sprite增加两个scale关键帧段,同样是在00:00:00到00:01:00之间,移动Timeine Cursor到第二帧上,改变Scale属性为0.2和0.1,对应X和Y轴.右键点击关键帧段,选择Ease out easing模式。

如果你也想添加某种ready指示,拖动LabelTTF node到stage。这个node应该是root node的child,而不是P_bg的child。把label的text改为Ready!改变Font尺寸到100.确保label的Visible复选框不要勾选,这样,一开始它使隐藏的。你还应该改变label的position类型为%,值为50,这样它就在stage中居中显示。

选中新的label,移动Timeline Cursor到1-second处,按V。移动到右侧,按V。重复这个过程,直到你有了3个等距等长的关键帧段。重要的一点是最后的关键帧应该设置label的Visible属性为NO。

如果你播放动画,menu会缩小并且移动到右上角,同时,ready label开始闪动三次。如果你在游戏中运行,当Timeline到达Callbacks关键帧处,游戏会返回。

 

Interlude:Pausing the Game When It Enters the Background

如果游戏在app返回到background时能够自动暂定更好。这经常发生,比如当接到一个电话,或者用户按下home键。

receive them as they are sent by iOS without having to manually pass there messages along or registering notifications and what not?事实上,实现起来很简单。

打开AppDelegate.m文件,添加:

- (void)performSelector:(SEL)selector onNode:(CCNode*)node withObject:(id)object recursive:(BOOL)recursive {
    if([node respondsToSelector:selector]) {
        [node performSelector:selector withObject:object];
    }
    if (recursive) {
        for(CCNode *child in node.children) {
            [self performSelector:selector onNode:child withObject:object recursive:YES];
        }
    }
}

Tip:如上的方法可能added as a category on NSObject instead-----in particular,if you find that you have use for it in the future.

performSelector:onNode:withObject:recursive:方法模仿其他通过NSObject类定义的performSelector,除了它只在实现它的node上通过测试respondsToSelector:first运行selector。

Note:performSelector:withObject:有可能产生"may cause a leak"警告。只要selector不返回一个pointer或者id类型就可以忽视它。同时注意selector必须带一个单独的参数---不多不少。如果你需要使用其他消息签名,你必须写不带object或者带两个object的方法。

 

如果recursive表示被设置,同样的方法会对每个node的children和它们的children上运行。最后,通过实现它,每个node hierarchy中得node都有机会去运行给定的selector。

这不是传递消息给其他nodes最有效的方式,所以避免在经常发生的事件上使用它。但是它作为会很方便的对于发生很少的消息很拥有,不用知道哪个nodes可能对接受该消息有兴趣。替代的方法是用NSNotificationCenter注册一个node,of course,properly unregister it as well,which is more cumbersome and error prone than just implementing a specific UIApplicationDelegate selector.

An example use is to forward the applicationWillResignActive:message to any node interested in handling that particular message.添加代码到AppDelegate.m。如下:

- (void)applicationWillResignActive:(UIApplication *)application {
    CCScene *scene = [CCDirector sharedDirector].runningScene;
    [self performSelector:_cmd onNode:scene withObject:application recursive:YES];
    [super applicationWillResignActive:application];
}

当前被CCDirector包含的正在运行的scene在scene中运行_cmd selector,并且递归到所有的child nodes,传递application object。CCDirector类是Cocos2D UIViewController 的subclass,并且管理COCos2d的程序状态。

这里的_cmd selector是什么呢?这是一个隐藏的方法参数,在scenes背后,每个OC方法都可以接收。

为了当程序退出后真正的暂停游戏,在GameMenuLayer.m中添加方法:

 

- (void)applicationWillResignActive:(UIApplication *)application {
    [self shouldPauseGame];
}

 

Killing the player

Game Over状态可以像暂停一样处理,允许玩家重新开始level或者退出当前游戏区域。

并且,player需要die!

Creating a "Game Over" Popover

在SpriteBuilder中,右键点击Popovers文件夹,添加新文件,命名为GameOverMenuLayer.ccb,类型为Layer,点击Create。编辑Game over菜单遵循暂停菜单的基本原则,下面是不同之处。

比如,没有resume按钮,只有exit和restart。确保设置GameOverMenuLayer.ccb root node的自定义类 ;否认,所有的buttons都不能工作。同样,exit和restart按钮应该有它们自己的Selectors,ShouldExitGame和shouldRestartGame.

Showing the "Game Over" Popover

返回Xcode,打开GameScene.m,定位ccPhysicsCollision方法,添加代码:

- (BOOL)ccPhysicsCollisionBegin:(CCPhysicsCollisionPair *)pair player:(CCNode *)player saw:(CCNode *)saw {
    [self showPopoverNamed:@"UserInterface/Popovers/GameOverMenuLayer"];
    return YES;
}

 

Interlude:Fixing the Initial Scroll Position

你可能注意到了当你按下restart按钮或者改变Menu scene为game scene时,the level view isn't centered on the player initially.

打开GameScene.m,定位LoadLevelNamed:方法。添加:

[self scrollToTarget:_playerNode];

这确保了_physicsNode.position在scene第一时间渲染之前,根据player的初始位置更新。

posted on 2015-02-26 14:41  SomeBod_Y  阅读(439)  评论(0编辑  收藏  举报