SpriteBuilder 学习笔记六

Chapter 7    Main Scene and Game Statenode

至今,咱们彻底忽略了主场景,而主场景是放置游戏的main menu的好地方。main menu放置设置菜单的好地方,Settings menu让你能够改变音量等。spring

settings popover须要一个grid-based layout,这就是为何你须要使用Box Layout node。你一样须要添加一个方法去永久存储game state----好比,解锁levels和audio volumes,这样就不会在app结束的时候丢失这些信息。安全

 

Main Scene Backgroundapp

Designing the Background Imagesless

在以前的章节,你已经添加了Menu和UserInterface Sprite Sheets。转到Tileless Editor View,在底部,勾选Menu folder。iphone

若是你想要提供你本身的图片,你应该有3个screen-sized图片,来构成menu的背景layers。ide

Screen-sized意味着-----568x384 points,若是你想能共同运行在iphones和ipads。oop

考虑到iPad和Retina iPads的scale,full-screen 图片应该有2272x1536布局

 pixels。若是你想让你的app仅仅运行在iPhones上,你的参考size应该是568x320 points。这样,你仅仅须要1136x640 pixels的图片来适配iPhone Retina屏幕。动画

你可能好奇为何全部的background layer图片都必须是screen-sized。这不是浪费内存么?实际上,不是这样的。若是你在SpriteSheet中放置这些图片,SpriteBuilder会减掉任何excess transparent areas。

上图顶部的灰色区域是被SpriteBuilder剪掉的,由于这一区域只包含透明的像素。

对于游戏开发者来讲,有一个永恒的矛盾:视觉质量 vs 内存使用/表现。

上图显示的时Menu Sprite Sheet以前的图片,你能够注意到M_bg.png图片和两个M_monsters.png和M_frontsmoke.png图片使用了相同的空间。进一步说,一些图片在SpriteSheet效率更高。

 

Designing the Background CCB

由于你但愿在其余scenes中重复使用menu的background layer,那么在一个分开的layer中设计就是一个好主意。

右击UserInterface 文件夹,(不是Sprite Sheet中文件夹),New File,建立MainMenuBackground.ccb,设置类型为layer,默认568x384尺寸。

像往常同样,第一件要作的事情是对Layer CCB的root node,设置Content size类型为%,Content size值为100.这确保了layer在全部设备上都是居中的。

添加一个常规的Node,到MainMenuBackground.ccb stage上。设置node的位置类型为%,值为50,这样这个node在layer上是居中的。把timeline中得node重命名为background。background node将是背景图片的容器。

你能够从Tileless Editor View中拖动背景图片到backgroud node中。如图:

M_bg

M_monsters

M_frontsmoke

它们应该是background node的children,这样sprites会自动的在layer中居中。

Note:改变一个node的anchor point会影响scale和rotate操做,也会影响bounding box和collision detection。anchor point仅仅会移动一个node的视觉表示,这个表现可能不会被物理的表示。你永远不该该把anchor point当作改变位置的功能用。总的来讲,anchor point通常都是0x0或者0.5x0.5.

Animating the Background

具体来讲,你应该对每一个background图片和node添加3个关键帧。首先给background node添加3个scale关键帧,按S。

一个关键帧在两端,第三个应该在中间,5-seconds处。移动Timeline Cursor到中间的关键帧,在Item Properties中,对background node,设置Scale值为1.02.

这个值很小,可是一旦你播放动画,就能马上看到效果。而后右键点击关键帧段,选择Ease In/Out。对其余段重复这个过程。

下一步,对M_bg和M_frontsmoke sprites重复添加3个关键帧,选择Ease In/Out。对于M_bg中间的关键帧,设置scale值为1.01和1.02.对于M_frontsmoke,设置中间的关键帧Scale值为1.02和1.06.

如今选择M_monsters node,改变它的Y position属性为-15.这将稍微移动一下node。而后添加3个position关键帧,按P。移动Timeline Cursor到中间的关键帧,编辑Y的position属性为0.加上Ease In/out mode。

最后,你还须要让这个动画循环。左键点击下方的No chained timeline,选择Default Timeline。

 

Launching with the Menu

为了在游戏中看到这段动画,你须要完成一些基础事务。你尚未添加background到MainScene.ccb中。

