SpriteBuilder 学习笔记六

Chapter 7    Main Scene and Game State

至今,我们完全忽略了主场景,而主场景是放置游戏的main menu的好地方。main menu放置设置菜单的好地方,Settings menu让你可以改变音量等。

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

 

Main Scene Background

Designing the Background Images

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

如果你想要提供你自己的图片,你应该有3个screen-sized图片,来构成menu的背景layers。

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

考虑到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方法,

未完待续

posted on 2015-03-06 13:16  SomeBod_Y  阅读(276)  评论(0编辑  收藏  举报