---恢复内容开始---node
Chapter 5数组
Timelines & Triggersapp
SpriteBuilder的一个主要特性就是能够用关键帧建立Timeline动画。甚至能够经过提供合适的碰撞属性让静态physics body也能产生动画效果。这一章会经过使用CCBAnimationManager类建立动画效果,也会建立一个可复用的trigger node,你能够把这个trigger node放在level中,这个trigger可让你作到当player进入trigger区域时,运行相应的代码----好比播放目标node的动画效果。less
What Are Timelines and Keyframes?oop
1. Timeline Controls:这些控制选项让你能够重置,快速向前,向后,中止,和播放动画效果。最右边的按钮用于在播放中重置播放循环,可是在game中没有影响。测试
2. Timeline List:一个CCB文件能够包含多个timelines。经过下拉菜单,能够添加,移除,复制和重命名timeline,图中选择当前编辑的是“default timeline”。动画
3.Timeline Chain:这个控制项容许你具体设定Timeline当当前timeline播放结束后是否应该被播放。若是被设置为No chained timeline,那么Timeline仅仅会被播放一次。为了在游戏中循环一个Timeline,能够在下拉菜单中设置Timeline为当前正在编辑的。为了循环图中的Timeline,应该把这一项改为Default Timeline。ui
4.Timeline Scale:这个控制规模。spa
5.Timeline Cursor:这个显示当前动画的时间点。debug
6.Keyframes:这些长方形表明关键帧。能够向左右拖动它们,改变它们的位置。两个长方形之间的时关键帧段,能够右击它们改变宽松模式(easing mode)。
下图是能够有动画效果属性的表。注意在全部nodes中不是全部属性均可以有动画效果。是否能够有动画效果首先是看node的类型和node是不是CCB文件的root node。Root node根本不能有动画效果,除非是Sprite CCB文件,你能够至少对颜色,透明度,和Sprite属性进行动画。
Using the Timeline Editor
在Tileless Editor view中,拖动一个doughnut图片到Level.ccb的CCPhysicsNode中。能够把它做为player进入这一level的地点。
在Timeline中选中doughnut图片。如今能够开始添加关键帧,来建立Timeline动画。首先改变Timeline持续时间从默认的10秒到6秒。
你也许会好奇Frames框表示什么。SpriteBuilder播放动画是以每秒30帧的速率。因此若是何时你须要一个Timeline的持续时间小于1秒,或者一秒半的时候,就必须编辑Frames框了。可选的值是0到29.好比,若是你须要持续时间为1.5秒,那么必须在Secs框中输入1,在Frames框中输入15.
确保动画的整个持续时间在Timeline中是可见的。使用Timeline Scale滑块确保6秒的动画在时间轴上都是可见的。
Adding Keyframes
确保选择了正确的node,而且Timeline Cursor也在正确的位置,如今能够添加关键帧了。一个方法是经过菜单Animations->Insert Keyframes,而后选择合适的关键帧类型,好比,Position或者Scale。可是最好使用快捷键完成操做。
在Cursor开始处,按下S。
如今,移动Cursor到3-seconds处,按下S。
在6秒处按下S。
注意到,这两个关键帧如今用红色的水平线链接在一块儿了。
Caution:若是你试图使开启了physics模式的node的Scale属性进行动画效果,那会引起一个异常。问题在于one naturally assumes the physics shape would scale along with the node ,but it does not.
Editing Animation Settings
那么动画效果如何决定哪些值应该被插入呢?到目前为止尚未任何效果。这里的关键是关键帧容许你编辑给定的属性,在给定的时间点。如今,Scale属性被设置成1,1。
确保doughnut仍然被选中,切换到属性选项卡。在3s处的关键帧的Scale属性的X和Y轴上改变值,均为1.3。
Looping Animations...Wheeeee!
为了确保在game中动画的循环,而不只仅是在SpriteBuilder中,必须chain the Timeline to itself.
一个Timeline Chain简单来讲就是告诉动画在另外一个timeline动画播放完后开始播放另外一个动画。因此,把一个动画和自身串起来会致使循环。为了完成循环,把“No chained timeline"改为”Default Timeline"。
Easing Animations with Ease
可使用easing模式使动画平滑。easing影响两个关键帧之间的值如何改变。为了编辑easing模式,右击关键帧段。
图1:
注意到在选择一个easing mode后,粉红色的线段发生了变化。若是你选择了Instant模式,粉红色线条会彻底消失。理论上说,Instant不是一个easing mode。
---恢复内容结束---
Chapter 5
Timelines & Triggers
SpriteBuilder的一个主要特性就是能够用关键帧建立Timeline动画。甚至能够经过提供合适的碰撞属性让静态physics body也能产生动画效果。这一章会经过使用CCBAnimationManager类建立动画效果,也会建立一个可复用的trigger node,你能够把这个trigger node放在level中,这个trigger可让你作到当player进入trigger区域时,运行相应的代码----好比播放目标node的动画效果。
What Are Timelines and Keyframes?
1. Timeline Controls:这些控制选项让你能够重置,快速向前,向后,中止,和播放动画效果。最右边的按钮用于在播放中重置播放循环,可是在game中没有影响。
2. Timeline List:一个CCB文件能够包含多个timelines。经过下拉菜单,能够添加,移除,复制和重命名timeline,图中选择当前编辑的是“default timeline”。
3.Timeline Chain:这个控制项容许你具体设定Timeline当当前timeline播放结束后是否应该被播放。若是被设置为No chained timeline,那么Timeline仅仅会被播放一次。为了在游戏中循环一个Timeline,能够在下拉菜单中设置Timeline为当前正在编辑的。为了循环图中的Timeline,应该把这一项改为Default Timeline。
4.Timeline Scale:这个控制规模。
5.Timeline Cursor:这个显示当前动画的时间点。
6.Keyframes:这些长方形表明关键帧。能够向左右拖动它们,改变它们的位置。两个长方形之间的时关键帧段,能够右击它们改变宽松模式(easing mode)。
下图是能够有动画效果属性的表。注意在全部nodes中不是全部属性均可以有动画效果。是否能够有动画效果首先是看node的类型和node是不是CCB文件的root node。Root node根本不能有动画效果,除非是Sprite CCB文件,你能够至少对颜色,透明度,和Sprite属性进行动画。
Using the Timeline Editor
在Tileless Editor view中,拖动一个doughnut图片到Level.ccb的CCPhysicsNode中。能够把它做为player进入这一level的地点。
在Timeline中选中doughnut图片。如今能够开始添加关键帧,来建立Timeline动画。首先改变Timeline持续时间从默认的10秒到6秒。
你也许会好奇Frames框表示什么。SpriteBuilder播放动画是以每秒30帧的速率。因此若是何时你须要一个Timeline的持续时间小于1秒,或者一秒半的时候,就必须编辑Frames框了。可选的值是0到29.好比,若是你须要持续时间为1.5秒,那么必须在Secs框中输入1,在Frames框中输入15.
确保动画的整个持续时间在Timeline中是可见的。使用Timeline Scale滑块确保6秒的动画在时间轴上都是可见的。
Adding Keyframes
确保选择了正确的node,而且Timeline Cursor也在正确的位置,如今能够添加关键帧了。一个方法是经过菜单Animations->Insert Keyframes,而后选择合适的关键帧类型,好比,Position或者Scale。可是最好使用快捷键完成操做。
在Cursor开始处,按下S。
如今,移动Cursor到3-seconds处,按下S。
在6秒处按下S。
注意到,这两个关键帧如今用红色的水平线链接在一块儿了。
Caution:若是你试图使开启了physics模式的node的Scale属性进行动画效果,那会引起一个异常。问题在于one naturally assumes the physics shape would scale along with the node ,but it does not.
Editing Animation Settings
那么动画效果如何决定哪些值应该被插入呢?到目前为止尚未任何效果。这里的关键是关键帧容许你编辑给定的属性,在给定的时间点。如今,Scale属性被设置成1,1。
确保doughnut仍然被选中,切换到属性选项卡。在3s处的关键帧的Scale属性的X和Y轴上改变值,均为1.3。
Looping Animations...Wheeeee!
为了确保在game中动画的循环,而不只仅是在SpriteBuilder中,必须chain the Timeline to itself.
一个Timeline Chain简单来讲就是告诉动画在另外一个timeline动画播放完后开始播放另外一个动画。因此,把一个动画和自身串起来会致使循环。为了完成循环,把“No chained timeline"改为”Default Timeline"。
Easing Animations with Ease
可使用easing模式使动画平滑。easing影响两个关键帧之间的值如何改变。为了编辑easing模式,右击关键帧段。
图:
注意到在选择一个easing mode后,粉红色的线段发生了变化。若是你选择了Instant模式,粉红色线条会彻底消失。理论上说,Instant不是一个easing mode。 It simply sets the animated value to that of the keyframe.对于其余全部的easing modes,粉红色线段的一端或者两端变得有一些阴影,这取决于你是选的In,Out,仍是In/Out。
In:应用于关键帧端的开始。
Out:应用于关键帧段的结束。
In/Out:应用于In和Out。
图:
Caution:若是两关键帧之间的时间很短,好比只有10帧或更少,你会很难注意到easing效果。
Keyframe Animations for Physics Nodes
关键帧动画只能应用在physics node设置成Static的状况。
Adding the Gear and Saw
在SpriteBuilder的File view中,选择Sprites Sheets文件夹,建立一个New Forder,命名为GameElements。并Make Smart Sprite Sheet.拖动circularsaw1.png和gear1.png到该文件夹中。
如今重复以下的步骤,建立两个新ccb文件,命名为Gear1.ccb和Saw1.ccb。
1.右击Prefabs文件夹,选择New File。
2.分别命名为Gear1.ccb和Saw1.ccb,更改成Node类型。稍后会解释为什么是Node,而不是sprite。
3.选择Tileless Editor View。打开Gear1.ccb,拖动gear1到stage。打开Saw1.ccb,拖动saw1到stage。
4.打开属性选项卡,设置gear/saw sprite的位置为0,0,锚点位置为0.5,0.5。
5.打开属性选项卡,选择Enable physics复选框。改变body type为static。保证Categories和Masks为空。
6.在Collision type中,输入gear,和saw,对应各自的属性框。
7.对于gear sprite:
图:
8.对于saw sprite:改变physics shape为Circle,改变suggested radius从208到190。
CCSprite 的root node只能够有一些视觉效果:sprite frames,opacity,和color。这就是为何我不使用Sprite CCB文件,可是取而代之的是使用Node文件,用一个sprite做为child node。
这样,能够具备彻底的动画效果。
Animation the Gear and Saw
随意建立额外的Gear1.ccb和Saw1.ccb复制品,它们有相同的内容,可是不一样的旋转动画。若是你这样作,你应该为这些额外添加的CCB文件选择合适的名字。好比说,Gear1_180AndBack.ccb。
Tip:注意到如今没有复制命令。可是若是之后会有,最可能出现该命令的地方是右击CCB文件。幸运的是,能够用Finder复制CCB文件。在Finder中,会发现全部文件,在SpriteBuilder Resources文件夹中。能够很简单的经过复制粘贴在Finder中建立ccb文件的复制文件。
不用担忧建立了太多复制文件,被发布的CCB文件很小,若是sprites使用相同的图片,那么几乎不会增长内存使用量。一个sprite实例,包括纹理,使用的内存小于1kb。
选择gear/saw sprite node,改变默认Timeline持续时间为4秒。
选择gear/saw,把Timeline cursor移动到00:00处,按"R",建立一幅关键帧。把Timeline cursor移动到04:00处,按“R",建立一幅关键帧,输入”360“。
最后,Timeline Chain须要从No chained timeline改变到Default Timeline,这样能够保证无限的旋转。
Adding Gears and Saws to the Level
如今应该放置一个或多个gear和saw对象于Level1.ccb中。第一步是拖动一个Node到CCPhysicsNode中,命名为gear和saw。第二步是拖动gear和saw从Tileless Edit view到node上。
Tip:当你在Level1.ccb中,按下Play按钮,会注意到不只level-entry doughnut有动画效果,gear和saw也会播放它们各自的动画效果。这容许你去预先看动画效果,即便一些动画效果是在别人CCB文件中编辑的。
如今,你也许想知道:当编辑一个Timeline动画时,以前你不能编辑动画Node的属性,除非是Timeline Cursor精确指向的关键帧。可是如今你能够播放和暂停动画,而且不论Timeline Cursor指向哪,总能够编辑node的属性。
为了理解,考虑你在编辑特别的Node的关键帧,好比,Gear1.ccb文件。经过添加关键帧,而且编辑关键帧的特殊属性,你能够告诉node在动画期间到底该怎么作。好比位置,旋转,大小等属性,这些属性必须与node的当前状态相关联。而后你放置一个Gear1.ccb和Level1.ccb的实例。这样就建立了一个Sub File Node,做为Gear1.ccb的引用。这个Sub File Node表明了Gear1的特定的实例。Sub File node容许你去指定node的初始位置,旋转和大小。在播放期间,Gear的Timeline根据Sub File Node的位置,旋转和大小播放。
How Not to Autoplay Animations
至今为止,当启动游戏后,动画效果会自动播放。迟早你但愿它不要这样。
假设你但愿在合适的时间,当游戏运行时去播放动画,这是你须要在代码中完成的。为了建立一个一开始不活动的saw,应该建立一个Saw1.ccb的复制。在Finder中建立该复制,重命名为:Saw1_noautoplay.ccb。
Editing Timelines to Uncheck Autoplay
首先,须要阻止动画效果从开始就播放。点击”Default Timeline“,
点击Edit Timelines,
注意到Default Timeline的Autoplay复选框默认已经被勾上。首先,解锁复选框以阻止Timeline动画自动播放。
Playing Animations Programmatically
在Level1.ccb中放置Saw1_noautoplay.ccb,做为gears和saws node的子node。而且把名字改为:sawNoAutoplay。
如今发布和运行APP,能够注意到saw再也不自动播放,可是如何让它在到了特定的time时开始播放呢。
Spritebuilder自带不少使用的类。你已经知道了CCBReader,这是负责载入CCB files的。另外一个使用最多的是CCAnimationManager。它负责存储和播放动画timelines,也就是Cocos2D指的sequences。
Note:Sequences和Timelines是相近的术语,尽管不是彻底相同。Cocos2D uses the term sequences for any set of CCAction classes wrapped in CCActionSequence class that runs the actions it wraps in sequential order.举例来讲,当一个node从A移动到B,而后旋转90度,而后移动到C,这就是sequence。一个SpriteBuilder中的Timeline能够被认为是多个sequences的复合,由于举例来讲,你能够对一个node的透明度和大小进行动画,同时进行移动和旋转动做。总的来讲,一个Timeline是一个或多个sequences同时进行播放。
每一个node实例都有一个animationManager属性,你能够进入CCAnimationManager,而且经过名字播放动画。
快速测试以下,增长代码在loadLevelNamed:方法中:
- (void)loadLevelNamed:(NSString*)levelCCB { _physicsNode = (CCPhysicsNode*)[_levelNode getChildByName:@"physics" recursively:NO]; //_physicsNode.debugDraw = _drawPhysicsShapes; _physicsNode.collisionDelegate = self; _playerNode = [_physicsNode getChildByName:@"player" recursively:NO]; _backgroundNode = [_levelNode getChildByName:@"background" recursively:NO]; NSAssert1(_playerNode, @"_playerNode not found! %@", levelCCB); NSAssert1(_physicsNode, @"_physicsNode not found! %@", levelCCB); NSAssert1(_backgroundNode, @"_backgound not found !%@", levelCCB); CCNode *sawNoAutoplay = [_physicsNode getChildByName:@"sawNoAutoplay" recursively:YES]; [sawNoAutoplay.animationManager runAnimationsForSequenceNamed:@"Default Timeline"]; }
最后两行代码是新加入的代码,第一行经过名字获取node的引用。animation manager开始运行sequences,名字叫Default Timeline。运行APP,会发现非自动播放的saw如今开始有动画效果了。
Note:每一个CCB文件有本身的CCAnimationManager实例,这样特殊的CCB文件的timelines能被存储。全部在CCB文件中被编辑的Timeline动画被存储在CCB文件的root node的CCAnimationManager中。
Animation Playback Completion Callback
CCAnimationManager类容许你在动画的播放结束后和一个循环动画重复的时候获得通知。为了收到这些通知,必须实现CCBAnimationManagerDelegate协议。
代码以下:
#import "CCNode.h" @interface GameScene : CCNode <CCPhysicsCollisionDelegate,CCBAnimationManagerDelegate> @end
- (void)loadLevelNamed:(NSString*)levelCCB { _physicsNode = (CCPhysicsNode*)[_levelNode getChildByName:@"physics" recursively:NO]; //_physicsNode.debugDraw = _drawPhysicsShapes; _physicsNode.collisionDelegate = self; _playerNode = [_physicsNode getChildByName:@"player" recursively:NO]; _backgroundNode = [_levelNode getChildByName:@"background" recursively:NO]; NSAssert1(_playerNode, @"_playerNode not found! %@", levelCCB); NSAssert1(_physicsNode, @"_physicsNode not found! %@", levelCCB); NSAssert1(_backgroundNode, @"_backgound not found !%@", levelCCB); CCNode *sawNoAutoplay = [_physicsNode getChildByName:@"sawNoAutoplay" recursively:YES]; sawNoAutoplay.animationManager.delegate = self;//添加的代码 [sawNoAutoplay.animationManager runAnimationsForSequenceNamed:@"Default Timeline"]; }
而后添加completedAnimationSequenceNamed:callback方法。
- (void)completedAnimationSequenceNamed:(NSString *)name { NSLog(@"completed animation sequences:%@",name); }
每次这个方法运行时,会记录下播放完成的动画的名字。该方法也会在一个循环动画完成而且从新开始时运行。This allows you to wait for a looping animation to play back to complete before stopping or replacing it with another animation,in order to prevent nasty jumps in the animation playback.
或者,你一样能够选择设置一个block,当动画结束时运行。优点在于你不须要实现CCBAnimationManagerDelegate协议,劣势是你要把CCAnimationManager做为输入,这样你必须涉及到lastCompletedSequenceName 属性,以获得结束的Timeline的名字。
Differentiating Between Animations
再次提醒每一个CCB文件有本身的CCAnimationManager实例。在这个例子中,只有Saw1_noautoplay.ccb动画会调用毁掉方法。
一样的,若是你使用self.animationManager.delegate = self,那么这个方法会在每次GameScene.ccb的Timeline动画结束时运行。可是,好比当Player.ccb或者Backround1.ccb结束的时候,不会运行。
注意到,使用相同的类做为多个动画manager实例的代理是可行的。好比,若是你在GameScene中须要获得其余CCB动画事件的通知,应该这么写:
self.animationManager.delegate = self;
_levelNode.animationManager.delegate = self;
_playerNode.animationManager.delegate = self;
_backgroundNode.animationManager.delegate = self;
Triggering Animations on Collision
Adding Trigger Targets to the Level
首先,在loadLevelNamed:方法中移除:
CCNode *sawNoAutoplay = [_physicsNode getChildByName:@"sawNoAutoplay" recursively:YES]; [sawNoAutoplay.animationManager runAnimationsForSequenceNamed:@"Default Timeline"];
接下来,打开Level1.ccb,假设你已经在gears and saws的node中添加了一个Saw1_noautoplay.ccb实例,那么再添加一个,你至少须要两个。
你应该为在Timeline中已经存在的saw实例重命名,而且复制它,排列好位置。
选择每一个Saw1_noautoplay prefab实例,改变它们的名字属性为triggerSawRotation。trigger node会获得一样的名字,以在trigger 区域和tobe-triggered nodes之间创建联系。
Creating a Trigger CCB
什么是一个好的trigger node?Color nodes是最好的选择,可是总的来讲你可使用任何类型的node。trigger区域应该是在编辑的时候可见,可是是透明的。The node color can be used to hightlight different types of triggers with different colors。
你应该建立一个trigger 对象做为一个模板。
在Prefabs中建立一个New File,名为TrigerOnce.ccb,类型为Layer。大小是128x128.
Note:Layer类型很重要由于放在Level中时,它给你一个长方形的区域。你也许注意到saw和gear实例有可旋转的区域。长方形区域在使用时更方便,并且你会常常须要改变node的大小,由于你须要去适应trigger区域的大小和长方形的形状。
拖动一个Color node到TriggerOnce.ccb中。位置设为0,0.长宽设置为128x128。选一个喜欢的颜色,透明度设置为0.3,之后根据须要常常修改颜色和透明度。
勾选Enable Physics,改变类型为static,Collision type为trigger。Categories和Masks目前能够为空。
为color node在Item Code Connections中输入自定义类名:Trigger。这会建立一个Trigger实例,在你每次添加TriggerOnce.ccb到level中时。
由于这个特定的trigger仅仅应该触发一次,因此打开属性区,在底部,点击Edit Custom Properties,加上一个自定义属性:triggerCount,类型为Int,值为1.
再添加一个trigger CCB,命名为:TriggerForever.ccb.
注意到,一样能够建立一个多边形的trigger。可是,一个多边形形状不会在SpriteBuilder中反应,由于color node填充一个长方形区域。若是你但愿可视化显示一个多边形trigger区域,你必须使用sprite node和对应的trigger图片。
另外一方面,你老是可使用多个长方形trigger创建一个更复杂的trigger区域。Trigger碰撞区域基本不会须要如此的精确。因此最好避免使用多边形形状的trigger。
Adding Triggers to the Level
首先拖动一个Node到Level1.ccb的CCPhysicsNode中,命名为:triggers。这是你的trigger nodes容器。
如今拖动一个TriggerOnce,从Tileless Editor View到triggers node中。改变trigger的名字属性为triggerSawRotation。这个名字链接了trigger和你以前添加的saw nodes,它们的名字相同。当player(或者其余nodes)触发了trigger后,目标nodes会收到一个消息。
改变trigger的位置,改变trigger的大小。确保player会和它发生碰撞。
也能够建立一个trigger,若是你但愿建立一个更复杂的trigger形状。多个triggers能够互相重叠而没有其余问题。
Creating the Trigger Class
如今运行这个APP,会收到一个错误,由于CCBReader会发现自定义类Trigger没法被找到。
为了修复这个错误,须要添加一个你在Code Connections中自定义名字相同名字的类。
运行APP,还会收到错误消息,由于忽略了triggerCount属性。使用@property,由于你可能须要在运行时由其余类获取或者改变这个值。代码以下:
#import "CCNode.h" @protocol TriggerDelegate <NSObject> - (void)didTriggerWithNode:(CCNode*)activator; @end @interface Trigger : CCNode @property int triggerCount; - (void)triggerActiveBy:(CCNode*)activator; @end
如今运行APP无错误,可是trigger还不能真正工做。
Programming the Trigger Class
Trigger.m实现起来有些复杂,可是能够很方便的复用。
Initializing Trigger and Target Arrays
以下:
#import "Trigger.h" static NSMutableDictionary *targetArrays; static NSMutableDictionary *triggerArrays; @implementation Trigger { BOOL _repeatsForever; } - (void)didLoadFromCCB { if(targetArrays == nil ) { targetArrays = [NSMutableDictionary dictionary]; triggerArrays = [NSMutableDictionary dictionary]; } [targetArrays removeAllObjects]; [triggerArrays removeAllObjects]; _repeatsForever = (_triggerCount <= 0); } @end
一开始,声明了两个static NSMutableDictionary变量。关键字static意味着这些变量能够被Trigger class的全部实例共享。本质上,它们是全局变量,but because they are declared in the scope of the implementation file they are accessible only to code in the Trigger.m file.
@implementation区域声明了一个_repeatsForever变量,它能够被用于决定特定的trigger will never remove itself.
didLoadFromCCB方法包含对两个全局dictionary的初始化和清理代码。若是targetArrays是nil,它会建立一个NSMutableDictionary实例并声明它。
下一步,两个dictionary都要作removeAllObjects。缘由是:到最后,你确定会要去改变levels,基于这个缘由,两个dictionary被声明为全局变量(static),当你改变levels而且呈现其余关的时候,它们和它们所包含的内容会再内存中保留。
因此,在每一个新的level中至少有一个Trigger实例,这些dictionary须要清理任何保留着的引用。
Note:让每一个trigger removeAllObjects看起来彷佛效率不高,彷佛作一次就够了,但我以为增长额外的代码来检查是否targetArrays和triggerArrays每次建立实例时是否为空是合理的理由。
最后,_repeatsForever被设置为_triggerCount等于或者小于0的结果,这意味着若是_triggerCount被初始化为0或者负数,trigger会重复。
Tip:在OC中,全部static变量和全部ivars都被初始化为0.对于BOOL ivars,0等于NO;对于id和OC类指针变量,0等于nil。
“Furthermore, since automatic reference counting (ARC) is enabled in every SpriteBuilder project, each local variable of type id and Objective-C class pointers are initialized to nil as well. The only variables you have to assign a value before reading from them are local variables that are primitive data types (i.e., BOOL, int, CGFloat, double, NSUInteger and similar) and C structs and C pointers like void*.”
摘录来自: Steffen Itterheim. “Learn SpriteBuilder for iOS Game Development”。 iBooks.
Finding Triggers and Targets
下一步是为每一个trigger匹配targets。Triggers和targets相链接,只要简单的对trigger和target nodes使用相同的名字。不只能够一个trigger对应多个targets,一样能够多个相同名字的triggers激活相同的target(s)。
Finding and Storing Triggers
在Trigger.m的@end上方添加代码:
- (void)onEnter { [super onEnter]; self.parent.visible = NO; NSAssert1(_parent.name.length > 0, @"Trigger node has no name:%@", self); self.name = _parent.name; NSPointerArray *triggers = [triggerArrays objectForKey:_name]; if(triggers == nil) { triggers = [NSPointerArray weakObjectsPointerArray]; [triggerArrays setObject:triggers forKey:_name]; } [triggers addPointer:(void*)self]; if([targetArrays objectForKey:_name] == nil) { NSPointerArray *targets = [NSPointerArray weakObjectsPointerArray]; [targetArrays setObject:targets forKey:_name]; [self addTargetsTo:targets searchInNode:self.scene]; NSAssert1(targets.count > 0, @"no target found for trigger named %@", _name); } }
注意:你不能用didLoadFromCCB方法去找到全部的triggers和targets;取而代之的是必须使用onEnter。由于didLoadFromCCB在每一个单独的node加载后就马上执行,若是你在didLoadFromCCB中运行这段代码,极可能致使target或者trigger nodes尚未被加载。最快能够开始搜索全部target或者trigger node是当onEnter运行时。onEnter方法被发送给每一个node 实例,在它做为一个child被添加到其余node以后。
Caution:注意onEnter须要调用[super onEnter]。若是没有调用[super onEnter],编译器会警告你,另外,scheduling和input events不会工做或者不会正确工做。
上述代码的第一项工做是设置trigger node的visible status为NO。事实上,这是在设置trigger的parent node。这是由于视觉上表明trigger区域的color node是Trigger自定义类。可是color node是TriggerOnce.ccb的子node。我喜欢隐藏root node,以便我想去使用多个color nodes设计更复杂的triggers。
下一步,node接管了parent的名字。这是由于在Level1.ccb中,Name属性被声明为Sub File node实例,它是TriggerOnce.ccb的引用。这结束于声明这个名字给Trigger.ccb root node,也就是color node的parent。以_parent.name声明self.name是为了方便,由于这容许你使用在CCNode类中声明的_name变量。
Tip:由于忘记给trigger一个name是常见的错误,因此决定在这里加入一个assertion去警告你若是_parent.name.length是0,即意味着parent的name也是nil活着一个空string。这个assertion会在两种状况下报错:若是_parent.name 是nil,整个test会一样返回nil(也就是0)由于发送给nil对象的message默认返回0.
你也许会疑问为何没有直接分配_name变量或者为何经过_parent.name获得name而不是self.parent.name.
根据经验,你应该倾向于使用一个available的变量,when merely reading its value。这使得代码更简洁,效率更高。可是,当分配一个值给属性时,最佳实践是经过self.name分配属性而不是直接分配给_name ivar。这里的根本缘由是self.name = @".."内部运行属性setter方法[self setName:@".."]。绕过属性setter会致使不少问题。若是你对于直接分配给一个ivar可能的影响有疑问,建议老是分配给属性或者运行property setter方法。
下一个代码段:
NSPointerArray *triggers = [triggerArrays objectForKey:_name]; if(triggers == nil) { triggers = [NSPointerArray weakObjectsPointerArray]; [triggerArrays setObject:triggers forKey:_name]; } [triggers addPointer:(void*)self];
第一行,一个NSPointerArray经过使用trigger的_name查找object获得。起初,没有任何object,致使triggers为nil。若是这样的话,if语句块会建立一个NSPointerArray而且分配给triggers,同时,使用trigger的_name做为key,在triggerArrays dictionary中存储。
定义NSPointerArray的部分颇有趣。它听起来像一个常规的NSArray或者NSMutableArray,可是它实际上不是它们的子类。它确实表现的很像NSMutableArray。它的特色在于存储指针引用而且容许你存储0值weak引用,这意味着triggers数组不包含trigger nodes added to it。
考虑到trigger和targe nodes有可能随时被移除出node hierarchy,不论它们有没有被激活。
“Trigger nodes should not be kept from deallocating simply because you maintain a list of triggers in a global array. Hence, storing them weakly (not retaining) in an NSPointerArray allows triggers that are removed from the node hierarchy to deallocate.”
摘录来自: Steffen Itterheim. “Learn SpriteBuilder for iOS Game Development”。 iBooks.
随着triggers 数组创建完毕,剩下的工做就是在addPointer中传递self,可是必须加上(void*),由于NSPointerArray存储普通的void*指针。
Finding and Storing Targets
if([targetArrays objectForKey:_name] == nil) { NSPointerArray *targets = [NSPointerArray weakObjectsPointerArray]; [targetArrays setObject:targets forKey:_name]; [self addTargetsTo:targets searchInNode:self.scene]; NSAssert1(targets.count > 0, @"no target found for trigger named %@", _name); }
// Searching for targets
注意到targets仅仅在给定_name却没有在targetArrays dictionary中找到对应项时被搜索。当你有多个相同名字的triggers时,这能够阻止屡次查到相同的targets。只有第一个trigger须要查找targets,由于其余的triggers会指向相同的targets。
若是在targetArrays中没有给定_name的array,如上代码会建立一个NSPointerArray,用于存储指向targets的weak指针。相同的array使用trigger _name做为它的key。下一个method作了一个递归查找,从self.scene node开始。它把全部相同名字的target nodes加入targets array。
- (void)addTargetsTo:(NSPointerArray*)targets searchInNode:(CCNode*)node { for(CCNode* child in node.children) { if([child.name isEqualToString:_name]) { if([child conformsToProtocol:@protocol(TriggerDelegate)]) { [targets addPointer:(void*)child]; } } [self addTargetsTo:targets searchInNode:child]; } }
addTargets:searchInNode:方法递归运行,以找到所给定的名字的全部nodes。在这里,不能使用getChildByName:方法,由于这会返回第一个child node,可是你须要全部相同名字的nodes的列表。
若是一个target node有正确的名字,而且服从TriggerDelegate协议,那么一个target node的引用就被添加到targets array中。
最后一行递归查找。这确保了在scene中得每一个node都被处理过了,而且不会有任何遗漏。
着一样意味着,当前版本的trigger系统没法识别加载GameScene.ccb以后添加的nodes。
你能够添加一个+(void) addTarget:(CCNode*)target方法。你应该确保target node服从TriggerDelegate协议。要添加一个target node,你应该在其余任何类中调用[Trigger addTarget:aTargetNode]。
Forwarding Trigger Events to Targets
剩下的工做是当trigger被激活后通知target nodes。triggerActivatedBy:方法:
- (void)triggerActiveBy:(CCNode *)activator { NSPointerArray *targets = [targetArrays objectForKey:_name]; for (id target in targets) { [target didTriggerWithNode:activator]; } if(_repeatsForever == NO) { NSPointerArray *triggers = [triggerArrays objectForKey:_name]; [self updateTriggers:triggers]; [self cleanupTriggers:triggers]; } }
基于trigger的_name,在targetArrays已经包含了targets的名单。全部的targets都被列举,向每个target发送di'dTriggerWithNode消息,这容许每一个target在被triggered时运行custom的代码。
在循环中使用id关键字声明target变量让你能够发送didTriggerWithNode:消息,编译器不会发出selector没有声明的警告。
若是由于某些缘由,你须要用CCNode*声明target,你必须包含TriggerDelegate协议。能够选择的另外一种循环为:
for(CCNode<TriggerDelegate>*target in targets)
同时,注意:各自的target变量有可能为nil,由于targets是一个存储弱指针的array。幸运的是,你不须要作额外的nil检查,由于发送消息给nil objects在OC中是合法的。
Caution:As with casting,给变量声明添加一个协议不能自动让object继承协议的selectors。因此仍然会引发一个未声明的selector运行错误。必须实现didTriggerWithNode:方法。
Cleaning Up Triggers and Targets
除非trigger不停地重复,即它的初始triggerCount被设置为1或者更多,那么,NSPointerArray包含全部相同名字的triggers须要更新或者进行可能的清除。下面两个方法起到这样的做用。
- (void)updateTriggers:(NSPointerArray*)triggers { for(Trigger *trigger in triggers) { trigger.triggerCount -- ; } } - (void)cleanupTriggers:(NSPointerArray*)triggers { if(_triggerCount <=0) { for(Trigger *trigger in triggers) { [trigger.parent removeFromParent]; } [targetArrays removeObjectForKey:_name]; [triggerArrays removeObjectForKey:_name]; } }
updateTriggers:方法减小triggerCount属性。它假设全部相同名字的trigger开始都有相同的triggerCount;不然,一些triggers可能比其余triggers更容易触发,这通常不是你指望的。特别是,它禁止你结合TriggerOnce.ccb和TriggerForever.ccb去触发相同的targets。若是这是你想要的,你必须更新cleanupTriggers:,以便它仅仅移除那些triggerCount已经减到0的triggers,或者是没有被设置为永远重复的triggers。
cleanUpTriggers方法检测刚被减小的trigger的_triggerCount。若是是0或者更少,就能够认为全部的triggers已经完成了各自的工做,能够被移除。就像以前说的,每一个trigger都是CCNode的child,TriggerOnce.ccb的root node,因此最好是经过发送removeFromParent消息以移除trigger的parent。移除parent会同时移除任何child nodes,包括trigger node自身。
考虑到这些triggers已经作完了工做,那么target或者triggers的NSPointerArray都再也不须要。因此,它们被移除出targetArrays和triggerArrays。
Informing Triggers About Collisions
如今,剩下的工做是发送triggerActivatedBy:消息给碰撞中的Trigger实例,而且实现一个服从TriggerDelegate协议的类,以便当didTriggerWithNode:selector运行时,运行自定义代码。
如今是时候让在TriggerOnce.ccb中得color node的Collision类型起做用了。你已经设置了"trigger"类型。可是,你仍然须要在GameScene.m中添加碰撞方法,该方法第三个参数是trigger。
- (BOOL)ccPhysicsCollisionBegin:(CCPhysicsCollisionPair *)pair player:(CCNode *)player trigger:(Trigger *)trigger { [trigger triggerActiveBy:player];
// return No to allow the bodies to pass through each other return NO; }
注意到:第三个参数是一个指向Trigger实例的指针,而不是一个CCNode实例。你可使用Trigger*参数,代替CCNode*参数,可是必须保证trigger参数永远是Trigger类。若是不能,那么会致使“unrecognized selector sent to instance"错误。
同时你必须在GameScene.m中import Trigger.h。
Triggering Target Nodes
每一个能够被trigger激活的target node须要有以下条件:
1.在SpriteBuilder中分配给node一个自定义类。
2.自定义类的头文件须要#import "Trigger.h"
3.自定义类的@interface须要采用TriggerDelegate协议。
4.自定义类的@implementation须要有一个-(void)didTriggerWithNode:(CCNode*)activator方法。
由于在被一个trigger激活后播放一段动画是很常见的功能,因此应该写一个通用类PlayTimelineWhenTriggered。可是在你写这个类以前,打开Saw1_noautoplay.ccb,选择root node,必定不要选saw sprite。切换到Item Code Conneciton,在自定义类中输入PlayTimelineWhenTriggered。
Caution:PlayTimelineWhenTriggered继承自CCNode,这就是为何做为继承自CCSprite的sprite node的自定义类时无效。sprites nodes的自定义类须要继承自CCSprite class。一样,button的自定义类须要继承自CCButton。
如今,你须要转到XCode,添加PlayTimelineWhenTriggered类。
右键点击Source group,选择New File。输入PlayTimelineWhenTriggered,而且让它做为CCNode的子类。
#import "CCNode.h" #import "Trigger.h" @interface PlayTimelineWhenTriggered : CCNode <TriggerDelegate> @property NSString* timelineName; @end
采用TriggerDelegate协议是必要的,这可使类的实例经过Trigger class做为一个target使用。timelineName属性是为了给这个类提供更多的弹性。
若是timelineName是nil或者空string,class会在被激活时播放Default Timeline;不然,会播放给定名字的Timeline。在任何你把PlayTimelineWhenTriggered设置为自定义类的node上,你能够经过添加自定义属性String类型的timelineName指定被播放的动画。
在.m中添加代码:
#import "PlayTimelineWhenTriggered.h" @implementation PlayTimelineWhenTriggered - (void)didLoadFromCCB { if(_timelineName.length == 0) { _timelineName = @"Default Timeline"; } } - (void)didTriggerWithNode:(CCNode *)activator { [self.animationManager runAnimationsForSequenceNamed:_timelineName]; } @end
didLoadFromCCB方法用于在_timelineName初始化为空string或者nil时,分配@"Default Timeline"字符串给_timelineName.
_timelineName变量被编译器自动建立,由于@property的缘故。didTriggerWithNode方法播放特定名字的动画效果。
你极可能须要建立不少服从TriggerDelegate协议的类,以实现当triggers被激活时作出不一样的行为。可是对于每一个这样的类,你应该尽力去使用自定义属性以让你或者你的团队改变类的行为。
若是你运行APP,当player进入trigger区域时,激活Saw1_noautoplay.ccb,会发生崩溃。
Avoiding the "Physics Space Locked" Error
之因此崩溃是由于Chipmunk错误:你不能在空间被锁住时手动从新索引对象(
you cannot manually reindex objects while the space is locked)必须等待,直到当前队列或步骤完成。
报错状况:!space->locked
这个错误是大部分物理引擎表明性的错误:在碰撞事件中,你不能修改物理世界肯定的方面(certain aspects of the physics world),(在Chipmunk中被称为space)。在这个例子中,播放动画会改变node的旋转状态。Chipmunk不喜欢这样,由于物理世界(space)被锁上了,意味着其中的bodies正在被处理,而且须要一个连续的状态,只有物理引擎自身能够对它作出改变。
考虑到播放动画行为来自于:一个Trigger实例发送了didTriggerWithNode:消息,这in turn有它的triggerActivatedBy:方法,经过GameScene.m的碰撞回调方法:ccPhysicsCollisionBegin:player:trigger。
幸运的是,有一个快速和简单的解决方法。
在PlayTimelineWhenTriggered.m中更新didTriggerWithNode:方法,以下:
- (void)didTriggerWithNode:(CCNode *)activator { //[self.animationManager runAnimationsForSequenceNamed:_timelineName]; [self scheduleBlock:^(CCTimer *timer) { [self.animationManager runAnimationsForSequenceNamed:_timelineName];} delay:0.0]; }
安排一个0延迟的block推迟执行block的内容,直到下一time,Cocos2D安排运行blocks。最近的话,这会在下一帧运行。
Tip:若是你发现你常常须要使用scheduleBlock:技巧去阻止space locked错误,你应该考虑在Trigger.m中更新triggerActivatedBy:方法,这样的话,对didTriggerWithNode的调用就已经被推迟了一个scheduled block。若是你这样作了,你就再也不须要在TriggerDelegate类中schedule blocks,而是全部的didTriggerWithNode:消息都会被推迟。