打开MainScene.ccb,移除gradient 和labelnodes。事实上,移除全部额外的nodes,除了play button;不然,直到下一章以前,你都不能play level。可是你能够把play button移动到左上角。

清理完MainScene后,拖动MainMenuBackground到stage,从Tileless Editor View或者FileView中拖动均可以。设置位置为0,0.重命名CCBFile实例为background。

打开AppDelegate.m,找到startScene方法,改成:

- (CCScene*) startScene
{
    return [CCBReader loadAsScene:@"MainScene"];
}

如今游戏会从MainScene启动。

Main Scene Logo and Buttons

如今,添加一些buttons和logo。button图片play.png和settings.png应该有旋转动画。

 

Designing Logo and Buttons

为buttons建立一个新的CCB文件。右键点击UserInterface文件夹,选择New File,命名为MainMenuButtons.ccb,类型为Layer。保持默认尺寸,点击Create。改变stage color为白色。

一样的,第一个要作的事情是选择root node,改变Content size类型为%,值为100,以确保居中。

添加一个常规Node到stage。改变位置类型为%,值为50.在Timeline中命名为logoAndButtons。

从Tileless Editor View中,拖动play,settins和title图片到logoAndButtons node中。它们的实际位置不是很重要。

title sprite的position为0,50;

play sprite的position为-50,-60;

settins sprite的position为50,-60;

Tip:仅仅不勾选timeline中得eye symbo只会在SpriteBuilder中隐藏,可是仍然会在底层渲染,这是一个很大的负担。

buttons须要一些text。

拖动LabelTTF node到play sprite上,再拖动一个到settings sprite上。labels应该是sprite nodes的child。

对于两个labels,先选择label,而后改变位置类型为%,值为50.这让label在各自的sprite上居中。而后输入PLAY和Settings,对应。play label的Font size设为20,settings为16。

 

Animating Logo and Buttons

左键点击Timeline List,选择Edit Timelines,打开对话框。点击+按钮,建立第二个Timeline,命名为loop。双击Default Timeline,更名为entry。不要勾选Autoplay复选框。

目标是让entry动画自动播放,而后又entry动画进入loop动画,而loop动画是循环的。

总的来讲,这是一个建立循环动画很方便的方法。

选择entry Timeline。在Timeline的底部,左击No chained timeline,设置为loop。再选择loop动画,点击No chained timeline,选择loop。这就正确的连接了entry Timeline进入loop,而后loop本身循环。

 

Editing the Entry Animation

选择entry Timeline.这是你第一个要进行动画设置的Timeline。想法是buttons初始化时在屏幕外,而后zapping in。这是一个时间很短的动画效果-----毕竟,用户但愿你的app很快,而且不想等动画结束,不论你花了多少功夫在动画上。

点击duration box,编辑Timeline持续时间。设置为1秒15帧,即00:01:15.这等于1.5秒,由于SpriteBuilder中,30帧为1秒。

选择logoAndButtons node,移动它到layer得左侧外。X position为-20,按P建立一个关键帧。移动Cursor到最右边,按P建立第二幅关键帧。编辑logoAndButtons node的位置,为50.

右键点击关键帧段,选择Elasic Out easing模式。再次点击关键诊断,Easing Setting选项如今已经可选了,点击Easing Setting。

在这里,这个floating-point值叫Period,它定义了动画的springy(弹性)程度,这个值越低,the more the node will move back and forth before coming to rest.在这里,输入0.9,点击Done,

 

entry动画能够再增长一些东西。一个好的效果是在合适的时间调整buttons的scale。对play喝settings sprite nodes重复下面的步骤:

1.选择sprite node(play 和 settings)

2.移动Timeline Cursor到timeline的中间。

3.添加一个Scale关键帧,按S。设置X和Y的scale属性为0。

4.移动Timeline Cursor到最右端

5.按S,添加另外一个Scale关键帧,设置X和Y的Scale属性为1。

6.右键点击关键帧段,选择Bounce Out easing模式。

你能够试着移动play和settings sprite的第一个关键帧,这样它们动画开始的时间就不同了。

以下:

Editing the Loop Animation

对于entry Timeline来讲,这些就够了。如今,切换到loop Timeline。你会注意到,全部已经存在关键帧都消失了。

这些对你没有影响,可是思考:你制做的动画效果在接下来的另外一个Timeline上可能不协调。容易造成忽然的移动。

在这个例子中,这一点很好修复,选择logoAndButtons node,改变它的位置为50%x50%。由于你已经选择了loop动画,这个位置的改变会不影响entry Timeline中得logoAndButtons。

改变Timeline的持续时间为2秒。而后对于play和settings sprites,作以下:

1.选择sprite node(play或者settings)

2.移动Timeline Cursor到最左边。

3.按R,以建立一个旋转关键帧。

4.移动Timeline Cursor到最右边。

5.按R,建立另外一个旋转关键帧。

6.改变该关键帧为360.

 

注意到sprites旋转了。同时注意labels和它们各自的parent sprites一块儿旋转。可是,这是一个好主意么?不彻底是的。

你能够用一个技巧:若是你在child node上播放和parent node同样的动画,可是方向相反,那么这两个动画会互相取消!因此,若是你向它们的parent sprites旋转的相反方向旋转label,那么它们会中止旋转。对两个labels作以下步骤:

1.选择sprite node的label(play或者settings)

2.移动Timeline Cursor到最左边。

3.按R,建立旋转关键帧。

4.改变旋转属性的值为360.

5.移动Cursor到最右边。

6.按R建立另外一个关键帧。

7.改变Rotation属性为0.

 

这样作的结果是sprites如今顺时针旋转,它们的child labels逆时针旋转。

由于这两个动画都是满旋转,label的旋转平衡了它们parent sprites的旋转,这样labels就保持不一样。

如今,右击每个label的关键帧段,改变easing modes为Ease In/Out,你能够再右击,而且改变Easing settings为低速率的值,好比1.3到1.8之间。

在任何状况下,由于两个动画再也不是同步的----label的旋转速度由于easing的缘由随着时间提升或降低,label如今会左右摆动。

若是你让parent和child的动画不一样,你会发现大多数奇怪的方法。

举例来讲,想象你要去移动一个带有easing  mode的node,向右200points的距离,而且向左移动它的child sprite node 150points距离,而且easing mode也不一样。这样就会有组合效果。

若是你运行app,你会发现logo从左边进入,而后buttons开始出现,而后buttons开始旋转。

 

Tip:若是你但愿让buttons在entry Timeline运行中就开始旋转呢?有两种通用的方法。

一是在entry Timeline中对sprites和它们的labels复制旋转关键帧。这能够工做,可是会工做两次,若是你曾经修改了buttons的旋转,可能会工做更屡次。

还有一种方法是对每一个button建立一个自定义CCB文档,而且建立旋转动画。可是,这意味着你必须对每一个buttons建立一个自定义类。

Adding the Buttons to MainScene

若是你想要在游戏中看新的logo和buttons,你还必须添加MainMenuButtons.ccb到MainScene.ccb中。拖动.ccb文件到MainScene.ccb stage或者在Tileless Editor View中拖动它。把新的CCBFile位置改成0,0,起名logoAndButtons。

 

Creating Buttons Out of Ordinary Sprites

是否是哪里不对呢?至今,buttons还不是真正的buttons,仅仅是一个带这label得sprit。若是让它们能够点击?

简单,经过添加一个button!而后移除button的frame和label。惟一有些不一样的地方是你不能让按钮的highlighted state动起来,由于button在仅仅是highlighted的状况下不会发送消息。可是至少你能够建立buttons而不用必须按照Button node的规矩让images动起来。

Note:试试使用play.png或者settings.png做为button的normal-state sprite frame。或者建立一个Sprite 9 Slice,而且分配它SpriteSheets/Menu/play.png。在内部,Button node使用Sprite 9 Slice。

 

打开MainMenuButtons.ccb,对play和settings sprites重复下面的步骤:

8.拖动一个Button到sprite(play或者settings)的Timeline中,button是sprite的child。

9.改变button的位置类型为%,值为50.

10.清空button的Title field。这会让button的label消失。

11.对于Normal State和Highlighted State,改变Sprite frame 属性为<NULL>.这会移除button的背景图片。如今它使一个不可见的button。

12.button的尺寸有点小,改变button的Preferred size属性为60x60.

你如今在play和settings sprites中有了两个不可见的buttons。

 

Connecting the Buttons

用selectors链接到buttons,选择Item Code Connetions。而后依次选择每一个button,在Selector field,输入shouldPlayGame(play button)和shouldShowSettings(settings button)。

还有一件事。selectors须要发送message到某处,而某处指的就是Document root。这个属于指的是MainMenuButtons.ccb的root node。选择它的root node,在自定义类中输入MainMenuButtons。

 打开XCode,添加一个新的OC类。类名必须是MainMenuButtons,CCNode的子类。编辑MainMenuButtons.m,添加以下代码:

- (void)shouldPlayGame {
    NSLog(@"Play");
}
- (void)shouldShowSettings {
    NSLog(@"SETTINGS");
}

这段代码是为了检测buttons是否正常工做的。

 

Settings Menu

settings menu必须是一个popover,就像pause和game over menu同样。这里有几个须要注意的点。一个是通用的关闭button,经过移除对应的parent node关闭。为了简单的建立settings menu layout,Box Layout node被用于安排横竖排得nodes。

另外一个特性是Slider controls,能够改变属性值----在这里,是audio volume levels。

Designing the Settings Menu with Box Layout

右键点击UserInterface文件夹,选择New File,建立一个新的CCB文件,命名为SettingsLayer.ccb,类型是Layer,尺寸为默认的568x384.改变root node的Content size类型为%,值为100.

拖动一个Node到stage,改变position type 为%,值为50.重命名这个node为settingsLayer。

从Tileless Editor View中拖动S_bg图片到settingsLayer node中。

 

Introducing Box Layout Nodes

如今,处理Box Layout node。这能够把nodes在水平和垂直方向布局。settings menu应该有一个label和两个volume sliders。

这个grid-like layout能够用parent-child关系来形容。一个垂直Box Layout nodes,有两个水平Box Layout nodes做为children。

拖动一个Box Layout node到settingsLayer上,让它成为settingsLayer的 child node。在Item Properties中,改变Direction属性从默认的Horizontal为Vertical,在Timeline中更名layout node为verticalLayout。

而后你应该添加一个LabelTTF和两个Box Layout nodes做为verticalLayout node的children。你应该重命名两个CCLayoutBox为horizontalLayoutMusic何horizontalLayoutSfx。对于label,改变它的Label text为Settings,Font Size设置为32.

每一个slider被添加到horizontal layout nodes中得一个,和label node一块儿。拖动一个LabelTTF和一个Slider node到horizontalLayoutMusic node中,对于horizontalLayoutSfx node也重复这个步骤。

选择每一个label,改变它的text为Music Volume和Effects Volume,以下图:

verticalLayout node垂直的排列它的children:settings label和两个horizontal layout nodes,每一个都包含一个LabelTTF和一个Slider node。

verticalLayout node中得内容,label在底部,horizontalLayoutSfx在顶部。

nodes并无在layer中居中,由于Box Layoutnodes的anchor point默认0,0.

因此,即便verticalLayout node的位置在stage中是居中的,可是由于anchor ponit的关系,内容不能居中。

选择verticalLayout node,改变它的Anchor point属性为0.5。这可让nodes居中。你应该改变verticalLayout node的Spcaing属性为30,以增长sliders和labels之间的数值区域。

可是sliders仍然太宽了,而且和labels重叠了。你能够减小slider的尺寸,选中slider,干煸Width of thePreferred尺寸属性,从默认的200改成150.

label和slider仍然重合,这是一个间距问题,经过编辑Spacing 属性,把spacing设置为20.

 

Left-Alignment with Box Layout

若是你仔细观察,你可能已经发现两个labels和sliders仍然没有完美对其。sliders并无在同一个X坐标位置开始和结束。

把volume label改成FX Volume后,会看的更清楚:

这里的问题是除非你使用彻底同样的text,不然labels的size必定是不一样的。

幸运的是,这能够经过确保horizontalLayoutSfx和horizontalLayoutMusic有着一样的内容size解决。若是你选择它们,能够看到它们的content size是不一样的。horizontalLayoutMusic的宽是246,horizontalLayoutSfx的宽是230,若是你已经改变了child label的text为FX Volume.

这意味着horizontalLayoutSfx的内容少了16points。因此你仅仅须要去让它再宽16points。考虑到你已经对两个horizontallayout nodes的Spacing属性都设置到了20。你须要去增长horizontalLayoutSfx node的spacing。

选择horizontalLayoutSfx node,改变Spacing属性为36----原始的20加上少的16.

 

Center-Alignment with Box Layout

可是若是你想让labels居中对齐呢,并且不想在改变了label的text后担忧spacing属性问题呢?

 

“As of now, you have each label and slider aligned horizontally in a Box Layout node, and the two box layout nodes are aligned vertically in another Box Layout node. You can always reverse this setup. In this instance, you could have both labels in a vertically aligned Box Layout node, and both sliders in another vertically aligned Box Layout node. You would then add both vertical box layout nodes to a horizontally aligned Box Layout node.”

“The result will be different in so far that each vertical column’s width is defined by the node with the largest width. In other words, the widest label now defines the alignment of all labels in relation to all sliders next to the labels.”

摘录来自: Steffen Itterheim. “Learn SpriteBuilder for iOS Game Development”。 iBooks.

首先,移除horizontalLayoutSfx和horizontalLayoutMusic nodes。这同时也会移除它们的child nodes。

而后拖动一个Box Layout node到现存的verticalLayout node上,而且命名为horizontalLayout。拖动另外两个Box Layout nodes到horizontalLayout node中,改变它们的Direction为Vertical,命名为verticalLayoutLabels和verticalLayoutSliders。

添加两个LabelTTF nodes到verticalLayoutLabels node,改变它们的label text为FX Volume和Music Volume。

同时,添加两个Slider nodes到verticalLayoutSliders node。如图:

如今你有一个以前的问题:sliders和labels位置太近了。设置horizontalLayout何verticalLayoutSliders的Spacing属性为25,verticalLayoutLabels的Spacing为18.

 

Changing the Slider Visuals

默认的sliders很差看。若是你选择一个slider,转到Item Properties,你会注意到CCSlider属性区域,如图:

这些设置容许你改变slider的背景图片和handle。

改变Backgroud image为SpriteSheets/UserInterface/S_bar.png,设置Handle的normal state图片为SpriteSheets/UserInterface/S_ball.png,对于Background和Handle images的Highlighted State为<NULL>。这意味着highlighted state------用户拖动handle的状态------会使用和normal state相同的图片。

注意到slider 背景图片应该有有一个特殊的尺寸,由于它根据须要被拉伸。

在内部,slider和button用CCSprite 9 Slice node做为images;你可使用28x10尺寸做为你本身的背景images,而且设置Scale 为x2.

 

Connecting the Sliders

选择Item Code Connections,输入volumeDidChange:在Selector field中,勾选Continuous check box。同时在Doc root var field中输入_effectsSlider。

对于music slider重复:设置Slector为volumeDidChange:,勾选ontinuous,在Doc root var field中输入_musicSlider.

使得,两个sliders使用同一个selector。注意到selectors后面的冒号-----若是在一个selector后面有一个冒号,这个方法接受一个参数。

“The parameter is always the CCControl object running the selector—in this case, the CCSlider instances. I’ll say more on this shortly.”

摘录来自: Steffen Itterheim. “Learn SpriteBuilder for iOS Game Development”。 iBooks.

 

少了什么呢?固然是root node的自定义类了。选择root node,输入SettingsLayer。

如今在Xcode中,建立SettingsLayer类。

编辑SettingsLayer.h,以下:

#import "CCNode.h"
@class MainMenuButtons;
@interface SettingsLayer : CCNode
@property (weak) MainMenuButtons *mainMenuButtons;
@end

编辑SettingsLayer.m,以下:

#import "SettingsLayer.h"
#import "MainMenuButtons.h"
@implementation SettingsLayer {
    __weak CCSlider *_musicSlider;
    __weak CCSlider *_effectsSlier;
}
- (void)volumeDidChange:(CCSlider*) sender {
    NSLog(@"volume changed,sender:%@",sender);
}

@end

sender参数永远是CCControl*类型,可是由于你能够确定只有sliders会运行这个selector,你能够安全的把参数类型设为CCSlider*。CCSlider是CCControl的子类。两个CCSlider变量用于决定是哪一个slider触发了volumeDidChange:selector。

在你能够试用sliders以前,你须要load而且在MainMenuButtons.m中显示settings layer。准确的说,换掉已经存在的shouldShowSettings方法。以下:

- (void)shouldShowSettings {
    SettingsLayer *settingsLayer = (SettingsLayer*)[CCBReader load:@"UserInterface/SettingsLayer"];
    settingsLayer.mainMenuButtons = self;
    [self.parent addChild:settingsLayer];
    self.visible = NO;
    NSLog(@"SETTINGS");
}

SettingsLayer经过CCBReader载入,使用SettingsLayer.ccb文件的完整路径。而后分配mainMenuButtons的引用,而且,settingsLayer做为child被添加,不是MainMenuButtons类而是它的parent(MainScene实例)。这是由于MainMenuButtons实例自身被设置为invisible-----若是setting layer是MainMenuButtons的child,它也会被隐藏。

你如今能够运行game,敲击Settings按钮,打开settings popover。若是你移动sliders,你会发现Console的log:

每一个sender目标是一个CCSlider的不一样实例。若是你在ItemProperties中给sliders一个名字,名字也会logged。

 

Dismissing the Settings Popover

你如今还不能消去settings popover。你应该经过引入一个通用关闭buton来解决这个问题。

你能够对其余layers使用相同的button。

可是在不少状况下,你不能这样作,由于buttons和其余CCControl nodes向document root(CCB root)的自定义类发送它们的selector。特别是,若是BUtton没有本身的CCB file,你就必须对每一个特殊的任务建立一个分开的button,也许是相同任务,可是不一样使用状况时,也要建立单独的buttons。有一个简单的解决方法。

右键点击UserInterface文件夹,选择New File。命名为CloseButton.ccb,使用Node类型。选择root node,转到Item Code Connections,设置自定义类,命名为CloseButton。

拖动一个Butotn node到stage。在Item Properties中,改变button的normal state和Highlighted State的sprite frame为S_back.png。改变Highlighted State的Background color为a light gray color.在Item Code Connections的Selector中,输入shouldClose。同时清除Title field,以去除button的text。

这就是整个button的CCB file。你应该打开SettingsLayer.ccb,拖动CloseButton.ccb到settingsLayer node中。

移动button到右上角,如图:

在Xcode中,建立另外一个OC类,命名为CloseButton,父类是CCNode,打开CloseButton.m文件,添加以下代码:

#import "CloseButton.h"

@implementation CloseButton
- (void)shouldClose {
    CCNode *aParent = self.parent;
    do {
        if([aParent respondsToSelector:_cmd]) {
            [aParent performSelector:_cmd];
            break;
        }
        aParent = aParent.parent;
    }while (aParent != nil);
}
@end

shouldClose方法在进入do/while循环以前采用button的parent。这检查了是否parent对_cmd selector回应,selector指的是shouldClose selector。_cmd的用法是让这段代码能够简单的用在其余buttons上,对于不一样的selector你不用更新代码。

若是给定的parent响应了同一个selector,跳出;不然,aParent变量被赋值为aParent的parent。

如今,剩下的工做时实现shouldClose方法。打开SettingsLayer.m,以下:

- (void)shouldClose {
    [self removeFromParent];
    [_mainMenuButtons show];
}

打开MainMenuButtons.h,以下:

#import "CCNode.h"

@interface MainMenuButtons : CCNode
- (void)show;
@end

在MainMenuButtons.m中,添加代码:

- (void)show {
    self.visible = YES;
}

 我已经给了关于当SettingsLayer关闭后你能够作什么的建议,除了移除settings layer和显示buttons。若是两个CCB类须要播放一个本身的Timeline动画呢,一个是SettingsLayer out,一个是MainMenuButtons in。

无论它们看起来怎么样,它们应该须要使用本身的animtionManager实例去运行animation,SettingsLayer必须对Callbacks selector做出回应。


Persisting Game States

如今是时候引入GameState类来记录game的变化状态了。

Introducing the GameState Class

GameState类是NSUserDefaults的包装,以免在代码中用相同的string keys注入相同的代码。就像NSUserDefaults,这回一个单例类。一个单例类是一个只能被初始化一次的类。

singletons是一个热门的讨论问题-----一些人甚至颇有意见。Singletons常常被过度使用或者误用,特别是对于新手,由于它们由于有着简单的全局可访问性,使得数据的传输十分简单,还有者诱人的结构,

在你使用singleton以前,确保你没有其余的方案了。相对而言,只有不多的变量和方法须要被全局化。

Tip:对于对象引用,避免它们全在一个singleton类中。任何类型为id的属性或者类指针都是strong类型,已阻止object deallocating。由于singleton自身在app运行期间,正常状况下永远不会deallocate。

在这个特殊情形下,这样作是合理的。建立另外一个OC类,命名为GameState,其父类为NSObject。

建立后,打开GameState.h,添加以下:

#import <Foundation/Foundation.h>

@interface GameState : NSObject
+ (GameState*)sharedGameState;
@property CGFloat musicVolume;
@property CGFloat effectsVolume;
@end

sharedGameState前面有一个+,这使得它成为一个类方法,能够被其余任何类访问。

在GameState.m中添加代码,这会建立一个single实例。

#import "GameState.h"

@implementation GameState
+ (GameState*) sharedGameState {
    static GameState *sharedInstance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{sharedInstance = [[GameState alloc]init];
    });
    return sharedInstance;
}
@end

NOTE:这种方法是ARC认同的。

 

如今,添加musicVolume属性setter和getter方法,以下:

static NSString * KeyForMusicVolume = @"musicVolume";
- (void)setMusicVolume:(CGFloat)musicVolume {
    [[NSUserDefaults standardUserDefaults] setDouble:volume forKey:KeyForMusicVolume];
}
- (CGFloat)musicVolume {
    NSNumber *number = [[NSUserDefaults standardUserDefaults]objectForKey:KeyForMusicVolume];
    return (number ? number.doubleValue : 1.0);
}

NSUserDefaults key被声明为static NSString*变量,这样你就不须要第二次再写这个string了。

属性的setter和getter方法遵循一个命名规则,getter和属性名同样,setter是属性名前加一个set,属性的第一个字母大写。

NSUserDefaults是一个能够保存任何内置数据类型的类,包括:NSData,NSString,NSNumber,NSDate,NSArray,NSDictionary。因此它不会保存你的自定义类。可是你能够单独存储你的类的属性,好比这里的volumes。

standaardUserDefaults是一个singleton accessor,就像sharedGameState.

setDouble:forKey:方法根据给定的key存储一个double类型的值,必须是一个NSString*对象。这个相同的key接下来被用在objectForKey:中,以接受相关联的NSNumber对象。NSUserDefaults永远都返回内置数据类型,能够返回nil,这一版是你第一次启动app的情形。

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

static NSString * KeyForEffectsVolume = @"effectsVolume";
- (void)setEffectsVolume:(CGFloat)volume {
    [[NSUserDefaults standardUserDefaults]setDouble:volume forKey:KeyForEffectsVolume];
}
- (CGFloat)effectsVolume {
    NSNumber *number = [[NSUserDefaults standardUserDefaults]objectForKey:KeyForEffectsVolume];
    return (number ? number.doubleValue:1.0);
}

Persisting the Volume Slider Values

在SettingsLayer.m中添加代码:

#import "SettingsLayer.h"
#import "MainMenuButtons.h"
#import "GameState.h"
@implementation SettingsLayer {
    __weak CCSlider *_musicSlider;
    __weak CCSlider *_effectsSlier;
}
- (void)didLoadFromCCB {
    GameState *gameState = [GameState sharedGameState];
    _musicSlider.sliderValue = gameState.musicVolume;
    _effectsSlier.sliderValue = gameState.effectsVolume;
}
- (void)volumeDidChange:(CCSlider*) sender {
    if (sender == _musicSlider) {
        [GameState sharedGameState].musicVolume = _musicSlider.sliderValue;
    }else if (sender == _effectsSlier) {
        [GameState sharedGameState].effectsVolume = _effectsSlier.sliderValue;
    }
    NSLog(@"volume changed,sender:%@",sender);
}
- (void)shouldClose {
    [[NSUserDefaults standardUserDefaults]synchronize];
    [self removeFromParent];
    [_mainMenuButtons show];
}

@end

在didLoadFromCCB中,volume 值被分配给slider values。由于sliderValue和volumes的值都在0.0到1.0中。首先,GameState volumes会返回1.0,这会把两个volume slider handles置于最右边。不论何时volumeDidChange方法,

未完待续

相关文章
相关标签/搜索